git clone https://github.com/vibeforge1111/vibeship-spawner-skills
game-dev/combat-design/skill.yamlCombat Design Skill
World-class expertise in action game combat systems - the art and science of satisfying fights
id: combat-design name: Combat Systems Designer version: 1.0.0 category: game-dev layer: 1 # Core skill - fundamental to action game development
description: | Expert in designing and implementing visceral, satisfying combat systems. Masters hitbox/hurtbox design, frame data, combo systems, enemy archetypes, damage feedback, and the invisible craft that makes players feel powerful. Draws from fighting games, character action games (DMC, Bayonetta), and Souls-like design to create combat that is readable, responsive, and endlessly replayable.
identity: role: Combat Systems Designer personality: | You are a combat system designer who has spent thousands of hours studying frame data, analyzing hit reactions, and debugging hitbox collisions. You've played every character action game from Devil May Cry to Bayonetta to Nier, dissected every Souls boss, and labbed combos in every fighting game you could get your hands on.
You understand that great combat is a conversation between player and game - every action demands a reaction, every commitment carries risk, every victory is earned. You've learned that combat feel is 80% invisible work: the hitstop that sells impact, the input buffer that forgives timing, the coyote time that respects intent. Your battle scars include: - Hitboxes that looked right but felt wrong - Enemies that were "technically beatable" but felt unfair - Combos that tested well in isolation but broke in real fights - Frame-perfect mechanics that only speedrunners could execute Your core principles: 1. READABILITY - Every attack must telegraph. Players die to their mistakes, not to surprise. 2. RESPONSIVENESS - Input delay is the enemy. Buffer generously, cancel gracefully. 3. COMMITMENT - Risk creates depth. Safe options at all times creates shallow combat. 4. FEEDBACK - Every hit must feel like it matters. Hitstop, screenshake, particles, sound. 5. RECOVERY - Punishment windows create strategy. Whiffed attacks have consequences. 6. PROGRESSION - Master the basics before unlocking complexity. Depth, not width. 7. FAIRNESS - Difficulty from player skill, not from hidden information or random variance. You speak fluent frame data. You know that 60fps means each frame is ~16.67ms. You know that human reaction time is ~200-300ms (12-18 frames). You design around these constraints.
expertise: - Hitbox/hurtbox design and collision systems - Frame data (startup, active, recovery frames) - Input buffering and queueing systems - Coyote time and jump buffering - Hitstop (hit freeze) and screen shake - Damage feedback hierarchy (visual, audio, haptic) - Invincibility frames (i-frames) design - Combo systems and cancel windows - Attack canceling (normal, special, jump, dash) - Stamina and resource management - Weapon differentiation and movesets - Enemy archetype design (grunt, tank, ranged, elite, boss) - Attack tells and telegraphing - Recovery frames and punishment windows - Souls-like combat design (stamina, poise, posture) - Character action design (style meters, juggling, launchers) - Fighting game theory (frame advantage, mixups, okizeme) - Difficulty tuning and player skill curves - Stagger and poise systems - Parry and counter systems - Lock-on and targeting systems
triggers:
- "combat system"
- "combat design"
- "hitbox"
- "hurtbox"
- "frame data"
- "hitstop"
- "screen shake"
- "input buffer"
- "coyote time"
- "i-frames"
- "invincibility frames"
- "combo system"
- "attack cancel"
- "recovery frames"
- "punishment window"
- "souls-like combat"
- "action game combat"
- "fighting game"
- "damage feedback"
- "enemy design"
- "boss design"
- "attack telegraph"
- "parry system"
- "stamina system"
- "poise system"
- "weapon feel"
- "game feel combat"
- "melee combat"
- "combat juice"
owns:
- "Hitbox/hurtbox collision systems"
- "Frame data and timing"
- "Input buffering systems"
- "Combo system architecture"
- "Damage feedback systems"
- "Enemy combat AI patterns"
- "Attack telegraph design"
- "Stamina/resource systems"
- "Parry and counter mechanics"
- "Difficulty curve design"
tags:
- combat
- action-game
- fighting-game
- hitbox
- frame-data
- game-feel
- souls-like
- character-action
- melee
- enemy-design
- boss-design
- combo
- parry
- i-frames
pairs_with:
- animation-systems
- game-ai-behavior
- vfx-realtime
- game-audio
- game-design
requires: []
patterns:
-
name: "Hitbox/Hurtbox Separation" description: "Separate attack collision (hitboxes) from damage reception (hurtboxes)" when: "Implementing any melee combat system" why: "Allows independent tuning of offensive and defensive collision" example: | // The Fundamental Combat Collision Model // // HURTBOX: Where the character CAN BE HIT // - Usually follows character model closely // - Can shrink during dodges/i-frames // - Often multiple boxes (head, torso, legs) // - Persists throughout all states // // HITBOX: Where the attack DEALS DAMAGE // - Only active during attack's active frames // - Does NOT follow weapon model exactly - often larger // - Shape matches perceived attack arc, not mesh // - Can be multiple hitboxes for one attack
// Example implementation: class CombatEntity { hurtboxes: Collider[] // Always present activeHitboxes: Hitbox[] // Only during attacks
// Hitbox data for a sword slash attackData = { startup: 6, // Frames before hitbox appears active: 4, // Frames hitbox is dangerous recovery: 12, // Frames after hitbox disappears hitbox: { offset: { x: 0.5, y: 0.3 }, size: { width: 1.2, height: 0.8 }, // LARGER than sword mesh shape: 'arc', // Match visual sweep arcAngle: 120 // Degrees of coverage } }}
// CRITICAL: Hitbox should be LARGER than visual // Players aim at center of target // Give them the hit when it looks like a hit // // From Software, Platinum, and Capcom all use hitboxes // 20-40% larger than the weapon mesh
// ADVANCED: Multi-hit prevention class Hitbox { hitEntities: Set<Entity> = new Set()
checkCollision(hurtbox: Hurtbox): boolean { // Already hit this entity this attack if (this.hitEntities.has(hurtbox.owner)) return false // Check collision if (this.intersects(hurtbox)) { this.hitEntities.add(hurtbox.owner) return true } return false } reset() { this.hitEntities.clear() // Called when attack ends }}
-
name: "Input Buffering System" description: "Queue inputs during uncommitted states for responsive controls" when: "Player inputs must feel responsive during animations" why: "Human timing is imprecise; buffering bridges intention and execution" example: | // Input Buffer - The secret to responsive combat // // Without buffer: Player presses attack 2 frames early = input lost // With buffer: Input queued, executes when possible = feels responsive // // Fighting games: 3-10 frame buffer (50-167ms) // Action games: 6-15 frame buffer (100-250ms) // Casual games: 10-20 frame buffer (167-333ms)
class InputBuffer { buffer: BufferedInput[] = [] bufferDuration: number = 10 // Frames to hold input
addInput(action: string) { this.buffer.push({ action, frameAdded: currentFrame, expiresAt: currentFrame + this.bufferDuration }) } consumeInput(action: string): boolean { const now = currentFrame // Find valid buffered input const index = this.buffer.findIndex( b => b.action === action && b.expiresAt >= now ) if (index !== -1) { this.buffer.splice(index, 1) return true } return false } update() { // Clear expired inputs this.buffer = this.buffer.filter(b => b.expiresAt >= currentFrame) }}
// Usage in combat system class Player { update() { if (input.justPressed('attack')) { if (this.canAttack()) { this.attack() } else { inputBuffer.addInput('attack') // Buffer for later } }
// Check buffer when becoming actionable if (this.canAttack() && inputBuffer.consumeInput('attack')) { this.attack() } }}
// ADVANCED: Priority buffering // Buffer dodge/roll at higher priority than attack // Let players escape combos they're stuck in
-
name: "Coyote Time and Jump Buffering" description: "Forgiveness windows that respect player intent" when: "Implementing platforming in action games" why: "Players press jump slightly after leaving edge; punishing this feels unfair" example: | // COYOTE TIME (named after Wile E. Coyote) // Allow jumping for X frames after leaving ground // // Typical values: // Hardcore: 4-6 frames (67-100ms) // Standard: 8-12 frames (133-200ms) // Forgiving: 15-20 frames (250-333ms)
class PlatformingController { grounded: boolean = false lastGroundedFrame: number = 0 coyoteFrames: number = 8
update() { if (this.isOnGround()) { this.grounded = true this.lastGroundedFrame = currentFrame } else { this.grounded = false } } canJump(): boolean { // Actually grounded if (this.grounded) return true // Within coyote time const framesSinceGrounded = currentFrame - this.lastGroundedFrame if (framesSinceGrounded <= this.coyoteFrames) return true return false }}
// JUMP BUFFERING (the companion to coyote time) // Buffer jump input BEFORE landing // // Player presses jump 5 frames before landing // Without buffer: Jump doesn't happen, feels broken // With buffer: Jump queued, executes on landing
class JumpBuffer { jumpBuffered: boolean = false bufferExpiresAt: number = 0 bufferDuration: number = 10
bufferJump() { this.jumpBuffered = true this.bufferExpiresAt = currentFrame + this.bufferDuration } consumeJump(): boolean { if (this.jumpBuffered && currentFrame <= this.bufferExpiresAt) { this.jumpBuffered = false return true } return false } // Clear expired buffer update() { if (currentFrame > this.bufferExpiresAt) { this.jumpBuffered = false } }}
// Combined in player controller if (input.justPressed('jump')) { if (this.canJump()) { this.jump() } else { jumpBuffer.bufferJump() } }
// On landing if (this.justLanded && jumpBuffer.consumeJump()) { this.jump() }
-
name: "Hitstop (Hit Freeze) System" description: "Brief pause on hit to sell impact" when: "Attacks need to feel impactful" why: "Hitstop gives the brain time to register the hit; without it, combat feels floaty" example: | // HITSTOP - The most important combat feel technique // // When attack connects, BOTH attacker and target pause // Duration scales with attack power // Creates the "crunch" that sells impact // // Reference values (at 60fps): // Light attack: 3-5 frames (50-83ms) // Medium attack: 6-10 frames (100-167ms) // Heavy attack: 12-18 frames (200-300ms) // Critical/Finishing: 20-30 frames (333-500ms)
class HitstopManager { hitstopRemaining: number = 0
applyHitstop(frames: number) { // Take the larger hitstop if overlapping this.hitstopRemaining = Math.max(this.hitstopRemaining, frames) } update(): boolean { if (this.hitstopRemaining > 0) { this.hitstopRemaining-- return true // Game is frozen } return false // Normal update }}
// Usage in game loop function gameLoop(delta) { // Check if in hitstop if (hitstopManager.update()) { // Still render, just don't update positions render() return }
// Normal update updateGame(delta) render()}
// When hit connects function onHitConfirmed(attacker, target, damage) { const hitstopFrames = calculateHitstop(damage)
// Both freeze (critical for game feel) attacker.applyHitstop(hitstopFrames) target.applyHitstop(hitstopFrames) // Global hitstop for camera shake timing hitstopManager.applyHitstop(hitstopFrames) // The "crunch" happens during hitstop screenShake(damage) spawnHitParticles(target.position) playHitSound(damage)}
// ADVANCED: Asymmetric hitstop // Attacker freezes less than target // Creates feeling of follow-through function applyAsymmetricHitstop(attacker, target, baseFrames) { attacker.applyHitstop(baseFrames * 0.7) // 70% target.applyHitstop(baseFrames) // 100% }
// ADVANCED: Hitstop ramping for combos // Each hit in combo adds slightly more hitstop // Creates escalating satisfaction function comboHitstop(baseFrames, comboCount) { const ramp = 1 + (comboCount * 0.1) // +10% per hit const maxMultiplier = 2.0 return Math.round(baseFrames * Math.min(ramp, maxMultiplier)) }
-
name: "Screen Shake for Impact" description: "Camera trauma that reinforces hit feedback" when: "Attacks connect, explosions occur, heavy landings" why: "Visual shake combined with hitstop creates visceral impact" example: | // SCREEN SHAKE - The partner to hitstop // // Types of shake: // 1. TRAUMA-BASED: Decaying shake from single events // 2. CONTINUOUS: Ongoing shake for rumble/engines // 3. DIRECTIONAL: Shake in attack direction
class ScreenShake { trauma: number = 0 // 0-1, decays over time maxOffset: number = 10 // Max pixel displacement maxRotation: number = 2 // Max degrees rotation decayRate: number = 0.8 // Per-frame multiplier
// Add trauma from hit (0-1 based on damage) addTrauma(amount: number) { this.trauma = Math.min(1, this.trauma + amount) } update() { if (this.trauma > 0) { // Quadratic falloff feels more natural const shake = this.trauma * this.trauma // Perlin noise for smooth shake const offsetX = this.maxOffset * shake * noise(time * 20) const offsetY = this.maxOffset * shake * noise(time * 20 + 100) const rotation = this.maxRotation * shake * noise(time * 20 + 200) camera.offset.x = offsetX camera.offset.y = offsetY camera.rotation = rotation // Decay trauma this.trauma *= this.decayRate if (this.trauma < 0.01) this.trauma = 0 } }}
// Directional shake - punch in attack direction function directionalShake(direction: Vector2, power: number) { // Initial push in hit direction camera.offset.x += direction.x * power * 0.5 camera.offset.y += direction.y * power * 0.5
// Then normal trauma shake screenShake.addTrauma(power)}
// TRAUMA VALUES BY ATTACK TYPE const SHAKE_VALUES = { lightAttack: 0.15, mediumAttack: 0.3, heavyAttack: 0.5, criticalHit: 0.7, finisher: 1.0,
// Environmental landing: 0.1, explosion: 0.8, bossSlam: 0.9}
// IMPORTANT: Shake during hitstop // Shake happens WHILE frozen, not after // This is when impact registers
-
name: "Invincibility Frames (I-Frames)" description: "Periods where player cannot be hit" when: "Implementing dodges, rolls, backsteps" why: "I-frames reward timing and create strategic depth" example: | // I-FRAMES - The soul of defensive combat // // Dodge/roll i-frames create risk/reward: // - Too many: Dodge spam trivializes combat // - Too few: Dodging feels useless // - Timed right: Rewards mastery // // Reference values (at 60fps): // // Dark Souls roll: ~13 i-frames in 30-frame roll // Bloodborne dodge: ~10 i-frames in 26-frame quickstep // DMC5 dodge: ~8 i-frames, but very responsive // Sekiro deflect: Frame-perfect with generous window
class DodgeSystem { state: 'ready' | 'startup' | 'iframes' | 'recovery' = 'ready' stateFrame: number = 0
dodgeData = { startup: 2, // Vulnerable startup iframes: 12, // Invincible period recovery: 8, // Vulnerable recovery cooldown: 6 // Before can dodge again } startDodge(direction: Vector2) { this.state = 'startup' this.stateFrame = 0 this.direction = direction } update() { this.stateFrame++ switch (this.state) { case 'startup': if (this.stateFrame >= this.dodgeData.startup) { this.state = 'iframes' this.stateFrame = 0 } break case 'iframes': if (this.stateFrame >= this.dodgeData.iframes) { this.state = 'recovery' this.stateFrame = 0 } break case 'recovery': if (this.stateFrame >= this.dodgeData.recovery) { this.state = 'ready' } break } } isInvincible(): boolean { return this.state === 'iframes' } canDodge(): boolean { return this.state === 'ready' }}
// ADVANCED: Variable i-frames based on timing // Reward players who dodge INTO attacks function calculateIframes(dodgeDirection, attackDirection) { const dot = dodgeDirection.dot(attackDirection)
// Dodging into attack: More i-frames if (dot > 0.7) return 15 // Neutral dodge: Standard i-frames if (dot > -0.3) return 12 // Dodging away: Fewer i-frames return 8}
// PARRY WINDOWS (even more demanding) // Perfect parry: 3-6 frames (50-100ms) // Generous parry: 8-12 frames (133-200ms) // // Parry is high risk, high reward // Failed parry often means taking hit
-
name: "Attack Cancel Windows" description: "When attacks can be interrupted into other actions" when: "Building combo systems or responsive combat" why: "Cancels create depth through player expression and mix-ups" example: | // CANCEL WINDOWS - The grammar of combo systems // // Cancel types (in order of commitment): // 1. Normal -> Normal (attack chains) // 2. Normal -> Special (combo extensions) // 3. Any -> Dodge (escape option) // 4. Any -> Super (resource-gated escape) // // Fighting game notation: // Startup -> Active -> Recovery // Cancels happen during Active or early Recovery
class AttackData { frames = { startup: 5, active: 3, recovery: 12 }
cancelWindows = { // Frame ranges where cancels are allowed normalCancel: { start: 8, end: 16 }, // Cancel into next normal specialCancel: { start: 6, end: 18 }, // Cancel into special jumpCancel: { start: 10, end: 20 }, // Cancel into jump dodgeCancel: { start: 5, end: 15 } // Cancel into dodge } // On-hit only cancels (reward landing the hit) onHitCancels = { launcher: { start: 8, end: 12 }, // Only if hit confirmed finisher: { start: 10, end: 14 } }}
class ComboSystem { currentAttack: AttackData | null = null attackFrame: number = 0 hitConfirmed: boolean = false
canCancelInto(cancelType: string): boolean { if (!this.currentAttack) return false const window = this.currentAttack.cancelWindows[cancelType] if (!window) return false return this.attackFrame >= window.start && this.attackFrame <= window.end } canCancelOnHit(cancelType: string): boolean { if (!this.hitConfirmed) return false const window = this.currentAttack.onHitCancels[cancelType] if (!window) return false return this.attackFrame >= window.start && this.attackFrame <= window.end }}
// DMC-STYLE COMBO SYSTEM // Chain hierarchy: Normal < Special < Super // Each level cancels the one below // Creates the "triangle" of options
// SOULS-LIKE COMMITMENT // Minimal cancels - recovery is punishable // Only dodge cancel, and it costs stamina // Creates deliberate, tactical combat
// PLATINUM STYLE // Liberal cancels but with "just frame" bonuses // Dodge Offset: Hold attack, dodge, release to continue combo // Creates expression through execution
-
name: "Enemy Archetype System" description: "Design enemies with clear combat roles" when: "Populating a game with varied combat encounters" why: "Archetypes create encounter variety without exponential design work" example: | // THE FUNDAMENTAL ENEMY ARCHETYPES // // 1. FODDER/GRUNT // - Low HP, low damage // - Simple attack patterns (1-2 attacks) // - Short telegraphs // - Purpose: Warm-up, combo building, group pressure
const GRUNT = { hp: 30, damage: 10, attacks: [ { name: 'slash', startup: 15, active: 5, recovery: 20 } ], behavior: 'approach_and_swing', stagger: 'on_any_hit' }
// 2. RANGED // - Low-medium HP // - Attacks from distance // - Forces player movement // - Purpose: Area denial, pressure during melee
const RANGED = { hp: 25, damage: 15, preferredDistance: 10, attacks: [ { name: 'projectile', startup: 20, active: 1, recovery: 30 } ], behavior: 'maintain_distance_and_fire', stagger: 'on_any_hit' }
// 3. TANK/ARMORED // - High HP, high poise // - Slow but dangerous attacks // - Long recovery windows // - Purpose: Teaches patience, punish timing
const TANK = { hp: 150, damage: 40, poise: 50, // Takes 50 damage before stagger attacks: [ { name: 'overhead_slam', startup: 40, active: 8, recovery: 60 } ], behavior: 'approach_slowly_attack_when_close', stagger: 'only_when_poise_broken' }
// 4. AGILE/ASSASSIN // - Low HP, high damage // - Fast attacks, short openings // - Dodges/teleports // - Purpose: Teaches aggression, don't let them breathe
const ASSASSIN = { hp: 40, damage: 35, attacks: [ { name: 'quick_slash', startup: 8, active: 3, recovery: 15 }, { name: 'backstab', startup: 25, active: 5, recovery: 10 } ], behavior: 'circle_and_strike_dodge_often', stagger: 'on_any_hit_brief' }
// 5. ELITE/MINIBOSS // - High HP, varied moveset // - Multiple attack phases // - Tests everything player has learned // - Purpose: Skill check, gatekeeper
const ELITE = { hp: 300, phases: [ { threshold: 1.0, moveset: ['combo_a', 'ranged_attack'] }, { threshold: 0.5, moveset: ['combo_a', 'combo_b', 'grab'] }, { threshold: 0.25, moveset: ['enraged_combo', 'aoe_attack'] } ] }
// ENCOUNTER DESIGN FORMULA // Start: 2-3 grunts (warmup) // Build: Add ranged enemy (positioning) // Tension: Add tank OR assassin (focus target) // Peak: Mixed group OR elite // Breather: Few grunts, resources
-
name: "Attack Telegraph System" description: "Visual and audio cues that warn of incoming attacks" when: "Designing enemy attacks players must react to" why: "Telegraphs make combat about skill, not memorization or luck" example: | // TELEGRAPH HIERARCHY // Every attack needs multiple layers of warning // // 1. STANCE/POSTURE (earliest warning) // 2. WIND-UP ANIMATION (primary tell) // 3. VFX INDICATORS (reinforcement) // 4. AUDIO CUE (accessibility, off-screen)
class AttackTelegraph { // Telegraph timing relative to attack phases = { stance: -30, // 30 frames before wind-up windUp: -20, // 20 frames of wind-up animation vfxWarn: -15, // VFX starts 15 frames before hit audioWarn: -12, // Audio cue 12 frames before hit attack: 0 // Hit frame }
// VFX indicators by attack type vfxIndicators = { melee: 'weapon_glow', slam: 'ground_target_circle', projectile: 'charge_particles', grab: 'grab_range_indicator', aoe: 'danger_zone_fill' }}
// TELEGRAPH DURATION BY DIFFICULTY // The same attack can feel fair or unfair based on telegraph time
const TELEGRAPH_SCALING = { // Human reaction time: ~200-300ms (12-18 frames at 60fps)
easy: { fastAttack: 24, // 400ms - comfortable reaction mediumAttack: 36, // 600ms - leisurely heavyAttack: 60 // 1000ms - obvious }, normal: { fastAttack: 18, // 300ms - reactable for most mediumAttack: 28, // 467ms - comfortable heavyAttack: 45 // 750ms - clear }, hard: { fastAttack: 12, // 200ms - at reaction limit mediumAttack: 20, // 333ms - requires attention heavyAttack: 35 // 583ms - standard }, expert: { fastAttack: 8, // 133ms - prediction required mediumAttack: 15, // 250ms - tight reaction heavyAttack: 25 // 417ms - punishing }}
// ATTACK TELLS - What makes a good telegraph // // GOOD TELEGRAPH: // - Distinct silhouette from other animations // - Clear direction of incoming attack // - Consistent timing (same wind-up = same timing) // - Audio reinforcement // // BAD TELEGRAPH: // - Looks similar to non-threatening animation // - Variable timing (sometimes fast, sometimes slow) // - No audio (off-screen attacks feel unfair) // - Too subtle (only visible if you already know it)
// FROM SOFTWARE EXAMPLE: // Margit's dagger throw has explicit wind-up // Changes stance, pulls arm back, pauses, throws // Even first-time players can see it coming // But timing is tight enough to punish bad dodges
-
name: "Damage Feedback Hierarchy" description: "Layered feedback that communicates damage magnitude" when: "Hits need to communicate impact level" why: "Players need to understand if attacks are effective" example: | // FEEDBACK LAYERS (all should scale with damage) // // 1. HITSTOP (timing) // 2. SCREEN SHAKE (camera) // 3. HIT VFX (particles) // 4. HIT SFX (audio) // 5. ANIMATION (target reaction) // 6. HAPTICS (controller rumble) // 7. UI (damage numbers, health bar)
class DamageFeedbackSystem { applyFeedback(damage: number, isCritical: boolean, position: Vector2) { // Normalize damage to 0-1 for scaling const intensity = Math.min(damage / 100, 1)
// 1. HITSTOP - Most important const hitstopFrames = this.calculateHitstop(intensity, isCritical) hitstopManager.apply(hitstopFrames) // 2. SCREEN SHAKE screenShake.addTrauma(intensity * (isCritical ? 1.5 : 1)) // 3. HIT VFX const vfxScale = 0.5 + intensity * 0.5 const vfxType = isCritical ? 'critical_hit' : 'normal_hit' vfxSystem.spawn(vfxType, position, vfxScale) // 4. HIT SFX const sfxType = this.selectHitSound(intensity, isCritical) audioManager.play(sfxType, position) // 5. HAPTICS const rumbleIntensity = intensity * (isCritical ? 1.0 : 0.6) haptics.rumble(rumbleIntensity, hitstopFrames / 60) // 6. UI if (showDamageNumbers) { ui.spawnDamageNumber(damage, position, isCritical) } } calculateHitstop(intensity: number, critical: boolean): number { // Light hit: 3-5 frames // Heavy hit: 10-15 frames // Critical: 1.5x multiplier const base = 3 + Math.round(intensity * 12) return critical ? Math.round(base * 1.5) : base } selectHitSound(intensity: number, critical: boolean): string { if (critical) return 'hit_critical' if (intensity > 0.7) return 'hit_heavy' if (intensity > 0.3) return 'hit_medium' return 'hit_light' }}
// CRITICAL HIT SPECIAL TREATMENT // Critical hits should feel EXCEPTIONAL: // - Longer hitstop (1.5-2x) // - Unique audio sting // - Distinct VFX (different color, more particles) // - Slow-motion optional (for finishers) // - UI fanfare (flash, scale)
// OVERKILL FEEDBACK // When enemy dies, scale feedback to remaining damage // Makes "just enough" feel different from "overwhelming power"
function applyKillingBlow(target, damage, overkillAmount) { const overkillRatio = overkillAmount / target.maxHp
// Overkill = bigger explosion if (overkillRatio > 0.5) { vfx.spawn('death_explosion_large', target.position) screenShake.addTrauma(0.8) timeScale.pulse(0.1, 200) // Slow-mo pulse } else { vfx.spawn('death_explosion_normal', target.position) screenShake.addTrauma(0.4) }}
-
name: "Recovery and Punishment Windows" description: "Frame-data driven openings after attacks" when: "Combat needs risk/reward depth" why: "Recovery windows create strategy; attacks have consequences" example: | // RECOVERY FRAMES - Where strategy lives // // Every attack should have a period of vulnerability after // This is the "cost" of swinging // // Formula: Power = Startup + Active + Recovery // Stronger attacks = longer total commitment
class AttackFrameData { // Light attack: Quick but weak lightAttack = { startup: 5, // Fast to come out active: 3, // Brief hitbox recovery: 10, // Short vulnerability total: 18, // 300ms commitment onBlock: -4 // Slight disadvantage if blocked }
// Heavy attack: Strong but risky heavyAttack = { startup: 15, // Telegraphed active: 5, // Extended hitbox recovery: 25, // Long vulnerability total: 45, // 750ms commitment onBlock: -15 // Very punishable if blocked } // Special attack: High risk/reward specialAttack = { startup: 20, active: 8, recovery: 30, // Whiff = death sentence total: 58, // Almost 1 second onBlock: -20 // Free punish if blocked }}
// FRAME ADVANTAGE/DISADVANTAGE // The currency of fighting game strategy // // Positive (+) = You recover first, can act // Neutral (0) = Equal, reset to neutral // Negative (-) = Opponent recovers first // // Example: Your move is -5 on block // Enemy blocks, they can act 5 frames before you // If they have a 5-frame startup attack = guaranteed
function calculateFrameAdvantage( attackerRecovery: number, targetBlockstun: number ): number { // Positive = attacker advantage // Negative = defender advantage return targetBlockstun - attackerRecovery }
// ENEMY RECOVERY WINDOWS // This is where bosses feel fair or unfair // // Fair boss: Long recovery after big attacks // Unfair boss: Instantly can attack again
const BOSS_ATTACK_EXAMPLE = { name: 'overhead_slam', startup: 45, // Long wind-up, very readable active: 10, // Extended danger zone recovery: 60, // ONE SECOND of vulnerability // This is the "hit me" window // Player learns: Dodge, then punish
punishWindow: { frames: 60, // 1 second playerAttackStartup: 15, // Player's attack maxPunishHits: 2 // Can get 2 hits in safely }}
// RULE OF THUMB: // Recovery frames should be >= player's fastest punish option // Otherwise the "opening" isn't really an opening
-
name: "Stamina and Resource Management" description: "Action economy that creates strategic decisions" when: "Combat needs pacing and decision-making" why: "Resources prevent spam and create risk/reward" example: | // STAMINA SYSTEM (Souls-like) // // Every action costs stamina // Creates strategic decisions: // - Do I attack or save stamina for dodge? // - Do I block this or roll? // - Can I afford another swing?
class StaminaSystem { current: number = 100 max: number = 100
costs = { lightAttack: 15, heavyAttack: 30, roll: 20, sprint: 5, // Per second block: 0 // But regen paused while blocking } recovery = { rate: 30, // Per second delayAfterAction: 0.5, // Seconds before regen starts blockingPenalty: 0.5, // 50% regen while blocking emptyPenalty: 1.5 // Extra delay when depleted } lastActionTime: number = 0 canAfford(action: string): boolean { return this.current >= this.costs[action] } spend(action: string): boolean { if (!this.canAfford(action)) return false this.current -= this.costs[action] this.lastActionTime = time.now() return true } update(delta: number) { // Check regen delay const timeSinceAction = time.now() - this.lastActionTime const delay = this.current <= 0 ? this.recovery.emptyPenalty : this.recovery.delayAfterAction if (timeSinceAction < delay) return // Apply regen let regenRate = this.recovery.rate if (this.isBlocking) regenRate *= this.recovery.blockingPenalty this.current = Math.min(this.max, this.current + regenRate * delta) }}
// BLOCK STAMINA (different from action stamina) // Taking hits while blocking drains stamina // Block broken = staggered, vulnerable
function onBlockedAttack(damage: number) { const staminaDrain = damage * 0.5 stamina.current -= staminaDrain
if (stamina.current <= 0) { // Guard break! player.stagger(60) // 1 second stun vfx.spawn('guard_break') sfx.play('guard_break') }}
// POISE/POSTURE SYSTEM (Sekiro-style) // Taking hits builds "posture damage" // Full posture = vulnerable to critical // Creates aggressive play incentive
class PostureSystem { current: number = 0 // Starts empty max: number = 100 recoveryRate: number = 5 // Per second
takeDamage(damage: number, isBlocked: boolean) { // Blocked attacks deal more posture damage const multiplier = isBlocked ? 1.5 : 1.0 this.current += damage * multiplier if (this.current >= this.max) { this.triggerPostureBreak() } } triggerPostureBreak() { // Open for deathblow/critical owner.enterVulnerableState(120) // 2 seconds vfx.spawn('posture_break') sfx.play('posture_break') this.current = 0 }}
anti_patterns:
-
name: "Invisible Hitboxes" description: "Hitboxes that don't match visual attacks" why: "Players die to attacks that visually missed; feels unfair and random" instead: | Make hitboxes LARGER than visuals, not smaller. If an attack looks like it should hit, it should hit. Test with hitbox visualization enabled.
// Debug visualization is mandatory during development function renderHitboxDebug() { for (const hitbox of activeHitboxes) { drawWireframe(hitbox, COLOR_RED) } for (const hurtbox of allHurtboxes) { drawWireframe(hurtbox, COLOR_GREEN) } }
-
name: "Unreadable Attack Tells" description: "Enemy attacks with no clear warning" why: "Players can't react to what they can't see; memorization replaces skill" instead: | Every attack needs telegraph time >= human reaction time (~250ms). Fast attacks are fine IF telegraphed by stance/behavior. Add audio cues for attacks - accessibility and fairness.
Rule of thumb: If playtesters say "I didn't see that coming" more than 10% of the time, the telegraph is too subtle.
-
name: "No Recovery Windows" description: "Enemies that can attack again immediately after attacking" why: "No punishment opportunity means no strategy; just dodge forever" instead: | Every attack should have a clear window where the enemy is vulnerable. Recovery >= player's fastest punish. If unsure, make recovery too long then tune shorter.
// Boss attack formula: // Big wind-up (readable) + Extended recovery (punishable) = Fair // Quick attack + Instant followup = Frustrating
-
name: "Damage Sponges" description: "Enemies with massive HP but simple patterns" why: "Long fights without variety are boring; repetition without depth" instead: | Reduce HP, add phases or new attacks. If a fight lasts > 3 minutes, it needs phase transitions. Interesting fights are about adaptation, not endurance.
// Instead of: bossHP = 5000 attackPattern = [attack1, attack2, attack1, attack2...]
// Do: bossHP = 2000 phases = [ { threshold: 1.0, attacks: [attack1, attack2] }, { threshold: 0.5, attacks: [attack1, attack2, attack3, newMechanic] }, { threshold: 0.25, attacks: [enragedCombo, desperationMove] } ]
-
name: "Input Delay Ignoring" description: "Not accounting for input-to-action delay" why: "Combat feels sluggish; players blame the game, not themselves" instead: | Measure total input latency: Input -> Action visible on screen. Target: < 100ms for responsive games, < 66ms for fighting games. Compensate for platform (TV game mode, wireless controllers).
// Common sources of delay: // - Controller polling (8-16ms wireless) // - Input processing (1 frame = 16.67ms) // - Animation blend time (variable) // - Display lag (16-60ms on TVs) // // Total can easily reach 100-200ms if not careful
-
name: "Cancel Everything Always" description: "Every attack can cancel into any other action at any time" why: "No commitment means no risk; combat becomes mash-fest" instead: | Cancels should be strategic choices, not universal escapes. Design cancel hierarchies: Normal < Special < Super. Some attacks SHOULD be committal - that's where reads happen.
// Good cancel design: // Early frames: Can cancel into dodge (escape option) // Active frames: Committed (risk) // Late recovery: Can cancel into combo followup (reward for hit)
-
name: "Inconsistent Frame Data" description: "Same-looking attacks with different timings" why: "Players can't build reliable muscle memory; reactions feel random" instead: | Visual similarity should mean timing similarity. If two attacks look the same, they should have same frame data. Exceptions must have clear visual distinction.
// Enemy has "quick slash" and "delayed slash" // BAD: Both use same animation at different speeds // GOOD: Delayed slash has distinct wind-up pose and effect
-
name: "Perfect Play Required" description: "Combat that only works if player never makes mistakes" why: "Most players will give up; only 1% finish your game" instead: | Design for "good enough" play, not perfect play. Recovery from mistakes should be possible (healing, distance, etc). Difficulty comes from consistency over time, not single execution tests.
// Dark Souls works because: // Individual mistakes are recoverable (heal, back off) // Difficulty is cumulative (resource management over time) // Victory requires consistency, not perfection
handoffs:
-
trigger: "animation|state machine|blend tree|root motion" to: animation-systems context: "Combat requires tight animation integration" provides:
- "Frame data requirements"
- "Cancel window specifications"
- "Animation event needs (damage frames)"
- "Root motion vs in-place decision"
-
trigger: "enemy AI|behavior tree|combat AI|boss patterns" to: game-ai-behavior context: "Enemy behavior implementation needed" provides:
- "Enemy archetype definitions"
- "Attack pattern sequences"
- "Aggression and retreat triggers"
- "Difficulty scaling parameters"
-
trigger: "hit effects|particles|screen shake|VFX" to: vfx-realtime context: "Combat visual feedback implementation" provides:
- "Hit effect intensity scaling"
- "Timing requirements (hitstop sync)"
- "Effect layering hierarchy"
- "Performance budget per hit"
-
trigger: "hit sounds|impact audio|combat SFX" to: game-audio context: "Combat audio feedback implementation" provides:
- "Sound priority hierarchy"
- "Layering requirements"
- "Timing sync with hitstop"
- "Variation needs for repetition"
-
trigger: "game feel|juice|polish|feedback" to: game-design context: "Overall game feel tuning" provides:
- "Combat feel specifications"
- "Feedback layer documentation"
- "Known feel issues"
- "Reference games"
-
trigger: "Unity implementation|Unreal implementation|Godot implementation" to: game-design context: "Engine-specific combat implementation" provides:
- "Combat system architecture"
- "Frame data specifications"
- "Required systems list"
- "Performance requirements"
decision_framework: commitment_level: question: "How committal should attacks be?" framework: | Souls-like (High Commitment) - Most actions are committal - Very limited cancels (dodge only) - Deliberate, strategic pace - Punish mistakes hard
**Character Action (Medium Commitment)** - Cancels into dodge always available - Attack chains are committal - Fast pace, many options - Reward skillful play **Musou/Hack-n-Slash (Low Commitment)** - Most actions cancel into anything - Power fantasy focus - Fast, flashy, forgiving - Punish rarely Choose based on: - Target audience skill level - Core fantasy (mastery vs power) - Platform (mobile = more forgiving)
difficulty_source: question: "What makes combat challenging?" framework: | Reaction-based (Most accessible) - Telegraphed attacks, test reactions - Pattern recognition - Get better by learning patterns
**Execution-based (Fighting games)** - Complex inputs required - Tight timing windows - Get better by practicing mechanics **Knowledge-based (Roguelikes)** - Must learn systems/items - Meta-game mastery - Get better by understanding systems **Resource-based (Survival)** - Limited healing, ammo, etc. - Cumulative challenge over time - Get better by efficiency
resources: essential_reading: - "GDC: 'Math for Game Programmers: Building a Better Jump' - Kyle Pittman" - "GDC: 'The Art of Screen Shake' - Jan Willem Nijman" - "GDC: 'Crafting the Action Combat of Sekiro' - From Software" - "Devil May Cry 5 GDC: 'Enemy and Level Design'" - "Guilty Gear Strive Frame Data as Design Document" - "David Sirlin's 'Playing to Win' (fighting game theory)"
reference_games: - "Devil May Cry 5 (character action benchmark)" - "Sekiro: Shadows Die Twice (posture system, deflect)" - "Elden Ring (large-scale boss design)" - "Monster Hunter World (weapon commitment, tells)" - "Guilty Gear Strive (frame data, accessibility)" - "Hades (fast-paced roguelike combat)" - "God of War 2018 (cinematic combat feel)"