Scripting/Spawning a Treasure Outside a Chest

From ZeldaHacking Wiki
Jump to navigation Jump to search

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.

.dw stands for "define word". In the context of the gameboy, a "word" is two bytes; so, each line is defining a 2-byte value to be inserted into the ROM. Gameboy addresses are two bytes long, so all of these script labels are ultimately resolved to "words", or two byte values, when the game is assembled.

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 if ROOMFLAG_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 is TREASURE_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 executing jp z,interactionDelete (delete this object if equal to $ff), it instead runs jr 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 the hl 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 to spawnitem 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