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,638 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getConditionStatMod = exports.addCondition = exports.hasCondition = exports.applyShield = exports.processConditions = void 0;
4
+ exports.deriveCombatStats = deriveCombatStats;
5
+ exports.deriveEnemyStats = deriveEnemyStats;
6
+ exports.startCombat = startCombat;
7
+ exports.playerAttack = playerAttack;
8
+ exports.playerDefend = playerDefend;
9
+ exports.playerFlee = playerFlee;
10
+ exports.playerUseSkill = playerUseSkill;
11
+ exports.enemyTurn = enemyTurn;
12
+ exports.tickCombat = tickCombat;
13
+ const species_1 = require("../../buddy/species");
14
+ const items_1 = require("../../inventory/items");
15
+ const types_1 = require("../types");
16
+ const conditions_1 = require("./conditions");
17
+ const lines_1 = require("../../dialogue/lines");
18
+ // ─── Species Dodge Chances ───────────────────────────────────
19
+ const SPECIES_DODGE = {
20
+ cat: 0.20,
21
+ fox: 0.15,
22
+ rabbit: 0.10,
23
+ owl: 0.08,
24
+ frog: 0.05,
25
+ phoenix: 0.05,
26
+ dragon: 0.03,
27
+ };
28
+ function pickCombatLine(pool, speciesId) {
29
+ const lines = pool[speciesId];
30
+ if (!lines || lines.length === 0)
31
+ return "";
32
+ return lines[Math.floor(Math.random() * lines.length)];
33
+ }
34
+ const COMBOS = [
35
+ { skill1: "focus", skill2: "pounce", name: "Focused Pounce", damageMultiplier: 2.5 },
36
+ { skill1: "focus", skill2: "fire_breath", name: "Focused Breath", damageMultiplier: 2.8, extraEffect: "burn" },
37
+ { skill1: "shadow_step", skill2: "pounce", name: "Shadow Ambush", damageMultiplier: 3.0 },
38
+ { skill1: "scale_shield", skill2: "fire_breath", name: "Dragon Fury", damageMultiplier: 2.5, extraEffect: "burn" },
39
+ { skill1: "keen_eye", skill2: "piercing_gaze", name: "Owl Strike", damageMultiplier: 3.0 },
40
+ { skill1: "quick_hop", skill2: "earthquake", name: "Nature's Wrath", damageMultiplier: 2.0, extraEffect: "stun" },
41
+ { skill1: "focus", skill2: "flame_burst", name: "Focused Flame", damageMultiplier: 2.4, extraEffect: "burn" },
42
+ ];
43
+ function checkCombo(lastSkill, currentSkill) {
44
+ if (!lastSkill)
45
+ return null;
46
+ return COMBOS.find((c) => c.skill1 === lastSkill && c.skill2 === currentSkill) ?? null;
47
+ }
48
+ // ─── Combat Stat Derivation ─────────────────────────────────
49
+ /** Collect all equipped gear items */
50
+ function getEquippedGear(state) {
51
+ const s = state.adventureStats;
52
+ return [
53
+ s.equippedWeapon ? (0, items_1.getItem)(s.equippedWeapon) ?? null : null,
54
+ s.equippedArmor ? (0, items_1.getItem)(s.equippedArmor) ?? null : null,
55
+ s.equippedHelmet ? (0, items_1.getItem)(s.equippedHelmet) ?? null : null,
56
+ s.equippedBoots ? (0, items_1.getItem)(s.equippedBoots) ?? null : null,
57
+ s.equippedAccessory ? (0, items_1.getItem)(s.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((25 + state.level * 8) * 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, playerLevel = 1) {
89
+ // Scale enemies to player level so combat stays challenging throughout the story.
90
+ // Scaling is gentle at low levels and ramps up — a level 1 player sees base stats,
91
+ // but a level 10 player sees ~1.9x HP and ~1.5x ATK/DEF.
92
+ const levelScale = 1 + (playerLevel - 1) * 0.08; // +8% per level above 1
93
+ const hpScale = enemy.isBoss ? levelScale * 1.15 : levelScale; // bosses get 15% more HP scaling
94
+ const hp = Math.floor(enemy.baseHp * hpScale);
95
+ return {
96
+ maxHp: hp,
97
+ hp,
98
+ atk: Math.floor(enemy.baseAtk * (1 + (playerLevel - 1) * 0.05)), // +5% ATK per level (slower than HP)
99
+ def: Math.floor(enemy.baseDef * (1 + (playerLevel - 1) * 0.05)), // +5% DEF per level
100
+ spd: enemy.baseSpd,
101
+ dodgeChance: enemy.baseDodge ?? 0.05,
102
+ };
103
+ }
104
+ // ─── Combat Initialization ──────────────────────────────────
105
+ function startCombat(state, enemy, morale = 50) {
106
+ const gear = getEquippedGear(state);
107
+ const energyBonus = sumGearProp(gear, "energyBonus");
108
+ const combatEnergy = 3 + Math.floor(state.level / 5) + energyBonus;
109
+ // Gear passive conditions at combat start
110
+ const startConditions = [];
111
+ for (const item of gear) {
112
+ if (!item)
113
+ continue;
114
+ if (item.id === "phoenix_robe")
115
+ startConditions.push({ type: "regen", healPerTurn: 3, turnsLeft: 99 });
116
+ if (item.id === "guardian_shield")
117
+ startConditions.push({ type: "shield", amount: 15 });
118
+ }
119
+ return {
120
+ phase: "player_turn",
121
+ playerStats: deriveCombatStats(state),
122
+ enemyStats: deriveEnemyStats(enemy, state.level),
123
+ enemy,
124
+ combatEnergy,
125
+ maxCombatEnergy: combatEnergy,
126
+ playerEffects: [],
127
+ enemyEffects: [],
128
+ playerConditions: startConditions,
129
+ enemyConditions: [],
130
+ isDefending: false,
131
+ turnCount: 0,
132
+ log: [enemy.encounterLine, pickCombatLine(lines_1.COMBAT_START_LINES, state.speciesId)],
133
+ ticksInPhase: 0,
134
+ damageTakenThisBattle: 0,
135
+ usedRevive: false,
136
+ morale,
137
+ playerElement: types_1.SPECIES_ELEMENTS[state.speciesId] ?? "neutral",
138
+ enemyElement: enemy.element,
139
+ equippedGearIds: gear.filter((g) => g !== null).map((g) => g.id),
140
+ comboTriggeredThisBattle: 0,
141
+ };
142
+ }
143
+ // ─── Damage Calculation ─────────────────────────────────────
144
+ function calculateDamage(atkStat, defStat) {
145
+ const variance = 0.85 + Math.random() * 0.3; // range: 0.85-1.15
146
+ return Math.max(1, Math.floor(atkStat * variance - defStat * 0.55));
147
+ }
148
+ function isCritical(effects, morale = 50) {
149
+ const critBonus = effects
150
+ .filter((e) => e.stat === "crit")
151
+ .reduce((sum, e) => sum + e.amount, 0);
152
+ const moraleBonus = morale > 70 ? 0.10 : 0; // high morale = +10% crit
153
+ return Math.random() < (0.10 + critBonus + moraleBonus);
154
+ }
155
+ function getEffectiveAtk(base, effects) {
156
+ let mod = 1;
157
+ for (const e of effects) {
158
+ if (e.stat === "atk")
159
+ mod += e.amount;
160
+ }
161
+ return Math.floor(base * mod);
162
+ }
163
+ function getEffectiveDef(base, effects) {
164
+ let mod = 1;
165
+ for (const e of effects) {
166
+ if (e.stat === "def")
167
+ mod += e.amount;
168
+ }
169
+ return Math.floor(base * mod);
170
+ }
171
+ function getEffectiveDodge(base, effects) {
172
+ let bonus = 0;
173
+ for (const e of effects) {
174
+ if (e.stat === "dodge")
175
+ bonus += e.amount;
176
+ }
177
+ return Math.min(0.8, base + bonus);
178
+ }
179
+ function tickEffects(effects) {
180
+ return effects
181
+ .map((e) => ({ ...e, turnsLeft: e.turnsLeft - 1 }))
182
+ .filter((e) => e.turnsLeft > 0);
183
+ }
184
+ // ─── Player Actions ─────────────────────────────────────────
185
+ function playerAttack(combat) {
186
+ let atk = getEffectiveAtk(combat.playerStats.atk, combat.playerEffects);
187
+ if (combat.morale < 20)
188
+ atk = Math.floor(atk * 0.9);
189
+ const def = getEffectiveDef(combat.enemyStats.def, combat.enemyEffects);
190
+ const crit = isCritical(combat.playerEffects, combat.morale);
191
+ let damage = calculateDamage(atk, def);
192
+ // Element type matchup
193
+ const { multiplier: elemMult, label: elemLabel } = (0, types_1.getElementMultiplier)(combat.playerElement, combat.enemyElement);
194
+ damage = Math.max(1, Math.floor(damage * elemMult));
195
+ if (crit)
196
+ damage = Math.floor(damage * 1.5);
197
+ const enemyDodge = getEffectiveDodge(combat.enemyStats.dodgeChance, combat.enemyEffects);
198
+ if (Math.random() < enemyDodge) {
199
+ return {
200
+ ...combat,
201
+ phase: "player_animate",
202
+ log: [...combat.log, `${combat.enemy.name} dodges!`],
203
+ ticksInPhase: 0,
204
+ };
205
+ }
206
+ const newEnemyHp = Math.max(0, combat.enemyStats.hp - damage);
207
+ const logLines = [...combat.log];
208
+ if (elemLabel)
209
+ logLines.push(elemLabel);
210
+ logLines.push(crit ? `CRITICAL HIT! Dealt ${damage} damage!` : `Dealt ${damage} damage!`);
211
+ const log = logLines;
212
+ // Check weapon on-hit passive effects
213
+ let enemyConds = combat.enemyConditions;
214
+ for (const gearId of combat.equippedGearIds) {
215
+ const item = (0, items_1.getItem)(gearId);
216
+ if (!item?.onHitEffect || !item.onHitChance)
217
+ continue;
218
+ if (Math.random() < item.onHitChance) {
219
+ const condType = item.onHitEffect;
220
+ const power = item.onHitPower ?? 1;
221
+ if (condType === "burn") {
222
+ enemyConds = (0, conditions_1.addCondition)(enemyConds, { type: "burn", damagePerTurn: power, turnsLeft: 2 }, combat.enemyElement);
223
+ log.push(`${item.name} inflicts burn!`);
224
+ }
225
+ else if (condType === "freeze") {
226
+ enemyConds = (0, conditions_1.addCondition)(enemyConds, { type: "freeze", turnsLeft: power }, combat.enemyElement);
227
+ log.push(`${item.name} freezes the enemy!`);
228
+ }
229
+ else if (condType === "charm") {
230
+ enemyConds = (0, conditions_1.addCondition)(enemyConds, { type: "charm", turnsLeft: power }, combat.enemyElement);
231
+ log.push(`${item.name} charms the enemy!`);
232
+ }
233
+ }
234
+ }
235
+ return {
236
+ ...combat,
237
+ phase: "player_animate",
238
+ enemyStats: { ...combat.enemyStats, hp: newEnemyHp },
239
+ enemyConditions: enemyConds,
240
+ log,
241
+ ticksInPhase: 0,
242
+ };
243
+ }
244
+ function playerDefend(combat) {
245
+ return {
246
+ ...combat,
247
+ phase: "player_animate",
248
+ isDefending: true,
249
+ log: [...combat.log, "Bracing for impact!"],
250
+ ticksInPhase: 0,
251
+ };
252
+ }
253
+ function playerFlee(combat) {
254
+ const fleeChance = 0.4 + (combat.playerStats.spd - combat.enemyStats.spd) * 0.05;
255
+ if (Math.random() < Math.max(0.2, Math.min(0.9, fleeChance))) {
256
+ return { ...combat, phase: "fled", log: [...combat.log, "Got away safely!"], ticksInPhase: 0 };
257
+ }
258
+ return {
259
+ ...combat,
260
+ phase: "player_animate",
261
+ log: [...combat.log, "Couldn't escape!"],
262
+ ticksInPhase: 0,
263
+ };
264
+ }
265
+ function playerUseSkill(combat, skill) {
266
+ if (combat.combatEnergy < skill.energyCost) {
267
+ return { ...combat, log: [...combat.log, "Not enough energy!"] };
268
+ }
269
+ // Check for combo
270
+ const combo = checkCombo(combat.lastSkillUsed, skill.id);
271
+ let result = { ...combat, combatEnergy: combat.combatEnergy - skill.energyCost, lastSkillUsed: skill.id };
272
+ const effect = skill.effect;
273
+ switch (effect.type) {
274
+ case "damage": {
275
+ const atk = getEffectiveAtk(result.playerStats.atk, result.playerEffects);
276
+ const multiplier = combo ? combo.damageMultiplier : effect.multiplier;
277
+ let damage = Math.floor(atk * multiplier);
278
+ if (!effect.ignoreDefense) {
279
+ const def = getEffectiveDef(result.enemyStats.def, result.enemyEffects);
280
+ damage = Math.max(1, damage - Math.floor(def / 2));
281
+ }
282
+ // Element matchup: use skill element if present, otherwise species element
283
+ const skillElem = skill.element ?? combat.playerElement;
284
+ const { multiplier: eMult, label: eLabel } = (0, types_1.getElementMultiplier)(skillElem, combat.enemyElement);
285
+ damage = Math.max(1, Math.floor(damage * eMult));
286
+ const newHp = Math.max(0, result.enemyStats.hp - damage);
287
+ let enemyConds = result.enemyConditions;
288
+ let skillLog;
289
+ if (combo) {
290
+ skillLog = [`COMBO! ${combo.name}!`, `Dealt ${damage} damage!`];
291
+ result = { ...result, comboTriggeredThisBattle: result.comboTriggeredThisBattle + 1 };
292
+ if (combo.extraEffect === "burn")
293
+ enemyConds = (0, conditions_1.addCondition)(enemyConds, { type: "burn", damagePerTurn: 4, turnsLeft: 3 }, result.enemyElement);
294
+ if (combo.extraEffect === "stun")
295
+ enemyConds = (0, conditions_1.addCondition)(enemyConds, { type: "stun", turnsLeft: 2 }, result.enemyElement);
296
+ }
297
+ else {
298
+ skillLog = eLabel ? [`${skill.name}! ${eLabel}`, `Dealt ${damage} damage!`] : [`${skill.name}! Dealt ${damage} damage!`];
299
+ }
300
+ // Apply bonus conditions based on specific skills
301
+ if (skill.id === "earthquake")
302
+ enemyConds = (0, conditions_1.addCondition)(enemyConds, { type: "stun", turnsLeft: 1 }, result.enemyElement);
303
+ if (skill.id === "shadow_strike")
304
+ enemyConds = (0, conditions_1.addCondition)(enemyConds, { type: "charm", turnsLeft: 1 }, result.enemyElement);
305
+ if (skill.id === "tidal_wave")
306
+ enemyConds = (0, conditions_1.addCondition)(enemyConds, { type: "freeze", turnsLeft: 1 }, result.enemyElement);
307
+ if (skill.id === "inferno")
308
+ enemyConds = (0, conditions_1.addCondition)(enemyConds, { type: "burn", damagePerTurn: 5, turnsLeft: 3 }, result.enemyElement);
309
+ if (skill.id === "supernova")
310
+ enemyConds = (0, conditions_1.addCondition)(enemyConds, { type: "burn", damagePerTurn: 5, turnsLeft: 3 }, result.enemyElement);
311
+ if (skill.id === "outfox")
312
+ enemyConds = (0, conditions_1.addCondition)(enemyConds, { type: "charm", turnsLeft: 1 }, result.enemyElement);
313
+ if (skill.id === "piercing_gaze")
314
+ enemyConds = (0, conditions_1.addCondition)(enemyConds, { type: "debuff", stat: "def", amount: 0.5, turnsLeft: 2 }, result.enemyElement);
315
+ result = {
316
+ ...result,
317
+ phase: "player_animate",
318
+ enemyStats: { ...result.enemyStats, hp: newHp },
319
+ enemyConditions: enemyConds,
320
+ log: [...result.log, ...skillLog],
321
+ ticksInPhase: 0,
322
+ };
323
+ break;
324
+ }
325
+ case "buff": {
326
+ // Special skills that apply conditions instead of simple buffs
327
+ let playerConds = result.playerConditions;
328
+ if (skill.id === "aqua_shield") {
329
+ playerConds = [...playerConds, { type: "shield", amount: 20 }];
330
+ result = { ...result, phase: "player_animate", playerConditions: playerConds, log: [...result.log, `${skill.name}! Shield active (20 HP)!`], ticksInPhase: 0 };
331
+ }
332
+ else if (skill.id === "natures_blessing") {
333
+ playerConds = [...playerConds, { type: "regen", healPerTurn: 5, turnsLeft: 3 }];
334
+ result = { ...result, phase: "player_animate", playerConditions: playerConds, log: [...result.log, `${skill.name}! Regenerating!`], ticksInPhase: 0 };
335
+ }
336
+ else if (skill.id === "shadow_step") {
337
+ // Dodge buff + guaranteed crit on next attack
338
+ playerConds = [...playerConds, { type: "buff", stat: "crit", amount: 1.0, turnsLeft: 1 }];
339
+ result = {
340
+ ...result, phase: "player_animate",
341
+ playerEffects: [...result.playerEffects, { stat: effect.stat, amount: effect.amount, turnsLeft: effect.turns }],
342
+ playerConditions: playerConds,
343
+ log: [...result.log, `${skill.name}! Next attack will crit!`], ticksInPhase: 0,
344
+ };
345
+ }
346
+ else {
347
+ result = {
348
+ ...result, phase: "player_animate",
349
+ playerEffects: [...result.playerEffects, { stat: effect.stat, amount: effect.amount, turnsLeft: effect.turns }],
350
+ log: [...result.log, `${skill.name}!`], ticksInPhase: 0,
351
+ };
352
+ }
353
+ break;
354
+ }
355
+ case "debuff":
356
+ result = {
357
+ ...result,
358
+ phase: "player_animate",
359
+ enemyEffects: [...result.enemyEffects, { stat: effect.stat, amount: -effect.amount, turnsLeft: effect.turns }],
360
+ log: [...result.log, `${skill.name}! Enemy weakened!`],
361
+ ticksInPhase: 0,
362
+ };
363
+ break;
364
+ case "poison":
365
+ result = {
366
+ ...result,
367
+ phase: "player_animate",
368
+ enemyEffects: [...result.enemyEffects, { stat: "poison", amount: effect.damagePerTurn, turnsLeft: effect.turns }],
369
+ log: [...result.log, `${skill.name}! Enemy poisoned!`],
370
+ ticksInPhase: 0,
371
+ };
372
+ break;
373
+ case "stun":
374
+ case "skip_enemy_turn": {
375
+ let newEnemyConds = result.enemyConditions;
376
+ // Special: illuminate applies blind instead of stun
377
+ if (skill.id === "illuminate") {
378
+ newEnemyConds = (0, conditions_1.addCondition)(newEnemyConds, { type: "blind", turnsLeft: 2 }, result.enemyElement);
379
+ result = { ...result, phase: "player_animate", enemyConditions: newEnemyConds, log: [...result.log, `${skill.name}! Enemy blinded!`], ticksInPhase: 0 };
380
+ }
381
+ else {
382
+ newEnemyConds = (0, conditions_1.addCondition)(newEnemyConds, { type: "stun", turnsLeft: effect.type === "stun" ? effect.turns : 1 }, result.enemyElement);
383
+ result = { ...result, phase: "player_animate", enemyConditions: newEnemyConds, log: [...result.log, `${skill.name}! Enemy stunned!`], ticksInPhase: 0 };
384
+ }
385
+ break;
386
+ }
387
+ case "revive":
388
+ // Mark revive as used — actual effect triggers on KO
389
+ result = {
390
+ ...result,
391
+ phase: "player_animate",
392
+ usedRevive: true,
393
+ log: [...result.log, `${skill.name} activated!`],
394
+ ticksInPhase: 0,
395
+ };
396
+ break;
397
+ }
398
+ return result;
399
+ }
400
+ // ─── Enemy Turn ─────────────────────────────────────────────
401
+ function enemyTurn(combat) {
402
+ const enemy = combat.enemy;
403
+ const hpPct = combat.enemyStats.hp / combat.enemyStats.maxHp;
404
+ let enemyHp = combat.enemyStats.hp;
405
+ const extraLog = [];
406
+ // ─── Boss Phase Logic ──────────────────────────────
407
+ // Phase 3: Enraged (HP < 25%) — boosted ATK, special every other turn
408
+ const isEnraged = enemy.isBoss && enemy.enrageAtkBonus && hpPct < 0.25;
409
+ if (isEnraged && combat.turnCount % 2 === 0 && enemy.specialMove) {
410
+ const special = enemy.specialMove;
411
+ if (special.damageMultiplier < 0) {
412
+ const heal = Math.floor(combat.enemyStats.maxHp * Math.abs(special.damageMultiplier));
413
+ const newHp = Math.min(combat.enemyStats.maxHp, enemyHp + heal);
414
+ 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 };
415
+ }
416
+ const enragedAtk = Math.floor(enemy.baseAtk * (1 + enemy.enrageAtkBonus));
417
+ const dmg = Math.floor(enragedAtk * special.damageMultiplier);
418
+ return applyDamageToPlayer(combat, dmg, ["ENRAGED!", special.line], enemyHp);
419
+ }
420
+ // Phase 2: (HP < 50%) — use phase2Move if available
421
+ if (enemy.isBoss && enemy.phase2Move && hpPct < 0.5 && hpPct >= 0.25) {
422
+ if (Math.random() < enemy.phase2Move.chance) {
423
+ const p2 = enemy.phase2Move;
424
+ if (p2.damageMultiplier < 0) {
425
+ const heal = Math.floor(combat.enemyStats.maxHp * Math.abs(p2.damageMultiplier));
426
+ const newHp = Math.min(combat.enemyStats.maxHp, enemyHp + heal);
427
+ return { ...combat, phase: "enemy_animate", enemyStats: { ...combat.enemyStats, hp: newHp }, enemyEffects: tickEffects(combat.enemyEffects), log: [...combat.log, p2.line, `Healed ${heal} HP!`], ticksInPhase: 0 };
428
+ }
429
+ const dmg = Math.floor(enemy.baseAtk * p2.damageMultiplier);
430
+ return applyDamageToPlayer(combat, dmg, [p2.line], enemyHp);
431
+ }
432
+ }
433
+ // Phase 1 / non-boss: specialMove at < 30%
434
+ if (enemy.specialMove && hpPct < 0.3) {
435
+ if (Math.random() < enemy.specialMove.chance) {
436
+ const special = enemy.specialMove;
437
+ if (special.damageMultiplier < 0) {
438
+ const heal = Math.floor(combat.enemyStats.maxHp * Math.abs(special.damageMultiplier));
439
+ const newHp = Math.min(combat.enemyStats.maxHp, enemyHp + heal);
440
+ return { ...combat, phase: "enemy_animate", enemyStats: { ...combat.enemyStats, hp: newHp }, enemyEffects: tickEffects(combat.enemyEffects), log: [...combat.log, special.line, `Healed ${heal} HP!`], ticksInPhase: 0 };
441
+ }
442
+ const dmg = Math.floor(enemy.baseAtk * special.damageMultiplier);
443
+ return applyDamageToPlayer(combat, dmg, [special.line], enemyHp);
444
+ }
445
+ }
446
+ // ─── AI Behavior ───────────────────────────────────
447
+ // Blind check — 40% miss
448
+ if ((0, conditions_1.hasCondition)(combat.enemyConditions, "blind") && Math.random() < 0.4) {
449
+ return { ...combat, phase: "enemy_animate", enemyEffects: tickEffects(combat.enemyEffects), isDefending: false, log: [...combat.log, `${enemy.name} attacks but misses! (blinded)`], ticksInPhase: 0 };
450
+ }
451
+ // Defensive: alternate attack/defend, defend when HP < 50%
452
+ if (enemy.aiBehavior === "defensive" && (hpPct < 0.5 || combat.turnCount % 2 === 1)) {
453
+ return { ...combat, phase: "enemy_animate", enemyEffects: tickEffects(combat.enemyEffects), isDefending: false, log: [...combat.log, enemy.defendLine ?? `${enemy.name} braces for impact!`], ticksInPhase: 0 };
454
+ // Defending enemies take less damage next turn — we'd need enemy isDefending state
455
+ // For now, defensive AI just skips attack occasionally (effectively a weaker turn)
456
+ }
457
+ // Evasive: high dodge, flee at < 20% HP
458
+ if (enemy.aiBehavior === "evasive" && hpPct < 0.2) {
459
+ // Enemy flees — player doesn't get full XP
460
+ return { ...combat, phase: "fled", log: [...combat.log, `${enemy.name} flees into the shadows!`], ticksInPhase: 0 };
461
+ }
462
+ // Tactical: debuff player first turn, then attack
463
+ if (enemy.aiBehavior === "tactical" && combat.turnCount <= 1) {
464
+ const debuffConds = (0, conditions_1.addCondition)(combat.playerConditions, { type: "debuff", stat: "def", amount: 0.2, turnsLeft: 3 }, combat.playerElement);
465
+ 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 };
466
+ }
467
+ // Berserker: ATK increases as HP drops
468
+ let atkMod = 1.0;
469
+ if (enemy.aiBehavior === "berserker" && hpPct < 0.25) {
470
+ atkMod = 1.5;
471
+ extraLog.push(`${enemy.name} is berserk!`);
472
+ }
473
+ else if (enemy.aiBehavior === "berserker" && hpPct < 0.5) {
474
+ atkMod = 1.25;
475
+ }
476
+ // Enraged boss ATK bonus (use max of berserker and enrage, don't stack)
477
+ if (isEnraged && enemy.enrageAtkBonus) {
478
+ atkMod = Math.max(atkMod, 1 + enemy.enrageAtkBonus);
479
+ }
480
+ // ─── Normal Attack ─────────────────────────────────
481
+ const atk = Math.floor(getEffectiveAtk(enemy.baseAtk, combat.enemyEffects) * atkMod);
482
+ const def = getEffectiveDef(combat.playerStats.def, combat.playerEffects);
483
+ let damage = calculateDamage(atk, def);
484
+ const { multiplier: enemyElemMult } = (0, types_1.getElementMultiplier)(combat.enemyElement, combat.playerElement);
485
+ damage = Math.max(1, Math.floor(damage * enemyElemMult));
486
+ if (combat.isDefending)
487
+ damage = Math.floor(damage * 0.5);
488
+ // Player dodge check
489
+ let dodge = getEffectiveDodge(combat.playerStats.dodgeChance, combat.playerEffects);
490
+ if (combat.morale < 40)
491
+ dodge = Math.max(0, dodge - 0.15);
492
+ // Evasive enemies are hard to counter-dodge against
493
+ if (Math.random() < dodge) {
494
+ return {
495
+ ...combat, phase: "enemy_animate",
496
+ enemyStats: { ...combat.enemyStats, hp: enemyHp },
497
+ enemyEffects: tickEffects(combat.enemyEffects),
498
+ isDefending: false,
499
+ log: [...combat.log, ...extraLog, enemy.attackLine, "Dodged!"],
500
+ ticksInPhase: 0,
501
+ };
502
+ }
503
+ return applyDamageToPlayer(combat, damage, [...extraLog, combat.enemy.attackLine], enemyHp);
504
+ }
505
+ function applyDamageToPlayer(combat, damage, extraLog, enemyHp) {
506
+ // Apply shield first
507
+ const shieldResult = (0, conditions_1.applyShield)(damage, combat.playerConditions);
508
+ damage = shieldResult.damage;
509
+ let playerConditions = shieldResult.conditions;
510
+ if (shieldResult.damage < damage)
511
+ extraLog.push("Shield absorbs some damage!");
512
+ // Freeze shatters on taking damage
513
+ playerConditions = playerConditions.filter((c) => c.type !== "freeze");
514
+ let playerHp = Math.max(0, combat.playerStats.hp - damage);
515
+ const damageTaken = combat.damageTakenThisBattle + damage;
516
+ // Low HP panic line
517
+ if (playerHp > 0 && playerHp < combat.playerStats.maxHp * 0.25 && combat.playerStats.hp >= combat.playerStats.maxHp * 0.25) {
518
+ const panicLine = lines_1.COMBAT_LOW_HP_LINES[Math.floor(Math.random() * lines_1.COMBAT_LOW_HP_LINES.length)];
519
+ extraLog.push(panicLine);
520
+ }
521
+ // Check revive
522
+ if (playerHp <= 0 && combat.usedRevive) {
523
+ playerHp = Math.floor(combat.playerStats.maxHp * 0.3);
524
+ return {
525
+ ...combat,
526
+ phase: "enemy_animate",
527
+ playerStats: { ...combat.playerStats, hp: playerHp },
528
+ enemyStats: { ...combat.enemyStats, hp: enemyHp },
529
+ enemyEffects: tickEffects(combat.enemyEffects),
530
+ playerEffects: tickEffects(combat.playerEffects),
531
+ isDefending: false,
532
+ usedRevive: false,
533
+ damageTakenThisBattle: damageTaken,
534
+ log: [...combat.log, ...extraLog, `Took ${damage} damage!`, "Revived!"],
535
+ ticksInPhase: 0,
536
+ };
537
+ }
538
+ if (playerHp <= 0) {
539
+ return {
540
+ ...combat,
541
+ phase: "defeat",
542
+ playerStats: { ...combat.playerStats, hp: 0 },
543
+ damageTakenThisBattle: damageTaken,
544
+ log: [...combat.log, ...extraLog, `Took ${damage} damage!`, "Knocked out!"],
545
+ ticksInPhase: 0,
546
+ };
547
+ }
548
+ return {
549
+ ...combat,
550
+ phase: "enemy_animate",
551
+ playerStats: { ...combat.playerStats, hp: playerHp },
552
+ enemyStats: { ...combat.enemyStats, hp: enemyHp },
553
+ enemyEffects: tickEffects(combat.enemyEffects),
554
+ playerEffects: tickEffects(combat.playerEffects),
555
+ isDefending: false,
556
+ damageTakenThisBattle: damageTaken,
557
+ log: [...combat.log, ...extraLog, `Took ${damage} damage!`],
558
+ ticksInPhase: 0,
559
+ };
560
+ }
561
+ // ─── Condition Processing (extracted to conditions.ts) ───────
562
+ // Re-export for backwards compatibility
563
+ var conditions_2 = require("./conditions");
564
+ Object.defineProperty(exports, "processConditions", { enumerable: true, get: function () { return conditions_2.processConditions; } });
565
+ Object.defineProperty(exports, "applyShield", { enumerable: true, get: function () { return conditions_2.applyShield; } });
566
+ Object.defineProperty(exports, "hasCondition", { enumerable: true, get: function () { return conditions_2.hasCondition; } });
567
+ Object.defineProperty(exports, "addCondition", { enumerable: true, get: function () { return conditions_2.addCondition; } });
568
+ Object.defineProperty(exports, "getConditionStatMod", { enumerable: true, get: function () { return conditions_2.getConditionStatMod; } });
569
+ // ─── Turn Advancement ───────────────────────────────────────
570
+ /** Called each tick to advance animation phases */
571
+ function tickCombat(combat) {
572
+ const ticks = combat.ticksInPhase + 1;
573
+ if (combat.phase === "player_animate" && ticks >= 2) {
574
+ if (combat.enemyStats.hp <= 0) {
575
+ return { ...combat, phase: "victory", ticksInPhase: 0, log: [...combat.log, `${combat.enemy.name} is defeated!`] };
576
+ }
577
+ // Process enemy conditions before enemy turn
578
+ const enemyProc = (0, conditions_1.processConditions)(combat.enemyStats.hp, combat.enemyStats.maxHp, combat.enemyConditions, combat.log, combat.enemy.name);
579
+ const updatedCombat = {
580
+ ...combat,
581
+ enemyStats: { ...combat.enemyStats, hp: enemyProc.hp },
582
+ enemyConditions: enemyProc.conditions,
583
+ log: enemyProc.log,
584
+ };
585
+ if (enemyProc.hp <= 0) {
586
+ return { ...updatedCombat, phase: "victory", ticksInPhase: 0, log: [...enemyProc.log, `${combat.enemy.name} is defeated!`] };
587
+ }
588
+ if (enemyProc.skipTurn) {
589
+ // Charm: enemy attacks itself
590
+ if ((0, conditions_1.hasCondition)(combat.enemyConditions, "charm")) {
591
+ const selfDmg = Math.max(1, Math.floor(combat.enemyStats.atk * 0.5));
592
+ return {
593
+ ...updatedCombat,
594
+ enemyStats: { ...updatedCombat.enemyStats, hp: Math.max(0, updatedCombat.enemyStats.hp - selfDmg) },
595
+ phase: "enemy_animate",
596
+ log: [...updatedCombat.log, `${combat.enemy.name} attacks itself for ${selfDmg} damage!`],
597
+ ticksInPhase: 0,
598
+ };
599
+ }
600
+ return { ...updatedCombat, phase: "enemy_animate", ticksInPhase: 0 };
601
+ }
602
+ return { ...updatedCombat, phase: "enemy_turn", ticksInPhase: 0 };
603
+ }
604
+ if (combat.phase === "enemy_turn") {
605
+ return enemyTurn(combat);
606
+ }
607
+ if (combat.phase === "enemy_animate" && ticks >= 2) {
608
+ // Process player conditions before player turn
609
+ const playerProc = (0, conditions_1.processConditions)(combat.playerStats.hp, combat.playerStats.maxHp, combat.playerConditions, combat.log, "You");
610
+ const turnCount = combat.turnCount + 1;
611
+ // Energy regen: +1 every 3 turns
612
+ const energyRegen = turnCount % 3 === 0 ? 1 : 0;
613
+ const newEnergy = Math.min(combat.maxCombatEnergy, combat.combatEnergy + energyRegen);
614
+ let regenLog = playerProc.log;
615
+ if (energyRegen > 0)
616
+ regenLog = [...regenLog, "+1 energy recovered!"];
617
+ if (playerProc.hp <= 0) {
618
+ return {
619
+ ...combat, phase: "defeat", ticksInPhase: 0,
620
+ playerStats: { ...combat.playerStats, hp: 0 },
621
+ playerConditions: playerProc.conditions,
622
+ log: [...regenLog, "Knocked out!"],
623
+ };
624
+ }
625
+ return {
626
+ ...combat,
627
+ phase: "player_turn",
628
+ ticksInPhase: 0,
629
+ turnCount,
630
+ combatEnergy: newEnergy,
631
+ playerStats: { ...combat.playerStats, hp: playerProc.hp },
632
+ playerConditions: playerProc.conditions,
633
+ log: regenLog,
634
+ };
635
+ }
636
+ return { ...combat, ticksInPhase: ticks };
637
+ }
638
+ //# sourceMappingURL=combat.js.map
@@ -0,0 +1,5 @@
1
+ import { BuddyState } from "../../buddy/types";
2
+ import { CombatState } from "../types";
3
+ export declare function renderCombat(combat: CombatState, state: BuddyState, adventureName: string): string;
4
+ export declare function renderSkillMenu(combat: CombatState, state: BuddyState): string;
5
+ //# sourceMappingURL=combatUI.d.ts.map