pokemon-io-core 0.0.22 → 0.0.24
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.
- package/dist/api/battle.d.ts +11 -3
- package/dist/core/engine.js +213 -47
- package/dist/core/events.d.ts +1 -1
- package/dist/core/items.d.ts +2 -1
- package/dist/core/moves.d.ts +20 -3
- package/dist/skins/pokemon/items.js +114 -24
- package/dist/skins/pokemon/moves.js +5 -1
- package/dist/skins/pokemon/statuses.js +26 -6
- package/package.json +1 -1
package/dist/api/battle.d.ts
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
|
+
import { StatusId } from "../core";
|
|
2
|
+
export interface BattlePlayerStatusView {
|
|
3
|
+
statusId: StatusId;
|
|
4
|
+
stacks: 1 | 2;
|
|
5
|
+
remainingTurns: number;
|
|
6
|
+
}
|
|
1
7
|
export interface BattlePlayerView {
|
|
2
8
|
playerId: string;
|
|
3
9
|
nickname: string;
|
|
4
10
|
hp: number;
|
|
11
|
+
statuses: BattlePlayerStatusView[];
|
|
5
12
|
}
|
|
6
|
-
export type
|
|
13
|
+
export type BattleStatus = "ongoing" | "finished";
|
|
7
14
|
export interface BattleView {
|
|
8
15
|
roomId: string;
|
|
9
|
-
status:
|
|
10
|
-
currentTurnPlayerId: string | null;
|
|
16
|
+
status: BattleStatus;
|
|
11
17
|
winnerId: string | null;
|
|
18
|
+
currentTurnPlayerId: string | null;
|
|
19
|
+
startedAt: string;
|
|
12
20
|
endsAt: string;
|
|
13
21
|
players: BattlePlayerView[];
|
|
14
22
|
}
|
package/dist/core/engine.js
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
// src/domain/combat/engine/battleEngine.ts
|
|
2
|
+
// ------------------------------------------------------
|
|
3
|
+
// Debug helper
|
|
4
|
+
// ------------------------------------------------------
|
|
5
|
+
const DEBUG_BATTLE = true;
|
|
6
|
+
const dbg = (...args) => {
|
|
7
|
+
if (!DEBUG_BATTLE)
|
|
8
|
+
return;
|
|
9
|
+
// eslint-disable-next-line no-console
|
|
10
|
+
console.log("[BATTLE]", ...args);
|
|
11
|
+
};
|
|
2
12
|
const DEFAULT_RULES = {
|
|
3
13
|
baseCritChance: 0.05,
|
|
4
14
|
critPerStat: 0.002,
|
|
@@ -7,18 +17,19 @@ const DEFAULT_RULES = {
|
|
|
7
17
|
randomMinDamageFactor: 0.85,
|
|
8
18
|
randomMaxDamageFactor: 1.0,
|
|
9
19
|
};
|
|
10
|
-
const buildMap = (list) => {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}, {});
|
|
15
|
-
};
|
|
20
|
+
const buildMap = (list) => list.reduce((acc, item) => {
|
|
21
|
+
acc[item.id] = item;
|
|
22
|
+
return acc;
|
|
23
|
+
}, {});
|
|
16
24
|
const cloneStats = (stats) => ({
|
|
17
25
|
offense: stats.offense,
|
|
18
26
|
defense: stats.defense,
|
|
19
27
|
speed: stats.speed,
|
|
20
28
|
crit: stats.crit,
|
|
21
29
|
});
|
|
30
|
+
// ------------------------------------------------------
|
|
31
|
+
// Creación de estado inicial
|
|
32
|
+
// ------------------------------------------------------
|
|
22
33
|
const createBattleFighter = (cfg) => {
|
|
23
34
|
if (cfg.moves.length !== 4) {
|
|
24
35
|
throw new Error("Each fighter must have exactly 4 moves in MVP");
|
|
@@ -77,10 +88,10 @@ export const createInitialBattleState = (config) => {
|
|
|
77
88
|
};
|
|
78
89
|
return state;
|
|
79
90
|
};
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
91
|
+
// ------------------------------------------------------
|
|
92
|
+
// Helpers
|
|
93
|
+
// ------------------------------------------------------
|
|
94
|
+
const getActiveFighter = (player) => player.fighterTeam[player.activeIndex];
|
|
84
95
|
const getOpponentAndSelf = (state, playerKey) => {
|
|
85
96
|
const selfPlayer = playerKey === "player1" ? state.player1 : state.player2;
|
|
86
97
|
const oppPlayer = playerKey === "player1" ? state.player2 : state.player1;
|
|
@@ -96,9 +107,7 @@ const getTypeEffectiveness = (state, attackerTypeId, defenderTypeId) => {
|
|
|
96
107
|
return 1;
|
|
97
108
|
return row[defenderTypeId] ?? 1;
|
|
98
109
|
};
|
|
99
|
-
const randomInRange = (min, max) =>
|
|
100
|
-
return Math.random() * (max - min) + min;
|
|
101
|
-
};
|
|
110
|
+
const randomInRange = (min, max) => Math.random() * (max - min) + min;
|
|
102
111
|
const chance = (probability) => {
|
|
103
112
|
if (probability <= 0)
|
|
104
113
|
return false;
|
|
@@ -106,8 +115,9 @@ const chance = (probability) => {
|
|
|
106
115
|
return true;
|
|
107
116
|
return Math.random() < probability;
|
|
108
117
|
};
|
|
109
|
-
//
|
|
110
|
-
//
|
|
118
|
+
// ------------------------------------------------------
|
|
119
|
+
// Cálculo de críticos y daño
|
|
120
|
+
// ------------------------------------------------------
|
|
111
121
|
const computeCritChance = (rules, critStat) => {
|
|
112
122
|
const raw = rules.baseCritChance + critStat * rules.critPerStat;
|
|
113
123
|
return Math.max(0, Math.min(1, raw));
|
|
@@ -175,14 +185,16 @@ const applyDamageToFighter = (state, defender, amount, actorId, isCritical) => {
|
|
|
175
185
|
}
|
|
176
186
|
return { updatedDefender, events };
|
|
177
187
|
};
|
|
188
|
+
// ------------------------------------------------------
|
|
189
|
+
// apply_status
|
|
190
|
+
// ------------------------------------------------------
|
|
178
191
|
const applyStatusToFighter = (state, target, statusId) => {
|
|
179
192
|
const def = state.runtime.statusesById[statusId];
|
|
180
193
|
if (!def)
|
|
181
194
|
return { updated: target, events: [] };
|
|
182
195
|
const events = [];
|
|
183
196
|
const existing = target.statuses.find((s) => s.statusId === statusId);
|
|
184
|
-
const duration = Math.floor(randomInRange(def.minDurationTurns, def.maxDurationTurns + 1)) ||
|
|
185
|
-
def.minDurationTurns;
|
|
197
|
+
const duration = Math.floor(randomInRange(def.minDurationTurns, def.maxDurationTurns + 1)) || def.minDurationTurns;
|
|
186
198
|
let newStatuses = [...target.statuses];
|
|
187
199
|
if (!existing) {
|
|
188
200
|
newStatuses.push({
|
|
@@ -199,6 +211,12 @@ const applyStatusToFighter = (state, target, statusId) => {
|
|
|
199
211
|
else {
|
|
200
212
|
newStatuses = newStatuses.map((s) => s.statusId === statusId ? { ...s, remainingTurns: duration } : s);
|
|
201
213
|
}
|
|
214
|
+
dbg("STATUS_APPLY", {
|
|
215
|
+
fighterId: target.fighterId,
|
|
216
|
+
statusId,
|
|
217
|
+
duration,
|
|
218
|
+
prevStacks: existing?.stacks ?? 0,
|
|
219
|
+
});
|
|
202
220
|
events.push({
|
|
203
221
|
...createBaseEvent(state.turnNumber, "status_applied", `Se aplica ${statusId} a ${target.fighterId}`),
|
|
204
222
|
targetId: target.fighterId,
|
|
@@ -209,6 +227,54 @@ const applyStatusToFighter = (state, target, statusId) => {
|
|
|
209
227
|
events,
|
|
210
228
|
};
|
|
211
229
|
};
|
|
230
|
+
const applyClearStatusToFighter = (state, target, effect) => {
|
|
231
|
+
const { statusIds, kinds, clearAll } = effect;
|
|
232
|
+
const events = [];
|
|
233
|
+
const before = target.statuses;
|
|
234
|
+
let after = before;
|
|
235
|
+
if (clearAll) {
|
|
236
|
+
after = [];
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
after = before.filter((st) => {
|
|
240
|
+
const def = state.runtime.statusesById[st.statusId];
|
|
241
|
+
if (statusIds && statusIds.includes(st.statusId)) {
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
if (kinds && def && kinds.includes(def.kind)) {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
return true;
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
if (after.length === before.length) {
|
|
251
|
+
// nada que limpiar
|
|
252
|
+
return { updated: target, events };
|
|
253
|
+
}
|
|
254
|
+
const removed = before
|
|
255
|
+
.filter((old) => !after.some((st) => st.statusId === old.statusId))
|
|
256
|
+
.map((st) => st.statusId);
|
|
257
|
+
dbg("STATUS_CLEAR", {
|
|
258
|
+
fighterId: target.fighterId,
|
|
259
|
+
cleared: removed,
|
|
260
|
+
clearAll: !!clearAll,
|
|
261
|
+
});
|
|
262
|
+
events.push({
|
|
263
|
+
...createBaseEvent(state.turnNumber, "status_cleared", `Se limpian estados (${removed.join(", ")}) de ${target.fighterId}`),
|
|
264
|
+
targetId: target.fighterId,
|
|
265
|
+
statusIds: removed,
|
|
266
|
+
});
|
|
267
|
+
return {
|
|
268
|
+
updated: {
|
|
269
|
+
...target,
|
|
270
|
+
statuses: after,
|
|
271
|
+
},
|
|
272
|
+
events,
|
|
273
|
+
};
|
|
274
|
+
};
|
|
275
|
+
// ------------------------------------------------------
|
|
276
|
+
// heal
|
|
277
|
+
// ------------------------------------------------------
|
|
212
278
|
const applyHealToFighter = (state, target, amount, sourceId) => {
|
|
213
279
|
const newHp = Math.min(target.maxHp, target.currentHp + amount);
|
|
214
280
|
const healed = newHp - target.currentHp;
|
|
@@ -218,6 +284,12 @@ const applyHealToFighter = (state, target, amount, sourceId) => {
|
|
|
218
284
|
isAlive: newHp > 0,
|
|
219
285
|
};
|
|
220
286
|
const events = [];
|
|
287
|
+
dbg("HEAL", {
|
|
288
|
+
sourceId,
|
|
289
|
+
targetId: target.fighterId,
|
|
290
|
+
healed,
|
|
291
|
+
newHp,
|
|
292
|
+
});
|
|
221
293
|
events.push({
|
|
222
294
|
...createBaseEvent(state.turnNumber, "heal", `${sourceId} cura ${healed} a ${target.fighterId}`),
|
|
223
295
|
actorId: sourceId,
|
|
@@ -226,6 +298,9 @@ const applyHealToFighter = (state, target, amount, sourceId) => {
|
|
|
226
298
|
});
|
|
227
299
|
return { updated, events };
|
|
228
300
|
};
|
|
301
|
+
// ------------------------------------------------------
|
|
302
|
+
// Motor de efectos genérico (move + item)
|
|
303
|
+
// ------------------------------------------------------
|
|
229
304
|
const applyEffectsOnTarget = (state, actor, target, effects) => {
|
|
230
305
|
let currentActor = actor;
|
|
231
306
|
let currentTarget = target;
|
|
@@ -244,6 +319,12 @@ const applyEffectsOnTarget = (state, actor, target, effects) => {
|
|
|
244
319
|
events.push(...res.events);
|
|
245
320
|
break;
|
|
246
321
|
}
|
|
322
|
+
case "clear_status": {
|
|
323
|
+
const res = applyClearStatusToFighter(state, currentTarget, eff);
|
|
324
|
+
currentTarget = res.updated;
|
|
325
|
+
events.push(...res.events);
|
|
326
|
+
break;
|
|
327
|
+
}
|
|
247
328
|
case "damage": {
|
|
248
329
|
if (typeof eff.amount === "number") {
|
|
249
330
|
const res = applyDamageToFighter(state, currentTarget, eff.amount, actor.fighterId, false);
|
|
@@ -253,6 +334,7 @@ const applyEffectsOnTarget = (state, actor, target, effects) => {
|
|
|
253
334
|
break;
|
|
254
335
|
}
|
|
255
336
|
default:
|
|
337
|
+
// otros efectos (modify_stats, etc.) se pueden añadir aquí
|
|
256
338
|
break;
|
|
257
339
|
}
|
|
258
340
|
}
|
|
@@ -281,6 +363,9 @@ const getMovePriorityAndSpeed = (state, playerKey, action) => {
|
|
|
281
363
|
speed: self.effectiveStats.speed,
|
|
282
364
|
};
|
|
283
365
|
};
|
|
366
|
+
// ------------------------------------------------------
|
|
367
|
+
// use_move
|
|
368
|
+
// ------------------------------------------------------
|
|
284
369
|
const resolveDamageMove = (state, playerKey, action) => {
|
|
285
370
|
if (action.kind !== "use_move") {
|
|
286
371
|
return { state, events: [] };
|
|
@@ -289,14 +374,23 @@ const resolveDamageMove = (state, playerKey, action) => {
|
|
|
289
374
|
const events = [];
|
|
290
375
|
const moveSlot = self.moves[action.moveIndex];
|
|
291
376
|
if (!moveSlot) {
|
|
377
|
+
dbg("use_move: empty slot", { playerKey, moveIndex: action.moveIndex });
|
|
292
378
|
return { state, events };
|
|
293
379
|
}
|
|
294
380
|
const move = state.runtime.movesById[moveSlot.moveId];
|
|
295
381
|
if (!move) {
|
|
382
|
+
dbg("use_move: move definition not found", {
|
|
383
|
+
playerKey,
|
|
384
|
+
moveId: moveSlot.moveId,
|
|
385
|
+
});
|
|
296
386
|
return { state, events };
|
|
297
387
|
}
|
|
298
|
-
// Sin PP → no hace nada
|
|
299
388
|
if (moveSlot.currentPP <= 0) {
|
|
389
|
+
dbg("use_move: no PP", {
|
|
390
|
+
playerKey,
|
|
391
|
+
fighterId: self.fighterId,
|
|
392
|
+
moveId: move.id,
|
|
393
|
+
});
|
|
300
394
|
return { state, events };
|
|
301
395
|
}
|
|
302
396
|
// Movimiento solo de curación → no calculamos daño base
|
|
@@ -315,7 +409,14 @@ const resolveDamageMove = (state, playerKey, action) => {
|
|
|
315
409
|
const newState = updateFightersInState(state, playerKey, finalSelf, finalOpp);
|
|
316
410
|
return { state: newState, events };
|
|
317
411
|
}
|
|
318
|
-
|
|
412
|
+
dbg("ACTION", {
|
|
413
|
+
playerKey,
|
|
414
|
+
fighterId: self.fighterId,
|
|
415
|
+
moveId: move.id,
|
|
416
|
+
moveName: move.name,
|
|
417
|
+
targetId: opponent.fighterId,
|
|
418
|
+
currentPP: moveSlot.currentPP,
|
|
419
|
+
});
|
|
319
420
|
events.push({
|
|
320
421
|
...createBaseEvent(state.turnNumber, "action_declared", `${self.fighterId} usa ${move.name}`),
|
|
321
422
|
actorId: self.fighterId,
|
|
@@ -329,6 +430,13 @@ const resolveDamageMove = (state, playerKey, action) => {
|
|
|
329
430
|
let updatedSelf = { ...self, moves: updatedMoves };
|
|
330
431
|
let updatedOpponent = { ...opponent };
|
|
331
432
|
if (!hit) {
|
|
433
|
+
dbg("MISS", {
|
|
434
|
+
fighterId: self.fighterId,
|
|
435
|
+
moveId: move.id,
|
|
436
|
+
moveName: move.name,
|
|
437
|
+
hitRoll,
|
|
438
|
+
accuracy,
|
|
439
|
+
});
|
|
332
440
|
events.push({
|
|
333
441
|
...createBaseEvent(state.turnNumber, "move_miss", `${self.fighterId} falla ${move.name}`),
|
|
334
442
|
actorId: self.fighterId,
|
|
@@ -338,7 +446,6 @@ const resolveDamageMove = (state, playerKey, action) => {
|
|
|
338
446
|
const newState = updateFightersInState(state, playerKey, updatedSelf, updatedOpponent);
|
|
339
447
|
return { state: newState, events };
|
|
340
448
|
}
|
|
341
|
-
// Crítico y daño base
|
|
342
449
|
const critChance = computeCritChance(state.runtime.rules, updatedSelf.effectiveStats.crit);
|
|
343
450
|
const isCritical = chance(critChance);
|
|
344
451
|
const { damage, effectiveness } = computeDamage({
|
|
@@ -348,6 +455,15 @@ const resolveDamageMove = (state, playerKey, action) => {
|
|
|
348
455
|
move,
|
|
349
456
|
isCritical,
|
|
350
457
|
});
|
|
458
|
+
dbg("HIT", {
|
|
459
|
+
fighterId: self.fighterId,
|
|
460
|
+
moveId: move.id,
|
|
461
|
+
moveName: move.name,
|
|
462
|
+
isCritical,
|
|
463
|
+
damage,
|
|
464
|
+
effectiveness,
|
|
465
|
+
defenderBeforeHp: opponent.currentHp,
|
|
466
|
+
});
|
|
351
467
|
events.push({
|
|
352
468
|
...createBaseEvent(state.turnNumber, "move_hit", `${self.fighterId} acierta ${move.name}`),
|
|
353
469
|
actorId: self.fighterId,
|
|
@@ -357,14 +473,21 @@ const resolveDamageMove = (state, playerKey, action) => {
|
|
|
357
473
|
effectiveness,
|
|
358
474
|
});
|
|
359
475
|
const damageRes = applyDamageToFighter(state, updatedOpponent, damage, updatedSelf.fighterId, isCritical);
|
|
476
|
+
dbg("AFTER_DAMAGE", {
|
|
477
|
+
targetId: opponent.fighterId,
|
|
478
|
+
newHp: damageRes.updatedDefender.currentHp,
|
|
479
|
+
});
|
|
360
480
|
updatedOpponent = damageRes.updatedDefender;
|
|
361
481
|
events.push(...damageRes.events);
|
|
362
|
-
// Efectos secundarios del movimiento (quemado, buffs, daño
|
|
482
|
+
// Efectos secundarios del movimiento (quemado, buffs, daño fijo, etc.)
|
|
363
483
|
const { actor: finalSelf, target: finalOpp, events: extraEvents, } = applyEffectsOnTarget(state, updatedSelf, updatedOpponent, move.effects);
|
|
364
484
|
events.push(...extraEvents);
|
|
365
485
|
const newState = updateFightersInState(state, playerKey, finalSelf, finalOpp);
|
|
366
486
|
return { state: newState, events };
|
|
367
487
|
};
|
|
488
|
+
// ------------------------------------------------------
|
|
489
|
+
// use_item (respeta item.target: "self" | "enemy")
|
|
490
|
+
// ------------------------------------------------------
|
|
368
491
|
const resolveItemUse = (state, playerKey, action) => {
|
|
369
492
|
if (action.kind !== "use_item") {
|
|
370
493
|
return { state, events: [] };
|
|
@@ -373,13 +496,20 @@ const resolveItemUse = (state, playerKey, action) => {
|
|
|
373
496
|
const events = [];
|
|
374
497
|
const itemSlot = self.items[action.itemIndex];
|
|
375
498
|
if (!itemSlot || itemSlot.usesRemaining <= 0) {
|
|
499
|
+
dbg("use_item: no charges", {
|
|
500
|
+
playerKey,
|
|
501
|
+
itemIndex: action.itemIndex,
|
|
502
|
+
});
|
|
376
503
|
return { state, events };
|
|
377
504
|
}
|
|
378
505
|
const itemDef = state.runtime.itemsById[itemSlot.itemId];
|
|
379
506
|
if (!itemDef) {
|
|
507
|
+
dbg("use_item: item definition not found", {
|
|
508
|
+
playerKey,
|
|
509
|
+
itemId: itemSlot.itemId,
|
|
510
|
+
});
|
|
380
511
|
return { state, events };
|
|
381
512
|
}
|
|
382
|
-
// Reducir usos
|
|
383
513
|
const updatedItems = self.items.map((it, idx) => idx === action.itemIndex
|
|
384
514
|
? { ...it, usesRemaining: Math.max(0, it.usesRemaining - 1) }
|
|
385
515
|
: it);
|
|
@@ -389,11 +519,29 @@ const resolveItemUse = (state, playerKey, action) => {
|
|
|
389
519
|
...createBaseEvent(state.turnNumber, "action_declared", `${self.fighterId} usa objeto ${itemDef.name}`),
|
|
390
520
|
actorId: self.fighterId,
|
|
391
521
|
});
|
|
392
|
-
|
|
393
|
-
|
|
522
|
+
let finalSelf = updatedSelf;
|
|
523
|
+
let finalOpp = updatedOpponent;
|
|
524
|
+
// interpretamos itemDef.target:
|
|
525
|
+
// - "self" → efectos aplican sobre el propio usuario
|
|
526
|
+
// - "enemy" → efectos sobre el rival
|
|
527
|
+
if (itemDef.target === "self") {
|
|
528
|
+
const res = applyEffectsOnTarget(state, updatedSelf, updatedSelf, itemDef.effects);
|
|
529
|
+
finalSelf = res.actor; // o res.target, son el mismo
|
|
530
|
+
// el oponente no cambia
|
|
531
|
+
events.push(...res.events);
|
|
532
|
+
}
|
|
533
|
+
else {
|
|
534
|
+
const res = applyEffectsOnTarget(state, updatedSelf, updatedOpponent, itemDef.effects);
|
|
535
|
+
finalSelf = res.actor;
|
|
536
|
+
finalOpp = res.target;
|
|
537
|
+
events.push(...res.events);
|
|
538
|
+
}
|
|
394
539
|
const newState = updateFightersInState(state, playerKey, finalSelf, finalOpp);
|
|
395
540
|
return { state: newState, events };
|
|
396
541
|
};
|
|
542
|
+
// ------------------------------------------------------
|
|
543
|
+
// Actualizar fighters en el estado
|
|
544
|
+
// ------------------------------------------------------
|
|
397
545
|
const updateFightersInState = (state, actingPlayerKey, updatedSelf, updatedOpponent) => {
|
|
398
546
|
const player1 = { ...state.player1 };
|
|
399
547
|
const player2 = { ...state.player2 };
|
|
@@ -421,14 +569,13 @@ const updateFightersInState = (state, actingPlayerKey, updatedSelf, updatedOppon
|
|
|
421
569
|
player2: selfPlayer,
|
|
422
570
|
};
|
|
423
571
|
};
|
|
424
|
-
//
|
|
425
|
-
//
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
};
|
|
572
|
+
// ------------------------------------------------------
|
|
573
|
+
// CC duro + check winner
|
|
574
|
+
// ------------------------------------------------------
|
|
575
|
+
const hasHardCc = (state, fighter) => fighter.statuses.some((st) => {
|
|
576
|
+
const def = state.runtime.statusesById[st.statusId];
|
|
577
|
+
return def?.kind === "hard_cc";
|
|
578
|
+
});
|
|
432
579
|
const checkWinner = (state) => {
|
|
433
580
|
const f1 = getActiveFighter(state.player1);
|
|
434
581
|
const f2 = getActiveFighter(state.player2);
|
|
@@ -442,7 +589,9 @@ const checkWinner = (state) => {
|
|
|
442
589
|
return "draw";
|
|
443
590
|
return "none";
|
|
444
591
|
};
|
|
445
|
-
//
|
|
592
|
+
// ------------------------------------------------------
|
|
593
|
+
// Fin de turno: aplicar DoT de estados
|
|
594
|
+
// ------------------------------------------------------
|
|
446
595
|
const applyEndOfTurnStatuses = (state) => {
|
|
447
596
|
const events = [];
|
|
448
597
|
const applyForPlayer = (player) => {
|
|
@@ -452,18 +601,27 @@ const applyEndOfTurnStatuses = (state) => {
|
|
|
452
601
|
for (const st of active.statuses) {
|
|
453
602
|
const def = state.runtime.statusesById[st.statusId];
|
|
454
603
|
if (!def) {
|
|
604
|
+
dbg("STATUS_MISSING_DEF", {
|
|
605
|
+
fighterId: active.fighterId,
|
|
606
|
+
statusId: st.statusId,
|
|
607
|
+
});
|
|
455
608
|
continue;
|
|
456
609
|
}
|
|
457
|
-
// MVP: interpretamos cualquier efecto kind: "damage" como DoT
|
|
458
610
|
let damageFromStatus = 0;
|
|
459
611
|
def.effectsPerStack.forEach((eff) => {
|
|
460
612
|
if (eff.kind === "damage") {
|
|
461
613
|
const stacksMultiplier = st.stacks === 2 ? 2 : 1;
|
|
462
|
-
const basePower =
|
|
614
|
+
const basePower = typeof eff.amount === "number" ? eff.amount : 10;
|
|
463
615
|
damageFromStatus += basePower * stacksMultiplier;
|
|
464
616
|
}
|
|
465
617
|
});
|
|
466
618
|
if (damageFromStatus > 0 && updated.isAlive) {
|
|
619
|
+
dbg("STATUS_DOT", {
|
|
620
|
+
fighterId: updated.fighterId,
|
|
621
|
+
statusId: st.statusId,
|
|
622
|
+
damageFromStatus,
|
|
623
|
+
stacks: st.stacks,
|
|
624
|
+
});
|
|
467
625
|
const damageRes = applyDamageToFighter(state, updated, damageFromStatus, updated.fighterId, false);
|
|
468
626
|
updated = damageRes.updatedDefender;
|
|
469
627
|
events.push(...damageRes.events);
|
|
@@ -476,6 +634,10 @@ const applyEndOfTurnStatuses = (state) => {
|
|
|
476
634
|
});
|
|
477
635
|
}
|
|
478
636
|
else {
|
|
637
|
+
dbg("STATUS_EXPIRE", {
|
|
638
|
+
fighterId: updated.fighterId,
|
|
639
|
+
statusId: st.statusId,
|
|
640
|
+
});
|
|
479
641
|
events.push({
|
|
480
642
|
...createBaseEvent(state.turnNumber, "status_expired", `El estado ${st.statusId} expira en ${updated.fighterId}`),
|
|
481
643
|
targetId: updated.fighterId,
|
|
@@ -502,15 +664,19 @@ const applyEndOfTurnStatuses = (state) => {
|
|
|
502
664
|
};
|
|
503
665
|
return { state: newState, events };
|
|
504
666
|
};
|
|
505
|
-
//
|
|
667
|
+
// ------------------------------------------------------
|
|
668
|
+
// Bucle principal de turno
|
|
669
|
+
// ------------------------------------------------------
|
|
506
670
|
export const resolveTurn = (state, actions) => {
|
|
507
671
|
const runtimeState = state;
|
|
508
672
|
const events = [];
|
|
509
|
-
|
|
673
|
+
dbg(`TURN ${runtimeState.turnNumber} start`, {
|
|
674
|
+
p1Action: actions.player1.kind,
|
|
675
|
+
p2Action: actions.player2.kind,
|
|
676
|
+
});
|
|
510
677
|
events.push({
|
|
511
678
|
...createBaseEvent(runtimeState.turnNumber, "turn_start", `Comienza el turno ${runtimeState.turnNumber}`),
|
|
512
679
|
});
|
|
513
|
-
// Construir orden con prioridad+speed
|
|
514
680
|
const entries = [
|
|
515
681
|
{
|
|
516
682
|
playerKey: "player1",
|
|
@@ -533,18 +699,20 @@ export const resolveTurn = (state, actions) => {
|
|
|
533
699
|
// empate total → coin flip
|
|
534
700
|
return Math.random() < 0.5 ? -1 : 1;
|
|
535
701
|
});
|
|
702
|
+
dbg(`TURN ${runtimeState.turnNumber} order`, entries.map((e) => ({
|
|
703
|
+
player: e.playerKey,
|
|
704
|
+
action: e.action.kind,
|
|
705
|
+
priority: e.priority,
|
|
706
|
+
speed: e.speed,
|
|
707
|
+
})));
|
|
536
708
|
let currentState = runtimeState;
|
|
537
709
|
for (const entry of entries) {
|
|
538
710
|
const { playerKey, action } = entry;
|
|
539
|
-
if (action.kind === "no_action")
|
|
711
|
+
if (action.kind === "no_action")
|
|
540
712
|
continue;
|
|
541
|
-
}
|
|
542
713
|
const { self } = getOpponentAndSelf(currentState, playerKey);
|
|
543
|
-
|
|
544
|
-
if (!self.isAlive || self.currentHp <= 0) {
|
|
714
|
+
if (!self.isAlive || self.currentHp <= 0)
|
|
545
715
|
continue;
|
|
546
|
-
}
|
|
547
|
-
// Hard CC → se salta acción
|
|
548
716
|
if (hasHardCc(currentState, self)) {
|
|
549
717
|
events.push({
|
|
550
718
|
...createBaseEvent(currentState.turnNumber, "action_skipped_hard_cc", `${self.fighterId} no puede actuar por control total`),
|
|
@@ -563,10 +731,9 @@ export const resolveTurn = (state, actions) => {
|
|
|
563
731
|
events.push(...result.events);
|
|
564
732
|
}
|
|
565
733
|
else {
|
|
566
|
-
// switch_fighter
|
|
734
|
+
// switch_fighter u otros no implementados aún
|
|
567
735
|
continue;
|
|
568
736
|
}
|
|
569
|
-
// Comprobar victoria inmediata tras cada acción
|
|
570
737
|
const winnerAfterAction = checkWinner(currentState);
|
|
571
738
|
if (winnerAfterAction !== "none") {
|
|
572
739
|
events.push({
|
|
@@ -583,7 +750,6 @@ export const resolveTurn = (state, actions) => {
|
|
|
583
750
|
};
|
|
584
751
|
}
|
|
585
752
|
}
|
|
586
|
-
// Fin de turno: aplicar estados
|
|
587
753
|
const statusResult = applyEndOfTurnStatuses(currentState);
|
|
588
754
|
currentState = statusResult.state;
|
|
589
755
|
events.push(...statusResult.events);
|
package/dist/core/events.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { FighterId, MoveId, ItemId, StatusId } from "./ids";
|
|
2
|
-
export type BattleEventKind = "turn_start" | "action_declared" | "action_skipped_hard_cc" | "move_miss" | "move_hit" | "item_used" | "damage" | "heal" | "shield_applied" | "status_applied" | "status_refreshed" | "status_expired" | "fighter_fainted" | "turn_end" | "battle_end";
|
|
2
|
+
export type BattleEventKind = "turn_start" | "action_declared" | "action_skipped_hard_cc" | "move_miss" | "move_hit" | "item_used" | "status_cleared" | "damage" | "heal" | "shield_applied" | "status_applied" | "status_refreshed" | "status_expired" | "fighter_fainted" | "turn_end" | "battle_end";
|
|
3
3
|
export interface BaseBattleEvent {
|
|
4
4
|
id: string;
|
|
5
5
|
turnNumber: number;
|
package/dist/core/items.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { ItemId } from "./ids";
|
|
2
|
-
import type { EffectDefinition } from "./moves";
|
|
2
|
+
import type { EffectDefinition, TargetKind } from "./moves";
|
|
3
3
|
export type ItemCategory = "damage" | "heal_shield" | "status_buff";
|
|
4
4
|
export interface ItemDefinition {
|
|
5
5
|
id: ItemId;
|
|
6
6
|
name: string;
|
|
7
7
|
category: ItemCategory;
|
|
8
|
+
target: TargetKind;
|
|
8
9
|
maxUses: number;
|
|
9
10
|
image: string;
|
|
10
11
|
effects: EffectDefinition[];
|
package/dist/core/moves.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import type { MoveId,
|
|
1
|
+
import type { MoveId, StatusId, TypeId } from "./ids";
|
|
2
|
+
import { StatusKind } from "./status";
|
|
2
3
|
export type MoveKind = "damage" | "heal" | "status" | "hybrid";
|
|
3
4
|
export type TargetKind = "self" | "enemy";
|
|
4
|
-
export type EffectKind = "damage" | "heal" | "shield" | "modify_stats" | "apply_status" | "modify_hit_chance" | "modify_crit_chance" | "modify_priority" | "modify_effective_speed";
|
|
5
|
+
export type EffectKind = "damage" | "heal" | "shield" | "modify_stats" | "apply_status" | "clear_status" | "modify_hit_chance" | "modify_crit_chance" | "modify_priority" | "modify_effective_speed";
|
|
5
6
|
export interface DamageEffect {
|
|
6
7
|
kind: "damage";
|
|
7
8
|
amount: number;
|
|
@@ -28,6 +29,22 @@ export interface ApplyStatusEffect {
|
|
|
28
29
|
kind: "apply_status";
|
|
29
30
|
statusId: StatusId;
|
|
30
31
|
}
|
|
32
|
+
export interface ClearStatusEffect {
|
|
33
|
+
kind: "clear_status";
|
|
34
|
+
statusIds?: StatusId[];
|
|
35
|
+
/**
|
|
36
|
+
* Filtros opcionales por tipo de estado (hard_cc / soft).
|
|
37
|
+
* Ejemplo: una berry que limpia solo debuffs “soft”.
|
|
38
|
+
*/
|
|
39
|
+
kinds?: StatusKind[];
|
|
40
|
+
/**
|
|
41
|
+
* Si quieres que limpie TODO sin importar id ni tipo:
|
|
42
|
+
* - statusIds === undefined
|
|
43
|
+
* - kinds === undefined
|
|
44
|
+
* - clearAll === true
|
|
45
|
+
*/
|
|
46
|
+
clearAll?: boolean;
|
|
47
|
+
}
|
|
31
48
|
export interface ModifyHitChanceEffect {
|
|
32
49
|
kind: "modify_hit_chance";
|
|
33
50
|
delta: number;
|
|
@@ -44,7 +61,7 @@ export interface ModifyEffectiveSpeedEffect {
|
|
|
44
61
|
kind: "modify_effective_speed";
|
|
45
62
|
multiplier: number;
|
|
46
63
|
}
|
|
47
|
-
export type EffectDefinition = DamageEffect | HealEffect | ShieldEffect | ModifyStatsEffect | ApplyStatusEffect | ModifyHitChanceEffect | ModifyCritChanceEffect | ModifyPriorityEffect | ModifyEffectiveSpeedEffect;
|
|
64
|
+
export type EffectDefinition = DamageEffect | HealEffect | ShieldEffect | ClearStatusEffect | ModifyStatsEffect | ApplyStatusEffect | ModifyHitChanceEffect | ModifyCritChanceEffect | ModifyPriorityEffect | ModifyEffectiveSpeedEffect;
|
|
48
65
|
export interface MoveDefinition {
|
|
49
66
|
id: MoveId;
|
|
50
67
|
name: string;
|
|
@@ -1,53 +1,143 @@
|
|
|
1
1
|
export const POKEMON_ITEMS = [
|
|
2
|
-
{
|
|
3
|
-
id: "potion",
|
|
4
|
-
name: "Poción",
|
|
5
|
-
category: "heal_shield",
|
|
6
|
-
maxUses: 2,
|
|
7
|
-
effects: [{ kind: "heal", amount: 30 }],
|
|
8
|
-
image: 'antidote',
|
|
9
|
-
},
|
|
10
2
|
{
|
|
11
3
|
id: "super_potion",
|
|
12
4
|
name: "Super Poción",
|
|
13
5
|
category: "heal_shield",
|
|
6
|
+
target: "self",
|
|
14
7
|
maxUses: 1,
|
|
15
|
-
effects: [{ kind: "heal", amount:
|
|
16
|
-
image:
|
|
8
|
+
effects: [{ kind: "heal", amount: 50 }],
|
|
9
|
+
image: "hiper-potion",
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
id: "potion",
|
|
13
|
+
name: "Poción",
|
|
14
|
+
category: "heal_shield",
|
|
15
|
+
target: "self",
|
|
16
|
+
maxUses: 2,
|
|
17
|
+
effects: [{ kind: "heal", amount: 25 }],
|
|
18
|
+
image: "potion",
|
|
17
19
|
},
|
|
18
20
|
{
|
|
19
21
|
id: "apple",
|
|
20
22
|
name: "Manzana",
|
|
21
23
|
category: "heal_shield",
|
|
24
|
+
target: "self",
|
|
22
25
|
maxUses: 3,
|
|
23
|
-
effects: [{ kind: "heal", amount:
|
|
24
|
-
image:
|
|
26
|
+
effects: [{ kind: "heal", amount: 17 }],
|
|
27
|
+
image: "fancy-apple",
|
|
25
28
|
},
|
|
26
29
|
{
|
|
27
30
|
id: "mushroom",
|
|
28
31
|
name: "Seta",
|
|
29
32
|
category: "heal_shield",
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
+
target: "self",
|
|
34
|
+
maxUses: 4,
|
|
35
|
+
effects: [{ kind: "heal", amount: 14 }],
|
|
36
|
+
image: "mushroom",
|
|
33
37
|
},
|
|
34
38
|
{
|
|
35
|
-
id: "
|
|
36
|
-
name: "
|
|
37
|
-
|
|
39
|
+
id: "shotgun",
|
|
40
|
+
name: "Escopeta",
|
|
41
|
+
target: "enemy",
|
|
42
|
+
category: "damage",
|
|
43
|
+
maxUses: 2,
|
|
44
|
+
effects: [
|
|
45
|
+
{ kind: "damage", amount: 15 },
|
|
46
|
+
{ kind: "apply_status", statusId: "burn" },
|
|
47
|
+
],
|
|
48
|
+
image: "shotgun",
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: "pistol",
|
|
52
|
+
name: "Pistola",
|
|
53
|
+
category: "damage",
|
|
54
|
+
target: "enemy",
|
|
55
|
+
maxUses: 4,
|
|
56
|
+
effects: [
|
|
57
|
+
{ kind: "damage", amount: 8 },
|
|
58
|
+
{ kind: "apply_status", statusId: "burn" },
|
|
59
|
+
],
|
|
60
|
+
image: "pistol",
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: "riffle",
|
|
64
|
+
name: "Rifle",
|
|
65
|
+
target: "enemy",
|
|
66
|
+
category: "damage",
|
|
38
67
|
maxUses: 3,
|
|
39
|
-
effects: [
|
|
40
|
-
|
|
68
|
+
effects: [
|
|
69
|
+
{ kind: "damage", amount: 10 },
|
|
70
|
+
{ kind: "apply_status", statusId: "burn" },
|
|
71
|
+
],
|
|
72
|
+
image: "riffle",
|
|
41
73
|
},
|
|
42
74
|
{
|
|
43
|
-
id: "
|
|
44
|
-
name: "
|
|
75
|
+
id: "sniper",
|
|
76
|
+
name: "Sniper",
|
|
77
|
+
target: "enemy",
|
|
45
78
|
category: "damage",
|
|
46
79
|
maxUses: 1,
|
|
47
80
|
effects: [
|
|
48
|
-
{ kind: "damage", amount:
|
|
81
|
+
{ kind: "damage", amount: 30 },
|
|
49
82
|
{ kind: "apply_status", statusId: "burn" },
|
|
50
83
|
],
|
|
51
|
-
image:
|
|
84
|
+
image: "sniper",
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
id: "blue-berry",
|
|
88
|
+
name: "Baya azul",
|
|
89
|
+
category: "status_buff",
|
|
90
|
+
maxUses: 1,
|
|
91
|
+
target: "self",
|
|
92
|
+
effects: [
|
|
93
|
+
{
|
|
94
|
+
kind: "clear_status",
|
|
95
|
+
statusIds: ["burn"], // cura QUEMADURA
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
image: "blue-berry",
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
id: "pink-berry",
|
|
102
|
+
name: "Baya rosa",
|
|
103
|
+
category: "status_buff",
|
|
104
|
+
maxUses: 1,
|
|
105
|
+
target: "self",
|
|
106
|
+
effects: [
|
|
107
|
+
{
|
|
108
|
+
kind: "clear_status",
|
|
109
|
+
statusIds: ["poison"], // cura VENENO
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
image: "pink-berry",
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
id: "yellow-berry",
|
|
116
|
+
name: "Baya amarilla",
|
|
117
|
+
category: "status_buff",
|
|
118
|
+
maxUses: 1,
|
|
119
|
+
target: "self",
|
|
120
|
+
effects: [
|
|
121
|
+
{
|
|
122
|
+
kind: "clear_status",
|
|
123
|
+
statusIds: ["paralysis"], // cura PARÁLISIS
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
image: "yellow-berry",
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
id: "green-berry",
|
|
130
|
+
name: "Baya verde",
|
|
131
|
+
category: "status_buff",
|
|
132
|
+
maxUses: 1,
|
|
133
|
+
target: "self",
|
|
134
|
+
effects: [
|
|
135
|
+
{
|
|
136
|
+
kind: "clear_status",
|
|
137
|
+
statusIds: ["burn", "poison"], // por ejemplo: limpia quemadura o veneno
|
|
138
|
+
kinds: ["soft"], // redundante pero semántico
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
image: "green-berry",
|
|
52
142
|
},
|
|
53
143
|
];
|
|
@@ -37,12 +37,16 @@ export const POKEMON_MOVES = [
|
|
|
37
37
|
name: "Ascuas",
|
|
38
38
|
typeId: POKEMON_TYPE_IDS.fire,
|
|
39
39
|
kind: "damage",
|
|
40
|
-
basePower:
|
|
40
|
+
basePower: 20,
|
|
41
41
|
accuracy: 100,
|
|
42
42
|
maxPP: 25,
|
|
43
43
|
priority: 0,
|
|
44
44
|
target: "enemy",
|
|
45
45
|
effects: [
|
|
46
|
+
{
|
|
47
|
+
kind: "damage",
|
|
48
|
+
amount: 20,
|
|
49
|
+
},
|
|
46
50
|
{
|
|
47
51
|
kind: "apply_status",
|
|
48
52
|
statusId: "burn",
|
|
@@ -3,19 +3,39 @@ export const POKEMON_STATUSES = [
|
|
|
3
3
|
id: "burn",
|
|
4
4
|
name: "Quemado",
|
|
5
5
|
kind: "soft",
|
|
6
|
+
minDurationTurns: 3,
|
|
7
|
+
maxDurationTurns: 5,
|
|
8
|
+
effectsPerStack: [
|
|
9
|
+
{ kind: "damage", amount: 6 }, // DoT
|
|
10
|
+
{ kind: "modify_stats", offenseDelta: -10 } // pega menos
|
|
11
|
+
]
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
id: "poison",
|
|
15
|
+
name: "Envenenado",
|
|
16
|
+
kind: "soft",
|
|
17
|
+
minDurationTurns: 3,
|
|
18
|
+
maxDurationTurns: 5,
|
|
19
|
+
effectsPerStack: [
|
|
20
|
+
{ kind: "damage", amount: 8 }
|
|
21
|
+
]
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: "paralysis",
|
|
25
|
+
name: "Paralizado",
|
|
26
|
+
kind: "soft",
|
|
6
27
|
minDurationTurns: 2,
|
|
7
28
|
maxDurationTurns: 4,
|
|
8
29
|
effectsPerStack: [
|
|
9
|
-
|
|
10
|
-
{ kind: "damage", amount: 10 }
|
|
30
|
+
{ kind: "modify_effective_speed", multiplier: 0.5 }
|
|
11
31
|
]
|
|
12
32
|
},
|
|
13
33
|
{
|
|
14
|
-
id: "
|
|
15
|
-
name: "
|
|
34
|
+
id: "sleep",
|
|
35
|
+
name: "Dormido",
|
|
16
36
|
kind: "hard_cc",
|
|
17
37
|
minDurationTurns: 1,
|
|
18
|
-
maxDurationTurns:
|
|
19
|
-
effectsPerStack: []
|
|
38
|
+
maxDurationTurns: 3,
|
|
39
|
+
effectsPerStack: [] // el hard_cc lo interpreta el motor
|
|
20
40
|
}
|
|
21
41
|
];
|