pokemon-io-core 0.0.54 → 0.0.56
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 +26 -0
- package/dist/api/battle.js +2 -0
- package/dist/api/room.d.ts +21 -0
- package/dist/api/room.js +2 -0
- package/dist/api/socketTypes.d.ts +46 -0
- package/dist/api/socketTypes.js +1 -0
- package/dist/core/actions.d.ts +21 -0
- package/dist/core/actions.js +1 -0
- package/dist/core/amulets.d.ts +18 -0
- package/dist/core/amulets.js +1 -0
- package/dist/core/battleState.d.ts +39 -0
- package/dist/core/battleState.js +1 -0
- package/dist/core/engine.d.ts +45 -0
- package/dist/core/engine.js +869 -0
- package/dist/core/events.d.ts +67 -0
- package/dist/core/events.js +1 -0
- package/dist/core/fighters.d.ts +13 -0
- package/dist/core/fighters.js +1 -0
- package/dist/core/ids.d.ts +6 -0
- package/dist/core/ids.js +1 -0
- package/dist/core/index.d.ts +11 -0
- package/dist/core/index.js +11 -0
- package/dist/core/items.d.ts +12 -0
- package/dist/core/items.js +1 -0
- package/dist/core/moves.d.ts +73 -0
- package/dist/core/moves.js +2 -0
- package/dist/core/status.d.ts +11 -0
- package/dist/core/status.js +1 -0
- package/dist/core/types.d.ts +14 -0
- package/dist/core/types.js +1 -0
- package/dist/engine/index.d.ts +1 -0
- package/dist/engine/index.js +1 -0
- package/dist/engine/pokemonBattleService.d.ts +6 -0
- package/dist/engine/pokemonBattleService.js +32 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/skins/CombatSkin.d.ts +18 -0
- package/dist/skins/CombatSkin.js +1 -0
- package/dist/skins/index.d.ts +2 -0
- package/dist/skins/index.js +2 -0
- package/dist/skins/pokemon/fighters.d.ts +2 -0
- package/dist/skins/pokemon/fighters.js +174 -0
- package/dist/skins/pokemon/index.d.ts +6 -0
- package/dist/skins/pokemon/index.js +6 -0
- package/dist/skins/pokemon/items.d.ts +2 -0
- package/dist/skins/pokemon/items.js +143 -0
- package/dist/skins/pokemon/moves.d.ts +2 -0
- package/dist/skins/pokemon/moves.js +459 -0
- package/dist/skins/pokemon/pokemonSkin.d.ts +13 -0
- package/dist/skins/pokemon/pokemonSkin.js +91 -0
- package/dist/skins/pokemon/statuses.d.ts +2 -0
- package/dist/skins/pokemon/statuses.js +85 -0
- package/dist/skins/pokemon/types.d.ts +13 -0
- package/dist/skins/pokemon/types.js +97 -0
- package/package.json +7 -7
|
@@ -0,0 +1,869 @@
|
|
|
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
|
+
};
|
|
12
|
+
const DEFAULT_RULES = {
|
|
13
|
+
baseCritChance: 0.05,
|
|
14
|
+
critPerStat: 0.002,
|
|
15
|
+
critMultiplier: 1.3,
|
|
16
|
+
stabMultiplier: 1.2,
|
|
17
|
+
randomMinDamageFactor: 0.85,
|
|
18
|
+
randomMaxDamageFactor: 1.0,
|
|
19
|
+
};
|
|
20
|
+
const buildMap = (list) => list.reduce((acc, item) => {
|
|
21
|
+
acc[item.id] = item;
|
|
22
|
+
return acc;
|
|
23
|
+
}, {});
|
|
24
|
+
const cloneStats = (stats) => ({
|
|
25
|
+
offense: stats.offense,
|
|
26
|
+
defense: stats.defense,
|
|
27
|
+
speed: stats.speed,
|
|
28
|
+
crit: stats.crit,
|
|
29
|
+
});
|
|
30
|
+
// ------------------------------------------------------
|
|
31
|
+
// Creación de estado inicial
|
|
32
|
+
// ------------------------------------------------------
|
|
33
|
+
const createBattleFighter = (cfg) => {
|
|
34
|
+
if (cfg.moves.length !== 4) {
|
|
35
|
+
throw new Error("Each fighter must have exactly 4 moves in MVP");
|
|
36
|
+
}
|
|
37
|
+
if (cfg.items.length > 4) {
|
|
38
|
+
throw new Error("A fighter cannot have more than 4 items");
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
fighterId: cfg.fighter.id,
|
|
42
|
+
classId: cfg.fighter.classId,
|
|
43
|
+
maxHp: cfg.maxHp,
|
|
44
|
+
currentHp: cfg.maxHp,
|
|
45
|
+
baseStats: cloneStats(cfg.fighter.baseStats),
|
|
46
|
+
effectiveStats: cloneStats(cfg.fighter.baseStats),
|
|
47
|
+
moves: cfg.moves.map((move) => ({
|
|
48
|
+
moveId: move.id,
|
|
49
|
+
currentPP: move.maxPP,
|
|
50
|
+
})),
|
|
51
|
+
items: cfg.items.map((item) => ({
|
|
52
|
+
itemId: item.id,
|
|
53
|
+
usesRemaining: item.maxUses,
|
|
54
|
+
})),
|
|
55
|
+
amuletId: cfg.amulet ? cfg.amulet.id : null,
|
|
56
|
+
statuses: [],
|
|
57
|
+
isAlive: true,
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
const recomputeEffectiveStatsForFighter = (state, fighter) => {
|
|
61
|
+
// Partimos de base
|
|
62
|
+
let eff = { ...fighter.baseStats };
|
|
63
|
+
// 1) Aplicar estados
|
|
64
|
+
for (const st of fighter.statuses) {
|
|
65
|
+
const def = state.runtime.statusesById[st.statusId];
|
|
66
|
+
if (!def)
|
|
67
|
+
continue;
|
|
68
|
+
const stacks = st.stacks === 2 ? 2 : 1;
|
|
69
|
+
for (const effDef of def.effectsPerStack) {
|
|
70
|
+
const totalStacks = stacks; // podrías multiplicar efectos por stacks
|
|
71
|
+
switch (effDef.kind) {
|
|
72
|
+
case "modify_stats":
|
|
73
|
+
eff.offense += (effDef.offenseDelta ?? 0) * totalStacks;
|
|
74
|
+
eff.defense += (effDef.defenseDelta ?? 0) * totalStacks;
|
|
75
|
+
eff.speed += (effDef.speedDelta ?? 0) * totalStacks;
|
|
76
|
+
eff.crit += (effDef.critDelta ?? 0) * totalStacks;
|
|
77
|
+
break;
|
|
78
|
+
case "modify_effective_speed":
|
|
79
|
+
eff.speed = Math.floor(eff.speed * Math.pow(effDef.multiplier, totalStacks));
|
|
80
|
+
break;
|
|
81
|
+
default:
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// 2) Aquí podrías aplicar amuletos, buffs temporales o lo que quieras
|
|
87
|
+
return {
|
|
88
|
+
...fighter,
|
|
89
|
+
effectiveStats: eff,
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
const createPlayerBattleState = (cfg) => {
|
|
93
|
+
const fighter = createBattleFighter(cfg);
|
|
94
|
+
return {
|
|
95
|
+
fighterTeam: [fighter],
|
|
96
|
+
activeIndex: 0,
|
|
97
|
+
};
|
|
98
|
+
};
|
|
99
|
+
export const createInitialBattleState = (config) => {
|
|
100
|
+
const rules = {
|
|
101
|
+
...DEFAULT_RULES,
|
|
102
|
+
...config.rules,
|
|
103
|
+
};
|
|
104
|
+
const runtime = {
|
|
105
|
+
rules,
|
|
106
|
+
typesById: buildMap(config.types),
|
|
107
|
+
movesById: buildMap(config.moves),
|
|
108
|
+
itemsById: buildMap(config.items),
|
|
109
|
+
amuletsById: buildMap(config.amulets),
|
|
110
|
+
statusesById: buildMap(config.statuses),
|
|
111
|
+
typeEffectiveness: config.typeEffectiveness,
|
|
112
|
+
};
|
|
113
|
+
const player1 = createPlayerBattleState(config.player1);
|
|
114
|
+
const player2 = createPlayerBattleState(config.player2);
|
|
115
|
+
const state = {
|
|
116
|
+
turnNumber: 1,
|
|
117
|
+
player1,
|
|
118
|
+
player2,
|
|
119
|
+
runtime,
|
|
120
|
+
};
|
|
121
|
+
return state;
|
|
122
|
+
};
|
|
123
|
+
// ------------------------------------------------------
|
|
124
|
+
// Helpers
|
|
125
|
+
// ------------------------------------------------------
|
|
126
|
+
const getActiveFighter = (player) => player.fighterTeam[player.activeIndex];
|
|
127
|
+
const getOpponentAndSelf = (state, playerKey) => {
|
|
128
|
+
const selfPlayer = playerKey === "player1" ? state.player1 : state.player2;
|
|
129
|
+
const oppPlayer = playerKey === "player1" ? state.player2 : state.player1;
|
|
130
|
+
return {
|
|
131
|
+
self: getActiveFighter(selfPlayer),
|
|
132
|
+
opponent: getActiveFighter(oppPlayer),
|
|
133
|
+
};
|
|
134
|
+
};
|
|
135
|
+
const getTypeEffectiveness = (state, attackerTypeId, defenderTypeId) => {
|
|
136
|
+
const matrix = state.runtime.typeEffectiveness;
|
|
137
|
+
const row = matrix[attackerTypeId];
|
|
138
|
+
if (!row)
|
|
139
|
+
return 1;
|
|
140
|
+
return row[defenderTypeId] ?? 1;
|
|
141
|
+
};
|
|
142
|
+
const randomInRange = (min, max) => Math.random() * (max - min) + min;
|
|
143
|
+
const chance = (probability) => {
|
|
144
|
+
if (probability <= 0)
|
|
145
|
+
return false;
|
|
146
|
+
if (probability >= 1)
|
|
147
|
+
return true;
|
|
148
|
+
return Math.random() < probability;
|
|
149
|
+
};
|
|
150
|
+
// ------------------------------------------------------
|
|
151
|
+
// Cálculo de críticos y daño
|
|
152
|
+
// ------------------------------------------------------
|
|
153
|
+
const computeCritChance = (rules, critStat) => {
|
|
154
|
+
const raw = rules.baseCritChance + critStat * rules.critPerStat;
|
|
155
|
+
return Math.max(0, Math.min(1, raw));
|
|
156
|
+
};
|
|
157
|
+
const computeDamage = (input) => {
|
|
158
|
+
const { state, attacker, defender, moveTypeId, basePower, isCritical } = input;
|
|
159
|
+
const rules = state.runtime.rules;
|
|
160
|
+
const attackerOff = attacker.effectiveStats.offense;
|
|
161
|
+
const defenderDef = defender.effectiveStats.defense;
|
|
162
|
+
const typeEffectiveness = getTypeEffectiveness(state, moveTypeId, defender.classId);
|
|
163
|
+
if (typeEffectiveness === 0) {
|
|
164
|
+
return {
|
|
165
|
+
damage: 0,
|
|
166
|
+
effectiveness: 0,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
const stabMultiplier = attacker.classId === moveTypeId ? rules.stabMultiplier : 1;
|
|
170
|
+
const critMultiplier = isCritical ? rules.critMultiplier : 1;
|
|
171
|
+
const randomFactor = randomInRange(rules.randomMinDamageFactor, rules.randomMaxDamageFactor);
|
|
172
|
+
const rawDamage = basePower *
|
|
173
|
+
(attackerOff / Math.max(1, defenderDef)) *
|
|
174
|
+
typeEffectiveness *
|
|
175
|
+
stabMultiplier *
|
|
176
|
+
critMultiplier *
|
|
177
|
+
randomFactor;
|
|
178
|
+
const finalDamage = Math.max(1, Math.floor(rawDamage));
|
|
179
|
+
return {
|
|
180
|
+
damage: finalDamage,
|
|
181
|
+
effectiveness: typeEffectiveness,
|
|
182
|
+
};
|
|
183
|
+
};
|
|
184
|
+
let eventCounter = 0;
|
|
185
|
+
const nextEventId = () => {
|
|
186
|
+
eventCounter += 1;
|
|
187
|
+
return `evt_${eventCounter}`;
|
|
188
|
+
};
|
|
189
|
+
const createBaseEvent = (turnNumber, kind, message) => ({
|
|
190
|
+
id: nextEventId(),
|
|
191
|
+
kind,
|
|
192
|
+
turnNumber,
|
|
193
|
+
message,
|
|
194
|
+
timestamp: Date.now(),
|
|
195
|
+
});
|
|
196
|
+
const applyDamageToFighter = (state, defender, amount, actorId, isCritical) => {
|
|
197
|
+
const events = [];
|
|
198
|
+
const newHp = Math.max(0, defender.currentHp - amount);
|
|
199
|
+
const updatedDefender = {
|
|
200
|
+
...defender,
|
|
201
|
+
currentHp: newHp,
|
|
202
|
+
isAlive: newHp > 0,
|
|
203
|
+
};
|
|
204
|
+
events.push({
|
|
205
|
+
...createBaseEvent(state.turnNumber, "damage", `${actorId} inflige ${amount} de daño a ${defender.fighterId}`),
|
|
206
|
+
actorId,
|
|
207
|
+
targetId: defender.fighterId,
|
|
208
|
+
amount,
|
|
209
|
+
isCritical,
|
|
210
|
+
});
|
|
211
|
+
if (!updatedDefender.isAlive) {
|
|
212
|
+
events.push({
|
|
213
|
+
...createBaseEvent(state.turnNumber, "fighter_fainted", `${defender.fighterId} ha caído`),
|
|
214
|
+
fighterId: defender.fighterId,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
return { updatedDefender, events };
|
|
218
|
+
};
|
|
219
|
+
// ------------------------------------------------------
|
|
220
|
+
// apply_status
|
|
221
|
+
// ------------------------------------------------------
|
|
222
|
+
const applyStatusToFighter = (state, target, statusId) => {
|
|
223
|
+
const def = state.runtime.statusesById[statusId];
|
|
224
|
+
dbg("APPLY_STATUS_FN", {
|
|
225
|
+
targetId: target.fighterId,
|
|
226
|
+
statusId,
|
|
227
|
+
hasDef: !!def,
|
|
228
|
+
currentStatuses: target.statuses,
|
|
229
|
+
});
|
|
230
|
+
if (!def)
|
|
231
|
+
return { updated: target, events: [] };
|
|
232
|
+
const events = [];
|
|
233
|
+
const existing = target.statuses.find((s) => s.statusId === statusId);
|
|
234
|
+
const duration = Math.floor(randomInRange(def.minDurationTurns, def.maxDurationTurns + 1)) ||
|
|
235
|
+
def.minDurationTurns;
|
|
236
|
+
let newStatuses = [...target.statuses];
|
|
237
|
+
if (!existing) {
|
|
238
|
+
newStatuses.push({
|
|
239
|
+
statusId,
|
|
240
|
+
stacks: 1,
|
|
241
|
+
remainingTurns: duration,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
else if (existing.stacks === 1) {
|
|
245
|
+
newStatuses = newStatuses.map((s) => s.statusId === statusId
|
|
246
|
+
? { ...s, stacks: 2, remainingTurns: duration }
|
|
247
|
+
: s);
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
newStatuses = newStatuses.map((s) => s.statusId === statusId ? { ...s, remainingTurns: duration } : s);
|
|
251
|
+
}
|
|
252
|
+
dbg("STATUS_APPLY", {
|
|
253
|
+
fighterId: target.fighterId,
|
|
254
|
+
statusId,
|
|
255
|
+
duration,
|
|
256
|
+
prevStacks: existing?.stacks ?? 0,
|
|
257
|
+
});
|
|
258
|
+
events.push({
|
|
259
|
+
...createBaseEvent(state.turnNumber, "status_applied", `Se aplica ${statusId} a ${target.fighterId}`),
|
|
260
|
+
targetId: target.fighterId,
|
|
261
|
+
statusId,
|
|
262
|
+
});
|
|
263
|
+
return {
|
|
264
|
+
updated: { ...target, statuses: newStatuses },
|
|
265
|
+
events,
|
|
266
|
+
};
|
|
267
|
+
};
|
|
268
|
+
// ------------------------------------------------------
|
|
269
|
+
// clear_status
|
|
270
|
+
// ------------------------------------------------------
|
|
271
|
+
const clearStatusFromFighter = (state, target, effect) => {
|
|
272
|
+
const { statusIds, kinds, clearAll } = effect;
|
|
273
|
+
const events = [];
|
|
274
|
+
const before = target.statuses;
|
|
275
|
+
let after = before;
|
|
276
|
+
if (clearAll) {
|
|
277
|
+
after = [];
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
after = before.filter((st) => {
|
|
281
|
+
const def = state.runtime.statusesById[st.statusId];
|
|
282
|
+
if (statusIds && statusIds.includes(st.statusId)) {
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
if (kinds && def && kinds.includes(def.kind)) {
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
return true;
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
if (after.length === before.length) {
|
|
292
|
+
// nada que limpiar
|
|
293
|
+
return { updated: target, events };
|
|
294
|
+
}
|
|
295
|
+
const removed = before
|
|
296
|
+
.filter((old) => !after.some((st) => st.statusId === old.statusId))
|
|
297
|
+
.map((st) => st.statusId);
|
|
298
|
+
dbg("STATUS_CLEAR", {
|
|
299
|
+
fighterId: target.fighterId,
|
|
300
|
+
cleared: removed,
|
|
301
|
+
clearAll: !!clearAll,
|
|
302
|
+
});
|
|
303
|
+
events.push({
|
|
304
|
+
...createBaseEvent(state.turnNumber, "status_cleared", `Se limpian estados (${removed.join(", ")}) de ${target.fighterId}`),
|
|
305
|
+
targetId: target.fighterId,
|
|
306
|
+
statusIds: removed,
|
|
307
|
+
});
|
|
308
|
+
return {
|
|
309
|
+
updated: {
|
|
310
|
+
...target,
|
|
311
|
+
statuses: after,
|
|
312
|
+
},
|
|
313
|
+
events,
|
|
314
|
+
};
|
|
315
|
+
};
|
|
316
|
+
// ------------------------------------------------------
|
|
317
|
+
// heal
|
|
318
|
+
// ------------------------------------------------------
|
|
319
|
+
const applyHealToFighter = (state, target, amount, sourceId) => {
|
|
320
|
+
const newHp = Math.min(target.maxHp, target.currentHp + amount);
|
|
321
|
+
const healed = newHp - target.currentHp;
|
|
322
|
+
const updated = {
|
|
323
|
+
...target,
|
|
324
|
+
currentHp: newHp,
|
|
325
|
+
isAlive: newHp > 0,
|
|
326
|
+
};
|
|
327
|
+
const events = [];
|
|
328
|
+
dbg("HEAL", {
|
|
329
|
+
sourceId,
|
|
330
|
+
targetId: target.fighterId,
|
|
331
|
+
healed,
|
|
332
|
+
newHp,
|
|
333
|
+
});
|
|
334
|
+
events.push({
|
|
335
|
+
...createBaseEvent(state.turnNumber, "heal", `${sourceId} cura ${healed} a ${target.fighterId}`),
|
|
336
|
+
actorId: sourceId,
|
|
337
|
+
targetId: target.fighterId,
|
|
338
|
+
amount: healed,
|
|
339
|
+
});
|
|
340
|
+
return { updated, events };
|
|
341
|
+
};
|
|
342
|
+
// ------------------------------------------------------
|
|
343
|
+
// Target por defecto y resolución de target
|
|
344
|
+
// ------------------------------------------------------
|
|
345
|
+
const defaultTargetForKind = (kind) => {
|
|
346
|
+
switch (kind) {
|
|
347
|
+
case "damage":
|
|
348
|
+
case "apply_status":
|
|
349
|
+
return "enemy";
|
|
350
|
+
case "heal":
|
|
351
|
+
case "clear_status":
|
|
352
|
+
case "modify_stats":
|
|
353
|
+
case "modify_hit_chance":
|
|
354
|
+
case "modify_crit_chance":
|
|
355
|
+
case "modify_priority":
|
|
356
|
+
case "modify_effective_speed":
|
|
357
|
+
case "shield":
|
|
358
|
+
return "self";
|
|
359
|
+
default:
|
|
360
|
+
return "self";
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
const resolveEffectTarget = (eff, actor, target) => {
|
|
364
|
+
const t = eff.target ?? defaultTargetForKind(eff.kind);
|
|
365
|
+
const isSelf = t === "self";
|
|
366
|
+
return {
|
|
367
|
+
primary: isSelf ? actor : target,
|
|
368
|
+
isSelf,
|
|
369
|
+
};
|
|
370
|
+
};
|
|
371
|
+
// ------------------------------------------------------
|
|
372
|
+
// Motor de efectos genérico (move + item)
|
|
373
|
+
// ------------------------------------------------------
|
|
374
|
+
const applyEffectsOnTarget = (state, actor, target, moveTypeId, isCritical, effects) => {
|
|
375
|
+
dbg("applyEffectsOnTarget", {
|
|
376
|
+
actorId: actor.fighterId,
|
|
377
|
+
targetId: target.fighterId,
|
|
378
|
+
moveTypeId,
|
|
379
|
+
isCritical,
|
|
380
|
+
effects,
|
|
381
|
+
});
|
|
382
|
+
let currentActor = actor;
|
|
383
|
+
let currentTarget = target;
|
|
384
|
+
const events = [];
|
|
385
|
+
dbg("APPLY_EFFECTS_START", {
|
|
386
|
+
actorId: actor.fighterId,
|
|
387
|
+
targetId: target.fighterId,
|
|
388
|
+
moveTypeId,
|
|
389
|
+
isCritical,
|
|
390
|
+
effectsCount: effects?.length ?? 0,
|
|
391
|
+
});
|
|
392
|
+
for (const eff of effects) {
|
|
393
|
+
dbg("EFFECT_LOOP", eff);
|
|
394
|
+
switch (eff.kind) {
|
|
395
|
+
case "heal": {
|
|
396
|
+
dbg("EFFECT_HEAL_START", {
|
|
397
|
+
amount: eff.amount,
|
|
398
|
+
targetDefault: eff.target,
|
|
399
|
+
});
|
|
400
|
+
const { primary, isSelf } = resolveEffectTarget(eff, currentActor, currentTarget);
|
|
401
|
+
const res = applyHealToFighter(state, primary, eff.amount, actor.fighterId);
|
|
402
|
+
if (isSelf)
|
|
403
|
+
currentActor = res.updated;
|
|
404
|
+
else
|
|
405
|
+
currentTarget = res.updated;
|
|
406
|
+
events.push(...res.events);
|
|
407
|
+
break;
|
|
408
|
+
}
|
|
409
|
+
case "apply_status": {
|
|
410
|
+
dbg("EFFECT_APPLY_STATUS_START", {
|
|
411
|
+
statusId: eff.statusId,
|
|
412
|
+
targetDefault: eff.target,
|
|
413
|
+
});
|
|
414
|
+
const { primary, isSelf } = resolveEffectTarget(eff, currentActor, currentTarget);
|
|
415
|
+
const res = applyStatusToFighter(state, primary, eff.statusId);
|
|
416
|
+
if (isSelf)
|
|
417
|
+
currentActor = res.updated;
|
|
418
|
+
else
|
|
419
|
+
currentTarget = res.updated;
|
|
420
|
+
events.push(...res.events);
|
|
421
|
+
break;
|
|
422
|
+
}
|
|
423
|
+
case "clear_status": {
|
|
424
|
+
dbg("debemos eliminar status");
|
|
425
|
+
const { primary, isSelf } = resolveEffectTarget(eff, currentActor, currentTarget);
|
|
426
|
+
const res = clearStatusFromFighter(state, primary, eff);
|
|
427
|
+
if (isSelf)
|
|
428
|
+
currentActor = res.updated;
|
|
429
|
+
else
|
|
430
|
+
currentTarget = res.updated;
|
|
431
|
+
events.push(...res.events);
|
|
432
|
+
break;
|
|
433
|
+
}
|
|
434
|
+
case "damage": {
|
|
435
|
+
dbg("debemos hacer daño");
|
|
436
|
+
const { primary, isSelf } = resolveEffectTarget(eff, currentActor, currentTarget);
|
|
437
|
+
let totalDamage = 0;
|
|
438
|
+
// Daño de fórmula (stats + tipo + crit…)
|
|
439
|
+
if (eff.basePower && moveTypeId) {
|
|
440
|
+
const { damage } = computeDamage({
|
|
441
|
+
state,
|
|
442
|
+
attacker: currentActor,
|
|
443
|
+
defender: primary,
|
|
444
|
+
moveTypeId,
|
|
445
|
+
basePower: eff.basePower,
|
|
446
|
+
isCritical,
|
|
447
|
+
});
|
|
448
|
+
totalDamage += damage;
|
|
449
|
+
}
|
|
450
|
+
// Daño plano adicional
|
|
451
|
+
if (eff.flatAmount && eff.flatAmount > 0) {
|
|
452
|
+
totalDamage += eff.flatAmount;
|
|
453
|
+
}
|
|
454
|
+
if (totalDamage > 0) {
|
|
455
|
+
const res = applyDamageToFighter(state, primary, totalDamage, currentActor.fighterId, isCritical);
|
|
456
|
+
if (isSelf)
|
|
457
|
+
currentActor = res.updatedDefender;
|
|
458
|
+
else
|
|
459
|
+
currentTarget = res.updatedDefender;
|
|
460
|
+
events.push(...res.events);
|
|
461
|
+
}
|
|
462
|
+
break;
|
|
463
|
+
}
|
|
464
|
+
// TODO: conectar shield, modify_stats, etc.
|
|
465
|
+
default:
|
|
466
|
+
break;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return { actor: currentActor, target: currentTarget, events };
|
|
470
|
+
};
|
|
471
|
+
const getMovePriorityAndSpeed = (state, playerKey, action) => {
|
|
472
|
+
const { self } = getOpponentAndSelf(state, playerKey);
|
|
473
|
+
if (action.kind === "no_action") {
|
|
474
|
+
return { priority: 0, speed: 0 };
|
|
475
|
+
}
|
|
476
|
+
if (action.kind === "use_move") {
|
|
477
|
+
const moveEntry = self.moves[action.moveIndex];
|
|
478
|
+
if (!moveEntry) {
|
|
479
|
+
return { priority: 0, speed: self.effectiveStats.speed };
|
|
480
|
+
}
|
|
481
|
+
const moveDef = state.runtime.movesById[moveEntry.moveId];
|
|
482
|
+
const priority = moveDef?.priority ?? 0;
|
|
483
|
+
return {
|
|
484
|
+
priority,
|
|
485
|
+
speed: self.effectiveStats.speed,
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
// Items y switch: prioridad base 0 por ahora
|
|
489
|
+
return {
|
|
490
|
+
priority: 0,
|
|
491
|
+
speed: self.effectiveStats.speed,
|
|
492
|
+
};
|
|
493
|
+
};
|
|
494
|
+
// ------------------------------------------------------
|
|
495
|
+
// use_move
|
|
496
|
+
// ------------------------------------------------------
|
|
497
|
+
const resolveDamageMove = (state, playerKey, action) => {
|
|
498
|
+
if (action.kind !== "use_move") {
|
|
499
|
+
return { state, events: [] };
|
|
500
|
+
}
|
|
501
|
+
const { self, opponent } = getOpponentAndSelf(state, playerKey);
|
|
502
|
+
const events = [];
|
|
503
|
+
const moveSlot = self.moves[action.moveIndex];
|
|
504
|
+
dbg("use_move: slot", moveSlot);
|
|
505
|
+
if (!moveSlot) {
|
|
506
|
+
dbg("use_move: empty slot", { playerKey, moveIndex: action.moveIndex });
|
|
507
|
+
return { state, events };
|
|
508
|
+
}
|
|
509
|
+
const move = state.runtime.movesById[moveSlot.moveId];
|
|
510
|
+
if (!move) {
|
|
511
|
+
dbg("use_move: move definition not found", {
|
|
512
|
+
playerKey,
|
|
513
|
+
moveId: moveSlot.moveId,
|
|
514
|
+
});
|
|
515
|
+
return { state, events };
|
|
516
|
+
}
|
|
517
|
+
if (moveSlot.currentPP <= 0) {
|
|
518
|
+
dbg("use_move: no PP", {
|
|
519
|
+
playerKey,
|
|
520
|
+
fighterId: self.fighterId,
|
|
521
|
+
moveId: move.id,
|
|
522
|
+
});
|
|
523
|
+
return { state, events };
|
|
524
|
+
}
|
|
525
|
+
events.push({
|
|
526
|
+
...createBaseEvent(state.turnNumber, "action_declared", `${self.fighterId} usa ${move.name}`),
|
|
527
|
+
actorId: self.fighterId,
|
|
528
|
+
});
|
|
529
|
+
const accuracy = move.accuracy ?? 100;
|
|
530
|
+
const hitRoll = randomInRange(0, 100);
|
|
531
|
+
const hit = hitRoll < accuracy;
|
|
532
|
+
const updatedMoves = self.moves.map((m, idx) => idx === action.moveIndex
|
|
533
|
+
? { ...m, currentPP: Math.max(0, m.currentPP - 1) }
|
|
534
|
+
: m);
|
|
535
|
+
let updatedSelf = { ...self, moves: updatedMoves };
|
|
536
|
+
let updatedOpponent = { ...opponent };
|
|
537
|
+
if (!hit) {
|
|
538
|
+
dbg("MISS", {
|
|
539
|
+
fighterId: self.fighterId,
|
|
540
|
+
moveId: move.id,
|
|
541
|
+
moveName: move.name,
|
|
542
|
+
hitRoll,
|
|
543
|
+
accuracy,
|
|
544
|
+
});
|
|
545
|
+
events.push({
|
|
546
|
+
...createBaseEvent(state.turnNumber, "move_miss", `${self.fighterId} falla ${move.name}`),
|
|
547
|
+
actorId: self.fighterId,
|
|
548
|
+
moveId: move.id,
|
|
549
|
+
targetId: opponent.fighterId,
|
|
550
|
+
});
|
|
551
|
+
const newState = updateFightersInState(state, playerKey, updatedSelf, updatedOpponent);
|
|
552
|
+
return { state: newState, events };
|
|
553
|
+
}
|
|
554
|
+
const critChance = computeCritChance(state.runtime.rules, updatedSelf.effectiveStats.crit);
|
|
555
|
+
const isCritical = chance(critChance);
|
|
556
|
+
const effectiveness = getTypeEffectiveness(state, move.typeId, updatedOpponent.classId);
|
|
557
|
+
dbg("HIT", {
|
|
558
|
+
fighterId: self.fighterId,
|
|
559
|
+
moveId: move.id,
|
|
560
|
+
moveName: move.name,
|
|
561
|
+
isCritical,
|
|
562
|
+
effectiveness,
|
|
563
|
+
defenderBeforeHp: opponent.currentHp,
|
|
564
|
+
});
|
|
565
|
+
events.push({
|
|
566
|
+
...createBaseEvent(state.turnNumber, "move_hit", `${self.fighterId} acierta ${move.name}`),
|
|
567
|
+
actorId: self.fighterId,
|
|
568
|
+
moveId: move.id,
|
|
569
|
+
targetId: opponent.fighterId,
|
|
570
|
+
isCritical,
|
|
571
|
+
effectiveness,
|
|
572
|
+
});
|
|
573
|
+
dbg("MOVE_EFFECTS", {
|
|
574
|
+
moveId: move.id,
|
|
575
|
+
moveName: move.name,
|
|
576
|
+
effects: move.effects,
|
|
577
|
+
});
|
|
578
|
+
// Todos los efectos (daño, aplicar estado, curas, etc.)
|
|
579
|
+
const { actor: finalSelf, target: finalOpp, events: extraEvents, } = applyEffectsOnTarget(state, updatedSelf, updatedOpponent, move.typeId, isCritical, move.effects);
|
|
580
|
+
events.push(...extraEvents);
|
|
581
|
+
const newState = updateFightersInState(state, playerKey, finalSelf, finalOpp);
|
|
582
|
+
return { state: newState, events };
|
|
583
|
+
};
|
|
584
|
+
// ------------------------------------------------------
|
|
585
|
+
// use_item (ahora el target real está en cada efecto)
|
|
586
|
+
// ------------------------------------------------------
|
|
587
|
+
const resolveItemUse = (state, playerKey, action) => {
|
|
588
|
+
if (action.kind !== "use_item") {
|
|
589
|
+
return { state, events: [] };
|
|
590
|
+
}
|
|
591
|
+
const { self, opponent } = getOpponentAndSelf(state, playerKey);
|
|
592
|
+
const events = [];
|
|
593
|
+
const itemSlot = self.items[action.itemIndex];
|
|
594
|
+
if (!itemSlot || itemSlot.usesRemaining <= 0) {
|
|
595
|
+
dbg("use_item: no charges", {
|
|
596
|
+
playerKey,
|
|
597
|
+
itemIndex: action.itemIndex,
|
|
598
|
+
});
|
|
599
|
+
return { state, events };
|
|
600
|
+
}
|
|
601
|
+
const itemDef = state.runtime.itemsById[itemSlot.itemId];
|
|
602
|
+
if (!itemDef) {
|
|
603
|
+
dbg("use_item: item definition not found", {
|
|
604
|
+
playerKey,
|
|
605
|
+
itemId: itemSlot.itemId,
|
|
606
|
+
});
|
|
607
|
+
return { state, events };
|
|
608
|
+
}
|
|
609
|
+
const updatedItems = self.items.map((it, idx) => idx === action.itemIndex
|
|
610
|
+
? { ...it, usesRemaining: Math.max(0, it.usesRemaining - 1) }
|
|
611
|
+
: it);
|
|
612
|
+
let updatedSelf = { ...self, items: updatedItems };
|
|
613
|
+
let updatedOpponent = { ...opponent };
|
|
614
|
+
events.push({
|
|
615
|
+
...createBaseEvent(state.turnNumber, "action_declared", `${self.fighterId} usa objeto ${itemDef.name}`),
|
|
616
|
+
actorId: self.fighterId,
|
|
617
|
+
});
|
|
618
|
+
const { actor: finalSelf, target: finalOpp, events: extraEvents, } = applyEffectsOnTarget(state, updatedSelf, updatedOpponent, null, // los items no usan fórmula de tipo por ahora
|
|
619
|
+
false, itemDef.effects);
|
|
620
|
+
events.push(...extraEvents);
|
|
621
|
+
const newState = updateFightersInState(state, playerKey, finalSelf, finalOpp);
|
|
622
|
+
return { state: newState, events };
|
|
623
|
+
};
|
|
624
|
+
// ------------------------------------------------------
|
|
625
|
+
// Actualizar fighters en el estado
|
|
626
|
+
// ------------------------------------------------------
|
|
627
|
+
const updateFightersInState = (state, actingPlayerKey, updatedSelf, updatedOpponent) => {
|
|
628
|
+
const player1 = { ...state.player1 };
|
|
629
|
+
const player2 = { ...state.player2 };
|
|
630
|
+
const updateInPlayer = (player, updated) => {
|
|
631
|
+
const newTeam = player.fighterTeam.map((f, idx) => idx === player.activeIndex ? updated : f);
|
|
632
|
+
return {
|
|
633
|
+
...player,
|
|
634
|
+
fighterTeam: newTeam,
|
|
635
|
+
};
|
|
636
|
+
};
|
|
637
|
+
if (actingPlayerKey === "player1") {
|
|
638
|
+
const selfPlayer = updateInPlayer(player1, updatedSelf);
|
|
639
|
+
const oppPlayer = updateInPlayer(player2, updatedOpponent);
|
|
640
|
+
return {
|
|
641
|
+
...state,
|
|
642
|
+
player1: selfPlayer,
|
|
643
|
+
player2: oppPlayer,
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
const selfPlayer = updateInPlayer(player2, updatedSelf);
|
|
647
|
+
const oppPlayer = updateInPlayer(player1, updatedOpponent);
|
|
648
|
+
return {
|
|
649
|
+
...state,
|
|
650
|
+
player1: oppPlayer,
|
|
651
|
+
player2: selfPlayer,
|
|
652
|
+
};
|
|
653
|
+
};
|
|
654
|
+
// ------------------------------------------------------
|
|
655
|
+
// CC duro + check winner
|
|
656
|
+
// ------------------------------------------------------
|
|
657
|
+
const hasHardCc = (state, fighter) => fighter.statuses.some((st) => {
|
|
658
|
+
const def = state.runtime.statusesById[st.statusId];
|
|
659
|
+
return def?.kind === "hard_cc";
|
|
660
|
+
});
|
|
661
|
+
const checkWinner = (state) => {
|
|
662
|
+
const f1 = getActiveFighter(state.player1);
|
|
663
|
+
const f2 = getActiveFighter(state.player2);
|
|
664
|
+
const p1Alive = f1.isAlive && f1.currentHp > 0;
|
|
665
|
+
const p2Alive = f2.isAlive && f2.currentHp > 0;
|
|
666
|
+
if (p1Alive && !p2Alive)
|
|
667
|
+
return "player1";
|
|
668
|
+
if (!p1Alive && p2Alive)
|
|
669
|
+
return "player2";
|
|
670
|
+
if (!p1Alive && !p2Alive)
|
|
671
|
+
return "draw";
|
|
672
|
+
return "none";
|
|
673
|
+
};
|
|
674
|
+
// ------------------------------------------------------
|
|
675
|
+
// Fin de turno: aplicar DoT de estados
|
|
676
|
+
// ------------------------------------------------------
|
|
677
|
+
const applyEndOfTurnStatuses = (state) => {
|
|
678
|
+
const events = [];
|
|
679
|
+
const applyForPlayer = (player) => {
|
|
680
|
+
const active = getActiveFighter(player);
|
|
681
|
+
let updated = { ...active };
|
|
682
|
+
const updatedStatuses = [];
|
|
683
|
+
for (const st of active.statuses) {
|
|
684
|
+
const def = state.runtime.statusesById[st.statusId];
|
|
685
|
+
if (!def) {
|
|
686
|
+
dbg("STATUS_MISSING_DEF", {
|
|
687
|
+
fighterId: active.fighterId,
|
|
688
|
+
statusId: st.statusId,
|
|
689
|
+
});
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
692
|
+
let damageFromStatus = 0;
|
|
693
|
+
def.effectsPerStack.forEach((eff) => {
|
|
694
|
+
if (eff.kind === "damage") {
|
|
695
|
+
const stacksMultiplier = st.stacks === 2 ? 2 : 1;
|
|
696
|
+
const base = typeof eff.flatAmount === "number"
|
|
697
|
+
? eff.flatAmount
|
|
698
|
+
: typeof eff.basePower === "number"
|
|
699
|
+
? eff.basePower
|
|
700
|
+
: 10; // fallback
|
|
701
|
+
damageFromStatus += base * stacksMultiplier;
|
|
702
|
+
}
|
|
703
|
+
});
|
|
704
|
+
if (damageFromStatus > 0 && updated.isAlive) {
|
|
705
|
+
dbg("STATUS_DOT", {
|
|
706
|
+
fighterId: updated.fighterId,
|
|
707
|
+
statusId: st.statusId,
|
|
708
|
+
damageFromStatus,
|
|
709
|
+
stacks: st.stacks,
|
|
710
|
+
});
|
|
711
|
+
const damageRes = applyDamageToFighter(state, updated, damageFromStatus, updated.fighterId, false);
|
|
712
|
+
updated = damageRes.updatedDefender;
|
|
713
|
+
events.push(...damageRes.events);
|
|
714
|
+
}
|
|
715
|
+
const remaining = st.remainingTurns - 1;
|
|
716
|
+
if (remaining > 0) {
|
|
717
|
+
updatedStatuses.push({
|
|
718
|
+
...st,
|
|
719
|
+
remainingTurns: remaining,
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
else {
|
|
723
|
+
dbg("STATUS_EXPIRE", {
|
|
724
|
+
fighterId: updated.fighterId,
|
|
725
|
+
statusId: st.statusId,
|
|
726
|
+
});
|
|
727
|
+
events.push({
|
|
728
|
+
...createBaseEvent(state.turnNumber, "status_expired", `El estado ${st.statusId} expira en ${updated.fighterId}`),
|
|
729
|
+
targetId: updated.fighterId,
|
|
730
|
+
statusId: st.statusId,
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
updated = {
|
|
735
|
+
...updated,
|
|
736
|
+
statuses: updatedStatuses,
|
|
737
|
+
};
|
|
738
|
+
const newTeam = player.fighterTeam.map((f, idx) => idx === player.activeIndex ? updated : f);
|
|
739
|
+
return {
|
|
740
|
+
...player,
|
|
741
|
+
fighterTeam: newTeam,
|
|
742
|
+
};
|
|
743
|
+
};
|
|
744
|
+
const p1 = applyForPlayer(state.player1);
|
|
745
|
+
const p2 = applyForPlayer(state.player2);
|
|
746
|
+
const newState = {
|
|
747
|
+
...state,
|
|
748
|
+
player1: p1,
|
|
749
|
+
player2: p2,
|
|
750
|
+
};
|
|
751
|
+
return { state: newState, events };
|
|
752
|
+
};
|
|
753
|
+
// ------------------------------------------------------
|
|
754
|
+
// Bucle principal de turno
|
|
755
|
+
// ------------------------------------------------------
|
|
756
|
+
export const resolveTurn = (state, actions) => {
|
|
757
|
+
const runtimeState = state;
|
|
758
|
+
let currentState = runtimeState;
|
|
759
|
+
const recalcForPlayer = (player) => {
|
|
760
|
+
const team = player.fighterTeam.map((f) => recomputeEffectiveStatsForFighter(runtimeState, f));
|
|
761
|
+
return { ...player, fighterTeam: team };
|
|
762
|
+
};
|
|
763
|
+
currentState = {
|
|
764
|
+
...currentState,
|
|
765
|
+
player1: recalcForPlayer(currentState.player1),
|
|
766
|
+
player2: recalcForPlayer(currentState.player2),
|
|
767
|
+
};
|
|
768
|
+
const events = [];
|
|
769
|
+
dbg(`TURN ${runtimeState.turnNumber} start`, {
|
|
770
|
+
p1Action: actions.player1.kind,
|
|
771
|
+
p2Action: actions.player2.kind,
|
|
772
|
+
});
|
|
773
|
+
events.push({
|
|
774
|
+
...createBaseEvent(runtimeState.turnNumber, "turn_start", `Comienza el turno ${runtimeState.turnNumber}`),
|
|
775
|
+
});
|
|
776
|
+
const entries = [
|
|
777
|
+
{
|
|
778
|
+
playerKey: "player1",
|
|
779
|
+
action: actions.player1,
|
|
780
|
+
...getMovePriorityAndSpeed(runtimeState, "player1", actions.player1),
|
|
781
|
+
},
|
|
782
|
+
{
|
|
783
|
+
playerKey: "player2",
|
|
784
|
+
action: actions.player2,
|
|
785
|
+
...getMovePriorityAndSpeed(runtimeState, "player2", actions.player2),
|
|
786
|
+
},
|
|
787
|
+
];
|
|
788
|
+
entries.sort((a, b) => {
|
|
789
|
+
if (b.priority !== a.priority) {
|
|
790
|
+
return b.priority - a.priority;
|
|
791
|
+
}
|
|
792
|
+
if (b.speed !== a.speed) {
|
|
793
|
+
return b.speed - a.speed;
|
|
794
|
+
}
|
|
795
|
+
// empate total → coin flip
|
|
796
|
+
return Math.random() < 0.5 ? -1 : 1;
|
|
797
|
+
});
|
|
798
|
+
dbg(`TURN ${runtimeState.turnNumber} order`, entries.map((e) => ({
|
|
799
|
+
player: e.playerKey,
|
|
800
|
+
action: e.action.kind,
|
|
801
|
+
priority: e.priority,
|
|
802
|
+
speed: e.speed,
|
|
803
|
+
})));
|
|
804
|
+
for (const entry of entries) {
|
|
805
|
+
const { playerKey, action } = entry;
|
|
806
|
+
if (action.kind === "no_action")
|
|
807
|
+
continue;
|
|
808
|
+
const { self } = getOpponentAndSelf(currentState, playerKey);
|
|
809
|
+
if (!self.isAlive || self.currentHp <= 0)
|
|
810
|
+
continue;
|
|
811
|
+
if (hasHardCc(currentState, self)) {
|
|
812
|
+
events.push({
|
|
813
|
+
...createBaseEvent(currentState.turnNumber, "action_skipped_hard_cc", `${self.fighterId} no puede actuar por control total`),
|
|
814
|
+
actorId: self.fighterId,
|
|
815
|
+
});
|
|
816
|
+
continue;
|
|
817
|
+
}
|
|
818
|
+
if (action.kind === "use_move") {
|
|
819
|
+
const result = resolveDamageMove(currentState, playerKey, action);
|
|
820
|
+
currentState = result.state;
|
|
821
|
+
events.push(...result.events);
|
|
822
|
+
}
|
|
823
|
+
else if (action.kind === "use_item") {
|
|
824
|
+
const result = resolveItemUse(currentState, playerKey, action);
|
|
825
|
+
currentState = result.state;
|
|
826
|
+
events.push(...result.events);
|
|
827
|
+
}
|
|
828
|
+
else {
|
|
829
|
+
// switch_fighter u otros no implementados aún
|
|
830
|
+
continue;
|
|
831
|
+
}
|
|
832
|
+
const winnerAfterAction = checkWinner(currentState);
|
|
833
|
+
if (winnerAfterAction !== "none") {
|
|
834
|
+
events.push({
|
|
835
|
+
...createBaseEvent(currentState.turnNumber, "battle_end", `La batalla termina: ${winnerAfterAction}`),
|
|
836
|
+
winner: winnerAfterAction,
|
|
837
|
+
});
|
|
838
|
+
const finalState = {
|
|
839
|
+
...currentState,
|
|
840
|
+
turnNumber: currentState.turnNumber + 1,
|
|
841
|
+
};
|
|
842
|
+
return {
|
|
843
|
+
newState: finalState,
|
|
844
|
+
events,
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
const statusResult = applyEndOfTurnStatuses(currentState);
|
|
849
|
+
currentState = statusResult.state;
|
|
850
|
+
events.push(...statusResult.events);
|
|
851
|
+
const winnerAtEnd = checkWinner(currentState);
|
|
852
|
+
if (winnerAtEnd !== "none") {
|
|
853
|
+
events.push({
|
|
854
|
+
...createBaseEvent(currentState.turnNumber, "battle_end", `La batalla termina: ${winnerAtEnd}`),
|
|
855
|
+
winner: winnerAtEnd,
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
events.push({
|
|
859
|
+
...createBaseEvent(currentState.turnNumber, "turn_end", `Termina el turno ${currentState.turnNumber}`),
|
|
860
|
+
});
|
|
861
|
+
const finalState = {
|
|
862
|
+
...currentState,
|
|
863
|
+
turnNumber: currentState.turnNumber + 1,
|
|
864
|
+
};
|
|
865
|
+
return {
|
|
866
|
+
newState: finalState,
|
|
867
|
+
events,
|
|
868
|
+
};
|
|
869
|
+
};
|