pokemon-io-core 0.0.69 → 0.0.71

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.
@@ -1,8 +1,10 @@
1
1
  export interface PlayerLoadoutView {
2
- fighterId: string | null;
3
- moveIds: string[];
2
+ fighterIds: string[];
3
+ movesByFighterId: Record<string, string[]>;
4
4
  itemIds: string[];
5
5
  amuletId: string | null;
6
+ fighterId: string | null;
7
+ moveIds: string[];
6
8
  }
7
9
  export interface PlayerView {
8
10
  id: string;
@@ -9,7 +9,8 @@ export interface UseItemAction {
9
9
  }
10
10
  export interface SwitchFighterAction {
11
11
  kind: "switch_fighter";
12
- targetIndex: number;
12
+ newIndex: number;
13
+ forced?: boolean;
13
14
  }
14
15
  export interface NoAction {
15
16
  kind: "no_action";
@@ -16,7 +16,39 @@ export interface BattleRuntime {
16
16
  statusesById: Record<StatusId, StatusDefinition>;
17
17
  typeEffectiveness: TypeEffectivenessMatrix;
18
18
  }
19
+ /**
20
+ * Slot de equipo: UN luchador con sus stats base y movimientos.
21
+ * OJO: aquí NO van los items; los items son por jugador.
22
+ */
23
+ export interface PlayerFighterConfig {
24
+ fighter: FighterDefinition;
25
+ maxHp: number;
26
+ moves: MoveDefinition[];
27
+ amulet: AmuletDefinition | null;
28
+ }
29
+ /**
30
+ * Configuración de un jugador para el combate.
31
+ *
32
+ * - Forma nueva (multi-team): usar `team`.
33
+ * - Forma legacy (1 vs 1): usar fighter/maxHp/moves/items/amulet como hasta ahora.
34
+ * Si `team` no viene o está vacío, el engine construye un equipo de 1 con esos datos.
35
+ */
36
+ export interface FighterSlotConfig {
37
+ fighter: FighterDefinition;
38
+ maxHp: number;
39
+ moves: MoveDefinition[];
40
+ amulet: AmuletDefinition | null;
41
+ }
19
42
  export interface PlayerBattleConfig {
43
+ /**
44
+ * Nueva forma: equipo completo. Si existe y tiene elementos,
45
+ * el engine usará esto como fuente de verdad.
46
+ */
47
+ team?: FighterSlotConfig[];
48
+ /**
49
+ * Forma legacy (1 vs 1). Se mantiene para compatibilidad
50
+ * con skins y backends actuales.
51
+ */
20
52
  fighter: FighterDefinition;
21
53
  maxHp: number;
22
54
  moves: MoveDefinition[];
@@ -30,29 +30,26 @@ const cloneStats = (stats) => ({
30
30
  // ------------------------------------------------------
31
31
  // Creación de estado inicial
32
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
- }
33
+ const createBattleFighter = (slot, playerItems) => {
40
34
  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,
35
+ fighterId: slot.fighter.id,
36
+ classId: slot.fighter.classId,
37
+ maxHp: slot.maxHp,
38
+ currentHp: slot.maxHp,
39
+ baseStats: slot.fighter.baseStats,
40
+ effectiveStats: slot.fighter.baseStats,
41
+ moves: slot.moves.map((m) => ({
42
+ moveId: m.id,
43
+ currentPP: m.maxPP,
50
44
  })),
51
- items: cfg.items.map((item) => ({
52
- itemId: item.id,
53
- usesRemaining: item.maxUses,
45
+ // ⚠️ De momento mantenemos el comportamiento actual:
46
+ // cada luchador tiene su copia de items.
47
+ // Más adelante, si quieres, los movemos a nivel jugador.
48
+ items: playerItems.map((it) => ({
49
+ itemId: it.id,
50
+ usesRemaining: it.maxUses ?? 1,
54
51
  })),
55
- amuletId: cfg.amulet ? cfg.amulet.id : null,
52
+ amuletId: slot.amulet?.id ?? null,
56
53
  statuses: [],
57
54
  isAlive: true,
58
55
  };
@@ -89,10 +86,36 @@ const recomputeEffectiveStatsForFighter = (state, fighter) => {
89
86
  effectiveStats: eff,
90
87
  };
91
88
  };
89
+ const normalizeTeamFromConfig = (cfg) => {
90
+ // Si ya viene en forma de equipo, úsalo tal cual
91
+ if (cfg.team && cfg.team.length > 0) {
92
+ return cfg.team;
93
+ }
94
+ // Forma legacy: un solo fighter → lo envolvemos en un array
95
+ return [
96
+ {
97
+ fighter: cfg.fighter,
98
+ maxHp: cfg.maxHp,
99
+ moves: cfg.moves,
100
+ amulet: cfg.amulet,
101
+ },
102
+ ];
103
+ };
92
104
  const createPlayerBattleState = (cfg) => {
93
- const fighter = createBattleFighter(cfg);
105
+ // Si viene team, lo usamos; si no, construimos equipo de 1 (modo legacy)
106
+ const teamSlots = cfg.team && cfg.team.length > 0
107
+ ? cfg.team
108
+ : [
109
+ {
110
+ fighter: cfg.fighter,
111
+ maxHp: cfg.maxHp,
112
+ moves: cfg.moves,
113
+ amulet: cfg.amulet,
114
+ },
115
+ ];
116
+ const fighterTeam = teamSlots.map((slot) => createBattleFighter(slot, cfg.items));
94
117
  return {
95
- fighterTeam: [fighter],
118
+ fighterTeam,
96
119
  activeIndex: 0,
97
120
  };
98
121
  };
@@ -762,6 +785,35 @@ const applyEndOfTurnStatuses = (state) => {
762
785
  };
763
786
  return { state: newState, events };
764
787
  };
788
+ const applySwitchAction = (state, playerKey, newIndex, events) => {
789
+ const playerState = state[playerKey];
790
+ const team = playerState.fighterTeam;
791
+ // Validaciones básicas
792
+ if (newIndex < 0 || newIndex >= team.length)
793
+ return state;
794
+ if (!team[newIndex]?.isAlive)
795
+ return state;
796
+ if (newIndex === playerState.activeIndex)
797
+ return state;
798
+ const oldIndex = playerState.activeIndex;
799
+ const oldFighter = team[oldIndex];
800
+ const newFighter = team[newIndex];
801
+ const newPlayerState = {
802
+ ...playerState,
803
+ activeIndex: newIndex,
804
+ };
805
+ const newState = {
806
+ ...state,
807
+ [playerKey]: newPlayerState,
808
+ };
809
+ events.push({
810
+ ...createBaseEvent(state.turnNumber, "fighter_switched", `${oldFighter.fighterId} se retira. ¡Adelante ${newFighter.fighterId}!`),
811
+ player: playerKey,
812
+ oldFighterId: oldFighter.fighterId,
813
+ newFighterId: newFighter.fighterId,
814
+ });
815
+ return newState;
816
+ };
765
817
  // ------------------------------------------------------
766
818
  // Bucle principal de turno
767
819
  // ------------------------------------------------------
@@ -797,6 +849,7 @@ export const resolveTurn = (state, actions) => {
797
849
  ...getMovePriorityAndSpeed(runtimeState, "player2", actions.player2),
798
850
  },
799
851
  ];
852
+ // Orden clásico por prioridad + velocidad
800
853
  entries.sort((a, b) => {
801
854
  if (b.priority !== a.priority) {
802
855
  return b.priority - a.priority;
@@ -813,7 +866,29 @@ export const resolveTurn = (state, actions) => {
813
866
  priority: e.priority,
814
867
  speed: e.speed,
815
868
  })));
816
- for (const entry of entries) {
869
+ // 🔁 1) FASE DE CAMBIOS (switch_fighter) – prioridad absoluta
870
+ const switchEntries = entries.filter((e) => e.action.kind === "switch_fighter");
871
+ for (const entry of switchEntries) {
872
+ const { playerKey, action } = entry;
873
+ if (action.kind !== "switch_fighter")
874
+ continue;
875
+ const { self } = getOpponentAndSelf(currentState, playerKey);
876
+ // Si el activo ya está muerto, no tiene sentido cambiar (o el combate ya está decidido).
877
+ if (!self.isAlive || self.currentHp <= 0)
878
+ continue;
879
+ if (hasHardCc(currentState, self)) {
880
+ // No puede cambiar tampoco si está bajo hard CC
881
+ events.push({
882
+ ...createBaseEvent(currentState.turnNumber, "action_skipped_hard_cc", `${self.fighterId} no puede actuar por control total`),
883
+ actorId: self.fighterId,
884
+ });
885
+ continue;
886
+ }
887
+ currentState = applySwitchAction(currentState, playerKey, action.newIndex, events);
888
+ }
889
+ // 🔁 2) FASE DE ACCIONES (movimientos / objetos)
890
+ const actionEntries = entries.filter((e) => e.action.kind !== "switch_fighter");
891
+ for (const entry of actionEntries) {
817
892
  const { playerKey, action } = entry;
818
893
  if (action.kind === "no_action")
819
894
  continue;
@@ -838,7 +913,7 @@ export const resolveTurn = (state, actions) => {
838
913
  events.push(...result.events);
839
914
  }
840
915
  else {
841
- // switch_fighter u otros no implementados aún
916
+ // Cualquier otro tipo de acción futura se ignora de momento
842
917
  continue;
843
918
  }
844
919
  const winnerAfterAction = checkWinner(currentState);
@@ -857,6 +932,7 @@ export const resolveTurn = (state, actions) => {
857
932
  };
858
933
  }
859
934
  }
935
+ // 🔁 3) Estados de final de turno
860
936
  const statusResult = applyEndOfTurnStatuses(currentState);
861
937
  currentState = statusResult.state;
862
938
  events.push(...statusResult.events);
@@ -1,5 +1,5 @@
1
1
  import type { FighterId, MoveId, ItemId, StatusId } from "./ids.js";
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";
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" | "fighter_switched" | "turn_end" | "battle_end";
3
3
  export interface BaseBattleEvent {
4
4
  id: string;
5
5
  turnNumber: number;
@@ -60,8 +60,14 @@ export interface FighterFaintedEvent extends BaseBattleEvent {
60
60
  kind: "fighter_fainted";
61
61
  fighterId: FighterId;
62
62
  }
63
+ export interface FighterSwitchedEvent extends BaseBattleEvent {
64
+ kind: "fighter_switched";
65
+ player: "player1" | "player2";
66
+ oldFighterId: FighterId;
67
+ newFighterId: FighterId;
68
+ }
63
69
  export interface BattleEndEvent extends BaseBattleEvent {
64
70
  kind: "battle_end";
65
71
  winner: "player1" | "player2" | "draw";
66
72
  }
67
- export type BattleEvent = ActionDeclaredEvent | MoveMissEvent | MoveHitEvent | ItemUsedEvent | DamageEvent | HealEvent | StatusAppliedEvent | StatusExpiredEvent | FighterFaintedEvent | BattleEndEvent | BaseBattleEvent;
73
+ export type BattleEvent = ActionDeclaredEvent | MoveMissEvent | MoveHitEvent | ItemUsedEvent | DamageEvent | HealEvent | StatusAppliedEvent | StatusExpiredEvent | FighterFaintedEvent | FighterSwitchedEvent | BattleEndEvent | BaseBattleEvent;
package/dist/index.d.ts CHANGED
@@ -3,4 +3,3 @@ export * from "./api/battle.js";
3
3
  export * from "./api/room.js";
4
4
  export * from "./api/socketTypes.js";
5
5
  export * from "./skins/CombatSkin.js";
6
- export * from "./engine/pokemonBattleService.js";
package/dist/index.js CHANGED
@@ -3,4 +3,3 @@ export * from "./api/battle.js";
3
3
  export * from "./api/room.js";
4
4
  export * from "./api/socketTypes.js";
5
5
  export * from "./skins/CombatSkin.js";
6
- export * from "./engine/pokemonBattleService.js";
@@ -2,9 +2,24 @@ import type { TypeDefinition, TypeEffectivenessMatrix, MoveDefinition, ItemDefin
2
2
  import type { PlayerBattleConfig } from "../core/engine.js";
3
3
  export interface FighterLoadout {
4
4
  fighterId: string | null;
5
+ moveIds: string[];
5
6
  itemIds: string[];
6
7
  amuletId: string | null;
7
- moveIds: string[];
8
+ /**
9
+ * Lista de luchadores del equipo (máx. 6).
10
+ * Si solo hay uno, puede contener un único id.
11
+ */
12
+ fighterIds?: string[];
13
+ /**
14
+ * Movimientos por luchador. Si falta para alguno, se usan
15
+ * `moveIds` como fallback.
16
+ */
17
+ movesByFighterId?: Record<string, string[]>;
18
+ /**
19
+ * (Opcional) Futuro: cuál es el activo actual.
20
+ * Si no se define, se asume fighterIds[0].
21
+ */
22
+ activeFighterId?: string | null;
8
23
  }
9
24
  export interface CombatSkin {
10
25
  readonly id: string;
@@ -1,8 +1,8 @@
1
- // pokemon-io-core/src/skins/pokemon/pokemonSkin.ts
1
+ // pokemon-io-core/src/skins/tribes/clicheSkin.ts
2
2
  import { POKEMON_ITEMS } from "../pokemon/index.js";
3
3
  import { TRIBE_FIGHTERS, TRIBE_MOVES, TRIBE_STATUSES, TRIBE_TYPES, TRIBE_TYPE_MATRIX, } from "./index.js";
4
4
  // --- helpers ---
5
- const findPokemonById = (id) => {
5
+ const findTribeFighterById = (id) => {
6
6
  const lower = id.toLowerCase();
7
7
  return TRIBE_FIGHTERS.find((f) => f.id.toLowerCase() === lower) ?? null;
8
8
  };
@@ -72,15 +72,27 @@ export class TribeSkin {
72
72
  throw new Error("fighterId is required in FighterLoadout");
73
73
  }
74
74
  console.log("buildPlayerConfig", loadout.fighterId);
75
- const fighter = findPokemonById(loadout.fighterId);
75
+ const fighter = findTribeFighterById(loadout.fighterId);
76
76
  if (!fighter) {
77
77
  throw new Error(`Unknown fighterId for Pokemon skin: ${loadout.fighterId}`);
78
78
  }
79
79
  const moves = buildMovesFromLoadout(fighter, loadout);
80
80
  const items = buildItemsFromLoadout(loadout);
81
+ const maxHp = fighter.baseStats.defense + fighter.baseStats.offense;
81
82
  return {
83
+ // ✅ NUEVO: modelo multi-equipo
84
+ team: [
85
+ {
86
+ fighter,
87
+ maxHp,
88
+ moves,
89
+ amulet: null,
90
+ },
91
+ ],
92
+ // ✅ LEGACY: seguimos rellenando los campos antiguos
93
+ // por si algún sitio sigue leyéndolos.
82
94
  fighter,
83
- maxHp: fighter.baseStats.defense + fighter.baseStats.offense,
95
+ maxHp,
84
96
  moves,
85
97
  items,
86
98
  amulet: null,
@@ -77,9 +77,21 @@ export class PokemonSkin {
77
77
  }
78
78
  const moves = buildMovesFromLoadout(fighter, loadout);
79
79
  const items = buildItemsFromLoadout(loadout);
80
+ const maxHp = fighter.baseStats.defense + fighter.baseStats.offense;
80
81
  return {
82
+ // ✅ NUEVO: modelo multi-equipo
83
+ team: [
84
+ {
85
+ fighter,
86
+ maxHp,
87
+ moves,
88
+ amulet: null,
89
+ },
90
+ ],
91
+ // ✅ LEGACY: seguimos rellenando los campos antiguos
92
+ // por si algún sitio sigue leyéndolos.
81
93
  fighter,
82
- maxHp: fighter.baseStats.defense + fighter.baseStats.offense,
94
+ maxHp,
83
95
  moves,
84
96
  items,
85
97
  amulet: null,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pokemon-io-core",
3
- "version": "0.0.69",
3
+ "version": "0.0.71",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "type": "module",