Markdown Converter
Agent skill for markdown-converter
All non-presentation code must be easily portable to Godot. Everything in `/src/core/` uses interfaces and pure TypeScript with zero React/browser dependencies.
Sign in to like and favorite skills
All non-presentation code must be easily portable to Godot. Everything in
/src/core/ uses interfaces and pure TypeScript with zero React/browser dependencies.
ESLint enforces this: The
eslint.config.js has a rule blocking React imports in /src/core/. Violations will fail linting.
The game has two major systems being developed in phases:
| System | Status | Description |
|---|---|---|
| Battle System | š¢ Active | Auto-battler with formations, targeting AI, combat |
| Economy System | š” Dormant | Idle progression, upgrades, currency (future integration) |
Current focus: Battle System - unit spawning, formations, combat mechanics.
/src āāā core/ # Pure TypeScript - portable to Godot ā āāā battle/ # š¢ BattleEngine, combat AI, unit types ā āāā engine/ # š” GameEngine, Formulas (idle economy) ā āāā persistence/ # š” IPersistenceAdapter, SaveManager ā āāā physics/ # š” Vector2, Zoom (math utilities) ā āāā theme/ # š¢ Centralized colors (faction, UI, arena) ā āāā types/ # GameState, Upgrade interfaces ā āāā utils/ # BigNumber (break_infinity.js wrapper) āāā hooks/ # React integration layer ā āāā useBattle.ts # š¢ Battle engine bridge ā āāā useGameLoop.ts # requestAnimationFrame tick loop ā āāā useGameState.ts # š” Economy engine bridge āāā components/ # React + Tailwind UI ā āāā battle/ # š¢ BattleView, BattleCanvas ā āāā ui/ # š” StatsDisplay, UpgradeShop ā āāā App.tsx āāā data/ āāā upgrades.json # š” Upgrade definitions (economy - dormant) āāā units/ # š¢ Unit definitions (warrior, archer, knight) āāā abilities/ # š¢ Ability definitions (triggers, effects) āāā battle-upgrades/ # š¢ Battle upgrade definitions (stat mods) āāā battle/ # š¢ Battle data initializer /tests # Centralized test folder (mirrors src structure) āāā setup.ts # Vitest setup (jest-dom matchers) āāā core/ # Tests for /src/core/ modules āāā battle/ # BattleEngine, units, modifiers tests āāā engine/ # GameEngine, Formulas tests āāā utils/ # BigNumber tests
š¢ = Active development | š” = Dormant (future use)
All code in
/src/core/ must be clean, testable, and directly portable to Godot.
Each module handles ONE concern. Never create god-classes that mix multiple responsibilities:
// ā Good - focused modules SelectionManager.ts // Only selection state DragController.ts // Only drag logic FormationManager.ts // Only spawn positioning // ā Bad - god-class BattleCanvas.ts // Selection + dragging + rendering + input + spawning
Prefer pure functions that take state and return new state. This maps directly to Godot's functional patterns:
// ā Good - pure function, easy to port function selectUnit(state: SelectionState, unitId: string): SelectionState { return { selectedIds: [unitId] }; } // ā Avoid - hidden state makes porting harder class SelectionManager { private selectedIds: string[] = []; selectUnit(unitId: string) { this.selectedIds = [unitId]; } }
React components must be thin wrappers. All logic lives in
/src/core/:
// ā Component just bridges input to core const handleMouseDown = (e: MouseEvent) => { const pos = getMousePos(e); // Platform-specific (React) const unit = findUnitAtPosition(pos, units); // Core module (portable) onSelectUnit(selectUnit(state, unit?.id)); // Core module (portable) }; // ā Logic embedded in component - not portable const handleMouseDown = (e: MouseEvent) => { for (const unit of units) { if (Math.hypot(pos.x - unit.x, pos.y - unit.y) < unit.size) { setSelectedIds([unit.id]); // React-specific, can't port } } };
Core modules accept simple types (Vector2, arrays), not platform-specific events:
// Core module - works anywhere function findUnitAtPosition(pos: Vector2, units: Unit[]): Unit | null // React converts MouseEvent ā Vector2 // Godot converts InputEventMouse ā Vector2
Write core code imagining how it translates to GDScript:
| TypeScript | GDScript Equivalent |
|---|---|
| |
| |
| |
| |
| |
| |
Game content lives in JSON, loaded by registries:
/src/data/units/*.json - Unit definitions/src/data/abilities/*.json - Ability definitions/src/data/battle-upgrades/*.json - Upgrade definitionsThis maps to Godot's Resource system - JSON becomes
.tres files.
For platform-specific features (persistence, physics, rendering), define interfaces in core and implement outside:
/src/core/ āāā persistence/ ā āāā IPersistenceAdapter.ts # Interface - what Godot must implement ā āāā SaveManager.ts # Uses interface, not concrete class
// ā Good - interface defines contract interface IPersistenceAdapter { save(key: string, data: string): Promise<void>; load(key: string): Promise<string | null>; } // ā Good - core depends on interface class SaveManager { constructor(private adapter: IPersistenceAdapter) {} } // ā Bad - core depends on concrete implementation class SaveManager { private adapter = new LocalStorageAdapter(); // Hardcoded! }
Index files export interfaces, not implementations:
// ā Good - /src/core/persistence/index.ts export type { IPersistenceAdapter } from './IPersistenceAdapter'; export { SaveManager } from './SaveManager'; // ā Bad - exports platform-specific implementation export { LocalStorageAdapter } from './LocalStorageAdapter'; // Don't!
Platform implementations live OUTSIDE core:
/src/adapters/LocalStorageAdapter.ts - React/browser implementationFileAccessNever instantiate platform-specific classes inside core. Pass dependencies in:
// ā Good - dependency injected (in React hook or Godot scene) const adapter = new LocalStorageAdapter(); // Platform layer creates this const saveManager = new SaveManager(adapter); // Core receives interface // ā Bad - hardcoded inside core module class GameManager { private saveManager = new SaveManager(new LocalStorageAdapter()); }
This lets Godot swap implementations without touching core:
# Godot implementation var adapter = GodotFileAdapter.new() var save_manager = SaveManager.new(adapter)
Current approach: Godot-style Scene/Node Pattern
The battle system uses a Godot-compatible entity architecture:
UnitEntity, ProjectileEntity) handle their own behavior// Entity handles its own behavior class UnitEntity extends BaseEntity implements IEntity, IEventEmitter { update(delta: number): void { this.updateTargeting(); this.updateCombat(delta); this.updateMovement(delta); } } // World manages entities (like Godot main scene) class BattleWorld { update(delta: number): void { for (const entity of this.entities) { entity.update(delta); // Each entity updates itself } } }
Godot mapping:
| TypeScript | GDScript Equivalent |
|---|---|
| |
| |
| |
| |
| |
| Main scene or autoload |
Key classes:
/src/core/battle/ āāā IEntity.ts # Lifecycle interface + typed events āāā entities/ ā āāā EventEmitter.ts # Signal system implementation ā āāā BaseEntity.ts # Abstract base with common functionality ā āāā UnitEntity.ts # Unit with targeting, combat, movement ā āāā ProjectileEntity.ts # Projectile with movement, hit detection ā āāā BattleWorld.ts # Entity manager (SceneTree equivalent) ā āāā IBattleWorld.ts # Query interface for entities ā āāā index.ts
Using the Event System (Godot Signals):
Entities emit events that map to Godot signals. Subscribe to react to game events:
// Subscribe to entity events (like Godot's connect()) const unit = engine.getWorld().getUnitById('unit_1'); unit.on('damaged', (event) => { console.log(`${event.entity.id} took ${event.amount} damage`); }); unit.on('killed', (event) => { console.log(`${event.entity.id} was killed by ${event.killer?.id}`); }); unit.on('attacked', (event) => { console.log(`${event.entity.id} attacked ${event.target.id} for ${event.damage}`); }); // Unsubscribe (like Godot's disconnect()) unit.off('damaged', myHandler);
Events: See
IEntity.ts for all typed events (spawned, destroyed, damaged, killed, attacked, moved) and world events (entity_added, entity_removed). BattleStats uses world events to auto-subscribe to new units.
Creating New Entity Types: Extend
BaseEntity, implement update(delta), use this.emit() for events. See UnitEntity.ts and ProjectileEntity.ts for examples.
Migration Status: ā COMPLETE
The battle system is fully Godot-ready:
| Layer | Status | Notes |
|---|---|---|
| Core entities | ā Done | UnitEntity, ProjectileEntity, BattleWorld |
| Event system | ā Done | IEventEmitter with BattleStats consumer |
| Unit definitions | ā Done | JSON files in |
| Unit registry | ā Done | BattleEngine uses UnitRegistry |
| Stats tracking | ā Done | BattleStats subscribes to entity events |
| React rendering | ā Done | Uses render data adapter (intentional) |
For new code: Use
engine.getWorld() to access entities directly.
For Godot port: Translate entity classes to GDScript scenes. Ignore render data types in types.ts - they only exist for React rendering.
All game constants (spacing, speeds, combat values, etc.) are in
/src/core/battle/BattleConfig.ts. Never use magic numbers in code - always import from BattleConfig.
// ā Bad - magic numbers in code if (dist < 1) { ... } const checkDist = this.size * 3; if (dot > 0.3) { ... } // ā Good - constants from BattleConfig import { MIN_MOVE_DISTANCE, DIRECTION_CHECK_MULTIPLIER, PATH_DOT_THRESHOLD } from './BattleConfig'; if (dist < MIN_MOVE_DISTANCE) { ... } const checkDist = this.size * DIRECTION_CHECK_MULTIPLIER; if (dot > PATH_DOT_THRESHOLD) { ... }
When adding new gameplay values:
BattleConfig.ts with descriptive name and JSDoc commentThe event system maps to Godot signals. Follow these rules to avoid bugs:
Single Point of Event Emission:
// ā Bad - event emitted in multiple places class UnitEntity { update(delta: number) { if (this.health <= 0) this.emit('killed', {...}); // Duplicate! } takeDamage(amount: number) { this.health -= amount; if (this.health <= 0) this.emit('killed', {...}); // Original } } // ā Good - event emitted in ONE place only class UnitEntity { update(delta: number) { if (this._destroyed) return; // Skip if already dead } takeDamage(amount: number) { this.health -= amount; if (this.health <= 0) { this.markDestroyed(); this.emit('killed', {...}); // Only place this event is emitted } } }
Proper Entity Cleanup (Prevent Memory Leaks):
// ā Bad - removing entity without cleanup removeDestroyedEntities() { this.entities = this.entities.filter(e => !e.isDestroyed()); // Memory leak! Listeners not cleared } // ā Good - call destroy() before removal removeDestroyedEntities() { const destroyed = this.entities.filter(e => e.isDestroyed()); for (const entity of destroyed) { entity.destroy(); // Emits 'destroyed', clears listeners } this.entities = this.entities.filter(e => !e.isDestroyed()); }
Destroy Method Pattern:
// ā Correct destroy() implementation destroy(): void { if (this._cleanedUp) return; // Prevent double cleanup this._cleanedUp = true; this.emit('destroyed', { entity: this }); this.clearAllListeners(); // Prevent memory leaks }
Core Modules (Godot-portable):
| File | Purpose |
|---|---|
| Centralized constants - all magic numbers in one place |
| Battle orchestrator - delegates to BattleWorld |
| Entity system (Godot Scene/Node pattern) |
| Entity manager - lifecycle, queries, separation |
| Unit entity - targeting, combat, movement AI |
| Projectile entity - movement, hit detection |
| Abstract base with lifecycle + events |
| Entity lifecycle interface (init/update/destroy) |
| Pure selection state - select, toggle, selectAllOfType |
| Multi-unit drag with relative positioning, edge clamping |
| Formation templates and spawn positioning |
| Platform-agnostic input - hit detection |
| Box/marquee selection for multiple units |
| Pure functions for arena boundary enforcement |
| Combat shuffle for melee units |
| Render data types for React rendering only |
| Battle statistics via entity events |
| Unit definitions, instances, registry, factory |
| Stat modification system with stacking rules |
| Trigger-based abilities (on_kill, on_hit, etc.) |
| Battle upgrades with cost scaling |
| Centralized color palette (AC6-inspired industrial mech theme) |
| 2D math utilities for positioning |
React Layer (Thin wrappers - uses render data interface):
| File | Purpose | Migration |
|---|---|---|
| React bridge - manages state | Uses with |
| Main battle UI | Uses render data type |
| Canvas rendering | Uses render data type |
Note: React layer intentionally uses render data interface. Core entities are Godot-ready; React rendering doesn't need migration.
Core Modules (Godot-portable):
| File | Purpose |
|---|---|
| Interface - what Godot must implement |
| Idle engine - , |
| Pure math: , |
| Interface for save/load (swap for Godot) |
| State interfaces (GameState, UpgradeState) |
| Upgrade definition interface |
| break_infinity.js wrapper, |
React Layer (Thin wrappers):
| File | Purpose |
|---|---|
| React bridge - owns engine, handles save/load |
| Upgrade definitions (baseCost, costMultiplier, baseProduction) |
Cost = BaseCost * CostMultiplier^LevelProduction = BaseProduction * LevelGained = TotalProduction * Deltaā ļø IMPORTANT: Always use Docker for development, never run npm commands directly.
# Start dev server with hot reload on port 5177 docker compose -f docker-compose.dev.yml up # Or run in background docker compose -f docker-compose.dev.yml up -d
npm run dev # Start Vite dev server npm run build # TypeScript check + production build npm run preview # Preview production build
npm run validate # Full validation (typecheck + lint + test) npm run typecheck # TypeScript type checking only npm run lint # ESLint check npm run lint:fix # ESLint with auto-fix npm run format # Prettier format all files npm run format:check # Prettier check (CI mode)
npm run test # Vitest watch mode npm run test:run # Single test run npm run test:coverage # With coverage report npm run test:core # Only core tests (Godot-portable code)
# Development (hot reload on port 5177) docker compose -f docker-compose.dev.yml up # Production (static build on port 3000) docker compose up -d docker compose down docker compose build # Rebuild after dependency changes
Tests use Vitest with
jsdom environment for React components.
/tests/ folder, mirroring /src/ structuretests/core/**/*.test.ts) are pure TypeScript with no React dependenciesnpm run test:core to validate Godot-portable code in isolationimport { describe, it, expect } from 'vitest'; import { Decimal } from '../../../src/core/utils/BigNumber'; describe('MyFunction', () => { it('does something', () => { expect(result.eq(expected)).toBe(true); // Use .eq() for Decimal comparison }); });
/src/core/ - Never import React, hooks, or browser APIs (enforced by ESLint)delta (seconds) to time-based functionsDecimal from break_infinity.js for all game numbersSaveManager depends on IPersistenceAdaptertext-xs in Tailwind. Minimum is text-sm for readabilityUI_COLORS or other theme constants from src/core/theme/colors.tsUI_COLORS.textPrimary for main text, UI_COLORS.textSecondary for labels, and UI_COLORS.textMuted for disabled/hint text. Accent colors like UI_COLORS.accentPrimary can be used for emphasis but not body textBattleConfig.ts. Never use literal numbers like 0.3, 100, 2 for gameplay-affecting valueskilled, damaged, etc.) must be emitted from exactly ONE place in the code. Never emit the same event from multiple methodsdestroy() to emit the destroyed event and clear listeners. Never just filter entities out of arraysupdate() methods, check if (this._destroyed) return; early to skip processing dead entitieseslint.config.js with TypeScript and React rules.prettierrc (single quotes, semicolons, 100 char width)Uses
break_infinity.js for numbers up to 10^9e15. Key functions in src/core/utils/BigNumber.ts:
formatNumber(value: Decimal) - Human-readable (1.5M, 2.3B, 1.2e15)serializeDecimal() / deserializeDecimal() - For save/loadAll colors are in
src/core/theme/colors.ts - never use hardcoded hex values. Inspired by Armored Core 6 industrial mech aesthetic with dark panels, metallic surfaces, and high-contrast accent colors. Includes utility functions (hexToRgba, getUnitColor). For Godot, convert hex to Color.html() or Color8().
/src/core/ to GDScript/C# (syntactic translation)colors.ts to GDScript using Color.html() or Color8()IPersistenceAdapter using Godot's ConfigFile_process(delta) instead of requestAnimationFrameRun
npm run test:core to validate core code independently of React.
The codebase has a manual scaling system in
BattleConfig.ts using scaleValue(baseValue, arenaHeight). This exists because React/browser doesn't have built-in viewport scaling.
For Godot, you have two options:
| Option | Approach | Recommendation |
|---|---|---|
| A (Recommended) | Use Godot's built-in stretch mode | Set project base resolution to 600px height, use . Remove calls and use constants directly. |
| B | Keep manual scaling | Set . Use as-is. |
Why Option A is better:
If using Option A, the
scaleValue() function and REFERENCE_ARENA_HEIGHT constant can be removed during the Godot port. The BASE_ prefixed constants become the actual values used directly.
action_idle_save in localStorageGitHub Actions workflow (
.github/workflows/ci.yml) runs on push/PR to main:
Use Puppeteer to take screenshots and verify UI changes:
# Take a screenshot of the running app (saves to screenshots/ folder) node scripts/screenshot.cjs http://localhost:5177 screenshots/my-screenshot.png
The script (
scripts/screenshot.cjs):
Use screenshots to:
The dev server runs on port 5177 (configured in
vite.config.ts with strictPort: true).
Units can have both melee and ranged attacks. They automatically switch based on distance:
Design: Pure melee units can gain ranged attacks via upgrades/equipment later.
The battle arena uses standard screen coordinates:
When positioning units:
| Document | Description |
|---|---|
| Game design document - mechanics, balance, progression |
| Physics engine architecture and Godot migration guide (outdated - IPhysicsEngine removed) |