Scripting/Spawning a Treasure Outside a Chest
This tutorial will teach you how to write your first script. If you're not sure what scripts are, check the scripting page.
The goal for this tutorial: Create a treasure which lies on the ground for Link to collect, and which does not reappear after being collected.
Example: Spirits Grave
As an example, let's look at the object which spawns the power bracelet in Oracle of Ages' Spirit's Grave:
It is using an object with ID INTERAC_DUNGEON_SCRIPT. This is an object which loads a custom script depending on the dungeon index and the SubID ($01 in this case). The INTERAC_DUNGEON_SCRIPT object is the most convenient to use when you want to create scripts without any boilerplate assembly code.
In object_code/ages/interactions/dungeonScripts.s, we can see the script table for dungeon 1 specifically:
@dungeon1:
.dw mainScripts.dungeonScript_spawnChestOnTriggerBit0 ; Subid $00
.dw mainScripts.spiritsGraveScript_spawnBracelet ; Subid $01
.dw mainScripts.dungeonScript_minibossDeath ; Subid $02
.dw mainScripts.dungeonScript_bossDeath ; Subid $03
.dw mainScripts.spiritsGraveScript_stairsToBraceletRoom ; Subid $04
.dw mainScripts.spiritsGraveScript_spawnMovingPlatform ; Subid $05
Comments have been added indicating the SubID numbers. So, the line that says mainScripts.spiritsGraveScript_spawnBracelet corresponds to SubID $01.
We can find the script spiritsGraveScript_spawnBracelet in scripts/ages/dungeonScripts.s. As you can see, it is very simple:
spiritsGraveScript_spawnBracelet:
stopifitemflagset
spawnitem TREASURE_BRACELET, $00
scriptend
Let's walk through this script line-by-line.
stopifitemflagset: Ends the script ifROOMFLAG_ITEMhas been set. Generally this flag is set when you've obtained an item in the room. See room flags.spawnitem TREASURE_BRACELET, $00: Spawns the treasure at the current position. In this case, the position is determined by where the object was placed in LynnaLab. The treasure ID isTREASURE_BRACELETand the SubID is$00.scriptend: End the script.
These opcodes are defined in include/script_commands.s, where you can find thorough documentation on all supported scripting opcodes.
Now let's use what we learned to create a new script that does the same thing, this time with the Magnet Gloves.
Creating a new script
Let's suppose we want to put the magnet gloves in Ages' Dungeon 3, for some reason. Let's start by finding the script table for that dungeon in object_code/ages/interactions/dungeonScript.s:
@dungeon3:
.dw mainScripts.dungeonScript_minibossDeath
.dw mainScripts.dungeonScript_bossDeath
.dw mainScripts.moonlitGrottoScript_spawnChestWhen2TorchesLit
It already has 3 scripts, corresponding to SubIDs $00, $01, and $02. Let's add one for SubID $03:
@dungeon3:
.dw mainScripts.dungeonScript_minibossDeath
.dw mainScripts.dungeonScript_bossDeath
.dw mainScripts.moonlitGrottoScript_spawnChestWhen2TorchesLit
.dw mainScripts.moonlitGrottoScript_spawnMagnetGloves
You could, of course, delete the references to the other scripts if they were not needed, and start at subid $00 instead, if you wished.
Now we'll need to create the script named moonlitGrottoScript_spawnMagnetGloves in scripts/ages/dungeonScripts.s. It can go anywhere in that file, but let's put it at the bottom. We will model it after the script for the spirit's grave power bracelet:
moonlitGrottoScript_spawnMagnetGloves:
stopifitemflagset
spawnitem TREASURE_MAGNET_GLOVES, $00
scriptend
Again, we're telling it to spawn a treasure object with ID TREASURE_MAGNET_GLOVES and Subid $00. The SubID will determine some of the properties the treasure object takes; we'll get into this in a minute.
Finally, let's add an interaction object of type INTERAC_DUNGEON_SCRIPT and subid $03 to a room somewhere in Moonlit Grotto:
Now walk into the room, and... uh, this happens...
This is happening because the magnet glove is designed to be opened from a chest by default. The simplest way to fix this is to add a chest in LynnaLab, set its ID to TREASURE_MAGNET_GLOVES and SubID to $00 (equivalent to the values used in our script), and change its "spawn mode" to TREASURE_SPAWN_MODE_INSTANT, and its "grab mode" to TREASURE_GRAB_MODE_2_HAND. The chest won't be used for anything, this is just a convenient method to access the treasure object data. See chest editing for details.
Remember to delete the chest once you've fixed the treasure object properties.
It is also important that the "Set Item Obtained" flag, at the bottom of the above screenshot, is checked. This sets ROOMFLAG_ITEM when you pick up the item. So, the next time you enter the room, the stopifitemflagset line in our script will detect that an item has been obtained in this room, and will cease execution of the script, preventing the treasure from spawning in again.
Making this work outside of dungeons
Unfortunately, the Oracle games don't have a built-in way to define scripts outside of a dungeon without some boilerplate assembly code. Fortunately, it is not too difficult to modify INTERAC_DUNGEON_SCRIPT to work outside of dungeons.
Locate the following code in object_code/{game}/interactions/dungeonScript.s(ages/seasons):
ld a,(wDungeonIndex)
cp $ff
jp z,interactionDelete
ld hl,@scriptTable
rst_addDoubleIndex
ldi a,(hl)
ld h,(hl)
ld l,a
ld e,Interaction.subid
ld a,(de)
...
Change it to the following:
ld a,(wDungeonIndex)
cp $ff
jr nz,@inDungeon
ld hl,@overworldScriptTable
jr @indexBySubid
@inDungeon:
ld hl,@scriptTable
rst_addDoubleIndex
ldi a,(hl)
ld h,(hl)
ld l,a
@indexBySubid:
ld e,Interaction.subid
ld a,(de)
...
This will change how INTERAC_DUNGEON_SCRIPT behaves in the overworld; it will now consult a table called @overworldScriptTable instead of the dungeon-specific tables.
Of course, you must define the new @overworldScriptTable. You can add this to the end of the file. To keep things simple we'll reuse the moonlit grotto script defined above:
@overworldScriptTable:
.dw mainScripts.moonlitGrottoScript_spawnMagnetGloves ; Subid $00
Now, any time INTERAC_DUNGEON_SCRIPT is used outside of a dungeon, it will consult this new table that we created.
It would be a good idea to use comments (starting with a semicolon) to mark each SubID value so that you can keep track of them.
How it works
To explain the code changes:
- When checking for dungeon index
$ff(not a dungeon), instead of executingjp z,interactionDelete(delete this object if equal to$ff), it instead runsjr nz,@inDungeon, which jumps to the new@inDungeonlabel we created if the dungeon index is not equal to$ff. - If the dungeon index is equal to
$ff, it instead executes the next two lines -ld hl,@overworldScriptTable; jr @indexBySubid, which sets thehlregister to the address of@overworldScriptTable, and then jumps to another label that we defined,@indexBySubid, which skips over the dungeon-specific part of the code that normally runs.
Notes
- This will not work if there is a chest in the room, because the "item flag" (ROOMFLAG_ITEM) will be set when you open the chest, causing the magnet gloves to disappear when the room is re-entered (and vice-versa).
- The opcode
spawnitem TREASURE_MAGNET_GLOVES, $00is also equivalent tospawnitem TREASURE_OBJECT_MAGNET_GLOVES_00, defined in data/ages/treasureObjectData.s#L37. You can edit the data in this file directly instead of using LynnaLab's chest editor, if you wish. - TODO: How to deal with limited scripting space, recognizing errors related to that


