pokemon-io-core 0.0.1
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 +14 -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/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 +37 -0
- package/dist/core/battleState.js +1 -0
- package/dist/core/engine.d.ts +45 -0
- package/dist/core/engine.js +481 -0
- package/dist/core/events.d.ts +67 -0
- package/dist/core/events.js +1 -0
- package/dist/core/fighters.d.ts +11 -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 +10 -0
- package/dist/core/index.js +10 -0
- package/dist/core/items.d.ts +10 -0
- package/dist/core/items.js +1 -0
- package/dist/core/moves.d.ts +58 -0
- package/dist/core/moves.js +1 -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/pokemonBattleService.d.ts +6 -0
- package/dist/engine/pokemonBattleService.js +32 -0
- package/dist/modules/auth/auth.schemas.d.ts +24 -0
- package/dist/modules/auth/auth.schemas.js +26 -0
- package/dist/modules/auth/index.d.ts +1 -0
- package/dist/modules/auth/index.js +1 -0
- package/dist/modules/enfrentamiento/enfrentamiento.enum.d.ts +13 -0
- package/dist/modules/enfrentamiento/enfrentamiento.enum.js +15 -0
- package/dist/modules/enfrentamiento/enfrentamiento.schemas.d.ts +22 -0
- package/dist/modules/enfrentamiento/enfrentamiento.schemas.js +17 -0
- package/dist/modules/enfrentamiento/enfrentamiento.types.d.ts +19 -0
- package/dist/modules/enfrentamiento/enfrentamiento.types.js +1 -0
- package/dist/modules/enfrentamiento/index.d.ts +3 -0
- package/dist/modules/enfrentamiento/index.js +3 -0
- package/dist/modules/equipos/equipos.enum.d.ts +5 -0
- package/dist/modules/equipos/equipos.enum.js +6 -0
- package/dist/modules/equipos/equipos.schemas.d.ts +20 -0
- package/dist/modules/equipos/equipos.schemas.js +20 -0
- package/dist/modules/equipos/equipos.types.d.ts +14 -0
- package/dist/modules/equipos/equipos.types.js +1 -0
- package/dist/modules/equipos/index.d.ts +3 -0
- package/dist/modules/equipos/index.js +3 -0
- package/dist/modules/liga/index.d.ts +3 -0
- package/dist/modules/liga/index.js +3 -0
- package/dist/modules/liga/liga.enum.d.ts +14 -0
- package/dist/modules/liga/liga.enum.js +17 -0
- package/dist/modules/liga/liga.schemas.d.ts +59 -0
- package/dist/modules/liga/liga.schemas.js +41 -0
- package/dist/modules/liga/liga.types.d.ts +25 -0
- package/dist/modules/liga/liga.types.js +1 -0
- package/dist/modules/user/index.d.ts +2 -0
- package/dist/modules/user/index.js +2 -0
- package/dist/modules/user/user.schemas.d.ts +15 -0
- package/dist/modules/user/user.schemas.js +15 -0
- package/dist/modules/user/user.types.d.ts +16 -0
- package/dist/modules/user/user.types.js +1 -0
- package/dist/shared/enums.d.ts +0 -0
- package/dist/shared/enums.js +1 -0
- package/dist/skins/CombatSkin.d.ts +17 -0
- package/dist/skins/CombatSkin.js +1 -0
- package/dist/skins/pokemon/fighters.d.ts +2 -0
- package/dist/skins/pokemon/fighters.js +37 -0
- package/dist/skins/pokemon/index.d.ts +3 -0
- package/dist/skins/pokemon/index.js +3 -0
- package/dist/skins/pokemon/moves.d.ts +2 -0
- package/dist/skins/pokemon/moves.js +83 -0
- package/dist/skins/pokemon/pokemonSkin.d.ts +13 -0
- package/dist/skins/pokemon/pokemonSkin.js +56 -0
- package/dist/skins/pokemon/types.d.ts +12 -0
- package/dist/skins/pokemon/types.js +78 -0
- package/dist/utils/logger.d.ts +9 -0
- package/dist/utils/logger.js +15 -0
- package/package.json +55 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface BattlePlayerView {
|
|
2
|
+
playerId: string;
|
|
3
|
+
nickname: string;
|
|
4
|
+
hp: number;
|
|
5
|
+
}
|
|
6
|
+
export type BattleStatusView = "ongoing" | "finished";
|
|
7
|
+
export interface BattleView {
|
|
8
|
+
roomId: string;
|
|
9
|
+
status: BattleStatusView;
|
|
10
|
+
currentTurnPlayerId: string | null;
|
|
11
|
+
winnerId: string | null;
|
|
12
|
+
endsAt: string;
|
|
13
|
+
players: BattlePlayerView[];
|
|
14
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface PlayerLoadoutView {
|
|
2
|
+
fighterId: string | null;
|
|
3
|
+
moveIds: string[];
|
|
4
|
+
itemIds: string[];
|
|
5
|
+
amuletId: string | null;
|
|
6
|
+
}
|
|
7
|
+
export interface PlayerView {
|
|
8
|
+
id: string;
|
|
9
|
+
nickname: string;
|
|
10
|
+
isOwner: boolean;
|
|
11
|
+
isSelf: boolean;
|
|
12
|
+
isReady: boolean;
|
|
13
|
+
loadout: PlayerLoadoutView | null;
|
|
14
|
+
}
|
|
15
|
+
export type RoomStatusView = "waiting" | "in_battle" | "finished";
|
|
16
|
+
export interface RoomView {
|
|
17
|
+
id: string;
|
|
18
|
+
ownerId: string;
|
|
19
|
+
status: RoomStatusView;
|
|
20
|
+
players: PlayerView[];
|
|
21
|
+
}
|
package/dist/api/room.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type ActionKind = "use_move" | "use_item" | "switch_fighter" | "no_action";
|
|
2
|
+
export interface UseMoveAction {
|
|
3
|
+
kind: "use_move";
|
|
4
|
+
moveIndex: number;
|
|
5
|
+
}
|
|
6
|
+
export interface UseItemAction {
|
|
7
|
+
kind: "use_item";
|
|
8
|
+
itemIndex: number;
|
|
9
|
+
}
|
|
10
|
+
export interface SwitchFighterAction {
|
|
11
|
+
kind: "switch_fighter";
|
|
12
|
+
targetIndex: number;
|
|
13
|
+
}
|
|
14
|
+
export interface NoAction {
|
|
15
|
+
kind: "no_action";
|
|
16
|
+
}
|
|
17
|
+
export type PlayerAction = UseMoveAction | UseItemAction | SwitchFighterAction | NoAction;
|
|
18
|
+
export interface BattleActions {
|
|
19
|
+
player1: PlayerAction;
|
|
20
|
+
player2: PlayerAction;
|
|
21
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { AmuletId, TypeId, StatusId } from "./ids";
|
|
2
|
+
import type { EffectDefinition } from "./moves";
|
|
3
|
+
export type PassiveTriggerKind = "always" | "on_hit" | "on_damage_taken" | "on_below_hp_threshold";
|
|
4
|
+
export interface PassiveTriggerCondition {
|
|
5
|
+
kind: PassiveTriggerKind;
|
|
6
|
+
hpThresholdPercent?: number;
|
|
7
|
+
}
|
|
8
|
+
export interface PassiveEffectDefinition {
|
|
9
|
+
trigger: PassiveTriggerCondition;
|
|
10
|
+
effects: EffectDefinition[];
|
|
11
|
+
appliedStatusId?: StatusId;
|
|
12
|
+
}
|
|
13
|
+
export interface AmuletDefinition {
|
|
14
|
+
id: AmuletId;
|
|
15
|
+
name: string;
|
|
16
|
+
classId: TypeId | null;
|
|
17
|
+
passiveEffects: PassiveEffectDefinition[];
|
|
18
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { FighterId, TypeId, MoveId, ItemId, AmuletId, StatusId } from "./ids";
|
|
2
|
+
import type { FighterStats } from "./types";
|
|
3
|
+
export interface EquippedMove {
|
|
4
|
+
moveId: MoveId;
|
|
5
|
+
currentPP: number;
|
|
6
|
+
}
|
|
7
|
+
export interface EquippedItem {
|
|
8
|
+
itemId: ItemId;
|
|
9
|
+
usesRemaining: number;
|
|
10
|
+
}
|
|
11
|
+
export interface ActiveStatus {
|
|
12
|
+
statusId: StatusId;
|
|
13
|
+
stacks: 1 | 2;
|
|
14
|
+
remainingTurns: number;
|
|
15
|
+
}
|
|
16
|
+
export interface BattleFighter {
|
|
17
|
+
fighterId: FighterId;
|
|
18
|
+
classId: TypeId;
|
|
19
|
+
maxHp: number;
|
|
20
|
+
currentHp: number;
|
|
21
|
+
baseStats: FighterStats;
|
|
22
|
+
effectiveStats: FighterStats;
|
|
23
|
+
moves: EquippedMove[];
|
|
24
|
+
items: EquippedItem[];
|
|
25
|
+
amuletId: AmuletId | null;
|
|
26
|
+
statuses: ActiveStatus[];
|
|
27
|
+
isAlive: boolean;
|
|
28
|
+
}
|
|
29
|
+
export interface PlayerBattleState {
|
|
30
|
+
fighterTeam: BattleFighter[];
|
|
31
|
+
activeIndex: number;
|
|
32
|
+
}
|
|
33
|
+
export interface BattleState {
|
|
34
|
+
turnNumber: number;
|
|
35
|
+
player1: PlayerBattleState;
|
|
36
|
+
player2: PlayerBattleState;
|
|
37
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type BattleState, type FighterDefinition, type MoveDefinition, type ItemDefinition, type AmuletDefinition, type StatusDefinition, type TypeDefinition, type TypeEffectivenessMatrix, type BattleActions, type BattleEvent, type MoveId, type ItemId, type StatusId, type TypeId } from ".";
|
|
2
|
+
interface BattleRules {
|
|
3
|
+
baseCritChance: number;
|
|
4
|
+
critPerStat: number;
|
|
5
|
+
critMultiplier: number;
|
|
6
|
+
stabMultiplier: number;
|
|
7
|
+
randomMinDamageFactor: number;
|
|
8
|
+
randomMaxDamageFactor: number;
|
|
9
|
+
}
|
|
10
|
+
interface BattleRuntime {
|
|
11
|
+
rules: BattleRules;
|
|
12
|
+
typesById: Record<TypeId, TypeDefinition>;
|
|
13
|
+
movesById: Record<MoveId, MoveDefinition>;
|
|
14
|
+
itemsById: Record<ItemId, ItemDefinition>;
|
|
15
|
+
amuletsById: Record<string, AmuletDefinition>;
|
|
16
|
+
statusesById: Record<StatusId, StatusDefinition>;
|
|
17
|
+
typeEffectiveness: TypeEffectivenessMatrix;
|
|
18
|
+
}
|
|
19
|
+
export interface PlayerBattleConfig {
|
|
20
|
+
fighter: FighterDefinition;
|
|
21
|
+
maxHp: number;
|
|
22
|
+
moves: MoveDefinition[];
|
|
23
|
+
items: ItemDefinition[];
|
|
24
|
+
amulet: AmuletDefinition | null;
|
|
25
|
+
}
|
|
26
|
+
export interface BattleConfig {
|
|
27
|
+
types: TypeDefinition[];
|
|
28
|
+
typeEffectiveness: TypeEffectivenessMatrix;
|
|
29
|
+
moves: MoveDefinition[];
|
|
30
|
+
items: ItemDefinition[];
|
|
31
|
+
amulets: AmuletDefinition[];
|
|
32
|
+
statuses: StatusDefinition[];
|
|
33
|
+
player1: PlayerBattleConfig;
|
|
34
|
+
player2: PlayerBattleConfig;
|
|
35
|
+
rules?: Partial<BattleRules>;
|
|
36
|
+
}
|
|
37
|
+
export interface RuntimeBattleState extends BattleState {
|
|
38
|
+
runtime: BattleRuntime;
|
|
39
|
+
}
|
|
40
|
+
export declare const createInitialBattleState: (config: BattleConfig) => RuntimeBattleState;
|
|
41
|
+
export declare const resolveTurn: (state: BattleState, actions: BattleActions) => {
|
|
42
|
+
newState: BattleState;
|
|
43
|
+
events: BattleEvent[];
|
|
44
|
+
};
|
|
45
|
+
export {};
|
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
// src/domain/combat/engine/battleEngine.ts
|
|
2
|
+
import { logger } from "../utils/logger";
|
|
3
|
+
const DEFAULT_RULES = {
|
|
4
|
+
baseCritChance: 0.05,
|
|
5
|
+
critPerStat: 0.002,
|
|
6
|
+
critMultiplier: 1.5,
|
|
7
|
+
stabMultiplier: 1.5,
|
|
8
|
+
randomMinDamageFactor: 0.85,
|
|
9
|
+
randomMaxDamageFactor: 1.0,
|
|
10
|
+
};
|
|
11
|
+
const buildMap = (list) => {
|
|
12
|
+
return list.reduce((acc, item) => {
|
|
13
|
+
acc[item.id] = item;
|
|
14
|
+
return acc;
|
|
15
|
+
}, {});
|
|
16
|
+
};
|
|
17
|
+
const cloneStats = (stats) => ({
|
|
18
|
+
offense: stats.offense,
|
|
19
|
+
defense: stats.defense,
|
|
20
|
+
speed: stats.speed,
|
|
21
|
+
crit: stats.crit,
|
|
22
|
+
});
|
|
23
|
+
const createBattleFighter = (cfg) => {
|
|
24
|
+
logger.info(`Creating fighter ${JSON.stringify(cfg)}`);
|
|
25
|
+
if (cfg.moves.length !== 4) {
|
|
26
|
+
throw new Error("Each fighter must have exactly 4 moves in MVP");
|
|
27
|
+
}
|
|
28
|
+
if (cfg.items.length > 4) {
|
|
29
|
+
throw new Error("A fighter cannot have more than 4 items");
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
fighterId: cfg.fighter.id,
|
|
33
|
+
classId: cfg.fighter.classId,
|
|
34
|
+
maxHp: cfg.maxHp,
|
|
35
|
+
currentHp: cfg.maxHp,
|
|
36
|
+
baseStats: cloneStats(cfg.fighter.baseStats),
|
|
37
|
+
effectiveStats: cloneStats(cfg.fighter.baseStats),
|
|
38
|
+
moves: cfg.moves.map((move) => ({
|
|
39
|
+
moveId: move.id,
|
|
40
|
+
currentPP: move.maxPP,
|
|
41
|
+
})),
|
|
42
|
+
items: cfg.items.map((item) => ({
|
|
43
|
+
itemId: item.id,
|
|
44
|
+
usesRemaining: item.maxUses,
|
|
45
|
+
})),
|
|
46
|
+
amuletId: cfg.amulet ? cfg.amulet.id : null,
|
|
47
|
+
statuses: [],
|
|
48
|
+
isAlive: true,
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
const createPlayerBattleState = (cfg) => {
|
|
52
|
+
const fighter = createBattleFighter(cfg);
|
|
53
|
+
return {
|
|
54
|
+
fighterTeam: [fighter],
|
|
55
|
+
activeIndex: 0,
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
export const createInitialBattleState = (config) => {
|
|
59
|
+
const rules = {
|
|
60
|
+
...DEFAULT_RULES,
|
|
61
|
+
...config.rules,
|
|
62
|
+
};
|
|
63
|
+
const runtime = {
|
|
64
|
+
rules,
|
|
65
|
+
typesById: buildMap(config.types),
|
|
66
|
+
movesById: buildMap(config.moves),
|
|
67
|
+
itemsById: buildMap(config.items),
|
|
68
|
+
amuletsById: buildMap(config.amulets),
|
|
69
|
+
statusesById: buildMap(config.statuses),
|
|
70
|
+
typeEffectiveness: config.typeEffectiveness,
|
|
71
|
+
};
|
|
72
|
+
const player1 = createPlayerBattleState(config.player1);
|
|
73
|
+
const player2 = createPlayerBattleState(config.player2);
|
|
74
|
+
const state = {
|
|
75
|
+
turnNumber: 1,
|
|
76
|
+
player1,
|
|
77
|
+
player2,
|
|
78
|
+
runtime,
|
|
79
|
+
};
|
|
80
|
+
return state;
|
|
81
|
+
};
|
|
82
|
+
// HELPERS
|
|
83
|
+
const getActiveFighter = (player) => {
|
|
84
|
+
return player.fighterTeam[player.activeIndex];
|
|
85
|
+
};
|
|
86
|
+
const getOpponentAndSelf = (state, playerKey) => {
|
|
87
|
+
const selfPlayer = playerKey === "player1" ? state.player1 : state.player2;
|
|
88
|
+
const oppPlayer = playerKey === "player1" ? state.player2 : state.player1;
|
|
89
|
+
return {
|
|
90
|
+
self: getActiveFighter(selfPlayer),
|
|
91
|
+
opponent: getActiveFighter(oppPlayer),
|
|
92
|
+
};
|
|
93
|
+
};
|
|
94
|
+
const getTypeEffectiveness = (state, attackerTypeId, defenderTypeId) => {
|
|
95
|
+
const matrix = state.runtime.typeEffectiveness;
|
|
96
|
+
const row = matrix[attackerTypeId];
|
|
97
|
+
if (!row)
|
|
98
|
+
return 1;
|
|
99
|
+
return row[defenderTypeId] ?? 1;
|
|
100
|
+
};
|
|
101
|
+
const randomInRange = (min, max) => {
|
|
102
|
+
return Math.random() * (max - min) + min;
|
|
103
|
+
};
|
|
104
|
+
const chance = (probability) => {
|
|
105
|
+
if (probability <= 0)
|
|
106
|
+
return false;
|
|
107
|
+
if (probability >= 1)
|
|
108
|
+
return true;
|
|
109
|
+
return Math.random() < probability;
|
|
110
|
+
};
|
|
111
|
+
// FIN HELPERS
|
|
112
|
+
// CALCULO DE CRITICOS Y DAÑO
|
|
113
|
+
const computeCritChance = (rules, critStat) => {
|
|
114
|
+
const raw = rules.baseCritChance + critStat * rules.critPerStat;
|
|
115
|
+
return Math.max(0, Math.min(1, raw));
|
|
116
|
+
};
|
|
117
|
+
const computeDamage = (input) => {
|
|
118
|
+
const { state, attacker, defender, move, isCritical } = input;
|
|
119
|
+
const rules = state.runtime.rules;
|
|
120
|
+
const basePower = move.basePower ?? 0;
|
|
121
|
+
const attackerOff = attacker.effectiveStats.offense;
|
|
122
|
+
const defenderDef = defender.effectiveStats.defense;
|
|
123
|
+
const typeEffectiveness = getTypeEffectiveness(state, move.typeId, defender.classId);
|
|
124
|
+
if (typeEffectiveness === 0) {
|
|
125
|
+
return {
|
|
126
|
+
damage: 0,
|
|
127
|
+
effectiveness: 0,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
const stabMultiplier = attacker.classId === move.typeId ? rules.stabMultiplier : 1;
|
|
131
|
+
const critMultiplier = isCritical ? rules.critMultiplier : 1;
|
|
132
|
+
const randomFactor = randomInRange(rules.randomMinDamageFactor, rules.randomMaxDamageFactor);
|
|
133
|
+
const rawDamage = basePower *
|
|
134
|
+
(attackerOff / Math.max(1, defenderDef)) *
|
|
135
|
+
typeEffectiveness *
|
|
136
|
+
stabMultiplier *
|
|
137
|
+
critMultiplier *
|
|
138
|
+
randomFactor;
|
|
139
|
+
const finalDamage = Math.max(1, Math.floor(rawDamage));
|
|
140
|
+
return {
|
|
141
|
+
damage: finalDamage,
|
|
142
|
+
effectiveness: typeEffectiveness,
|
|
143
|
+
};
|
|
144
|
+
};
|
|
145
|
+
let eventCounter = 0;
|
|
146
|
+
const nextEventId = () => {
|
|
147
|
+
eventCounter += 1;
|
|
148
|
+
return `evt_${eventCounter}`;
|
|
149
|
+
};
|
|
150
|
+
const createBaseEvent = (turnNumber, kind, message) => ({
|
|
151
|
+
id: nextEventId(),
|
|
152
|
+
kind,
|
|
153
|
+
turnNumber,
|
|
154
|
+
message,
|
|
155
|
+
timestamp: Date.now(),
|
|
156
|
+
});
|
|
157
|
+
const applyDamageToFighter = (state, defender, amount, actorId, isCritical) => {
|
|
158
|
+
const events = [];
|
|
159
|
+
const newHp = Math.max(0, defender.currentHp - amount);
|
|
160
|
+
const updatedDefender = {
|
|
161
|
+
...defender,
|
|
162
|
+
currentHp: newHp,
|
|
163
|
+
isAlive: newHp > 0,
|
|
164
|
+
};
|
|
165
|
+
events.push({
|
|
166
|
+
...createBaseEvent(state.turnNumber, "damage", `${actorId} inflige ${amount} de daño a ${defender.fighterId}`),
|
|
167
|
+
actorId,
|
|
168
|
+
targetId: defender.fighterId,
|
|
169
|
+
amount,
|
|
170
|
+
isCritical,
|
|
171
|
+
});
|
|
172
|
+
if (!updatedDefender.isAlive) {
|
|
173
|
+
events.push({
|
|
174
|
+
...createBaseEvent(state.turnNumber, "fighter_fainted", `${defender.fighterId} ha caído`),
|
|
175
|
+
fighterId: defender.fighterId,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
return { updatedDefender, events };
|
|
179
|
+
};
|
|
180
|
+
const getMovePriorityAndSpeed = (state, playerKey, action) => {
|
|
181
|
+
const { self } = getOpponentAndSelf(state, playerKey);
|
|
182
|
+
if (action.kind === "no_action") {
|
|
183
|
+
return { priority: 0, speed: 0 };
|
|
184
|
+
}
|
|
185
|
+
if (action.kind === "use_move") {
|
|
186
|
+
const moveEntry = self.moves[action.moveIndex];
|
|
187
|
+
if (!moveEntry) {
|
|
188
|
+
return { priority: 0, speed: self.effectiveStats.speed };
|
|
189
|
+
}
|
|
190
|
+
const moveDef = state.runtime.movesById[moveEntry.moveId];
|
|
191
|
+
const priority = moveDef?.priority ?? 0;
|
|
192
|
+
return {
|
|
193
|
+
priority,
|
|
194
|
+
speed: self.effectiveStats.speed,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
// Items y switch: prioridad base 0 por ahora
|
|
198
|
+
return {
|
|
199
|
+
priority: 0,
|
|
200
|
+
speed: self.effectiveStats.speed,
|
|
201
|
+
};
|
|
202
|
+
};
|
|
203
|
+
const resolveDamageMove = (state, playerKey, action) => {
|
|
204
|
+
if (action.kind !== "use_move") {
|
|
205
|
+
// por ahora ignoramos otros tipos aquí
|
|
206
|
+
return { state, events: [] };
|
|
207
|
+
}
|
|
208
|
+
const { self, opponent } = getOpponentAndSelf(state, playerKey);
|
|
209
|
+
const events = [];
|
|
210
|
+
const moveSlot = self.moves[action.moveIndex];
|
|
211
|
+
if (!moveSlot) {
|
|
212
|
+
// movimiento vacío → no hace nada
|
|
213
|
+
return { state, events };
|
|
214
|
+
}
|
|
215
|
+
const move = state.runtime.movesById[moveSlot.moveId];
|
|
216
|
+
if (!move) {
|
|
217
|
+
return { state, events };
|
|
218
|
+
}
|
|
219
|
+
// Sin PP → no hace nada (podrías emitir evento de "sin PP" si quieres)
|
|
220
|
+
if (moveSlot.currentPP <= 0) {
|
|
221
|
+
return { state, events };
|
|
222
|
+
}
|
|
223
|
+
// Movimientos de estado/curación aún no implementados en detalle
|
|
224
|
+
if (move.kind === "status" || move.kind === "heal") {
|
|
225
|
+
// stub: no hacemos nada por ahora
|
|
226
|
+
return { state, events };
|
|
227
|
+
}
|
|
228
|
+
// Acción declarada (para narrador avanzado)
|
|
229
|
+
events.push({
|
|
230
|
+
...createBaseEvent(state.turnNumber, "action_declared", `${self.fighterId} usa ${move.name}`),
|
|
231
|
+
actorId: self.fighterId,
|
|
232
|
+
});
|
|
233
|
+
// Comprobar accuracy
|
|
234
|
+
const accuracy = move.accuracy ?? 100;
|
|
235
|
+
const hitRoll = randomInRange(0, 100);
|
|
236
|
+
const hit = hitRoll < accuracy;
|
|
237
|
+
// Reducir PP
|
|
238
|
+
const updatedMoves = self.moves.map((m, idx) => idx === action.moveIndex
|
|
239
|
+
? { ...m, currentPP: Math.max(0, m.currentPP - 1) }
|
|
240
|
+
: m);
|
|
241
|
+
let updatedSelf = { ...self, moves: updatedMoves };
|
|
242
|
+
if (!hit) {
|
|
243
|
+
events.push({
|
|
244
|
+
...createBaseEvent(state.turnNumber, "move_miss", `${self.fighterId} falla ${move.name}`),
|
|
245
|
+
actorId: self.fighterId,
|
|
246
|
+
moveId: move.id,
|
|
247
|
+
targetId: opponent.fighterId,
|
|
248
|
+
});
|
|
249
|
+
const newState = updateFightersInState(state, playerKey, updatedSelf, opponent);
|
|
250
|
+
return { state: newState, events };
|
|
251
|
+
}
|
|
252
|
+
// Crítico
|
|
253
|
+
const critChance = computeCritChance(state.runtime.rules, updatedSelf.effectiveStats.crit);
|
|
254
|
+
const isCritical = chance(critChance);
|
|
255
|
+
const { damage, effectiveness } = computeDamage({
|
|
256
|
+
state,
|
|
257
|
+
attacker: updatedSelf,
|
|
258
|
+
defender: opponent,
|
|
259
|
+
move,
|
|
260
|
+
isCritical,
|
|
261
|
+
});
|
|
262
|
+
events.push({
|
|
263
|
+
...createBaseEvent(state.turnNumber, "move_hit", `${self.fighterId} acierta ${move.name}`),
|
|
264
|
+
actorId: self.fighterId,
|
|
265
|
+
moveId: move.id,
|
|
266
|
+
targetId: opponent.fighterId,
|
|
267
|
+
isCritical,
|
|
268
|
+
effectiveness,
|
|
269
|
+
});
|
|
270
|
+
const damageResult = applyDamageToFighter(state, opponent, damage, updatedSelf.fighterId, isCritical);
|
|
271
|
+
events.push(...damageResult.events);
|
|
272
|
+
const newState = updateFightersInState(state, playerKey, updatedSelf, damageResult.updatedDefender);
|
|
273
|
+
return { state: newState, events };
|
|
274
|
+
};
|
|
275
|
+
const updateFightersInState = (state, actingPlayerKey, updatedSelf, updatedOpponent) => {
|
|
276
|
+
const player1 = { ...state.player1 };
|
|
277
|
+
const player2 = { ...state.player2 };
|
|
278
|
+
const updateInPlayer = (player, updated) => {
|
|
279
|
+
const newTeam = player.fighterTeam.map((f, idx) => idx === player.activeIndex ? updated : f);
|
|
280
|
+
return {
|
|
281
|
+
...player,
|
|
282
|
+
fighterTeam: newTeam,
|
|
283
|
+
};
|
|
284
|
+
};
|
|
285
|
+
if (actingPlayerKey === "player1") {
|
|
286
|
+
const selfPlayer = updateInPlayer(player1, updatedSelf);
|
|
287
|
+
const oppPlayer = updateInPlayer(player2, updatedOpponent);
|
|
288
|
+
return {
|
|
289
|
+
...state,
|
|
290
|
+
player1: selfPlayer,
|
|
291
|
+
player2: oppPlayer,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
const selfPlayer = updateInPlayer(player2, updatedSelf);
|
|
295
|
+
const oppPlayer = updateInPlayer(player1, updatedOpponent);
|
|
296
|
+
return {
|
|
297
|
+
...state,
|
|
298
|
+
player1: oppPlayer,
|
|
299
|
+
player2: selfPlayer,
|
|
300
|
+
};
|
|
301
|
+
};
|
|
302
|
+
// FIN RESOLUCION DE ACCION DE use_move
|
|
303
|
+
// ORDEN DE ACCIONES HARD CC Y FIN DE TURNO
|
|
304
|
+
const hasHardCc = (state, fighter) => {
|
|
305
|
+
return fighter.statuses.some((st) => {
|
|
306
|
+
const def = state.runtime.statusesById[st.statusId];
|
|
307
|
+
return def?.kind === "hard_cc";
|
|
308
|
+
});
|
|
309
|
+
};
|
|
310
|
+
const checkWinner = (state) => {
|
|
311
|
+
const f1 = getActiveFighter(state.player1);
|
|
312
|
+
const f2 = getActiveFighter(state.player2);
|
|
313
|
+
const p1Alive = f1.isAlive && f1.currentHp > 0;
|
|
314
|
+
const p2Alive = f2.isAlive && f2.currentHp > 0;
|
|
315
|
+
if (p1Alive && !p2Alive)
|
|
316
|
+
return "player1";
|
|
317
|
+
if (!p1Alive && p2Alive)
|
|
318
|
+
return "player2";
|
|
319
|
+
if (!p1Alive && !p2Alive)
|
|
320
|
+
return "draw";
|
|
321
|
+
return "none";
|
|
322
|
+
};
|
|
323
|
+
// Estados al final del turno: MVP = aplicar DoT básico
|
|
324
|
+
const applyEndOfTurnStatuses = (state) => {
|
|
325
|
+
const events = [];
|
|
326
|
+
const applyForPlayer = (player) => {
|
|
327
|
+
const active = getActiveFighter(player);
|
|
328
|
+
let updated = { ...active };
|
|
329
|
+
const updatedStatuses = [];
|
|
330
|
+
for (const st of active.statuses) {
|
|
331
|
+
const def = state.runtime.statusesById[st.statusId];
|
|
332
|
+
if (!def) {
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
// MVP: interpretamos cualquier efecto kind: "damage" como DoT
|
|
336
|
+
let damageFromStatus = 0;
|
|
337
|
+
def.effectsPerStack.forEach((eff) => {
|
|
338
|
+
if (eff.kind === "damage") {
|
|
339
|
+
const stacksMultiplier = st.stacks === 2 ? 2 : 1;
|
|
340
|
+
const basePower = 10; // DoT base “genérico” (podrás parametrizar)
|
|
341
|
+
damageFromStatus += basePower * stacksMultiplier;
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
if (damageFromStatus > 0 && updated.isAlive) {
|
|
345
|
+
const damageRes = applyDamageToFighter(state, updated, damageFromStatus, updated.fighterId, false);
|
|
346
|
+
updated = damageRes.updatedDefender;
|
|
347
|
+
events.push(...damageRes.events);
|
|
348
|
+
}
|
|
349
|
+
const remaining = st.remainingTurns - 1;
|
|
350
|
+
if (remaining > 0) {
|
|
351
|
+
updatedStatuses.push({
|
|
352
|
+
...st,
|
|
353
|
+
remainingTurns: remaining,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
events.push({
|
|
358
|
+
...createBaseEvent(state.turnNumber, "status_expired", `El estado ${st.statusId} expira en ${updated.fighterId}`),
|
|
359
|
+
targetId: updated.fighterId,
|
|
360
|
+
statusId: st.statusId,
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
updated = {
|
|
365
|
+
...updated,
|
|
366
|
+
statuses: updatedStatuses,
|
|
367
|
+
};
|
|
368
|
+
const newTeam = player.fighterTeam.map((f, idx) => idx === player.activeIndex ? updated : f);
|
|
369
|
+
return {
|
|
370
|
+
...player,
|
|
371
|
+
fighterTeam: newTeam,
|
|
372
|
+
};
|
|
373
|
+
};
|
|
374
|
+
const p1 = applyForPlayer(state.player1);
|
|
375
|
+
const p2 = applyForPlayer(state.player2);
|
|
376
|
+
const newState = {
|
|
377
|
+
...state,
|
|
378
|
+
player1: p1,
|
|
379
|
+
player2: p2,
|
|
380
|
+
};
|
|
381
|
+
return { state: newState, events };
|
|
382
|
+
};
|
|
383
|
+
// FIN ORDEN DE ACCIONES HARD CC Y FIN DE TURNO
|
|
384
|
+
export const resolveTurn = (state, actions) => {
|
|
385
|
+
const runtimeState = state;
|
|
386
|
+
const events = [];
|
|
387
|
+
// Evento de inicio de turno
|
|
388
|
+
events.push({
|
|
389
|
+
...createBaseEvent(runtimeState.turnNumber, "turn_start", `Comienza el turno ${runtimeState.turnNumber}`),
|
|
390
|
+
});
|
|
391
|
+
// Construir orden con prioridad+speed
|
|
392
|
+
const entries = [
|
|
393
|
+
{
|
|
394
|
+
playerKey: "player1",
|
|
395
|
+
action: actions.player1,
|
|
396
|
+
...getMovePriorityAndSpeed(runtimeState, "player1", actions.player1),
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
playerKey: "player2",
|
|
400
|
+
action: actions.player2,
|
|
401
|
+
...getMovePriorityAndSpeed(runtimeState, "player2", actions.player2),
|
|
402
|
+
},
|
|
403
|
+
];
|
|
404
|
+
entries.sort((a, b) => {
|
|
405
|
+
if (b.priority !== a.priority) {
|
|
406
|
+
return b.priority - a.priority;
|
|
407
|
+
}
|
|
408
|
+
if (b.speed !== a.speed) {
|
|
409
|
+
return b.speed - a.speed;
|
|
410
|
+
}
|
|
411
|
+
// empate total → coin flip
|
|
412
|
+
return Math.random() < 0.5 ? -1 : 1;
|
|
413
|
+
});
|
|
414
|
+
let currentState = runtimeState;
|
|
415
|
+
for (const entry of entries) {
|
|
416
|
+
const { playerKey, action } = entry;
|
|
417
|
+
if (action.kind === "no_action") {
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
const { self } = getOpponentAndSelf(currentState, playerKey);
|
|
421
|
+
// Si ya está muerto, no hace nada
|
|
422
|
+
if (!self.isAlive || self.currentHp <= 0) {
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
// Hard CC → se salta acción
|
|
426
|
+
if (hasHardCc(currentState, self)) {
|
|
427
|
+
events.push({
|
|
428
|
+
...createBaseEvent(currentState.turnNumber, "action_skipped_hard_cc", `${self.fighterId} no puede actuar por control total`),
|
|
429
|
+
actorId: self.fighterId,
|
|
430
|
+
});
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
if (action.kind === "use_move") {
|
|
434
|
+
const result = resolveDamageMove(currentState, playerKey, action);
|
|
435
|
+
currentState = result.state;
|
|
436
|
+
events.push(...result.events);
|
|
437
|
+
}
|
|
438
|
+
else {
|
|
439
|
+
// MVP: ignoramos use_item y switch_fighter de momento
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
// Comprobar victoria inmediata tras cada acción
|
|
443
|
+
const winnerAfterAction = checkWinner(currentState);
|
|
444
|
+
if (winnerAfterAction !== "none") {
|
|
445
|
+
events.push({
|
|
446
|
+
...createBaseEvent(currentState.turnNumber, "battle_end", `La batalla termina: ${winnerAfterAction}`),
|
|
447
|
+
winner: winnerAfterAction,
|
|
448
|
+
});
|
|
449
|
+
const finalState = {
|
|
450
|
+
...currentState,
|
|
451
|
+
turnNumber: currentState.turnNumber + 1,
|
|
452
|
+
};
|
|
453
|
+
return {
|
|
454
|
+
newState: finalState,
|
|
455
|
+
events,
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
// Fin de turno: aplicar estados
|
|
460
|
+
const statusResult = applyEndOfTurnStatuses(currentState);
|
|
461
|
+
currentState = statusResult.state;
|
|
462
|
+
events.push(...statusResult.events);
|
|
463
|
+
const winnerAtEnd = checkWinner(currentState);
|
|
464
|
+
if (winnerAtEnd !== "none") {
|
|
465
|
+
events.push({
|
|
466
|
+
...createBaseEvent(currentState.turnNumber, "battle_end", `La batalla termina: ${winnerAtEnd}`),
|
|
467
|
+
winner: winnerAtEnd,
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
events.push({
|
|
471
|
+
...createBaseEvent(currentState.turnNumber, "turn_end", `Termina el turno ${currentState.turnNumber}`),
|
|
472
|
+
});
|
|
473
|
+
const finalState = {
|
|
474
|
+
...currentState,
|
|
475
|
+
turnNumber: currentState.turnNumber + 1,
|
|
476
|
+
};
|
|
477
|
+
return {
|
|
478
|
+
newState: finalState,
|
|
479
|
+
events,
|
|
480
|
+
};
|
|
481
|
+
};
|