| name | Add Feature |
| description | Guide for adding new moves, items, or mechanics to the modular Pokemon Battle system. |
Adding a New Feature (Move, Item, or Mechanic)
Description
A systematic guide to adding new content to the Pokemon Battle Modular system, ensuring all relevant files are updated and the modular structure is respected.
Prerequisites
- Understand the file structure:
js/data: Definitions (Moves, Items, Constants)
js/core: Logic (Battle flow, Mechanics, Effects)
js/ui: Visuals (UI, Menus)
js/anim: Animation engine, registry, and high-level API
js/screens: Screen modules (Party, Summary, Selection)
js/systems: Audio engine, Logger, Storage
Adding a New Move
1. Determine Move Type
- Standard Move: Uses existing damage/status logic (just add to PokeAPI data)
- Unique Move: Requires custom logic in
MOVE_DEX
Move Generation Rules
When a Pokémon is generated (Wild, Boss, or Starter), its moves are selected dynamically:
- Gen 2 Prioritization: The system strictly follows Pokémon Crystal and Gold/Silver level-up tables. If a Pokémon didn't exist in Gen 2, it defaults to its debut generation's moveset to ensure a consistent, non-redundant growth path.
- Elite Moves: 10% base chance (100% for Bosses) to include an Egg move or High-level move.
- Special Learning Moment: Every level-up has a 15% chance to trigger a "Special Learning" event where the Pokémon wants to learn a move from its TM or Tutor pool that it wouldn't normally learn by level.
- Natural Count: Pokémon only show up with the number of moves they would naturally know at that level (plus any elite moves).
2. Add to MOVE_DEX (if unique)
Location: js/data/moves.js
const MOVE_DEX = {
'MOVE NAME': {
isUnique: true,
onHit: async (battle, user, target, weatherMod) => {
return true;
}
}
};
3. Common Move Patterns
Damage + Volatile Status (e.g., Bind, Fire Spin)
'BIND': {
isUnique: true,
onHit: async (battle, user, target, weatherMod) => {
const moveData = { name: 'BIND', type: 'normal', power: 15, category: 'physical' };
const result = await battle.handleDamageSequence(user, target, moveData, user === battle.p, weatherMod);
if (result.success && target.currentHp > 0) {
target.volatiles.trapped = {
turns: Math.floor(Math.random() * 4) + 2,
source: user.name,
moveName: 'BIND'
};
await UI.typeText(`${target.name} was trapped!`);
}
return result.success;
}
}
Status Move with Immunity Check (e.g., Leech Seed)
'LEECH SEED': {
isUnique: true,
onHit: async (battle, user, target) => {
if (target.types.includes('grass')) {
await UI.typeText("It doesn't affect\n" + target.name + "...");
return 'IMMUNE';
}
if (target.volatiles.seeded) {
await UI.typeText(`${target.name} is already\nseeded!`);
return 'FAIL';
}
target.volatiles.seeded = { source: user === battle.p ? 'player' : 'enemy' };
await UI.typeText(`${target.name} was seeded!`);
return true;
}
}
Move Requiring Last Move Tracking (e.g., Disable)
'DISABLE': {
isUnique: true,
onHit: async (battle, user, target) => {
if (!target.lastMoveUsed || !target.lastMoveUsed.name) {
await UI.typeText("But it failed!");
return 'FAIL';
}
target.volatiles.disabled = {
moveName: target.lastMoveUsed.name,
turns: Math.floor(Math.random() * 4) + 4
};
await UI.typeText(`${target.name}'s ${target.lastMoveUsed.name}\nwas disabled!`);
return true;
}
}
4. Add End-of-Turn Processing (if needed)
Location: js/core/effects.js → processEndTurnStatus()
if (mon.volatiles.trapped) {
await wait(400);
const trap = mon.volatiles.trapped;
await UI.typeText(`${mon.name} is hurt by\n${trap.moveName}!`);
const dmg = Math.floor(mon.maxHp / 16);
await battle.applyDamage(mon, Math.max(1, dmg), 'normal');
trap.turns--;
if (trap.turns <= 0) {
delete mon.volatiles.trapped;
await UI.typeText(`${mon.name} was freed!`);
}
if (mon.currentHp <= 0) return;
}
5. Add Move Blocking Logic (if needed)
Location: js/core/turn_manager.js → processAttack()
if (attacker.volatiles.disabled && attacker.volatiles.disabled.moveName === move.name) {
await UI.typeText(`${attacker.name}'s ${move.name}\nis disabled!`);
return;
}
6. Add Visual Animation (optional but recommended)
Location: js/anim/anim_registry.js
Register a custom animation for the move using the data-driven AnimFramework. See the Battle Animations & Timing skill for the full reference.
AnimFramework.register('move-name', [
{ type: 'sfx', sound: 'fire' },
{ type: 'spriteMove', target: 'attacker', preset: 'lunge' },
{ type: 'stream', from: 'attacker', to: 'defender',
count: 15, interval: 25, spread: 12, travelTime: 350,
size: 6, color: '#ff4500', outline: '#8b0000',
scaleStart: 0.6, scaleEnd: 2.0, svgShape: 'fire'
},
{ type: 'sfx', sound: 'fire' },
{ type: 'parallel', steps: [
{ type: 'screenFx', class: 'fx-fire', duration: 500 },
{ type: 'spriteShake', target: 'defender', duration: 400 },
{ type: 'formation', target: 'defender', pattern: 'fireBlast',
shape: 'fire', particleSize: 8, color: '#ff4500', duration: 500
}
]}
]);
20 step types available:
- Audio/Control:
sfx, cry, wait, callback, parallel
- Scene effects:
screenFx, flash, tilt, bgColor, invert, wave, cssClass
- Sprite effects:
spriteShake, spriteMove (presets: lunge, dodge, jump, recoil, charge, slam, float, shake)
- Projectiles:
stream (supports svgShape), beam, volley, particles
- Shapes/Overlays:
overlay (SVG shapes: lightning, fire, water, leaf, star, claw, fist, skull, spiral), formation (patterns: fireBlast, cross, ring, xShape), spawn
Use 'attacker' and 'defender' as position/element references — the framework resolves player/enemy side automatically.
To play: await AnimFramework.play('move-name', { attacker, defender, isPlayerAttacker });
7. Update AI Heuristics (for AI-Aware Moves)
Location: js/engine/ai.js → AIHeuristics
If you add a move with a unique mechanic (e.g., healing, charging, sleep-dependent, or hazards), you must update the AI heuristics so the enemy understands how and when to use it optimally.
- Status/Utility: Add rules to
evaluateStatusUtility
- Buffs/Debuffs: Add rules to
evaluateBuffUtility
- Advanced Strategies: Add unique logic to
evaluateAdvancedStrategies (e.g., checking if the player is asleep before using a move).
8. Update Documentation
- Add to
README.md under appropriate section
- If new volatile status, add to volatile status list
Adding a New Item
1. Add to ITEMS
Location: js/data/items.js
const ITEMS = {
'item-key': {
name: 'Item Name',
desc: 'Description',
type: 'heal',
heal: 50,
img: 'url-to-sprite'
}
};
2. Implement Usage Logic
- Healing/Revive: Handled automatically in
js/screens/party.js
- Pokéballs: Add logic to
js/core/capture.js
- Battle Items: Add logic to
js/core/battle.js → performItem()
Common Pitfalls
❌ Wrong Return Values
if (target.types.includes('grass')) {
await UI.typeText("It doesn't affect...");
return false;
}
if (target.types.includes('grass')) {
await UI.typeText("It doesn't affect...");
return 'IMMUNE';
}
❌ Forgetting End-of-Turn Processing
If your volatile status needs to tick/expire, you MUST add logic to processEndTurnStatus() in effects.js.
❌ Not Tracking State
- Use
target.volatiles.X for temporary effects
- Use
target.lastMoveUsed for move history (tracked automatically)
- Use
battle.weather, battle.terrain for field effects
Testing Checklist