Initial Commit
This commit is contained in:
180
openspec/changes/enterprise-gaming-combat/proposal.md
Normal file
180
openspec/changes/enterprise-gaming-combat/proposal.md
Normal file
@@ -0,0 +1,180 @@
|
||||
# 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`.
|
||||
Reference in New Issue
Block a user