# Layout Data Versus Gameplay Data

Lecture ID: lecture_w03_d02
Week 3, Day 2

## Objectives
- Design minimal clone versions of map loading, script execution, party data, battle turns, item use, and save persistence.
- Connect week 3 material to the overall course plan.

## Transcript
Good afternoon. Let’s pick up exactly where yesterday’s map record discussion left us. Yesterday we looked at the map record as a bundle of references and event lists, and we treated it as an architectural junction point. Today I want to sharpen a distinction that sounds simple, but becomes one of the most important design tools in the whole course: layout data is not gameplay data.

When I say “layout data,” I mean the information that describes the physical substrate of a map. Dimensions. Border tiles. blockdata references. Tileset references. The rectangular space that the player can walk through or collide with. When I say “gameplay data,” I mean the information that makes that substrate mean something in the game: objects, scripts, warps, coordinate triggers, background events, trainer behavior, item pickup state, battle entry, and eventually persistence. In FireRed, those two categories are connected very tightly, but they are not the same thing.

Put that sentence on the board, please: “A layout answers where space exists; gameplay data answers what the space does.” That is the core distinction for today.

Now, because you are advanced students, I do not want us to leave that as a vague design slogan. Let’s make it concrete. In the source focus for today, we have the BattleColosseum two-player layout in pokefirered/data/layouts/layouts.json, chunk_76c0c20446e9e415, and the four-player layout in the same file, chunk_4a91aa64203b6d9a. Those are layout registry entries. They are not a quest. They are not a battle rule. They are not, by themselves, “the player enters a link battle.” They describe a reusable spatial artifact: a map width and height, a border block reference, a blockdata reference, and related layout-level material. Then, separately, the BattleColosseum_2P map record in pokefirered/data/maps/BattleColosseum_2P/map.json, chunk_919152bd3bfceb95, gives us the map header-style record with references to that layout and event collections like object events and warps. The coordinate triggers are in that same map JSON in chunk_237e353ea279eb27. And the map’s placement in the link group is represented in pokefirered/data/maps/map_groups.json, chunk_f22eb0b9c9a5d2a4.

So the first move is separating identity from geometry, and geometry from behavior. The map has an identity. The layout has geometry. The event lists have behavior hooks. Those three are often adjacent in the compiled game, but they are separate responsibilities.

Let me draw this as a three-layer box on the board. At the bottom, I’m writing “layout registry.” Under that, I’ll write “dimensions, border, blockdata, tilesets.” Above it, I’m writing “map record.” Under that: “layout ID, map type, scripts, music, weather, events.” Above that, I’m writing “runtime state.” Under that: “player position, active objects, flags, variables, current script, dynamic warp, save.” The mistake beginners make is to collapse all three into one giant map object. The mistake advanced programmers make is subtler: they keep the files separate, but at runtime they still let every system freely mutate every field. That gives you the appearance of architecture without the actual boundary.

A minimal clone should respect the boundary even if it is tiny. Suppose we build a loader for the lab. Our layout schema might say: layout_id equals BattleColosseum_2P, width equals some number, height equals some number, block_grid points to a tile matrix, permissions points to a movement-permission matrix. Our map schema says: map_id equals BattleColosseum_2P, layout_id equals BattleColosseum_2P, objects equals a list of NPC placements, warps equals a list of exits, coord_triggers equals a list of coordinate-activated scripts. The loader first resolves the layout ID. Then it builds a collision or permission view from the layout. Then it instantiates gameplay events from the map record.

Notice the order. You cannot correctly instantiate an object event until you know the coordinate system it lives in. You cannot correctly test a coordinate trigger until you know whether the player can step onto that tile. And you cannot correctly persist the player’s location until you know both map identity and coordinate within the layout. That is why the layout is not just “art data.” It constrains gameplay, but it does not own gameplay.

Check for understanding here. If I change the blockdata of the BattleColosseum_2P layout so that the central floor uses different metatiles, but I leave object events, warps, and coordinate triggers untouched, what kind of change is that? Mostly layout data. It may have gameplay consequences if movement permissions or metatile behaviors change, but the intent is spatial substrate. Now if I leave the floor exactly as it is and move the warp destination or add a coordinate trigger that starts a script, what kind of change is that? Gameplay data. Same room, different meaning.

That “mostly” in the first answer matters. FireRed, like many production games, uses tiles that carry behavior. A tile is not only a visual square. A ledge tile, a water tile, a door tile, a counter tile, a tall grass tile, a collision tile: these are visual and behavioral at the same time. So our distinction is not “visual versus code.” It is “stable spatial substrate versus eventful game state.” In the real codebase, that distinction crosses file boundaries imperfectly, but architecturally it is still the distinction that keeps the system understandable.

Let’s connect that to the course plan. In week 1, you decomposed the whole creature RPG into runtime shell, overworld maps, event scripting, creature data, battle, inventory, UI, save, and link systems. In week 2, you traced data records through globals, callbacks, tasks, scripts, structs, and visible behavior. We are now inside the overworld map runtime, weeks 3 through 5. The reason we are spending this much time on maps is not that maps are cute rectangles. It is that maps are where nearly every other system touches the player’s continuous experience. Scripts start from maps. Battles start from maps. Items are found on maps. Party checks are triggered by maps. Save location is usually map location. Link rooms are maps with special rules. So if you blur layout data and gameplay data here, the rest of the engine inherits the confusion.

Now let’s do a minimal clone pass. Imagine I call a function named load_map, and I hand it a map ID. In a rushed implementation, load_map opens one JSON blob, builds tiles, spawns objects, registers scripts, sets music, moves the player, maybe even starts a cutscene. It works for a demo. It fails as architecture. In the FireRed-inspired version, load_map should first resolve the map group and map number, or in our simplified version, a map key. The map group relationship is why pokefirered/data/maps/map_groups.json, chunk_f22eb0b9c9a5d2a4, is relevant. It shows that maps are not just isolated files; they sit in a registry that the engine can address. Then the map record gives us the layout reference. The layout reference points into pokefirered/data/layouts/layouts.json, such as chunk_76c0c20446e9e415 for the two-player colosseum layout. Only after layout resolution should runtime systems create the loaded map instance.

On the board, I’ll write a small pseudo-structure verbally. MapDefinition has id, layout_id, object_events, warp_events, coord_triggers, background_events, scripts. LayoutDefinition has id, width, height, blockdata, movement_permissions. LoadedMap has definition, layout, active_objects, trigger_index, warp_index. PlayerState has map_id, x, y, facing, party, bag, flags, variables. The key is that LoadedMap points at definitions; it does not become the permanent owner of all mutable progress.

Why? Because persistence becomes impossible to reason about if every loaded map owns its own progress. Think about item pickups. A Poké Ball item sitting on a map is an object event. The fact that it has already been collected is not a property of the layout. It is not even purely a property of the map definition. The definition says, “there can be an item object here.” The save file says, “in this playthrough, that object has already been consumed.” That split is the difference between a reusable authored world and a particular player’s timeline through that world.

Let’s stay with that item example because it connects several systems. A visible item ball on the map is object-event gameplay data anchored to a coordinate in layout space. When the player presses A facing it, field interaction code resolves the object, starts a script, gives the item, sets a flag, hides or removes the object, and eventually returns control to the overworld. If the item is a Potion and the player uses it later, now we are in inventory and party data. The map does not own the Potion’s healing effect. The bag owns the count. The item table owns the meaning of the item. The party data owns the current HP that changes. The save system persists the bag, party, and event flag. The map only supplied one way to acquire the item.

That is why the lecture title is not just a map lecture. “Layout Data Versus Gameplay Data” is a general architecture lecture using maps as the clearest example.

Now, some of you will be thinking, “But FireRed is C and compiled data; our lab is JSON. Are we accidentally learning JSON architecture instead of FireRed architecture?” Good. That is exactly the concern I want you to have. We already established in week 2 that generated data versus runtime JSON must not be confused. The decomp repository uses JSON as source data for tooling, but the shipped game does not parse JSON at runtime. For our clone, JSON is a teaching convenience and a pipeline convenience. The architectural lesson is the shape of the data and the boundaries between systems, not the runtime text format. So when I cite pokefirered/data/maps/BattleColosseum_2P/map.json, chunk_919152bd3bfceb95, I am using it as source-level evidence of authored map structure, not claiming the GBA is doing a JSON parse while you walk through the room.

Let’s now bring in scripts. A coordinate trigger from pokefirered/data/maps/BattleColosseum_2P/map.json, chunk_237e353ea279eb27, is not the script execution itself. It is a trigger declaration. It says, in effect, at this coordinate, under these conditions, associate a script or behavior. The runtime still needs a script engine, a current script pointer or instruction index, a way to block player input while text or movement runs, and a way to set flags and variables. So in our minimal clone, a CoordTrigger record might include x, y, condition_flag, script_id. The map loader indexes it by coordinate. The movement system, after a successful step, asks, “Is there a coordinate trigger at the player’s new position, and is its condition satisfied?” If yes, it hands script_id to the script executor.

That handoff is the boundary. The map system detects spatial eligibility. The script system executes game logic. Do not let the coordinate trigger itself mutate the party, start the battle, give the badge, and save the game. It should point to the thing that can do those operations in a controlled way.

Here is a concrete example. Suppose our link colosseum map has a coordinate trigger at the entrance tile that says, “when both players are ready, begin battle setup.” The layout tells us the entrance tile exists and can be walked on. The map record tells us there is a trigger on that tile. The script says, “lock player movement, check link state, choose battle mode, transition to battle.” In FireRed, battle setup code is not isolated from overworld concerns; pokefirered/src/battle_setup.c, chunk_494d132a59422c85, includes overworld, item, script, event data, field movement, battle transition, and battle headers. That include cluster is evidence of a real architectural junction. Starting a battle is not “the battle engine alone.” It is a negotiated transition from field state into battle state.

But again, negotiated does not mean melted together. In our minimal clone, script execution might issue a command called start_battle with parameters. The battle system receives a BattleRequest: opponent, battle_type, terrain, participants. It builds a BattleState from party data and move data. The map runtime pauses or exits. The battle loop handles turns. When battle ends, a result goes back to script or overworld: won, lost, fled, link_disconnected, and so on.

Let me pause and ask a check question. Why shouldn’t the map loader directly create the BattleState when it sees a battle-related trigger in the map JSON? Think it through. The answer is that loading a map and entering a battle are different moments. You may load the map and never step on the trigger. The trigger may be conditional. The party may change before activation. The link partner may not be ready. The player may save and reload. If the map loader eagerly creates battle state, it freezes gameplay too early and gives layout-adjacent data authority it should not have.

That same principle applies to party data. A party member is not part of a map layout. It is not part of a map definition. Yet map scripts constantly inspect party state. They ask whether you have a certain creature, whether someone can use a field move, whether the first party member is fainted, whether the party has space for a gift. The party system is persistent player state with structured creature records. The map and script systems should query it through commands or service functions. In FireRed source organization, you see this repeated cross-system relationship in files like pokefirered/src/pokemon_summary_screen.c, chunk_e8e17a42bbf17d46, where party, item, battle, moves, and UI headers meet around the summary screen. That is not our map source for today, but it supports the broader course claim: creature data is shared across UI, battle, item, and field contexts, so you need clean ownership boundaries.

Now, let’s talk about warps, because warps sit exactly between layout and gameplay. A warp has coordinates in the current map. That part is spatial. It has a destination map and destination coordinates, or sometimes a dynamic destination. That part is gameplay navigation. Yesterday we touched on warp events and dynamic warps, so I will not re-teach the whole mechanism. Today, focus on the data boundary. The floor tile at x, y might look like a doorway. The warp event says stepping on this coordinate transfers the player. The save state may remember where the player came from, or a dynamic warp may be set by a script. In the BattleColosseum_2P map record, pokefirered/data/maps/BattleColosseum_2P/map.json, chunk_919152bd3bfceb95, the same map header area that ties the map to a layout also provides event lists such as warps and objects. That is exactly the composition pattern we want to model.

A minimal warp handler should do four things. First, after movement, test whether the player is on a warp tile or warp event. Second, resolve whether the warp is static or dynamic. Third, produce a MapTransition request. Fourth, let the overworld loading pipeline perform the transition using the destination map’s own layout and events. The warp handler should not manually rewrite the current layout grid into the destination layout. That sounds obvious, but many student engines accidentally do the equivalent by storing map-to-map transitions as direct object references into already-loaded map objects. That works until you add save reloads, dynamic destinations, or map groups.

Here is the mental model: warps do not move rooms; warps move the player’s map identity and coordinate. The destination map then loads by its own definition.

Now let’s zoom out to the battle colosseum example. Why use a link-room style map in a week about layouts and gameplay data? Because it is a clean demonstration that the same physical map machinery supports ordinary exploration and specialized modes. The BattleColosseum_2P layout in layouts.json, chunk_76c0c20446e9e415, and BattleColosseum_4P layout in chunk_4a91aa64203b6d9a, are spatial forms. The link map group entry in map_groups.json, chunk_f22eb0b9c9a5d2a4, situates those maps in a category of maps used by link functionality. The gameplay meaning of the room comes from objects, scripts, triggers, and link-state interaction. The architecture lets a room be a room first, and a multiplayer staging area second.

That is a powerful pattern. In your own clone, you might have a town square layout used once for ordinary story progression and once for a festival event. Do not duplicate the entire layout just because the gameplay meaning changes. Conversely, do not cram festival state into the layout definition. Let layout describe space. Let map records and scripts describe eventful meaning. Let save data describe what has happened in this playthrough.

Let me write a small runtime flow on the board. Player presses a direction. Movement system checks movement permissions from the loaded layout. If blocked, no step. If allowed, player position updates. Object system checks object collision and maybe NPC movement. Warp system checks warp events at the new coordinate. Trigger system checks coordinate triggers. Script system may start. If script starts battle, battle setup creates a battle request from current party and context. Inventory and item effects may be called inside scripts or menus. Save persistence records durable player state, not the entire loaded map cache.

That flow is intentionally modest. It is not all of FireRed. It is the minimal clone skeleton we need for this week’s lab and homework. But it preserves the important dependency directions. Movement permissions come from layout-derived data. Warps and triggers come from gameplay data anchored to layout coordinates. Party, bag, flags, and variables come from save-backed player state. Battle turns are their own state machine, entered by request, not embedded inside the map.

Let’s spend two minutes on battle turns because it may feel far away from maps. In the full course, battle gets its own major module later. For today, our minimal clone battle can be tiny: choose player action, choose opponent action, order actions, apply effects, check fainting, emit result. The important link to today’s lecture is this: the map does not store the turn counter, move choices, volatile statuses, or battle participants’ temporary combat state. The map may start the encounter. The map may receive the outcome. It should not become the battle state container.

The same is true for item use. If the player uses a Potion from the bag while standing in the colosseum room, the layout does not care. The map definition does not care, unless some script condition later checks party health or item possession. The bag loses one Potion. The party member gains HP. The UI closes or refreshes. The save system eventually persists the new bag count and party HP. In source terms, the recurring include relationships around item, party, battle, and UI in pokefirered/src/pokemon_summary_screen.c, chunk_e8e17a42bbf17d46, are a useful reminder that item use is a cross-module behavior. Cross-module does not mean ownerless. You still need to decide which data lives where.

Let’s pause again. If I ask you to implement save persistence for the lab, what should you save for a map? Not the decompressed tile grid, unless your game supports player-modified terrain and you have chosen to persist that. Not the full list of loaded object instances with transient animation counters. You save current map ID, player x and y, facing direction, event flags, variables, maybe dynamic warp state, and object-hidden flags or local event state if your model uses them. You save party records. You save bag contents. You save story progress. The authored layout stays in the data files. On reload, you reconstruct runtime objects from definitions plus saved progress.

That reconstruction is the heart of data-driven architecture. The save file is not a memory dump. It is a compact set of durable facts sufficient to rebuild the player’s world. FireRed’s exact save system is more complex than our toy version, and we will study persistence more directly later, but the principle starts here. If you cannot reload a map cleanly from map definition, layout definition, and save state, your boundaries are probably wrong.

Now I want to bring this back to the week 3 lab. You are building a minimal map schema loader that reads layout ID, map dimensions, object events, warps, coordinate triggers, and movement permissions from a FireRed-like JSON record. The hidden test of that lab is not whether you can parse JSON. You can all parse JSON. The hidden test is whether your loader produces the right runtime shape. If your loader returns one enormous dictionary and the rest of the engine pokes at string keys, you have technically loaded the schema but architecturally learned very little. If your loader returns typed definitions, resolves layout references, builds a movement query interface, and leaves script execution as a separate subsystem, you are much closer to the FireRed lesson.

So as you work, ask this sequence of questions. Is this field authored data or runtime state? Is it spatial substrate or gameplay meaning? Is it global progress or local loaded-map cache? Is it a trigger declaration or an executing script? Is it a battle request or battle state? Is it an item definition, bag quantity, or party effect? These distinctions are what let a large RPG remain understandable.

Let’s do one more concrete walkthrough before the break. The player appears in BattleColosseum_2P. The engine resolves the map through the map group registry, represented for our purposes by pokefirered/data/maps/map_groups.json, chunk_f22eb0b9c9a5d2a4. It reads the map definition from pokefirered/data/maps/BattleColosseum_2P/map.json, chunk_919152bd3bfceb95. That definition references a layout from pokefirered/data/layouts/layouts.json, chunk_76c0c20446e9e415. The loader constructs the visible and collidable room. It instantiates object events from the map definition. It indexes warps. It indexes coordinate triggers from chunk_237e353ea279eb27. The player takes a step. The movement system checks the layout-derived permission. The trigger system sees whether the new coordinate has a script. If a script starts a link battle, battle setup crosses into battle architecture, which is why pokefirered/src/battle_setup.c, chunk_494d132a59422c85, is full of includes from overworld, script, event data, item, and battle-related headers. The battle runs elsewhere. The result returns. Save data records the durable consequences.

If that sounds like a lot of systems for one room, yes. That is the point of using a creature RPG as our architecture case study. The player experiences one continuous fiction. The engine maintains that fiction by keeping many kinds of data separate enough to be reliable and connected enough to produce behavior.

Before we move into the next segment, I want a quick verbal check. Imagine I duplicate the BattleColosseum_2P layout entry and make a BattleColosseum_Practice layout with the same dimensions and blockdata but different object events and scripts. Did I need a new layout? Probably not, unless the spatial substrate changed or the asset pipeline requires a separate registered layout for some practical reason. I likely needed a new map definition pointing at the same layout. Now imagine I keep the same map definition but want the room to become blocked off after a story event. Should I edit the layout at runtime? Usually no. I should use event flags, object events, script conditions, or a map reload variation. If the actual collision grid changes permanently, then we need to talk carefully about whether that is a dynamic map behavior or a different layout.

That is where we will go next: how to design the minimal script executor so that triggers and warps can cause meaningful gameplay without stealing ownership from party, battle, inventory, or save systems.

So let us build the script executor in the smallest possible form, and I want you to notice the boundary we are drawing. The script executor is allowed to coordinate gameplay. It is not allowed to become gameplay.

On the board, I am going to write three words: observe, decide, dispatch. That is our minimal script system. Observe means the script can read a fact: the player stepped on coordinate three, four; flag number twelve is set; the player has an item; the party has a usable creature. Decide means the script can branch: if this flag is set, run this line; otherwise run that line. Dispatch means the script can ask another system to do work: start a battle, show a message, move an object, warp the player, give an item, set a flag. But it should not contain the battle engine. It should not contain the inventory model. It should not contain the save serializer. It is a traffic controller, not the whole city.

Now, connect that back to the Battle Colosseum examples from the source focus. We have layout records in pokefirered/data/layouts/layouts.json, specifically chunk_76c0c20446e9e415 for BattleColosseum_2P and chunk_4a91aa64203b6d9a for BattleColosseum_4P. Those are layout-side facts: dimensions, block layout identity, tileset relationship, the spatial substrate. Then we have pokefirered/data/maps/BattleColosseum_2P/map.json chunk_919152bd3bfceb95, which is map-side gameplay attachment: map header, objects, warps. And chunk_237e353ea279eb27, which gives coordinate triggers. The important architectural lesson is not merely that these are separate files. The important lesson is that they are separate reasons to change.

If I change the dimensions of the 2P Colosseum, that is layout territory. If I change which script fires when the player steps on a special tile, that is map territory. If I change what that script does after firing, that is script territory. If that script starts a battle, then the battle system owns turn resolution. This is the chain I want you to be able to trace without blurring it.

Let’s do a concrete minimal clone version. Imagine our map loader reads a FireRed-like map record. It reads layout ID, map width, map height, object events, warp events, coordinate triggers, and movement permissions. At load time, it resolves the layout ID through a layout registry. That phrase “layout registry” matters. The map does not duplicate the entire layout unless the engine has chosen a packed format where duplication is part of generation. In our clone, the map says, “I use layout BattleColosseum_2P,” and the loader says, “Fine, I will fetch dimensions, block grid, tileset references, and collision permissions from the layout registry.”

Then the map runtime creates a current map state. Not the save file. Not the global game state. A current map state. It contains the resolved layout pointer or reference, the object instances for this visit, the active trigger table, and the warp table. The player avatar moves across the collision grid. Every time the player attempts to move, the runtime asks, “Is the target tile passable?” That answer comes from layout and movement permission data. After a successful step, the runtime asks, “Is there a coordinate trigger on this tile?” That answer comes from the map definition. If yes, it dispatches the script ID attached to that trigger.

Check your understanding here. If the player steps onto a tile and receives a message saying, “The door is locked,” where did that behavior come from? Not from the layout by itself. The layout may tell us the tile exists and whether it is walkable. The coordinate trigger or background event tells us there is behavior attached. The script provides the message and condition. If that script checks a flag, event data owns the durable story fact. So even this tiny moment is already a chain across several systems.

Now, let’s make the script executor. In a minimal version, I would not begin with a text parser. I would begin with a small instruction list. Each instruction has an opcode and operands. The opcodes can be very modest: show message, set flag, clear flag, check flag, jump, warp, give item, start battle, end. That is enough to demonstrate the architecture. We do not need the full richness of FireRed’s script language on day one. We need the ownership boundaries.

Suppose a coordinate trigger points to script number 210. Script 210 says, in plain English, “If event flag ColosseumIntroDone is false, show a welcome message, set ColosseumIntroDone, then end. Otherwise, end.” The executor reads check flag. It does not know why the flag matters. It asks the event data system. Then it reads show message. It does not draw text itself in a serious engine; it dispatches to the message or UI layer. Then it reads set flag. Again, it asks event data. Then it ends and returns control to the overworld callback.

This is where our prior weeks come back, but only as foundation. We already studied callbacks and tasks, so I am not going to re-teach them. I only want to remind you of the shape: the overworld is running as part of the main game loop, and scripts fit into that loop as stateful sequences. In FireRed, the codebase repeatedly shows cross-system collaboration through includes and calls, not through one monolithic “gameplay manager.” For example, pokefirered/src/battle_setup.c chunk_494d132a59422c85 includes overworld, item, load_save, script, event_data, battle, battle_transition, and related modules. That does not mean battle setup owns all those modules. It means battle setup sits at a boundary where map events, script conditions, player state, and battle entry have to meet.

And that is an advanced point, so let me say it more sharply. Coupling is not automatically bad. Unowned coupling is bad. A battle transition necessarily knows there is an overworld and a battle. A script command that starts a battle necessarily talks to battle setup. The question is whether each piece has a crisp responsibility. Battle setup can prepare the battle context. The battle engine can run turn logic. The script executor can suspend while battle happens and resume after it receives a result. The save system can persist the flags and party state that changed as a consequence.

Let’s walk through that entire flow in our minimal clone.

The player steps on coordinate four, seven. The current map runtime finds a coordinate trigger. It asks the trigger, “What script?” The trigger says, “ColosseumChallengeScript.” The script executor starts at instruction zero. Instruction zero checks whether the player has at least one conscious party member. The script executor does not inspect HP fields directly. It asks the party system, “Can battle?” The party system owns party data. It knows the six slots, current HP, species identity, level, moves, and whatever minimal status model we support. It returns true or false.

If false, script shows “You need a rested party.” If true, script starts a trainer or link-style challenge. In our clone, start battle creates a battle request. That request contains participants, battle type, terrain or arena ID, and maybe a callback target for after the battle. Then the battle system takes over. The script is suspended. Not ended, suspended. This is important because many game events are not instantaneous. Messages wait for input. Fades take frames. Warps transition. Battles last many turns. So even a minimal script executor should not assume every command completes immediately.

On the board, I would draw the script instruction pointer as a little arrow. When the executor reaches start battle, the arrow stays at “waiting for battle result.” The main runtime changes mode or callback into battle. Later, battle returns win, loss, flee, or cancel, depending on what the game supports. The script executor receives that result and continues. It may set a flag if the player won. It may warp them out if they lost. It may award an item. But again, it coordinates those outcomes; it does not calculate damage.

Now, what is the minimal battle turn design? For this week, we only need a clone version strong enough to connect systems. A battle state has two active battlers, or four if we are preparing for double battles later, but begin with two. Each battler references a creature from party data or an enemy template. Each creature has current HP, stats, moves, and status. A battle turn has phases: collect commands, determine order, execute move or item action, apply fainting checks, determine whether battle ends. That is enough.

Notice how this differs from map data. A map trigger can start the battle, but the map trigger should not say “Tackle does 17 damage.” Move power, type, accuracy, stat stages, damage formula, and turn order belong to battle data and battle runtime. Later in the course, when we compare with Pokemon Showdown, we will use it as a secondary model for battle data and deterministic tests, not as our authority for FireRed’s overworld architecture. The research packet even points us toward pokemon-showdown/data/abilities.ts chunk_da22cf969179258c as an example of data-enriched battle modeling, where abilities carry ratings and explanatory metadata. That is useful contrast. FireRed’s architecture is not trying to be a modern competitive simulator, but both systems teach the same separation: battle facts belong with battle systems, not with map layouts.

Let’s fold item use into the same boundary. If the script says give item, the script executor dispatches to the inventory system. The inventory system decides whether there is space. It updates item counts. It may return success or failure. The script can branch on that result. If the player uses a potion from the bag during battle, the map is not involved at all. The bag UI asks inventory whether the item exists; item logic asks whether the target creature is valid; party data applies HP recovery; battle runtime consumes the turn if the item was used in battle. That distinction shows why “gameplay data” is not one bucket. We have map gameplay data, script data, party data, battle data, item data, and save data. They interact, but they are not interchangeable.

The source packet gives us a nice hint of this interaction in pokefirered/src/pokemon_summary_screen.c chunk_e8e17a42bbf17d46. That file includes pokemon, item, party_menu, battle_main, moves, region map sections, and related UI systems. A summary screen is not a creature by itself. It is a view over creature data, plus commands that may connect to item use, moves, party behavior, or battle context. That is the pattern I want you to internalize: user-visible screens often sit across multiple systems, but the data they display still has an owner.

Now let me ask a check question. In our minimal clone, where should “current HP” live? Not in the map. Not in the script. Not in the save file as an active object. It lives in party data while the game is running. The save system persists it, yes, but persistence is not ownership of behavior. The save system writes and reads durable state. It should not decide whether a potion can heal a fainted creature. That is item and party logic.

And that brings us to save persistence, because save files are where students often accidentally smash every boundary together. A save file is a snapshot of durable facts. It may include player position, current map group and map number, facing direction, party creatures, inventory quantities, money, flags, variables, and perhaps some system settings. It generally should not include transient execution details unless the engine specifically supports saving in that state. For a minimal clone, I would save only at safe points: overworld idle states, not mid-script, not mid-battle. That design choice dramatically simplifies persistence.

If we save on the overworld, what do we need? We need the current map identity, not the whole resolved layout. We need player coordinates and facing. We need event flags and variables. We need the party records. We need inventory records. We need enough progression state to reload the same world condition. On load, we rebuild runtime objects from data. The map loader reads the map record again. It resolves the layout registry again. It instantiates object events again, then hides or modifies them based on saved flags. This is the key phrase: saved state customizes loaded data; it does not replace the data catalog.

Think back to pokefirered/data/maps/map_groups.json chunk_f22eb0b9c9a5d2a4, which the source focus identifies as the link map group. A map group record helps identify maps within a registry. In a clone, your save should point to a stable map ID or group-and-map pair. It should not store “the full JSON of BattleColosseum_2P after I loaded it,” because then every content patch becomes a save migration problem. The durable save says where the player is and what happened. The content database says what that place means.

Now, here is a practical design example for your lab. Suppose your FireRed-like JSON record has a map header with a layout ID, music, region name, map type, and whether running or cycling is allowed. The lab loader does not have to implement music playback. But it should preserve the field in the loaded map header because the runtime contract will need it later. The same goes for object events. You do not need full NPC movement today, but you should parse object ID, sprite ID, x, y, elevation or movement permission fields if present, movement type, and script reference. Why? Because next week, when we make objects meaningful, the loader should already be faithful to the map schema.

Be careful, though, about pretending to implement more than you do. An advanced learner should be honest in the type model. If a field is parsed but unused, call it parsed metadata. If a field changes runtime behavior, test that behavior. For movement permissions, you can test it now: a tile marked blocked should reject movement. A warp tile should transfer the player to another map or at least produce a warp request object. A coordinate trigger should enqueue or start a script. Those are observable runtime effects.

Let’s return to dynamic warps, because they are a perfect case of layout versus gameplay data. A static warp says, “At coordinate x, y, send the player to map A at warp index B.” A dynamic warp says, “At coordinate x, y, send the player to a destination selected by runtime state.” The coordinate remains map data. The possible destination may be script or event data. The final selected destination is runtime state. The layout still only knows about space and passability. In Battle Colosseum-like link rooms, this is especially relevant because the same physical-looking room may serve different connection modes or player counts. The two-player and four-player colosseum layouts in layouts.json, chunks chunk_76c0c20446e9e415 and chunk_4a91aa64203b6d9a, give us a grounded example for thinking about similar spaces that are not necessarily identical gameplay contexts.

Now, pause and test yourself. If I ask you to build a four-player version of a room, what is the first question? It is not “What script do I copy?” The first question is, “Did the spatial substrate change?” If yes, layout may change. If no, maybe only the map definition changes. The second question is, “Did the interaction set change?” If yes, map events and scripts may change. The third question is, “Did the rules of the activity change?” If yes, battle or link systems may change. That order keeps you from solving everything in the map file.

Let me put a small architecture on the board, verbally. MapLoader produces LoadedMap. LoadedMap references Layout. OverworldRuntime owns CurrentMapState. ScriptRunner owns ScriptFrame and instruction pointer. EventData owns flags and variables. PartySystem owns party creatures. InventorySystem owns item stacks. BattleSystem owns active battle state. SaveSystem owns serialization of durable state. Arrows go from ScriptRunner to those systems as commands. Arrows do not go from Layout to BattleSystem. That missing arrow is as important as the arrows we draw.

And yes, real codebases are messier than classroom diagrams. FireRed has historical constraints, generated data, hardware constraints, and C-era global state. We are not pretending it is a dependency-injection textbook example. But the research packet repeatedly supports the architectural reading that FireRed is a stack of cooperating state machines and data tables. The battle setup excerpt in pokefirered/src/battle_setup.c chunk_494d132a59422c85 shows the boundary-heavy include surface. The summary screen excerpt in pokefirered/src/pokemon_summary_screen.c chunk_e8e17a42bbf17d46 shows how UI crosses party, item, move, and battle concepts. The map JSON chunks show map records attaching behavior to spatial records. Our job is to learn from that shape, then build a minimal version we can reason about.

Before we move on, I want to handle a subtle design trap. Some of you will want the script executor to be “data-driven” by making every system behavior a script opcode. So you add opcodes for calculate damage, remove PP, apply poison, check badge obedience, serialize party, and update inventory capacity. That feels flexible for about two days. Then your script VM becomes a second programming language with poor tooling and unclear ownership. Use scripts for sequencing authored events. Use domain systems for domain rules. A good script command is “start battle with trainer X.” A suspicious script command is “for each battler, recompute modified speed unless paralysis flag is set.” That belongs in battle.

The same warning applies in the other direction. Do not bury authored event flow inside C or TypeScript functions if it is really content. If an NPC says different lines before and after a flag, that is scriptable content. If a tile warps differently after a story event, that can be script or event data. If a map object appears only after a variable reaches three, that is map object plus event condition. Keep authored variation in data where reasonable. Keep core rules in systems.

So, in today’s workshop, when you build the minimal map schema loader, I want you to validate the separation by asking one question after every field: who consumes this? Layout ID is consumed by the map loader. Dimensions are consumed by rendering, collision, and bounds checks. Object events are consumed by object spawning and interaction. Warps are consumed by movement resolution. Coordinate triggers are consumed by the trigger scanner after movement. Movement permissions are consumed by collision. Script IDs are consumed by the script runner. Flags are consumed by event data. If you cannot name the consumer, do not guess; mark it as parsed but not yet interpreted.

That is the bridge from week 3 into the rest of the course. This week is not only “maps are JSON records.” It is the beginning of the overworld runtime module. Weeks 3 through 5 are where the static world starts becoming playable. We began with headers and layouts, we are now separating layout data from gameplay data, and next we will make triggers, warps, and objects participate in a frame-by-frame runtime without taking over party, battle, inventory, UI, or save. Keep that mental model ready, because in the next section I am going to take one coordinate trigger from the Colosseum-style map record and walk it all the way through a minimal script runner, including what pauses, what resumes, and what state is allowed to persist.

All right, let’s pick up exactly there. I want one coordinate trigger, not because coordinate triggers are the whole map system, but because they force us to confront the boundary between layout data and gameplay data. A coordinate trigger sits at an interesting seam. It uses map space, so it must agree with the layout’s width, height, and collision surface, but its meaning is not graphical. Its meaning is, “When the player reaches this condition, ask the script or event system to do something.”

Let me put a small diagram on the board. On the left, I’ll write “layout.” Under that: dimensions, block data, tileset references, movement permissions. On the right, I’ll write “gameplay record.” Under that: objects, warps, coordinate triggers, background events, scripts, flags, variables. Now I’ll draw one arrow from gameplay record back to layout, and I’m labeling that arrow “coordinates must be valid.” That is the relationship. The trigger does not own the floor. The trigger points into a floor.

That distinction is visible in our FireRed source focus for today. The BattleColosseum_2P layout is represented in pokefirered/data/layouts/layouts.json, chunk_76c0c20446e9e415, while the BattleColosseum_2P map record is represented separately in pokefirered/data/maps/BattleColosseum_2P/map.json, chunk_919152bd3bfceb95 for the header, objects, and warps, and chunk_237e353ea279eb27 for coordinate triggers. That separation is the architectural fact I want you to preserve in your minimal clone. The loader may read them together for convenience, but the model should not pretend they are the same kind of data.

So here is the minimal clone design. When the player enters a map, the map loader should resolve the map header first. The header says, in effect, “Here is the layout identity, here are the event lists, here are the scripts or script references, here are the attributes the overworld runtime needs.” Then the loader asks the layout registry for the layout by ID. This is the part that corresponds to the layouts.json entries, including the BattleColosseum_2P layout chunk I just named. The map runtime should end up with two connected objects in memory, not one flattened blob: a layout instance, which answers physical questions, and a map event instance, which answers gameplay questions.

Now, suppose our coordinate trigger says, “At x equals 4, y equals 7, if variable V is zero, run script S.” I’m making up the exact coordinate for the example; I am not claiming that is the Colosseum chunk’s exact value. The architectural shape is what matters. The trigger record has a coordinate, a condition, and an action. The layout answers, “Can the player stand on this coordinate?” The event system answers, “Is the trigger active?” The script runner answers, “What happens next?”

Check for understanding here: if I ask you where collision belongs, you should not say “the coordinate trigger.” The trigger may depend on collision being meaningful, but the movement permission belongs with the layout and metatile behavior side of the system. If I ask you where the condition variable belongs, you should not say “the layout.” A variable saying whether a scene has already played is gameplay state. It belongs in the event or save-facing state model.

Now let’s walk the frame. The player presses right. The overworld movement code attempts to move the avatar from one tile to the next. In our minimal clone, that attempt calls something like canMoveTo(map.layout, nextX, nextY). If false, no trigger is considered, because the player did not enter the tile. If true, the player’s position changes. Only after the movement completes should the coordinate trigger scanner run. This order matters. A trigger is not “the player is trying to face this coordinate.” It is “the player has reached this coordinate,” unless you explicitly design a different trigger type.

At that moment, the scanner looks through the coordinate triggers for the current map. In FireRed’s data organization, those triggers live with the map record, and for today’s BattleColosseum_2P source focus, that is pokefirered/data/maps/BattleColosseum_2P/map.json, chunk_237e353ea279eb27. In our clone, the runtime representation might be an array of Trigger structs or objects. Each one has x, y, maybe elevation or layer if your schema includes it, a condition variable, an expected value, and a script identifier.

Here is the first advanced design point: do not execute script text directly from the trigger scanner. The scanner should request script execution. That sounds fussy, but it prevents the overworld from becoming a pile of hidden call stacks. If movement code directly calls trigger code, and trigger code directly calls message UI, and message UI directly mutates player state, your clone will work for one demo and become painful by week 5. Instead, the scanner should enqueue or start a script context, then put the overworld into a controlled state: player input paused, movement task idle or locked, script runner active.

This ties back to week 1 and week 2 without re-teaching them. We already studied the callback and task shell, and we already studied headers as cross-module contracts. Use that foundation. A script runner is not magic. It is just another state machine that advances over frames and cooperates with the overworld shell. FireRed’s broader codebase reinforces this style through many systems. For example, pokefirered/src/battle_setup.c, chunk_494d132a59422c85, includes overworld, item, script, event data, battle, battle transition, and related modules in one place because starting battle is not a single isolated function call. It is a transition across systems. That is the same kind of boundary we are practicing at smaller scale with a coordinate trigger.

Now I’m going to write a tiny script on the board in plain language. “Lock player. Show message: Welcome. Wait for A. Set variable sceneSeen to one. Release player. End.” That is enough script behavior for the lab. You do not need a full bytecode VM yet. You need the architectural posture of a VM: a current instruction pointer, a paused or running state, and a way for instructions to yield until another subsystem finishes.

Let’s act this out. The trigger scanner starts script S. The script runner reads instruction zero: lock player. That completes immediately, so the instruction pointer advances. Instruction one: show message. This does not complete immediately. It opens a text box and yields. The script context says, “I am waiting on message UI.” The overworld main frame continues. The text box animates, accepts input, and eventually reports done. Then the script runner resumes at instruction two or at the continuation after the message instruction, depending on your representation.

This is where many clones accidentally mix gameplay data and UI state. The trigger record should not store “message box currently on line two.” The map should not store that either. The script context stores that it is waiting. The message UI stores its own animation and input state. The save system eventually stores sceneSeen if that variable should persist. Notice the chain: trigger starts script, script sets variable, save persists variable. Each layer has a narrower job than the whole story.

Let me pause and ask: what is allowed to persist after the player leaves the map? The layout instance? No, not as gameplay state. It can be reloaded from data. The trigger definitions? No, also data. The player’s current map and coordinates? Yes, probably. The event variable sceneSeen? Yes, if it changes future behavior. The fact that a text box was halfway through drawing a character? Usually no, unless you are implementing exact suspend-and-resume in the middle of a frame, which we are not doing in the minimal clone.

That answer sets up save persistence. A FireRed-like architecture has large global state and save blocks, but our clone can be smaller. We need a SaveState object with player location, party data, inventory quantities, event flags and variables, and enough world state to reconstruct the scene. It should not contain layout pixels, block arrays, or duplicate object definitions from the map JSON. Save the result of play, not the authored content.

Now, because this course is not only about maps, I want to connect this trigger path forward to party data, battle turns, and item use. Imagine the coordinate trigger starts a trainer battle instead of showing a message. The script instruction might be “start battle with trainer T.” Again, that instruction should not run battle logic inside the map trigger scanner. It should request a transition to battle setup. The FireRed source packet shows this cross-module relationship in pokefirered/src/battle_setup.c, chunk_494d132a59422c85, where battle setup sits near includes for overworld, script, event data, item, and battle transition. That is evidence of the architectural boundary: battle begins from overworld conditions, but battle is its own runtime.

For the minimal clone, party data can be plain but must be real. A Party has up to six Creature instances. Each Creature has species ID, level, current HP, max HP, moves, experience if you are modeling it, status if needed, and maybe held item later. Species data is static. The individual Creature is gameplay state. This is the same distinction again. Species base stats are like layout data in the sense that they are authored data. Current HP is like an event variable in the sense that it changes through play and must persist.

A battle turn then becomes a controlled exchange between state machines. The player chooses a move. The opponent chooses a move. The battle engine orders actions, applies damage, checks fainting, emits messages and animations, then returns either “battle continues” or “battle ended.” FireRed’s battle utilities are large, but the source packet gives us the right lesson from pokefirered/src/battle_util.c, chunk_9335e5727aa0657d: battle behavior draws from constants, moves, items, abilities, weather, event data, random, and battle scripts. The important architectural point is not that your clone should copy all of that. The point is that battle is a data-heavy subsystem with its own rules, and the overworld should not secretly know how to calculate damage.

So, if a coordinate trigger starts a battle, the transition looks like this. Overworld detects trigger. Script runner begins. Script requests battle setup. Overworld yields control to battle mode. Battle mode reads party data and enemy data. Battle mode mutates HP and maybe inventory or flags. Battle mode returns a result. Script resumes and sets defeatedTrainer flag. Overworld unlocks the player. Save persistence later records party HP, inventory changes, event flags, and player location.

That is the course in miniature. Map, script, party, battle, item, save. Each one participates, but each one owns a different category of truth.

Let’s add item use, because item use is a useful stress test. Suppose the player uses a Potion from the bag while standing in the Battle Colosseum map. The map layout should not care. The coordinate triggers should not care unless an event condition explicitly checks party state later. The inventory system decrements the Potion quantity. The party system increases one creature’s current HP, capped at max HP. The UI system displays selection menus and confirmation text. The save system persists the new item quantity and HP. Again, authored item data is separate from gameplay item state. “Potion heals 20 HP” is static item definition data. “The player has three Potions” is save data.

This is why pokefirered/src/pokemon_summary_screen.c, chunk_e8e17a42bbf17d46, is useful as supporting evidence even though we are not teaching the summary screen today. That file’s include surface touches pokemon data, item constants, party menu, battle, moves, region map sections, and UI helpers. A summary screen is a view over gameplay state and static data. It should not own the data model. In your clone, if the party screen becomes the only place that knows how HP is stored, you have inverted the architecture. The screen should ask the party model for HP, not define what HP means.

Let me do a quick concrete pass with names. The player enters BattleColosseum_2P. The map header from pokefirered/data/maps/BattleColosseum_2P/map.json, chunk_919152bd3bfceb95, points to the relevant layout and event lists. The layout registry resolves the BattleColosseum_2P layout from pokefirered/data/layouts/layouts.json, chunk_76c0c20446e9e415. The map group information in pokefirered/data/maps/map_groups.json, chunk_f22eb0b9c9a5d2a4, is part of how the authored world organizes maps into groups, especially for link-related map grouping in this week’s source focus. The runtime loads the layout dimensions and movement permissions. It also loads object events, warps, and coordinate triggers. Then the avatar moves. On a specific coordinate, the trigger list from chunk_237e353ea279eb27 is scanned. If a matching trigger passes its condition, script execution begins.

Now, here is the check I would put on an exam for advanced students. “A student builds a map loader that copies trigger records into each tile of the collision grid. Is that good architecture?” The answer is: probably not, unless the engine has a deliberate spatial index abstraction. If they are doing it because “the trigger is on a tile, so it belongs inside the tile,” they have confused coordinate reference with ownership. A spatial index can be a runtime optimization. It must not erase the distinction between physical layout and gameplay event definitions.

That phrase, runtime optimization, is important. You may absolutely build a trigger lookup table keyed by coordinate. In fact, for a large map, that is reasonable. But the source of truth should remain the trigger list from the map event data. The lookup table is derived. Derived runtime structures can be thrown away and rebuilt. Save files should not store them. Designers should not author them directly unless your tools intentionally expose that format.

Let’s now connect this to warps. A warp is another gameplay event that references a coordinate. It says, “When the player enters this tile or activates this transition, move to another map and position.” The BattleColosseum_2P map record’s header, objects, and warps are in pokefirered/data/maps/BattleColosseum_2P/map.json, chunk_919152bd3bfceb95. A warp needs the layout for coordinate validity, but it is not layout. The tile may look like a doorway, but the tile graphic does not decide the destination. The warp event decides the destination. That is how the same visual doorway concept can lead to different maps in different contexts.

Dynamic warps make the separation even more obvious. A dynamic warp may change destination based on variables or runtime state. You cannot put that into static collision without distorting the model. The movement grid can say, “This tile is passable.” The warp system says, “This passable tile transfers the player to map group M, map N, coordinate X Y, possibly resolved through runtime state.” If you try to bake that into layout, you make layout depend on story progression, which is exactly what we are trying to avoid.

Let’s step back for a recap. Layout data tells the engine what the place physically is. Gameplay data tells the engine what can happen there. Runtime state tells the engine what has already happened and what is happening right now. Persistence records the subset of runtime state that must survive reset or reload. If you can classify every field in your clone into one of those categories, your design will stay much cleaner.

Now, because the lab asks you to build a minimal map schema loader, I want you to keep the first implementation modest. Read the layout ID. Read dimensions. Read movement permissions. Read object events. Read warps. Read coordinate triggers. Validate coordinates against dimensions. Do not implement a full scripting language on the first pass. A script can be an identifier mapped to a small list of commands in your code. What matters is that the loader does not interpret every command while parsing. Parsing says, “I found a trigger with script S.” Runtime says, “The player stepped here; should S run?” Script execution says, “Command one is message; command two sets flag.” Save says, “This flag now persists.”

And I want you to notice how this prepares us for weeks 4 and 5. We are still in the overworld module, but we are already touching the script VM, party model, battle entry, inventory effects, and save state. That does not mean the map loader should import everything. It means the map loader sits near the front door of gameplay. The player experiences a continuous world, but the architecture is a set of contracts between specialized systems.

One more check for understanding before we move on. If a trigger gives the player an item, which systems are involved? The trigger system detects location. The script system runs the command. The inventory system adds the item quantity. The message or fanfare UI may display feedback. The save system persists the inventory result. The layout system is involved only to the extent that the coordinate was meaningful and reachable. It does not know what the item is.

Good. In the next movement of the lecture, I am going to turn this into a small end-to-end pseudo-runtime. We will sketch the data structures first, then walk one frame where input, movement, trigger detection, script yielding, and save-relevant mutation all happen in order. Keep the categories on the board in mind: authored layout, authored gameplay event, transient runtime context, persistent save state.

So now I am going to draw the small runtime. Not FireRed’s whole runtime, and not a modern engine either. A clone-sized version. The purpose is to preserve the boundaries that FireRed teaches us, while making the moving parts small enough that you could implement them in a lab without needing the entire GBA engine around it.

On the board, I am writing four boxes from left to right. First, MapRecord. Second, LayoutRecord. Third, MapRuntime. Fourth, SaveState. Under MapRecord I am putting map type, layout id, object events, warp events, coordinate triggers, and background events. Under LayoutRecord I am putting width, height, border, block data, and permission data. Under MapRuntime I am putting player position, loaded metatile grid, active objects, pending script, and transition state. Under SaveState I am putting party, bag, flags, variables, location, and story progress.

Now pause there, because that board layout is the architecture lesson. The map record says, “This room has these gameplay hooks.” The layout record says, “This room has this physical shape.” The runtime says, “This is what is currently happening on screen.” The save state says, “This is what must survive power-off.” Those are not interchangeable categories.

This is exactly why the Battle Colosseum examples are useful even though they are not a normal forest or town route. In pokefirered/data/layouts/layouts.json, chunk_76c0c20446e9e415 gives us a BattleColosseum_2P layout, and chunk_4a91aa64203b6d9a gives us the BattleColosseum_4P layout. The two layouts are authored layout records. They are about spatial data. In contrast, pokefirered/data/maps/BattleColosseum_2P/map.json, chunk_919152bd3bfceb95, gives us the map header material, object events, and warps for the two-player colosseum map. Then chunk_237e353ea279eb27 gives us coordinate triggers. That is gameplay data attached to a map. The distinction is not academic. It determines which loader owns the data, when the data is interpreted, and whether the data belongs in the save file.

Let’s walk one frame in our clone runtime.

Imagine the player is standing at coordinate x equals 5, y equals 8. The current map record points to layout id BattleColosseum_2P. The layout loader has already resolved that id through the layout registry, loaded dimensions, block data, and movement permissions. The map loader has already created object instances from object event records. It has loaded warp definitions, coordinate triggers, and background event definitions. The save state says the player has a party of six, a bag with three Potions, a set of story flags, and the current location.

Now the player presses down. First, the input system produces an intention: move south. Second, the movement system asks the loaded layout permission grid whether the destination tile is passable. Notice the wording. The movement system does not ask the script system whether dirt is walkable. It does not ask the bag. It does not ask the battle system. It asks the movement permission data derived from the layout. If the destination tile is blocked, the avatar may bump and the frame ends with no coordinate trigger checked for a new tile. If the tile is passable, the runtime begins movement.

In a GBA-style engine, that movement may take several frames. We are not going to re-teach tasks and callbacks today, but connect it back to what we already know: per-frame progress is managed by lightweight state machines, and systems advance one small piece at a time. In our clone, we can say MapRuntime has a movement state: idle, stepping, or locked. While stepping, the player is visually between tiles. When the step completes, the runtime commits the new coordinate to player position.

Now we check triggers. This order matters. Do not fire a coordinate trigger merely because the player intends to move. Fire it when the player has arrived on the coordinate, unless your design explicitly says otherwise. FireRed’s map records separate coordinate triggers from layout data, and that distinction is visible in the BattleColosseum_2P map data. The coordinate trigger chunk, pokefirered/data/maps/BattleColosseum_2P/map.json chunk_237e353ea279eb27, is not a block collision table. It is a set of authored event hooks. The layout tells you that the tile exists and may be reachable. The trigger says, “When the player reaches this authored location, consider running this script.”

Let me make that concrete. Suppose tile 5, 9 has a coordinate trigger called Script_GiveEntryBadge. In our clone runtime, the trigger detector finds a trigger at the player’s new coordinate. It asks a small condition evaluator, “Is this trigger enabled?” Maybe the condition checks a flag named FLAG_ALREADY_RECEIVED_ENTRY_BADGE. If that flag is false, the runtime starts a script. Starting a script means we lock player control, set MapRuntime.pendingScript to a script pointer or script id, and let the script VM begin executing.

Now here is the temptation in student projects: put the item directly in the trigger record. Something like, “trigger at 5, 9 gives item 42.” That is fine for a toy prototype, but it collapses categories too early. A more FireRed-shaped design makes the trigger point to a script, and the script performs commands. Why? Because today the trigger gives an item, tomorrow it checks party composition, opens a message box, starts a battle, moves an NPC, or warps the player. The trigger is not the behavior. The trigger is the entry point into behavior.

That script might execute command one: lock player. Command two: show message, “Please take this.” Command three: add item Potion quantity one. Command four: set flag already received. Command five: wait for fanfare. Command six: release player. Even in this small sequence, we crossed several systems. The script VM owns sequencing. The text UI owns the message. The inventory system owns item quantities. The event flag table records progression. The map runtime owns player locking. The save system later persists the bag and flag mutation.

Check for understanding. If I change the visual floor tile under coordinate 5, 9 from a metal tile to a carpet tile, should the script change? Usually no. If I move the trigger from 5, 9 to 6, 9, should the layout block data change? Usually no. If the item is now a Super Potion instead of a Potion, should the movement permission grid change? Definitely no. These answers are the practical payoff of the separation.

Now let’s extend the same tiny runtime into party data and battle turns, because week 3 is not only about maps in isolation. It is about how maps become the staging area for the rest of the RPG.

Suppose the coordinate trigger starts a trainer battle instead of giving an item. The trigger still only detects location. The script still sequences behavior. But one script command now says, “start battle with trainer id 17.” At that boundary, the overworld runtime prepares battle setup data and hands control to the battle system. In the source packet, pokefirered/src/battle_setup.c chunk_494d132a59422c85 is useful evidence here because the includes alone show this module sitting at a crossroads: overworld, item, script, event data, field effects, battle, battle transition, and related systems are all in scope there. That does not mean every module owns every other module. It means battle setup is an integration boundary. It gathers enough context from the field side to launch the battle side.

For our clone, we can keep this minimal. BattleStartRequest has opponent kind, opponent id, battle type, terrain hint, and return location. PartyData has up to six creatures, each with species, level, current HP, max HP, moves, status, held item, and experience. BattleRuntime has active battlers, selected actions, turn order, temporary stat stages, volatile effects, and outcome. SaveState owns the durable party records. BattleRuntime owns the temporary combat situation.

That last sentence is another board-worthy rule: party is persistent, battle runtime is transient, but battle results may mutate persistent party. During battle, if a creature takes damage, the battle runtime needs current HP. At the end of the turn, or immediately depending on your architecture, that HP change must be reflected back to party data if it persists after battle. If a creature uses Growl and lowers an enemy’s attack stage, that stat stage is battle runtime only. It should not be written into the saved party record. If a creature gains experience and levels up, that is persistent party mutation. If poison remains after battle in your ruleset, that status is persistent. If confusion clears after battle, that is transient. Your architecture follows the rules of the game.

FireRed’s battle side is much larger than our clone, of course, but the packet points us toward the right separation. pokefirered/src/battle_util.c chunk_9335e5727aa0657d is full of battle-facing dependencies: battle scripts, battle controllers, constants for moves, items, abilities, weather, hold effects, and move effects. That is battle-domain machinery. It is not where map layout dimensions belong. Conversely, the map layout records should not know how critical hits are calculated. Advanced architecture is largely the discipline of letting each system be complicated in its own place.

Let’s do one complete battle turn in the clone.

The player chooses Tackle. The opponent chooses Scratch. The battle system reads the active creatures’ speed values from battle-visible creature stats. It determines order. It executes the first move. The move command references move data: base power, accuracy, type, effect id. The damage calculator uses attacker stats, defender stats, move power, type relation, and random variation. The result subtracts HP from the defender. If HP reaches zero, the battle runtime marks the defender fainted and schedules fainting messages and replacement logic. If the battle ends, the battle system returns an outcome: win, lose, flee, or special.

Where is the map during this? Mostly suspended. The overworld is not interpreting coordinate triggers while the battle menu is open. The current map identity and return position still matter, but they sit in context. That is why our earlier callback and task material matters as prior foundation. Control has shifted from one runtime mode to another. We do not need to re-open the whole callback discussion, but we do need to remember that a creature RPG is a stack of cooperating state machines, not one giant loop where every system is equally active every frame.

Now we add item use.

There are two item-use cases I want you to distinguish. First, field item use, such as using a Potion from the bag while standing on the overworld. Second, battle item use, such as using a Potion from the battle menu. Same item, different context. The bag data is persistent in both cases. The item effect may be shared. But the legal targets, UI flow, and timing rules differ.

In field use, the player opens the menu, selects Bag, selects Potion, selects a party member, and confirms. The item system checks whether the item can be used in the field. The party system checks whether the target creature can receive the effect. The item effect restores HP up to max HP. The bag decrements quantity. The UI shows feedback. The save state is now dirty because party HP and bag quantity changed.

In battle use, the player’s selected action for the turn may be “use item.” The item may consume the player’s turn. The battle controller must fit that action into turn resolution. The Potion still restores HP and decrements bag quantity, but the timing belongs to battle. That is why, in a clone architecture, I would not put “Potion.use()” as a single context-free method that blindly mutates everything. I would define an item effect that can be invoked by field-use and battle-use workflows, with each workflow responsible for legality, target selection, turn cost, animation, and messaging.

The source packet supports this view indirectly through module boundaries. pokefirered/src/pokemon_summary_screen.c chunk_e8e17a42bbf17d46 includes party menu, item, battle main, moves, region map, field specials, and constants. A summary screen is a UI surface over persistent creature data, but it is aware of contexts that can open or constrain it. pokefirered/src/daycare.c chunk_119931105f546346 similarly touches data, battle, items, storage, event data, script, party menu, overworld, Pokédex, naming, and trade. These are not layout systems. They are gameplay systems that consume and mutate persistent creature-related records through controlled workflows.

Now let’s connect save persistence, because this is where many clone projects either oversave or undersave.

SaveState should not contain the loaded block grid. It should not contain the entire LayoutRecord. It should not contain every inactive trigger copied from the map JSON. Those are authored data. You can reload them from content. SaveState should contain what has changed because of play. Player location. Facing direction, if you need it. Party creatures. Bag quantities. Money. Flags. Variables. Current quest state. Maybe object local state if the game supports persistent moved objects. Maybe respawn timers. But the default is: authored data stays in the content database; player-caused durable changes go in save.

So, in our trigger example, after the player receives the item, what is saved? Not “trigger 5, 9 exists.” That was already in the map data. We save the bag quantity and the flag that prevents repeated collection. If we later reload the game, the map loader again sees the trigger at 5, 9. The condition evaluator sees FLAG_ALREADY_RECEIVED_ENTRY_BADGE is true, so the script does not run or takes a different branch. That is the elegant pattern. Persistent flags specialize reusable authored maps for a particular playthrough.

Let me ask another check for understanding. A warp from the colosseum back to a lobby exists in map data. The player steps on it, and the game transfers maps. What is saved? The warp definition itself? No. The new player location may be saved if the game saves after transfer or if the player saves manually there. Story variables affected by the transfer may be saved. But the authored warp event remains content. In the packet, pokefirered/data/maps/BattleColosseum_2P/map.json chunk_919152bd3bfceb95 is where we point for the map’s header, objects, and warps. The group organization in pokefirered/data/maps/map_groups.json chunk_f22eb0b9c9a5d2a4 tells us maps are organized into groups, including link-oriented maps. That grouping is content organization and lookup structure, not per-player history.

Now I want to put all six minimal clone systems into one flow, because this is the moment where week 3 touches the rest of the course.

The player loads a save. Save persistence gives us player location, party, bag, flags, and variables. The overworld map loader uses the saved location to find the map group and map number, then loads the MapRecord. The MapRecord references a LayoutRecord. The layout loader builds the collision and visual base. The map loader instantiates objects, warps, coordinate triggers, and background events. The player walks. Movement consults layout permissions. Trigger detection consults map gameplay events. Script execution sequences commands. A command may use an item, set a flag, start a battle, warp the player, or update an object. Battle turns operate on battle runtime and party data. Item use mutates bag and party through context-specific rules. Save persistence records only durable results.

That paragraph is the architecture of the course in miniature. Weeks 1 and 2 gave us the runtime shell, callbacks, tasks, headers, generated data, and source organization. Week 3 says, “Now put real authored world data into that shell.” Weeks 4 and 5 will keep deepening the overworld: scripts, progression, and the field systems that make maps feel alive rather than merely loaded. Later, when we reach creature data, battle, inventory, UI, and save, we will keep returning to this same discipline: authored data, runtime state, persistent state, and integration boundaries.

I am going to underline integration boundaries on the board, because advanced students often make the opposite mistake from beginners. Beginners sometimes put everything in one object because it is simple. Advanced students sometimes overcorrect and make every system pure, isolated, and unable to accomplish a real game action without ceremony. FireRed is not pure in that academic sense. The source packet shows practical integration modules. battle_setup.c chunk_494d132a59422c85 is an integration module. Summary and daycare screens touch many systems because their job is to coordinate gameplay records and UI flows. The trick is not to avoid dependencies entirely. The trick is to make dependencies point in meaningful directions.

A map layout depending on battle damage would be nonsense. A battle setup module depending on overworld context is meaningful. A script command invoking item addition is meaningful. A low-level item quantity table trying to move the player through a warp would be nonsense. Direction matters.

For your lab implementation, I would expect a minimal loader to read a FireRed-like JSON record with a layout id, dimensions through the layout reference, object events, warp events, coordinate triggers, and movement permissions. But I would also expect you to avoid turning the JSON into your runtime wholesale. Parse it into content records. Then construct runtime instances. An object event record might say, “NPC template id, local id, starting x, starting y, movement type, script.” The runtime object instance says, “current x, current y, current animation frame, current movement state, locked or not.” The save state may only say, “object local id 3 has disappeared because flag X is set,” or maybe nothing at all if the object can be regenerated from flags.

Before we leave this segment, let’s run one final quick scenario.

A player enters BattleColosseum_2P. The map group lookup finds the map. The map header points to the layout. The layout dimensions and movement permissions are loaded from the layout registry, represented by the layouts.json chunks we cited earlier. The map’s object events and warps come from the map.json header chunk. The coordinate triggers come from the coordinate trigger chunk. The player steps onto a special tile. The trigger starts a script. The script checks party eligibility for a link battle-like interaction. If valid, it transitions into battle setup. Battle setup builds the battle request from overworld and party state. The battle system runs turns. Afterward, the result updates party HP, maybe records a flag or variable, and returns to the map. The save system persists the changed party and flags. At no point did the layout record learn what a turn order is. At no point did the battle system parse the whole map JSON. That is the design.

So, check your mental model. When I say “layout,” you should hear physical and visual arrangement. When I say “map record,” you should hear authored gameplay hooks attached to a place. When I say “runtime,” you should hear currently executing state. When I say “save,” you should hear durable player-specific mutation. If those four categories stay clean, your clone can grow. If they blur too early, every new feature becomes a special case. In the next part, we will take this same model and look more closely at dynamic warps and background events, because those are where the clean categories start to feel pressure from real game design.

All right, so let’s put dynamic warps under the microscope, because this is one of those places where beginners often say, “Ah, the map data changed,” when actually the runtime interpretation changed.

Imagine I draw two boxes on the board. On the left, I write “authored warp event.” On the right, I write “resolved destination.” Those are not the same thing. The authored warp event is the thing a designer or tool emits into the map record: at coordinate x, y, when the player steps here, transfer them somewhere. The resolved destination is what the engine actually chooses at that moment, after it looks at the current state of the game. In FireRed-style architecture, that distinction matters because the same physical tile can sometimes mean different travel behavior depending on flags, scripts, link state, or special map context.

Now, in the BattleColosseum material we have been using, the distinction is intentionally small but visible. The layout registry entries in pokefirered/data/layouts/layouts.json, chunk_76c0c20446e9e415 for BattleColosseum_2P and chunk_4a91aa64203b6d9a for BattleColosseum_4P, give us layout identity and physical arrangement. Then the actual BattleColosseum_2P map record in pokefirered/data/maps/BattleColosseum_2P/map.json, chunk_919152bd3bfceb95, gives us the map header, object events, and warp events. That is our separation again. The floor plan comes from the layout side. The gameplay hooks come from the map side.

Let me say that in clone terms. If you are building the minimal map schema loader for this week’s workshop, your loader should not say, “Load layout, then bake all future gameplay behavior into a tile array.” That is tempting, because it feels efficient. Instead, it should say, “Load layout geometry. Load gameplay event records beside it. At runtime, when the avatar attempts to step or interact, query the appropriate event list.” That one design choice keeps you from hard-coding doors, signs, NPCs, scripted spots, and battle triggers into the same blob.

Look at the board for a second. I’m going to write the clone pipeline as four verbs: load, place, query, dispatch. Load the map header and the layout reference. Place the player and object events into runtime structures. Query movement permissions and event tables during player action. Dispatch scripts, warps, or interactions when a condition matches. That is enough for a serious minimal clone. Notice what is not there. We are not loading a battle engine from the map. We are not loading inventory rules from the layout. We are not storing party HP in the warp definition. We are letting each system own its own kind of state.

Now, dynamic warps put pressure on that clean model because they ask, “What if the destination is not constant?” A simple warp says, “Step on tile 4, 7, go to map A at warp 2.” A dynamic warp says, “Step here, and the current game state tells us where this goes.” In a link room, a trade center, or a scripted facility, this is very useful. You may enter through a generic door, but the actual return destination might be where you came from, or a link-session staging room, or a special map selected by some previous script. That destination does not belong in the layout. It also should not be randomly recalculated by the renderer. It belongs in gameplay state, usually controlled by scripts and save or temporary variables.

Check for understanding here. If the player enters a building from Route 1, and the exit should take them back to Route 1, where does that return information live? Not in the tileset. Not in the metatile art. Not in the static collision data. In a minimal clone, it could live in a current-session field like previousMap and previousPosition, or in a special dynamicWarp structure. If it must persist across save and reload, then it belongs in save-backed state. The exact implementation can vary, but the category cannot.

This is where our week 1 and week 2 material becomes useful without needing to re-teach it. We already have the runtime shell idea: the game is always in some callback-driven mode. We already have the task idea: small state machines can advance transitions over frames. We already have the generated-data idea: JSON-like authoring records can become compiled data. Today we are asking where the boundaries are once the overworld starts using those records.

Let’s walk a concrete example. Suppose our clone has a map called DuelLobby. It has a layout ID, width, height, border tiles, block data, and a permission grid. The layout says there is a doorway tile at coordinate 5, 10. The map record says there is a warp event at 5, 10. But the destination is marked dynamic. When the player steps onto 5, 10, the overworld runtime does not ask the layout, “Where should this go?” It asks the warp handler, “Is this warp static or dynamic?” If static, read the destination fields. If dynamic, consult runtime state. Maybe dynamicWarp.destinationMap equals LastOutdoorMap. Maybe it equals LinkRoomExit. Maybe it is invalid, and the script layer must initialize it first.

That invalid case is important. Advanced architecture is not just drawing clean boxes. It is knowing what failure looks like. A dynamic warp without initialized runtime state is not a layout error. It is a gameplay-state error. So your debug message should not say, “bad tile.” It should say, “dynamic warp destination missing for map DuelLobby warp 0.” This tells the designer or programmer which system failed to supply its contract.

Now let’s bring in coordinate triggers, because they are the sibling of warps. The BattleColosseum_2P coordinate trigger chunk, pokefirered/data/maps/BattleColosseum_2P/map.json, chunk_237e353ea279eb27, is relevant because it shows that authored behavior can be attached to position without being a door. A warp is position plus transfer. A coordinate trigger is position plus condition plus script. In many RPGs, these are where tutorials, cutscenes, trainer confrontations, puzzle switches, and room-state transitions live.

So I’m going to erase “warp only” from the board and replace it with “position event.” Under that, I’ll put three cases: movement permission, coordinate trigger, warp. Movement permission answers, “Can the avatar step here?” Coordinate trigger answers, “After stepping here, should something execute?” Warp answers, “Should stepping here transfer the avatar to a new map?” They all touch coordinates, but they are not the same data.

This is one of the most valuable habits you can build as an engine designer: do not collapse things just because they share an index. Many systems share x and y. Collision, encounter zones, signs, NPC activation, warps, cutscenes, ledges, surf tiles, and camera behavior all refer to map position. But sharing coordinates does not mean sharing responsibility. The position is a join key, not proof that the data should be merged.

In your minimal loader, then, I would expect separate runtime collections. One structure for layout dimensions and block permissions. One list for object events. One list for warp events. One list for coordinate triggers. One list for background events, if your clone includes signs or hidden items. The map header is allowed to reference all of these. The map runtime is allowed to query all of these. But the records remain separable.

Now background events deserve a few minutes, because they are easy to underestimate. A background event is not usually triggered by entering a tile. It is usually triggered by interacting with a tile, often by pressing the action button while facing something. Think of a signpost, a bookshelf, a hidden item, a machine, or a decorative object with text. From a player’s perspective, it looks like “the map has text.” From an architecture perspective, it is an interaction query: take the avatar’s facing direction, compute the target coordinate, search background events at that coordinate, and if one matches, dispatch a script.

Let me make that concrete. The avatar is standing at 3, 5 facing north. The player presses A. The runtime computes target coordinate 3, 4. It checks object events first, perhaps, because NPCs have priority. If no object interaction consumes the action, it checks background events at 3, 4. If there is a sign event, it starts the script “ShowSignText_WelcomeToLobby.” The script system owns the text command and the message box progression. The map system only identified the hook. That is the boundary.

And notice how that boundary matches source organization. The research packet points us to FireRed modules such as pokefirered/src/battle_setup.c, chunk_494d132a59422c85, where battle setup includes dependencies on overworld, script, event data, field movement, field map, and battle modules. That include pattern is not random. It reflects a real crossing point: the overworld and script systems can initiate battle, but battle remains its own subsystem. Similarly, pokefirered/src/battle_util.c, chunk_9335e5727aa0657d, pulls in battle constants, move constants, item constants, abilities, hold effects, and battle scripts. Once you are inside battle resolution, the battle engine needs battle-shaped data, not the entire map record.

So let’s tie that back to the objective for today: minimal clone versions of map loading, script execution, party data, battle turns, item use, and save persistence. The point is not to build all of FireRed in one lab. The point is to define the contracts narrowly enough that each minimal system can work.

For map loading, the minimal contract is: given a map ID, load its header, resolve its layout ID, expose dimensions, movement permissions, object events, warps, coordinate triggers, and background events. For script execution, the minimal contract is: given a script ID, run commands that can read and write variables and flags, display text, move objects, start battle, give items, and warp the player. For party data, the minimal contract is: maintain a list of creatures with species, level, current HP, max HP, moves, status, and maybe held item. For battle turns, the minimal contract is: accept combatants, choose actions, determine order, apply move effects, update HP and status, and return an outcome. For item use, the minimal contract is: check inventory count, check target validity, apply an effect, and decrement if appropriate. For save persistence, the minimal contract is: serialize durable player state, including location, flags, variables, party, inventory, and progress.

Now I want you to hear the architectural rhythm there. Map loading can start script execution. Script execution can start battle. Battle can modify party data. Item use can modify party data or flags. Save persistence can store the durable portions of all of those. But none of those systems becomes all of the others.

Here is a classic mistake. A student writes a coordinate trigger called “start rival battle.” Inside the map loader, when it sees that coordinate trigger, it directly creates the rival’s party, calculates turn order, plays the battle, grants money, sets the defeated flag, and returns the player to overworld. That works for one demo. It fails as architecture. The map loader should not know how to calculate damage. The coordinate trigger should point to a script. The script can say, “if flag rival defeated is false, start trainer battle rival one.” Then the battle setup layer prepares the battle, and the battle system resolves it. Afterward, the script or battle result handler sets the flag.

In FireRed terms, that kind of crossing between overworld and battle is exactly why source files like pokefirered/src/battle_setup.c, chunk_494d132a59422c85, are so interesting for this course. It is not just “battle code.” It is boundary code. It includes overworld and script-facing pieces because battle has to be entered from field context. But boundary code is different from smearing all behavior into map records. Boundary code exists to translate one subsystem’s request into another subsystem’s setup.

Let’s do a check. I’ll say a feature, and you decide which system owns the durable truth. “The player picked up a hidden item from a trash can.” What owns the fact that it was picked up? The save system stores it, but the gameplay variable is likely a flag. The background event is just the authored hook. The item system owns what the item does when acquired or used. The map layout owns none of it. Another one: “The player’s lead creature has 12 HP after a battle.” That is party data, persisted by save. It is not map data, even if the battle began on a coordinate trigger. One more: “The tile at 8, 3 blocks movement because it is a wall.” That belongs to layout or movement permissions, depending on how your clone splits metatile behavior and collision. It is not a script flag unless you are implementing a door, bridge, or obstacle that can change.

Now, that last case is where things get subtle. What about a boulder that moves? What about a cut tree that disappears? What about a door that opens? The physical map seems to change. Do we mutate the layout? Maybe at runtime, yes, we update a runtime tilemap or object list. But the authored layout record still remains the base layout. The save stores the durable mutation as a flag or variable, and on map load the script or map initialization logic applies the change. This is the difference between base data and effective runtime state.

I’ll write those two phrases on the board: base data and effective state. Base data is what the ROM or content files say before player history is considered. Effective state is what the player experiences after applying flags, variables, temporary runtime values, object visibility, and script changes. A strong engine can explain every visible behavior as base data plus state plus execution.

This also gives you a debugging method. If the player sees the wrong thing on a map, ask three questions. Is the base data wrong? Is the saved or temporary state wrong? Is the execution logic interpreting them incorrectly? Those are different bugs. A misplaced wall in the layout is base data. A defeated trainer reappearing is probably state or flag logic. A sign that only works when facing the wrong direction is likely interaction query logic. A warp that sends you to the wrong return map may be dynamic warp state or script initialization.

Let’s connect briefly to map groups. The packet points us to pokefirered/data/maps/map_groups.json, chunk_f22eb0b9c9a5d2a4, for the link map group. Grouping maps is another form of data organization that should not be confused with runtime ownership. A map group helps identify and organize maps, sometimes compacting references or supporting engine lookup. It does not mean all maps in that group share one script state or one layout. For a clone, you can simplify this. You might use string IDs at first, like “BattleColosseum_2P,” but you should still understand what the original architecture gains from registries and grouping: stable references, compact identifiers, and tool-friendly organization.

Now let’s turn that into a minimal implementation sketch, spoken rather than coded. The map manager has a function loadMap(mapId). It looks up a map record. The map record contains a layoutId. The layout registry resolves layoutId into dimensions and block data. The map manager initializes runtime object instances from object event definitions. It keeps warp definitions and coordinate triggers as event tables. Then the overworld update loop handles input. When movement is attempted, it checks bounds, collision, object occupancy, and special movement rules. If movement succeeds, it updates player position. After movement completes, it checks coordinate triggers and warps. If the player presses the action button, it checks object interaction and background events.

The script runner is separate. It has a program counter, a current script, and access to command handlers. Some commands are pure presentation, like show text. Some commands read or write event flags. Some commands call services, like giveItem or startBattle. Some commands suspend the script until another state machine finishes. That last point should sound familiar from our earlier callback and task discussions. We are not re-teaching them, but we are using the same idea: a long player-facing sequence is usually broken into small resumable steps.

The party model is also separate. It does not know that a creature is standing in Viridian Forest or in a colosseum. It knows species, level, stats, moves, current HP, status, experience, and whatever else your scope demands. The summary screen source excerpt, pokefirered/src/pokemon_summary_screen.c, chunk_e8e17a42bbf17d46, is useful here because it shows how many systems can inspect party-shaped data: menu helpers, items, moves, battle constants, region map sections, party menu, and more. A party member is a shared domain object. That does not mean every UI owns it. The party data model owns it; UI and battle read or mutate it through rules.

Battle turns then operate on battle participants derived from party data. In a minimal clone, the battle system may copy enough data into battler slots to resolve speed, moves, HP, and status. At the end, it writes appropriate changes back to party members. This prevents battle-only transient fields from polluting the permanent party model. For example, a volatile condition like flinch or a one-turn charging state should not become permanent save data. Current HP should. Status may. Stat stage boosts usually should not after battle. This is exactly the kind of detail advanced students need to get right, because bad persistence boundaries create strange long-term bugs.

Item use is another pressure point. Suppose a potion is used from the bag in the overworld. The item system checks that the item exists in inventory. It asks whether the selected party member is a valid target. It applies healing up to max HP. It decrements inventory. It may trigger a message. Now suppose a potion is used in battle. Same item data, similar effect, different execution context. The battle system may restrict timing, consume the turn, trigger animations, and cause opponent action afterward. Do not solve that by creating two unrelated potion definitions. Solve it by separating item definition, item effect, and context-specific use rules.

Save persistence wraps around all of this. It is the durable projection of player progress. Not every runtime variable should be saved. The current script program counter usually should not be saved unless the game explicitly supports saving mid-script. A transition fade counter should not be saved. A temporary menu cursor may or may not be saved depending on UI design, but usually not. Location, party, inventory, flags, variables, money, badges, and certain world mutations should be saved. Dynamic warp state may be saved if the game can be saved before that warp is consumed; otherwise it can remain temporary.

Let me pause there and ask a more advanced check. Why is save persistence not simply “dump all memory”? In an emulator or save-state system, sure, you can snapshot everything. But in a game’s designed save file, dumping everything couples persistence to implementation details. If you later change your task layout, script runner internals, or battle temporary fields, old saves break or produce nonsense. A designed save schema says, “These are the facts about the player’s durable progress.” That is why save belongs in our architecture discussion today. It is the other side of the layout distinction. Layout is durable world base data. Save is durable player-specific mutation.

So, if you are designing your week 3 lab with ambition, here is the standard I would hold you to. Your map loader should be able to read a FireRed-like JSON record with layout ID, dimensions, object events, warps, coordinate triggers, and movement permissions. But your test cases should not stop at “it parsed.” Test one map where the same layout is used with different event hooks. Test one coordinate trigger that dispatches a script. Test one background interaction that displays text or gives an item. Test one warp that changes the current map. If you include dynamic warp, test that the destination comes from runtime state and that missing state produces a clear error.

And now we can see why the BattleColosseum examples are pedagogically useful even though they are not the flashiest maps in the game. The 2P and 4P layouts in pokefirered/data/layouts/layouts.json, chunk_76c0c20446e9e415 and chunk_4a91aa64203b6d9a, let us compare similar spaces with different intended use. The BattleColosseum_2P map record in pokefirered/data/maps/BattleColosseum_2P/map.json, chunk_919152bd3bfceb95, anchors that layout to gameplay records such as objects and warps. The coordinate trigger chunk, chunk_237e353ea279eb27, reminds us that not all position-based behavior is travel. And the map group entry in pokefirered/data/maps/map_groups.json, chunk_f22eb0b9c9a5d2a4, reminds us that maps also participate in registries and group-level organization.

Before we move on, let’s synthesize this segment in one sentence. Layout data answers what the place is physically; map gameplay data answers what authored hooks exist there; runtime state answers what is happening now; save data answers what player-specific facts must survive. Keep those four statements separate, and your clone can add doors, signs, battles, items, party changes, and persistence without turning the map file into a junk drawer.

All right, next we are going to push one level deeper into script execution, because scripts are the bridge that make these clean data categories feel like a living world. They are also where architecture can quietly go wrong: if the script VM becomes too powerful, it eats the engine; if it is too weak, every special case leaks into C-style native code or hard-coded clone logic. So keep the BattleColosseum records in mind, keep dynamic warps in mind, and now shift your attention from “what data is attached to a coordinate?” to “what is allowed to happen when that data dispatches behavior?”

Good. So, imagine the player steps onto a coordinate trigger in the two-player Battle Colosseum. The coordinate trigger is not the event. It is the lookup key that says, “When avatar reaches this tile, ask the script system whether anything should run.” That distinction is small, but it is the difference between an engine you can extend and an engine that becomes a pile of coordinate exceptions.

On the board, I’m going to write a very small chain: map record, trigger record, script pointer or script id, script interpreter, engine services. That last phrase, engine services, is doing a lot of work. A script should not own the party model. It should not own the battle loop. It should not own the save format. It asks those systems to do things through narrow operations.

So for your minimal clone, I would not start by building a full bytecode VM. That is too much machinery too early. I would start with a tiny command list. Something like: lock_player, show_message, check_flag, set_flag, warp, give_item, start_battle, heal_party, end. The important part is not the syntax. The important part is that each command crosses into a different gameplay subsystem through an intentional boundary.

Let’s make that concrete. Suppose a colosseum attendant object event faces the player and starts a link battle setup. The object event belongs in the map file because it is spatial and presentational. The attendant’s script id can belong there too. But once the script says “start battle,” the map runtime should hand off to a battle setup service, not build a battle itself. That is exactly the architectural shape we see hinted at in FireRed’s source boundaries: battle setup pulls in overworld, script, event data, item, Pokémon, and battle transition facilities in pokefirered/src/battle_setup.c, chunk_494d132a59422c85. The include list itself is evidence of an orchestration layer. It is not just one monolithic “map does everything” file.

Now check your understanding here. If a script command says, “give the player a potion,” where should the item quantity live after that command completes? Not in the script. Not in the map. Not in the object event. It should live in the inventory or bag state, and then the save system should know how to persist that inventory state. The script caused a change; it did not become the owner of the changed data.

That same idea applies to party data. A party member is gameplay state. It may be affected by a map event, a battle, an item, or a daycare interaction, but it should not be stored inside those systems. In the source packet, pokefirered/src/pokemon_summary_screen.c, chunk_e8e17a42bbf17d46, is useful because its includes show how many systems need to inspect party facts: Pokémon data, item data, battle constants, party menu, region map sections, and summary UI. The summary screen is a reader and presenter of party state. It should not define what a party member fundamentally is.

So in a clone, your party record can be small but serious: species id, level, current hp, max hp, moves, experience, status, held item, maybe personality or nature if you want later extensibility. The minimal battle turn then reads from that structure, applies move rules, writes back hp and status, and returns a result. Again, do not let the map event directly subtract hit points. Do not let item-use code directly know about map coordinates unless the item is specifically a field item and asks the field service to do something.

Let me slow down for one example because this is where many advanced projects become tangled. The player talks to an NPC. The NPC script says, “Your creatures look tired. Let me heal them.” The script interpreter sees heal_party. What happens? It calls partyService.healAll, or in C terms, it calls a function owned by the party or Pokémon module. That function iterates party slots and restores HP, clears status, maybe restores PP. The script interpreter then resumes and displays, “All better.” The save system does not need to run immediately in every clone architecture, but the next save operation must serialize the updated party state. The map never stores “party healed equals true” unless there is a separate progression flag, such as “this NPC has already given a one-time reward.”

Now let’s connect that back to layout data. The BattleColosseum_2P and BattleColosseum_4P layout records in pokefirered/data/layouts/layouts.json, chunks chunk_76c0c20446e9e415 and chunk_4a91aa64203b6d9a, are layout choices. They answer questions about dimensions, tiles, border blocks, and movement permissions. They do not answer whether the player has a team, whether a battle has started, or whether an item was consumed. The BattleColosseum_2P map record in pokefirered/data/maps/BattleColosseum_2P/map.json, chunk_919152bd3bfceb95, then attaches header facts, object events, and warps. The coordinate triggers in chunk_237e353ea279eb27 attach behavior dispatch points. That layered separation is the whole lecture in miniature.

Put another way: layout is reusable geometry; map is a configured place; script is conditional behavior; gameplay systems are persistent rules and state. If you preserve those four boxes, your implementation can stay small without becoming simplistic.

For battle turns, the minimal clone version should also be a service boundary. A battle turn should accept battle state and selected actions. It should determine order, apply effects, emit messages or events, and produce a new battle state. I’m saying “emit messages” deliberately. The battle engine should not directly draw the text box if you can avoid it. FireRed’s real code is optimized for the Game Boy Advance and deeply integrated, but even there we see battle utility code separated into support functions and constants in pokefirered/src/battle_util.c, chunk_9335e5727aa0657d. For your clone, that suggests a clean split: battle rules calculate; UI presents; scripts only initiate and receive outcomes.

Item use follows the same pattern. A potion has item data: name, price, effect kind, target rules. The bag has quantities. The party has HP. The UI has selection. The item-use service coordinates those pieces. If the player uses a potion from the overworld menu, the map is almost irrelevant. If the player uses an escape rope, the map suddenly matters because the item asks the overworld system for a valid return destination. That is not a contradiction. It means item effects should be typed or dispatched carefully, not crammed into a single “use item” blob.

Now, quick check: why should “escape rope destination” not be stored as a property of the item record alone? Because the same item behaves differently depending on where the player is. The item data can say “this item invokes escape behavior,” but the current map, dungeon metadata, or last safe warp determines the actual destination. That is the pattern we also discussed with dynamic warps. A static warp says, “this tile leads to this destination.” A dynamic warp says, “this tile leads to a destination computed or previously stored by gameplay state.” The map provides a doorway; the engine resolves the trip.

Save persistence is the last piece, and it is where you find out whether your architecture is honest. If your save file has to serialize half of your renderer or every temporary script variable, your boundaries are leaking. A minimal save should persist player position, current map id, facing direction, party, bag, money, flags, variables, maybe storage boxes later, and enough pending state to resume safely. It should not persist the loaded tileset pixels. It should not persist every object sprite’s transient animation frame. It should not persist the callback stack unless your engine truly requires that, and most clones should avoid that requirement.

Here is the board-work version of the lab target. Your map loader reads the map group entry, such as the link map grouping shown in pokefirered/data/maps/map_groups.json, chunk_f22eb0b9c9a5d2a4. It resolves the map id to a map record. The map record points to a layout id. The loader pulls dimensions and collision or movement permissions from the layout. Then it instantiates object events, warp events, coordinate triggers, and background events into runtime arrays. Not full gameplay objects yet. Runtime map affordances. When the player moves, the field controller asks collision first, then checks warps and triggers, then dispatches scripts where appropriate.

Notice how little battle logic I mentioned in that loader. None. That is a good sign. The loader can load a colosseum map without knowing the rules of a battle. The script attached to that map may request a battle. The battle setup system may inspect party data, link state, trainer data, or special rules. But the loader’s job remains stable.

This is also why week 3 matters to the whole course. In week 1, we decomposed the RPG into runtime shell, overworld maps, script VM, creature data, battle, inventory, UI, save, and link systems. In week 2, we learned to trace data through callbacks, tasks, structs, globals, and visible behavior. Today’s material is where those two skills meet. You are no longer just naming subsystems. You are deciding what kind of fact belongs to each subsystem.

One last example before we close. Suppose homework asks you to add a one-time item ball on a map. The layout does not change. The map record gains an object event at a coordinate with an item-ball sprite and a script id. The script checks a flag, gives an item through the inventory service, sets the flag, hides or removes the object, and ends. The bag count is gameplay state. The flag is progression state. The object’s coordinate is map data. The collision under the object is layout behavior. The save file persists the bag and flag, not the original map record. That is the clean answer.

So the synthesis for today is this: layout data describes traversable space; map data places entrances, actors, and trigger surfaces into that space; script data sequences conditional actions; gameplay systems own the durable rules and state those actions touch. FireRed is not valuable to us because every clone should copy its exact implementation. It is valuable because its data records and source boundaries keep showing this recurring pressure: separate what can be authored ahead of time from what must be computed at runtime, and separate what is true of a place from what is true of the player.

For the lab, build the loader small, but make the boundaries real. Read the layout id, dimensions, movement permissions, object events, warps, coordinate triggers, and map group relationship. Then stub the script commands that cross into party, battle, item, and save systems. The stubs can be minimal. The architecture should not be. That is the standard now for week 3 work: not just “can I load the JSON,” but “can I load it without lying about what the JSON means.”

## Source References
- pokefirered/src/battle_setup.c
- pokefirered/src/pokemon_summary_screen.c
- pokefirered/src/daycare.c
- pokefirered/src/battle_util.c
- pokemon-showdown/data/abilities.ts
- pokemon-showdown/data/mods/biomechmons/scripts.ts
