aether-shards/specs/Combat_Skill_Usage.spec.md
Matthew Mone 56aa6d79df Add Combat Skill Usage and Targeting System Specifications
Introduce detailed specifications for combat skill usage, including interaction flow, state machine updates, and the skill targeting system. Implement the SkillTargetingSystem to handle targeting validation and area of effect calculations. Enhance the CombatHUD specification to define the UI overlay for combat phases. Integrate these systems into the GameLoop for improved combat mechanics and user experience.
2025-12-23 21:01:54 -08:00

4.9 KiB

Combat Skill Usage Specification

This document defines the workflow for selecting, targeting, and executing Active Skills during the Combat Phase.

1. The Interaction Flow

The process follows a strict 3-step sequence:

  1. Selection (UI): Player clicks a skill button in the HUD.
  2. Targeting (Grid): Game enters TARGETING_MODE. Player moves cursor to select a target. Valid targets are highlighted.
  3. Execution (Engine): Player confirms selection. Costs are paid, and effects are applied.

2. State Machine Updates

We need to expand the CombatState in GameLoop to handle targeting.

State Description Input Behavior
IDLE / SELECTING_MOVE Standard state. Cursor highlights movement range. Click Unit = Select. Click Empty = Move.
TARGETING_SKILL Player has selected a skill. Cursor highlights Skill Range. Hover: Update AoE Reticle. Click: Execute Skill. Cancel (B/Esc): Return to IDLE.
EXECUTING_SKILL Animation playing. Input locked. None.

3. The Skill Targeting System

We need a helper system (src/systems/SkillTargetingSystem.js) to handle the complex math of "Can I hit this?" without cluttering the GameLoop.

Core Logic: isValidTarget(source, targetTile, skillDef)

  1. Range Check: Manhattan distance between Source and Target <= skill.range.
  2. Line of Sight (LOS): Raycast from Source head height to Target center. Must not hit isSolid voxels (unless skill has ignore_cover).
  3. Content Check:
    • If target_type is ENEMY: Tile must contain a unit AND unit.team != source.team.
    • If target_type is ALLY: Tile must contain a unit AND unit.team == source.team.
    • If target_type is EMPTY: Tile must be empty.

Visual Logic: getAffectedTiles(targetTile, skillDef)

Calculates the Area of Effect (AoE) to highlight in Red.

  • SINGLE: Just the target tile.
  • CIRCLE (Radius R): All tiles within distance R of target.
  • LINE (Length L): Raycast L tiles in the cardinal direction from Source to Target.
  • CONE: A triangle pattern originating from Source.

4. Integration Steps (How to Code It)

Step 1: Create SkillTargetingSystem

This class encapsulates the math.

class SkillTargetingSystem {
    constructor(grid, unitManager) { ... }

    /** Returns { valid: boolean, reason: string } */
    validateTarget(sourceUnit, targetPos, skillId) { ... }

    /** Returns array of {x,y,z} for highlighting */
    getAoETiles(sourcePos, cursorPos, skillId) { ... }
}

Step 2: Update GameLoop State

Add activeSkillId to track which skill is pending.

// In GameLoop.js

// 1. Handle UI Event
onSkillClicked(skillId) {
    // Validate unit has AP
    if (this.unit.currentAP < getSkillCost(skillId)) return;

    this.combatState = 'TARGETING_SKILL';
    this.activeSkillId = skillId;

    // VISUALS: Clear Movement Blue Grid -> Show Attack Red Grid (Range)
    const skill = this.unit.skills.get(skillId);
    this.voxelManager.highlightRange(this.unit.pos, skill.range, 'RED_OUTLINE');
}

// 2. Handle Cursor Hover (InputManager event)
onCursorHover(pos) {
    if (this.combatState === 'TARGETING_SKILL') {
        const aoeTiles = this.targetingSystem.getAoETiles(this.unit.pos, pos, this.activeSkillId);
        this.voxelManager.showReticle(aoeTiles); // Solid Red Highlight
    }
}

Step 3: Execution Logic

When the player confirms the click.

// In GameLoop.js -> triggerSelection()

if (this.combatState === 'TARGETING_SKILL') {
    const valid = this.targetingSystem.validateTarget(this.unit, cursor, this.activeSkillId);

    if (valid) {
        this.executeSkill(this.activeSkillId, cursor);
    } else {
        // Audio: Error Buzz
        console.log("Invalid Target");
    }
}

executeSkill(skillId, targetPos) {
    this.combatState = 'EXECUTING_SKILL';

    // 1. Deduct Costs (AP, Cooldown) via SkillManager
    this.unit.skillManager.payCosts(skillId);

    // 2. Get Targets (Units in AoE)
    const targets = this.targetingSystem.getUnitsInAoE(targetPos, skillId);

    // 3. Process Effects (Damage, Status) via EffectProcessor
    const skillDef = this.registry.get(skillId);
    skillDef.effects.forEach(eff => {
        targets.forEach(t => this.effectProcessor.process(eff, this.unit, t));
    });

    // 4. Cleanup
    this.combatState = 'IDLE';
    this.activeSkillId = null;
    this.voxelManager.clearHighlights();
}