docs

Terminology

Minerals (₥) are the game currency. A tick is one frame of the game loop. Grid cells are square tiles sized by GAME_CONFIG.gridSize. Plots are pre-defined building locations.

Spec

Two factions compete on a map with fixed building slots. Each faction places buildings into slots—bases generate income and spawn units, labs unlock advanced units. Spawned units fight auto-battler style: they move, target, and attack autonomously using a counter-type priority system. Destroy all enemy bases to win. Matches run 2-5 minutes.

Game flow

Click a plot to place your first Base. After a brief countdown, combat begins. The AI auto-places at the farthest available plot. Train units, expand with more Bases, destroy all enemy bases to win.

Buildings

Base generates income and spawns units. Lab unlocks Tank, Shade, and Druid.

Templates have cost and hpRatio (multiplied by buildingHpMultiplier). Derived stats export as BUILDINGS.

Units

Five unit types. Four combat units form a rock-paper-scissors counter system: Brut is basic melee Small. Flint is ranged Small with anti-air. Tank is slow Big with splash damage. Shade is a fast AntiBig assassin with bonus damage against Big types. Druid is a support unit that restores HP to wounded allies instead of attacking.

Each template defines cost (normalized), hpRatio and dpsRatio or hpsRatio (stat budget allocation, 0-1), attackSpeed (attacks per second), range (grid cells), speed, and type (small/big/antiBig/support). Optional fields: antiAir, splash, bonusVsLarge, heals.

Derived stats export as UNITS, colors as UNIT_COLORS. See window.UNITS for current values.

Combat

Units auto-target the nearest enemy, prioritizing favorable counter matchups. The counter triangle: Small beats Big/Splash, AntiBig beats Small, Big/Splash beats AntiBig. Druids target wounded allies instead, prioritizing the most damaged unit within range.

Target stickiness: units commit to their target and keep chasing until it dies or exceeds maxChaseRange (12 grid cells). This enables kiting, baiting, and pulling tactics.

Tank splash hits nearby enemies at reduced damage. Shade gets bonus damage vs Big (see bonusVsLarge). Druids cannot attack and will idle if no allies need healing.

Flocking behavior prevents stacking. Units push away from nearby allies while moving toward enemies. Tune with GAME_CONFIG.separationRadius and separationStrength.

Combat uses discrete attacks via combat.ts. Units create attack effects when in range; processEffects() ticks them and returns CombatEvents with position data. Events flow through handleCombatEvents() to combat log, visuals, and sounds.

Units drift perpendicular to their target while attacking, creating organic movement. Toggle with GAME_CONFIG.driftEnabled, tune with driftSpeed and driftFrequency.

AI

AI players prioritize building a Lab, expand when affordable, and spawn units on a timer. Unit selection uses weighted randomness favoring cheaper units. AI never spawns Druids. Runs for any faction not controlled by human (Watch mode runs AI on both sides).

Balance

hpPerCost, dpsPerCost, and hpsPerCost set HP, DPS, and HPS per normalized cost unit. Healing is deliberately less efficient than damage to prevent stalemates. mineralDisplay converts internal cost to displayed minerals. buildingHpMultiplier makes buildings tougher than units. See window.BALANCE for current values.

Each Base generates passive income. startingMinerals computes from startingCost × mineralDisplay. baseGenerationRate is income per second (incomePerMinute / 60).

Stats derive from normalized cost. Templates define cost and allocation ratios. Final stats compute automatically: HP = cost × hpPerCost × hpRatio, DPS = cost × dpsPerCost × dpsRatio, HPS = cost × hpsPerCost × hpsRatio.

Run bun test src/game/balance.test.ts to verify economy timing, cost efficiency across units, TTK ranges, counter effectiveness, and role differentiation. The test file prints TTK matrix, cost efficiency metrics, and economy timeline.

Balance CLI at src/game/balance-cli.ts for quick analysis:

Visual balance dashboard at /balance shows the same data in browser.

Visuals

visuals.ts renders ephemeral combat feedback without coupling to game logic.

Targeting lines show which units are attacking which targets — dashed lines drawn from active effects. Attack flashes are brief colored lines that appear when damage fires. Heals and splash damage have distinct colors.

Toggle via VISUAL_CONFIG.showTargetingLines and VISUAL_CONFIG.showAttackFlash. Tune flashDuration, lineAlpha, flashAlpha for feel.

Sounds

Hybrid system using @pixi/sound. Combat and game events are procedurally generated via Web Audio oscillators. UI sounds use sample pools with multiple variations for natural feel.

Procedural sounds: hit (square wave burst), death (sawtooth descent), explosion (low boom + noise), heal (ascending shimmer), spawn (sine tone), build (triangle wave), victory (sine tone), defeat (sawtooth), countdownTick, countdownGo.

Sample pools in /public/sfx/ui/ subfolders for click, hover, error, confirm. Each play picks a random sample from the pool.

Throttling prevents audio spam for high-frequency sounds. playSoundVaried() adds pitch variation.

Controls: toggleSound(), setMasterVolume(volume), window.SOUND_CONFIG. Edit src/game/sounds.ts for volume settings.

Controls

1-5 spawn units (Brut, Flint, Tank, Shade, Druid). 6-7 select buildings (Base, Lab) for placement. Tab cycles faction control: P1 (blue), Watch (AI vs AI), P2 (red). Space pauses, Escape deselects, arrow keys pan.

Click on map to set rally point - your units move there, fighting enemies in range (attack-move). Pulsing circle shows rally position. Use this to bait enemies, retreat, or stage flanks.

S opens scenario picker. R restarts after game over. G toggles target priority (units vs buildings).

Time controls: - or [ slows, + or ] speeds. F enables frame-step mode, . advances one frame.

Tank, Shade, and Druid require a Lab.

Architecture

Three layers, each with clear responsibility:

lib/ is the pure stdlib. effects.ts defines effect timing/payloads, combat-log.ts has event types and query filters. No game knowledge, no pixi, no side effects. Reusable across projects.

src/game/combat.ts bridges stdlib and game. Knows about Unit/Entity types, creates effects from unit stats, processes effects and returns combat events. Still no pixi, no sounds—just data in, data out.

src/game/combat-log-store.ts handles event logging and IndexedDB persistence. Browser-specific concerns live here.

src/game/entities.ts orchestrates the live game. Targeting, movement, then calls processEffects(), then handleCombatEvents() routes events to log/visuals/sounds. Side effects happen here.

src/game/simulation.ts is the headless twin. Same logic path, no graphics. Used for balance testing.

runSimulation(scenario, { seed, maxTicks }) loads a scenario and ticks until someone wins or times out. Seeded RNG makes results deterministic for regression testing. simulation.test.ts runs every scenario to completion and logs win rates across seeds.

Config

PixiJS engine, @pixi/sound for audio, Astro framework. Runs at 60 FPS via requestAnimationFrame with delta time tracking. Grid-based positioning (cell size in GAME_CONFIG.gridSize), fixed plot locations in BASE_POSITIONS.

Logging

WoW-style combat log. Types and queries in lib/combat-log.ts, storage in src/game/combat-log-store.ts. Events use short keys (t time, e event, s source, d dest, v value) and entity tuples [id, owner, type].

Composable filters (byType, bySourceOwner, byTimeRange) and aggregations (totalDamageByOwner, killsByOwner, avgLifespanByUnitType). Games persist to IndexedDB locally and sync to Cloudflare D1 remotely.

Remote storage uses a games table with queryable metadata (id, ts, scenario, duration, winner) plus a data column storing the full GameRecord JSON blob. API endpoints in src/pages/api/games/:

Client functions: syncToRemote(game), getRemoteGameIndex(), loadRemoteGame(id). Games auto-sync when finishGame() completes. Schema in schema.sql, D1 binding configured in wrangler.jsonc.

Debugging

The time device in the bottom-right shows game time, tick counter (T###), and speed multiplier. Transport buttons control playback: << slows, >|| plays/pauses, >> speeds, >| enables frame-step mode.

After load, window.simuu exposes config for live tweaking:

simuu.BALANCE // core balance constants
simuu.UNIT_TEMPLATES // unit stat templates
simuu.BUILDING_TEMPLATES
simuu.GAME_CONFIG // grid size, speeds, etc
simuu.UNITS // derived unit stats
simuu.gameState // live game state
simuu.VISUAL_CONFIG // targeting lines, attack flashes
simuu.SCENARIOS // scenario definitions
simuu.loadScenario('swarmVsTank') // load a test scenario

References