- Add escalation effects module: creature speed/aggro/damage multipliers, reaction instability, environmental damage — all scale 0→1 - Add getCrisisPlayerDamage/getCrisisTint to crisis module - Integrate escalation effects into GameScene (env damage at high entropy) - Dynamic crisis overlay tint that intensifies with progress - Phase indicator shows entropy %, aggression multiplier, crisis status - 14 new tests for escalation + crisis damage/tint (349 total) Co-authored-by: Cursor <cursoragent@cursor.com>
148 lines
5.0 KiB
TypeScript
148 lines
5.0 KiB
TypeScript
import { describe, it, expect, beforeEach } from 'vitest';
|
|
import type { RunState } from '../src/run/types';
|
|
import { RunPhase, ESCALATION_RATE } from '../src/run/types';
|
|
import { createRunState, advancePhase, updateEscalation } from '../src/run/state';
|
|
import {
|
|
getEscalationEffects,
|
|
type EscalationEffects,
|
|
} from '../src/run/escalation';
|
|
import {
|
|
createCrisisState,
|
|
applyCrisisDamage,
|
|
attemptNeutralize,
|
|
isCrisisResolved,
|
|
getCrisisPlayerDamage,
|
|
getCrisisTint,
|
|
CHEMICAL_PLAGUE,
|
|
} from '../src/run/crisis';
|
|
|
|
// ─── Escalation Effects ──────────────────────────────────────────
|
|
|
|
describe('Escalation Effects', () => {
|
|
it('at 0 escalation, all multipliers are 1.0', () => {
|
|
const fx = getEscalationEffects(0);
|
|
expect(fx.creatureSpeedMultiplier).toBe(1.0);
|
|
expect(fx.creatureAggroRange).toBe(1.0);
|
|
expect(fx.creatureAttackMultiplier).toBe(1.0);
|
|
expect(fx.reactionInstability).toBe(0);
|
|
expect(fx.environmentalDamage).toBe(0);
|
|
});
|
|
|
|
it('at 0.5 escalation, effects are moderate', () => {
|
|
const fx = getEscalationEffects(0.5);
|
|
expect(fx.creatureSpeedMultiplier).toBeGreaterThan(1.0);
|
|
expect(fx.creatureSpeedMultiplier).toBeLessThan(2.0);
|
|
expect(fx.creatureAggroRange).toBeGreaterThan(1.0);
|
|
expect(fx.creatureAttackMultiplier).toBeGreaterThan(1.0);
|
|
expect(fx.reactionInstability).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('at 1.0 escalation, effects are maximal', () => {
|
|
const fx = getEscalationEffects(1.0);
|
|
expect(fx.creatureSpeedMultiplier).toBe(1.5);
|
|
expect(fx.creatureAggroRange).toBe(1.8);
|
|
expect(fx.creatureAttackMultiplier).toBe(1.6);
|
|
expect(fx.reactionInstability).toBe(0.3);
|
|
expect(fx.environmentalDamage).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('escalation clamps values correctly', () => {
|
|
const fx = getEscalationEffects(2.0); // over max
|
|
expect(fx.creatureSpeedMultiplier).toBe(1.5);
|
|
|
|
const fxNeg = getEscalationEffects(-1.0); // under min
|
|
expect(fxNeg.creatureSpeedMultiplier).toBe(1.0);
|
|
});
|
|
});
|
|
|
|
// ─── Crisis Damage ───────────────────────────────────────────────
|
|
|
|
describe('Crisis Player Damage', () => {
|
|
it('no damage below 30% progress', () => {
|
|
const crisis = createCrisisState(CHEMICAL_PLAGUE);
|
|
crisis.progress = 0.2;
|
|
expect(getCrisisPlayerDamage(crisis, 1000)).toBe(0);
|
|
});
|
|
|
|
it('damage increases with progress above 30%', () => {
|
|
const crisis = createCrisisState(CHEMICAL_PLAGUE);
|
|
crisis.progress = 0.5;
|
|
const dmg = getCrisisPlayerDamage(crisis, 1000);
|
|
expect(dmg).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('damage scales with delta time', () => {
|
|
const crisis = createCrisisState(CHEMICAL_PLAGUE);
|
|
crisis.progress = 0.8;
|
|
const dmg1 = getCrisisPlayerDamage(crisis, 1000);
|
|
const dmg2 = getCrisisPlayerDamage(crisis, 2000);
|
|
expect(dmg2).toBeCloseTo(dmg1 * 2, 1);
|
|
});
|
|
|
|
it('no damage when resolved', () => {
|
|
const crisis = createCrisisState(CHEMICAL_PLAGUE);
|
|
crisis.progress = 0.8;
|
|
crisis.resolved = true;
|
|
expect(getCrisisPlayerDamage(crisis, 1000)).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('Crisis Visual Tint', () => {
|
|
it('returns no tint at 0 progress', () => {
|
|
const crisis = createCrisisState(CHEMICAL_PLAGUE);
|
|
crisis.progress = 0;
|
|
const tint = getCrisisTint(crisis);
|
|
expect(tint.alpha).toBe(0);
|
|
});
|
|
|
|
it('returns tint at high progress', () => {
|
|
const crisis = createCrisisState(CHEMICAL_PLAGUE);
|
|
crisis.progress = 0.8;
|
|
const tint = getCrisisTint(crisis);
|
|
expect(tint.alpha).toBeGreaterThan(0);
|
|
expect(tint.color).toBeDefined();
|
|
});
|
|
|
|
it('returns no tint when resolved', () => {
|
|
const crisis = createCrisisState(CHEMICAL_PLAGUE);
|
|
crisis.progress = 0.8;
|
|
crisis.resolved = true;
|
|
const tint = getCrisisTint(crisis);
|
|
expect(tint.alpha).toBe(0);
|
|
});
|
|
});
|
|
|
|
// ─── Escalation-Phase Integration ────────────────────────────────
|
|
|
|
describe('Escalation Integration', () => {
|
|
let state: RunState;
|
|
|
|
beforeEach(() => {
|
|
state = createRunState(1, 'alchemist');
|
|
state.phase = RunPhase.Escalation;
|
|
});
|
|
|
|
it('escalation grows steadily over time', () => {
|
|
// Simulate 30 seconds
|
|
for (let i = 0; i < 30; i++) {
|
|
updateEscalation(state, 1000);
|
|
}
|
|
expect(state.escalation).toBeCloseTo(ESCALATION_RATE * 30, 2);
|
|
});
|
|
|
|
it('effects scale linearly with escalation', () => {
|
|
updateEscalation(state, 100_000); // ~100 seconds
|
|
const fx = getEscalationEffects(state.escalation);
|
|
expect(fx.creatureSpeedMultiplier).toBeGreaterThan(1.0);
|
|
expect(fx.creatureAggroRange).toBeGreaterThan(1.0);
|
|
});
|
|
|
|
it('full escalation cycle reaches crisis threshold', () => {
|
|
// Simulate until threshold
|
|
for (let i = 0; i < 200; i++) {
|
|
updateEscalation(state, 1000);
|
|
}
|
|
expect(state.escalation).toBeGreaterThanOrEqual(CHEMICAL_PLAGUE.triggerThreshold);
|
|
});
|
|
});
|