clibuddy 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +60 -0
  3. package/dist/adventure/adventureUI.d.ts +24 -0
  4. package/dist/adventure/adventureUI.js +290 -0
  5. package/dist/adventure/adventures.d.ts +4 -0
  6. package/dist/adventure/adventures.js +206 -0
  7. package/dist/adventure/biomes.d.ts +30 -0
  8. package/dist/adventure/biomes.js +80 -0
  9. package/dist/adventure/combat/combat.d.ts +14 -0
  10. package/dist/adventure/combat/combat.js +638 -0
  11. package/dist/adventure/combat/combatUI.d.ts +5 -0
  12. package/dist/adventure/combat/combatUI.js +116 -0
  13. package/dist/adventure/combat/conditions.d.ts +20 -0
  14. package/dist/adventure/combat/conditions.js +111 -0
  15. package/dist/adventure/combat/enemies.d.ts +4 -0
  16. package/dist/adventure/combat/enemies.js +430 -0
  17. package/dist/adventure/combat/gear.d.ts +3 -0
  18. package/dist/adventure/combat/gear.js +199 -0
  19. package/dist/adventure/combat/skills.d.ts +6 -0
  20. package/dist/adventure/combat/skills.js +197 -0
  21. package/dist/adventure/combat.d.ts +31 -0
  22. package/dist/adventure/combat.js +732 -0
  23. package/dist/adventure/combatUI.d.ts +5 -0
  24. package/dist/adventure/combatUI.js +116 -0
  25. package/dist/adventure/endless.d.ts +18 -0
  26. package/dist/adventure/endless.js +154 -0
  27. package/dist/adventure/enemies.d.ts +4 -0
  28. package/dist/adventure/enemies.js +320 -0
  29. package/dist/adventure/engine.d.ts +20 -0
  30. package/dist/adventure/engine.js +137 -0
  31. package/dist/adventure/gear.d.ts +3 -0
  32. package/dist/adventure/gear.js +149 -0
  33. package/dist/adventure/generation/biomes.d.ts +30 -0
  34. package/dist/adventure/generation/biomes.js +102 -0
  35. package/dist/adventure/generation/endless.d.ts +18 -0
  36. package/dist/adventure/generation/endless.js +154 -0
  37. package/dist/adventure/generation/generator.d.ts +9 -0
  38. package/dist/adventure/generation/generator.js +245 -0
  39. package/dist/adventure/generation/templates.d.ts +25 -0
  40. package/dist/adventure/generation/templates.js +228 -0
  41. package/dist/adventure/generator.d.ts +9 -0
  42. package/dist/adventure/generator.js +245 -0
  43. package/dist/adventure/skills.d.ts +6 -0
  44. package/dist/adventure/skills.js +197 -0
  45. package/dist/adventure/templates.d.ts +25 -0
  46. package/dist/adventure/templates.js +228 -0
  47. package/dist/adventure/types.d.ts +236 -0
  48. package/dist/adventure/types.js +97 -0
  49. package/dist/app/state.d.ts +49 -0
  50. package/dist/app/state.js +51 -0
  51. package/dist/buddy/activities.d.ts +16 -0
  52. package/dist/buddy/activities.js +90 -0
  53. package/dist/buddy/decay.d.ts +3 -0
  54. package/dist/buddy/decay.js +45 -0
  55. package/dist/buddy/leveling.d.ts +11 -0
  56. package/dist/buddy/leveling.js +25 -0
  57. package/dist/buddy/roll.d.ts +4 -0
  58. package/dist/buddy/roll.js +61 -0
  59. package/dist/buddy/species.d.ts +4 -0
  60. package/dist/buddy/species.js +592 -0
  61. package/dist/buddy/titles.d.ts +17 -0
  62. package/dist/buddy/titles.js +89 -0
  63. package/dist/buddy/types.d.ts +92 -0
  64. package/dist/buddy/types.js +21 -0
  65. package/dist/commands/actions.d.ts +2 -0
  66. package/dist/commands/actions.js +141 -0
  67. package/dist/commands/admin.d.ts +2 -0
  68. package/dist/commands/admin.js +202 -0
  69. package/dist/commands/registry.d.ts +25 -0
  70. package/dist/commands/registry.js +31 -0
  71. package/dist/commands/social.d.ts +2 -0
  72. package/dist/commands/social.js +92 -0
  73. package/dist/dialogue/engine.d.ts +7 -0
  74. package/dist/dialogue/engine.js +68 -0
  75. package/dist/dialogue/lines.d.ts +26 -0
  76. package/dist/dialogue/lines.js +294 -0
  77. package/dist/events/engine.d.ts +20 -0
  78. package/dist/events/engine.js +51 -0
  79. package/dist/events/events.d.ts +13 -0
  80. package/dist/events/events.js +149 -0
  81. package/dist/index.d.ts +10 -0
  82. package/dist/index.js +1665 -0
  83. package/dist/inventory/finding.d.ts +11 -0
  84. package/dist/inventory/finding.js +99 -0
  85. package/dist/inventory/items.d.ts +31 -0
  86. package/dist/inventory/items.js +63 -0
  87. package/dist/minigames/copycat.d.ts +2 -0
  88. package/dist/minigames/copycat.js +153 -0
  89. package/dist/minigames/fetch.d.ts +2 -0
  90. package/dist/minigames/fetch.js +179 -0
  91. package/dist/minigames/moodmatch.d.ts +2 -0
  92. package/dist/minigames/moodmatch.js +144 -0
  93. package/dist/minigames/quickpaws.d.ts +2 -0
  94. package/dist/minigames/quickpaws.js +142 -0
  95. package/dist/minigames/registry.d.ts +5 -0
  96. package/dist/minigames/registry.js +16 -0
  97. package/dist/minigames/rpsplus.d.ts +2 -0
  98. package/dist/minigames/rpsplus.js +168 -0
  99. package/dist/minigames/treasurehunt.d.ts +2 -0
  100. package/dist/minigames/treasurehunt.js +146 -0
  101. package/dist/minigames/types.d.ts +30 -0
  102. package/dist/minigames/types.js +69 -0
  103. package/dist/rendering/commandPalette.d.ts +16 -0
  104. package/dist/rendering/commandPalette.js +77 -0
  105. package/dist/rendering/display.d.ts +9 -0
  106. package/dist/rendering/display.js +231 -0
  107. package/dist/rendering/inventoryUI.d.ts +14 -0
  108. package/dist/rendering/inventoryUI.js +99 -0
  109. package/dist/rendering/items.d.ts +7 -0
  110. package/dist/rendering/items.js +34 -0
  111. package/dist/rendering/listUtils.d.ts +3 -0
  112. package/dist/rendering/listUtils.js +24 -0
  113. package/dist/rendering/minigameUI.d.ts +11 -0
  114. package/dist/rendering/minigameUI.js +37 -0
  115. package/dist/rendering/overlayUI.d.ts +24 -0
  116. package/dist/rendering/overlayUI.js +184 -0
  117. package/dist/rendering/scene.d.ts +8 -0
  118. package/dist/rendering/scene.js +87 -0
  119. package/dist/rendering/screen.d.ts +43 -0
  120. package/dist/rendering/screen.js +97 -0
  121. package/dist/sound/sound.d.ts +11 -0
  122. package/dist/sound/sound.js +55 -0
  123. package/dist/state/save.d.ts +5 -0
  124. package/dist/state/save.js +100 -0
  125. package/dist/state/settings.d.ts +7 -0
  126. package/dist/state/settings.js +81 -0
  127. package/dist/story/mainStory.d.ts +4 -0
  128. package/dist/story/mainStory.js +3111 -0
  129. package/dist/story/npcs.d.ts +17 -0
  130. package/dist/story/npcs.js +137 -0
  131. package/dist/story/progress.d.ts +26 -0
  132. package/dist/story/progress.js +168 -0
  133. package/dist/story/seasonal.d.ts +6 -0
  134. package/dist/story/seasonal.js +96 -0
  135. package/dist/story/shop.d.ts +7 -0
  136. package/dist/story/shop.js +26 -0
  137. package/dist/story/sideQuests.d.ts +4 -0
  138. package/dist/story/sideQuests.js +151 -0
  139. package/dist/story/types.d.ts +61 -0
  140. package/dist/story/types.js +36 -0
  141. package/dist/updates.d.ts +23 -0
  142. package/dist/updates.js +142 -0
  143. package/package.json +53 -0
@@ -0,0 +1,732 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.deriveCombatStats = deriveCombatStats;
4
+ exports.deriveEnemyStats = deriveEnemyStats;
5
+ exports.startCombat = startCombat;
6
+ exports.playerAttack = playerAttack;
7
+ exports.playerDefend = playerDefend;
8
+ exports.playerFlee = playerFlee;
9
+ exports.playerUseSkill = playerUseSkill;
10
+ exports.enemyTurn = enemyTurn;
11
+ exports.processConditions = processConditions;
12
+ exports.applyShield = applyShield;
13
+ exports.hasCondition = hasCondition;
14
+ exports.addCondition = addCondition;
15
+ exports.getConditionStatMod = getConditionStatMod;
16
+ exports.tickCombat = tickCombat;
17
+ const species_1 = require("../buddy/species");
18
+ const items_1 = require("../inventory/items");
19
+ const types_1 = require("./types");
20
+ const lines_1 = require("../dialogue/lines");
21
+ // ─── Species Dodge Chances ───────────────────────────────────
22
+ const SPECIES_DODGE = {
23
+ cat: 0.20,
24
+ fox: 0.15,
25
+ rabbit: 0.10,
26
+ owl: 0.08,
27
+ frog: 0.05,
28
+ phoenix: 0.05,
29
+ dragon: 0.03,
30
+ };
31
+ function pickCombatLine(pool, speciesId) {
32
+ const lines = pool[speciesId];
33
+ if (!lines || lines.length === 0)
34
+ return "";
35
+ return lines[Math.floor(Math.random() * lines.length)];
36
+ }
37
+ const COMBOS = [
38
+ { skill1: "focus", skill2: "pounce", name: "Focused Pounce", damageMultiplier: 2.5 },
39
+ { skill1: "focus", skill2: "fire_breath", name: "Focused Breath", damageMultiplier: 2.8, extraEffect: "burn" },
40
+ { skill1: "shadow_step", skill2: "pounce", name: "Shadow Ambush", damageMultiplier: 3.0 },
41
+ { skill1: "scale_shield", skill2: "fire_breath", name: "Dragon Fury", damageMultiplier: 2.5, extraEffect: "burn" },
42
+ { skill1: "keen_eye", skill2: "piercing_gaze", name: "Owl Strike", damageMultiplier: 3.0 },
43
+ { skill1: "quick_hop", skill2: "earthquake", name: "Nature's Wrath", damageMultiplier: 2.0, extraEffect: "stun" },
44
+ { skill1: "focus", skill2: "flame_burst", name: "Focused Flame", damageMultiplier: 2.4, extraEffect: "burn" },
45
+ ];
46
+ function checkCombo(lastSkill, currentSkill) {
47
+ if (!lastSkill)
48
+ return null;
49
+ return COMBOS.find((c) => c.skill1 === lastSkill && c.skill2 === currentSkill) ?? null;
50
+ }
51
+ // ─── Combat Stat Derivation ─────────────────────────────────
52
+ /** Collect all equipped gear items */
53
+ function getEquippedGear(state) {
54
+ return [
55
+ state.adventureStats.equippedWeapon ? (0, items_1.getItem)(state.adventureStats.equippedWeapon) ?? null : null,
56
+ state.adventureStats.equippedArmor ? (0, items_1.getItem)(state.adventureStats.equippedArmor) ?? null : null,
57
+ state.adventureStats.equippedAccessory ? (0, items_1.getItem)(state.adventureStats.equippedAccessory) ?? null : null,
58
+ ];
59
+ }
60
+ /** Sum a numeric gear property across all equipped items */
61
+ function sumGearProp(gear, prop) {
62
+ let total = 0;
63
+ for (const item of gear) {
64
+ if (item && typeof item[prop] === "number")
65
+ total += item[prop];
66
+ }
67
+ return total;
68
+ }
69
+ function deriveCombatStats(state) {
70
+ const species = (0, species_1.getSpecies)(state.speciesId);
71
+ const mod = species?.statModifier ?? 1.0;
72
+ const gear = getEquippedGear(state);
73
+ const bonusATK = sumGearProp(gear, "combatATK");
74
+ const bonusDEF = sumGearProp(gear, "combatDEF");
75
+ const bonusHP = sumGearProp(gear, "combatHP");
76
+ const bonusSPD = sumGearProp(gear, "combatSPD");
77
+ const bonusDodge = sumGearProp(gear, "dodgeBonus");
78
+ const maxHp = Math.floor((20 + state.level * 5) * mod) + bonusHP;
79
+ return {
80
+ maxHp,
81
+ hp: maxHp,
82
+ atk: Math.floor((3 + state.level * 2) * mod) + bonusATK,
83
+ def: Math.floor((2 + state.level * 1) * mod) + bonusDEF,
84
+ spd: Math.floor((5 + state.level * 1) * mod) + bonusSPD,
85
+ dodgeChance: (SPECIES_DODGE[state.speciesId] ?? 0.05) + bonusDodge,
86
+ };
87
+ }
88
+ function deriveEnemyStats(enemy) {
89
+ return {
90
+ maxHp: enemy.baseHp,
91
+ hp: enemy.baseHp,
92
+ atk: enemy.baseAtk,
93
+ def: enemy.baseDef,
94
+ spd: enemy.baseSpd,
95
+ dodgeChance: enemy.baseDodge ?? 0.05,
96
+ };
97
+ }
98
+ // ─── Combat Initialization ──────────────────────────────────
99
+ function startCombat(state, enemy, morale = 50) {
100
+ const gear = getEquippedGear(state);
101
+ const energyBonus = sumGearProp(gear, "energyBonus");
102
+ const combatEnergy = 3 + Math.floor(state.level / 5) + energyBonus;
103
+ // Gear passive conditions at combat start
104
+ const startConditions = [];
105
+ for (const item of gear) {
106
+ if (!item)
107
+ continue;
108
+ if (item.id === "phoenix_robe")
109
+ startConditions.push({ type: "regen", healPerTurn: 3, turnsLeft: 99 });
110
+ if (item.id === "guardian_shield")
111
+ startConditions.push({ type: "shield", amount: 15 });
112
+ }
113
+ return {
114
+ phase: "player_turn",
115
+ playerStats: deriveCombatStats(state),
116
+ enemyStats: deriveEnemyStats(enemy),
117
+ enemy,
118
+ combatEnergy,
119
+ maxCombatEnergy: combatEnergy,
120
+ playerEffects: [],
121
+ enemyEffects: [],
122
+ playerConditions: startConditions,
123
+ enemyConditions: [],
124
+ isDefending: false,
125
+ turnCount: 0,
126
+ log: [enemy.encounterLine, pickCombatLine(lines_1.COMBAT_START_LINES, state.speciesId)],
127
+ ticksInPhase: 0,
128
+ damageTakenThisBattle: 0,
129
+ usedRevive: false,
130
+ morale,
131
+ playerElement: types_1.SPECIES_ELEMENTS[state.speciesId] ?? "neutral",
132
+ enemyElement: enemy.element,
133
+ equippedGearIds: gear.filter((g) => g !== null).map((g) => g.id),
134
+ comboTriggeredThisBattle: 0,
135
+ };
136
+ }
137
+ // ─── Damage Calculation ─────────────────────────────────────
138
+ function calculateDamage(atkStat, defStat) {
139
+ const variance = 0.8 + Math.random() * 0.4;
140
+ return Math.max(1, Math.floor(atkStat * variance - defStat / 2));
141
+ }
142
+ function isCritical(effects, morale = 50) {
143
+ const critBonus = effects
144
+ .filter((e) => e.stat === "crit")
145
+ .reduce((sum, e) => sum + e.amount, 0);
146
+ const moraleBonus = morale > 70 ? 0.10 : 0; // high morale = +10% crit
147
+ return Math.random() < (0.10 + critBonus + moraleBonus);
148
+ }
149
+ function getEffectiveAtk(base, effects) {
150
+ let mod = 1;
151
+ for (const e of effects) {
152
+ if (e.stat === "atk")
153
+ mod += e.amount;
154
+ }
155
+ return Math.floor(base * mod);
156
+ }
157
+ function getEffectiveDef(base, effects) {
158
+ let mod = 1;
159
+ for (const e of effects) {
160
+ if (e.stat === "def")
161
+ mod += e.amount;
162
+ }
163
+ return Math.floor(base * mod);
164
+ }
165
+ function getEffectiveDodge(base, effects) {
166
+ let bonus = 0;
167
+ for (const e of effects) {
168
+ if (e.stat === "dodge")
169
+ bonus += e.amount;
170
+ }
171
+ return Math.min(0.8, base + bonus);
172
+ }
173
+ function tickEffects(effects) {
174
+ return effects
175
+ .map((e) => ({ ...e, turnsLeft: e.turnsLeft - 1 }))
176
+ .filter((e) => e.turnsLeft > 0);
177
+ }
178
+ // ─── Player Actions ─────────────────────────────────────────
179
+ function playerAttack(combat) {
180
+ let atk = getEffectiveAtk(combat.playerStats.atk, combat.playerEffects);
181
+ if (combat.morale < 20)
182
+ atk = Math.floor(atk * 0.9);
183
+ const def = getEffectiveDef(combat.enemyStats.def, combat.enemyEffects);
184
+ const crit = isCritical(combat.playerEffects, combat.morale);
185
+ let damage = calculateDamage(atk, def);
186
+ // Element type matchup
187
+ const { multiplier: elemMult, label: elemLabel } = (0, types_1.getElementMultiplier)(combat.playerElement, combat.enemyElement);
188
+ damage = Math.max(1, Math.floor(damage * elemMult));
189
+ if (crit)
190
+ damage = Math.floor(damage * 1.5);
191
+ const enemyDodge = getEffectiveDodge(combat.enemyStats.dodgeChance, combat.enemyEffects);
192
+ if (Math.random() < enemyDodge) {
193
+ return {
194
+ ...combat,
195
+ phase: "player_animate",
196
+ log: [...combat.log, `${combat.enemy.name} dodges!`],
197
+ ticksInPhase: 0,
198
+ };
199
+ }
200
+ const newEnemyHp = Math.max(0, combat.enemyStats.hp - damage);
201
+ const logLines = [...combat.log];
202
+ if (elemLabel)
203
+ logLines.push(elemLabel);
204
+ logLines.push(crit ? `CRITICAL HIT! Dealt ${damage} damage!` : `Dealt ${damage} damage!`);
205
+ const log = logLines;
206
+ // Check weapon on-hit passive effects
207
+ let enemyConds = combat.enemyConditions;
208
+ for (const gearId of combat.equippedGearIds) {
209
+ const item = (0, items_1.getItem)(gearId);
210
+ if (!item?.onHitEffect || !item.onHitChance)
211
+ continue;
212
+ if (Math.random() < item.onHitChance) {
213
+ const condType = item.onHitEffect;
214
+ const power = item.onHitPower ?? 1;
215
+ if (condType === "burn") {
216
+ enemyConds = addCondition(enemyConds, { type: "burn", damagePerTurn: power, turnsLeft: 2 }, combat.enemyElement);
217
+ log.push(`${item.name} inflicts burn!`);
218
+ }
219
+ else if (condType === "freeze") {
220
+ enemyConds = addCondition(enemyConds, { type: "freeze", turnsLeft: power }, combat.enemyElement);
221
+ log.push(`${item.name} freezes the enemy!`);
222
+ }
223
+ else if (condType === "charm") {
224
+ enemyConds = addCondition(enemyConds, { type: "charm", turnsLeft: power }, combat.enemyElement);
225
+ log.push(`${item.name} charms the enemy!`);
226
+ }
227
+ }
228
+ }
229
+ return {
230
+ ...combat,
231
+ phase: "player_animate",
232
+ enemyStats: { ...combat.enemyStats, hp: newEnemyHp },
233
+ enemyConditions: enemyConds,
234
+ log,
235
+ ticksInPhase: 0,
236
+ };
237
+ }
238
+ function playerDefend(combat) {
239
+ return {
240
+ ...combat,
241
+ phase: "player_animate",
242
+ isDefending: true,
243
+ log: [...combat.log, "Bracing for impact!"],
244
+ ticksInPhase: 0,
245
+ };
246
+ }
247
+ function playerFlee(combat) {
248
+ const fleeChance = 0.4 + (combat.playerStats.spd - combat.enemyStats.spd) * 0.05;
249
+ if (Math.random() < Math.max(0.2, Math.min(0.9, fleeChance))) {
250
+ return { ...combat, phase: "fled", log: [...combat.log, "Got away safely!"], ticksInPhase: 0 };
251
+ }
252
+ return {
253
+ ...combat,
254
+ phase: "player_animate",
255
+ log: [...combat.log, "Couldn't escape!"],
256
+ ticksInPhase: 0,
257
+ };
258
+ }
259
+ function playerUseSkill(combat, skill) {
260
+ if (combat.combatEnergy < skill.energyCost) {
261
+ return { ...combat, log: [...combat.log, "Not enough energy!"] };
262
+ }
263
+ // Check for combo
264
+ const combo = checkCombo(combat.lastSkillUsed, skill.id);
265
+ let result = { ...combat, combatEnergy: combat.combatEnergy - skill.energyCost, lastSkillUsed: skill.id };
266
+ const effect = skill.effect;
267
+ switch (effect.type) {
268
+ case "damage": {
269
+ const atk = getEffectiveAtk(result.playerStats.atk, result.playerEffects);
270
+ const multiplier = combo ? combo.damageMultiplier : effect.multiplier;
271
+ let damage = Math.floor(atk * multiplier);
272
+ if (!effect.ignoreDefense) {
273
+ const def = getEffectiveDef(result.enemyStats.def, result.enemyEffects);
274
+ damage = Math.max(1, damage - Math.floor(def / 2));
275
+ }
276
+ // Element matchup: use skill element if present, otherwise species element
277
+ const skillElem = skill.element ?? combat.playerElement;
278
+ const { multiplier: eMult, label: eLabel } = (0, types_1.getElementMultiplier)(skillElem, combat.enemyElement);
279
+ damage = Math.max(1, Math.floor(damage * eMult));
280
+ const newHp = Math.max(0, result.enemyStats.hp - damage);
281
+ let enemyConds = result.enemyConditions;
282
+ let skillLog;
283
+ if (combo) {
284
+ skillLog = [`COMBO! ${combo.name}!`, `Dealt ${damage} damage!`];
285
+ result = { ...result, comboTriggeredThisBattle: result.comboTriggeredThisBattle + 1 };
286
+ if (combo.extraEffect === "burn")
287
+ enemyConds = addCondition(enemyConds, { type: "burn", damagePerTurn: 4, turnsLeft: 3 }, result.enemyElement);
288
+ if (combo.extraEffect === "stun")
289
+ enemyConds = addCondition(enemyConds, { type: "stun", turnsLeft: 2 }, result.enemyElement);
290
+ }
291
+ else {
292
+ skillLog = eLabel ? [`${skill.name}! ${eLabel}`, `Dealt ${damage} damage!`] : [`${skill.name}! Dealt ${damage} damage!`];
293
+ }
294
+ // Apply bonus conditions based on specific skills
295
+ if (skill.id === "earthquake")
296
+ enemyConds = addCondition(enemyConds, { type: "stun", turnsLeft: 1 }, result.enemyElement);
297
+ if (skill.id === "shadow_strike")
298
+ enemyConds = addCondition(enemyConds, { type: "charm", turnsLeft: 1 }, result.enemyElement);
299
+ if (skill.id === "tidal_wave")
300
+ enemyConds = addCondition(enemyConds, { type: "freeze", turnsLeft: 1 }, result.enemyElement);
301
+ if (skill.id === "inferno")
302
+ enemyConds = addCondition(enemyConds, { type: "burn", damagePerTurn: 5, turnsLeft: 3 }, result.enemyElement);
303
+ if (skill.id === "supernova")
304
+ enemyConds = addCondition(enemyConds, { type: "burn", damagePerTurn: 5, turnsLeft: 3 }, result.enemyElement);
305
+ if (skill.id === "outfox")
306
+ enemyConds = addCondition(enemyConds, { type: "charm", turnsLeft: 1 }, result.enemyElement);
307
+ if (skill.id === "piercing_gaze")
308
+ enemyConds = addCondition(enemyConds, { type: "debuff", stat: "def", amount: 0.5, turnsLeft: 2 }, result.enemyElement);
309
+ result = {
310
+ ...result,
311
+ phase: "player_animate",
312
+ enemyStats: { ...result.enemyStats, hp: newHp },
313
+ enemyConditions: enemyConds,
314
+ log: [...result.log, ...skillLog],
315
+ ticksInPhase: 0,
316
+ };
317
+ break;
318
+ }
319
+ case "buff": {
320
+ // Special skills that apply conditions instead of simple buffs
321
+ let playerConds = result.playerConditions;
322
+ if (skill.id === "aqua_shield") {
323
+ playerConds = [...playerConds, { type: "shield", amount: 20 }];
324
+ result = { ...result, phase: "player_animate", playerConditions: playerConds, log: [...result.log, `${skill.name}! Shield active (20 HP)!`], ticksInPhase: 0 };
325
+ }
326
+ else if (skill.id === "natures_blessing") {
327
+ playerConds = [...playerConds, { type: "regen", healPerTurn: 5, turnsLeft: 3 }];
328
+ result = { ...result, phase: "player_animate", playerConditions: playerConds, log: [...result.log, `${skill.name}! Regenerating!`], ticksInPhase: 0 };
329
+ }
330
+ else if (skill.id === "shadow_step") {
331
+ // Dodge buff + guaranteed crit on next attack
332
+ playerConds = [...playerConds, { type: "buff", stat: "crit", amount: 1.0, turnsLeft: 1 }];
333
+ result = {
334
+ ...result, phase: "player_animate",
335
+ playerEffects: [...result.playerEffects, { stat: effect.stat, amount: effect.amount, turnsLeft: effect.turns }],
336
+ playerConditions: playerConds,
337
+ log: [...result.log, `${skill.name}! Next attack will crit!`], ticksInPhase: 0,
338
+ };
339
+ }
340
+ else {
341
+ result = {
342
+ ...result, phase: "player_animate",
343
+ playerEffects: [...result.playerEffects, { stat: effect.stat, amount: effect.amount, turnsLeft: effect.turns }],
344
+ log: [...result.log, `${skill.name}!`], ticksInPhase: 0,
345
+ };
346
+ }
347
+ break;
348
+ }
349
+ case "debuff":
350
+ result = {
351
+ ...result,
352
+ phase: "player_animate",
353
+ enemyEffects: [...result.enemyEffects, { stat: effect.stat, amount: -effect.amount, turnsLeft: effect.turns }],
354
+ log: [...result.log, `${skill.name}! Enemy weakened!`],
355
+ ticksInPhase: 0,
356
+ };
357
+ break;
358
+ case "poison":
359
+ result = {
360
+ ...result,
361
+ phase: "player_animate",
362
+ enemyEffects: [...result.enemyEffects, { stat: "poison", amount: effect.damagePerTurn, turnsLeft: effect.turns }],
363
+ log: [...result.log, `${skill.name}! Enemy poisoned!`],
364
+ ticksInPhase: 0,
365
+ };
366
+ break;
367
+ case "stun":
368
+ case "skip_enemy_turn": {
369
+ let newEnemyConds = result.enemyConditions;
370
+ // Special: illuminate applies blind instead of stun
371
+ if (skill.id === "illuminate") {
372
+ newEnemyConds = addCondition(newEnemyConds, { type: "blind", turnsLeft: 2 }, result.enemyElement);
373
+ result = { ...result, phase: "player_animate", enemyConditions: newEnemyConds, log: [...result.log, `${skill.name}! Enemy blinded!`], ticksInPhase: 0 };
374
+ }
375
+ else {
376
+ newEnemyConds = addCondition(newEnemyConds, { type: "stun", turnsLeft: effect.type === "stun" ? effect.turns : 1 }, result.enemyElement);
377
+ result = { ...result, phase: "player_animate", enemyConditions: newEnemyConds, log: [...result.log, `${skill.name}! Enemy stunned!`], ticksInPhase: 0 };
378
+ }
379
+ break;
380
+ }
381
+ case "revive":
382
+ // Mark revive as used — actual effect triggers on KO
383
+ result = {
384
+ ...result,
385
+ phase: "player_animate",
386
+ usedRevive: true,
387
+ log: [...result.log, `${skill.name} activated!`],
388
+ ticksInPhase: 0,
389
+ };
390
+ break;
391
+ }
392
+ return result;
393
+ }
394
+ // ─── Enemy Turn ─────────────────────────────────────────────
395
+ function enemyTurn(combat) {
396
+ const enemy = combat.enemy;
397
+ const hpPct = combat.enemyStats.hp / combat.enemyStats.maxHp;
398
+ let enemyHp = combat.enemyStats.hp;
399
+ const extraLog = [];
400
+ // ─── Boss Phase Logic ──────────────────────────────
401
+ // Phase 3: Enraged (HP < 25%) — boosted ATK, special every other turn
402
+ const isEnraged = enemy.isBoss && enemy.enrageAtkBonus && hpPct < 0.25;
403
+ if (isEnraged && combat.turnCount % 2 === 0 && enemy.specialMove) {
404
+ const special = enemy.specialMove;
405
+ if (special.damageMultiplier < 0) {
406
+ const heal = Math.floor(combat.enemyStats.maxHp * Math.abs(special.damageMultiplier));
407
+ const newHp = Math.min(combat.enemyStats.maxHp, enemyHp + heal);
408
+ return { ...combat, phase: "enemy_animate", enemyStats: { ...combat.enemyStats, hp: newHp }, enemyEffects: tickEffects(combat.enemyEffects), log: [...combat.log, "ENRAGED!", special.line, `Healed ${heal} HP!`], ticksInPhase: 0 };
409
+ }
410
+ const enragedAtk = Math.floor(enemy.baseAtk * (1 + enemy.enrageAtkBonus));
411
+ const dmg = Math.floor(enragedAtk * special.damageMultiplier);
412
+ return applyDamageToPlayer(combat, dmg, ["ENRAGED!", special.line], enemyHp);
413
+ }
414
+ // Phase 2: (HP < 50%) — use phase2Move if available
415
+ if (enemy.isBoss && enemy.phase2Move && hpPct < 0.5 && hpPct >= 0.25) {
416
+ if (Math.random() < enemy.phase2Move.chance) {
417
+ const p2 = enemy.phase2Move;
418
+ if (p2.damageMultiplier < 0) {
419
+ const heal = Math.floor(combat.enemyStats.maxHp * Math.abs(p2.damageMultiplier));
420
+ const newHp = Math.min(combat.enemyStats.maxHp, enemyHp + heal);
421
+ return { ...combat, phase: "enemy_animate", enemyStats: { ...combat.enemyStats, hp: newHp }, enemyEffects: tickEffects(combat.enemyEffects), log: [...combat.log, p2.line, `Healed ${heal} HP!`], ticksInPhase: 0 };
422
+ }
423
+ const dmg = Math.floor(enemy.baseAtk * p2.damageMultiplier);
424
+ return applyDamageToPlayer(combat, dmg, [p2.line], enemyHp);
425
+ }
426
+ }
427
+ // Phase 1 / non-boss: specialMove at < 30%
428
+ if (enemy.specialMove && hpPct < 0.3) {
429
+ if (Math.random() < enemy.specialMove.chance) {
430
+ const special = enemy.specialMove;
431
+ if (special.damageMultiplier < 0) {
432
+ const heal = Math.floor(combat.enemyStats.maxHp * Math.abs(special.damageMultiplier));
433
+ const newHp = Math.min(combat.enemyStats.maxHp, enemyHp + heal);
434
+ return { ...combat, phase: "enemy_animate", enemyStats: { ...combat.enemyStats, hp: newHp }, enemyEffects: tickEffects(combat.enemyEffects), log: [...combat.log, special.line, `Healed ${heal} HP!`], ticksInPhase: 0 };
435
+ }
436
+ const dmg = Math.floor(enemy.baseAtk * special.damageMultiplier);
437
+ return applyDamageToPlayer(combat, dmg, [special.line], enemyHp);
438
+ }
439
+ }
440
+ // ─── AI Behavior ───────────────────────────────────
441
+ // Blind check — 40% miss
442
+ if (hasCondition(combat.enemyConditions, "blind") && Math.random() < 0.4) {
443
+ return { ...combat, phase: "enemy_animate", enemyEffects: tickEffects(combat.enemyEffects), isDefending: false, log: [...combat.log, `${enemy.name} attacks but misses! (blinded)`], ticksInPhase: 0 };
444
+ }
445
+ // Defensive: alternate attack/defend, defend when HP < 50%
446
+ if (enemy.aiBehavior === "defensive" && (hpPct < 0.5 || combat.turnCount % 2 === 1)) {
447
+ return { ...combat, phase: "enemy_animate", enemyEffects: tickEffects(combat.enemyEffects), isDefending: false, log: [...combat.log, enemy.defendLine ?? `${enemy.name} braces for impact!`], ticksInPhase: 0 };
448
+ // Defending enemies take less damage next turn — we'd need enemy isDefending state
449
+ // For now, defensive AI just skips attack occasionally (effectively a weaker turn)
450
+ }
451
+ // Evasive: high dodge, flee at < 20% HP
452
+ if (enemy.aiBehavior === "evasive" && hpPct < 0.2) {
453
+ // Enemy flees — player doesn't get full XP
454
+ return { ...combat, phase: "fled", log: [...combat.log, `${enemy.name} flees into the shadows!`], ticksInPhase: 0 };
455
+ }
456
+ // Tactical: debuff player first turn, then attack
457
+ if (enemy.aiBehavior === "tactical" && combat.turnCount <= 1) {
458
+ const debuffConds = addCondition(combat.playerConditions, { type: "debuff", stat: "def", amount: 0.2, turnsLeft: 3 }, combat.playerElement);
459
+ return { ...combat, phase: "enemy_animate", playerConditions: debuffConds, enemyEffects: tickEffects(combat.enemyEffects), isDefending: false, log: [...combat.log, `${enemy.name} studies your weaknesses! (-20% DEF)`], ticksInPhase: 0 };
460
+ }
461
+ // Berserker: ATK increases as HP drops
462
+ let atkMod = 1.0;
463
+ if (enemy.aiBehavior === "berserker" && hpPct < 0.25) {
464
+ atkMod = 1.5;
465
+ extraLog.push(`${enemy.name} is berserk!`);
466
+ }
467
+ else if (enemy.aiBehavior === "berserker" && hpPct < 0.5) {
468
+ atkMod = 1.25;
469
+ }
470
+ // Enraged boss ATK bonus (use max of berserker and enrage, don't stack)
471
+ if (isEnraged && enemy.enrageAtkBonus) {
472
+ atkMod = Math.max(atkMod, 1 + enemy.enrageAtkBonus);
473
+ }
474
+ // ─── Normal Attack ─────────────────────────────────
475
+ const atk = Math.floor(getEffectiveAtk(enemy.baseAtk, combat.enemyEffects) * atkMod);
476
+ const def = getEffectiveDef(combat.playerStats.def, combat.playerEffects);
477
+ let damage = calculateDamage(atk, def);
478
+ const { multiplier: enemyElemMult } = (0, types_1.getElementMultiplier)(combat.enemyElement, combat.playerElement);
479
+ damage = Math.max(1, Math.floor(damage * enemyElemMult));
480
+ if (combat.isDefending)
481
+ damage = Math.floor(damage * 0.5);
482
+ // Player dodge check
483
+ let dodge = getEffectiveDodge(combat.playerStats.dodgeChance, combat.playerEffects);
484
+ if (combat.morale < 40)
485
+ dodge = Math.max(0, dodge - 0.15);
486
+ // Evasive enemies are hard to counter-dodge against
487
+ if (Math.random() < dodge) {
488
+ return {
489
+ ...combat, phase: "enemy_animate",
490
+ enemyStats: { ...combat.enemyStats, hp: enemyHp },
491
+ enemyEffects: tickEffects(combat.enemyEffects),
492
+ isDefending: false,
493
+ log: [...combat.log, ...extraLog, enemy.attackLine, "Dodged!"],
494
+ ticksInPhase: 0,
495
+ };
496
+ }
497
+ return applyDamageToPlayer(combat, damage, [...extraLog, combat.enemy.attackLine], enemyHp);
498
+ }
499
+ function applyDamageToPlayer(combat, damage, extraLog, enemyHp) {
500
+ // Apply shield first
501
+ const shieldResult = applyShield(damage, combat.playerConditions);
502
+ damage = shieldResult.damage;
503
+ let playerConditions = shieldResult.conditions;
504
+ if (shieldResult.damage < damage)
505
+ extraLog.push("Shield absorbs some damage!");
506
+ // Freeze shatters on taking damage
507
+ playerConditions = playerConditions.filter((c) => c.type !== "freeze");
508
+ let playerHp = Math.max(0, combat.playerStats.hp - damage);
509
+ const damageTaken = combat.damageTakenThisBattle + damage;
510
+ // Low HP panic line
511
+ if (playerHp > 0 && playerHp < combat.playerStats.maxHp * 0.25 && combat.playerStats.hp >= combat.playerStats.maxHp * 0.25) {
512
+ const panicLine = lines_1.COMBAT_LOW_HP_LINES[Math.floor(Math.random() * lines_1.COMBAT_LOW_HP_LINES.length)];
513
+ extraLog.push(panicLine);
514
+ }
515
+ // Check revive
516
+ if (playerHp <= 0 && combat.usedRevive) {
517
+ playerHp = Math.floor(combat.playerStats.maxHp * 0.3);
518
+ return {
519
+ ...combat,
520
+ phase: "enemy_animate",
521
+ playerStats: { ...combat.playerStats, hp: playerHp },
522
+ enemyStats: { ...combat.enemyStats, hp: enemyHp },
523
+ enemyEffects: tickEffects(combat.enemyEffects),
524
+ playerEffects: tickEffects(combat.playerEffects),
525
+ isDefending: false,
526
+ usedRevive: false,
527
+ damageTakenThisBattle: damageTaken,
528
+ log: [...combat.log, ...extraLog, `Took ${damage} damage!`, "Revived!"],
529
+ ticksInPhase: 0,
530
+ };
531
+ }
532
+ if (playerHp <= 0) {
533
+ return {
534
+ ...combat,
535
+ phase: "defeat",
536
+ playerStats: { ...combat.playerStats, hp: 0 },
537
+ damageTakenThisBattle: damageTaken,
538
+ log: [...combat.log, ...extraLog, `Took ${damage} damage!`, "Knocked out!"],
539
+ ticksInPhase: 0,
540
+ };
541
+ }
542
+ return {
543
+ ...combat,
544
+ phase: "enemy_animate",
545
+ playerStats: { ...combat.playerStats, hp: playerHp },
546
+ enemyStats: { ...combat.enemyStats, hp: enemyHp },
547
+ enemyEffects: tickEffects(combat.enemyEffects),
548
+ playerEffects: tickEffects(combat.playerEffects),
549
+ isDefending: false,
550
+ damageTakenThisBattle: damageTaken,
551
+ log: [...combat.log, ...extraLog, `Took ${damage} damage!`],
552
+ ticksInPhase: 0,
553
+ };
554
+ }
555
+ // ─── Condition Processing ────────────────────────────────────
556
+ /** Apply start-of-turn conditions to a combatant. Returns updated HP, conditions, and log messages. */
557
+ function processConditions(hp, maxHp, conditions, log, targetName) {
558
+ let skipTurn = false;
559
+ const newLog = [...log];
560
+ let newConditions = [];
561
+ for (const cond of conditions) {
562
+ switch (cond.type) {
563
+ case "burn":
564
+ hp = Math.max(0, hp - cond.damagePerTurn);
565
+ newLog.push(`${targetName} takes ${cond.damagePerTurn} burn damage!`);
566
+ if (cond.turnsLeft > 1)
567
+ newConditions.push({ ...cond, turnsLeft: cond.turnsLeft - 1 });
568
+ // turnsLeft <= 1: burn expires, not pushed back
569
+ break;
570
+ case "poison":
571
+ hp = Math.max(0, hp - cond.damagePerTurn);
572
+ newLog.push(`${targetName} takes ${cond.damagePerTurn} poison damage!`);
573
+ if (cond.turnsLeft > 1)
574
+ newConditions.push({ ...cond, turnsLeft: cond.turnsLeft - 1 });
575
+ break;
576
+ case "freeze":
577
+ skipTurn = true;
578
+ newLog.push(`${targetName} is frozen solid!`);
579
+ // Freeze breaks after 1 turn or when damaged
580
+ if (cond.turnsLeft > 1)
581
+ newConditions.push({ ...cond, turnsLeft: cond.turnsLeft - 1 });
582
+ break;
583
+ case "stun":
584
+ skipTurn = true;
585
+ newLog.push(`${targetName} is stunned!`);
586
+ if (cond.turnsLeft > 1)
587
+ newConditions.push({ ...cond, turnsLeft: cond.turnsLeft - 1 });
588
+ break;
589
+ case "charm":
590
+ skipTurn = true; // handled specially — enemy attacks itself
591
+ newLog.push(`${targetName} is charmed and confused!`);
592
+ if (cond.turnsLeft > 1)
593
+ newConditions.push({ ...cond, turnsLeft: cond.turnsLeft - 1 });
594
+ break;
595
+ case "blind":
596
+ // Blind doesn't skip turn, just reduces accuracy — handled in attack logic
597
+ if (cond.turnsLeft > 1)
598
+ newConditions.push({ ...cond, turnsLeft: cond.turnsLeft - 1 });
599
+ break;
600
+ case "regen":
601
+ const heal = Math.min(cond.healPerTurn, maxHp - hp);
602
+ if (heal > 0) {
603
+ hp = hp + heal;
604
+ newLog.push(`${targetName} regenerates ${heal} HP!`);
605
+ }
606
+ if (cond.turnsLeft > 1)
607
+ newConditions.push({ ...cond, turnsLeft: cond.turnsLeft - 1 });
608
+ break;
609
+ case "shield":
610
+ // Shield persists until broken by damage — not tick-based
611
+ newConditions.push(cond);
612
+ break;
613
+ case "buff":
614
+ case "debuff":
615
+ if (cond.turnsLeft > 1)
616
+ newConditions.push({ ...cond, turnsLeft: cond.turnsLeft - 1 });
617
+ break;
618
+ }
619
+ }
620
+ return { hp, conditions: newConditions, log: newLog, skipTurn };
621
+ }
622
+ /** Apply shield absorption — returns remaining damage and updated conditions */
623
+ function applyShield(damage, conditions) {
624
+ const newConds = [];
625
+ let remaining = damage;
626
+ for (const cond of conditions) {
627
+ if (cond.type === "shield" && remaining > 0) {
628
+ if (cond.amount > remaining) {
629
+ newConds.push({ ...cond, amount: cond.amount - remaining });
630
+ remaining = 0;
631
+ }
632
+ else {
633
+ remaining -= cond.amount;
634
+ // shield broken, don't keep it
635
+ }
636
+ }
637
+ else {
638
+ newConds.push(cond);
639
+ }
640
+ }
641
+ return { damage: remaining, conditions: newConds };
642
+ }
643
+ /** Check if target has a specific condition */
644
+ function hasCondition(conditions, type) {
645
+ return conditions.some((c) => c.type === type);
646
+ }
647
+ /** Add a condition, respecting immunities */
648
+ function addCondition(conditions, cond, targetElement) {
649
+ if ((0, types_1.isImmune)(cond.type, targetElement))
650
+ return conditions;
651
+ return [...conditions, cond];
652
+ }
653
+ /** Get effective stat modifier from conditions (buff/debuff) */
654
+ function getConditionStatMod(conditions, stat) {
655
+ let mod = 0;
656
+ for (const c of conditions) {
657
+ if ((c.type === "buff" || c.type === "debuff") && c.stat === stat) {
658
+ mod += c.type === "buff" ? c.amount : -c.amount;
659
+ }
660
+ }
661
+ return mod;
662
+ }
663
+ // ─── Turn Advancement ───────────────────────────────────────
664
+ /** Called each tick to advance animation phases */
665
+ function tickCombat(combat) {
666
+ const ticks = combat.ticksInPhase + 1;
667
+ if (combat.phase === "player_animate" && ticks >= 2) {
668
+ if (combat.enemyStats.hp <= 0) {
669
+ return { ...combat, phase: "victory", ticksInPhase: 0, log: [...combat.log, `${combat.enemy.name} is defeated!`] };
670
+ }
671
+ // Process enemy conditions before enemy turn
672
+ const enemyProc = processConditions(combat.enemyStats.hp, combat.enemyStats.maxHp, combat.enemyConditions, combat.log, combat.enemy.name);
673
+ const updatedCombat = {
674
+ ...combat,
675
+ enemyStats: { ...combat.enemyStats, hp: enemyProc.hp },
676
+ enemyConditions: enemyProc.conditions,
677
+ log: enemyProc.log,
678
+ };
679
+ if (enemyProc.hp <= 0) {
680
+ return { ...updatedCombat, phase: "victory", ticksInPhase: 0, log: [...enemyProc.log, `${combat.enemy.name} is defeated!`] };
681
+ }
682
+ if (enemyProc.skipTurn) {
683
+ // Charm: enemy attacks itself
684
+ if (hasCondition(combat.enemyConditions, "charm")) {
685
+ const selfDmg = Math.max(1, Math.floor(combat.enemyStats.atk * 0.5));
686
+ return {
687
+ ...updatedCombat,
688
+ enemyStats: { ...updatedCombat.enemyStats, hp: Math.max(0, updatedCombat.enemyStats.hp - selfDmg) },
689
+ phase: "enemy_animate",
690
+ log: [...updatedCombat.log, `${combat.enemy.name} attacks itself for ${selfDmg} damage!`],
691
+ ticksInPhase: 0,
692
+ };
693
+ }
694
+ return { ...updatedCombat, phase: "enemy_animate", ticksInPhase: 0 };
695
+ }
696
+ return { ...updatedCombat, phase: "enemy_turn", ticksInPhase: 0 };
697
+ }
698
+ if (combat.phase === "enemy_turn") {
699
+ return enemyTurn(combat);
700
+ }
701
+ if (combat.phase === "enemy_animate" && ticks >= 2) {
702
+ // Process player conditions before player turn
703
+ const playerProc = processConditions(combat.playerStats.hp, combat.playerStats.maxHp, combat.playerConditions, combat.log, "You");
704
+ const turnCount = combat.turnCount + 1;
705
+ // Energy regen: +1 every 3 turns
706
+ const energyRegen = turnCount % 3 === 0 ? 1 : 0;
707
+ const newEnergy = Math.min(combat.maxCombatEnergy, combat.combatEnergy + energyRegen);
708
+ let regenLog = playerProc.log;
709
+ if (energyRegen > 0)
710
+ regenLog = [...regenLog, "+1 energy recovered!"];
711
+ if (playerProc.hp <= 0) {
712
+ return {
713
+ ...combat, phase: "defeat", ticksInPhase: 0,
714
+ playerStats: { ...combat.playerStats, hp: 0 },
715
+ playerConditions: playerProc.conditions,
716
+ log: [...regenLog, "Knocked out!"],
717
+ };
718
+ }
719
+ return {
720
+ ...combat,
721
+ phase: "player_turn",
722
+ ticksInPhase: 0,
723
+ turnCount,
724
+ combatEnergy: newEnergy,
725
+ playerStats: { ...combat.playerStats, hp: playerProc.hp },
726
+ playerConditions: playerProc.conditions,
727
+ log: regenLog,
728
+ };
729
+ }
730
+ return { ...combat, ticksInPhase: ticks };
731
+ }
732
+ //# sourceMappingURL=combat.js.map