Implementing Basic Combat Actions In RPG
Embark on the journey of implementing basic combat actions in your RPG! This guide, focusing on Phase 2 of development, dives deep into enabling melee and ranged attacks, target selection, and visualizing combat results. If you're looking to add depth to your game's combat system, you've come to the right place. Let's break down the process step-by-step, ensuring a smooth and engaging combat experience for your players.
Goal: Enable Basic Combat
The primary goal of this phase is to enable basic melee and ranged attacks, complete with target selection and clear result visualization. This means players will be able to choose an enemy, initiate an attack, and see the outcome—hit or miss, damage dealt, and any critical effects.
Context: Building the Core Combat Loop
Currently, the attack prototype exists within the rpg-api, but it lacks a React hook and user interface (UI) implementation. This phase bridges that gap by creating the core combat loop: target an enemy, execute an attack, and display the results. This is a crucial step in bringing your game's combat system to life.
Prerequisites: Setting the Stage for Combat
Before diving into the tasks, ensure that the following prerequisites are met:
- Phase 1 (Movement Polish) Completed: Smooth player movement and interaction with the game world are essential foundations for combat.
- FeedbackPanel Available: A UI element for displaying attack results (hits, misses, damage) is necessary. This panel provides immediate feedback to the player, making combat more engaging.
- Movement Validation Working: The game should accurately determine valid movement ranges and prevent players from moving through obstacles. This is crucial for strategic positioning during combat.
Tasks: Step-by-Step Implementation
Let's break down the implementation into manageable tasks.
Task 2.1: Create Attack Hook (Estimated Time: 45-60 minutes)
This task involves implementing a React hook for the attack endpoint, following the existing pattern in your codebase. This hook will handle the communication between the UI and the game's backend when an attack is initiated.
Files to Create/Modify:
src/api/encounterHooks.ts(adduseAttack)
Implementation:
export interface UseAttackOptions {
encounterId: string;
attackerId: string;
targetId: string;
weaponId?: string;
}
export function useAttack() {
const [state, setState] = useState<AsyncState<AttackResponse>>({
data: null,
loading: false,
error: null,
});
const attack = useCallback(async ({
encounterId,
attackerId,
targetId,
weaponId
}: UseAttackOptions) => {
setState({ data: null, loading: true, error: null });
try {
const request = create(AttackRequestSchema, {
encounterId,
attackerId,
targetId,
weaponId,
});
const response = await encounterService.attack(request);
setState({ data: response, loading: false, error: null });
return response;
} catch (err) {
const error = err instanceof Error ? err : new Error('Attack failed');
setState({ data: null, loading: false, error });
throw error;
}
}, []);
return { attack, loading, error, data };
}
Testing:
- Add console logging to verify request/response. This helps ensure that the hook is correctly sending and receiving data.
- Test with a valid attack (should succeed). This confirms the basic functionality of the hook.
- Test with invalid IDs (should error gracefully). This ensures that the hook handles errors appropriately, providing a stable experience.
Task 2.2: Targeting Mode & State Management (Estimated Time: 60-90 minutes)
This task focuses on adding a targeting mode that allows players to select enemies for attacks. This involves managing the game's state to track whether the player is in targeting mode and which enemy is currently selected.
Files to Modify:
src/components/EncounterDemo.tsxsrc/components/HexGrid.tsxsrc/components/encounter/BattleMapPanel.tsx
State Changes:
// In EncounterDemo.tsx
// Replace: const [movementMode, setMovementMode] = useState(false);
// With:
type ActionMode = 'IDLE' | 'MOVING' | 'TARGETING';
const [actionMode, setActionMode] = useState<ActionMode>('IDLE');
const [targetEntityId, setTargetEntityId] = useState<string | null>(null);
Targeting Logic:
- Enter Targeting Mode:
- When the Attack button is clicked:
setActionMode('TARGETING') - Clear any previous target:
setTargetEntityId(null)
- When the Attack button is clicked:
- Highlight Valid Targets (HexGrid):
// In HexGrid.tsx
const validTargets = useMemo(() => {
if (actionMode !== 'TARGETING' || !selectedEntityId) return new Set();
const selectedEntity = room.entities[selectedEntityId];
if (!selectedEntity?.position) return new Set();
// For Phase 2: Simple range check (5 feet for melee, weapon range for ranged)
// TODO Phase 5: Consider weapon type, line of sight, etc.
const maxRange = 1; // 1 hex = 5 feet (melee only for now)
const targets = new Set<string>();
Object.entries(room.entities).forEach(([entityId, entity]) => {
if (entityId === selectedEntityId) return; // Can't target self
if (entity.entityType === 'PLAYER') return; // Can't target allies (for now)
if (!entity.position) return;
const distance = hexDistance(selectedEntity.position, entity.position);
if (distance <= maxRange) {
targets.add(entityId);
}
});
return targets;
}, [actionMode, selectedEntityId, room]);
- Visual Highlighting:
- Valid targets: Red border or highlight
- Hovered target: Brighter red + pointer cursor
- Out-of-range enemies: Grey/dimmed
- Target Selection:
// In EncounterDemo.tsx handleEntityClick
const handleEntityClick = (entityId: string) => {
if (actionMode === 'TARGETING') {
setTargetEntityId(entityId);
// Show confirmation or immediately attack
}
};
Task 2.3: Attack Execution & Results (Estimated Time: 60-90 minutes)
This task focuses on executing the attack and displaying the results to the user. This involves calling the attack hook, processing the response, and providing feedback to the player.
Files to Modify:
src/components/EncounterDemo.tsxsrc/components/combat-v2/panels/ActionPanel.tsx- Optionally create:
src/components/combat-v2/panels/TargetingPanel.tsx
Attack Flow:
// In EncounterDemo.tsx
const { attack, loading: attackLoading } = useAttack();
const handleAttackExecute = async () => {
if (!encounterId || !selectedEntityId || !targetEntityId) return;
try {
const response = await attack({
encounterId,
attackerId: selectedEntityId,
targetId: targetEntityId,
weaponId: undefined, // Let server choose default weapon
});
// Show result
if (response.hit) {
showFeedback({
type: 'success',
message: `Hit for ${response.damage} damage!`,
details: response.critical ? 'Critical Hit!' : undefined,
});
} else {
showFeedback({
type: 'info',
message: 'Attack missed!',
});
}
// Update state
if (response.updatedCombatState) {
setCombatState(response.updatedCombatState);
}
if (response.updatedRoom) {
setRoom(response.updatedRoom);
}
// Clear targeting mode
setActionMode('IDLE');
setTargetEntityId(null);
} catch (error) {
showFeedback({
type: 'error',
message: 'Attack failed',
details: error instanceof Error ? error.message : undefined,
});
}
};
Result Display:
Option A: Use existing FeedbackPanel (simpler)
- Success: "Hit for 12 damage!" (green)
- Miss: "Attack missed!" (yellow)
- Critical: "Critical Hit! 24 damage!" (gold)
Option B: Create dedicated result panel (optional)
- Show attack roll breakdown
- Show AC comparison
- Animate damage number
- Play sound effect
UI Updates:
- ActionPanel Attack Button:
<button
onClick={() => setActionMode('TARGETING')}
disabled={!canAttack || actionMode === 'TARGETING'}
className={actionMode === 'TARGETING' ? 'active' : ''}
>
{actionMode === 'TARGETING' ? 'Select Target' : 'Attack'}
</button>
- Cancel Targeting:
Add escape key or cancel button:
useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape' && actionMode === 'TARGETING') {
setActionMode('IDLE');
setTargetEntityId(null);
}
};
window.addEventListener('keydown', handleEscape);
return () => window.removeEventListener('keydown', handleEscape);
}, [actionMode]);
- Mark Action as Used:
After a successful attack, the action should be marked as used in TurnState. The server should handle this, but verify the response.
Task 2.4: HP Updates & Visual Feedback (Estimated Time: 30-45 minutes)
This task focuses on showing HP changes on damaged entities and providing visual feedback to the player. This involves updating the UI to reflect the entity's current health and animating the damage dealt.
Files to Modify:
src/components/HexGrid.tsx
Implementation:
- HP Bar Display:
// In HexGrid entity rendering
const showHpBar = entity.entityType !== 'PLAYER' || entity.currentHp < entity.maxHp;
{showHpBar && entity.currentHp !== undefined && entity.maxHp !== undefined && (
<div className="hp-bar">
<div
className="hp-bar-fill"
style={{
width: `${(entity.currentHp / entity.maxHp) * 100}%`,
backgroundColor: getHpColor(entity.currentHp / entity.maxHp)
}}
/>
</div>
)}
- Damage Animation:
// Optional: Animate HP bar decrease
// CSS transition on hp-bar-fill width
// Optional: Floating damage number
<div className="damage-number">-{damageAmount}</div>
- HP Color Coding:
function getHpColor(percentage: number): string {
if (percentage > 0.5) return '#4ade80'; // Green
if (percentage > 0.25) return '#facc15'; // Yellow
return '#f87171'; // Red
}
Acceptance Criteria: Ensuring Quality
To ensure that the implementation is successful, the following acceptance criteria should be met:
- [ ]
useAttackhook implemented following existing pattern - [ ] Hook properly handles success and error cases
- [ ]
ActionModeenum replaces booleanmovementMode - [ ] Attack button toggles targeting mode
- [ ] Targeting mode highlights valid enemies in red
- [ ] Can click enemy to select as target
- [ ] Attack executes when target selected
- [ ] Hit/miss result displayed in FeedbackPanel
- [ ] Damage amount shown on successful hit
- [ ] Critical hits indicated visually
- [ ] HP bars display on entities
- [ ] HP bars update after damage
- [ ] Action marked as used after attack
- [ ] Can cancel targeting with the Escape key
- [ ] Attack button disabled when no action is available
- [ ] No attacks possible on non-enemy entities
- [ ] CI checks pass (
npm run ci-check)
Testing Checklist: Rigorous Validation
Testing is crucial to ensure the combat system works as expected. Here's a detailed checklist:
Manual Tests:
- [ ] Click Attack button → enters targeting mode
- [ ] Valid targets highlighted in red
- [ ] Invalid targets (allies, out of range) not highlighted
- [ ] Click valid target → attack executes
- [ ] Hit result shows damage in feedback panel
- [ ] Miss result shows