pokemon-io-core 0.0.98 → 0.0.99

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,5 +1,6 @@
1
1
  import { BattleView } from "./battle.js";
2
2
  import { RoomView } from "./room.js";
3
+ import { EmoteId } from "../core/emotes.js";
3
4
  export interface ClientToServerEvents {
4
5
  "room:create": (payload: {
5
6
  nickname: string;
@@ -59,6 +60,10 @@ export interface ClientToServerEvents {
59
60
  "battle:rematchChangeLoadout": (payload: {
60
61
  roomId: string;
61
62
  }) => void;
63
+ "battle:sendEmote": (payload: {
64
+ roomId: string;
65
+ emoteId: EmoteId;
66
+ }) => void;
62
67
  ping: () => void;
63
68
  }
64
69
  export interface ServerToClientEvents {
@@ -73,5 +78,10 @@ export interface ServerToClientEvents {
73
78
  message: string;
74
79
  }) => void;
75
80
  "battle:state": (payload: BattleView) => void;
81
+ "battle:emote": (payload: {
82
+ playerId: string;
83
+ emoteId: EmoteId;
84
+ timestamp: number;
85
+ }) => void;
76
86
  pong: (msg: string) => void;
77
87
  }
@@ -0,0 +1,13 @@
1
+ export type EmoteCategory = "emotional" | "strategic" | "social";
2
+ export type EmoteId = string;
3
+ export interface EmoteDefinition {
4
+ id: EmoteId;
5
+ category: EmoteCategory;
6
+ emoji: string;
7
+ label: string;
8
+ description?: string;
9
+ }
10
+ export declare const EMOTES: EmoteDefinition[];
11
+ export declare const getEmoteById: (id: EmoteId) => EmoteDefinition | undefined;
12
+ export declare const getEmotesByCategory: (category: EmoteCategory) => EmoteDefinition[];
13
+ export declare const isValidEmoteId: (id: string) => id is EmoteId;
@@ -0,0 +1,127 @@
1
+ // src/core/emotes.ts
2
+ export const EMOTES = [
3
+ // Emotional (6)
4
+ {
5
+ id: "laugh",
6
+ category: "emotional",
7
+ emoji: "😂",
8
+ label: "Risa",
9
+ description: "¡Jaja!",
10
+ },
11
+ {
12
+ id: "scared",
13
+ category: "emotional",
14
+ emoji: "😱",
15
+ label: "Susto",
16
+ description: "¡Oh no!",
17
+ },
18
+ {
19
+ id: "cool",
20
+ category: "emotional",
21
+ emoji: "😎",
22
+ label: "Cool",
23
+ description: "Genial",
24
+ },
25
+ {
26
+ id: "angry",
27
+ category: "emotional",
28
+ emoji: "😡",
29
+ label: "Enfadado",
30
+ description: "¡Grr!",
31
+ },
32
+ {
33
+ id: "sad",
34
+ category: "emotional",
35
+ emoji: "😢",
36
+ label: "Triste",
37
+ description: "Auch...",
38
+ },
39
+ {
40
+ id: "thinking",
41
+ category: "emotional",
42
+ emoji: "🤔",
43
+ label: "Pensando",
44
+ description: "Hmm...",
45
+ },
46
+ // Strategic (6)
47
+ {
48
+ id: "attack",
49
+ category: "strategic",
50
+ emoji: "💥",
51
+ label: "Voy a atacar",
52
+ description: "¡Prepárate!",
53
+ },
54
+ {
55
+ id: "heal",
56
+ category: "strategic",
57
+ emoji: "💊",
58
+ label: "Voy a curarme",
59
+ description: "Necesito curarme",
60
+ },
61
+ {
62
+ id: "switch",
63
+ category: "strategic",
64
+ emoji: "🔄",
65
+ label: "Voy a cambiar",
66
+ description: "¡Cambio!",
67
+ },
68
+ {
69
+ id: "special",
70
+ category: "strategic",
71
+ emoji: "⚡",
72
+ label: "Movimiento especial",
73
+ description: "¡Algo grande viene!",
74
+ },
75
+ {
76
+ id: "defend",
77
+ category: "strategic",
78
+ emoji: "🛡️",
79
+ label: "Voy a defenderme",
80
+ description: "Me protejo",
81
+ },
82
+ {
83
+ id: "plan",
84
+ category: "strategic",
85
+ emoji: "🎯",
86
+ label: "Tengo un plan",
87
+ description: "Sé lo que hago",
88
+ },
89
+ // Social (4)
90
+ {
91
+ id: "well_played",
92
+ category: "social",
93
+ emoji: "👍",
94
+ label: "Bien jugado",
95
+ description: "¡Buen movimiento!",
96
+ },
97
+ {
98
+ id: "thanks",
99
+ category: "social",
100
+ emoji: "🙏",
101
+ label: "Gracias",
102
+ description: "Gracias por la partida",
103
+ },
104
+ {
105
+ id: "hello",
106
+ category: "social",
107
+ emoji: "👋",
108
+ label: "Hola",
109
+ description: "¡Hola!",
110
+ },
111
+ {
112
+ id: "good_luck",
113
+ category: "social",
114
+ emoji: "🤝",
115
+ label: "Buena suerte",
116
+ description: "¡Suerte!",
117
+ },
118
+ ];
119
+ export const getEmoteById = (id) => {
120
+ return EMOTES.find((emote) => emote.id === id);
121
+ };
122
+ export const getEmotesByCategory = (category) => {
123
+ return EMOTES.filter((emote) => emote.category === category);
124
+ };
125
+ export const isValidEmoteId = (id) => {
126
+ return EMOTES.some((emote) => emote.id === id);
127
+ };
@@ -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" | "fighter_switched" | "shield_applied" | "status_applied" | "status_refreshed" | "status_expired" | "fighter_fainted" | "turn_end" | "stats_changed" | "battle_end";
2
+ export type BattleEventKind = "turn_start" | "action_declared" | "action_skipped_hard_cc" | "move_miss" | "move_hit" | "item_used" | "status_cleared" | "damage" | "heal" | "fighter_switched" | "shield_applied" | "status_applied" | "status_refreshed" | "status_expired" | "status_tick" | "fighter_fainted" | "turn_end" | "stats_changed" | "battle_end";
3
3
  export interface BaseBattleEvent {
4
4
  id: string;
5
5
  turnNumber: number;
@@ -79,6 +79,12 @@ export interface StatusExpiredEvent extends BaseBattleEvent {
79
79
  targetId: FighterId;
80
80
  statusId: StatusId;
81
81
  }
82
+ export interface StatusTickEvent extends BaseBattleEvent {
83
+ kind: "status_tick";
84
+ targetId: FighterId;
85
+ statusId: StatusId;
86
+ stacks: number;
87
+ }
82
88
  export interface FighterFaintedEvent extends BaseBattleEvent {
83
89
  kind: "fighter_fainted";
84
90
  fighterId: FighterId;
@@ -87,4 +93,4 @@ export interface BattleEndEvent extends BaseBattleEvent {
87
93
  kind: "battle_end";
88
94
  winner: "player1" | "player2" | "draw";
89
95
  }
90
- export type BattleEvent = ActionDeclaredEvent | MoveMissEvent | MoveHitEvent | ItemUsedEvent | DamageEvent | HealEvent | StatusAppliedEvent | StatusExpiredEvent | FighterFaintedEvent | FighterSwitchedEvent | BattleEndEvent | StatsChangedEvent | BaseBattleEvent;
96
+ export type BattleEvent = ActionDeclaredEvent | MoveMissEvent | MoveHitEvent | ItemUsedEvent | DamageEvent | HealEvent | StatusAppliedEvent | StatusExpiredEvent | StatusTickEvent | FighterFaintedEvent | FighterSwitchedEvent | BattleEndEvent | StatsChangedEvent | BaseBattleEvent;
@@ -1,6 +1,7 @@
1
1
  export * from "./actions.js";
2
2
  export * from "./amulets.js";
3
3
  export * from "./battleState.js";
4
+ export * from "./emotes.js";
4
5
  export * from "./events.js";
5
6
  export * from "./fighters.js";
6
7
  export * from "./ids.js";
@@ -1,6 +1,7 @@
1
1
  export * from "./actions.js";
2
2
  export * from "./amulets.js";
3
3
  export * from "./battleState.js";
4
+ export * from "./emotes.js";
4
5
  export * from "./events.js";
5
6
  export * from "./fighters.js";
6
7
  export * from "./ids.js";
@@ -37,10 +37,13 @@ export const applyStatusToFighter = (state, target, statusId) => {
37
37
  duration,
38
38
  prevStacks: existing?.stacks ?? 0,
39
39
  });
40
+ const finalStacks = !existing ? 1 : existing.stacks === 1 ? 2 : existing.stacks;
40
41
  events.push({
41
42
  ...createBaseEvent(state.turnNumber, "status_applied", `Se aplica ${statusId} a ${target.fighterId}`),
42
43
  targetId: target.fighterId,
43
44
  statusId,
45
+ stacks: finalStacks,
46
+ durationTurns: duration,
44
47
  });
45
48
  return {
46
49
  updated: { ...target, statuses: newStatuses },
@@ -34,6 +34,13 @@ export const applyEndOfTurnStatuses = (state) => {
34
34
  }
35
35
  });
36
36
  if (damageFromStatus > 0) {
37
+ // Emit status tick event for narration
38
+ events.push({
39
+ ...createBaseEvent(state.turnNumber, "status_tick", `${updated.fighterId} sufre por ${st.statusId}`),
40
+ targetId: updated.fighterId,
41
+ statusId: st.statusId,
42
+ stacks: st.stacks,
43
+ });
37
44
  const damageRes = applyDamageToFighter(state, updated, damageFromStatus, updated.fighterId, false);
38
45
  updated = damageRes.updatedDefender;
39
46
  events.push(...damageRes.events);
@@ -144,42 +144,61 @@ export const resolveTurn = (state, actions) => {
144
144
  const { self } = getOpponentAndSelf(currentState, playerKey);
145
145
  if (!self.isAlive || self.currentHp <= 0)
146
146
  continue;
147
- // ✅ Paralysis check (25% chance to skip)
147
+ // ✅ Paralysis check (25% chance to skip ALL actions, including items)
148
148
  if (self.statuses.some((s) => s.statusId === "paralysis") &&
149
149
  Math.random() < 0.25) {
150
150
  events.push({
151
151
  ...createBaseEvent(currentState.turnNumber, "action_skipped_hard_cc", `${self.fighterId} está paralizado y no puede moverse`),
152
152
  actorId: self.fighterId,
153
+ statusId: "paralysis",
153
154
  });
154
155
  continue;
155
156
  }
156
- if (hasHardCc(currentState, self)) {
157
- let interruptionSuffix = "";
158
- // Si estaba cargando y es interrumpido, pierde la carga (Fly/Dig fallan)
159
- if (self.volatileStatus?.kind === "charging") {
160
- const newFighter = { ...self, volatileStatus: null };
161
- currentState = {
162
- ...currentState,
163
- [playerKey]: {
164
- ...currentState[playerKey],
165
- fighterTeam: currentState[playerKey].fighterTeam.map((f) => f.fighterId === self.fighterId ? newFighter : f),
166
- },
167
- };
168
- interruptionSuffix = " (Concentración perdida)";
169
- }
170
- events.push({
171
- ...createBaseEvent(currentState.turnNumber, "action_skipped_hard_cc", `${self.fighterId} no puede actuar por control total${interruptionSuffix}`),
172
- actorId: self.fighterId,
173
- });
174
- continue;
175
- }
176
- if (action.kind === "use_move") {
177
- const result = resolveDamageMove(currentState, playerKey, action);
157
+ // ✅ Items can be used regardless of hard CC (trainer action, not Pokémon action)
158
+ if (action.kind === "use_item") {
159
+ const result = resolveItemUse(currentState, playerKey, action);
178
160
  currentState = result.state;
179
161
  events.push(...result.events);
180
162
  }
181
- else if (action.kind === "use_item") {
182
- const result = resolveItemUse(currentState, playerKey, action);
163
+ else if (action.kind === "use_move") {
164
+ // Moves are blocked by hard CC (sleep, freeze, flinch)
165
+ if (hasHardCc(currentState, self)) {
166
+ // Find the hard CC status
167
+ const ccStatus = self.statuses.find((s) => {
168
+ const def = currentState.runtime.statusesById[s.statusId];
169
+ return def?.kind === "hard_cc";
170
+ });
171
+ let interruptionSuffix = "";
172
+ // Si estaba cargando y es interrumpido, pierde la carga (Fly/Dig fallan)
173
+ if (self.volatileStatus?.kind === "charging") {
174
+ const newFighter = { ...self, volatileStatus: null };
175
+ currentState = {
176
+ ...currentState,
177
+ [playerKey]: {
178
+ ...currentState[playerKey],
179
+ fighterTeam: currentState[playerKey].fighterTeam.map((f) => f.fighterId === self.fighterId ? newFighter : f),
180
+ },
181
+ };
182
+ interruptionSuffix = " (Concentración perdida)";
183
+ }
184
+ // Status-specific messages
185
+ const statusMessages = {
186
+ sleep: `${self.fighterId} está dormido y no puede moverse`,
187
+ freeze: `${self.fighterId} está congelado y no puede actuar`,
188
+ flinch: `${self.fighterId} se acobarda y no puede atacar`,
189
+ };
190
+ const statusId = ccStatus?.statusId || "unknown";
191
+ const message = statusMessages[statusId] ||
192
+ `${self.fighterId} no puede actuar por control total`;
193
+ events.push({
194
+ ...createBaseEvent(currentState.turnNumber, "action_skipped_hard_cc", `${message}${interruptionSuffix}`),
195
+ actorId: self.fighterId,
196
+ statusId: statusId,
197
+ });
198
+ continue;
199
+ }
200
+ // Execute the move
201
+ const result = resolveDamageMove(currentState, playerKey, action);
183
202
  currentState = result.state;
184
203
  events.push(...result.events);
185
204
  }
@@ -43,7 +43,6 @@ export const POKEMON_ITEMS = [
43
43
  maxUses: 2,
44
44
  effects: [
45
45
  { kind: "damage", flatAmount: 15 },
46
- { kind: "apply_status", statusId: "burn" },
47
46
  ],
48
47
  image: "shotgun",
49
48
  },
@@ -55,7 +54,6 @@ export const POKEMON_ITEMS = [
55
54
  maxUses: 4,
56
55
  effects: [
57
56
  { kind: "damage", flatAmount: 8 },
58
- { kind: "apply_status", statusId: "burn" },
59
57
  ],
60
58
  image: "pistol",
61
59
  },
@@ -67,7 +65,6 @@ export const POKEMON_ITEMS = [
67
65
  maxUses: 3,
68
66
  effects: [
69
67
  { kind: "damage", flatAmount: 10 },
70
- { kind: "apply_status", statusId: "burn" },
71
68
  ],
72
69
  image: "riffle",
73
70
  },
@@ -79,7 +76,6 @@ export const POKEMON_ITEMS = [
79
76
  maxUses: 1,
80
77
  effects: [
81
78
  { kind: "damage", flatAmount: 30 },
82
- { kind: "apply_status", statusId: "burn" },
83
79
  ],
84
80
  image: "sniper",
85
81
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pokemon-io-core",
3
- "version": "0.0.98",
3
+ "version": "0.0.99",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "type": "module",