pokemon-io-core 0.0.89 → 0.0.91
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
CHANGED
|
@@ -4,6 +4,16 @@ export interface BattlePlayerStatusView {
|
|
|
4
4
|
stacks: 1 | 2;
|
|
5
5
|
remainingTurns: number;
|
|
6
6
|
}
|
|
7
|
+
export interface BattleMovePpView {
|
|
8
|
+
moveId: string;
|
|
9
|
+
currentPP: number;
|
|
10
|
+
maxPP: number;
|
|
11
|
+
}
|
|
12
|
+
export interface BattleItemUsesView {
|
|
13
|
+
itemId: string;
|
|
14
|
+
usesRemaining: number;
|
|
15
|
+
maxUses: number;
|
|
16
|
+
}
|
|
7
17
|
export interface BattlePlayerView {
|
|
8
18
|
playerId: string;
|
|
9
19
|
nickname: string;
|
|
@@ -11,9 +21,11 @@ export interface BattlePlayerView {
|
|
|
11
21
|
statuses: BattlePlayerStatusView[];
|
|
12
22
|
activeFighterId: string | null;
|
|
13
23
|
activeIndex: number;
|
|
24
|
+
activeMoves?: BattleMovePpView[];
|
|
25
|
+
inventory?: BattleItemUsesView[];
|
|
14
26
|
}
|
|
15
27
|
export type BattleStatus = "ongoing" | "awaiting_forced_switch" | "finished";
|
|
16
|
-
export type ForcedSwitchReason = "roar";
|
|
28
|
+
export type ForcedSwitchReason = "roar" | "faint";
|
|
17
29
|
export interface ForcedSwitchView {
|
|
18
30
|
targetPlayerId: string;
|
|
19
31
|
sourcePlayerId: string;
|
package/dist/core/battleState.js
CHANGED
|
@@ -17,28 +17,20 @@ export const applyForcedSwitchChoice = (state, targetPlayer, newIndex) => {
|
|
|
17
17
|
const fromFighter = self.fighterTeam[fromIndex];
|
|
18
18
|
const events = [
|
|
19
19
|
{
|
|
20
|
-
...createBaseEvent(state.turnNumber, "fighter_switched", `El entrenador
|
|
20
|
+
...createBaseEvent(state.turnNumber, "fighter_switched", `El entrenador cambia a ${toFighter.fighterId}`),
|
|
21
21
|
fromFighterId: fromFighter.fighterId,
|
|
22
22
|
toFighterId: toFighter.fighterId,
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
// ✅ CLAVE para tu replay:
|
|
24
|
+
toHp: toFighter.currentHp,
|
|
25
|
+
toStatuses: (toFighter.statuses ?? []).map((st) => ({
|
|
26
|
+
statusId: st.statusId,
|
|
27
|
+
stacks: st.stacks,
|
|
28
|
+
remainingTurns: st.remainingTurns,
|
|
29
|
+
})),
|
|
26
30
|
},
|
|
27
31
|
];
|
|
28
|
-
const
|
|
29
|
-
? {
|
|
30
|
-
|
|
31
|
-
forcedSwitch: null,
|
|
32
|
-
player1: { ...state.player1, activeIndex: newIndex },
|
|
33
|
-
}
|
|
34
|
-
: {
|
|
35
|
-
...state,
|
|
36
|
-
forcedSwitch: null,
|
|
37
|
-
player2: { ...state.player2, activeIndex: newIndex },
|
|
38
|
-
};
|
|
39
|
-
const nextState = {
|
|
40
|
-
...switchedState,
|
|
41
|
-
turnNumber: switchedState.turnNumber + 1,
|
|
42
|
-
};
|
|
32
|
+
const nextState = targetPlayer === "player1"
|
|
33
|
+
? { ...state, forcedSwitch: null, player1: { ...state.player1, activeIndex: newIndex } }
|
|
34
|
+
: { ...state, forcedSwitch: null, player2: { ...state.player2, activeIndex: newIndex } };
|
|
43
35
|
return { state: nextState, events };
|
|
44
36
|
};
|
|
@@ -1,22 +1,26 @@
|
|
|
1
1
|
import { applyDamageToFighter } from "../combat/damage.js";
|
|
2
|
-
import { dbg } from "../debug.js";
|
|
3
2
|
import { createBaseEvent } from "../events.js";
|
|
4
3
|
import { getActiveFighter } from "../fighters/selectors.js";
|
|
4
|
+
const isFainted = (f) => !f.isAlive || f.currentHp <= 0;
|
|
5
5
|
export const applyEndOfTurnStatuses = (state) => {
|
|
6
6
|
const events = [];
|
|
7
7
|
const applyForPlayer = (player) => {
|
|
8
8
|
const active = getActiveFighter(player);
|
|
9
|
+
// ✅ Si ya está KO, no hay ticks ni expiraciones
|
|
10
|
+
if (!active || isFainted(active)) {
|
|
11
|
+
return player;
|
|
12
|
+
}
|
|
9
13
|
let updated = { ...active };
|
|
10
14
|
const updatedStatuses = [];
|
|
11
15
|
for (const st of active.statuses) {
|
|
16
|
+
// ✅ Si ha muerto durante ticks previos, paramos y limpiamos
|
|
17
|
+
if (isFainted(updated)) {
|
|
18
|
+
updated = { ...updated, statuses: [] };
|
|
19
|
+
break;
|
|
20
|
+
}
|
|
12
21
|
const def = state.runtime.statusesById[st.statusId];
|
|
13
|
-
if (!def)
|
|
14
|
-
dbg("STATUS_MISSING_DEF", {
|
|
15
|
-
fighterId: active.fighterId,
|
|
16
|
-
statusId: st.statusId,
|
|
17
|
-
});
|
|
22
|
+
if (!def)
|
|
18
23
|
continue;
|
|
19
|
-
}
|
|
20
24
|
let damageFromStatus = 0;
|
|
21
25
|
def.effectsPerStack.forEach((eff) => {
|
|
22
26
|
if (eff.kind === "damage") {
|
|
@@ -25,33 +29,25 @@ export const applyEndOfTurnStatuses = (state) => {
|
|
|
25
29
|
? eff.flatAmount
|
|
26
30
|
: typeof eff.basePower === "number"
|
|
27
31
|
? eff.basePower
|
|
28
|
-
: 10;
|
|
32
|
+
: 10;
|
|
29
33
|
damageFromStatus += base * stacksMultiplier;
|
|
30
34
|
}
|
|
31
35
|
});
|
|
32
|
-
if (damageFromStatus > 0
|
|
33
|
-
dbg("STATUS_DOT", {
|
|
34
|
-
fighterId: updated.fighterId,
|
|
35
|
-
statusId: st.statusId,
|
|
36
|
-
damageFromStatus,
|
|
37
|
-
stacks: st.stacks,
|
|
38
|
-
});
|
|
36
|
+
if (damageFromStatus > 0) {
|
|
39
37
|
const damageRes = applyDamageToFighter(state, updated, damageFromStatus, updated.fighterId, false);
|
|
40
38
|
updated = damageRes.updatedDefender;
|
|
41
39
|
events.push(...damageRes.events);
|
|
40
|
+
// ✅ Si muere por este tick: no expiraciones, no más ticks
|
|
41
|
+
if (isFainted(updated)) {
|
|
42
|
+
updated = { ...updated, statuses: [] };
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
42
45
|
}
|
|
46
|
+
// solo decrementamos si sigue vivo
|
|
43
47
|
const remaining = st.remainingTurns - 1;
|
|
44
|
-
if (remaining > 0)
|
|
45
|
-
updatedStatuses.push({
|
|
46
|
-
...st,
|
|
47
|
-
remainingTurns: remaining,
|
|
48
|
-
});
|
|
49
|
-
}
|
|
48
|
+
if (remaining > 0)
|
|
49
|
+
updatedStatuses.push({ ...st, remainingTurns: remaining });
|
|
50
50
|
else {
|
|
51
|
-
dbg("STATUS_EXPIRE", {
|
|
52
|
-
fighterId: updated.fighterId,
|
|
53
|
-
statusId: st.statusId,
|
|
54
|
-
});
|
|
55
51
|
events.push({
|
|
56
52
|
...createBaseEvent(state.turnNumber, "status_expired", `El estado ${st.statusId} expira en ${updated.fighterId}`),
|
|
57
53
|
targetId: updated.fighterId,
|
|
@@ -59,22 +55,13 @@ export const applyEndOfTurnStatuses = (state) => {
|
|
|
59
55
|
});
|
|
60
56
|
}
|
|
61
57
|
}
|
|
62
|
-
updated
|
|
63
|
-
...updated,
|
|
64
|
-
|
|
65
|
-
};
|
|
58
|
+
if (!isFainted(updated)) {
|
|
59
|
+
updated = { ...updated, statuses: updatedStatuses };
|
|
60
|
+
}
|
|
66
61
|
const newTeam = player.fighterTeam.map((f, idx) => idx === player.activeIndex ? updated : f);
|
|
67
|
-
return {
|
|
68
|
-
...player,
|
|
69
|
-
fighterTeam: newTeam,
|
|
70
|
-
};
|
|
62
|
+
return { ...player, fighterTeam: newTeam };
|
|
71
63
|
};
|
|
72
64
|
const p1 = applyForPlayer(state.player1);
|
|
73
65
|
const p2 = applyForPlayer(state.player2);
|
|
74
|
-
|
|
75
|
-
...state,
|
|
76
|
-
player1: p1,
|
|
77
|
-
player2: p2,
|
|
78
|
-
};
|
|
79
|
-
return { state: newState, events };
|
|
66
|
+
return { state: { ...state, player1: p1, player2: p2 }, events };
|
|
80
67
|
};
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Bucle principal de turno
|
|
1
|
+
// turn/resolveTurn.ts (o donde lo tengas)
|
|
3
2
|
import { resolveItemUse } from "../actions/item.js";
|
|
4
3
|
import { resolveDamageMove } from "../actions/move.js";
|
|
5
4
|
import { getMovePriorityAndSpeed } from "../actions/priority.js";
|
|
@@ -12,6 +11,42 @@ import { getOpponentAndSelf } from "../fighters/selectors.js";
|
|
|
12
11
|
import { applyEndOfTurnStatuses } from "../status/endOfTurn.js";
|
|
13
12
|
import { hasHardCc } from "../status/hardCc.js";
|
|
14
13
|
// ------------------------------------------------------
|
|
14
|
+
// Helpers KO -> forced switch (estilo Pokémon real)
|
|
15
|
+
// ------------------------------------------------------
|
|
16
|
+
const isActiveFainted = (player) => {
|
|
17
|
+
const active = player.fighterTeam[player.activeIndex];
|
|
18
|
+
if (!active)
|
|
19
|
+
return true;
|
|
20
|
+
return !active.isAlive || active.currentHp <= 0;
|
|
21
|
+
};
|
|
22
|
+
const hasAliveBench = (player) => player.fighterTeam.some((f, idx) => idx !== player.activeIndex && f.isAlive && f.currentHp > 0);
|
|
23
|
+
/**
|
|
24
|
+
* Si hay un activo KO con banca viva, pedimos forced switch.
|
|
25
|
+
* Regla:
|
|
26
|
+
* - primero forzamos al oponente del que acaba de actuar (lo normal)
|
|
27
|
+
* - si no, forzamos al propio (recoil/status raro)
|
|
28
|
+
*/
|
|
29
|
+
const computeForcedSwitchFromFaint = (state, lastActingPlayerKey, actorFighterId) => {
|
|
30
|
+
if (state.forcedSwitch)
|
|
31
|
+
return null;
|
|
32
|
+
const oppKey = lastActingPlayerKey === "player1" ? "player2" : "player1";
|
|
33
|
+
const oppPlayer = oppKey === "player1" ? state.player1 : state.player2;
|
|
34
|
+
if (isActiveFainted(oppPlayer) && hasAliveBench(oppPlayer)) {
|
|
35
|
+
return {
|
|
36
|
+
targetPlayer: oppKey,
|
|
37
|
+
reason: { kind: "faint", actorFighterId },
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
const selfPlayer = lastActingPlayerKey === "player1" ? state.player1 : state.player2;
|
|
41
|
+
if (isActiveFainted(selfPlayer) && hasAliveBench(selfPlayer)) {
|
|
42
|
+
return {
|
|
43
|
+
targetPlayer: lastActingPlayerKey,
|
|
44
|
+
reason: { kind: "faint", actorFighterId },
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
};
|
|
49
|
+
// ------------------------------------------------------
|
|
15
50
|
export const resolveTurn = (state, actions) => {
|
|
16
51
|
const runtimeState = state;
|
|
17
52
|
let currentState = runtimeState;
|
|
@@ -19,6 +54,7 @@ export const resolveTurn = (state, actions) => {
|
|
|
19
54
|
const team = player.fighterTeam.map((f) => recomputeEffectiveStatsForFighter(runtimeState, f));
|
|
20
55
|
return { ...player, fighterTeam: team };
|
|
21
56
|
};
|
|
57
|
+
// Recalcular stats efectivos antes del turno
|
|
22
58
|
currentState = {
|
|
23
59
|
...currentState,
|
|
24
60
|
player1: recalcForPlayer(currentState.player1),
|
|
@@ -45,13 +81,10 @@ export const resolveTurn = (state, actions) => {
|
|
|
45
81
|
},
|
|
46
82
|
];
|
|
47
83
|
entries.sort((a, b) => {
|
|
48
|
-
if (b.priority !== a.priority)
|
|
84
|
+
if (b.priority !== a.priority)
|
|
49
85
|
return b.priority - a.priority;
|
|
50
|
-
|
|
51
|
-
if (b.speed !== a.speed) {
|
|
86
|
+
if (b.speed !== a.speed)
|
|
52
87
|
return b.speed - a.speed;
|
|
53
|
-
}
|
|
54
|
-
// empate total → coin flip
|
|
55
88
|
return Math.random() < 0.5 ? -1 : 1;
|
|
56
89
|
});
|
|
57
90
|
dbg(`TURN ${runtimeState.turnNumber} order`, entries.map((e) => ({
|
|
@@ -64,6 +97,30 @@ export const resolveTurn = (state, actions) => {
|
|
|
64
97
|
const { playerKey, action } = entry;
|
|
65
98
|
if (action.kind === "no_action")
|
|
66
99
|
continue;
|
|
100
|
+
// ✅ CLAVE: permitir SWITCH incluso si el activo está KO (evita soft-locks)
|
|
101
|
+
if (action.kind === "switch_fighter") {
|
|
102
|
+
const result = resolveSwitchFighter(currentState, playerKey, action);
|
|
103
|
+
currentState = result.state;
|
|
104
|
+
events.push(...result.events);
|
|
105
|
+
// Si el engine ya trae forcedSwitch (por roar u otro), paramos aquí
|
|
106
|
+
if (currentState.forcedSwitch) {
|
|
107
|
+
return { newState: currentState, events };
|
|
108
|
+
}
|
|
109
|
+
const winnerAfterSwitch = checkWinner(currentState);
|
|
110
|
+
if (winnerAfterSwitch !== "none") {
|
|
111
|
+
events.push({
|
|
112
|
+
...createBaseEvent(currentState.turnNumber, "battle_end", `La batalla termina: ${winnerAfterSwitch}`),
|
|
113
|
+
winner: winnerAfterSwitch,
|
|
114
|
+
});
|
|
115
|
+
const finalState = {
|
|
116
|
+
...currentState,
|
|
117
|
+
turnNumber: currentState.turnNumber + 1,
|
|
118
|
+
};
|
|
119
|
+
return { newState: finalState, events };
|
|
120
|
+
}
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
// A partir de aquí: moves/items solo si el activo está vivo
|
|
67
124
|
const { self } = getOpponentAndSelf(currentState, playerKey);
|
|
68
125
|
if (!self.isAlive || self.currentHp <= 0)
|
|
69
126
|
continue;
|
|
@@ -74,30 +131,31 @@ export const resolveTurn = (state, actions) => {
|
|
|
74
131
|
});
|
|
75
132
|
continue;
|
|
76
133
|
}
|
|
77
|
-
if (action.kind === "switch_fighter") {
|
|
78
|
-
const result = resolveSwitchFighter(currentState, playerKey, action);
|
|
79
|
-
currentState = result.state;
|
|
80
|
-
events.push(...result.events);
|
|
81
|
-
}
|
|
82
134
|
if (action.kind === "use_move") {
|
|
83
135
|
const result = resolveDamageMove(currentState, playerKey, action);
|
|
84
136
|
currentState = result.state;
|
|
85
137
|
events.push(...result.events);
|
|
86
138
|
}
|
|
87
|
-
if (action.kind === "use_item") {
|
|
139
|
+
else if (action.kind === "use_item") {
|
|
88
140
|
const result = resolveItemUse(currentState, playerKey, action);
|
|
89
141
|
currentState = result.state;
|
|
90
142
|
events.push(...result.events);
|
|
91
143
|
}
|
|
92
144
|
else {
|
|
93
|
-
// switch_fighter u otros no implementados aún
|
|
94
145
|
continue;
|
|
95
146
|
}
|
|
147
|
+
// ✅ forcedSwitch existente (roar, etc.)
|
|
96
148
|
if (currentState.forcedSwitch) {
|
|
97
|
-
return {
|
|
98
|
-
|
|
99
|
-
|
|
149
|
+
return { newState: currentState, events };
|
|
150
|
+
}
|
|
151
|
+
// ✅ NUEVO: forced switch automático por KO (sin consumir turno)
|
|
152
|
+
const faintForced = computeForcedSwitchFromFaint(currentState, playerKey, self.fighterId);
|
|
153
|
+
if (faintForced) {
|
|
154
|
+
currentState = {
|
|
155
|
+
...currentState,
|
|
156
|
+
forcedSwitch: faintForced,
|
|
100
157
|
};
|
|
158
|
+
return { newState: currentState, events };
|
|
101
159
|
}
|
|
102
160
|
const winnerAfterAction = checkWinner(currentState);
|
|
103
161
|
if (winnerAfterAction !== "none") {
|
|
@@ -109,15 +167,38 @@ export const resolveTurn = (state, actions) => {
|
|
|
109
167
|
...currentState,
|
|
110
168
|
turnNumber: currentState.turnNumber + 1,
|
|
111
169
|
};
|
|
112
|
-
return {
|
|
113
|
-
newState: finalState,
|
|
114
|
-
events,
|
|
115
|
-
};
|
|
170
|
+
return { newState: finalState, events };
|
|
116
171
|
}
|
|
117
172
|
}
|
|
173
|
+
// Fin de turno: estados (DoT, etc.)
|
|
118
174
|
const statusResult = applyEndOfTurnStatuses(currentState);
|
|
119
175
|
currentState = statusResult.state;
|
|
120
176
|
events.push(...statusResult.events);
|
|
177
|
+
// ✅ NUEVO: KO por estados al final del turno también fuerza cambio
|
|
178
|
+
if (!currentState.forcedSwitch) {
|
|
179
|
+
const p1Needs = isActiveFainted(currentState.player1) && hasAliveBench(currentState.player1);
|
|
180
|
+
const p2Needs = isActiveFainted(currentState.player2) && hasAliveBench(currentState.player2);
|
|
181
|
+
if (p1Needs) {
|
|
182
|
+
currentState = {
|
|
183
|
+
...currentState,
|
|
184
|
+
forcedSwitch: {
|
|
185
|
+
targetPlayer: "player1",
|
|
186
|
+
reason: { kind: "faint", actorFighterId: "status" },
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
return { newState: currentState, events };
|
|
190
|
+
}
|
|
191
|
+
if (p2Needs) {
|
|
192
|
+
currentState = {
|
|
193
|
+
...currentState,
|
|
194
|
+
forcedSwitch: {
|
|
195
|
+
targetPlayer: "player2",
|
|
196
|
+
reason: { kind: "faint", actorFighterId: "status" },
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
return { newState: currentState, events };
|
|
200
|
+
}
|
|
201
|
+
}
|
|
121
202
|
const winnerAtEnd = checkWinner(currentState);
|
|
122
203
|
if (winnerAtEnd !== "none") {
|
|
123
204
|
events.push({
|