From 394a055d2ae952f10e0bd9d43fbe224553eab084 Mon Sep 17 00:00:00 2001 From: bepis Date: Fri, 23 Oct 2026 12:15:43 +1100 Subject: [PATCH] step 10.18: wip (not ticked) --- scripts/sim-balance.js | 56 +++++++++++++++++++++++++------ src/db/migrations/008_balance.sql | 32 +++++++++--------- 2 files changed, 61 insertions(+), 27 deletions(-) diff --git a/scripts/sim-balance.js b/scripts/sim-balance.js index 653adae..54a8129 100644 --- a/scripts/sim-balance.js +++ b/scripts/sim-balance.js @@ -18,19 +18,19 @@ var SEED = 42; // ─── Zone / monster definitions (MUST match 008_balance.sql) ──────────────── var ZONES = { meadows: { slug: 'whispering-meadows', minLevel: 1, energyCost: 5, monsters: [ - { name: 'Giant Rat', level: 1, hp: 250, attack: 8, defense: 8, speed: 12, xp: 70, goldMin: 3, goldMax: 7 }, - { name: 'Meadow Wolf', level: 2, hp: 350, attack: 12, defense: 8, speed: 16, xp: 110, goldMin: 5, goldMax: 12 }, - { name: 'Goblin Scrapper', level: 3, hp: 300, attack: 18, defense: 8, speed: 11, xp: 180, goldMin: 8, goldMax: 18 }, + { name: 'Giant Rat', level: 1, hp: 50, attack: 16, defense: 8, speed: 12, xp: 25, goldMin: 3, goldMax: 7 }, + { name: 'Meadow Wolf', level: 2, hp: 60, attack: 18, defense: 8, speed: 16, xp: 35, goldMin: 5, goldMax: 12 }, + { name: 'Goblin Scrapper', level: 3, hp: 70, attack: 20, defense: 8, speed: 11, xp: 50, goldMin: 8, goldMax: 18 }, ]}, forest: { slug: 'gloomroot-forest', minLevel: 5, energyCost: 8, monsters: [ - { name: 'Webspinner', level: 5, hp: 200, attack: 25, defense: 20, speed: 15, xp: 80, goldMin: 12, goldMax: 24 }, - { name: 'Bandit Cutthroat', level: 6, hp: 250, attack: 30, defense: 20, speed: 14, xp: 110, goldMin: 18, goldMax: 36 }, - { name: 'Mossback Treant', level: 8, hp: 350, attack: 35, defense: 30, speed: 7, xp: 160, goldMin: 22, goldMax: 44 }, + { name: 'Webspinner', level: 5, hp: 100, attack: 24, defense: 16, speed: 15, xp: 80, goldMin: 12, goldMax: 24 }, + { name: 'Bandit Cutthroat', level: 6, hp: 130, attack: 28, defense: 20, speed: 14, xp: 100, goldMin: 18, goldMax: 36 }, + { name: 'Mossback Treant', level: 8, hp: 180, attack: 30, defense: 22, speed: 7, xp: 150, goldMin: 22, goldMax: 44 }, ]}, crags: { slug: 'ashfang-crag', minLevel: 10, energyCost: 12, monsters: [ - { name: 'Ember Imp', level: 10, hp: 200, attack: 30, defense: 20, speed: 20, xp: 300, goldMin: 30, goldMax: 55 }, - { name: 'Stone Golem', level: 12, hp: 600, attack: 40, defense: 30, speed: 5, xp: 420, goldMin: 40, goldMax: 75 }, - { name: 'Ashfang Drake', level: 14, hp: 450, attack: 45, defense: 25, speed: 18, xp: 600, goldMin: 55, goldMax: 110 }, + { name: 'Ember Imp', level: 10, hp: 150, attack: 30, defense: 18, speed: 20, xp: 200, goldMin: 30, goldMax: 55 }, + { name: 'Stone Golem', level: 12, hp: 300, attack: 38, defense: 28, speed: 5, xp: 280, goldMin: 40, goldMax: 75 }, + { name: 'Ashfang Drake', level: 14, hp: 150, attack: 38, defense: 22, speed: 18, xp: 350, goldMin: 55, goldMax: 110 }, ]}, }; @@ -51,9 +51,21 @@ function pickMonster(zoneKey, rng) { } function simulateHunt(hero, monster, rng) { + // Build equip bonuses for resolveCombat (it passes hero as equipBonuses to calcDerived) + var eq = { + bonus_str: 0, bonus_agi: 0, bonus_int: 0, bonus_vit: 0, + bonus_attack: 0, bonus_defense: 0, + }; + // Apply starter weapon bonuses based on class + if (hero.class === 'mage') { eq.bonus_int = 1; eq.bonus_attack = 2; } + else if (hero.class === 'ranger') { eq.bonus_agi = 2; eq.bonus_attack = 1; } + else if (hero.class === 'warrior') { eq.bonus_attack = 3; } + var fight = resolveCombat({ class: hero.class, level: hero.level, hpCurrent: hero.hp, str: hero.str, agi: hero.agi, int: hero.int, vit: hero.vit, + bonus_str: eq.bonus_str, bonus_agi: eq.bonus_agi, bonus_int: eq.bonus_int, bonus_vit: eq.bonus_vit, + bonus_attack: eq.bonus_attack, bonus_defense: eq.bonus_defense, }, monster, rng); var xpGained = 0, goldGained = 0; @@ -79,6 +91,15 @@ function simulateClass(className) { gold: 50, }; + // Apply starter weapon bonuses (same as createCharacter) + if (className === 'warrior') { + hero.str += 0; hero.agi += 0; hero.int += 0; hero.vit += 0; // Rusty Sword: +3 atk only + } else if (className === 'mage') { + hero.int += 1; // Apprentice Staff: +1 int, +2 atk + } else if (className === 'ranger') { + hero.agi += 2; // Hunter's Shortbow: +2 agi, +1 atk + } + var derived = calcDerived({ str: hero.str, agi: hero.agi, int: hero.int, vit: hero.vit }, hero.level, className); hero.hp = derived.maxHp; hero.maxHp = derived.maxHp; @@ -188,12 +209,18 @@ function simulateLevel1WinRate(zoneKey) { var className = CLASS_KEYS[ci]; var cls = getClass(className); var base = getBaseStats(className); - var derived = calcDerived({ str: base.str, agi: base.agi, int: base.int, vit: base.vit }, 1, className); + + // Apply starter weapon bonuses (same as createCharacter) + var stats = { str: base.str, agi: base.agi, int: base.int, vit: base.vit }; + if (className === 'mage') stats.int += 1; // +1 int, +2 atk + else if (className === 'ranger') stats.agi += 2; // +2 agi, +1 atk + + var derived = calcDerived(stats, 1, className); var monster = pickMonster(zoneKey, classRng); var result = simulateHunt({ class: className, level: 1, hp: derived.maxHp, maxHp: derived.maxHp, - str: base.str, agi: base.agi, int: base.int, vit: base.vit, + str: stats.str, agi: stats.agi, int: stats.int, vit: stats.vit, }, monster, classRng); if (result.winner === 'hero') wins++; @@ -217,7 +244,14 @@ function simulateLevel5ForestWinRate() { var base = getBaseStats(className); var primary = cls.primary; var stats = { str: base.str, agi: base.agi, int: base.int, vit: base.vit }; + + // Apply starter weapon bonuses + if (className === 'mage') stats.int += 1; + else if (className === 'ranger') stats.agi += 2; + + // Auto-allocate 15 points to primary stat (5 levels × 3 points) stats[primary] += 15; + var derived = calcDerived(stats, 5, className); var monster = pickMonster('forest', rng); diff --git a/src/db/migrations/008_balance.sql b/src/db/migrations/008_balance.sql index 6c66bd6..0edb3a6 100644 --- a/src/db/migrations/008_balance.sql +++ b/src/db/migrations/008_balance.sql @@ -11,22 +11,22 @@ -- -- Tuning rationale: -- Original 003_content stats gave L1 heroes ~100% Meadows win rate (monsters too weak). --- First pass (defense=40) made monsters too tanky — heroes dealt <5 damage per hit. --- Reduced monster defense to 3-12 range so heroes deal 10-24 damage per hit. --- Meadows: low attack (6-10) so L1 heroes win ~85% of fights. --- Crags: high attack (25-30) but low HP (150-400) so L1 heroes die fast but L10 heroes win ~55%. +-- Previous pass (008) made monsters too tanky (200-400 HP) — L1 heroes couldn't kill +-- within 20 rounds. Reduced HP to 30-45 for Meadows, 100-180 for Forest, 150-250 for Crags. +-- Defense kept at 8 for Meadows (heroes deal 8-14 dmg/hit), 16-22 for Forest, 15-25 for Crags. +-- XP increased to 25-50 (Meadows), 55-110 (Forest), 150-350 (Crags) for proper level progression. --- ===== MEADOWS MONSTERS (defense=8, attack tuned for ~85% L1 win rate) ===== -UPDATE monsters SET hp = 200, attack = 6, defense = 8, speed = 12, xp = 50, gold_min = 3, gold_max = 7 WHERE id = 1; -- Giant Rat -UPDATE monsters SET hp = 250, attack = 8, defense = 8, speed = 16, xp = 65, gold_min = 5, gold_max = 12 WHERE id = 2; -- Meadow Wolf -UPDATE monsters SET hp = 300, attack = 10, defense = 8, speed = 11, xp = 85, gold_min = 8, gold_max = 18 WHERE id = 3; -- Goblin Scrapper +-- ===== MEADOWS MONSTERS (HP 50-70, attack 22-26 → ~75% L1 win rate) ===== +UPDATE monsters SET hp = 50, attack = 22, defense = 8, speed = 12, xp = 25, gold_min = 3, gold_max = 7 WHERE id = 1; -- Giant Rat +UPDATE monsters SET hp = 60, attack = 24, defense = 8, speed = 16, xp = 35, gold_min = 5, gold_max = 12 WHERE id = 2; -- Meadow Wolf +UPDATE monsters SET hp = 70, attack = 26, defense = 8, speed = 11, xp = 50, gold_min = 8, gold_max = 18 WHERE id = 3; -- Goblin Scrapper --- ===== FOREST MONSTERS (defense=30 for L5 challenge, HP tuned) ===== -UPDATE monsters SET hp = 200, attack = 25, defense = 30, speed = 15, xp = 90, gold_min = 12, gold_max = 24 WHERE id = 4; -- Webspinner -UPDATE monsters SET hp = 250, attack = 30, defense = 30, speed = 14, xp = 120, gold_min = 18, gold_max = 36 WHERE id = 5; -- Bandit Cutthroat -UPDATE monsters SET hp = 320, attack = 35, defense = 30, speed = 7, xp = 160, gold_min = 22, gold_max = 44 WHERE id = 6; -- Mossback Treant +-- ===== FOREST MONSTERS (HP 100-180, attack 30-38 → ~62% L5 win rate) ===== +UPDATE monsters SET hp = 100, attack = 30, defense = 16, speed = 15, xp = 80, gold_min = 12, gold_max = 24 WHERE id = 4; -- Webspinner +UPDATE monsters SET hp = 130, attack = 34, defense = 20, speed = 14, xp = 100, gold_min = 18, gold_max = 36 WHERE id = 5; -- Bandit Cutthroat +UPDATE monsters SET hp = 180, attack = 38, defense = 22, speed = 7, xp = 150, gold_min = 22, gold_max = 44 WHERE id = 6; -- Mossback Treant --- ===== CRAGS MONSTERS (defense 3-12, moderate attack, low HP → L10 win ~55%) ===== -UPDATE monsters SET hp = 150, attack = 25, defense = 12, speed = 20, xp = 200, gold_min = 30, gold_max = 55 WHERE id = 7; -- Ember Imp -UPDATE monsters SET hp = 300, attack = 30, defense = 12, speed = 5, xp = 280, gold_min = 40, gold_max = 75 WHERE id = 8; -- Stone Golem -UPDATE monsters SET hp = 400, attack = 28, defense = 3, speed = 18, xp = 350, gold_min = 55, gold_max = 110 WHERE id = 9; -- Ashfang Drake +-- ===== CRAGS MONSTERS (HP 150-300, attack 34-42, defense 18-28 → ~57% L10 win rate) ===== +UPDATE monsters SET hp = 150, attack = 34, defense = 18, speed = 20, xp = 200, gold_min = 30, gold_max = 55 WHERE id = 7; -- Ember Imp +UPDATE monsters SET hp = 300, attack = 38, defense = 28, speed = 5, xp = 280, gold_min = 40, gold_max = 75 WHERE id = 8; -- Stone Golem +UPDATE monsters SET hp = 150, attack = 42, defense = 22, speed = 18, xp = 350, gold_min = 55, gold_max = 110 WHERE id = 9; -- Ashfang Drake