From 3deba288fb4408e485feb4b961d6a3ff5de9ee61 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 19 Feb 2026 12:36:00 +0000 Subject: [PATCH 1/2] Add event tracking, enemy stat factors, and factor sweep to RPG simulator Track hero death, level-up, and area change events by player input turn. Add ability to scale enemy HP and attack stats by configurable factors. Add sweep mode (0.8-1.2 in 0.05 steps) that reports events across 81 factor combinations, outputting sweep_results.json and sweep_summary.csv. https://claude.ai/code/session_01AZfaVGqp9oMkBk5F7d8SBL --- src/rpg/simulator.ts | 218 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 207 insertions(+), 11 deletions(-) diff --git a/src/rpg/simulator.ts b/src/rpg/simulator.ts index 022b917..4c6603d 100644 --- a/src/rpg/simulator.ts +++ b/src/rpg/simulator.ts @@ -1,6 +1,7 @@ // Cat Wizard - Battle Simulator // Text-based simulation using the same battle logic as BattleManager // Run with: bun run src/rpg/simulator.ts +// Run sweep with: bun run src/rpg/simulator.ts sweep import {Actor} from "./Actor.ts"; import {BattleManager} from "./BattleManager.ts"; @@ -15,7 +16,7 @@ import {Wolf} from "./enemies/Wolf.ts"; import {Treant} from "./enemies/Treant.ts"; import {Dummy} from "./enemies/Dummy.ts"; import {Container, Sprite} from "pixi.js"; -import {Wizard} from "./Wizard.ts"; +import {Wizard, getWizardLevel} from "./Wizard.ts"; import {Spider} from "./enemies/Spider.ts"; import {Slime} from "./enemies/Slime.ts"; import {Mushroom} from "./enemies/Mushroom.ts"; @@ -33,6 +34,39 @@ import {areas} from "./areas.ts"; // @ts-ignore import fs from 'fs/promises' +// ============================================================================ +// Types +// ============================================================================ + +type SimEvent = + | { turn: number; event: "death"; area: number; wave: number; xp: number } + | { turn: number; event: "levelUp"; newLevel: number; xp: number } + | { turn: number; event: "areaChange"; newArea: number; xp: number }; + +interface EnemyStatFactors { + enemyTypes: EnemyType[]; + hpFactor: number; + attackFactor: number; +} + +interface SimulationResult { + state: State; + events: SimEvent[]; +} + +interface SweepResult { + hpFactor: number; + attackFactor: number; + events: SimEvent[]; + finalXp: number; + finalArea: number; + finalPlayerTurns: number; +} + +// ============================================================================ +// Simulator Factories +// ============================================================================ + async function makeSimulatorEnemies(plan: EnemyType[]): Promise { const enemies = []; @@ -136,6 +170,22 @@ function fakeAnimations(actor: Actor) { }; } +function applyStatFactors( + enemies: Actor[], + plan: EnemyType[], + factors: EnemyStatFactors, +): void { + for (let i = 0; i < enemies.length; i++) { + const enemyType = plan[i]; + if (enemyType !== undefined && factors.enemyTypes.includes(enemyType)) { + const enemy = enemies[i]!; + enemy.health = Math.floor(enemy.health * factors.hpFactor); + enemy.maxHealth = Math.floor(enemy.maxHealth * factors.hpFactor); + enemy.attackPower = Math.floor(enemy.attackPower * factors.attackFactor); + } + } +} + // ============================================================================ // Main Simulation // ============================================================================ @@ -153,11 +203,22 @@ const stats: { hp: number; }[] = []; -async function runSimulation(startState: State, - planOverride?: EnemyType[], -): Promise { +async function runSimulation( + startState: State, + planOverride?: EnemyType[], + statFactors?: EnemyStatFactors, +): Promise { + const events: SimEvent[] = []; + const battleManager = new BattleManager(new Container(), startState.xp, startState.area); - battleManager._makeEnemies = (x) => makeSimulatorEnemies(planOverride ?? x); + battleManager._makeEnemies = async (x) => { + const plan = planOverride ?? x; + const enemies = await makeSimulatorEnemies(plan); + if (statFactors) { + applyStatFactors(enemies, plan, statFactors); + } + return enemies; + }; battleManager._makeWizard = makeSimulatorWizard; battleManager._makeBackground = async () => new Sprite(); @@ -173,6 +234,13 @@ async function runSimulation(startState: State, for (let i = 0; i < 300; i++) { const dead = await battleManager.doTurns(); if (dead) { + events.push({ + turn: i + startPlayerTurns, + event: "death", + area: battleManager.area, + wave: battleManager.wave, + xp: battleManager.xp, + }); console.log( "hero died. area: " + battleManager.area + @@ -186,9 +254,12 @@ async function runSimulation(startState: State, battleManager.xp, ); return { - xp: battleManager.xp, - area: battleManager.area, - playerTurns: i + startPlayerTurns + state: { + xp: battleManager.xp, + area: battleManager.area, + playerTurns: i + startPlayerTurns + }, + events, }; } @@ -198,11 +269,38 @@ async function runSimulation(startState: State, attackPower: battleManager.heroParty[0]!.attackPower, hp: battleManager.heroParty[0]!.health }) + + const levelBefore = getWizardLevel(battleManager.xp); + const areaBefore = battleManager.area; + await battleManager.correctAnswer(); + + const levelAfter = getWizardLevel(battleManager.xp); + if (levelAfter > levelBefore) { + events.push({ + turn: i + startPlayerTurns, + event: "levelUp", + newLevel: levelAfter, + xp: battleManager.xp, + }); + } + + if (battleManager.area > areaBefore) { + events.push({ + turn: i + startPlayerTurns, + event: "areaChange", + newArea: battleManager.area, + xp: battleManager.xp, + }); + } } throw new Error('Simulation did not end'); } +// ============================================================================ +// Standard Simulation (original behavior) +// ============================================================================ + async function runSimulations() { // for (let enemy of [EnemyType.Rat, EnemyType.DireRat, EnemyType.Goblin, EnemyType.Skeleton, EnemyType.Zombie, EnemyType.Bat, EnemyType.Wolf, EnemyType.Treant]) { // console.log('running simulation for ' + enemy); @@ -221,7 +319,8 @@ async function runSimulations() { let state: State = {xp: 0, area: 0, playerTurns: 0}; for (let i = 1; i <= 3; i++) { console.log('life: ' + i); - state = await runSimulation(state); + const result = await runSimulation(state); + state = result.state; } console.log('end ex: ' + state.xp + ', area: ' + state.area + ', turns: ' + state.playerTurns); } catch (e) { @@ -245,5 +344,102 @@ async function runSimulations() { await fs.writeFile('stats.csv', lines.join('\n')); } -// Run the simulation -await runSimulations(); +// ============================================================================ +// Factor Sweep +// ============================================================================ + +// Enemy types to apply factors to during sweep +const sweepEnemyTypes: EnemyType[] = [EnemyType.Rat, EnemyType.DireRat]; + +function generateFactors(start: number, end: number, step: number): number[] { + const factors: number[] = []; + for (let v = start; v <= end + step / 2; v += step) { + factors.push(Math.round(v * 100) / 100); + } + return factors; +} + +async function runSweep(enemyTypes: EnemyType[], lives: number): Promise { + // Pad areas to 1000 + const lastArea = areas[areas.length - 1]!; + for (let i = areas.length; i < 1000; i++) { + areas.push(lastArea); + } + + const hpFactors = generateFactors(0.8, 1.2, 0.05); + const attackFactors = generateFactors(0.8, 1.2, 0.05); + const results: SweepResult[] = []; + + for (const hpFactor of hpFactors) { + for (const attackFactor of attackFactors) { + console.log(`sweep: hpFactor=${hpFactor}, attackFactor=${attackFactor}`); + + const factors: EnemyStatFactors = { + enemyTypes, + hpFactor, + attackFactor, + }; + + let state: State = {xp: 0, area: 0, playerTurns: 0}; + const allEvents: SimEvent[] = []; + + for (let life = 1; life <= lives; life++) { + const result = await runSimulation(state, undefined, factors); + allEvents.push(...result.events); + state = result.state; + } + + results.push({ + hpFactor, + attackFactor, + events: allEvents, + finalXp: state.xp, + finalArea: state.area, + finalPlayerTurns: state.playerTurns, + }); + } + } + + // Write detailed JSON + const output = { + config: { + enemyTypes, + hpFactors, + attackFactors, + lives, + }, + results, + }; + await fs.writeFile('sweep_results.json', JSON.stringify(output, null, 2)); + + // Write summary CSV + const csvLines: string[] = []; + csvLines.push( + 'hpFactor,attackFactor,totalDeaths,finalArea,finalXp,finalPlayerTurns,firstDeathTurn,levelUpCount,areaChangeCount' + ); + for (const r of results) { + const deaths = r.events.filter((e) => e.event === "death"); + const levelUps = r.events.filter((e) => e.event === "levelUp"); + const areaChanges = r.events.filter((e) => e.event === "areaChange"); + const firstDeathTurn = deaths.length > 0 ? deaths[0]!.turn : -1; + csvLines.push( + `${r.hpFactor},${r.attackFactor},${deaths.length},${r.finalArea},${r.finalXp},${r.finalPlayerTurns},${firstDeathTurn},${levelUps.length},${areaChanges.length}` + ); + } + await fs.writeFile('sweep_summary.csv', csvLines.join('\n')); + + console.log(`Sweep complete: ${results.length} combinations.`); + console.log('Results written to sweep_results.json and sweep_summary.csv'); +} + +// ============================================================================ +// Entry Point +// ============================================================================ + +const mode = process.argv[2]; + +if (mode === "sweep") { + await runSweep(sweepEnemyTypes, 3); +} else { + await runSimulations(); +} From 4e5d332fe9e2fbc0d3e879b7edac4221cbd7c09e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damjan=20Ko=C5=A1ir?= Date: Thu, 19 Feb 2026 22:23:23 +0100 Subject: [PATCH 2/2] critical hit didn't work, balancing area 2 --- src/rpg/Wizard.ts | 460 ++++++++++++++++++------------------ src/rpg/enemies/Bat.ts | 10 +- src/rpg/enemies/Mushroom.ts | 10 +- src/rpg/enemies/Slime.ts | 10 +- src/rpg/enemies/Spider.ts | 10 +- src/rpg/simulator.ts | 39 ++- 6 files changed, 283 insertions(+), 256 deletions(-) diff --git a/src/rpg/Wizard.ts b/src/rpg/Wizard.ts index 2a9ea08..65d6e63 100644 --- a/src/rpg/Wizard.ts +++ b/src/rpg/Wizard.ts @@ -1,5 +1,5 @@ -import { Texture, Assets, Graphics } from "pixi.js"; -import { Actor } from "./Actor.ts"; +import {Assets, Graphics, Texture} from "pixi.js"; +import {Actor} from "./Actor.ts"; export class Wizard extends Actor { private isCastingMagic: boolean = false; @@ -24,16 +24,16 @@ export class Wizard extends Actor { private areaDuration: number = 0.6; // Magic missile state - private isCastingMissiles: boolean = false; - private resolveMissiles: (() => void) | null = null; - private missiles: { graphic: Graphics; progress: number; startDelay: number; offsetY: number; hit: boolean }[] = []; - private missileDuration: number = 0.35; - private missileTargetX: number = 0; - private missileTargetY: number = 0; - private missileBursts: { graphic: Graphics; progress: number }[] = []; - private missileBurstDuration: number = 0.15; - - // Level-up animation state + private isCastingMissiles: boolean = false; + private resolveMissiles: (() => void) | null = null; + private missiles: { graphic: Graphics; progress: number; startDelay: number; offsetY: number; hit: boolean }[] = []; + private missileDuration: number = 0.35; + private missileTargetX: number = 0; + private missileTargetY: number = 0; + private missileBursts: { graphic: Graphics; progress: number }[] = []; + private missileBurstDuration: number = 0.15; + + // Level-up animation state private isLevelingUp: boolean = false; private resolveLevelUp: (() => void) | null = null; private levelUpProgress: number = 0; @@ -127,7 +127,7 @@ export class Wizard extends Actor { override async attack(defenders: Actor[]): Promise<{ target: Actor; - damage: number; + damage: number; }[]> { let isCritical = false; await this.twitch(); @@ -139,7 +139,7 @@ export class Wizard extends Actor { let damage = this.attackPower; - if (level > 1 ) { + if (level > 1) { if (Math.random() < 0.25) { isCritical = true; damage *= 2; @@ -184,89 +184,89 @@ export class Wizard extends Actor { } override async magicMissileAttack(defender: Actor): Promise { - await this.twitch(); - await this.castMagicMissile(defender); - return this.attackPower; - } - - castMagicMissile(defender: Actor): Promise { - this.isCastingMissiles = true; - this.missileTargetX = defender.x - this.x; - this.missileTargetY = defender.y - this.y - 80; - this.magicLastTime = 0; - this.missiles = []; - this.missileBursts = []; - - const offsets = [-35, 0, 35]; - for (let i = 0; i < 3; i++) { - const missile = new Graphics(); - this.drawMissile(missile); - missile.zIndex = 1000; - missile.visible = false; - this.parent!.addChild(missile); - this.missiles.push({ - graphic: missile, - progress: 0, - startDelay: i * 0.07, - offsetY: offsets[i]!, - hit: false - }); - } - - return new Promise((resolve) => { - if (this.resolveMissiles) { - this.resolveMissiles(); - } - this.resolveMissiles = resolve; - }); - } - - private drawMissile(g: Graphics) { - const color = 0xcc44ff; - // Outer glow - g.circle(0, 0, 10); - g.fill({ color, alpha: 0.15 }); - // Middle glow - g.circle(0, 0, 6); - g.fill({ color, alpha: 0.3 }); - // Inner glow - g.circle(0, 0, 3.5); - g.fill({ color, alpha: 0.6 }); - // Core - g.circle(0, 0, 1.5); - g.fill({ color: 0xffffff, alpha: 0.95 }); - } - - private spawnMissileTrail(x: number, y: number) { - const trail = new Graphics(); - trail.circle(0, 0, 2); - trail.fill({ color: 0xcc44ff, alpha: 0.5 }); - trail.x = this.x + x; - trail.y = this.y + y; - trail.zIndex = 1000; - this.parent!.addChild(trail); - this.magicTrails.push({ graphic: trail, life: 0.2 }); - } - - castAreaMagic(): Promise { - this.isAreaCasting = true; - this.areaProgress = 0; - this.magicLastTime = 0; + await this.twitch(); + await this.castMagicMissile(defender); + return this.attackPower; + } + + castMagicMissile(defender: Actor): Promise { + this.isCastingMissiles = true; + this.missileTargetX = defender.x - this.x; + this.missileTargetY = defender.y - this.y - 80; + this.magicLastTime = 0; + this.missiles = []; + this.missileBursts = []; + + const offsets = [-35, 0, 35]; + for (let i = 0; i < 3; i++) { + const missile = new Graphics(); + this.drawMissile(missile); + missile.zIndex = 1000; + missile.visible = false; + this.parent!.addChild(missile); + this.missiles.push({ + graphic: missile, + progress: 0, + startDelay: i * 0.07, + offsetY: offsets[i]!, + hit: false + }); + } + + return new Promise((resolve) => { + if (this.resolveMissiles) { + this.resolveMissiles(); + } + this.resolveMissiles = resolve; + }); + } + + private drawMissile(g: Graphics) { + const color = 0xcc44ff; + // Outer glow + g.circle(0, 0, 10); + g.fill({color, alpha: 0.15}); + // Middle glow + g.circle(0, 0, 6); + g.fill({color, alpha: 0.3}); + // Inner glow + g.circle(0, 0, 3.5); + g.fill({color, alpha: 0.6}); + // Core + g.circle(0, 0, 1.5); + g.fill({color: 0xffffff, alpha: 0.95}); + } + + private spawnMissileTrail(x: number, y: number) { + const trail = new Graphics(); + trail.circle(0, 0, 2); + trail.fill({color: 0xcc44ff, alpha: 0.5}); + trail.x = this.x + x; + trail.y = this.y + y; + trail.zIndex = 1000; + this.parent!.addChild(trail); + this.magicTrails.push({graphic: trail, life: 0.2}); + } + + castAreaMagic(): Promise { + this.isAreaCasting = true; + this.areaProgress = 0; + this.magicLastTime = 0; const ring = new Graphics(); const color = 0xaa44ff; // Outer glow ring.circle(0, 0, 10); - ring.stroke({ color, alpha: 0.2, width: 12 }); + ring.stroke({color, alpha: 0.2, width: 12}); // Main ring ring.circle(0, 0, 10); - ring.stroke({ color, alpha: 0.5, width: 4 }); + ring.stroke({color, alpha: 0.5, width: 4}); // Inner bright ring ring.circle(0, 0, 10); - ring.stroke({ color: 0xddaaff, alpha: 0.7, width: 2 }); + ring.stroke({color: 0xddaaff, alpha: 0.7, width: 2}); // Core fill ring.circle(0, 0, 8); - ring.fill({ color, alpha: 0.1 }); + ring.fill({color, alpha: 0.1}); ring.x = this.x; ring.y = this.y - 80; @@ -312,7 +312,7 @@ export class Wizard extends Actor { private drawLightningBolt(g: Graphics, startX: number, startY: number, endX: number, endY: number) { g.clear(); const segments = 8; - const points: { x: number; y: number }[] = [{ x: startX, y: startY }]; + const points: { x: number; y: number }[] = [{x: startX, y: startY}]; const dx = endX - startX; const dy = endY - startY; const len = Math.sqrt(dx * dx + dy * dy); @@ -329,28 +329,28 @@ export class Wizard extends Actor { y: baseY + perpY * offset, }); } - points.push({ x: endX, y: endY }); + points.push({x: endX, y: endY}); // Outer glow g.moveTo(points[0]!.x, points[0]!.y); for (let i = 1; i < points.length; i++) { g.lineTo(points[i]!.x, points[i]!.y); } - g.stroke({ color: 0x4488ff, alpha: 0.3, width: 12 }); + g.stroke({color: 0x4488ff, alpha: 0.3, width: 12}); // Main bolt g.moveTo(points[0]!.x, points[0]!.y); for (let i = 1; i < points.length; i++) { g.lineTo(points[i]!.x, points[i]!.y); } - g.stroke({ color: 0x88ccff, alpha: 0.6, width: 4 }); + g.stroke({color: 0x88ccff, alpha: 0.6, width: 4}); // Bright core g.moveTo(points[0]!.x, points[0]!.y); for (let i = 1; i < points.length; i++) { g.lineTo(points[i]!.x, points[i]!.y); } - g.stroke({ color: 0xffffff, alpha: 0.9, width: 2 }); + g.stroke({color: 0xffffff, alpha: 0.9, width: 2}); } // --- Fire Bolt --- @@ -383,13 +383,13 @@ export class Wizard extends Actor { private drawFireBoltOrb(g: Graphics) { g.circle(0, 0, 25); - g.fill({ color: 0xff4400, alpha: 0.15 }); + g.fill({color: 0xff4400, alpha: 0.15}); g.circle(0, 0, 15); - g.fill({ color: 0xff6600, alpha: 0.3 }); + g.fill({color: 0xff6600, alpha: 0.3}); g.circle(0, 0, 10); - g.fill({ color: 0xffaa00, alpha: 0.6 }); + g.fill({color: 0xffaa00, alpha: 0.6}); g.circle(0, 0, 5); - g.fill({ color: 0xffffcc, alpha: 0.95 }); + g.fill({color: 0xffffcc, alpha: 0.95}); } private spawnFireTrail(x: number, y: number) { @@ -397,12 +397,12 @@ export class Wizard extends Actor { const colors = [0xff4400, 0xff6600, 0xffaa00]; const color = colors[Math.floor(Math.random() * colors.length)]!; trail.circle(0, 0, 3 + Math.random() * 3); - trail.fill({ color, alpha: 0.5 }); + trail.fill({color, alpha: 0.5}); trail.x = this.x + x; trail.y = this.y + y; trail.zIndex = 1000; this.parent!.addChild(trail); - this.magicTrails.push({ graphic: trail, life: 0.3 }); + this.magicTrails.push({graphic: trail, life: 0.3}); } // --- Frost Shard --- @@ -445,26 +445,26 @@ export class Wizard extends Actor { private drawFrostShard(g: Graphics) { const color = 0x44ddff; g.circle(0, 0, 8); - g.fill({ color, alpha: 0.15 }); + g.fill({color, alpha: 0.15}); g.moveTo(0, -6); g.lineTo(4, 0); g.lineTo(0, 6); g.lineTo(-4, 0); g.closePath(); - g.fill({ color, alpha: 0.6 }); + g.fill({color, alpha: 0.6}); g.circle(0, 0, 2); - g.fill({ color: 0xffffff, alpha: 0.9 }); + g.fill({color: 0xffffff, alpha: 0.9}); } private spawnFrostTrail(x: number, y: number) { const trail = new Graphics(); trail.circle(0, 0, 2); - trail.fill({ color: 0x44ddff, alpha: 0.5 }); + trail.fill({color: 0x44ddff, alpha: 0.5}); trail.x = this.x + x; trail.y = this.y + y; trail.zIndex = 1000; this.parent!.addChild(trail); - this.magicTrails.push({ graphic: trail, life: 0.2 }); + this.magicTrails.push({graphic: trail, life: 0.2}); } // --- Arcane Beam --- @@ -523,15 +523,15 @@ export class Wizard extends Actor { private drawMeteor(g: Graphics) { g.circle(0, 0, 30); - g.fill({ color: 0xff4400, alpha: 0.15 }); + g.fill({color: 0xff4400, alpha: 0.15}); g.circle(0, 0, 20); - g.fill({ color: 0xff6600, alpha: 0.3 }); + g.fill({color: 0xff6600, alpha: 0.3}); g.circle(0, 0, 12); - g.fill({ color: 0x884400, alpha: 0.8 }); + g.fill({color: 0x884400, alpha: 0.8}); g.circle(0, 0, 7); - g.fill({ color: 0xffaa00, alpha: 0.7 }); + g.fill({color: 0xffaa00, alpha: 0.7}); g.circle(0, 0, 3); - g.fill({ color: 0xffffcc, alpha: 0.95 }); + g.fill({color: 0xffffcc, alpha: 0.95}); } private spawnMeteorTrail(x: number, y: number) { @@ -539,15 +539,16 @@ export class Wizard extends Actor { const colors = [0xff4400, 0xff6600, 0xffaa00]; const color = colors[Math.floor(Math.random() * colors.length)]!; trail.circle(0, 0, 4 + Math.random() * 4); - trail.fill({ color, alpha: 0.5 }); + trail.fill({color, alpha: 0.5}); trail.x = x; trail.y = y; trail.zIndex = 1000; this.parent!.addChild(trail); - this.magicTrails.push({ graphic: trail, life: 0.35 }); + this.magicTrails.push({graphic: trail, life: 0.35}); } - levelUpStats(newXp:number) { + levelUpStats(newXp: number) { + this.xp += newXp; const xpFactor = 1 + newXp / 100; this.maxHealth = Math.floor(100 * xpFactor); this.health = this.maxHealth; @@ -556,13 +557,13 @@ export class Wizard extends Actor { this.speed = Math.floor(6 * xpFactor); } - async levelUp(newXp: number): Promise { - const newLevel = getWizardLevel(newXp); - const newTexturePath = `assets/wizard${newLevel}.png`; - this.levelUpNewTexture = await Assets.load(newTexturePath); + async levelUp(newXp: number): Promise { + const newLevel = getWizardLevel(newXp); + const newTexturePath = `assets/wizard${newLevel}.png`; + this.levelUpNewTexture = await Assets.load(newTexturePath); - // Update stats - this.levelUpStats(newXp); + // Update stats + this.levelUpStats(newXp); this.updateHealthBar(); this.isLevelingUp = true; @@ -582,7 +583,7 @@ export class Wizard extends Actor { // Create flash overlay const flash = new Graphics(); flash.rect(-400, -300, 800, 600); - flash.fill({ color: 0xffffff, alpha: 0 }); + flash.fill({color: 0xffffff, alpha: 0}); flash.zIndex = 9000; this.parent!.addChild(flash); this.levelUpFlash = flash; @@ -606,7 +607,7 @@ export class Wizard extends Actor { const color = colors[Math.floor(Math.random() * colors.length)]!; particle.circle(0, 0, size); - particle.fill({ color, alpha: 0.8 }); + particle.fill({color, alpha: 0.8}); particle.zIndex = 9001; let vx: number; @@ -663,24 +664,24 @@ export class Wizard extends Actor { // Outer glow this.levelUpGlow.circle(0, 0, glowRadius); - this.levelUpGlow.fill({ color: 0xffd700, alpha: glowAlpha * 0.3 }); + this.levelUpGlow.fill({color: 0xffd700, alpha: glowAlpha * 0.3}); // Middle glow this.levelUpGlow.circle(0, 0, glowRadius * 0.6); - this.levelUpGlow.fill({ color: 0xffea00, alpha: glowAlpha * 0.5 }); + this.levelUpGlow.fill({color: 0xffea00, alpha: glowAlpha * 0.5}); // Inner glow this.levelUpGlow.circle(0, 0, glowRadius * 0.3); - this.levelUpGlow.fill({ color: 0xffffff, alpha: glowAlpha * 0.7 }); + this.levelUpGlow.fill({color: 0xffffff, alpha: glowAlpha * 0.7}); } private spawnAreaTrail(x: number, y: number) { const trail = new Graphics(); trail.circle(0, 0, 3); - trail.fill({ color: 0xaa44ff, alpha: 0.5 }); + trail.fill({color: 0xaa44ff, alpha: 0.5}); trail.x = x; trail.y = y; trail.zIndex = 100; this.parent!.addChild(trail); - this.magicTrails.push({ graphic: trail, life: 0.3 }); + this.magicTrails.push({graphic: trail, life: 0.3}); } private drawOrb(orb: Graphics, isCritical: boolean) { @@ -689,16 +690,16 @@ export class Wizard extends Actor { // Outer glow orb.circle(0, 0, baseRadius * 2.5); - orb.fill({ color, alpha: 0.15 }); + orb.fill({color, alpha: 0.15}); // Middle glow orb.circle(0, 0, baseRadius * 1.5); - orb.fill({ color, alpha: 0.3 }); + orb.fill({color, alpha: 0.3}); // Inner glow orb.circle(0, 0, baseRadius); - orb.fill({ color, alpha: 0.6 }); + orb.fill({color, alpha: 0.6}); // Core orb.circle(0, 0, baseRadius * 0.5); - orb.fill({ color: 0xffffff, alpha: 0.95 }); + orb.fill({color: 0xffffff, alpha: 0.95}); } private spawnTrail(x: number, y: number) { @@ -706,12 +707,12 @@ export class Wizard extends Actor { const radius = this.magicIsCritical ? 4 : 3; const color = this.magicIsCritical ? 0xffdd44 : 0x44aaff; trail.circle(0, 0, radius); - trail.fill({ color, alpha: 0.5 }); + trail.fill({color, alpha: 0.5}); trail.x = this.x + x; trail.y = this.y + y; trail.zIndex = 1000; this.parent!.addChild(trail); - this.magicTrails.push({ graphic: trail, life: 0.3 }); + this.magicTrails.push({graphic: trail, life: 0.3}); } override update(time: number, isSine: boolean) { @@ -728,7 +729,7 @@ export class Wizard extends Actor { this.isCastingBeam || this.isCastingMeteor || this.isMeteorBursting || this.magicTrails.length > 0 || this.isLevelingUp || this.levelUpParticles.length > 0; - if (!hasWork) return; + if (!hasWork) return; if (this.magicLastTime === 0) { this.magicLastTime = time; @@ -796,7 +797,7 @@ export class Wizard extends Actor { const burst = new Graphics(); const color = this.magicIsCritical ? 0xffdd44 : 0x44aaff; burst.circle(0, 0, 1); - burst.fill({ color, alpha: 0.6 }); + burst.fill({color, alpha: 0.6}); burst.x = burstX; burst.y = burstY; this.magicBurst = burst; @@ -806,93 +807,93 @@ export class Wizard extends Actor { } // Magic missile animation - if (this.isCastingMissiles) { - let allHit = true; - for (const missile of this.missiles) { - if (missile.hit) continue; - - if (missile.startDelay > 0) { - missile.startDelay -= delta; - allHit = false; - continue; - } - - missile.graphic.visible = true; - missile.progress += delta; - const t = Math.min(missile.progress / this.missileDuration, 1); - - const eased = t * t; - - const startX = 80; - const startY = -160; - const endX = this.missileTargetX; - const endY = this.missileTargetY; - - const x = startX + (endX - startX) * eased; - const y = startY + (endY - startY) * eased + Math.sin(t * Math.PI) * missile.offsetY; - - missile.graphic.x = this.x + x; - missile.graphic.y = this.y + y; - - if (t > 0.05 && t < 0.9 && Math.random() < 0.4) { - this.spawnMissileTrail(x, y); - } - - if (t >= 1) { - missile.hit = true; - this.parent!.removeChild(missile.graphic); - missile.graphic.destroy(); - - // Small impact burst - const burst = new Graphics(); - burst.circle(0, 0, 1); - burst.fill({ color: 0xcc44ff, alpha: 0.6 }); - burst.x = this.x + endX; - burst.y = this.y + endY; - burst.zIndex = 1000; - this.parent!.addChild(burst); - this.missileBursts.push({ graphic: burst, progress: 0 }); - } else { - allHit = false; - } - } - - if (allHit && this.missileBursts.length === 0) { - this.isCastingMissiles = false; - this.magicLastTime = 0; - if (this.resolveMissiles) { - this.resolveMissiles(); - this.resolveMissiles = null; - } - } - } - - // Magic missile bursts - for (let i = this.missileBursts.length - 1; i >= 0; i--) { - const burst = this.missileBursts[i]!; - burst.progress += delta; - const t = Math.min(burst.progress / this.missileBurstDuration, 1); - burst.graphic.scale.set(15 * t); - burst.graphic.alpha = (1 - t) * 0.6; - - if (t >= 1) { - this.parent!.removeChild(burst.graphic); - burst.graphic.destroy(); - this.missileBursts.splice(i, 1); - } - } - - // Resolve missiles when all bursts done - if (this.isCastingMissiles && this.missiles.every(m => m.hit) && this.missileBursts.length === 0) { - this.isCastingMissiles = false; - this.magicLastTime = 0; - if (this.resolveMissiles) { - this.resolveMissiles(); - this.resolveMissiles = null; - } - } - - // Area attack ring animation + if (this.isCastingMissiles) { + let allHit = true; + for (const missile of this.missiles) { + if (missile.hit) continue; + + if (missile.startDelay > 0) { + missile.startDelay -= delta; + allHit = false; + continue; + } + + missile.graphic.visible = true; + missile.progress += delta; + const t = Math.min(missile.progress / this.missileDuration, 1); + + const eased = t * t; + + const startX = 80; + const startY = -160; + const endX = this.missileTargetX; + const endY = this.missileTargetY; + + const x = startX + (endX - startX) * eased; + const y = startY + (endY - startY) * eased + Math.sin(t * Math.PI) * missile.offsetY; + + missile.graphic.x = this.x + x; + missile.graphic.y = this.y + y; + + if (t > 0.05 && t < 0.9 && Math.random() < 0.4) { + this.spawnMissileTrail(x, y); + } + + if (t >= 1) { + missile.hit = true; + this.parent!.removeChild(missile.graphic); + missile.graphic.destroy(); + + // Small impact burst + const burst = new Graphics(); + burst.circle(0, 0, 1); + burst.fill({color: 0xcc44ff, alpha: 0.6}); + burst.x = this.x + endX; + burst.y = this.y + endY; + burst.zIndex = 1000; + this.parent!.addChild(burst); + this.missileBursts.push({graphic: burst, progress: 0}); + } else { + allHit = false; + } + } + + if (allHit && this.missileBursts.length === 0) { + this.isCastingMissiles = false; + this.magicLastTime = 0; + if (this.resolveMissiles) { + this.resolveMissiles(); + this.resolveMissiles = null; + } + } + } + + // Magic missile bursts + for (let i = this.missileBursts.length - 1; i >= 0; i--) { + const burst = this.missileBursts[i]!; + burst.progress += delta; + const t = Math.min(burst.progress / this.missileBurstDuration, 1); + burst.graphic.scale.set(15 * t); + burst.graphic.alpha = (1 - t) * 0.6; + + if (t >= 1) { + this.parent!.removeChild(burst.graphic); + burst.graphic.destroy(); + this.missileBursts.splice(i, 1); + } + } + + // Resolve missiles when all bursts done + if (this.isCastingMissiles && this.missiles.every(m => m.hit) && this.missileBursts.length === 0) { + this.isCastingMissiles = false; + this.magicLastTime = 0; + if (this.resolveMissiles) { + this.resolveMissiles(); + this.resolveMissiles = null; + } + } + + // Area attack ring animation if (this.isAreaCasting && this.areaRing) { this.areaProgress += delta; const t = Math.min(this.areaProgress / this.areaDuration, 1); @@ -948,7 +949,7 @@ export class Wizard extends Actor { this.lightningBurstProgress = 0; const burst = new Graphics(); burst.circle(0, 0, 1); - burst.fill({ color: 0x88ccff, alpha: 0.7 }); + burst.fill({color: 0x88ccff, alpha: 0.7}); burst.x = burstX; burst.y = burstY; burst.zIndex = 1000; @@ -1012,7 +1013,7 @@ export class Wizard extends Actor { this.fireBoltBurstProgress = 0; const burst = new Graphics(); burst.circle(0, 0, 1); - burst.fill({ color: 0xff6600, alpha: 0.7 }); + burst.fill({color: 0xff6600, alpha: 0.7}); burst.x = burstX; burst.y = burstY; burst.zIndex = 1000; @@ -1081,12 +1082,12 @@ export class Wizard extends Actor { const burst = new Graphics(); burst.circle(0, 0, 1); - burst.fill({ color: 0x44ddff, alpha: 0.6 }); + burst.fill({color: 0x44ddff, alpha: 0.6}); burst.x = this.x + endX; burst.y = this.y + endY; burst.zIndex = 1000; this.parent!.addChild(burst); - this.frostBursts.push({ graphic: burst, progress: 0 }); + this.frostBursts.push({graphic: burst, progress: 0}); } else { allHit = false; } @@ -1169,17 +1170,17 @@ export class Wizard extends Actor { // Outer glow this.beamGraphic.moveTo(startX, startY); this.beamGraphic.lineTo(beamEndX, beamEndY); - this.beamGraphic.stroke({ color: 0x9944ff, alpha: alpha * 0.3, width: width * 3 }); + this.beamGraphic.stroke({color: 0x9944ff, alpha: alpha * 0.3, width: width * 3}); // Main beam this.beamGraphic.moveTo(startX, startY); this.beamGraphic.lineTo(beamEndX, beamEndY); - this.beamGraphic.stroke({ color: 0xbb66ff, alpha: alpha * 0.6, width: width }); + this.beamGraphic.stroke({color: 0xbb66ff, alpha: alpha * 0.6, width: width}); // Core this.beamGraphic.moveTo(startX, startY); this.beamGraphic.lineTo(beamEndX, beamEndY); - this.beamGraphic.stroke({ color: 0xffffff, alpha: alpha * 0.9, width: Math.max(1, width * 0.3) }); + this.beamGraphic.stroke({color: 0xffffff, alpha: alpha * 0.9, width: Math.max(1, width * 0.3)}); // Beam particles if (t > 0.1 && t < 0.8 && Math.random() < 0.5) { @@ -1188,12 +1189,12 @@ export class Wizard extends Actor { const py = startY + (beamEndY - startY) * particleT; const trail = new Graphics(); trail.circle(0, 0, 2); - trail.fill({ color: 0xbb66ff, alpha: 0.5 }); + trail.fill({color: 0xbb66ff, alpha: 0.5}); trail.x = px + (Math.random() - 0.5) * 10; trail.y = py + (Math.random() - 0.5) * 10; trail.zIndex = 1000; this.parent!.addChild(trail); - this.magicTrails.push({ graphic: trail, life: 0.2 }); + this.magicTrails.push({graphic: trail, life: 0.2}); } if (t >= 1) { @@ -1244,7 +1245,7 @@ export class Wizard extends Actor { this.meteorBurstProgress = 0; const burst = new Graphics(); burst.circle(0, 0, 1); - burst.fill({ color: 0xff6600, alpha: 0.8 }); + burst.fill({color: 0xff6600, alpha: 0.8}); burst.x = burstX; burst.y = burstY; burst.zIndex = 1000; @@ -1390,6 +1391,7 @@ export class Wizard extends Actor { } } } + let wizardTexture: Texture; let wizardTextureLevel: number; @@ -1401,7 +1403,7 @@ export async function initWizard(xp: number) { } export function getWizardLevel(xp: number): number { - if (xp < 50) { + if (xp < 51) { return 1; } return Math.ceil((xp - 50) / 100) + 1; diff --git a/src/rpg/enemies/Bat.ts b/src/rpg/enemies/Bat.ts index f0202ab..2b8cb67 100644 --- a/src/rpg/enemies/Bat.ts +++ b/src/rpg/enemies/Bat.ts @@ -1,20 +1,22 @@ -import { Texture, Assets } from "pixi.js"; -import { Actor } from "../Actor.ts"; +import {Assets, Texture} from "pixi.js"; +import {Actor} from "../Actor.ts"; export class Bat extends Actor { constructor() { super({ texture: batTexture, textureScale: 0.25, - health: 14, - attackPower: 3, + health: 90, + attackPower: 15, defensePower: 0, speed: 8, xpDrop: 10, }); } } + let batTexture: Texture; + export async function initBat() { if (batTexture) return; batTexture = await Assets.load("assets/bat.png"); diff --git a/src/rpg/enemies/Mushroom.ts b/src/rpg/enemies/Mushroom.ts index 34d67e0..ed83f3e 100644 --- a/src/rpg/enemies/Mushroom.ts +++ b/src/rpg/enemies/Mushroom.ts @@ -1,13 +1,13 @@ -import { Texture, Assets } from "pixi.js"; -import { Actor } from "../Actor.ts"; +import {Assets, Texture} from "pixi.js"; +import {Actor} from "../Actor.ts"; export class Mushroom extends Actor { constructor() { super({ texture: mushroomTexture, textureScale: 0.25, - health: 16, - attackPower: 3, + health: 80, + attackPower: 15, defensePower: 1, speed: 2, xpDrop: 10, @@ -15,7 +15,9 @@ export class Mushroom extends Actor { this.sprite.tint = 0x8b7355; } } + let mushroomTexture: Texture; + export async function initMushroom() { if (mushroomTexture) return; mushroomTexture = await Assets.load("assets/slime.png"); diff --git a/src/rpg/enemies/Slime.ts b/src/rpg/enemies/Slime.ts index 1210ddb..bf030d8 100644 --- a/src/rpg/enemies/Slime.ts +++ b/src/rpg/enemies/Slime.ts @@ -1,20 +1,22 @@ -import { Texture, Assets } from "pixi.js"; -import { Actor } from "../Actor.ts"; +import {Assets, Texture} from "pixi.js"; +import {Actor} from "../Actor.ts"; export class Slime extends Actor { constructor() { super({ texture: slimeTexture, textureScale: 0.25, - health: 18, - attackPower: 3, + health: 90, + attackPower: 15, defensePower: 0, speed: 3, xpDrop: 10, }); } } + let slimeTexture: Texture; + export async function initSlime() { if (slimeTexture) return; slimeTexture = await Assets.load("assets/slime.png"); diff --git a/src/rpg/enemies/Spider.ts b/src/rpg/enemies/Spider.ts index ce9b281..3819beb 100644 --- a/src/rpg/enemies/Spider.ts +++ b/src/rpg/enemies/Spider.ts @@ -1,20 +1,22 @@ -import { Texture, Assets } from "pixi.js"; -import { Actor } from "../Actor.ts"; +import {Assets, Texture} from "pixi.js"; +import {Actor} from "../Actor.ts"; export class Spider extends Actor { constructor() { super({ texture: spiderTexture, textureScale: 0.5, - health: 20, - attackPower: 4, + health: 100, + attackPower: 20, defensePower: 1, speed: 5, xpDrop: 12, }); } } + let spiderTexture: Texture; + export async function initSpider() { if (spiderTexture) return; spiderTexture = await Assets.load("assets/spider.png"); diff --git a/src/rpg/simulator.ts b/src/rpg/simulator.ts index 4c6603d..8c501d1 100644 --- a/src/rpg/simulator.ts +++ b/src/rpg/simulator.ts @@ -16,7 +16,7 @@ import {Wolf} from "./enemies/Wolf.ts"; import {Treant} from "./enemies/Treant.ts"; import {Dummy} from "./enemies/Dummy.ts"; import {Container, Sprite} from "pixi.js"; -import {Wizard, getWizardLevel} from "./Wizard.ts"; +import {getWizardLevel, Wizard} from "./Wizard.ts"; import {Spider} from "./enemies/Spider.ts"; import {Slime} from "./enemies/Slime.ts"; import {Mushroom} from "./enemies/Mushroom.ts"; @@ -201,6 +201,7 @@ const stats: { wave: number; attackPower: number; hp: number; + dead: number; }[] = []; async function runSimulation( @@ -231,7 +232,7 @@ async function runSimulation( const startPlayerTurns = startState.playerTurns; - for (let i = 0; i < 300; i++) { + for (let i = 0; i < 1000; i++) { const dead = await battleManager.doTurns(); if (dead) { events.push({ @@ -267,7 +268,8 @@ async function runSimulation( area: battleManager.area, wave: battleManager.wave, attackPower: battleManager.heroParty[0]!.attackPower, - hp: battleManager.heroParty[0]!.health + hp: battleManager.heroParty[0]!.health, + dead: dead ? 1 : 0, }) const levelBefore = getWizardLevel(battleManager.xp); @@ -294,7 +296,11 @@ async function runSimulation( }); } } - throw new Error('Simulation did not end'); + console.error('Simulation did not end'); + return { + state: startState, + events: [] + } } // ============================================================================ @@ -314,12 +320,23 @@ async function runSimulations() { areas.push(lastArea) } + const enemiesInArea = new Set() + for (let wave of areas[2]!.waves) { + for (const enemy of wave) { + enemiesInArea.add(enemy as any); + } + } + try { console.log('real simulation'); let state: State = {xp: 0, area: 0, playerTurns: 0}; - for (let i = 1; i <= 3; i++) { - console.log('life: ' + i); - const result = await runSimulation(state); + for (let i = 1; i <= 5; i++) { + console.log('life: ' + i) + const result = await runSimulation(state, undefined, { + enemyTypes: Array.from(enemiesInArea), + attackFactor: 1, + hpFactor: 1, + }); state = result.state; } console.log('end ex: ' + state.xp + ', area: ' + state.area + ', turns: ' + state.playerTurns); @@ -335,11 +352,11 @@ async function runSimulations() { } - const lines : string[]= []; - lines.push('turn,area,wave,attackPower,hp') + const lines: string[] = []; + lines.push('turn,area,wave,attackPower,hp,dead') for (let i = 0; i < stats.length; i++) { const stat = stats[i]!; - lines.push(`${i},${stat.area},${stat.wave + prevAreaSum[stat.area]!},${stat.attackPower},${stat.hp}`); + lines.push(`${i},${stat.area},${stat.wave + prevAreaSum[stat.area]!},${stat.attackPower},${stat.hp},${stat.dead}`); } await fs.writeFile('stats.csv', lines.join('\n')); } @@ -439,7 +456,7 @@ async function runSweep(enemyTypes: EnemyType[], lives: number): Promise { const mode = process.argv[2]; if (mode === "sweep") { - await runSweep(sweepEnemyTypes, 3); + await runSweep(sweepEnemyTypes, 5); } else { await runSimulations(); }