Skip to content

llm.md

Guide for coding agents

This is the text a coding agent reads to build its own version of the game with this site’s data, on any platform. Copy it into the agent, or give it the link to the Markdown file.

Open llm.md

Contents
  1. Files
  2. Conventions
  3. Units and their animations
  4. Movement and AI
  5. Duel
  6. Game rules
  7. Items
  8. Spells and runes
  9. Spell scenes
  10. Aiming a spell
  11. Status effects
  12. Combat
  13. HP bar
  14. Enemy marker
  15. Mana crystals
  16. Battle camera
  17. Damage numbers
  18. Chant screen
  19. Battle end
  20. Title screen
  21. Characters
  22. Battle maps and battles
  23. Appear seals
  24. World map
  25. The story: /juego/data.json
  26. Audio
  27. Other pictures
  28. Not in the data

LostMagic: game data for building your own version

LostMagic (Taito, 2006, Nintendo DS) is a tactics game. Isaac, the hero, casts spells by drawing runes on the touch screen and fights alongside the monsters he recruits, through a story whose choices lead to four endings.

These files hold what was extracted from the game's cartridge (US release): sprites and animations of every unit, character portraits, battle maps with where each enemy starts, the full story with its choices and the logic that orders the missions, texts in English, Spanish, French and German, music, sound effects and the rest of the art. Use them to build a version of the game on any platform.

Base URL: https://lostmagic.skapxd.dev/

Paths inside the JSON files start with / and are relative to the base URL.

To download everything at once: https://lostmagic.skapxd.dev/data/files.txt (one URL per line).

Files

File Contents
/data/units.md, /data/units.json The 84 units (77 with a name, the rest unnamed appearances), each linking to its details
/data/units/<slug>.md, /data/units/<slug>.json One unit: its stats, sprite sheet, the exact place of every frame, and its animations
/data/levels.md, /data/levels.json Levels 1 to 50: experience, the growth curves of the stats and the Line Up capacity
/data/rules.md, /data/rules.json The game's limits: team, monsters, items, levels and experience
/data/ai.md, /data/ai.json Movement and AI: routes, target search and the units' orders
/data/duel.md, /data/duel.json The duel mode: the ready-made teams and the Wi-Fi rating
/data/items.md, /data/items.json The 8 items: what they raise, how much at each level and which chests hold them
/data/chest.md, /data/chest.json The treasure chest as a sprite: its closed picture and its five animations
/data/appear-seals.md, /data/appear-seals.json The appear seals that glow on the floor of some battles, and how a unit comes out
/data/spells.md, /data/spells.json The 396 spells: runes, MP, aiming, power, status, terrain and their numbers
/data/spell-families.md, /data/spell-families.json The families of similar spells, what changes inside each one and each spell's total effect
/data/best-spells.md, /data/best-spells.json The best spells for the player: yield per MP, big jumps, the ones not worth moving up to, a stroke score for each rune and spell, and the ranking by battle situation
/data/spell-scenes.md, /data/spell-scenes.json Scenes: graphics, palettes, frames and sounds for 396 spells; 90 units and 141 statuses
/data/runes.md, /data/runes.json The 18 runes: the strokes the game accepts, where they are learned and the MP of each
/data/rune-strokes.md, /data/rune-strokes.json 640 test strokes for the rune recognizer and what the game gives for each
/data/statuses.md, /data/statuses.json Status effects: what they do, how long they last and which spells cause them
/data/combat.md, /data/combat.json Battle constants: opposite elements, normal attacks, shots and element-level growth
/data/camera.md, /data/camera.json The battle camera: world bounds, D-pad, chant recentring, scripts, shake, top screen and each map's backdrop speed
/data/aiming.md, /data/aiming.json Aiming a spell: how each mode is aimed, the arrow of the shots, the reticle, the areas, the icons and the range shade, with their pictures
/data/damage-numbers.md, /data/damage-numbers.json The damage and healing numbers over units: digits, colours, place and how long they stay, with their pictures
/data/chant-screen.md, /data/chant-screen.json The chant screen: the magic circle and the layer that dims the field, the stroke and the top screen's rune list, with their pictures
/data/battle-end.md, /data/battle-end.json The end of a battle: the WIN, LOSE, DRAW and TIME OVER banners, their timing and music, and the results screen after a win, with their pictures
/data/title-screen.md, /data/title-screen.json The title screen: both backgrounds, the five buttons with their label in each language, how they come in, the touch and where each one leads
/data/enemy-marker.md, /data/enemy-marker.json The letter next to every enemy's HP bar: picture per language, position and when it shows
/data/characters.md, /data/characters.json The 19 story characters and their portrait expressions
/data/maps.md, /data/maps.json The 55 battle maps and their 176 battles: missions and starting units
/data/world-map.md, /data/world-map.json The world map: 44 places, 50 paths and what each place offers in each chapter
/juego/data.json The story and its logic: chapters, missions, scenes, battles, flags and endings
/juego/textos/<en|es|fr|de>.json Every text of /juego/data.json in one language
/data/audio.md, /data/audio.json 24 songs and 147 sound effects
/data/gallery.md, /data/gallery.json 818 more pictures: runes, spells and effects, backgrounds, world map, interface, unused art

Conventions

  • Every file under /data/ comes in two formats with the same content: .md, as tables, easier to read and lighter in tokens, and .json, to load the data in a program. The fields described below are the JSON keys; the Markdown tables use the same names as column headers (name.en, name.es for the two languages).
  • Each of the DS's two screens is 256×192 px. Story scenes are drawn for the top one.
  • Coordinates are in pixels, from the top-left corner, with y growing downwards.
  • Durations come in ticks of 1/60 s, the console's frame rate. ms fields are the same, rounded.
  • The art is pixel art: scale it by whole numbers with nearest-neighbour sampling to keep it sharp.
  • Names come in English and Spanish as { "en": …, "es": … }. The story has four languages.
  • Pictures are PNG; animated GIFs are previews only. Audio is AAC in .m4a: convert it if your platform does not play AAC.

Units and their animations

/data/units.json lists every unit: uid (the id battles and the story use; null for unnamed appearances), slug, name, element (fire, wind, water, earth, dark, light or null), a portrait picture, the sheet, and details, the URL of its full data.

Each unit has one sprite sheet: a PNG holding every frame the unit uses. sheet.cells gives the exact rectangle of each frame on it (x, y, w, h); the position in that list is the frame's number.

An animation is a loop of frames drawn on a fixed canvas of width × height:

  • Each item of frames says which sheet frame to show (cell), where its top-left corner goes on the canvas (x, y) and for how long (ticks). The same cell can appear several times.
  • origin is the unit's anchor point on the canvas. It stays fixed while the frames move around it, which is how jumps and attacks move the body. Put the canvas so that origin falls on the unit's position.
  • action and facing say when the game plays it: the unit's action and which way it faces (down front view, up back view, left, right; empty when the action has no facing). used is false for the ones that are drawn but never played.
  • preview is the same animation as a GIF, to see what it shows. GIFs store times in 1/100 s and merge repeated frames, so use frames for timing.

The actions:

action When
idle Standing still. Usually a single frame, on the sheet but not as an animation
walk While it moves
attack Its normal attack, up close or from a distance
chant While the runes are drawn (Isaac and bosses)
cast When the spell is released
knocked_back A spell that knocks units down hits it; get_up follows
get_up Standing up again after the fall
sleep A spell puts it to sleep
dance Isaac, with the dancing spells
trapped A capture trap holds it
captured The capture works
die Its HP reaches 0. Plays once; half a second later the unit fades out
run, victory Drawn, never used (used: false)

When a unit has no animation of its own for an action, it keeps its idle pose. Right-facing animations are separate drawings, not a mirror made at play time.

{
  "uid": 1,
  "slug": "isaac",
  "name": {
    "en": "Isaac",
    "es": "Isaac"
  },
  "element": null,
  "description": {
    "en": "A 14 year old boy. His face plainly displays his emotions.",
    "es": "Un joven. Su rostro muestra emociones."
  },
  "stats": {
    "hp": [
      300,
      450
    ],
    "mp": [
      30,
      130
    ],
    "attack": [
      80,
      80
    ],
    "defense": [
      25,
      38
    ],
    "magic": [
      30,
      130
    ],
    "magic_defense": [
      87,
      92
    ],
    "speed": [
      21,
      23
    ],
    "accuracy": [
      100,
      100
    ],
    "evasion": [
      0,
      30
    ]
  },
  "growth": "linear",
  "up_cost": 1,
  "squad_size": 1,
  "flying": false,
  "ranged": false,
  "can_capture_crystals": true,
  "attack_interval_ticks": 60,
  "attack_reach_px": 0,
  "radius_px": 8,
  "head_px": 19,
  "speed_px_per_tick": [
    0.65625,
    0.71875
  ],
  "attack_strike_ticks": 8,
  "attack_element": null,
  "projectile_type": null,
  "shot_start_px": null,
  "sight_px": 0,
  "keep_distance_px": 0,
  "terrain_spared": [],
  "attack_used_by": "dance_spells",
  "attack_note": {
    "en": "Never attacks: its Attack is used only by the dance spells. Isaac deals damage with his spells and the creatures he sends.",
    "es": "Nunca ataca: su Ataque solo lo usan los hechizos de baile. Isaac hace daño con sus hechizos y con las criaturas que manda."
  },
  "portrait": {
    "url": "/unidades/isaac/retrato.png",
    "width": 125,
    "height": 125
  },
  "sheet": {
    "url": "/unidades/isaac/hoja.png",
    "width": 1012,
    "height": 156,
    "cells": [
      {
        "x": 6,
        "y": 10,
        "w": 32,
        "h": 32
      },
      "…"
    ]
  },
  "animations": [
    {
      "index": 1,
      "action": "walk",
      "facing": "down",
      "used": true,
      "preview": "/unidades/isaac/01.gif",
      "width": 35,
      "height": 34,
      "origin": {
        "x": 20,
        "y": 23
      },
      "frames": [
        {
          "cell": 4,
          "x": 3,
          "y": 2,
          "ticks": 15,
          "ms": 250
        },
        "…"
      ]
    },
    "…"
  ],
  "battles": [],
  "casts_spells": []
}

battles lists the story battles the unit takes part in.

To draw frame i of an animation: take the rectangle sheet.cells[frames[i].cell] and paste it on the canvas at (frames[i].x, frames[i].y). Show it for frames[i].ticks / 60 seconds, then go to the next frame and back to the first at the end.

Stats

Each named unit's /data/units/<slug>.json also has its numbers from the game's tables:

  • stats: hp, mp, attack, defense, magic, magic_defense, speed, accuracy and evasion, each as [level 1, level 50]. At level L a stat is level1 + floor((level50 − level1) × factor / 128), where factor is the value of the unit's growth curve (early, late or linear) for level L in /data/levels.json.
  • speed_px_per_tick: how far it walks per tick at levels 1 and 50; it is speed / 32. Flying units use the same speed.
  • flying, ranged (its normal attack is a projectile), attack_interval_ticks (time between attacks), attack_reach_px (it attacks when the gap between the two units' edges is below this) and radius_px (its body is a circle of this radius).
  • head_px: the height of its head above its point; the HP bar and the status icons go over it.
  • up_cost (what it takes from the Line Up's capacity) and squad_size (how many units one squad brings).
  • attack_used_by: what its attack stat is for. normal_attack, its normal attack. The units that never attack (attack_reach_px 0) have dance_spells (Isaac: only the dance spells use it, hitting with his full attack minus a quarter of the target's defense) or nothing (the mages, the story bosses and the Villager: it is never used). attack_note explains it in English and Spanish, and says how the unit deals damage. /data/units.json has both fields for every unit.
  • element is the unit's element, and description the game's text about it, in English and Spanish.

/data/levels.json has, per level: exp_to_next (the experience to reach the next one), the factors of the three curves (growth, out of 128) and lineup_up, how many UP of units the Line Up holds at that level of Isaac's.

Movement and AI

/data/ai.json describes how units move and what they do on their own. It is read from the game's code (evidence: static); it was not observed in an emulator.

  • movement: routes are straight (route: straight), with no path finding and up to route_points_max points; a unit reaches a point when it is less than point_reached_distance_px away. A group sent to a point spreads around it: each unit goes to the point plus its direction from the group's centre × (its radius + group_spacing_extra_px). A unit walks speed / 32 px per tick, without acceleration; obstacles and other units push it out.

  • Target: every retarget_every_ticks ticks, in the states_with_target_search states, it looks for enemies less than search_px away and picks the nearest, the one with most HP or the one with least HP (these two prefer targets within its reach).

  • orders: each order (order, with the game's classes in classes) sets a decision state (state):

    • wait with mode 0 (state 0) attacks without chasing whoever comes within its reach + attack_in_place_extra_px; with another mode (state_other_mode, 1) it chases within its sight radius.
    • move follows the route without attacking; move_auto attacks on the way without leaving it; move_attack chases within its sight radius or follows the route; move_wait_auto follows the route, chases and, once stopped, waits.
    • guard (states 9 and 10) watches over a point: guard says when it walks back (distance > sight) and how far (distance <= sight / 2).
    • escape (11) runs straight away while the distance between centres is under the unit's keep_distance (/data/units.json).
    • take_crystal (8) goes to the nearest mana crystal of another side within order_list.take_crystal_radius_px and stays until it is its side's.

    The sight radius is each unit's sight, and missions change it.

  • unresolved_states: states that exist but whose producing order is not known yet.

Unit orders in battles

Each unit of a battle in /data/maps.json has orders, its list of orders; when it is empty, the unit gets none at the start. unit_orders in /data/ai.json describes each order: state is the state it leaves the unit in (null: unchanged), target how it gives the point and ends when it ends.

  • Points: x, y is a map point in px; dx, dy are added to the unit's position when the order starts. The unit walks straight to it.
  • wait: ticks (null: forever); chase: false is state 0 (attacks without moving), true state 1 (chases within its sight).
  • back: goes back steps orders, never before the first one. This is how patrols repeat.
  • target_nearest, target_most_hp, target_least_hp: the rule it picks targets by from then on. set_sight: its sight radius becomes sight_px.
  • change_max_hp: its max HP becomes floor(max × percent / 100); set_max_hp: hp. Its HP becomes that same value, never above the max it had.
  • guard never ends: the list stays there.

How the list runs (order_list), one step per tick (steps_per_tick):

  1. It starts at the first order. The tick an order starts in does not count towards ending it.
  2. ends: at_once ends in the tick it starts; route, the first tick after the unit reaches the point (on arrival it is in state route_end_state); ticks, once ticks ticks have been counted after the start tick, on the next one; crystal, when the crystal is its side's, or at once when none is that close; jump is back; removed, like at_once, and the order also leaves the list.
  3. The next order starts on the next tick. With a wait of N ticks, the next order starts N + 2 ticks after the wait started.
  4. back counts only the orders still in the list: removed_when_done are gone.
  5. After the last one ends, the unit keeps the state it is in and gets no more orders.

Example, the first Misty Mush of battle 1: change_max_hp 75 (leaves the list), move to (344, 153), wait 30, move to (344, 133), wait 30, back 4. It patrols between the two points and stays at each one for the wait's 30 ticks plus the ticks the list takes to step; it does not attack while it walks and attacks without moving while it waits.

Duel

/data/duel.json describes the duel mode, where two people play each other. It comes from the game's data and code (DB/chrSelScnDat.cssd, DB/eloRate.elo); it was not observed in an emulator.

  • presets: the 8 ready-made teams a player can pick. Each has level (the player's level, 40), element_levels (the six element levels, which the game clamps to 1–50) and squads, three squads (unit_id of /data/units.json, name, slug and level). The cartridge does not name them: they are numbered 1 to 8.
  • rating: the rating of Wi-Fi matches, between min and max. Each of the rows has the bound on the difference (difference, own rating − the opponent's) and the points a win (win), a draw (draw) or a loss (loss) adds. A negative difference uses the first row whose bound is at least the difference; any other, the last row whose bound is at most it. Then the rating is kept between min and max. elo_k is 50 because all 53 rows match, within one point, the Elo system with K = 50: K × (result − 1 / (1 + 10^(−difference/400))), with result 1, 0.5 or 0.

Game rules

/data/rules.json has the game's limits, read from its code:

  • team: squads_max, the squads that go with Isaac, never more than the map's squad start points; player_units_max; formation_px, where each unit of a squad starts from its start point (y grows downwards); capacity_up_by_isaac_level, the UP the team holds at each of Isaac's levels, from level 1.
  • roster: monsters_max, the monsters that can be kept, and captures_per_battle_max.
  • items: inventory_max, found_level_max (the highest level of a found item) and found_per_battle_max.
  • levels: level_max of Isaac and the monsters, element_level_max and enemy_level_max.
  • experience: per_kill_per_level (each unit that dies gives that number × its level), battle_max and level_1_to_max.
  • battle: default_time_limit_s, the limit when the mission sets none.
  • new_game_plus: the levels enemies gain in each chapter (1 to 5, capped at levels.enemy_level_max) and the extra levels of chest items (capped at items.found_level_max).
  • checkpoints: how many map missions and scenes and how many free battles take a checkpoint when they start.

The rules that are not a number:

  • Isaac is always in the team and carries no item. Each squad carries one item, which works on every unit in it, and has as many units as its species' squad_size.
  • UP is only checked when the team is put together, not in battle.
  • Only regular enemy units can be captured, not bosses or allies. They join after a win, at their battle level with 0 experience. Repeated species in the list and repeated items in the inventory are allowed; with the list full, the new one replaces one that is not in a squad, or is dropped.
  • Every unit of the enemy side or allied unit of the map that dies gives experience, whoever kills it; captured ones give nothing. On a win, Isaac and every squad each get the whole amount; it is not split.
  • A defeat or a draw discards everything gained in the battle. Continuing puts the story and the runes back to the checkpoint, taken when a mission, scene or free battle starts from the map; level, monsters and items are kept, and the mission replays from its start.
  • Retiring from the pause menu discards the battle and goes to the title, with no continue.
  • In New Game+ Isaac keeps his level, experience, element levels, runes, monsters, team and items; the story starts again with harder battles.
  • The results screen, only after a win, shows in order: experience and element levels, items found, captured monsters and new spells. There are no ranks or grades.
  • There is no money or shop, and no maximum of units on the field or of active spells. Isaac can cast as soon as the previous cast animation ends; MP is the limit.

Items

/data/items.json has the 8 items. Each squad carries one, applied to every unit in it when the battle starts; Isaac carries none.

icon is the 16 × 16 px icon the menus show. It depends on the stat the item raises, so several items share one. Battles draw no item icon: chests always look the same.

pictures are each item's icons at levels 1 to 6, made with AI for this site: they are not in the game. sheet is the 120 × 160 px sheet and by_level gives each level's 20 × 20 px cell (x, y, w, h). From level 2 the icon has an aura of another color, and from level 4, sparkles. To keep them sharp, scale them by whole numbers with image-rendering: pixelated.

  • stat: the stat it changes (hp, speed, attack, interval, accuracy, evasion, defense or magic_defense).
  • bonus_by_level: the value v at levels 1 to 10, out of 128. Chests only give levels 1 to items.found_level_max of /data/rules.json.
  • effect, how v applies to the unit's stat x:
    • multiply: x + floor(x × v / 128). With hp, the unit also starts with its HP full.
    • reduce: x − floor(x × v / 128), at least 1.
    • double: x × 2, at any level.
    • gap_to_100: x + floor((100 − x) × v / 128).
  • in_battle (Wind Shield, Hunter Eye, Angel Coat and Dragon Scale; null for the rest): what the item changes in a battle, in en and es: what (what the stat is), examples (worked examples with level-1 units), good, bad, caution, level (when the item's level does not matter) and anchor (the section of /logic/combat/ with the rule). examples has the same examples as numbers: accuracy, evasion and hit_percent as [without the item, with it].
  • chests: the story chests that can give the item: battle (the same number as in /data/maps.json), map, the position x, y in map pixels, chance (0 to 1), levels, the chance of each level, and new_game_plus_levels, the same chances in New Game+.

Level 6 is almost never found in the story: chests give up to level 5, except one Demon Horn chest on map 038 (1.1 %). In New Game+ the game adds new_game_plus.chest_level_bonus (2) to the chest's level, capped at items.found_level_max (6), so chests of levels 4 and 5 give level 6.

How accuracy and evasion decide a hit (stats at a level in /data/units/<slug>.json; Hunter Eye doubles the attacker's accuracy, Angel Coat the defender's evasion, at any item level):

  • Melee hits when a random number from 0 to 100, plus accuracy, minus evasion, reaches 100: a chance of clamp(accuracy − evasion + 1, 0, 101) / 101.
  • A shot hits when a random number from 0 to 99 is under (accuracy − evasion) & 0xff: the game keeps one byte of the difference. So it always hits when evasion is higher than accuracy, and from a difference of 256 it starts again from 0 (a shooter with Hunter Eye can hit less). This comes from reading the game's code (ov1 0x2130f40); it was not tested in an emulator.

godot says how each item will work in our Godot port (approved design, version 1), apart from the original's fields: same_as_original, effect and bonus_by_level as above (plus add: x + bonus_by_level, in points), line_up_shows_effect (the Line Up screen shows the effect of all 8), the text in en and es (same, the same as same_as_original; change, what changes and why, or "Same as the original"; examples; notes) and, for Hunter Eye and Angel Coat, examples as numbers: hit_percent without the item, by_level with it at levels 1, 3, 5 and 6, and original_hit_percent with the original's item. In Godot:

  • Hunter Eye: multiply, accuracy × (1 + v/128), like Demon Horn.
  • Angel Coat: add, 4 points of evasion a level (v × 40 / 128, rounded).
  • A shot hits with a chance of clamp(accuracy − evasion, 0, 100) / 100: with higher evasion, it always misses.
  • The other items, the levels (1 to 6, +2 in New Game+) and who carries items stay as in the original.

When the battle loads, the game draws each chest's item and level. The item goes to the player's unit that opens the chest and is received on a win. In New Game+ the level rises by 2, with the same cap.

/data/chest.json is the chest as a sprite:

  • closed is the closed picture the maps draw, placed at the chest's point plus (dx, dy).
  • sheet holds cells equal cells of cell.width × cell.height px in columns columns. The chest's point is at cell.origin in each cell.
  • animations: idle0 (closed), open (opening), loop (open with the reward), suka (smoke) and emptybox (empty). Each frames is a list of cell and ticks of 1/60 s. loop says whether it repeats, and preview is a GIF or PNG to look at.
  • flow: with a reward the game goes from idle0 to open and stays in loop; without one, from idle0 to emptybox. suka has used_by_game false: the game never picks it.

Spells and runes

Runes

/data/runes.json has the 18 runes: id (1 to 18), element, level (1 to 3), its picture, spell (the spell it casts alone) and learned (the missions where it is learned). Isaac casts a spell by drawing 1 to 3 runes in order while holding L (R with the left-handed option); releasing the button ends the chant. Each rune is a single stroke: lifting the stylus closes it.

templates are the strokes the game accepts for each rune: points on a 0–63 grid (y down), in drawing order. from_dot marks the ones that start at the dot of the rune's picture; the rest are the same drawing from another start or in the other direction.

To recognize a stroke:

  1. Keep one point per frame while the screen is touched. After the first, keep only the points that move at least 1 px in x from the last one kept: the game ignores movement in y. A stroke of fewer than 4 points cancels the chant.
  2. Take a square centred on the stroke, with side max(width, height), so the proportions are kept. Simplify it: keep both ends and, piece by piece, the point farthest from the chord if it is max(1, 2 × side / 64) px away or more, up to 40 points. Map it to the 0–63 grid with (p − origin) × 64 / (side + 1) and drop repeated consecutive points.
  3. For each point k ≥ 1 compute angle[k], the direction of the segment that reaches it, and ratio[k], the share of the total length that segment is. Point 0 takes the direction of the first segment.
  4. Compare the stroke (I, with nI points) with each template (P, with nP points) by walking both lines together along their length:
i = j = 1; remP = ratioP[1]; remI = ratioI[1]; score = 0
repeat nP + nI − 3 times:
  A = 1 − |angleP[j] − angleI[i]| / 180°        # angle difference wrapped to 0–180°
  if remP <= remI:
    remI −= remP
    q = I[i] if remI < ratioI[i] / 2 else I[i − 1]
    score += remP × A × (1 − manhattan(q, P[j]) / 128)
    j += 1; remP = ratioP[j]
  else:
    remP −= remI
    q = P[j] if remP < ratioP[j] / 2 else P[j − 1]
    score += remI × A × (1 − manhattan(I[i], q) / 128)
    i += 1; remI = ratioI[i]
  1. The template with the highest score (0 to 1) wins; on a tie, the first one. If it reaches accept (0.70), the stroke is its rune. Rotation is not forgiven: about 30° off is already at the limit.

A recognized rune must also be learned, the runes drawn so far must begin some spell, the runes-per-spell limit cannot be passed (runes_per_spell: the missions that allow 2 and 3) and there must be enough MP for that rune. If anything fails, the chant is cancelled.

A spell's quality is the average score of its runes. If it reaches great (0.85), 0.5 is added: the factor goes from 0.70–0.85 to 1.35–1.50.

/logic/runes/practice/ lets you practise drawing with this recognizer, ported to TypeScript with the same integer arithmetic: it gives the rune, the score, whether it passes accept or reaches great, the factor and the three closest templates. /data/rune-strokes.json holds 640 test strokes with what the game's recognizer gives for each; a port must give the same. Each one is samples (one point per frame at 60 Hz, in pixels of the 256 × 192 touch screen) and kept (how many the capture keeps); with fewer than 4, cancelled is true; else top is the three closest templates as [rune, template, score], the template numbered as in templates of /data/runes.json (runes in order) and the score out of 4096: accepted from 2868, great from 3482.

Spells

/data/spells.json has the 396 spells:

  • name: the cartridge's name in English and Spanish. The Spanish ones are often abbreviated (none is longer than 12 characters, for example «Torbell. f.»); the site keeps them as they are, without expanding or translating them, and shows the English one beside them.
  • runes: its runes, in order. element: its element, which is its last rune's: 294 of the 396 mix runes of different elements.
  • kind: the family (cloud, wall, bullet, blast, falling_objects, aura, zone, unit_effect, object, dragon). variant: the exact behaviour within the family.
  • mp_by_rank: the MP cost with the element at rank 0 to 4. Rank is floor((element level − 1) / 10). The cost is the sum of its runes: mp_per_rune[position][rank of that rune's element][rune level − 1] from /data/runes.json. Each rune is paid as it is drawn.
  • targeting: how it is aimed (circle: centre and radius; line: start and end; fixed_circle and point: a point; direction: it leaves Isaac towards where the stylus is released; unit: a unit; instant: no aiming). targets: enemies, allies or all; null for the three that touch no unit (Transport and Flight move whoever casts them, Restore only changes the ground). numbers.cast_range_px is the farthest distance from Isaac.
  • effect_group: only on unit_effect (null for the other kinds), what it does to units. heal restores HP or removes statuses: the heal flag, a status of group cure or regeneration, or the variants that move HP to the caster's side (life_thread, life_shine, light_transfer). boost puts a status on allies and weaken on enemies: every status the game casts on allies helps them and every one it casts on enemies hinders them. other is the rest: no status and no healing, or a status on both sides. It is the spells page's filter, not a field of the cartridge.
  • power: with power_scaled, the power at cast is floor(floor((power + caster's magic) × quality factor) × (rank + 4) / 4); without it, power is used as it is. With the heal flag, it heals.
  • status: the status it can cause: its record (id), group, chance (at rank 0 and factor 1; multiply by the factor, by (rank + 4) / 4 and by the element factor in elements.status_chance of /data/combat.json), duration_ticks and power.
  • terrain: how it changes the ground (heat turns it into magma, harden hardens magma, freeze freezes water, dry dries swamp into sand, swamp turns quicksand into swamp, restore restores it).
  • terrain_overlay: what the spell draws over the map while its zone lives, lasts_ticks (1200, 20 s). The zone is a circle: area.radius_px, or one circle of area.per_object_radius_px for each falling object. The 8 × 8 px cells whose centre is inside the circle are painted. paints is the layer it puts and on which surface (on); image is its 32 tiles animated, each frame lasting frame_ticks (null: still). clears are the layers it removes inside its circle while it lives, even when another spell put them, before or after. That is how opposites cancel: Storm removes Fire Rain's magma, Fire Rain removes Ice Maiden's ice, and restore removes everything. It appears at once and, when the zone ends, vanishes with no animation: there is no entry or exit animation, and the frame_ticks frames loop the whole layer. It is only the picture: the terrain statuses give the effect on units. on_any_map says whether that surface exists on any map. dry paints mf04 on swamp, but no map has swamp (on_any_map: false): in the game it only removes the swamp swamp leaves.
  • flags: piercing, homing, rapid (bursts), spread (fan), lock_on (one shot per target), flying_only and walking_only limit who it affects, knockback, knockdown.
  • numbers: its family's numbers, in ticks of 1/60 s and px.

Spell families

/data/spell-families.json groups the 396 spells by the game code that runs them: the effect class and subkind in their data. The generic spell on units (direct) is split by role (heal, damage or status-only) and status group, and Prank, Joke and Trick go together as tub. Each family has page (its page on the site, /logic/families/<family>/, or /es/logic/families/<family>/ in Spanish: its types and its spells in an element × variant matrix (rows such as Level I, Rapid or King, and «Others» for the spells that fit none) when most of them fit, or as cards, each with its animation and a panel with all its numbers; /logic/families/ is the catalog of all of them), spells (the slugs of /data/spells.json), same (the fields equal in all, with their value) and differs (the ones that change). The fields are those of /data/spells.json: numbers.<name> and status.<field> are flattened, and runes is how many it has. power is only compared within a family, because each one uses it differently. note (EN/ES), when present, says when one pays off over another, only where the research supports it; it comes from reading the code, not from playing. Families of one spell have empty same and differs.

total_effect gives, per spell, what one unit receives over the whole spell, at the fixed reference in total_effect_reference: the caster has magic 30 (Isaac at level 1), the element at rank 0 (level 1–10) and a perfect stroke (quality 1, factor 1.5); the receiver has defense and magic defense 0, a neutral element and no weakness or shield; no random factor. kind is damage, heal, status or other; hp is the damage or healing in HP when there is one (else null); text (EN/ES) says how it adds up or what it does. The rules: the damage per hit is floor(floor((power + 30) × 1.5) × (0 + 4) / 4), except clouds, which hit with their raw power numbers.hits times; burn and poison add status.power every 60 ticks, floor((status.duration_ticks − 1) / 60) times, if the status lands (the unit's status pass counts the life down before each tick), and regeneration heals once more because its first one is at once; the status chance is status.chance × 1.5, at most 1. up_to marks a maximum: every shot or object lands (bursts and falling objects), or the unit stays where the spell reaches it. Normal poison only ticks while the unit moves or attacks. A dragon flies through a unit, goes on 192 px and comes back, so it hits a unit that stands still on every pass: numbers.passes_max at most, passes_typical in most spots, passes_min in the worst. An armor hits an enemy that stays within its pulse numbers.touches_max times with the power and numbers.aura_ticks_max times with status.power. A dance is numbers.hits_max hits of numbers.hit_attack (Isaac's attack at level 1, against defense 0). A whirlwind hits once per contact and only steers to units ahead, so it hits a unit again only if that unit comes back into its path. confirmed: false marks what the code read so far does not settle; since #256 no spell has it.

Each family has description (EN/ES): a sentence with no numbers on what it does, built from its data with a template per kind or variant and the effects of at least half its spells («some also…» adds up to three of the rest). What each spell does, worked out from its data and never written by hand, comes in two levels: tags, specific effects (damage, heal, protect, empower, haste, weaken, burn, poison, freeze, sleep, charm, pacify, immobilize, knock, capture, summon, movement, terrain), and types, their eight general groups (attack, heal, buff, debuff, control, summon, movement, terrain). The tags and types at the top of the file give the name (EN/ES), the rule and, for each tag, its type. The rules: damage if its total effect is damage (or switch_life); heal with heal in flags, a total effect that heals, a cure or regeneration status, or light_transfer; the status it applies, with every branch of a random status, gives the rest: a stat change up protects (defense, magic defense, evasion), strengthens (attack, magic, accuracy) or speeds up (speed, or less interval), and down slows (speed; all of it or a bubble roots) or weakens (anything else, weakness, virus, recoil, forget); burn and poison are burn and poison; sleep, freeze and charm, their own; no attacking and terror, pacify; berserk strengthens an ally and, cast on an enemy, counts as charm. knockdown and knockback are knock; capture traps, capture; kind wall, object or dragon, summon; variant transport, gather or self_cannon, movement; terrain or a terrain status, terrain. Holy Pulse applies no status of its own: it strengthens the allies and weakens the enemies. In each family, tags and types list its spells', the most common first, with spells = how many have it; spell_types gives each spell's.

Best spells

/data/best-spells.json says which spells pay off for the player, worked out from the data (evidence: static), not from playing. Each spell's effect is hp_expected: the total effect of /data/spell-families.json at the same reference, with burn and poison counted by their chance (for clouds, the chance it lands at least once over their hits). spells gives per spell its mp, chapter (the chapter in which Isaac has learned all its runes and can chain that many), stroke, reach_px (the larger of the cast range and the shot range), area_px (its radius, if any) and badges: per_mp, jump, easy and not_worth.

  • rankings.per_mp: effect per MP at least twice its family's median (vs_family), or effect at least twice the median of the other spells of its family with the same MP (vs_same_mp).
  • rankings.jumps: the next spell by MP of the same family and element, with effect ×2 or more and effect_ratio / mp_ratio ≥ 1.5.
  • rankings.not_worth: another spell of its family and element (better) costs less MP and is as good in everything; when gains_only is not empty, the dearer one only gains in those numbers.
  • rankings.easy: every spell, from the easiest stroke to the hardest.
  • By situation, only spells that deal damage: grouped (with an area, by the effect on each unit inside), scattered (no area that also hits allies; by mp_per_kill = MP × casts to take the weak enemy's HP, then by stroke, then by reach) and boss (by the effect on one unit). The weak enemy (reference.weak_enemy) has the median HP of the unnamed units in the battles of chapters 1 and 2, at their level.

The stroke score (method.stroke) is an estimate of how hard a rune is to draw by hand, not a value of the game. For each shape the rune accepts (templates of /data/runes.json, 0..63 grid = one rune width), points closer than min_step to the previous one are dropped and the score adds: the length in widths, w_sharp per corner sharper than sharp_deg degrees, w_turn per full turn (the sum of turning angles / 360°) and w_against per width the stroke goes up or to the left. A rune takes its easiest shape (runes[].template, runes[].points) and a spell adds its runes plus w_extra_rune per rune after the first. The easiest quarter of the spells gets the easy badge.

Spells with rules of their own, by variant:

variant Rule
bomb_man Puts a bomb on each target unit, of both sides. After 180 ticks each living bomb hits every unit within 15 px with the power (fire), once per unit, its carrier included
dance Isaac walks to the target at 2 px/tick and takes no damage (gives up after 180 ticks). Within 12 px he dances 300 ticks: hits on ticks 9, 19 and 30 of each 43-tick cycle, with his whole attack minus a quarter of the defense. It ends if the target dies or gets more than 34 px away
light_transfer Dark damage; Isaac recovers twice the damage dealt, up to 32,767
life_thread / life_shine Heals what the target lacks, taken in equal shares from the other allies / the enemies, never below 1 HP
holy_pulse 600 ticks: the caster's side gains 25/128 attack and the other side loses 25/128 defense inside the circle. Casting it again restarts the time
transport The caster jumps to the point if it fits (otherwise it fails); no receiver, no impact. Departure flash on casting, arrival flash at 20 ticks and position change at 30; until then it can be hit where it stood. Flight is the same jump with a longer range (250 px instead of 100)
self_cannon Isaac flies at 2 px/tick for up to 240 ticks; within 12 px, 620 wind magic damage to the target (not scaled) and 255 to Isaac
free_lift Lifts the target (not Isaac or bosses) for 64 ticks; when it drops, the power (wind) and a strong burn to enemies within 40 px
switch_life Swaps Isaac's current HP and the target's, each capped at the receiver's max HP
gather Brings each ally next to Isaac, one every 6 ticks, at a random angle, both radii plus 0–31 px away
death_gamble Picks a random living unit of either side after 282 ticks; it takes 2 × the power (fire) and others within 50 px the power
prank, trick One tub per target; after 20 ticks it takes exactly 1 HP, which counts as damage
mine Each mine lasts 920 ticks and goes off when a walking enemy comes within 14 px: the power (earth) and a knock-down to enemies within 14 px. A new mine disarms the side's earlier ones
capture_trap Catches the first regular enemy within trigger_radius_px. Under capture_hp_below the capture always succeeds; intended_chance_* are not used. A new trap cancels the side's earlier one
elemental_wall Posts have no HP; each post hurts a unit touching it at most once every 120 ticks

Spell scenes

/data/spell-scenes.json (also .md) maps every spell to graphics, sounds and actual bosses, with 90 unit appearances and 141 statuses. Each spell page offers caster, receiver, four facings, distance, stroke quality, selected outcome, replay and sound. Receiver affiliation follows targets; example outcomes/status application are user choices, not battle rolls. Transport and Flight have no receiver or outcome: their scene moves the caster to the destination.

  • assets[id]: PNG, CAN/NCER/NANR and exact palette; cells holds [x,y,w,h] sheet rectangles and origins their anchor-relative positions. Empty frames still consume their time.
  • sequences[].frames: [cell,ticks,dx,dy,rotation degrees,scaleX,scaleY]. Single-frame animations are retained. mode: 1 once,2 loop,3 ping-pong once,4 ping-pong loop; loop_start is the loop's starting frame. ticks and seconds retain original durations.
  • units[].actions[action][facing]: sequence, own and fallback. Monsters without chant/cast actions use idle. launch_px positions the shot; boss_spells identifies actual battle use.
  • spells[].graphics: role, atlas, sequences, palette, evidence and confidence. times_seconds converts spell time fields; distances remain px. Projectile travel is distance/speed, rounded up to a tick.
  • sounds: audio, anchor (chant,stroke,rune,cast,release,shot,hit,status,death,vanish,capture,capture_close), offset seconds/ticks, condition and confidence. timing_confidence separates sound dispatch from example phase placement. Rune/stroke repeat per rune; hit per shot. Quality <0.85 uses KAKUNIN/INVOKE; ≥0.85 uses MENU_MAGIC/INVOKE2. Sound requires interaction.
  • statuses[].overlay, popup, sounds and duration_seconds describe each status. /data/statuses.json also includes this as visual.
  • Knockdown requires flag 0x800: 40 ticks grounded and 10 getting up. Push requires 0x400 and decelerates over 30 ticks. Defeat plays die, waits 30 ticks, uses puff 272 for monsters, then removes after 80. Resolved capture uses capture_timing: success closes at tick 2 and removes the unit at tick 33; failure closes/releases at tick 60. Book sequences 199/200 determine the end, independently of armed trap lifetime.

Walls (class 1) create ground structures, without a receiver or hit outcome. structure_timing preserves the 5 ticks between posts; targeting distinguishes line and circle. Elemental statuses apply on contact, not placement. The viewer allows adjusting line length.

Projectile orientation is in graphics[].rotation: the original artwork points upward and the ROM adds 90° to its travel angle. Trails inherit that angle; dark/light spin 16.875°/tick. The preview fixes the random initial phase to 0. The manifest has a revision to prevent browser reuse of earlier exports.

Spatial composition is illustrative, not a complete battle simulation. preview declares drawing speed (0.8 s/rune) and conventions; confidence/evidence distinguish decoded behavior from probable assignments. Follow-up extraccion/investigacion/visuals/SCENE_DISPATCH.md confirms chant/drawing sound dispatch, byte-7 status icons and Fallthing sound tables in code. Zero entries skip that sound. Other families have independent calls; fall and object_hit repeat per object, using a 20-tick descent as a preview convention.

visual_behavior distinguishes shared feedback (Holy Pulse: icon 11; Life Thread/Life Shine: icon 7), actor motion (Self Cannon: unit_motion, 2 px/tick, 12 px collision gap) and conditional statuses (Betrayal/Mutiny/Uprising: choices). dance (the six dances, 361–366) marks Isaac as the dancer: he walks to the receiver at approach_px_per_tick (2), dances beside it for numbers.duration_ticks (300; in the spell's record, as in /data/spells.json) once the gap is start_gap_px (12) or less, and hits on hit_ticks (9, 19, 30) of each cycle_ticks (43) cycle: 21 hits. The receiver does not dance. The element aura starts where Isaac stands (aura_origin); aura_pending says what is not decoded yet. caster_teleport (Transport and Flight) marks the caster's jump: departure flash on casting, arrival flash at arrival_effect_ticks (20) and position change at move_ticks (30), with no receiver or impact. Their probabilities are conditional on success; the viewer selects a branch without simulating a roll. own_graphic_confirmed describes dispatch in the original report, not whether shared feedback exists.

Blast/dragon composition and knockdown still require in-game verification. Phase synchronization, Missile's absolute phase placement, life donors and Self Cannon's self-damage are not fully reconstructed. Follow-up extraccion/investigacion/visuals/SCENE_RENDERING.md adds dragon_assembly (14 indexed parts, emergence 23+index and scales), explosion_assembly (main, ring and inward charging particles) and lock_timing (ticks 8/16/24). Dragon routing and preliminary Explosion positions remain illustrative. How each effect ends is in extraccion/investigacion/visuals/SCENE_ENDINGS.md: rain_assembly (the 6 spells with MagicEffectRain) gives 20 clouds, 10 drops with their alpha cycle (drop_alpha) and 10 splashes, and removed_after_hit_ticks (63) is when the owner deletes itself and takes everything with it. For the other blast and falling-object families, when the effect is removed is not decoded; the scene removes it 120 ticks after the impact. The 27 terrain spells carry terrain_scene:

  • map and origin say on which game map and spot the scene stands, so the terrain the spell acts on shows;
  • zone says when the zone is born: on the impact (hit), at each falling object (object_hits) or when the area appears (release);
  • radius_px and lasts_ticks (1200) give its circle and its duration;
  • prior, for the spells that only remove, is the earlier overlay the scene paints so you can see what they remove.

terrain holds each material's rule, the layer the game picks, the neighbour tile tables (tiles), each layer's sheet with its ticks per frame and the NHIT grid of each map used (details in extraccion/investigacion/terrain_morph/STATIC_REVIEW.md). sounds[].duration_ticks stops chanting at cast and drawing at each stroke end; *-sustain clips use ROM samples/instruments with a 3 s preview hold. Resume continues held notes with their current offset and remaining duration. None of these findings is emulator-validated.

Aiming a spell

/data/aiming.json says what the battle shows between the end of the chant and the cast, by the spell's targeting (see "Spells and runes"). It is read from the code (evidence: static), not measured in an emulator; confidence says how sure each point is. Positions are px of the bottom screen (256 × 192), times are ticks of 1/60 s and angles are degrees with 0 = right and 90 = down (y grows downward).

  • Modes (modes). Each targeting has its icon (icon) and its way to confirm (confirm). arrow says whether it uses the arrow; range_graphics_3d, whether it draws something on the ground (circles or shade). direction draws nothing on the ground: only the arrow and the icon.
  • Arrow (arrow, direction only: Fireball and the other shots). The picture image (32 × 56) points up. Its point pivot_px (15, 36), the centre of the ring, goes on Isaac's point (anchor: caster_point, his feet) and the picture turns around that point. The tip is tip_px (36) px ahead of Isaac and the handle tail_px (15) px behind, so the arrow comes out from under him. Drawing angle = atan2(dy, dx) in degrees + rotation_offset_deg (90), where (dx, dy) goes from Isaac's point to the stylus, turning clockwise on screen: the arrow always points at the stylus. The turn is free (turn_steps 4096 per turn), with no snapping to 4 or 8 directions. It does not stretch or scale with distance (scale 1) and is opaque (alpha 1). It is drawn under Isaac (layer: under_caster). Colours: the picture's own, the same for every element.
  • When (direction_flow). The arrow appears as soon as aiming starts, pointing up (rest.end_offset_px (0, −10): −90°). Once the stylus touches the screen (anywhere), every tick the arrow points at the stylus and Isaac turns toward it in 4 directions (right, down, left, up, split at 45°). Lifting the stylus casts the spell from Isaac toward the stylus's last point, and the arrow and the icon disappear. There is no distance limit (range_limit: null): the shot travels its shot_range_px from /data/spells.json. Before touching, pressing the chant button (L, or R with the left-handed option) cancels: SE_CANCEL (effect 4) plays and nothing is cast; once touched, it can no longer be cancelled.
  • Icon (icon). While aiming, the mode's icon (32 × 32, point at its centre) stays put on the screen at at_px: (20, 172), bottom left, or (236, 172) with the left-handed option, above the field. If the arrow points into that corner (between the angles of move_when_arrow_deg: 90–180 right-handed, 0–90 left-handed) it jumps to moved_at_px (236, 20 or 20, 20) so the hand does not cover it.
  • Circle (circle_flow, targeting: circle). Outside the reach (cast_range_px from Isaac) the field is shaded with shade_outside_range (#525252, alpha 20/31). A touch outside the reach does not count; one inside sets the centre. Dragging picks the radius, from radius.min_px (5) up to the spell's radius_px; releasing casts with that centre and radius. The area is coloured by targets (area_colour_by_targets: red enemies, blue allies, yellow all; area_colour_other for the spells it lists) and with area_colour_at_max_radius once it reaches the largest radius. Shape (area): a 24-sided disc on the chosen centre, of the dragged radius, alpha 24/31. While the radius is below the largest it has four small triangles (handles) 4 px outside the edge, 5 px long with an 8 px base, pointing out and turned so that one points at the stylus; at the largest radius they go away. The shade outside the reach only shows before the touch.
  • Instant (instant_flow). Any touch casts the spell; the chant button cancels.
  • Line (line_flow, walls such as Fire Wall). The first touch has to land strictly inside the reach; it sets the start. A touch outside does nothing. From then on the end follows the stylus, at most the spell's max_length_px long. If the stylus leaves the reach, the end stops where the line from the start to the stylus crosses the reach's edge: the whole line stays inside. Isaac turns toward the stylus only before the start is set. Releasing casts from the start to the end (the same call as the arrow). It draws (graphics, colour #e6b552, alpha 28/31) an 8 px radius disc on each end and an 8 px wide band between them, sticking out 4 px past each end. The shade outside the reach always shows.
  • Point (point_flow) and fixed circle (fixed_circle_flow) are the same code. The first touch has to land strictly inside the reach. Then the point follows the stylus and, if the stylus leaves the reach, stays on the edge in the stylus's direction. Isaac turns toward the stylus. Releasing casts at the point. point draws the reticle (reticle, 16 × 16 px at scale 1.5: 24 × 24 on screen, centred on the point, above the field) and no area. fixed_circle draws at the point a 24-sided disc of the spell's radius (radius_px, trigger_radius_px or ring_radius_px by class), always #ffa5a5 alpha 24/31 and without triangles, because the code draws it at the largest radius. It would only show the reticle with radius 0, which no spell has. The cast call also gets a fixed 20 px radius (cast_radius_arg_px); what it does with it is unknown. The shade outside the reach always shows.
  • Unit (unit_flow). Any touch starts. A cursor follows the stylus; if the stylus leaves the reach, the cursor stays on the edge. Isaac turns toward the stylus. Every tick the nearest unit to the cursor is picked among those targets allows whose touch box holds the cursor. Box: in x, the unit's x ± (touch_box_by_unit[slug][0] + 4 px); in y, from the unit's y − ([1] + 4 px) to its y + (the unit's radius_px + 4 px). It draws a three-line marker (marker.lines_px, px from its point) in red #ff5252 on the picked unit, also for ally spells, or in yellow #ffff52 on the cursor when there is none. Releasing casts on that unit. With no unit it goes back to waiting for a touch, and the chant button cancels again. The shade outside the reach always shows.
  • cross (a cross) is loaded for every mode but none shows it. numbers are digits from the same file that aiming does not use: they are the damage numbers' digits (see "Damage numbers").

Status effects

/data/statuses.json has the 141 status records; a spell's status.id points to one. duration_ticks is how long it lasts (0: it acts once) and caused_by, the spells that cause it. The strength of the ones that hurt or heal is the spell's status.power. key says what it does:

key Effect
stats Applies changes while it lasts
virus changes; units within spread_px of a first victim catch it for the time it has left
berserk changes; attacks its target and ignores orders. Cancels charm
burn, strong_burn Fire damage equal to the strength every every_ticks, the first after every_ticks. It does not work in fords; water damage puts it out; it removes freeze and bubble. strong_burn replaces burn
poison Dark damage every every_ticks, only while the unit moves or attacks
vile_poison Dark damage every every_ticks
regeneration Heals the strength every every_ticks, the first at once
pulse element magic damage equal to the strength at once and after every_ticks
wall_contact On touching a post: element damage (heal: healing) equal to the wall's strength, at most once every 120 ticks per post
freeze, long_freeze Cannot attack or move; fire damage ends it
sleep, deep_sleep Cannot attack or move; any damage wakes it
bubble Cannot attack or move until it is hit twice
charm Drops its orders and fights its own side. Cancels berserk
terror Runs away from the unit that threatens it
forget Stands still
no_attack, no_move, no_damage Cannot attack / cannot move / takes no damage
invincible Takes no damage, cannot attack or move, and cannot be pushed
hidden Fades out over 127 ticks
recoil Each hit it lands costs it share × the damage it deals
weakness, halving ×2 / ×½ element magic damage; one cancels the other
cure Removes removes (all: every status and stat change; stats: stat changes, curses and auras) and applies changes if it has them
terrain Marks the ground with change
random Applies one of options by its chance
none No effect

Each item of changes has a stat (attack, defense, speed, magic, magic_defense, interval = time between attacks, accuracy, evasion) and a mode: percent → stat × (1 + amount); flat → stat + amount; gap → stat + (100 − stat) × amount.

  • Statuses do not stack: applying one again restarts its time. Stat changes, auras and curses do stack, and they multiply.
  • Every frame stats are rebuilt from the unit's own, applying its statuses from newest to oldest, then clamped: none below 0; attack, defense, speed, magic and interval at most twice the unit's own (interval at least 1); magic defense at most 99; accuracy and evasion at most 150 over the unit's own.
  • Isaac and bosses are immune to berserk, charm, terror and forget.

Combat

Battle logic runs at 60 ticks per second. /data/combat.json has the constants from the code; each unit's own are in /data/units/<slug>.json.

Normal attack. gap = distance between centres − both units' radius_px.

  • It starts a swing at gap < attack_reach_px and keeps it while gap < attack_reach_px + attack.reach_pad_px. With attack_reach_px 0 it never attacks; when chasing it stops at attack.stop_gap_px.
  • Rhythm: wait attack_interval_ticks, play the attack animation; the hit or the shot comes out attack_strike_ticks after it starts; when the animation ends, wait again.
  • Melee hit: random(0..100) + accuracy − evasion ≥ 100. Ranged: random(0..99) < (accuracy − evasion) mod 256 (a negative difference always hits).
  • Damage: d = floor(attack / 2) − floor(defense / 4); if d ≤ 0, nothing. With an attack_element: ×½ against the same element, ×1.5 against the opposite one (opposite). Shots do ×1.5 to flying units. Finally floor(d × (128 + r) / 128), with r a random integer from −10 to 10.

Shots (the unit's projectile_type, projectiles in /data/combat.json): they leave from shot_start_px for the facing, straight towards where the target's centre was, at speed_px_per_tick, without following it. They travel at most attack_reach_px + reach_pad_px. They hit the nearest opposing unit within shot_hit_radius_px + its radius and do not go through. High obstacles stop them; water and pits do not. on_hit: stick stays stick_ticks on the target, vanish disappears. On a miss they bounce back and fade in shot_fade_ticks.

Sight. Every search_every_ticks a unit looks for enemies within max(sight_px, search_px) and chases while gap < sight_px. Mission scripts often change sight_px.

Spell damage. With the power at cast P (see "Spells and runes"), on each target:

  1. Element: no element, P; same element as the target, −floor(P / 8); opposite, floor(3P / 2).
  2. If above 0, × (100 − magic defense) / 100 (magic defense ≤ 99); if 0 or less, halve it (the same element heals about P/16).
  3. × (128 + r) / 128, with r random from −10 to 10.
  4. If the target has more weakness than halving statuses for that element, ×2; with more halving, ×½. Spells with heal add P with no element, defense or randomness.

Element against element (elements in /data/combat.json). The unit's element (null: no element) is compared with the hit's. elements.spell_table is step 1 above, by the spell's element (rows) and the target's (columns): -0.125 is the same element, which heals; 1.5, the opposite one; 1, the rest, and always when either side has no element. A fire spell heals a fire creature instead of hurting it, whatever its magic defense. No unit is immune to an element's damage.

  • Normal attacks (elements.attack): ×½ against the same element, ×1.5 against the opposite one.
  • Chance of the spell's status (elements.status_chance): ×½ if the target has the spell's element, ×1.5 if it has the opposite one. The spell's element is what counts, not the status's. No status has an element exception: a fire creature can be burned and a water one can be frozen, at half the chance when the spell is of its element.
  • Damage dealt by statuses (elements.status_damage): burn is fire and poison is dark, and both go through steps 1 to 4. On a fire creature, each burn tick heals it instead (1 or 2 HP at power 10); on a water one it does ×1.5. Freeze carries water but deals no damage.

Bosses. Only each battle's boss unit casts spells (casts_spells in its file), when its script tells it to; there is no range check. It has its own MP (its mp stat), pays each rune at rank 0, and its power is power + magic, with no quality or rank.

Isaac's element levels. They start at growth.start. They only change on a battle's results screen. With old the level before the battle, c the spells of that element cast in it (one per spell, its last rune's element; cancelled chants don't count) and fav the element cast strictly the most (none on a tie):

  • old ≤ low_band_max: +low_band_many_gain if c ≥ many_casts, +1 if 1 ≤ c < many_casts; +1 more if it is fav.
  • old ≤ mid_band_max: +1 if c ≥ many_casts; +1 more if it is fav.
  • Otherwise: +1 only if it is fav, and then the opposite element loses 1 if its level was decay_min or more.
  • Then each level is clamped to level_min–level_max. The level only changes the rank (floor((level − 1) / 10)), and the rank only changes the runes' MP, the power (×(rank + 4) / 4) and the status chance (×(rank + 4) / 4).

HP bar

hp_bar in /data/combat.json is the HP bar the game draws over every unit on the field, all the time. It is made of solid rectangles, with no picture (drawn_by_site: true). Sizes are in px from the unit's point (x, y in /data/maps.json) raised by its head height, head_px in /data/units/<slug>.json: a rectangle with left_px and top_px spans width_px × height_px from (x + left_px, y − head_px + top_px).

  1. frame: the frame, in frame.color, under everything. What the bar does not cover shows that colour.
  2. bar: the HP, floor(width_px × HP / max HP) px wide, with the effective max HP (after change_max_hp). Its colour, color_by_side: player for Isaac's side, enemy for enemies, ally for allies and characters on no side.
  3. change: for change.ticks ticks after the HP changes, another bar at the same place and height sits between the frame and bar. Only the last change counts.
    • After damage, bar shows the new HP and the one under it the HP before, in damage_color.enemy on enemies and damage_color.other on the rest.
    • After healing, bar shows the HP before and the one under it the new HP, in heal_color.

The bar does not rise with units lifted off the ground. A defeated unit keeps it through its die animation and the 30-tick wait, and loses it when the puff it vanishes in starts (hidden_from: vanish).

Enemy marker

/data/enemy-marker.json is the small letter the game draws just left of every enemy's HP bar. It marks the side: only enemies carry it; Isaac, his squads and allies carry none (carried_by).

  • Picture. letters: "E" with the game in English, French or Spanish and "F" in German (languages; the game takes the console's language and any other counts as English). Each is 8 × 8 px (size_px), pink and white, not animated.
  • Position. From the unit's point (x, y in /data/maps.json) raised by its head height (head_px): the picture's pivot_px goes at (x + offset_px[0], y − head_px + offset_px[1]), that is (x − 14, y − head_px − 6). It sits gap_to_hp_bar_frame_px (5 px) left of the HP bar's frame. Scale 1, opaque, in front of its unit. Example (example): on a Misty Mush (head_px 14) the picture spans (x − 18, y − 24) to (x − 11, y − 17).
  • When it shows. While the enemy is on the field and on screen, all the time; it does not depend on selection or damage. Every unit has an appear counter from 0 to fade_max (127): it enters the field at 63 and goes up 1 per tick to 127; while the unit blinks as it appears, the letter stays steady (blinks: false). An enemy with the hidden status goes down 1 per tick and the letter goes when it reaches 0. When it dies, the letter stays during the vanishing puff, even though the HP bar is no longer drawn, and goes when the unit is removed.

Mana crystals

Mana crystals (MP pillars) stand on the field in many battles. Each battle in /data/maps.json has crystals: the position x, y in map px and owner, who holds it when the battle starts (player, enemy or neutral). The rules are in mana_crystals of /data/combat.json, in px and ticks of 1/60 s:

  • It is a solid circle of radius_px that stops walking and flying units and shots. It has no HP: it only changes hands.
  • Capture. Every tick, each unit that can capture (can_capture_crystals in /data/units/<slug>.json), alive and within capture_radius_px, adds gauge_per_unit_per_tick to the gauge. When nobody adds, the gauge drops gauge_decay_per_tick. Over capture_gauge.neutral (nobody's crystal) or capture_gauge.owned (a held one), it goes to the side taking it, and the gauge goes back to 0.
  • Blocking. Nobody takes it while a unit of its owner is within presence_radius_px. A crystal of nobody does not move on either while units of two sides are that close.
  • Sides. sides_that_capture: in the story only Isaac and his squads take crystals; the enemies never take or defend them.
  • Healing. Its owner's units within heal.radius_px recover heal.hp HP every heal.every_ticks while they are hurt.
  • MP. Every mp_regen.every_ticks, Isaac's MP rises by mp_regen.base_mp, plus mp_regen.mp_per_crystal for each crystal he holds, plus 1 for each earlier rise in a row without chanting, up to mp_regen.streak_max. Without crystals he still recovers: the base and the streak. After a chant the first rise comes after mp_regen.chant_wait_ticks; with crystals, after chant_wait_ticks − chant_wait_cut_ticks_per_crystal × crystals, at least chant_wait_min_ticks.
  • Picture. pictures has one picture per owner, of picture_size. Its top-left corner goes at the crystal's point plus picture_offset.
  • Capture bar. capture_bar describes the bar the game draws with rectangles, not with a picture. drawn_by_site is true because the site redraws it from these rules:
    • It is height_px tall and sits above_px above the crystal's point.
    • The background, in colors.background, starts at background.<neutral|owned>.left_px and is width_px wide.
    • Over it, from the same edge and in colors.remaining, goes what is left: (capture_gauge − gauge) / remaining_divisor px, with the gauge of the last push.
    • Each push shows it for fade_ticks ticks, with opacity ticks left / 2 out of alpha_max.
  • Missions. The objective "Purify the Mana Crystals!" is won when they are all the player's (all_crystals in conditions).

Battle camera

/data/camera.json has the constants from the code, in px of the map and px per frame (60 a second). The view is view_px (256 × 192): the bottom screen, the touch screen, where the game puts the main graphics engine (3D and the map's backgrounds). The top screen is covered by "Top screen" below.

  • Position. It is the view's top-left corner. It goes from 0 to each map's view_x_max_px and view_y_max_px in maps (the map's size minus the view); every move is clamped to those limits. They are the world bounds (world_bounds.always): there is nothing outside the map, so the camera stops on each axis when it reaches the edge and keeps going on the other axis if it still can. The game has a flag to lift them, but only the camera's init writes it, with 0; nothing turns it on. Units and effects are drawn at position on the map − camera position, in whole px. A unit more than 40 px outside the view (unit_draw_margin_px) is not drawn.
  • It follows nobody. The camera does not move by itself when Isaac walks, and it does not follow spells, targets or captured creatures. Only the four cases below move it, with no smoothing or dead zone: constant speed and an exact arrival.
  • At the start of a battle it centres Isaac: it puts him at battle_start_isaac_at_px (128, 96) of the screen, clamped to the limits.
  • D-pad. While held, the view moves dpad.px_per_frame (7) that way, or dpad.diagonal_px_per_frame_per_axis (4.9) per axis on a diagonal. With the left-handed option, A, Y, B and X act as right, left, down and up. There is no automatic return: the view stays where the player leaves it. It does not move while a spell is being prepared or chanted, or when the mission turns the D-pad off.
  • Chant. When L is held (R with the left-handed option) and Isaac is outside the screen rectangle chant_recentre.isaac_rect_px (x 20–236, y 20–172), the chant waits and the view moves toward him at chant_recentre.px_per_frame (10) in a straight line, until he is at target_px (128, 96). With less than jump_below_px (10) to go, it jumps there. It stops early if Isaac enters the rectangle or a map edge stops the view. During the chant it keeps correcting the same way.
  • Missions. A script reads the view's position and moves it in a straight line to (x, y) at speed × script_pan.px_per_frame_per_speed_unit (speed / 4) px per frame; missions almost always use 24, that is 6 px per frame. It reports when it arrives. It does not go back to Isaac by itself. If the target is past the limits, the view stops at the edge.
  • Shake. Big Drop moves the view by shake.big_drop: ±2 px horizontally, switching side every 2 frames, for 30 frames, then back to 0. The earthquake effect (earthquake_unused, three phases) is in the code, but no spell uses it. The shake does not change the camera position, only what is drawn.
  • Top screen (top_screen). It depends on the chant state. In states 0, 1 and 2 (no chant) it shows the minimap, which places four marks from the view's position. While drawing (states 7 and 8) it shows the rune list: a rune_grid of 6 columns, one per element, by 3 rows, one per rune level, next to a bar of Isaac's MP. When the chant gives a spell (state 9) it shows that spell: its number, name and description. It goes back to the minimap as soon as the state is 0, 1 or 2 again. That unlearned runes show as "?" was seen in the game, not traced in the code.
  • Backdrops. Every map layer gets the view's position. BACK (the map) and ANIM use it as it is. NORM multiplies it by speed_factor (PARM / 256): with mode 3 or 4 it is a backdrop behind the map (sky, sea, lava) that moves at that fraction; with mode 1 it goes in front. On a map with ANIM, the NORM layer is not shown (shown: false). Reading the code did not confirm whether a backdrop repeats when it reaches its edge.

Damage numbers

/data/damage-numbers.json says how the number that appears over a unit when its HP changes is drawn. It is read from the code (evidence: static); the colour, the place, the duration and the hundreds slip were also checked in the emulator with Fireball in the first battle. confidence says how sure each point is. Positions are screen px (y grows downward) and times are ticks of 1/60 s.

  • When (shown_when: hp_changes). Every time a unit's HP changes, from a spell, a normal attack, a shot, a status, the terrain or a crystal's healing (sources), on any side (Isaac, his squads and the enemies alike). It shows the absolute value of the change (value.shown), with no sign, up to value.max (9999). It is the damage worked out, not the HP the unit had left: a hit of 347 on a unit with 20 HP shows 347. A change of 0 shows nothing, and a miss does not change HP, so it shows nothing either.
  • A slip of the game at 1000 and above (value.drops_zero_hundreds). The digits are picked like this (value.digits_drawn): the thousands if the value is over 999; the hundreds only if the rest (value mod 1000) is over 99; the tens if the rest (mod 100) is over 9 or a digit was already drawn; and the units. The hundreds step does not check for a thousands digit, so a 0 in the hundreds is lost: 1050 shows "150", 1005 "105" and 2000 "200". To draw it as the DS does, copy that slip.
  • Digits (font). Only the digits 0–9, in 8 × 8 px cells with their point at (4, 4), the centre. They are the same in the four languages. The pictures in kinds.<kind>.image are 80 × 8 strips with digit d at x = 8 × d.
  • Colour (kinds). It depends only on the sign, not on the side or the element: damage (HP lost) is pink-red #de1852 with a white outline; healing (HP gained) is green #18bd73 with a white outline. Damage to Isaac or an ally looks the same as damage to an enemy.
  • Where (place). The number's centre goes on the unit's point (its feet) plus that unit's radius_px downward (/data/units/<slug>.json). It is worked out every tick, so it follows the unit if it moves. It is not drawn while the unit is not drawn (more than 40 px outside the view).
  • How the digits are laid out (layout). With n digits, the centre of digit k (from 0, left to right) is at x = −(3n − 3) + 6k from the number's centre, and y = 0. They are 6 px apart (advance_px) although each cell is 8 wide, so the white outlines overlap: each digit is drawn over the one before.
  • Motion (timing). None: it does not rise, bounce, scale or fade. It appears whole, stays lasts_ticks (40, two thirds of a second) and disappears at once. If the hit kills the unit (timing.killing_blow), the countdown stops while it dies: the number stays until the unit vanishes in its cloud (about 190 ticks for the first battle's Misty Mush).
  • Several at once (stacking). Each unit has one number. If its HP changes again before it goes, the new value replaces it and the 40 ticks start over; numbers are not added up or stacked. Numbers of different units are independent.
  • Layer (layer). It is sorted with the rest of the field as if it were 20 px lower than the unit: it is in front of the unit and of whatever is less than 20 px below it.
  • Sound. Damage (not healing) plays damage_sound (SE_UNIT_DAMAGE, effect 50).
  • Example (example). Fireball takes 347 from a Misty Mush (radius_px 6): three pink digits with their centres at (−6, 6), (0, 6) and (6, 6) from the mushroom's feet, for 40 ticks. example.image is that number already put together (20 × 8), with its centre at point_px (10, 4).

Chant screen

/data/chant-screen.json has the chant screen's graphics, read from the code (evidence: static), not measured in an emulator. Positions are screen px (256 × 192), times are ticks of 1/60 s, and alphas go from 0 (clear) to 1 (opaque; the DS uses 0 to 31).

  • When. The circle and the dimming layer appear when drawing starts and vanish at once when the drawing ends, together with the strokes. They do not follow the camera: they stay put on the bottom screen.
  • Dimming layer (dim_layer). A rectangle over the whole screen, colour colour (#525252), alpha-blended over the field. Its alpha starts at alpha_from (10/31) and rises 1/31 every step_every_ticks (2) ticks up to alpha_to (20/31): it gets there in 20 ticks. This is what tints the field; the circle does not tint it.
  • Circle (circle). The picture image (256 × 192, its own orange palette) goes with its point pivot_px (128, 79) on centre_px (128, 96); the drawing fills art_box_px of the picture, so its centre lands on (128, 103) of the screen. It is drawn opaque (alpha 1) above the dimming layer. When it appears, for appear.ticks (30) it grows from almost 0 by scale_per_tick (0.0332) per tick and turns turns_per_tick (1/30 of a turn per tick): one full turn. On tick 31 it is at scale 1 and angle 0, and it stays still. The direction of the turn is not confirmed. fade_unused is a spinning fade-out that is in the code but the game does not use.
  • Stroke (stroke). Each stroke is a ribbon width_px (8) wide around the stylus points (one per tick, up to max_points), with round caps reaching cap_px (4) past the first and last point. It is painted with the texture texture (32 × 32: rim yellow edge, core white centre) shrunk to the ribbon's width: the texture's left half is the start cap, its right half the end cap, and the middle columns stretch along the stroke. The texture colour is multiplied by the stroke colour (per channel, in 0..31: (texture + 1) × (stroke + 1) / 32 − 1) and alpha-blended.
  • Stroke colour. While drawing it is white and opaque: the texture shows as it is. When a stroke is recognised as a rune, over rune.ticks (11) ticks each channel drops by step31 per tick down to its element's colour (by_element, in 0..31) and the alpha drops from 31 to 20 (alpha_to); it stays like that. A stroke that is no rune (not_a_rune) stays white and loses 1/31 of alpha per tick down to 1/31 in 30 ticks.
  • Rune list (rune_list, top screen, while drawing). Over the background background, which already has a "?" in every cell and the MP bar's track, go the six element icons (elements) and one 32 × 32 picture per learned rune in its cell (cells: column = element, row = level). A rune not learned draws nothing, so the "?" shows. A learned one Isaac cannot pay with his current MP shows no_mp (an X) instead of the rune. preview is the list with all 18.
  • Numbers. Each rune that can be paid carries its MP cost, cost.digits (2) digits at cost.offset_px (18, 18) from its cell's corner; the cost is mp_per_rune of /data/runes.json for the chant's next rune. Isaac's current MP goes with 3 digits at mp.at_px (217, 45). The font of those digits is not in this data.
  • MP bars (bars). Two 64 × 64 pictures (grey and yellow) with their point pivot_px (29, 64) on at_px (224, 155): they grow upward and, full, are full_height_px (88) tall. Both follow Isaac's MP; which one is in front is not confirmed.
  • Sparkle (sparkle). A strip of 32 × 32 frames with their length in ticks, placed over the cell of the rune just drawn.

Battle end

/data/battle-end.json says what the DS shows from the moment a battle is decided to the end of the results screen. It is read from the code (evidence: static), not measured in an emulator; confidence says how sure each point is. Screen px (256 × 192 per screen), ticks of 1/60 s.

  • Outcomes (outcomes). A mission's trigger ends the battle with one of four actions (action_id, the action number of the mission scripts). Each one sets the word the field shows (word), the song (song) and whether the results screen follows (results_screen). GameClearStart (3) is a win: WIN and song 11. GameOverStart (2) is a defeat: LOSE, or TIME OVER (word_when_time_limit) when the trigger that fired was the time limit. GameLostClearStart (5) shows LOSE but counts as a win. GameEvenStart (6) is a draw: DRAW. All but the win play song 12.
  • The battle stops (battle). From that tick no unit moves, thinks or attacks and no trigger runs; the units are still drawn. There is no victory pose. A chant in progress is cancelled and its sound (stops_sound, SE_MAGIC_EISHO) stops. The battle waits settle_ticks (30) and until the effects in flight are over.
  • Banner (banner). Then the song starts and the banner runs, on the bottom screen, fixed to the screen at at_px (128, 96); each picture of words (one per language, from the console's language: French, German, Spanish, any other one English) goes with its pivot_px on that point.
    1. For ticks.circle_only (31) only the chant's magic circle and dim layer show, entering as when Isaac chants (/data/chant-screen.json, circle and dim_layer). The word is not drawn yet.
    2. The word appears, opaque, for ticks.shown (31) and stays ticks.held (61) more.
    3. Then the word goes at once (the code means to fade it, but its alpha starts at 0; seen in the emulator), and for ticks.circle_leaving (31) the circle leaves (circle.exit): it turns 24° per tick and its alpha drops 1/31 per tick.
    4. At ticks.total (154) the circle and the dim layer go at once.
  • Then. With song 12 the battle also waits for that song to end (waits_while_song_plays; it plays once, 4.287 s), so a defeat lasts longer than a win. Last, both screens fade to black (fade_out, 30 ticks). After a win comes the results screen; after a defeat or a draw, Continue (section "Game rules").
  • START (banner.start). Before play the battle shows START the same way, unless the mission calls native 46 first. The first battle (qc01_m001) calls it, so it starts with no banner; its New Game+ version shows START.

Results screen

results. It only follows a win. Song results.song (10, SEQ_QUESTCL) starts with it and plays through all of it. It has up to four phases in the order of results.phases; a phase with nothing to show is skipped. Each phase fades in from black and out to black (30 ticks). After the last one comes the chapter title, if one is pending, or the world map.

Experience (results.exp), always shown. previews has both screens put together from these fields, in English and Spanish, without the text. The game's menu font is not decoded: texts are drawn with fixed 7-px characters from their point, in a font of your choice.

  • Top screen: top.background (it already has Isaac's portrait, the LV box and three dark rows). Isaac: the LV label at isaac.lv_label_at_px (the _fr_es one in French and Spanish), his level as %2d at level_text_at_px, the EXP bar at exp_bar_at_px (bar_frame_64, then bar_back_64, then bar_fill stretched to exp_bar_fill_px (62) × EXP / EXP to the next level). Each squad of the team, in roster order, at most 3: its unit's icon at squads.icon_at_px, the plate, its name and level and its bar at the offsets of squads from the icon (bar 46 px, bar_frame_48 and bar_back_48). The element levels are not on the top screen.
  • Bottom screen, page 1 (bottom.pages 1): the four bars at their at_px, and the texts: clear_time the battle time in seconds (at most its time limit) as hh:mm:ss; defeated the kills; survival the squad monsters alive × 10000 / those deployed, as %3d.%02d%%, or ---.--% with no squads; exp_pool the EXP still to give, which counts down.
  • Bottom screen, page 2 (bottom.pages 2, 192 px further down): the element radar. The six element icons at radar.icons, and two translucent hexagons centred on radar.centre_px, one vertex per element (top light, upper right fire, lower right earth, bottom dark, lower left water, upper left wind). Red: radius floor((level + 10) × 68 / 50); green: floor(rank × 10 × 68 / 50), rank floor((level − 1) / 10) + 1. The green one is drawn in front of the red one (seen in the emulator); their alpha is not decoded.
  • Timeline (waits_ticks 30, 60, 30, 60):
    1. Wait 30 ticks.
    2. Count-up, every tick: step = max(1, floor(pool / 80)), pool − step, and Isaac and every squad each get step. Sound count_step (18, SE_KASAN) every tick; each level gained plays level_up (19, SE_LEVELUP) and shows level_up ("UP!", its frames move it by dy_px) at Isaac's level_up_at_px or on the squad's icon. Bars, levels and exp_pool change every tick. At 0 the Next button shows and the screen waits.
    3. Wait 60 ticks, then page 2 scrolls up into view in 64 ticks (3 px per tick).
    4. Wait 30 ticks, then the element levels go from the old to the new ones (growth rule in "Game rules") in 30 ticks, the hexagons redrawn each tick; element_step (20, SE_ZOKUSEI) plays at 0, 10 and 20 ticks. The Next button shows and the screen waits.
    5. Wait 60 ticks and fade out.
  • Input. The Next button (next_button, by language) is at (232, 168), or (24, 168) with the left-handed option; it fires when the stylus is lifted within 20 px of that point. A new press of A, B, X, Y or the D-pad also counts as Next. During the count-up any touch or one of those buttons gives the rest at once (each member gets ceil(pool / members), a quirk of the game) and skips that Next. A touch during the scroll jumps to page 2.

Items and monsters (results.getting): one screen per item found, then one per monster captured, in the order the battle got them. Bottom screen: the background (item or monster), the title text (70 "You found an item!" or 75 "You captured a monster!", centred at x 128, y 24), the found row at found_row_at_px (its background button_images 11 for items, 10 for monsters; icon, name 16 px to the right, LV and level), and the two option buttons buttons.options (104 × 48, texts 71/72 or 76/77). "Add" adds it to the list; with the list full the title becomes 73/78 and Next opens the list (item_list/monster_list, 6 rows 18 px apart, list_images, page arrows 30/31) to pick one to replace; equipped items and monsters in a squad can't be picked. "Throw away"/"Let go" asks with buttons.confirm (YES 8, NO 9). Touch only; a button fires when the stylus is lifted inside it. The top screen shows the item's name and description (item_top, item_top_window).

New magic (results.magic): one screen per new spell, spells learned first and then the ones only seen (their runes show as "?"). Bottom: background and a row at row_at_px (32, 76): bar coloured by element (images 9–14), %03d: number, name 32 px right, runes at 132, 152 and 172 px (images 15–32, "?" 34). The top screen shows the spell as the battle's top screen does. Next button or A/B/X/Y/D-pad goes on.

/data/battle-end.md has the same data as tables.

Title screen

/data/title-screen.json is the menu that follows TOUCH TO START: the logo above and five buttons below. It is read from the code and checked in an emulator (evidence: static_and_emulator); confidence says how sure each point is. Screen px (256 × 192 per screen, from the top-left) and ticks of 1/60 s.

  • Backgrounds. top.background already holds the logo and "©TAITO CORP. 2005, 2006"; bottom.background the sky, Isaac on the hill, the staff and the birds. Both go at (0, 0) and nothing moves: the birds are painted.
  • Buttons (buttons, in order: New Game, Load Game, Multiplayer, Wi-Fi Connection, Hand Settings). Each one is the panel panel (128 × 24) with its text (label, en, es, fr, de) centred at label.offset_px (64, 4) from its corner. images has the finished button in each language, captured from the emulator: the menu font is not decoded. The game picks the language from the console's (French, German, Spanish; any other one, English).
  • Entry (entry). When the menu appears both screens brighten from black in 30 ticks (fade_in) and, at the same time, each button moves in a straight line from from_px to to_px in 30 ticks: New Game and Multiplayer from the left, Load Game and Wi-Fi from the right, Hand Settings from below. The step per tick is step_px, truncated as the game does (entry.position), so New Game ends at x 29 and Multiplayer at x 40, 1 px short of table_to_px. Touches don't count until all of them stop.
  • Touch (press). Touch screen only: the D-pad and the buttons do nothing here. A touch that starts inside a button (hit_box, the edge not included) marks it; while the stylus stays inside, the button is drawn 2 px right and 2 px down, and it goes back 1 px per tick once released. Lifting the stylus inside chooses it: press.sound (3, SE_KETTEI) plays and the other buttons stop responding. There is no cursor or cancel sound.
  • On choosing (choose). The buttons don't leave: both screens fade to black in 30 ticks and the game goes where does says. New Game starts the prologue. Load Game opens the load screen even with no save: its three slots say "No Data". The menu never disables a button; a disabled one would have grey text (label.colour_bgr555.disabled) and not respond.
  • Music. The song music (21, SEQ_DM_TITLE) is started by the title demo, before TOUCH TO START, and the menu doesn't start another. Touching TOUCH TO START plays 2 (SE_CURSOR), fades to black in 30 ticks and stays black about 57 while the menu loads.
  • Idle (idle). Left alone nothing changes and it doesn't go back to the demo.
  • Hand Settings (hand_settings). It doesn't leave the menu: the five buttons go back to their from_px in 30 ticks, Hand Settings returns to its place, and the question ("Left/Right handed?", question) and the two hands (hands, from chr/select_hand.can) come in. Touching a hand plays 3, saves 1 for the left one and 0 for the right one, and the menu comes back. Which value means left-handed is not confirmed.
  • On the DS this screen has no Options or Quit.

/data/title-screen.md has the same data as tables.

Characters

/data/characters.json lists the 19 characters who speak in the story, with their name, how many lines they have (lines) and faces: one picture per expression, in the game's order. The story scenes use these portraits through /juego/data.json (see "Scenes").

Battle maps and battles

/data/maps.json lists each battle map: the full image, the game's minimap, and its battles. A battle has its number (battle), the story missions that use it, and the units on the field when it starts:

  • uid and unit (the slug in /data/units.json) say who it is.
  • x, y is its position on the map. With the sprite's origin on that point, units stand on the walkable ground of the map.
  • level, and named: true for story characters (bosses and allies), false for regular monsters.
  • orders: what it does on its own from when it enters the field (see "Unit orders in battles").

Each mission in missions also has objective (the text the game shows), conditions (how it ends: result victory, defeat, defeat_passed —a defeat that counts as passed— or draw, when trigger is met: no_enemies, boss_dies, isaac_dies, time (value in seconds), all_crystals, no_crystals, crystals_held (value crystals), isaac_reaches_area, boss_reaches_area, spell_cast (value = spell id), chest_opened, capture, draw…), time_limit_s (3600 when it sets none) and music (the song's n in /data/audio.json). When a mission has several battles, the list covers them all.

chests are the battle's chests: position x, y and odds, what it can give (item from /data/items.json, chance and the chance of each level in levels).

seals are the appear seals on the battle's floor: point x, y and units, the indexes in units of the hidden units that stand on it. How to draw them is in "Appear seals".

animation is set on the maps the game animates: falling snow (tile set ac03, 8 frames of 11 ticks) and running sand (ac04, 4 frames of 7 ticks). Each frame lasts one tick more than the value in the file: the game's clock counts down to 0 and changes frame on the next tick. base is the map without that layer; frames holds the frames stacked top to bottom, each the size of base and transparent where the layer is empty. Draw frame i over base for ticks[i] ticks of 1/60 s, in a loop. image is base with the first frame. On other maps animation is null.

decorations are the game's objects drawn over the map: bridges, tree tops and others. Each has its point x, y in map px, alpha (opacity 0 to 31) and height: the game draws them in order of y + height. frames is a strip of frames of frame_size, stacked top to bottom; origin is the point within each frame. Frame i shows for ticks[i] ticks, in a loop; with one frame the decoration is still. sprite groups those that share a picture.

lava is set on the magma maps (025–028, 044, 305): the lava glows because the game changes the colours of one row of its palette. mask is a picture the size of the map: its red is that row's colour (1–15) at each pixel, and 0 outside the lava. Frame i paints those pixels with palettes[i] (16 colours #rrggbb; 0 is unused) for ticks[i] ticks, in a loop. Frame 0 is the map's picture.

terrain is the map's collision and terrain, in map px (shapes may sit off the edge). There is no grid: units are pushed out of shapes every frame.

  • boxes (centre x, y ± half_width, half_height) and circles (x, y, radius), each with its kind:
    • high_obstacle: stops walking and flying units and shots. low_obstacle: walking units only.
    • Zones, for walking units only (flying units are never affected): magma and damage_floor deal 50 damage every 60 ticks (fire and no element); ground the same, only when a spell heated it; ford half speed unless frozen, puts out and blocks burns; ice nothing, half speed when a spell melts it; bog half unless dried; sand nothing, half when turned to swamp; quicksand pushes 0.25 px per tick towards push.
    • The 50 damage is multiplied by (118 + random(0..20)) / 128; a unit of the same element heals 6 and one of the opposite element takes 75. Water units ignore the ford and melted-ice slowing; earth units ignore bog, sand and swamp slowing and the push.
    • event_area (with its number area): missions ask whether units are inside; it blocks nothing.
  • squad_starts: where the player's squads appear. A squad's units are placed at that point plus team.formation_px from /data/rules.json.
  • Terrain spells (terrain in /data/spells.json) change how zones behave inside a circle for 20 s; they do not change the shapes.

terrain_effects lists the zones with an effect on each map (kind, plus the push directions of quicksand). The full rules are in terrain of /data/rules.json:

  • effects says what each kind does to a walking unit inside it: push_px_per_tick, speed_multiplier, or damage every every_ticks with its element. spared_elements are the elements spared its slowing or push, and altered is the effect while a terrain spell changes it (heat, harden, swamp, dry, freeze).
  • damage: a unit of the same element heals same_element_heals_fraction of the damage, one of the opposite element takes opposite_element_multiplier, and the result is multiplied by a random value between the two in random_multiplier.
  • flying_units_affected is false, and sides_affected says Isaac, his squads and the enemies are treated alike.
  • The animated map layers only draw: the running sand matches the quicksand zones, the snow has no zone of its own and the lava is magma.
  • Each unit has terrain_spared in /data/units/<slug>.json: all if it flies, the kind whose slowing or push it ignores, fire_heals if fire terrain damage heals it and fire_x1.5 if it takes 1.5 times.

Appear seals

Some story battles draw hexagram seals on the floor at the points where enemies appear (98 seals in 34 battles; battle 1 has six, one under each Misty Mush). /data/appear-seals.json has the pictures, the rules and, in battles, each battle's seals with the units that stand on them; seals in /data/maps.json has the same points. Map px, ticks of 1/60 s, alpha out of 31 (opacity = alpha / 31). It comes from the game's code, and the colours, the depth, the blink and the light column were checked in an emulator on battle 1.

  • When. Every seal is on the floor from the start of the battle to its end. It does not react to the units: nothing spawns from it and nothing changes when a unit appears on it.
  • Picture. seal.image is a strip of 32 × 25 frames, left to right base, fire, wind, water, earth, light. Put the frame's pivot (16, 12) on the seal's point. The seal is drawn under every unit.
  • Base. Frame base (grey), always opaque.
  • Colour layer. One colour frame over the base, starting with fire. Every step_ticks (4) a step counter goes up; the layer's alpha is alpha_by_step[step − 1] (1 up to 30, then down to 2). After steps_per_color (59) steps, color_ticks (236), it passes to the next colour in colors, and from light back to fire. So the seal slowly glows and fades in fire (yellow-orange), wind (green), water (blue), earth (orange) and light (yellow).
  • Size. The seal does not change size (scale 1).

A unit appearing. A hidden unit comes out when the mission's script makes it appear, on its own point (in battle 1 that is a seal) or on another. Nothing sounds. Two things happen at once, from tick 1:

  • Blink. A counter goes from counter_from (63) to counter_to (127), one per tick. While it rises, the unit is drawn only when counter & 2 is not 0: 2 ticks shown, 2 hidden, for 64 ticks. Then it stays opaque.
  • Light column. column.image holds two 32 × 40 frames with the pivot (16, 32): back, drawn behind the unit, and front, drawn in front of it. Put the pivot on the unit's point moved down by its radius (radius_px in /data/units/<slug>.json), with alpha 16. Both mirror horizontally every other tick (mirror_frames). For ticks 1–29 their height is t / 30 of the full height, growing up from the pivot; from tick 30 they are full size and their alpha is 16 − floor(0.4 × (t − 30)); after tick 70 they are gone. The column follows the unit.

World map

/data/world-map.json describes the map Isaac travels between missions. picture is /juego/mundo.png; positions are pixels of that picture.

  • places: the 44 places, with id, x, y and neighbors, the places a path joins to it. region is its position in regions: the name and description the game shows (name, description, in en, es, fr and de); several places share a region.
  • availability_group (0 to 15) is the switch that enables the place, and unlock_flag the flag of /juego/data.json that turns it on. According to the code, a place is enabled when its flag is on, and a path when both its places are.
  • paths: the 50 paths, as pairs of id. They are straight and go both ways. Isaac only goes to a place joined by a path, and each leg takes movement.leg_frames (60) frames, short or long.
  • offers: the missions and towns the map offers on each place.
    • chapter is the chapter of /juego/data.json (qc01 to qc06; sc01 to sc03 for the side chapters) and step, the step inside the chapter. optional is true when completing it does not close the step.
    • objective is the text of its battles, empty when it only has scenes. scenes are ids of /juego/data.json and battles carries battle and map of /data/maps.json (null when the battle has no map page).
    • The same script can appear on two places: the game offers it on one or the other depending on a condition.
  • free_battles: the free battles the game registers on each place, with their chapter.
  • Arriving at a place that holds something starts it, and the game takes a checkpoint.
  • markers: the map's three markers. Red marks a place where a mission or a town is offered; blue, another kind of event, not identified; for green, what shows it is not known.
  • confidence says how each part is known: rom comes from the game's data; code, from reading its code, not checked in a play session.

The story: /juego/data.json

This file is the game's story logic, read from its scripts: which missions are open, what each one plays, what the player's answers change and which ending they reach. Paths inside it are relative to /juego/. Texts are in /juego/textos/<language>.json, under the same ids.

Game state

A game is four things, all starting empty:

  • flags: numbered switches, on or off.
  • counters: numbered values from 0 to 127. Counter rules.compassion is the one that decides between two endings.
  • answers: for each scene, the option chosen in each choice slot.
  • done: the missions completed.

Conditions

Many entries carry a list of conditions; all must hold. Each is [negate, test] (when negate is true, the test must fail):

Test Holds when
["flag", n] Flag n is on
["counter", n, op, k] Counter n compared with k (<, <=, >, >=, ==, !=) is true
["answer", scene, slot, option] In that scene, that option was chosen in that slot
["done", mission] That mission was completed
["result", k] The mission's last battle ended by condition k: 2, no enemy is left alive; 3, the boss died. Both are wins

Chapters and missions

chapters lists the chapters in order: qc01 to qc06 for the main story, sc01 to sc03 for side missions. Each chapter is a list of steps on the world map; each step offers missions as [mission, offer, conditions] and has flags to turn on when it ends:

  • Only missions whose conditions hold are offered.
  • offer 159 marks an optional mission. A step ends when one of its other missions is done (or any mission, if all are optional); then its flags turn on and the next step opens.
  • If no step is open (something must still be found), the last step that offered missions stays open so its places can be visited again.
  • The current chapter is the first qc0N whose flag in rules.chapterFlags (one per chapter, 1 to 4) is off. With all four on, the story continues in qc06 if flag rules.route6Flag is on, and in qc05 otherwise. Side chapters run at the same time.

missions describes each mission: kind (m battle, t town, r free battle) and a timeline played in order, skipping entries whose conditions (the last element) fail:

Entry Effect
["scene", id, how, conditions] Play that scene; how is 32 for a scene on its own, 29 or 31 for one shown at the battle that follows
["battle", id, conditions] Fight that battle
["flag", n, value, conditions] Set flag n to value (1 is on)
["counter", n, delta, conditions] Add delta to counter n, kept between 0 and 127

At the end the mission counts as done. Mission and chapter names are in the text files (missions, chapters).

Scenes

scenes[id].blocks holds the scene's blocks; it starts in main and runs its items in order:

Item Meaning
["s", background, cameraX, cameraY, cast] What the top screen shows from now on
["l", speaker, line] A line of dialogue: names[speaker] says scenes[id].lines[line]
["c", slot, [[line, block], …]] The player chooses: each option's text is scenes[id].lines[line]. Record the option's position as the answer for that slot, then continue in its block
["n", block] Continue in that block

The scene ends when a block runs out of items. Lines keep the game's own line breaks (\n). Isaac is the hero's default name. [L], [B] and [+Control Pad] (in each language's words) stand for the console's buttons.

To draw the top screen (256×192):

  • Background: bgs[background] is [picture, width, height]. Show the part that starts at x = clamp(-cameraX, 0, width - 256), y = clamp(-cameraY, 0, height - 192).
  • Characters: each [actor, x, y, expression, mirror] of cast is a portrait. portraits["<actor>_<expression>"] is [picture, dx, dy, width, height]: its top-left corner goes at (x + dx, 160 + y + dy), mirrored horizontally when mirror is 1. names[actor] is who it is.

Endings

When a mission turns on flag endings.gameOverFlag, the game ends: the ending scene is the one paired with the first flag that is on in endings.byFlag ([flag, scene]), or endings.default. Their titles are in the text files (endings).

Battles in the story

battles[id] is a battle of the story: map (its picture), size, and units as [uid, x, y, level, character] (character is not 0 for story characters). The same battles, with names, are in /data/maps.json.

Text files

/juego/textos/<language>.json has, for that language: names (speakers, by number), scenes (each scene's title and lines), missions, chapters, endings and units (unit names by uid). Its ui holds the labels the site's novel uses; you can ignore it.

Audio

/data/audio.json has music (code is the song's name in the game) and effects grouped by category. Songs that loop are recorded playing the loop twice and fading out. In the game, a song with loop_end_s jumps back to loop_start_s when it reaches loop_end_s and plays forever; one with end_s plays once. missions and scenes count the missions and scenes that use it (each mission's is in /data/maps.json), and screen is the fixed screen that uses it: world_map, mission_complete, victory and defeat (when a battle ends), game_over, title or ending.

sound_events says what plays the sound effects of the menus, the world map, the results screen, treasure chests and mana crystals. It is read from the game's code (evidence: static): each place that plays a sound and the number it loads; it was not observed in an emulator. The game plays sound number N as entry N − 1 of the selected sound archive. That is normally SEQ_SE, the "Game" category of effects.

  • events: group (menu, world_map, results, chest, crystal), event, sounds (id, name, url) and calls, the places in the code that play it. In menus, cursor, confirm and cancel are identified; other gathers the remaining menu sounds, whose triggering action on each screen was not traced. On the map: move (one leg of a path), enter_event (arriving at a scene or a town) and enter_battle. Results: exp_count (each step of the experience count) and level_up. Chest: open and item (the item obtained). Crystal: captured and lost.
  • download: while the duel mode's Download screen is open, the game uses another archive, SEQ_SE_CURSORS. In it, numbers 2, 3, 4 and 108 play their own versions (replaces is the normal sound of that number). url is null when the site has no clip of it.

Other pictures

/data/gallery.json groups the rest of the art by section: runes (the shape of each one, as a picture), spells and battle effects, backgrounds, world map and its markers, interface, and unused or test art. /juego/mundo.png is the world map.

Not in the data

Your version has to define these:

  • Which orders produce the AI's states 6, 7 and 12 (unresolved_states in /data/ai.json) and how the AI looks in a battle: what is published comes from the code, not from an emulator.

The graphics, texts and music belong to Taito and Ubisoft.

1×