544 lines
20 KiB
TypeScript
544 lines
20 KiB
TypeScript
/**
|
|
* Diablo II: Lord of Destruction v1.13c — Aura Target & Visual Parity Test Suite
|
|
*
|
|
* Ground Truth Invariants (Issue #397):
|
|
* 1. Town vs Wilderness Filtering:
|
|
* - Friendly auras (Defiance #104, Might #98, Prayer #99, etc.) buff ONLY self in town.
|
|
* - In wilderness, they broadcast to self, player mercenary, party members, party mercenaries, and summons.
|
|
* 2. Conviction Asymmetry:
|
|
* - Caster receives ground ring visual only (convictionback), ZERO resistance or defense reduction on self!
|
|
* - In wilderness, hostile enemies in range receive -Def% and -Res% debuffs AND display convictionback ring under feet.
|
|
* - In town, enemy debuffs and enemy ground rings are completely suspended.
|
|
* 3. Holy Freeze Parity:
|
|
* - Caster displays cyan frost ground ring (holyfreeze).
|
|
* - In wilderness, hostile enemies in range receive cold pulse damage, slow (item_slow), and blue chill tint (chillTicks).
|
|
* - Authentic D2 Invariant: Holy Freeze NEVER displays an aura ring under enemy feet!
|
|
* - In town, enemy damage pulses and slow are completely suspended.
|
|
* 4. Complete Paladin Aura Coverage:
|
|
* - All 20 Paladin auras are recognized by isAuraSkill() and mapped to canonical 1.13c overlays.
|
|
* - Activating an aura spawns a looping player overlay that follows the player and wraps animation frames.
|
|
* - Deactivating/switching an aura cleanly disposes old aura overlays and enemy conviction rings.
|
|
* 5. Spell Cast Overlay Immunity:
|
|
* - Transient spell cast overlays (e.g. Fire Bolt, Teleport) do NOT expire or interrupt active aura overlays.
|
|
*/
|
|
import { describe, expect, it } from 'vitest'
|
|
import { getSharedDataRegistry } from '../../src/game/engine/data-registry.ts'
|
|
import { WorldArena } from '../../src/game/engine/world-arena.ts'
|
|
import { AuraScanner } from '../../src/game/engine/aura-scanner.ts'
|
|
import { UnitStatList } from '../../src/game/engine/stat-list.ts'
|
|
import { StateBus } from '../../src/game/engine/state-bus.ts'
|
|
import type { CombatUnitContext } from '../../src/game/engine/combat-pipeline.ts'
|
|
import { GameEngine } from '../../src/game/engine.ts'
|
|
import { DEMO_EXPERIENCE, DEMO_SKILLS, DEMO_QUESTS } from '../../src/game/demo-data.ts'
|
|
import {
|
|
PALADIN_AURA_OVERLAYS,
|
|
PALADIN_AURA_OVERLAY_PARTS,
|
|
PALADIN_AURA_SKILL_IDS,
|
|
isAuraSkill,
|
|
getAuraOverlayName,
|
|
getAuraOverlayNames,
|
|
} from '../../src/game/skills.ts'
|
|
|
|
function createMockTerrain() {
|
|
return {
|
|
widthPx: 2000,
|
|
heightPx: 2000,
|
|
overlap: () => 0,
|
|
}
|
|
}
|
|
|
|
const DEFAULT_COMBAT_OPTIONS = {
|
|
playerSpeed: 100,
|
|
playerReach: 40,
|
|
playerCooldownTicks: 12,
|
|
playerDamage: 5,
|
|
playerManaPerAttack: 1,
|
|
respawnTicks: 50,
|
|
disableMonsterAggro: false,
|
|
}
|
|
|
|
const DEFAULT_ENGINE_OPTIONS = {
|
|
talkRadius: 50,
|
|
pickupRadius: 30,
|
|
inventoryCols: 10,
|
|
inventoryRows: 4,
|
|
}
|
|
|
|
function makeMonsterStats(id: string, hp = 50) {
|
|
return {
|
|
id,
|
|
name: id,
|
|
hp,
|
|
damage: 2,
|
|
cooldownTicks: 25,
|
|
reach: 40,
|
|
aggroRadius: 200,
|
|
speed: 60,
|
|
xp: 10,
|
|
}
|
|
}
|
|
|
|
describe('Issue #397 — Diablo II v1.13c Aura Target & Visual Parity', () => {
|
|
it('1. Verifies all 20 Paladin auras are mapped to authentic 1.13c overlays', () => {
|
|
expect(PALADIN_AURA_SKILL_IDS.size).toBe(20)
|
|
for (const skillId of PALADIN_AURA_SKILL_IDS) {
|
|
expect(isAuraSkill(skillId)).toBe(true)
|
|
const overlay = getAuraOverlayName(skillId)
|
|
expect(overlay).toBeDefined()
|
|
expect(typeof overlay).toBe('string')
|
|
expect(overlay!.length).toBeGreaterThan(0)
|
|
}
|
|
|
|
// Verify canonical names
|
|
expect(getAuraOverlayName(98)).toBe('might')
|
|
expect(getAuraOverlayName(99)).toBe('prayer')
|
|
expect(getAuraOverlayName(100)).toBe('resist_fire')
|
|
expect(getAuraOverlayName(102)).toBe('holy_fire')
|
|
expect(getAuraOverlayName(103)).toBe('thorns')
|
|
expect(getAuraOverlayName(104)).toBe('defiance')
|
|
expect(getAuraOverlayName(105)).toBe('resist_cold')
|
|
expect(getAuraOverlayName(108)).toBe('blessed_aim')
|
|
expect(getAuraOverlayName(109)).toBe('cleansing')
|
|
expect(getAuraOverlayName(110)).toBe('resist_lightning')
|
|
expect(getAuraOverlayName(113)).toBe('concentration')
|
|
expect(getAuraOverlayName(114)).toBe('holy_freeze')
|
|
expect(getAuraOverlayName(115)).toBe('vigor')
|
|
expect(getAuraOverlayName(118)).toBe('holy_shock')
|
|
expect(getAuraOverlayName(119)).toBe('sanctuary')
|
|
expect(getAuraOverlayName(120)).toBe('meditation')
|
|
expect(getAuraOverlayName(122)).toBe('fanaticism')
|
|
expect(getAuraOverlayName(123)).toBe('conviction')
|
|
expect(getAuraOverlayName(124)).toBe('redemption')
|
|
expect(getAuraOverlayName(125)).toBe('salvation')
|
|
|
|
// Verify 13 two-part auras have [front, back] parts
|
|
const twoPartAuraIds = [98, 99, 102, 103, 104, 108, 109, 113, 115, 118, 119, 122, 125]
|
|
for (const id of twoPartAuraIds) {
|
|
const parts = getAuraOverlayNames(id)
|
|
expect(parts.length).toBe(2)
|
|
expect(parts[0]).toMatch(/_front$/)
|
|
expect(parts[1]).toMatch(/_back$/)
|
|
}
|
|
|
|
// Verify 7 full-ring auras have exactly 1 part
|
|
const fullRingAuraIds = [100, 105, 110, 114, 120, 123, 124]
|
|
for (const id of fullRingAuraIds) {
|
|
const parts = getAuraOverlayNames(id)
|
|
expect(parts.length).toBe(1)
|
|
}
|
|
})
|
|
|
|
it('2. Defiance (#104): Only buffs self in town; broadcasts to allies/mercs/summons in wilderness', async () => {
|
|
const registry = await getSharedDataRegistry()
|
|
|
|
// --- Scenario A: In Town (isTown: true) ---
|
|
const townArena = new WorldArena({
|
|
registry,
|
|
classCode: 'pal',
|
|
skillId: 104,
|
|
slvl: 20,
|
|
isTown: true,
|
|
})
|
|
const mercInTown = townArena.addDummyAlly('mercenary', 'Desert Mercenary')
|
|
const summonInTown = townArena.addDummyAlly('minion', 'Clay Golem')
|
|
const partyInTown = townArena.addDummyAlly('party', 'Sorceress Ally')
|
|
|
|
townArena.triggerCast()
|
|
townArena.stepTicks(15)
|
|
|
|
// Player MUST receive defiance aura state and armor bonus
|
|
expect(townArena.player.stateBus.hasState('defiance')).toBe(true)
|
|
expect(townArena.player.statList.getAccruedStat('skill_armor_percent')).toBeGreaterThan(0)
|
|
|
|
// Allies in town must NOT receive defiance state or stat bonus!
|
|
expect(mercInTown.stateBus.hasState('defiance')).toBe(false)
|
|
expect(summonInTown.stateBus.hasState('defiance')).toBe(false)
|
|
expect(partyInTown.stateBus.hasState('defiance')).toBe(false)
|
|
expect(mercInTown.statList.getAccruedStat('skill_armor_percent')).toBe(0)
|
|
expect(summonInTown.statList.getAccruedStat('skill_armor_percent')).toBe(0)
|
|
expect(partyInTown.statList.getAccruedStat('skill_armor_percent')).toBe(0)
|
|
|
|
// --- Scenario B: In Wilderness (isTown: false) ---
|
|
const wildArena = new WorldArena({
|
|
registry,
|
|
classCode: 'pal',
|
|
skillId: 104,
|
|
slvl: 20,
|
|
isTown: false,
|
|
})
|
|
const mercWild = wildArena.addDummyAlly('mercenary', 'Desert Mercenary')
|
|
const summonWild = wildArena.addDummyAlly('minion', 'Clay Golem')
|
|
const partyWild = wildArena.addDummyAlly('party', 'Sorceress Ally')
|
|
|
|
wildArena.triggerCast()
|
|
wildArena.stepTicks(15)
|
|
|
|
// Player and ALL allies in wilderness receive defiance aura state!
|
|
expect(wildArena.player.stateBus.hasState('defiance')).toBe(true)
|
|
expect(mercWild.stateBus.hasState('defiance')).toBe(true)
|
|
expect(summonWild.stateBus.hasState('defiance')).toBe(true)
|
|
expect(partyWild.stateBus.hasState('defiance')).toBe(true)
|
|
|
|
// Stat bonuses propagated
|
|
expect(mercWild.statList.getAccruedStat('skill_armor_percent')).toBeGreaterThan(0)
|
|
expect(summonWild.statList.getAccruedStat('skill_armor_percent')).toBeGreaterThan(0)
|
|
expect(partyWild.statList.getAccruedStat('skill_armor_percent')).toBeGreaterThan(0)
|
|
})
|
|
|
|
it('3. Conviction (#123) Asymmetry: Caster has ground ring visual only with 0 debuffs; wilderness enemies get debuffs and foot rings', async () => {
|
|
const registry = await getSharedDataRegistry()
|
|
|
|
// --- Arena verification for stat asymmetry ---
|
|
const wildArena = new WorldArena({
|
|
registry,
|
|
classCode: 'pal',
|
|
skillId: 123,
|
|
slvl: 20,
|
|
isTown: false,
|
|
})
|
|
|
|
const initialPlayerDef = wildArena.player.statList.getAccruedStat('armorclass')
|
|
const initialTargetDef = wildArena.targets[0]!.statList.getAccruedStat('armorclass')
|
|
|
|
wildArena.triggerCast()
|
|
wildArena.stepTicks(15)
|
|
|
|
// Caster has conviction state for visual overlay...
|
|
expect(wildArena.player.stateBus.hasState('conviction')).toBe(true)
|
|
// BUT ZERO defense or resistance penalty on caster!
|
|
expect(wildArena.player.statList.getAccruedStat('item_armor_percent')).toBe(0)
|
|
expect(wildArena.player.statList.getAccruedStat('armorclass')).toBe(initialPlayerDef)
|
|
|
|
// Enemy in wilderness receives defense penalty and pierce debuff
|
|
expect(wildArena.targets[0]!.stateBus.hasState('conviction')).toBe(true)
|
|
expect(wildArena.targets[0]!.statList.getAccruedStat('item_armor_percent')).toBeLessThan(0)
|
|
expect(wildArena.targets[0]!.statList.getAccruedStat('armorclass')).toBeLessThan(initialTargetDef)
|
|
|
|
// --- GameEngine verification for visual overlays ---
|
|
const engineWild = new GameEngine(createMockTerrain(), {
|
|
...DEFAULT_ENGINE_OPTIONS,
|
|
spawn: { x: 500, y: 500 },
|
|
isTown: false,
|
|
levelId: 2, // Blood Moor (wilderness)
|
|
stats: [makeMonsterStats('zombie', 50)],
|
|
monsterCount: 3,
|
|
monsterSpread: 100, // close to player
|
|
combatOptions: DEFAULT_COMBAT_OPTIONS,
|
|
xpTable: DEMO_EXPERIENCE,
|
|
skills: DEMO_SKILLS,
|
|
npcDefs: [],
|
|
questDefs: DEMO_QUESTS,
|
|
})
|
|
|
|
// Activate Conviction on player
|
|
engineWild.updateActiveAura(123, 20)
|
|
expect(engineWild.activeAuraSkillId).toBe(123)
|
|
expect(engineWild.activeAuraOverlay).toBeDefined()
|
|
expect(engineWild.activeAuraOverlay!.overlayName).toBe('convictionback')
|
|
expect(engineWild.activeAuraOverlay!.loop).toBe(true)
|
|
|
|
// Step engine ticks to allow conviction aura to acquire monsters
|
|
engineWild.tick({
|
|
movement: { x: 0, y: 0 },
|
|
attacking: false,
|
|
pickingUp: false,
|
|
talking: false,
|
|
digits: [],
|
|
saving: false,
|
|
loading: false,
|
|
})
|
|
|
|
// Alive monsters in range MUST have convictionback foot ring
|
|
const monsterRings = engineWild.overlays.filter(
|
|
ov => ov.isAura && ov.auraSkillId === 123 && ov.targetMonsterIndex !== undefined && !ov.expired,
|
|
)
|
|
expect(monsterRings.length).toBeGreaterThan(0)
|
|
expect(monsterRings[0]!.overlayName).toBe('convictionback')
|
|
expect(monsterRings[0]!.loop).toBe(true)
|
|
|
|
// --- In Town: Verify enemy debuffs & overlays are completely suspended ---
|
|
const engineTown = new GameEngine(createMockTerrain(), {
|
|
...DEFAULT_ENGINE_OPTIONS,
|
|
spawn: { x: 500, y: 500 },
|
|
isTown: true,
|
|
levelId: 1, // Rogue Encampment (town)
|
|
stats: [makeMonsterStats('zombie', 50)],
|
|
monsterCount: 3,
|
|
monsterSpread: 100,
|
|
combatOptions: DEFAULT_COMBAT_OPTIONS,
|
|
xpTable: DEMO_EXPERIENCE,
|
|
skills: DEMO_SKILLS,
|
|
npcDefs: [],
|
|
questDefs: DEMO_QUESTS,
|
|
})
|
|
|
|
engineTown.updateActiveAura(123, 20)
|
|
engineTown.tick({
|
|
movement: { x: 0, y: 0 },
|
|
attacking: false,
|
|
pickingUp: false,
|
|
talking: false,
|
|
digits: [],
|
|
saving: false,
|
|
loading: false,
|
|
})
|
|
|
|
// In town, caster has aura ring, but NO monsters receive conviction foot rings
|
|
const townMonsterRings = engineTown.overlays.filter(
|
|
ov => ov.isAura && ov.auraSkillId === 123 && ov.targetMonsterIndex !== undefined && !ov.expired,
|
|
)
|
|
expect(townMonsterRings.length).toBe(0)
|
|
})
|
|
|
|
it('4. Holy Freeze (#114) Parity: Caster has cyan ring, enemies get cold damage & chill, but NEVER an enemy foot ring', async () => {
|
|
const engineWild = new GameEngine(createMockTerrain(), {
|
|
...DEFAULT_ENGINE_OPTIONS,
|
|
spawn: { x: 500, y: 500 },
|
|
isTown: false,
|
|
levelId: 2,
|
|
stats: [makeMonsterStats('fallenshaman', 60)],
|
|
monsterCount: 2,
|
|
monsterSpread: 80,
|
|
combatOptions: DEFAULT_COMBAT_OPTIONS,
|
|
xpTable: DEMO_EXPERIENCE,
|
|
skills: DEMO_SKILLS,
|
|
npcDefs: [],
|
|
questDefs: DEMO_QUESTS,
|
|
})
|
|
|
|
engineWild.updateActiveAura(114, 20)
|
|
expect(engineWild.activeAuraSkillId).toBe(114)
|
|
expect(engineWild.activeAuraOverlay!.overlayName).toBe('holyfreeze')
|
|
expect(engineWild.activeAuraOverlay!.loop).toBe(true)
|
|
|
|
const initialHp = engineWild.world.monsters[0]!.hp
|
|
|
|
// Step 50 ticks to trigger a pulse
|
|
for (let t = 0; t < 50; t++) {
|
|
engineWild.tick({
|
|
movement: { x: 0, y: 0 },
|
|
attacking: false,
|
|
pickingUp: false,
|
|
talking: false,
|
|
digits: [],
|
|
saving: false,
|
|
loading: false,
|
|
})
|
|
}
|
|
|
|
// Monster must have taken cold damage and have chillTicks > 0
|
|
expect(engineWild.world.monsters[0]!.hp).toBeLessThan(initialHp)
|
|
expect(engineWild.world.monsters[0]!.chillTicks).toBeGreaterThan(0)
|
|
|
|
// Invariant: ZERO overlays on the monster! Holy Freeze does not spawn enemy foot rings
|
|
const monsterOverlays = engineWild.overlays.filter(
|
|
ov => ov.targetMonsterIndex !== undefined && !ov.expired,
|
|
)
|
|
expect(monsterOverlays.length).toBe(0)
|
|
})
|
|
|
|
it('5. Overlay lifecycle: Looping overlays wrap frames, follow player, and survive transient spell casts', () => {
|
|
const engine = new GameEngine(createMockTerrain(), {
|
|
...DEFAULT_ENGINE_OPTIONS,
|
|
spawn: { x: 300, y: 300 },
|
|
isTown: false,
|
|
stats: [],
|
|
monsterCount: 0,
|
|
combatOptions: DEFAULT_COMBAT_OPTIONS,
|
|
xpTable: DEMO_EXPERIENCE,
|
|
skills: DEMO_SKILLS,
|
|
npcDefs: [],
|
|
questDefs: DEMO_QUESTS,
|
|
})
|
|
|
|
// Activate Defiance aura (#104) -> authentic 1.13c spawns BOTH front and back overlays
|
|
engine.updateActiveAura(104, 20)
|
|
expect(engine.overlays.length).toBe(2)
|
|
const frontOv = engine.overlays.find(o => o.overlayName === 'defiance_front')!
|
|
const backOv = engine.overlays.find(o => o.overlayName === 'defiance_back')!
|
|
expect(frontOv).toBeDefined()
|
|
expect(backOv).toBeDefined()
|
|
expect(frontOv.preDraw).toBe(false)
|
|
expect(backOv.preDraw).toBe(true)
|
|
expect(frontOv.isAura).toBe(true)
|
|
expect(frontOv.loop).toBe(true)
|
|
expect(backOv.isAura).toBe(true)
|
|
expect(backOv.loop).toBe(true)
|
|
|
|
// Simulate 100 ticks (animation frames should wrap without expiring)
|
|
for (let i = 0; i < 100; i++) {
|
|
engine.tick({
|
|
movement: { x: 1, y: 0 }, // Player moving
|
|
attacking: false,
|
|
pickingUp: false,
|
|
talking: false,
|
|
digits: [],
|
|
saving: false,
|
|
loading: false,
|
|
})
|
|
}
|
|
|
|
expect(frontOv.expired).toBe(false)
|
|
expect(backOv.expired).toBe(false)
|
|
expect(frontOv.x).toBe(engine.world.player.x)
|
|
expect(frontOv.y).toBe(engine.world.player.y)
|
|
expect(backOv.x).toBe(engine.world.player.x)
|
|
expect(backOv.y).toBe(engine.world.player.y)
|
|
|
|
// Spawn a transient spell cast overlay (e.g. Fire Bolt cast overlay)
|
|
const castOv = engine.spawnOverlay('fire_cast_1', { x: engine.world.player.x, y: engine.world.player.y }, 'player')
|
|
expect(castOv).toBeDefined()
|
|
expect(castOv.isAura).toBeFalsy()
|
|
|
|
// The active aura overlays MUST NOT be expired by the spell cast!
|
|
expect(frontOv.expired).toBe(false)
|
|
expect(backOv.expired).toBe(false)
|
|
expect(engine.overlays).toContain(frontOv)
|
|
expect(engine.overlays).toContain(backOv)
|
|
expect(engine.overlays).toContain(castOv)
|
|
|
|
// Switching aura to Fanaticism (#122) cleanly replaces the aura overlays
|
|
engine.updateActiveAura(122, 20)
|
|
expect(frontOv.expired).toBe(true)
|
|
expect(backOv.expired).toBe(true)
|
|
expect(engine.activeAuraSkillId).toBe(122)
|
|
expect(engine.activeAuraOverlays.length).toBe(2)
|
|
expect(engine.activeAuraOverlays.map(o => o.overlayName).sort()).toEqual(['fanaticism_back', 'fanaticism_front'])
|
|
|
|
// Deactivating aura completely clears the aura overlays
|
|
engine.updateActiveAura(null)
|
|
expect(engine.activeAuraSkillId).toBeNull()
|
|
expect(engine.activeAuraOverlay).toBeNull()
|
|
expect(engine.activeAuraOverlays.length).toBe(0)
|
|
expect(engine.overlays.filter(ov => ov.isAura && !ov.expired).length).toBe(0)
|
|
})
|
|
|
|
it('6. Redemption (#124): Redeems corpses in wilderness, suspended in town', () => {
|
|
// --- Wilderness ---
|
|
const engineWild = new GameEngine(createMockTerrain(), {
|
|
...DEFAULT_ENGINE_OPTIONS,
|
|
spawn: { x: 500, y: 500 },
|
|
isTown: false,
|
|
levelId: 2,
|
|
stats: [makeMonsterStats('zombie', 20)],
|
|
monsterCount: 1,
|
|
monsterSpread: 20,
|
|
combatOptions: DEFAULT_COMBAT_OPTIONS,
|
|
xpTable: DEMO_EXPERIENCE,
|
|
skills: DEMO_SKILLS,
|
|
npcDefs: [],
|
|
questDefs: DEMO_QUESTS,
|
|
})
|
|
|
|
// Kill monster to create a fresh corpse
|
|
const mon = engineWild.world.monsters[0]!
|
|
mon.state = 'dead'
|
|
mon.corpseTicks = 100
|
|
|
|
// Set player health/mana lower to verify healing
|
|
engineWild.world.player.hp = 100
|
|
engineWild.world.player.maxHp = 500
|
|
engineWild.world.player.mana = 100
|
|
engineWild.world.player.maxMana = 500
|
|
|
|
engineWild.updateActiveAura(124, 20)
|
|
|
|
// Step 50 ticks to trigger pulse
|
|
for (let t = 0; t < 50; t++) {
|
|
engineWild.tick({
|
|
movement: { x: 0, y: 0 },
|
|
attacking: false,
|
|
pickingUp: false,
|
|
talking: false,
|
|
digits: [],
|
|
saving: false,
|
|
loading: false,
|
|
})
|
|
}
|
|
|
|
// Corpse consumed, player restored
|
|
expect(mon.corpseTicks).toBe(0)
|
|
expect(engineWild.world.player.hp).toBeGreaterThan(100)
|
|
expect(engineWild.world.player.mana).toBeGreaterThan(100)
|
|
|
|
// --- Town ---
|
|
const engineTown = new GameEngine(createMockTerrain(), {
|
|
...DEFAULT_ENGINE_OPTIONS,
|
|
spawn: { x: 500, y: 500 },
|
|
isTown: true,
|
|
levelId: 1,
|
|
stats: [makeMonsterStats('zombie', 20)],
|
|
monsterCount: 1,
|
|
monsterSpread: 20,
|
|
combatOptions: DEFAULT_COMBAT_OPTIONS,
|
|
xpTable: DEMO_EXPERIENCE,
|
|
skills: DEMO_SKILLS,
|
|
npcDefs: [],
|
|
questDefs: DEMO_QUESTS,
|
|
})
|
|
|
|
const townMon = engineTown.world.monsters[0]!
|
|
townMon.state = 'dead'
|
|
townMon.corpseTicks = 100
|
|
engineTown.world.player.hp = 100
|
|
engineTown.world.player.maxHp = 500
|
|
|
|
engineTown.updateActiveAura(124, 20)
|
|
for (let t = 0; t < 50; t++) {
|
|
engineTown.tick({
|
|
movement: { x: 0, y: 0 },
|
|
attacking: false,
|
|
pickingUp: false,
|
|
talking: false,
|
|
digits: [],
|
|
saving: false,
|
|
loading: false,
|
|
})
|
|
}
|
|
|
|
// In town, corpse consumption is suspended (corpse natural decay is 1/tick so 100 - 50 = 50, not consumed to 0)
|
|
expect(townMon.corpseTicks).toBe(50)
|
|
expect(engineTown.world.player.hp).toBe(100)
|
|
})
|
|
|
|
it('7. 3D Isometric Aura Ring Sandwich: Two-part auras spawn front (depth +0.1) and back (depth -0.1)', () => {
|
|
const engine = new GameEngine(createMockTerrain(), {
|
|
...DEFAULT_ENGINE_OPTIONS,
|
|
spawn: { x: 500, y: 500 },
|
|
isTown: false,
|
|
stats: [],
|
|
monsterCount: 0,
|
|
combatOptions: DEFAULT_COMBAT_OPTIONS,
|
|
xpTable: DEMO_EXPERIENCE,
|
|
skills: DEMO_SKILLS,
|
|
npcDefs: [],
|
|
questDefs: DEMO_QUESTS,
|
|
})
|
|
|
|
// Test a two-part aura: Holy Fire (102)
|
|
engine.updateActiveAura(102, 20)
|
|
expect(engine.overlays.length).toBe(2)
|
|
const holyFireFront = engine.overlays.find(o => o.overlayName === 'holy_fire_front')!
|
|
const holyFireBack = engine.overlays.find(o => o.overlayName === 'holy_fire_back')!
|
|
expect(holyFireFront).toBeDefined()
|
|
expect(holyFireBack).toBeDefined()
|
|
expect(holyFireFront.preDraw).toBe(false)
|
|
expect(holyFireBack.preDraw).toBe(true)
|
|
|
|
// Test a full-ring aura: Holy Freeze (114)
|
|
engine.updateActiveAura(114, 20)
|
|
expect(engine.overlays.length).toBe(1)
|
|
const holyFreeze = engine.overlays[0]!
|
|
expect(holyFreeze.overlayName).toBe('holyfreeze')
|
|
expect(holyFreeze.preDraw).toBe(true)
|
|
|
|
// Test another two-part aura: Might (98)
|
|
engine.updateActiveAura(98, 20)
|
|
expect(engine.overlays.length).toBe(2)
|
|
const mightFront = engine.overlays.find(o => o.overlayName === 'might_front')!
|
|
const mightBack = engine.overlays.find(o => o.overlayName === 'might_back')!
|
|
expect(mightFront.preDraw).toBe(false)
|
|
expect(mightBack.preDraw).toBe(true)
|
|
})
|
|
})
|