181 lines
4.9 KiB
Markdown
181 lines
4.9 KiB
Markdown
# OpenSpec Change: enterprise-gaming-combat
|
||
|
||
- **Change ID**: enterprise-gaming-combat
|
||
- **Package**: `@suaveplan/combat`
|
||
- **Category**: gaming/combat
|
||
- **Target path**: `gaming/combat/combat`
|
||
- **Status**: PROPOSED
|
||
- **Date**: 2026-04-24
|
||
- **Reference spec**: `/mnt/s/web/suaveplan/enterprise/combat.md`
|
||
|
||
---
|
||
|
||
## Summary
|
||
|
||
Deliver `@suaveplan/combat` on the genesis branch from scratch at enterprise quality. The package provides core combat mechanics: multiple damage types (physical, elemental, true), health/armor/shield layers, resistances, status effects (burn, stun, slow, poison) with duration and tick logic, damage calculation pipeline with modifier chain, and `CombatManager` that processes hit events and emits `@suaveplan/events` combat messages. Genesis adds Zod schemas for all combat data, structured errors, OTel damage counters, injectable clock, and 100% coverage including property tests for damage pipeline commutativity.
|
||
|
||
---
|
||
|
||
## Current-state audit (genesis)
|
||
|
||
Package does not exist on genesis yet — fresh build.
|
||
|
||
---
|
||
|
||
## Available workspace deps on genesis
|
||
|
||
| Package | Role |
|
||
|---|---|
|
||
| `@suaveplan/error` (`workspace:^`) | `CombatError`; `COMBAT.*` codes |
|
||
| `@suaveplan/types` (`workspace:^`) | `Clock`, `IdGenerator` |
|
||
| `@suaveplan/events` (`workspace:^`) | `combat:hit`, `combat:death`, `combat:status` events |
|
||
| `zod` (`catalog:forms`) | `DamageEvent`, `StatusEffect`, `CombatantStats` schemas |
|
||
| `@opentelemetry/api` (`catalog:observability`) | Damage counter, death counter |
|
||
|
||
---
|
||
|
||
## Public API surface
|
||
|
||
```ts
|
||
type DamageType = 'physical' | 'fire' | 'ice' | 'lightning' | 'poison' | 'true'
|
||
|
||
interface CombatantStats {
|
||
maxHealth: number
|
||
maxArmor: number
|
||
maxShield: number
|
||
resistances: Partial<Record<DamageType, number>> // 0–1 reduction
|
||
defense: number
|
||
}
|
||
|
||
function createCombatant(id: string, stats: CombatantStats): Combatant
|
||
|
||
interface Combatant {
|
||
readonly id: string
|
||
readonly health: number
|
||
readonly armor: number
|
||
readonly shield: number
|
||
readonly isDead: boolean
|
||
readonly statusEffects: readonly StatusEffect[]
|
||
applyDamage(event: DamageEvent): DamageResult
|
||
applyHealing(amount: number, type?: 'health' | 'shield'): void
|
||
applyStatus(effect: StatusEffect): void
|
||
tick(dt: number): void // processes status effect durations + ticks
|
||
revive(health?: number): void
|
||
}
|
||
|
||
interface DamageEvent {
|
||
sourceId: string
|
||
targetId: string
|
||
amount: number
|
||
type: DamageType
|
||
isCritical?: boolean
|
||
modifiers?: DamageModifier[]
|
||
}
|
||
|
||
interface DamageResult {
|
||
finalDamage: number
|
||
healthDamage: number
|
||
armorDamage: number
|
||
shieldDamage: number
|
||
wasKilled: boolean
|
||
statusesApplied: string[]
|
||
}
|
||
|
||
interface StatusEffect {
|
||
id: string
|
||
name: string
|
||
duration: number
|
||
tickInterval?: number
|
||
tickDamage?: number
|
||
tickDamageType?: DamageType
|
||
speedModifier?: number
|
||
damageModifier?: number
|
||
}
|
||
|
||
interface DamageModifier {
|
||
type: 'multiply' | 'add' | 'set'
|
||
value: number
|
||
priority?: number
|
||
}
|
||
|
||
function createCombatManager(options?: CombatManagerOptions): CombatManager
|
||
|
||
interface CombatManager {
|
||
registerCombatant(combatant: Combatant): void
|
||
unregisterCombatant(id: string): void
|
||
processHit(event: DamageEvent): DamageResult
|
||
tick(dt: number): void
|
||
getCombatant(id: string): Combatant | undefined
|
||
}
|
||
```
|
||
|
||
### Error codes
|
||
|
||
```ts
|
||
type CombatErrorCode =
|
||
| 'COMBAT_COMBATANT_NOT_FOUND'
|
||
| 'COMBAT_TARGET_DEAD'
|
||
| 'COMBAT_INVALID_DAMAGE'
|
||
| 'COMBAT_STATUS_OVERFLOW'
|
||
```
|
||
|
||
---
|
||
|
||
## New additions for genesis
|
||
|
||
1. **Damage pipeline** — ordered modifier chain: resistances → armor absorption → shield drain → health damage.
|
||
2. **Status effect stacking** — configurable max-stack count; refresh vs. stack vs. replace policy.
|
||
3. **Critical hit multiplier** — `isCritical` doubles damage before modifier chain.
|
||
4. **OTel** — `combat.damage_total` counter (by type); `combat.deaths_total` counter; `combat.active_combatants` gauge.
|
||
|
||
---
|
||
|
||
## File layout
|
||
|
||
```text
|
||
gaming/combat/combat/
|
||
package.json
|
||
tsconfig.json
|
||
vitest.config.ts
|
||
src/
|
||
index.ts
|
||
error-codes.ts
|
||
constants/index.ts
|
||
types/common.ts
|
||
types/common.md
|
||
core/
|
||
combatant/
|
||
combatant.ts
|
||
combatant.md
|
||
combatant.types.ts
|
||
combatant.test.ts
|
||
index.ts
|
||
damage-pipeline/
|
||
damage-pipeline.ts
|
||
damage-pipeline.md
|
||
damage-pipeline.types.ts
|
||
damage-pipeline.test.ts
|
||
index.ts
|
||
status-effect/
|
||
status-effect.ts
|
||
status-effect.md
|
||
status-effect.types.ts
|
||
status-effect.test.ts
|
||
index.ts
|
||
combat-manager/
|
||
combat-manager.ts
|
||
combat-manager.md
|
||
combat-manager.types.ts
|
||
combat-manager.test.ts
|
||
index.ts
|
||
tests/
|
||
integration.test.ts
|
||
property.test.ts
|
||
```
|
||
|
||
---
|
||
|
||
## Impact
|
||
|
||
**Downstream**: `@suaveplan/weapons` feeds damage events into combat manager; `@suaveplan/r3f-combat` renders damage numbers. Prerequisites: `@suaveplan/error`, `@suaveplan/types`, `@suaveplan/events`.
|