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.esfor 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.
msfields 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
framessays 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. originis 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 thatoriginfalls on the unit's position.actionandfacingsay when the game plays it: the unit's action and which way it faces (downfront view,upback view,left,right; empty when the action has no facing).usedisfalsefor the ones that are drawn but never played.previewis the same animation as a GIF, to see what it shows. GIFs store times in 1/100 s and merge repeated frames, so useframesfor 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,accuracyandevasion, each as[level 1, level 50]. At level L a stat islevel1 + floor((level50 − level1) × factor / 128), wherefactoris the value of the unit'sgrowthcurve (early,lateorlinear) for level L in/data/levels.json.speed_px_per_tick: how far it walks per tick at levels 1 and 50; it isspeed / 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) andradius_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) andsquad_size(how many units one squad brings).attack_used_by: what itsattackstat is for.normal_attack, its normal attack. The units that never attack (attack_reach_px0) havedance_spells(Isaac: only the dance spells use it, hitting with his full attack minus a quarter of the target's defense) ornothing(the mages, the story bosses and the Villager: it is never used).attack_noteexplains it in English and Spanish, and says how the unit deals damage./data/units.jsonhas both fields for every unit.elementis the unit's element, anddescriptionthe 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 toroute_points_maxpoints; a unit reaches a point when it is less thanpoint_reached_distance_pxaway. 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 walksspeed / 32px per tick, without acceleration; obstacles and other units push it out.Target: every
retarget_every_ticksticks, in thestates_with_target_searchstates, it looks for enemies less thansearch_pxaway 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 inclasses) sets a decision state (state):waitwith 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.movefollows the route without attacking;move_autoattacks on the way without leaving it;move_attackchases within its sight radius or follows the route;move_wait_autofollows the route, chases and, once stopped, waits.guard(states 9 and 10) watches over a point:guardsays 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'skeep_distance(/data/units.json).take_crystal(8) goes to the nearest mana crystal of another side withinorder_list.take_crystal_radius_pxand 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,yis a map point in px;dx,dyare added to the unit's position when the order starts. The unit walks straight to it. wait:ticks(null: forever);chase: falseis state 0 (attacks without moving),truestate 1 (chases within its sight).back: goes backstepsorders, 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 becomessight_px.change_max_hp: its max HP becomesfloor(max × percent / 100);set_max_hp:hp. Its HP becomes that same value, never above the max it had.guardnever ends: the list stays there.
How the list runs (order_list), one step per tick (steps_per_tick):
- It starts at the first order. The tick an order starts in does not count towards ending it.
ends:at_onceends in the tick it starts;route, the first tick after the unit reaches the point (on arrival it is in stateroute_end_state);ticks, onceticksticks 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;jumpisback;removed, likeat_once, and the order also leaves the list.- The next order starts on the next tick. With a
waitof Nticks, the next order starts N + 2 ticks after the wait started. backcounts only the orders still in the list:removed_when_doneare gone.- 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 haslevel(the player's level, 40),element_levels(the six element levels, which the game clamps to 1–50) andsquads, three squads (unit_idof/data/units.json,name,slugandlevel). The cartridge does not name them: they are numbered 1 to 8.rating: the rating of Wi-Fi matches, betweenminandmax. Each of therowshas 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 betweenminandmax.elo_kis 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, andcaptures_per_battle_max.items:inventory_max,found_level_max(the highest level of a found item) andfound_per_battle_max.levels:level_maxof Isaac and the monsters,element_level_maxandenemy_level_max.experience:per_kill_per_level(each unit that dies gives that number × its level),battle_maxandlevel_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 atlevels.enemy_level_max) and the extra levels of chest items (capped atitems.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,defenseormagic_defense).bonus_by_level: the value v at levels 1 to 10, out of 128. Chests only give levels 1 toitems.found_level_maxof/data/rules.json.effect, how v applies to the unit's stat x:multiply:x + floor(x × v / 128). Withhp, 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;nullfor the rest): what the item changes in a battle, inenandes:what(what the stat is),examples(worked examples with level-1 units),good,bad,caution,level(when the item's level does not matter) andanchor(the section of/logic/combat/with the rule).exampleshas the same examples as numbers:accuracy,evasionandhit_percentas [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 positionx,yin map pixels,chance(0 to 1),levels, the chance of each level, andnew_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:
closedis the closed picture the maps draw, placed at the chest's point plus (dx,dy).sheetholdscellsequal cells ofcell.width×cell.heightpx incolumnscolumns. The chest's point is atcell.originin each cell.animations:idle0(closed),open(opening),loop(open with the reward),suka(smoke) andemptybox(empty). Eachframesis a list ofcellandticksof 1/60 s.loopsays whether it repeats, andpreviewis a GIF or PNG to look at.flow: with a reward the game goes fromidle0toopenand stays inloop; without one, fromidle0toemptybox.sukahasused_by_gamefalse: 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:
- 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.
- 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 ismax(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. - For each point k ≥ 1 compute
angle[k], the direction of the segment that reaches it, andratio[k], the share of the total length that segment is. Point 0 takes the direction of the first segment. - 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]
- 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 isfloor((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_circleandpoint: a point;direction: it leaves Isaac towards where the stylus is released;unit: a unit;instant: no aiming).targets:enemies,alliesorall;nullfor the three that touch no unit (Transport and Flight move whoever casts them, Restore only changes the ground).numbers.cast_range_pxis the farthest distance from Isaac.effect_group: only onunit_effect(nullfor the other kinds), what it does to units.healrestores HP or removes statuses: thehealflag, a status of groupcureorregeneration, or the variants that move HP to the caster's side (life_thread,life_shine,light_transfer).boostputs a status on allies andweakenon enemies: every status the game casts on allies helps them and every one it casts on enemies hinders them.otheris 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: withpower_scaled, the power at cast isfloor(floor((power + caster's magic) × quality factor) × (rank + 4) / 4); without it,poweris used as it is. With thehealflag, 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) / 4and by the element factor inelements.status_chanceof/data/combat.json),duration_ticksandpower.terrain: how it changes the ground (heatturns it into magma,hardenhardens magma,freezefreezes water,drydries swamp into sand,swampturns quicksand into swamp,restorerestores 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 ofarea.per_object_radius_pxfor each falling object. The 8 × 8 px cells whose centre is inside the circle are painted.paintsis the layer it puts and on which surface (on);imageis its 32 tiles animated, each frame lastingframe_ticks(null: still).clearsare 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, andrestoreremoves everything. It appears at once and, when the zone ends, vanishes with no animation: there is no entry or exit animation, and theframe_ticksframes loop the whole layer. It is only the picture: the terrain statuses give the effect on units.on_any_mapsays whether that surface exists on any map.drypaintsmf04on swamp, but no map has swamp (on_any_map: false): in the game it only removes the swampswampleaves.flags:piercing,homing,rapid(bursts),spread(fan),lock_on(one shot per target),flying_onlyandwalking_onlylimit 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 andeffect_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; whengains_onlyis 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; bymp_per_kill= MP × casts to take the weak enemy's HP, then by stroke, then by reach) andboss(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;cellsholds [x,y,w,h] sheet rectangles andoriginstheir 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_startis the loop's starting frame.ticksandsecondsretain original durations.units[].actions[action][facing]: sequence,ownandfallback. Monsters without chant/cast actions use idle.launch_pxpositions the shot;boss_spellsidentifies actual battle use.spells[].graphics: role, atlas, sequences, palette, evidence and confidence.times_secondsconverts 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_confidenceseparates 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,soundsandduration_secondsdescribe each status./data/statuses.jsonalso includes this asvisual.- 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:
mapandoriginsay on which game map and spot the scene stands, so the terrain the spell acts on shows;zonesays when the zone is born: on the impact (hit), at each falling object (object_hits) or when the area appears (release);radius_pxandlasts_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). Eachtargetinghas its icon (icon) and its way to confirm (confirm).arrowsays whether it uses the arrow;range_graphics_3d, whether it draws something on the ground (circles or shade).directiondraws nothing on the ground: only the arrow and the icon. - Arrow (
arrow,directiononly: Fireball and the other shots). The pictureimage(32 × 56) points up. Its pointpivot_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 istip_px(36) px ahead of Isaac and the handletail_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_steps4096 per turn), with no snapping to 4 or 8 directions. It does not stretch or scale with distance (scale1) and is opaque (alpha1). 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 itsshot_range_pxfrom/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 atat_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 ofmove_when_arrow_deg: 90–180 right-handed, 0–90 left-handed) it jumps tomoved_at_px(236, 20 or 20, 20) so the hand does not cover it. - Circle (
circle_flow,targeting: circle). Outside the reach (cast_range_pxfrom Isaac) the field is shaded withshade_outside_range(#525252, alpha 20/31). A touch outside the reach does not count; one inside sets the centre. Dragging picks the radius, fromradius.min_px(5) up to the spell'sradius_px; releasing casts with that centre and radius. The area is coloured bytargets(area_colour_by_targets: red enemies, blue allies, yellow all;area_colour_otherfor the spells it lists) and witharea_colour_at_max_radiusonce 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'smax_length_pxlong. 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.pointdraws 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_circledraws at the point a 24-sided disc of the spell's radius (radius_px,trigger_radius_pxorring_radius_pxby 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 thosetargetsallows 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'sradius_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.numbersare 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
intervalat most twice the unit's own (intervalat 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,terrorandforget.
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_pxand keeps it whilegap < attack_reach_px + attack.reach_pad_px. Withattack_reach_px0 it never attacks; when chasing it stops atattack.stop_gap_px. - Rhythm: wait
attack_interval_ticks, play theattackanimation; the hit or the shot comes outattack_strike_ticksafter 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); ifd ≤ 0, nothing. With anattack_element: ×½ against the same element, ×1.5 against the opposite one (opposite). Shots do ×1.5 to flying units. Finallyfloor(d × (128 + r) / 128), withra 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:
- Element: no element, P; same element as the target,
−floor(P / 8); opposite,floor(3P / 2). - If above 0,
× (100 − magic defense) / 100(magic defense ≤ 99); if 0 or less, halve it (the same element heals about P/16). × (128 + r) / 128, withrrandom from −10 to 10.- If the target has more
weaknessthanhalvingstatuses for that element, ×2; with morehalving, ×½. Spells withhealadd 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_gainifc ≥ many_casts, +1 if1 ≤ c < many_casts; +1 more if it isfav.old ≤ mid_band_max: +1 ifc ≥ many_casts; +1 more if it isfav.- Otherwise: +1 only if it is
fav, and then the opposite element loses 1 if its level wasdecay_minor 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).
frame: the frame, inframe.color, under everything. What the bar does not cover shows that colour.bar: the HP,floor(width_px × HP / max HP)px wide, with the effective max HP (afterchange_max_hp). Its colour,color_by_side:playerfor Isaac's side,enemyfor enemies,allyfor allies and characters on no side.change: forchange.ticksticks after the HP changes, another bar at the same place and height sits between the frame andbar. Only the last change counts.- After damage,
barshows the new HP and the one under it the HP before, indamage_color.enemyon enemies anddamage_color.otheron the rest. - After healing,
barshows the HP before and the one under it the new HP, inheal_color.
- After damage,
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,yin/data/maps.json) raised by its head height (head_px): the picture'spivot_pxgoes at(x + offset_px[0], y − head_px + offset_px[1]), that is(x − 14, y − head_px − 6). It sitsgap_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_px14) 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 thehiddenstatus 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_pxthat 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_crystalsin/data/units/<slug>.json), alive and withincapture_radius_px, addsgauge_per_unit_per_tickto the gauge. When nobody adds, the gauge dropsgauge_decay_per_tick. Overcapture_gauge.neutral(nobody's crystal) orcapture_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_pxrecoverheal.hpHP everyheal.every_tickswhile they are hurt. - MP. Every
mp_regen.every_ticks, Isaac's MP rises bymp_regen.base_mp, plusmp_regen.mp_per_crystalfor each crystal he holds, plus 1 for each earlier rise in a row without chanting, up tomp_regen.streak_max. Without crystals he still recovers: the base and the streak. After a chant the first rise comes aftermp_regen.chant_wait_ticks; with crystals, afterchant_wait_ticks − chant_wait_cut_ticks_per_crystal × crystals, at leastchant_wait_min_ticks. - Picture.
pictureshas one picture per owner, ofpicture_size. Its top-left corner goes at the crystal's point pluspicture_offset. - Capture bar.
capture_bardescribes the bar the game draws with rectangles, not with a picture.drawn_by_siteistruebecause the site redraws it from these rules:- It is
height_pxtall and sitsabove_pxabove the crystal's point. - The background, in
colors.background, starts atbackground.<neutral|owned>.left_pxand iswidth_pxwide. - Over it, from the same edge and in
colors.remaining, goes what is left:(capture_gauge − gauge) / remaining_divisorpx, with the gauge of the last push. - Each push shows it for
fade_ticksticks, with opacityticks left / 2out ofalpha_max.
- It is
- Missions. The objective "Purify the Mana Crystals!" is won when they are all the player's (
all_crystalsinconditions).
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_pxandview_y_max_pxinmaps(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 atposition 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, ordpad.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 atchant_recentre.px_per_frame(10) in a straight line, until he is attarget_px(128, 96). With less thanjump_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)atspeed × 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: arune_gridof 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) andANIMuse it as it is.NORMmultiplies it byspeed_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 withANIM, theNORMlayer 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 tovalue.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 inkinds.<kind>.imageare 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#de1852with a white outline;healing(HP gained) is green#18bd73with 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'sradius_pxdownward (/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). Withndigits, the centre of digitk(from 0, left to right) is at x =−(3n − 3) + 6kfrom 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, stayslasts_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_px6): three pink digits with their centres at (−6, 6), (0, 6) and (6, 6) from the mushroom's feet, for 40 ticks.example.imageis that number already put together (20 × 8), with its centre atpoint_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, colourcolour(#525252), alpha-blended over the field. Its alpha starts atalpha_from(10/31) and rises 1/31 everystep_every_ticks(2) ticks up toalpha_to(20/31): it gets there in 20 ticks. This is what tints the field; the circle does not tint it. - Circle (
circle). The pictureimage(256 × 192, its own orange palette) goes with its pointpivot_px(128, 79) oncentre_px(128, 96); the drawing fillsart_box_pxof 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, forappear.ticks(30) it grows from almost 0 byscale_per_tick(0.0332) per tick and turnsturns_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_unusedis a spinning fade-out that is in the code but the game does not use. - Stroke (
stroke). Each stroke is a ribbonwidth_px(8) wide around the stylus points (one per tick, up tomax_points), with round caps reachingcap_px(4) past the first and last point. It is painted with the texturetexture(32 × 32:rimyellow edge,corewhite 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 bystep31per 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 backgroundbackground, 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 showsno_mp(an X) instead of the rune.previewis the list with all 18. - Numbers. Each rune that can be paid carries its MP cost,
cost.digits(2) digits atcost.offset_px(18, 18) from its cell's corner; the cost ismp_per_runeof/data/runes.jsonfor the chant's next rune. Isaac's current MP goes with 3 digits atmp.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 pointpivot_px(29, 64) onat_px(224, 155): they grow upward and, full, arefull_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 inticks, 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 waitssettle_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 atat_px(128, 96); each picture ofwords(one per language, from the console's language: French, German, Spanish, any other one English) goes with itspivot_pxon that point.- For
ticks.circle_only(31) only the chant's magic circle and dim layer show, entering as when Isaac chants (/data/chant-screen.json,circleanddim_layer). The word is not drawn yet. - The word appears, opaque, for
ticks.shown(31) and staysticks.held(61) more. - 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. - At
ticks.total(154) the circle and the dim layer go at once.
- For
- 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 atisaac.lv_label_at_px(the_fr_esone in French and Spanish), his level as%2datlevel_text_at_px, the EXP bar atexp_bar_at_px(bar_frame_64, thenbar_back_64, thenbar_fillstretched toexp_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 atsquads.icon_at_px, the plate, its name and level and its bar at the offsets ofsquadsfrom the icon (bar 46 px,bar_frame_48andbar_back_48). The element levels are not on the top screen. - Bottom screen, page 1 (
bottom.pages1): the fourbarsat theirat_px, and thetexts:clear_timethe battle time in seconds (at most its time limit) ashh:mm:ss;defeatedthe kills;survivalthe squad monsters alive × 10000 / those deployed, as%3d.%02d%%, or---.--%with no squads;exp_poolthe EXP still to give, which counts down. - Bottom screen, page 2 (
bottom.pages2, 192 px further down): the element radar. The six element icons atradar.icons, and two translucent hexagons centred onradar.centre_px, one vertex per element (top light, upper right fire, lower right earth, bottom dark, lower left water, upper left wind). Red: radiusfloor((level + 10) × 68 / 50); green:floor(rank × 10 × 68 / 50), rankfloor((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_ticks30, 60, 30, 60):- Wait 30 ticks.
- Count-up, every tick:
step = max(1, floor(pool / 80)), pool − step, and Isaac and every squad each getstep. Soundcount_step(18,SE_KASAN) every tick; each level gained playslevel_up(19,SE_LEVELUP) and showslevel_up("UP!", itsframesmove it bydy_px) at Isaac'slevel_up_at_pxor on the squad's icon. Bars, levels andexp_poolchange every tick. At 0 the Next button shows and the screen waits. - Wait 60 ticks, then page 2 scrolls up into view in 64 ticks (3 px per tick).
- 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. - 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 getsceil(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.backgroundalready holds the logo and "©TAITO CORP. 2005, 2006";bottom.backgroundthe 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 panelpanel(128 × 24) with its text (label, en, es, fr, de) centred atlabel.offset_px(64, 4) from its corner.imageshas 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 fromfrom_pxtoto_pxin 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 isstep_px, truncated as the game does (entry.position), so New Game ends at x 29 and Multiplayer at x 40, 1 px short oftable_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 wheredoessays. 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 theirfrom_pxin 30 ticks, Hand Settings returns to its place, and the question ("Left/Right handed?",question) and the two hands (hands, fromchr/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:
uidandunit(the slug in/data/units.json) say who it is.x,yis its position on the map. With the sprite'soriginon that point, units stand on the walkable ground of the map.level, andnamed:truefor story characters (bosses and allies),falsefor 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(centrex,y±half_width,half_height) andcircles(x,y,radius), each with itskind:high_obstacle: stops walking and flying units and shots.low_obstacle: walking units only.- Zones, for walking units only (flying units are never affected):
magmaanddamage_floordeal 50 damage every 60 ticks (fire and no element);groundthe same, only when a spell heated it;fordhalf speed unless frozen, puts out and blocks burns;icenothing, half speed when a spell melts it;boghalf unless dried;sandnothing, half when turned to swamp;quicksandpushes 0.25 px per tick towardspush. - 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 numberarea): 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 plusteam.formation_pxfrom/data/rules.json.- Terrain spells (
terrainin/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:
effectssays what eachkinddoes to a walking unit inside it:push_px_per_tick,speed_multiplier, ordamageeveryevery_tickswith itselement.spared_elementsare the elements spared its slowing or push, andalteredis the effect while a terrain spell changes it (heat,harden,swamp,dry,freeze).damage: a unit of the same element healssame_element_heals_fractionof the damage, one of the opposite element takesopposite_element_multiplier, and the result is multiplied by a random value between the two inrandom_multiplier.flying_units_affectedisfalse, andsides_affectedsays Isaac, his squads and the enemies are treated alike.- The animated map layers only draw: the running sand matches the
quicksandzones, the snow has no zone of its own and the lava ismagma. - Each unit has
terrain_sparedin/data/units/<slug>.json:allif it flies, thekindwhose slowing or push it ignores,fire_healsif fire terrain damage heals it andfire_x1.5if 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.imageis a strip of 32 × 25 frames, left to rightbase,fire,wind,water,earth,light. Put the frame'spivot(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. Everystep_ticks(4) a step counter goes up; the layer's alpha isalpha_by_step[step − 1](1 up to 30, then down to 2). Aftersteps_per_color(59) steps,color_ticks(236), it passes to the next colour incolors, and fromlightback tofire. 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 (
scale1).
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) tocounter_to(127), one per tick. While it rises, the unit is drawn only whencounter & 2is not 0: 2 ticks shown, 2 hidden, for 64 ticks. Then it stays opaque. - Light column.
column.imageholds two 32 × 40 frames with the pivot (16, 32):back, drawn behind the unit, andfront, drawn in front of it. Put the pivot on the unit's point moved down by its radius (radius_pxin/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, withid,x,yandneighbors, the places a path joins to it.regionis its position inregions: the name and description the game shows (name,description, inen,es,frandde); several places share a region.availability_group(0 to 15) is the switch that enables the place, andunlock_flagthe flag of/juego/data.jsonthat 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 ofid. They are straight and go both ways. Isaac only goes to a place joined by a path, and each leg takesmovement.leg_frames(60) frames, short or long.offers: the missions and towns the map offers on each place.chapteris the chapter of/juego/data.json(qc01toqc06;sc01tosc03for the side chapters) andstep, the step inside the chapter.optionalistruewhen completing it does not close the step.objectiveis the text of its battles, empty when it only has scenes.scenesare ids of/juego/data.jsonandbattlescarriesbattleandmapof/data/maps.json(nullwhen 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.confidencesays how each part is known:romcomes 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.compassionis 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.
offer159 marks an optional mission. A step ends when one of its other missions is done (or any mission, if all are optional); then itsflagsturn 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
qc0Nwhose flag inrules.chapterFlags(one per chapter, 1 to 4) is off. With all four on, the story continues inqc06if flagrules.route6Flagis on, and inqc05otherwise. 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 atx = clamp(-cameraX, 0, width - 256),y = clamp(-cameraY, 0, height - 192). - Characters: each
[actor, x, y, expression, mirror]ofcastis a portrait.portraits["<actor>_<expression>"]is[picture, dx, dy, width, height]: its top-left corner goes at(x + dx, 160 + y + dy), mirrored horizontally whenmirroris 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) andcalls, the places in the code that play it. In menus,cursor,confirmandcancelare identified;othergathers 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) andenter_battle. Results:exp_count(each step of the experience count) andlevel_up. Chest:openanditem(the item obtained). Crystal:capturedandlost.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 (replacesis the normal sound of that number).urlisnullwhen 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_statesin/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.