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_ITEM
has 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_BRACELET
and 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@inDungeon
label 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 thehl
register 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, $00
is 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