pokemon-io-core 0.0.125 → 0.0.126

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.
Files changed (41) hide show
  1. package/dist/core/ids.d.ts +18 -6
  2. package/dist/core/ids.js +10 -1
  3. package/dist/core/index.d.ts +2 -1
  4. package/dist/core/index.js +2 -1
  5. package/dist/core/randomEvents.d.ts +21 -5
  6. package/dist/core/randomEvents.js +4 -4
  7. package/dist/core/randomEventsList.d.ts +10 -4
  8. package/dist/core/randomEventsList.js +24 -163
  9. package/dist/core/status.d.ts +1 -0
  10. package/dist/core/validation.d.ts +21 -0
  11. package/dist/core/validation.js +183 -0
  12. package/dist/engine/combat/damage.js +2 -1
  13. package/dist/engine/events/applyRandomEvent.js +13 -6
  14. package/dist/engine/turn/resolveTurn.js +3 -2
  15. package/dist/skins/CombatSkin.d.ts +1 -0
  16. package/dist/skins/cliches/clicheSkin.d.ts +2 -1
  17. package/dist/skins/cliches/clicheSkin.js +3 -0
  18. package/dist/skins/cliches/constants.d.ts +135 -0
  19. package/dist/skins/cliches/constants.js +136 -0
  20. package/dist/skins/cliches/fighters.d.ts +1 -1
  21. package/dist/skins/cliches/fighters.js +165 -165
  22. package/dist/skins/cliches/items.js +45 -44
  23. package/dist/skins/cliches/moves.js +74 -69
  24. package/dist/skins/cliches/randomEvents.d.ts +7 -0
  25. package/dist/skins/cliches/randomEvents.js +1167 -0
  26. package/dist/skins/cliches/statuses.js +63 -36
  27. package/dist/skins/cliches/types.d.ts +12 -12
  28. package/dist/skins/cliches/types.js +12 -11
  29. package/dist/skins/pokemon/constants.d.ts +83 -0
  30. package/dist/skins/pokemon/constants.js +84 -0
  31. package/dist/skins/pokemon/fighters.js +55 -55
  32. package/dist/skins/pokemon/items.js +23 -22
  33. package/dist/skins/pokemon/moves.js +59 -58
  34. package/dist/skins/pokemon/pokemonSkin.d.ts +2 -1
  35. package/dist/skins/pokemon/pokemonSkin.js +3 -0
  36. package/dist/skins/pokemon/randomEvents.d.ts +6 -0
  37. package/dist/skins/pokemon/randomEvents.js +165 -0
  38. package/dist/skins/pokemon/statuses.js +5 -4
  39. package/dist/skins/pokemon/types.d.ts +6 -6
  40. package/dist/skins/pokemon/types.js +7 -6
  41. package/package.json +1 -1
@@ -1,6 +1,18 @@
1
- export type TypeId = string;
2
- export type FighterId = string;
3
- export type MoveId = string;
4
- export type ItemId = string;
5
- export type AmuletId = string;
6
- export type StatusId = string;
1
+ declare const __brand: unique symbol;
2
+ type Brand<T, TBrand extends string> = T & {
3
+ [__brand]: TBrand;
4
+ };
5
+ export type TypeId = Brand<string, 'TypeId'>;
6
+ export type FighterId = Brand<string, 'FighterId'>;
7
+ export type MoveId = Brand<string, 'MoveId'>;
8
+ export type ItemId = Brand<string, 'ItemId'>;
9
+ export type AmuletId = Brand<string, 'AmuletId'>;
10
+ export type StatusId = Brand<string, 'StatusId'>;
11
+ export declare const typeId: (id: string) => TypeId;
12
+ export declare const fighterId: (id: string) => FighterId;
13
+ export declare const moveId: (id: string) => MoveId;
14
+ export declare const itemId: (id: string) => ItemId;
15
+ export declare const amuletId: (id: string) => AmuletId;
16
+ export declare const statusId: (id: string) => StatusId;
17
+ export declare const SYSTEM_FIGHTER_ID: FighterId;
18
+ export {};
package/dist/core/ids.js CHANGED
@@ -1 +1,10 @@
1
- export {};
1
+ // Helper functions to create branded IDs
2
+ // Use these when defining data to get type safety
3
+ export const typeId = (id) => id;
4
+ export const fighterId = (id) => id;
5
+ export const moveId = (id) => id;
6
+ export const itemId = (id) => id;
7
+ export const amuletId = (id) => id;
8
+ export const statusId = (id) => id;
9
+ // Special system IDs for events not caused by fighters
10
+ export const SYSTEM_FIGHTER_ID = fighterId("__system__");
@@ -1,6 +1,6 @@
1
1
  export * from "./actions.js";
2
2
  export * from "./randomEvents.js";
3
- export { CORE_RANDOM_EVENTS, POKEMON_RANDOM_EVENTS } from "./randomEventsList.js";
3
+ export { CORE_RANDOM_EVENTS, POKEMON_RANDOM_EVENTS, TRIBES_RANDOM_EVENTS, getRandomEventsForSkin } from "./randomEventsList.js";
4
4
  export * from "./emotes.js";
5
5
  export * from "./events.js";
6
6
  export * from "./events.js";
@@ -14,3 +14,4 @@ export * from "./battleRuntime.js";
14
14
  export * from "./battleState.js";
15
15
  export * from "./amulets.js";
16
16
  export * from "./types.js";
17
+ export * from "./validation.js";
@@ -1,6 +1,6 @@
1
1
  export * from "./actions.js";
2
2
  export * from "./randomEvents.js";
3
- export { CORE_RANDOM_EVENTS, POKEMON_RANDOM_EVENTS } from "./randomEventsList.js";
3
+ export { CORE_RANDOM_EVENTS, POKEMON_RANDOM_EVENTS, TRIBES_RANDOM_EVENTS, getRandomEventsForSkin } from "./randomEventsList.js";
4
4
  export * from "./emotes.js";
5
5
  export * from "./events.js";
6
6
  export * from "./events.js";
@@ -14,3 +14,4 @@ export * from "./battleRuntime.js";
14
14
  export * from "./battleState.js";
15
15
  export * from "./amulets.js";
16
16
  export * from "./types.js";
17
+ export * from "./validation.js";
@@ -1,10 +1,26 @@
1
+ import { StatusId } from "./ids";
1
2
  export type EventRarity = "common" | "rare" | "epic" | "legendary";
2
3
  export type EventEffectType = "damage_percent" | "heal_percent" | "stat_change" | "apply_status" | "reset_stats" | "swap_hp";
3
4
  export type EventTarget = "both" | "self" | "opponent" | "random_one";
4
- export interface EventCondition {
5
- type: "has_item" | "hp_below_percent" | "hp_above_percent" | "fighter_type" | "has_status" | "stat_stage_above" | "always";
6
- value?: string | number;
7
- }
5
+ export type EventCondition = {
6
+ type: "has_item";
7
+ } | {
8
+ type: "hp_below_percent";
9
+ value: number;
10
+ } | {
11
+ type: "hp_above_percent";
12
+ value: number;
13
+ } | {
14
+ type: "fighter_type";
15
+ value: string | string[];
16
+ } | {
17
+ type: "has_status";
18
+ } | {
19
+ type: "stat_stage_above";
20
+ value: number;
21
+ } | {
22
+ type: "always";
23
+ };
8
24
  export interface RandomEventEffect {
9
25
  effectType: EventEffectType;
10
26
  target: EventTarget;
@@ -12,7 +28,7 @@ export interface RandomEventEffect {
12
28
  percentValue?: number;
13
29
  statType?: "offense" | "defense" | "speed" | "crit" | "accuracy" | "evasion";
14
30
  statChange?: number;
15
- statusId?: string;
31
+ statusId?: StatusId;
16
32
  statusDuration?: number;
17
33
  randomChoices?: RandomEventEffect[];
18
34
  }
@@ -1,8 +1,8 @@
1
1
  // @pokemon-io/combat-core/src/core/randomEvents.ts
2
2
  // Rarity weights for selection
3
3
  export const RARITY_WEIGHTS = {
4
- common: 60,
5
- rare: 25,
6
- epic: 12,
7
- legendary: 3,
4
+ common: 45,
5
+ rare: 30,
6
+ epic: 15,
7
+ legendary: 10,
8
8
  };
@@ -1,11 +1,17 @@
1
1
  import type { RandomEventDefinition } from "./randomEvents.js";
2
2
  /**
3
3
  * CORE Random Events - Generic events that work with any skin
4
- * Phase 1 + Phase 2: 9 total CORE events
4
+ * These events only use damage/heal effects and stat changes
5
+ * They don't reference any skin-specific types, statuses, or items
5
6
  */
6
7
  export declare const CORE_RANDOM_EVENTS: RandomEventDefinition[];
8
+ export { POKEMON_RANDOM_EVENTS } from "../skins/pokemon/randomEvents.js";
9
+ export { TRIBES_RANDOM_EVENTS } from "../skins/cliches/randomEvents.js";
7
10
  /**
8
- * POKEMON-SPECIFIC Random Events
9
- * These only appear when using the Pokemon skin
11
+ * Get all random events for a specific skin
12
+ * Combines core events with skin-specific events
13
+ *
14
+ * @param skinId - The ID of the skin (e.g., "pokemon", "tribes")
15
+ * @returns Array of all available random events for that skin
10
16
  */
11
- export declare const POKEMON_RANDOM_EVENTS: RandomEventDefinition[];
17
+ export declare function getRandomEventsForSkin(skinId: string): RandomEventDefinition[];
@@ -1,7 +1,10 @@
1
1
  // @pokemon-io/combat-core/src/core/randomEventsList.ts
2
+ import { POKEMON_RANDOM_EVENTS } from "../skins/pokemon/randomEvents.js";
3
+ import { TRIBES_RANDOM_EVENTS } from "../skins/cliches/randomEvents.js";
2
4
  /**
3
5
  * CORE Random Events - Generic events that work with any skin
4
- * Phase 1 + Phase 2: 9 total CORE events
6
+ * These events only use damage/heal effects and stat changes
7
+ * They don't reference any skin-specific types, statuses, or items
5
8
  */
6
9
  export const CORE_RANDOM_EVENTS = [
7
10
  // ==================== COMMON (4 events) ====================
@@ -162,167 +165,25 @@ export const CORE_RANDOM_EVENTS = [
162
165
  ],
163
166
  },
164
167
  ];
168
+ // Export skin-specific events
169
+ export { POKEMON_RANDOM_EVENTS } from "../skins/pokemon/randomEvents.js";
170
+ export { TRIBES_RANDOM_EVENTS } from "../skins/cliches/randomEvents.js";
165
171
  /**
166
- * POKEMON-SPECIFIC Random Events
167
- * These only appear when using the Pokemon skin
172
+ * Get all random events for a specific skin
173
+ * Combines core events with skin-specific events
174
+ *
175
+ * @param skinId - The ID of the skin (e.g., "pokemon", "tribes")
176
+ * @returns Array of all available random events for that skin
168
177
  */
169
- export const POKEMON_RANDOM_EVENTS = [
170
- // ==================== COMMON (3 events) ====================
171
- {
172
- id: "pokemon_center",
173
- name: "🏥 Centro Pokémon Móvil",
174
- description: "Una enfermera Joy aparece y cura a ambos Pokémon.",
175
- narration: ["¡Suena una música familiar!", "¡La Enfermera Joy ha venido a ayudar!"],
176
- rarity: "common",
177
- effects: [
178
- {
179
- effectType: "heal_percent",
180
- target: "both",
181
- percentValue: 15,
182
- },
183
- ],
184
- },
185
- {
186
- id: "team_rocket",
187
- name: "🚀 ¡Equipo Rocket!",
188
- description: "¡Preparaos para los problemas! El ataque de ambos baja.",
189
- narration: ["¡Prepárense para los problemas!", "¡Y más vale que teman!"],
190
- rarity: "common",
191
- effects: [
192
- {
193
- effectType: "stat_change",
194
- target: "both",
195
- statType: "offense",
196
- statChange: -1,
197
- },
198
- ],
199
- },
200
- {
201
- id: "rare_candy",
202
- name: "🍬 Caramelo Raro",
203
- description: "Un Pokémon aleatorio encuentra un caramelo y se fortalece.",
204
- narration: ["¡Un objeto brillante aparece!", "¡Es un Caramelo Raro!"],
205
- rarity: "common",
206
- effects: [
207
- {
208
- effectType: "stat_change",
209
- target: "random_one",
210
- statType: "offense",
211
- statChange: 1,
212
- },
213
- {
214
- effectType: "stat_change",
215
- target: "random_one",
216
- statType: "defense",
217
- statChange: 1,
218
- },
219
- ],
220
- },
221
- // ==================== RARE (4 events) ====================
222
- {
223
- id: "sandstorm",
224
- name: "🌪️ Tormenta de Arena",
225
- description: "Una tormenta de arena daña a todos los Pokémon.",
226
- narration: ["¡El viento empieza a soplar con fuerza!", "¡Una tormenta de arena azota el combate!"],
227
- rarity: "rare",
228
- effects: [
229
- {
230
- effectType: "damage_percent",
231
- target: "both",
232
- percentValue: 8,
233
- },
234
- ],
235
- },
236
- {
237
- id: "professor_oak",
238
- name: "👴 Profesor Oak",
239
- description: "¡El Profesor Oak anima a tu Pokémon!",
240
- narration: ["¡Un mensaje del laboratorio!", "¡El Profesor Oak envía sus ánimos!"],
241
- rarity: "rare",
242
- effects: [
243
- {
244
- effectType: "stat_change",
245
- target: "random_one",
246
- statType: "offense",
247
- statChange: 2,
248
- },
249
- {
250
- effectType: "stat_change",
251
- target: "random_one",
252
- statType: "speed",
253
- statChange: 1,
254
- },
255
- ],
256
- },
257
- {
258
- id: "dawn_stone",
259
- name: "💎 Piedra Alba",
260
- description: "Una piedra evolutiva aumenta la defensa de un Pokémon.",
261
- narration: ["¡Una extraña piedra emite luz!", "¡El poder de la Piedra Alba se manifiesta!"],
262
- rarity: "rare",
263
- effects: [
264
- {
265
- effectType: "stat_change",
266
- target: "random_one",
267
- statType: "defense",
268
- statChange: 3,
269
- },
270
- ],
271
- },
272
- {
273
- id: "potion_spray",
274
- name: "💊 Lluvia de Pociones",
275
- description: "Pociones caen del cielo curando a ambos Pokémon.",
276
- narration: ["¡Empieza a llover... ¿medicina?!", "¡Una lluvia de pociones cubre el estadio!"],
277
- rarity: "rare",
278
- effects: [
279
- {
280
- effectType: "heal_percent",
281
- target: "both",
282
- percentValue: 12,
283
- },
284
- ],
285
- },
286
- // ==================== EPIC (2 events) ====================
287
- {
288
- id: "legendary_appearance",
289
- name: "✨ Aparición Legendaria",
290
- description: "Un Pokémon legendario otorga poder a un luchador aleatorio.",
291
- narration: ["¡UNA PRESENCIA ABRUMADORA SE SIENTE!", "¡Un Pokémon Legendario observa el combate!"],
292
- rarity: "epic",
293
- effects: [
294
- {
295
- effectType: "stat_change",
296
- target: "random_one",
297
- statType: "offense",
298
- statChange: 3,
299
- },
300
- {
301
- effectType: "stat_change",
302
- target: "random_one",
303
- statType: "defense",
304
- statChange: 2,
305
- },
306
- {
307
- effectType: "stat_change",
308
- target: "random_one",
309
- statType: "speed",
310
- statChange: 2,
311
- },
312
- ],
313
- },
314
- {
315
- id: "max_revive",
316
- name: "⚡ Revivir Máximo",
317
- description: "El Pokémon con menos vida recupera un 30% de HP.",
318
- narration: ["¡Un cristal amarillo brilla con intensidad!", "¡El poder del Revivir Máximo restaura la energía!"],
319
- rarity: "epic",
320
- effects: [
321
- {
322
- effectType: "heal_percent",
323
- target: "random_one", // TODO: Cambiar a "lowest_hp" cuando se implemente
324
- percentValue: 30,
325
- },
326
- ],
327
- },
328
- ];
178
+ export function getRandomEventsForSkin(skinId) {
179
+ const coreEvents = CORE_RANDOM_EVENTS;
180
+ switch (skinId) {
181
+ case "pokemon":
182
+ return [...coreEvents, ...POKEMON_RANDOM_EVENTS];
183
+ case "tribes":
184
+ return [...coreEvents, ...TRIBES_RANDOM_EVENTS];
185
+ default:
186
+ // For unknown skins, return only core events
187
+ return coreEvents;
188
+ }
189
+ }
@@ -5,6 +5,7 @@ export interface StatusDefinition {
5
5
  id: StatusId;
6
6
  name: string;
7
7
  kind: StatusKind;
8
+ color?: string;
8
9
  minDurationTurns: number;
9
10
  maxDurationTurns: number;
10
11
  effectsPerStack: EffectDefinition[];
@@ -0,0 +1,21 @@
1
+ import type { CombatSkin } from "../skins/CombatSkin.js";
2
+ export declare class SkinValidationError extends Error {
3
+ skinId: string;
4
+ entityType: string;
5
+ entityId: string;
6
+ issue: string;
7
+ constructor(skinId: string, entityType: string, entityId: string, issue: string);
8
+ }
9
+ export interface ValidationResult {
10
+ valid: boolean;
11
+ errors: SkinValidationError[];
12
+ warnings: string[];
13
+ }
14
+ export declare class SkinValidator {
15
+ validate(skin: CombatSkin): ValidationResult;
16
+ private validateFighters;
17
+ private validateMoves;
18
+ private validateStatuses;
19
+ private validateTypeEffectiveness;
20
+ private checkDuplicates;
21
+ }
@@ -0,0 +1,183 @@
1
+ export class SkinValidationError extends Error {
2
+ skinId;
3
+ entityType;
4
+ entityId;
5
+ issue;
6
+ constructor(skinId, entityType, entityId, issue) {
7
+ super(`[${skinId}] ${entityType} "${entityId}": ${issue}`);
8
+ this.skinId = skinId;
9
+ this.entityType = entityType;
10
+ this.entityId = entityId;
11
+ this.issue = issue;
12
+ this.name = 'SkinValidationError';
13
+ }
14
+ }
15
+ export class SkinValidator {
16
+ validate(skin) {
17
+ const errors = [];
18
+ const warnings = [];
19
+ const types = skin.getTypes();
20
+ const moves = skin.getMoves();
21
+ const fighters = skin.getFighters ? skin.getFighters() : [];
22
+ const statuses = skin.getStatuses();
23
+ const items = skin.getItems();
24
+ // Create sets for fast lookups
25
+ const typeIds = new Set(types.map((t) => t.id));
26
+ const moveIds = new Set(moves.map((m) => m.id));
27
+ const fighterIds = new Set(fighters.map((f) => f.id));
28
+ const statusIds = new Set(statuses.map((s) => s.id));
29
+ const itemIds = new Set(items.map((i) => i.id));
30
+ // Validate fighters
31
+ this.validateFighters(skin.id, fighters, typeIds, moveIds, itemIds, errors);
32
+ // Validate moves
33
+ this.validateMoves(skin.id, moves, typeIds, statusIds, fighterIds, errors);
34
+ // Validate statuses
35
+ this.validateStatuses(skin.id, statuses, errors);
36
+ // Validate type effectiveness matrix
37
+ this.validateTypeEffectiveness(skin.id, types, skin.getTypeEffectiveness(), warnings);
38
+ // Check for duplicate IDs
39
+ this.checkDuplicates('Type', types.map((t) => t.id), skin.id, errors);
40
+ this.checkDuplicates('Fighter', fighters.map((f) => f.id), skin.id, errors);
41
+ this.checkDuplicates('Move', moves.map((m) => m.id), skin.id, errors);
42
+ this.checkDuplicates('Status', statuses.map((s) => s.id), skin.id, errors);
43
+ this.checkDuplicates('Item', items.map((i) => i.id), skin.id, errors);
44
+ return {
45
+ valid: errors.length === 0,
46
+ errors,
47
+ warnings
48
+ };
49
+ }
50
+ validateFighters(skinId, fighters, typeIds, moveIds, itemIds, errors) {
51
+ for (const fighter of fighters) {
52
+ // Validate types
53
+ if (!fighter.classIds || fighter.classIds.length === 0) {
54
+ errors.push(new SkinValidationError(skinId, 'Fighter', fighter.id, 'Must have at least one type'));
55
+ }
56
+ else {
57
+ for (const typeId of fighter.classIds) {
58
+ if (!typeIds.has(typeId)) {
59
+ errors.push(new SkinValidationError(skinId, 'Fighter', fighter.id, `Invalid type "${typeId}"`));
60
+ }
61
+ }
62
+ }
63
+ // Validate recommended moves
64
+ if (fighter.recommendedMoves) {
65
+ for (const moveId of fighter.recommendedMoves) {
66
+ if (!moveIds.has(moveId)) {
67
+ errors.push(new SkinValidationError(skinId, 'Fighter', fighter.id, `Invalid recommended move "${moveId}"`));
68
+ }
69
+ }
70
+ }
71
+ // Validate recommended items
72
+ if (fighter.recommendedItems) {
73
+ for (const itemId of fighter.recommendedItems) {
74
+ if (!itemIds.has(itemId)) {
75
+ errors.push(new SkinValidationError(skinId, 'Fighter', fighter.id, `Invalid recommended item "${itemId}"`));
76
+ }
77
+ }
78
+ }
79
+ // Validate base stats
80
+ if (!fighter.baseStats) {
81
+ errors.push(new SkinValidationError(skinId, 'Fighter', fighter.id, 'Missing base stats'));
82
+ }
83
+ else {
84
+ const { offense, defense, speed, crit } = fighter.baseStats;
85
+ if (offense < 0 || defense < 0 || speed < 0 || crit < 0) {
86
+ errors.push(new SkinValidationError(skinId, 'Fighter', fighter.id, 'Stats cannot be negative'));
87
+ }
88
+ }
89
+ }
90
+ }
91
+ validateMoves(skinId, moves, typeIds, statusIds, fighterIds, errors) {
92
+ for (const move of moves) {
93
+ // Validate type
94
+ if (!typeIds.has(move.typeId)) {
95
+ errors.push(new SkinValidationError(skinId, 'Move', move.id, `Invalid type "${move.typeId}"`));
96
+ }
97
+ // Validate effects
98
+ if (!move.effects || move.effects.length === 0) {
99
+ errors.push(new SkinValidationError(skinId, 'Move', move.id, 'Must have at least one effect'));
100
+ }
101
+ else {
102
+ for (let i = 0; i < move.effects.length; i++) {
103
+ const effect = move.effects[i];
104
+ // Validate apply_status effects
105
+ if (effect.kind === 'apply_status' && effect.statusId) {
106
+ if (!statusIds.has(effect.statusId)) {
107
+ errors.push(new SkinValidationError(skinId, 'Move', move.id, `Invalid status "${effect.statusId}" in effect ${i + 1}`));
108
+ }
109
+ }
110
+ // Validate clear_status effects
111
+ if (effect.kind === 'clear_status' && effect.statusIds) {
112
+ for (const statusId of effect.statusIds) {
113
+ if (!statusIds.has(statusId)) {
114
+ errors.push(new SkinValidationError(skinId, 'Move', move.id, `Invalid status "${statusId}" in clear_status effect`));
115
+ }
116
+ }
117
+ }
118
+ }
119
+ }
120
+ // Validate onlyForFighterIds
121
+ if (move.onlyForFighterIds) {
122
+ for (const fighterId of move.onlyForFighterIds) {
123
+ if (!fighterIds.has(fighterId)) {
124
+ errors.push(new SkinValidationError(skinId, 'Move', move.id, `Invalid fighter restriction "${fighterId}"`));
125
+ }
126
+ }
127
+ }
128
+ // Validate accuracy
129
+ if (move.accuracy !== undefined && (move.accuracy < 0 || move.accuracy > 100)) {
130
+ errors.push(new SkinValidationError(skinId, 'Move', move.id, `Accuracy must be between 0 and 100, got ${move.accuracy}`));
131
+ }
132
+ // Validate maxPP
133
+ if (move.maxPP <= 0) {
134
+ errors.push(new SkinValidationError(skinId, 'Move', move.id, `maxPP must be positive, got ${move.maxPP}`));
135
+ }
136
+ }
137
+ }
138
+ validateStatuses(skinId, statuses, errors) {
139
+ for (const status of statuses) {
140
+ // Validate duration
141
+ if (status.minDurationTurns < 0) {
142
+ errors.push(new SkinValidationError(skinId, 'Status', status.id, `minDurationTurns cannot be negative`));
143
+ }
144
+ if (status.maxDurationTurns < status.minDurationTurns) {
145
+ errors.push(new SkinValidationError(skinId, 'Status', status.id, `maxDurationTurns (${status.maxDurationTurns}) must be >= minDurationTurns (${status.minDurationTurns})`));
146
+ }
147
+ // Validate kind
148
+ if (status.kind !== 'hard_cc' && status.kind !== 'soft') {
149
+ errors.push(new SkinValidationError(skinId, 'Status', status.id, `Invalid kind "${status.kind}", must be "hard_cc" or "soft"`));
150
+ }
151
+ }
152
+ }
153
+ validateTypeEffectiveness(skinId, types, effectiveness, warnings) {
154
+ for (const attackerType of types) {
155
+ if (!effectiveness[attackerType.id]) {
156
+ warnings.push(`Type "${attackerType.id}" missing from effectiveness matrix`);
157
+ continue;
158
+ }
159
+ for (const defenderType of types) {
160
+ const value = effectiveness[attackerType.id][defenderType.id];
161
+ if (value === undefined) {
162
+ warnings.push(`Missing effectiveness: ${attackerType.id} vs ${defenderType.id}`);
163
+ }
164
+ else if (![0, 0.8, 0.9, 1, 1.1, 1.2].includes(value)) {
165
+ warnings.push(`Unusual effectiveness value ${value} for ${attackerType.id} vs ${defenderType.id}`);
166
+ }
167
+ }
168
+ }
169
+ }
170
+ checkDuplicates(entityType, ids, skinId, errors) {
171
+ const seen = new Set();
172
+ const duplicates = new Set();
173
+ for (const id of ids) {
174
+ if (seen.has(id)) {
175
+ duplicates.add(id);
176
+ }
177
+ seen.add(id);
178
+ }
179
+ for (const duplicate of duplicates) {
180
+ errors.push(new SkinValidationError(skinId, entityType, duplicate, `Duplicate ID found`));
181
+ }
182
+ }
183
+ }
@@ -1,3 +1,4 @@
1
+ import { TRIBE_STATUS_IDS } from "../../skins/cliches/constants.js";
1
2
  import { dbg } from "../debug.js";
2
3
  import { createBaseEvent } from "../events.js";
3
4
  import { randomInRange } from "../rng.js";
@@ -63,7 +64,7 @@ export const applyDamageToFighter = (state, defender, amount, actorId, isCritica
63
64
  events.push({
64
65
  ...createBaseEvent(state.turnNumber, "status_cleared", `${defender.fighterId} se despierta por el golpe`),
65
66
  targetId: defender.fighterId,
66
- statusIds: ["sleep"],
67
+ statusIds: [TRIBE_STATUS_IDS.SLEEP],
67
68
  });
68
69
  }
69
70
  events.push({
@@ -1,3 +1,4 @@
1
+ import { SYSTEM_FIGHTER_ID } from "../../core/ids.js";
1
2
  import { applyStatusToFighter } from "../status/apply.js";
2
3
  import { applyDamageToFighter } from "../combat/damage.js";
3
4
  import { applyHealToFighter } from "../combat/heal.js";
@@ -93,10 +94,17 @@ const checkCondition = (fighter, condition) => {
93
94
  const maxHp2 = fighter.baseStats.offense + fighter.baseStats.defense;
94
95
  const hpPercent2 = (fighter.currentHp / maxHp2) * 100;
95
96
  return hpPercent2 > condition.value;
96
- case "fighter_type":
97
- if (typeof condition.value !== "string")
97
+ case "fighter_type": {
98
+ const { value } = condition;
99
+ if (!value)
98
100
  return false;
99
- return fighter.classIds.includes(condition.value);
101
+ // Handle array of types (fighter matches if it has ANY of the types)
102
+ if (Array.isArray(value)) {
103
+ return value.some(typeId => fighter.classIds.includes(typeId));
104
+ }
105
+ // Handle single type
106
+ return fighter.classIds.includes(value);
107
+ }
100
108
  case "has_status":
101
109
  return fighter.statuses && fighter.statuses.length > 0;
102
110
  case "stat_stage_above":
@@ -120,8 +128,7 @@ const applySingleEffectToFighter = (state, playerKey, fighter, effect) => {
120
128
  if (effect.percentValue) {
121
129
  const maxHp = fighter.baseStats.offense + fighter.baseStats.defense;
122
130
  const damage = Math.floor(maxHp * (effect.percentValue / 100));
123
- const res = applyDamageToFighter(state, updatedFighter, damage, "random_event", // attackerId for log
124
- false);
131
+ const res = applyDamageToFighter(state, updatedFighter, damage, SYSTEM_FIGHTER_ID, false);
125
132
  updatedFighter = res.updatedDefender;
126
133
  events.push(...res.events);
127
134
  }
@@ -130,7 +137,7 @@ const applySingleEffectToFighter = (state, playerKey, fighter, effect) => {
130
137
  if (effect.percentValue) {
131
138
  const maxHp = fighter.baseStats.offense + fighter.baseStats.defense;
132
139
  const healAmount = Math.floor(maxHp * (effect.percentValue / 100));
133
- const res = applyHealToFighter(state, updatedFighter, healAmount, "random_event");
140
+ const res = applyHealToFighter(state, updatedFighter, healAmount, SYSTEM_FIGHTER_ID);
134
141
  updatedFighter = res.updated;
135
142
  events.push(...res.events);
136
143
  }
@@ -13,6 +13,7 @@ import { hasHardCc } from "../status/hardCc.js";
13
13
  import { shouldTriggerRandomEvent, selectRandomEvent } from "../events/randomEventTrigger.js";
14
14
  import { applyRandomEvent } from "../events/applyRandomEvent.js";
15
15
  import { CORE_RANDOM_EVENTS } from "../../core/index.js";
16
+ import { TRIBE_STATUS_IDS } from "../../skins/cliches/constants.js";
16
17
  // ------------------------------------------------------
17
18
  // Helpers KO -> forced switch (estilo Pokémon real)
18
19
  // ------------------------------------------------------
@@ -153,7 +154,7 @@ export const resolveTurn = (state, actions) => {
153
154
  events.push({
154
155
  ...createBaseEvent(currentState.turnNumber, "action_skipped_hard_cc", `${self.fighterId} está paralizado y no puede moverse`),
155
156
  actorId: self.fighterId,
156
- statusId: "paralysis",
157
+ statusId: TRIBE_STATUS_IDS.PARALYSIS,
157
158
  });
158
159
  continue;
159
160
  }
@@ -190,7 +191,7 @@ export const resolveTurn = (state, actions) => {
190
191
  freeze: `${self.fighterId} está congelado y no puede actuar`,
191
192
  flinch: `${self.fighterId} se acobarda y no puede atacar`,
192
193
  };
193
- const statusId = ccStatus?.statusId || "unknown";
194
+ const statusId = ccStatus?.statusId || TRIBE_STATUS_IDS.SLEEP;
194
195
  const message = statusMessages[statusId] ||
195
196
  `${self.fighterId} no puede actuar por control total`;
196
197
  events.push({
@@ -13,6 +13,7 @@ export interface CombatSkin {
13
13
  readonly id: string;
14
14
  getTypes(): TypeDefinition[];
15
15
  getTypeEffectiveness(): TypeEffectivenessMatrix;
16
+ getFighters(): any[];
16
17
  getMoves(): MoveDefinition[];
17
18
  getItems(): ItemDefinition[];
18
19
  getAmulets(): AmuletDefinition[];
@@ -1,9 +1,10 @@
1
- import { ItemDefinition, MoveDefinition } from "../../core/index.js";
1
+ import { FighterDefinition, ItemDefinition, MoveDefinition } from "../../core/index.js";
2
2
  import { PlayerBattleConfig } from "../../engine/rules.js";
3
3
  import type { CombatSkin, PlayerTeamLoadout } from "../CombatSkin.js";
4
4
  export declare class TribeSkin implements CombatSkin {
5
5
  readonly id = "tribe";
6
6
  getTypes(): import("../../index.js").TypeDefinition[];
7
+ getFighters(): FighterDefinition[];
7
8
  getTypeEffectiveness(): import("../../index.js").TypeEffectivenessMatrix;
8
9
  getMoves(): MoveDefinition[];
9
10
  getItems(): ItemDefinition[];