The modes
Every copy of the game has these four. They're variants of Slayer, so you find them under Slayer when you pick a game type. To make your own, copy one of them and change a number or two, or write one from the start.
Gun Game
Work through all 24 of Halo 3's weapons, from the Spartan Laser down to the Energy Sword. Each kill swaps the weapon in your hands at once, but melee strikes don't count, and a melee kill from behind sends your victim back a weapon. The first kill with the Energy Sword wins the round.
Growth
Everyone starts tiny, at a tenth of normal size, health and damage, with two random weapons. A kill adds your victim's size to yours, up to three times normal, so big targets are worth more. Dying shrinks you back to a tenth.
One in the Chamber
A Magnum with one bullet, and every hit kills. Each kill, melee included, earns one more bullet; miss and you're down to melee. Three lives each and no shields.
Zone Control
King of the hill for everyone at once. The hill appears where the first player spawns, with a Shotgun beside it. Stand in it alone to score a point a second, faster and with a white aura while you hold it; a second player in the hill stops the scoring for both. A HUD row shows how far away the hill is.
| Mode | To win | When you die | Set up for you |
|---|---|---|---|
| Gun Game | A point per weapon. The Energy Sword kill, the 24th point, wins the round; the most points over three rounds wins the game. | You keep your weapon. A melee kill from behind takes you back one weapon and one point. | Free for all, 3 rounds, no time limit, unlimited ammo, no weapons or vehicles on the map, no pickups or grenades. Plays on every map. |
| Growth | A point per kill. The most points over two rounds wins. | Back to 10% size, health and damage | Free for all, 2 rounds of 10 minutes, two random starting weapons, the map's own weapons, no vehicles |
| One in the Chamber | 25 kills | Back to one bullet | Free for all, 3 lives, 10 minutes, no shields, no weapons or vehicles on the map, no pickups or grenades |
| Zone Control | 30 points, earned only in the hill. Kills score nothing. | You lose the hill and its bonus until you get back in. | Free for all, 1 round of 10 minutes, the map's own weapons and vehicles. Points per second can be changed: see below. |
Zone Control's hill is a ball that reaches 8 units out from its middle, with no visible edge: follow the Hill distance row on your HUD, which counts down as you get closer. While you're in it alone, a Controlling hill row fills up to each point and you move at 125% speed with a white aura. Hosts can change the points a second, from 1 to 10, under Game Options → Slayer rules → Custom mode options. It's written in version 2, so it's also the example to start from for objective modes.
Gun Game's ladder runs from the weapon with the hardest-hitting shot down: Spartan Laser, Rocket Launcher, Shotgun, Missile Pod, Sniper Rifle, Beam Rifle, Brute Shot, Fuel Rod Gun, Mauler, Battle Rifle, Machine Gun Turret, Magnum, Plasma Cannon, Carbine, Plasma Rifle, Spiker, Assault Rifle, Flamethrower, Plasma Pistol, SMG, Needler and Sentinel Beam, then the Gravity Hammer and the Energy Sword. Your score is always the number of weapons you've completed, so the scoreboard shows who's furthest along. Sword and hammer kills count as weapon kills, not melee strikes. A kill made after you died, with a grenade or rocket still in flight, counts too: you respawn with the next weapon. Drop a turret, the Missile Pod or the Flamethrower by switching weapons and you get it straight back.
In Growth, your size is also your damage and your health: at 10% you deal a tenth of normal damage and can take a tenth as much, at 300% three times both. Two 10% players fight each other like two normal players. Growing doesn't heal you; it keeps the share of health you had.
The game updates the bundled modes by itself: a mode you never changed gets its new rules the next time you open Host Game, as Gun Game and Growth did in 0.8.12. A copy you edited is kept as it is. To get the new version, move your copy out of the Game Modes folder and choose Refresh.
Play one
- Host a gameChoose Host Game in the main menu.
- Pick the modeChoose Game Type, select the Slayer card and choose Gun Game, Growth, One in the Chamber or Zone Control from its variants. The lobby's game type row shows the mode's name.
- Pick a mapAny multiplayer map. For a first game, use one everyone has installed.
- Change the basics (optional)Game Options changes the time limit, score to win and lives like for any game type. Leave weapons, pickups and ammo as the mode has them until you've played it: the modes depend on them. A mode with settings of its own, such as Zone Control's points a second, lists them under Slayer rules → Custom mode options, within the limits its author set.
- StartPlayers join from the Server Browser as usual. Choose Start Game.
- Only the host needs the mode. The script runs on the host, or on the dedicated server, and players who join see what it does through the game itself. They don't download or run it. Everyone needs version 0.8.10 or later, and the host 0.8.12 or later for the new Gun Game and Growth and the script features added in 0.8.12. For Zone Control and other version 2 or Megalo modes, the host and every player need 0.8.14 or later: HUD rows, traits and object changes reach players through it.
- Every round starts fresh: sizes, weapon stages, zones, timers and the numbers a script keeps all reset.
- In a playlist: in the playlist editor, pick a map, choose the mode under Slayer as the game type and choose Add to Rotation. In a playlist file, name it like any saved game type:
{ "map": "Guardian", "game": "Gun Game" }. See custom playlists. - On a dedicated server: the server folder gets the four modes in
content/Game Modes/Project Reclaimerthe first timededicated checkordedicatedruns. Name one in a playlist, or give a servermap = "Guardian"andgame = "Gun Game". See share and host.
How a mode works
A game mode is a Slayer game type saved as a JSON text file, the same kind Game Options → Save as variant writes, with one part added: script. The rest of the file holds the usual game type settings, such as the score to win, lives, time limit and which weapons are on the map. The script adds what those settings can't: changes that happen during the game, to one player at a time.
"script": {
"version": 1, 1, or 2 for version 2
"name": "Growth",
"initial": [0.1], each player's numbers when they join
"on_spawn": [ … ], actions when the player spawns
"on_kill": [ … ], actions when the player kills an enemy
"on_death": [ … ] actions when the player dies
}
- Events: a script reacts to three things that happen to a player: spawning, killing an enemy and dying. Each has a list of actions that run in order. See events.
- Numbers: each player carries up to 16 numbers through the round, such as their size or weapon stage. Actions read them and change them. See player numbers.
- Actions: set a number, change the player's size, health, damage, weapon or ammo, or give them points. Any action can carry a condition,
when, so it runs only sometimes, such as for kills that weren't melee. See actions.
That's version 1, the language Gun Game, Growth and One in the Chamber are written in, and what the next sections cover. It has no loops or timers, which keeps modes simple to write. When a mode needs more, such as a hill, a countdown or a winner chosen by the script, version 2 adds events for the whole round, timers, loops, objects and zones, HUD rows, player traits and team changes. Everything in version 1 works in version 2 too. And if you know ReachVariantTool's script language, Megalo scripts compile into version 2 modes. Every version is limited to gameplay: no files, network or programs.
There's no script editor in the game yet. Use any text editor, such as Notepad or Visual Studio Code. JSON needs double quotes around names and text, has no comments, and allows no comma after the last item of a list.
Your first mode
The quickest start is a copy of a mode that works. This makes a slower Growth, where each kill adds half of the victim's size instead of all of it.
- Open the Game Modes folderIn Host Game, choose Game Type and then Open Folder. Or browse to
Documents\My Games\Project Reclaimer\Game Modes. The bundled modes are in itsProject Reclaimerfolder. - Copy GrowthCopy
Project Reclaimer\growth.jsonintoGame Modesitself and rename the copymy-growth.json. Leave the original alone: the game puts back any bundled mode that's missing, but never overwrites one you changed. - Rename itOpen
my-growth.jsonin a text editor. Change"name": "Growth"to"name": "My Growth"in two places: at the top, which is the name the game type picker shows, and inside"script". - Change the rewardIn
"on_kill", the first action's"value"is["self.0", "victim.0", "+", 3, "min"]. Change it to["self.0", "victim.0", 0.5, "*", "+", 3, "min"]: the victim's size times a half. Save the file. -
Check it
Open a terminal in the game client's folder (in File Explorer, right-click inside the folder and choose Open in Terminal) and run
mode-checkwith your file's path. Type.\projectand press Tab for the program's name, and drag the file onto the terminal window to paste its path:It answersPowerShell.\project-reclaimer-v0.8.14.exe mode-check "C:\Users\you\Documents\My Games\Project Reclaimer\Game Modes\my-growth.json"Valid custom mode: My Growth (script version 1), or names what's wrong. See troubleshooting for the messages. - Play itIn Host Game's game type picker, choose Refresh. My Growth is now under Slayer. Select it and start a game with at least one other player.
After every change to the file, choose Refresh and select the mode again: the lobby keeps the copy it loaded, and a round that's running keeps its script until it ends.
A complete mode
This is a whole game type file, not just a script. Save it as small-growth.json in your Game Modes folder, or download it. Players start at normal size, each kill adds a tenth of the victim's size up to twice normal size, health and damage grow in step with size, and dying resets all three.
{
"version": 1,
"name": "Small Growth",
"mode": "Slayer",
"rules": {
"score_to_win": 25,
"kill_points": 1,
"death_points": 0,
"suicide_points": 0,
"betrayal_points": 0
},
"options": {
"rounds.teams": 0,
"rounds.limit": 1,
"rounds.minutes": 10
},
"script": {
"version": 1,
"name": "Small Growth",
"initial": [1],
"on_spawn": [
{"op": "scale", "value": ["self.0"]},
{"op": "health", "value": ["self.0"]},
{"op": "damage", "value": ["self.0"]}
],
"on_kill": [
{"op": "set", "slot": 0, "value": ["self.0", "victim.0", 0.1, "*", "+", 2, "min"]},
{"op": "scale", "value": ["self.0"]},
{"op": "health", "value": ["self.0"]},
{"op": "damage", "value": ["self.0"]}
],
"on_death": [
{"op": "set", "slot": 0, "value": [1]},
{"op": "scale", "value": [1]},
{"op": "health", "value": [1]},
{"op": "damage", "value": [1]}
]
}
}
| Part | What it does |
|---|---|
version, name | The file format, always 1, and the name the game type picker shows, up to 48 characters. |
mode | Always "Slayer": scripts run in Slayer games. |
rules | Scoring: 25 points to win, one per kill, nothing taken away for deaths, suicides or betrayals. See rules and scoring. |
options | Game options: no teams, one round of 10 minutes. |
script | The script: one number per player (slot 0, their size, starting at 1) and what the three events do with it. |
The map isn't in the file: the host picks it in the lobby.
Events
| Event | Runs for | When |
|---|---|---|
on_spawn | The player who spawned | Every spawn, the first of the round included. |
on_kill | The killer | Killing an enemy player. It can read the victim's numbers, as they were before the victim died. |
on_death | The player who died | Every death: killed by an enemy, by a teammate, by themselves or by the map. |
When Alpha kills Bravo:
- Alpha's on_killRuns first, so it can read Bravo's numbers (
victim.0and so on) before anything resets them. This is how Growth rewards bigger victims. - Bravo's on_deathRuns next: the place to reset Bravo's numbers and effects.
- Bravo's on_spawnRuns when Bravo respawns: the place to equip the new body.
- Suicides, deaths from falling or the map, and betrayals of a teammate run only the victim's
on_death. Nobody getson_kill. - A kill made after the killer died, with a grenade or rocket still in flight, still runs the killer's
on_kill. What it gives them, such as a new weapon, comes with their next spawn. - Actions run in the order they're written, and each sees what the ones before it changed.
- Each list is optional; leave out an event the mode doesn't need. One in the Chamber has no
on_death, so dying changes nothing but the bullets a new body starts with. - There's no event for damage, a timer, a round starting or a player joining.
initialsets the numbers for joins and new rounds.
How the kill was made
on_kill and on_death can read two values about the death, in any expression. Both events of the same death see the same values.
| Value | 1 when | Otherwise |
|---|---|---|
"kill.melee" | The killing blow was a melee strike with whatever the player held, the hit Halo 3 gives a Beat Down medal for. Energy Sword and Gravity Hammer attacks are weapon kills, not melee strikes. | 0 |
"kill.assassination" | The killing blow was a melee attack from behind, the one that earns an Assassin medal, sword and hammer attacks included. | 0 |
They're most useful with when: Gun Game moves a player up only when kill.melee is 0, and sets the victim back when kill.assassination is 1. on_spawn can't use them, since nobody died.
Player numbers
Every player has 16 numbers, called slots 0 to 15, that the script can read and change. A slot can mean anything: a size, a weapon stage, a kill streak. The script just calls them by number: self.0 is the player's slot 0, self.3 their slot 3.
initiallists the starting values, from slot 0 on.[1]starts slot 0 at 1; slots you don't list start at 0;[]starts them all at 0.- A player gets the starting values when they join and at the start of every round. Leaving and rejoining starts them over.
- Numbers stay through deaths and respawns. Reset them yourself in
on_deathif they should start over. victim.0tovictim.15read the victim's numbers, inon_killonly.kill.meleeandkill.assassinationaren't slots: they say how the kill was made.- Only
setchanges a slot. Reading one in an expression leaves it as it is.
For example, to count each player's kills in slot 1 while slot 0 holds their size:
"initial": [1, 0],
"on_kill": [
{"op": "set", "slot": 1, "value": ["self.1", 1, "+"]}
]
The effects work the same way: a player's size, health, damage and weapon stay as the last action set them, through death and respawn, until an action changes them again. To reset them, set them in on_death as well as the slot that drives them.
Expressions
Wherever an action needs a value, it takes an expression: a list of numbers, slots and operators in the order a calculator would take them, with each operator after the two values it works on. ["self.0", 2, "*"] is slot 0 times two. [10, 2, "/"] is 10 divided by 2.
Numbers go in as they are, slots and operators in double quotes. The operators:
| Operator | Result |
|---|---|
"+" "-" | Add, subtract: [5, 2, "-"] is 3 |
"*" "/" | Multiply, divide: [5, 2, "/"] is 2.5. Dividing by zero stops the script (see limits). |
"min" | The smaller of the two: ["self.0", 3, "min"] never goes above 3 |
"max" | The larger of the two: ["self.0", 1, "max"] never goes below 1 |
"<" "<=" ">" ">=" | Compare: 1 when true, 0 when false. ["self.0", 3, ">="] is 1 once slot 0 reaches 3 |
"==" "!=" | Equal, not equal: 1 when true, 0 when false. ["kill.melee", 0, "=="] is 1 for every kill but a melee strike |
How it's worked out
The game reads the list from left to right and keeps a pile of values. A number or a slot goes on top of the pile. An operator takes the top two off, works them out, and puts the answer back. At the end exactly one value must be left. My Growth's reward for a kill, when a 10% player kills a 150% player:
["self.0", "victim.0", 0.5, "*", "+", 3, "min"]
| Read | Pile after it | What happened |
|---|---|---|
"self.0" | 0.1 | The killer's size |
"victim.0" | 0.1, 1.5 | The victim's size |
0.5 | 0.1, 1.5, 0.5 | A number |
"*" | 0.1, 0.75 | 1.5 × 0.5: half of the victim's size |
"+" | 0.85 | 0.1 + 0.75: the killer's new size |
3 | 0.85, 3 | The largest size allowed |
"min" | 0.85 | The smaller of 0.85 and 3: the answer |
Common expressions
| You want | Expression |
|---|---|
| A fixed value | [1] |
| Slot 0 as it is | ["self.0"] |
| Slot 0 plus one | ["self.0", 1, "+"] |
| Slot 0 plus one, at most 9 | ["self.0", 1, "+", 9, "min"] |
| Slot 0 squared | ["self.0", "self.0", "*"] |
| Slot 0 kept between 1 and 3 | ["self.0", 1, "max", 3, "min"] |
| 1 plus a fifth of slot 0 | [1, "self.0", 0.2, "*", "+"] |
| 1 minus a tenth of slot 0 | [1, "self.0", 0.1, "*", "-"] |
| Slot 0 minus one, never below 0 | ["self.0", 1, "-", 0, "max"] |
| 1 if slot 0 is above 0 | ["self.0", 0, ">"] |
| 1 for every kill but a melee strike | ["kill.melee", 0, "=="] |
| 1 for a kill from behind while slot 0 is above 0 | ["kill.assassination", "self.0", 0, ">", "min"] |
An operator always comes after its two values: ["self.0", 1, "+"], never ["self.0", "+", 1]. Slots are numbered, not named: "self.size" isn't a slot. Two comparisons both true is the min of them, either one true their max. Version 1 has no random numbers (version 2 does); min and max set the limits of anything that grows.
Actions
Each action is written as {"op": "name", …} with the fields it takes, and optionally a when. The game keeps values within the range shown, so a size of 5 becomes 3.
| Action | Fields | What it does |
|---|---|---|
set | slot, value | Sets one of the player's numbers: {"op": "set", "slot": 0, "value": [1]}. Slots 0 to 15. |
scale | value | The player's size, from 0.1 (a tenth) to 3 (three times normal). |
health | value | How much damage the player can take, from 0.1 to 10 times normal. |
damage | value | The damage the player deals, from 0.1 to 3 times normal. |
weapon | index, choices | Replaces the player's weapons with one from a list of 1 to 32. index picks it, counting from 0. |
ammo | value | Sets the player's ammo, magazine and reserve together, from 0 to 999. |
add_ammo | value | Adds to the ammo the player has left, within the same range. |
lethal | enabled | true: every hit the player lands on another player kills. false turns it off. |
score | value | Gives the player points, or takes them away with a negative value: whole points, rounded down, from −100 to 100 per action. See rules and scoring. |
Run an action only sometimes: when
Any action can have a when: an expression that decides whether it runs. The action runs when when works out to more than 0, and is skipped otherwise; the event goes on with the next action either way. With a comparison, that means the action runs when the comparison is true.
"on_kill": [
{"op": "score", "when": ["kill.melee", 0, "=="], "value": [1]},
{"op": "set", "slot": 0, "when": ["kill.melee", 0, "=="], "value": ["self.0", 1, "+", 23, "min"]},
…
]
For every kill except a melee strike, the player gets a point and moves up a stage. A melee strike skips both. Each action needs its own when: it covers that action only.
- And: the
minof two comparisons is 1 only when both are.["kill.assassination", "self.0", 0, ">", "min"]is a kill from behind while slot 0 is above 0. - Or: the
maxof two comparisons is 1 when either is. - Not: compare with 0:
["kill.melee", 0, "=="]. - A
whenis checked when its action's turn comes, so it sees what the actions before it changed.
Size, health and damage
- Health works as protection: at 2, the player takes half the damage from everything, at 0.1 ten times as much. Raising it doesn't heal, it keeps the share of health and shields the player had. The HUD's shield and health bars don't change.
- Damage multiplies what the player deals to other players. Where Halo 3 deals no damage, such as with friendly fire off, it stays none.
- Size changes the body: bigger players are bigger targets and may not fit through small doors or under low ceilings. Everyone in the game sees the same size, players who joined mid-game too. Players walk at normal speed at any size, and a tiny player's view sits near the floor.
- Lethal counts for melee too. It can't get past what Halo 3 makes immune, such as a player who can't be damaged.
Weapons
The weapons a weapon action can give:
assault_rifle battle_rifle smg carbine magnum plasma_pistol plasma_rifle spiker needler shotgun sniper_rifle beam_rifle rocket_launcher spartan_laser brute_shot mauler energy_sword gravity_hammer sentinel_beam fuel_rod_gun flamethrower missile_pod machine_gun_turret plasma_cannon
indexis an expression, rounded down and kept within the list: in a list of four, 0 is the first, 3 and anything higher the last.- The weapon has to be one the map offers in multiplayer. Every Halo 3 multiplayer map has all 24; a mod map may not.
- Giving the weapon the player already holds doesn't refill it. Use
ammofor that. - The Flamethrower, the Missile Pod and the two turrets are support weapons: players drop them when they switch weapons. The host gives a dropped one straight back, with the ammo it had.
Ammo
ammocounts the magazine and the reserve together. The magazine fills first, the rest goes to the reserve.add_ammoadds to what the player has actually left, so bullets they fired don't come back.- Ammo actions need a weapon with a magazine; energy weapons such as the plasma rifle and the energy sword don't have one. In
on_spawn, give the weapon before its ammo. - Turn infinite ammo off in the game type's options for modes that count bullets.
- With unlimited ammo on, as in Gun Game, the host refills a weapon the script gave once its magazine and reserve are both empty, because the Missile Pod and the turrets can't reload. The Flamethrower keeps its fuel; only its heat limits it.
Rules and scoring
The game type decides who wins, with Halo 3's own score, lives and time limits, so a mode's win condition goes in rules and options. A script can add to that score with the score action, which counts like the points for a kill: the score limit still ends the round the moment a player reaches it.
A mode that scores its own points usually sets kill_points to 0, so kills give nothing by themselves. Gun Game does: its script gives a point for each weapon completed and takes one back for a set-back, so a player's score is always their stage, and its score limit is 24, the number of weapons. The Energy Sword kill is the 24th point and ends the round.
| Rule | Meaning |
|---|---|
score_to_win | Points that win a round. 0 for no limit: the time limit ends the round, as in Growth. |
kill_points | Points for a kill, on top of any the script gives. |
death_points | Points for dying, usually 0 or negative. |
suicide_points | Points for killing yourself, usually 0 or negative. |
betrayal_points | Points for killing a teammate, usually 0 or negative. |
The simplest way to get options right is to let the game write them: host with your mode, set what you want in Game Options, and choose Save as variant. The saved file holds your script with every option you changed. Many options are stored as a number for a choice rather than an amount, "player.shields": 1 means no shields, for example, so don't guess them from their names.
- With more than one round (
rounds.limit), scores start at 0 each round, and the total over all rounds decides the final standings. - Leave Player size in Game Options → World & Forge at its default: a game type with a script can't set it too.
- Scripts work with Slayer only, in a
.jsongame type. A Halo 3.bingame variant, Forge or the campaign can't have one.
Version 2: timers, objects, HUD and rounds
Version 1 reacts to three things that happen to one player. Version 2 sees the whole round: it runs on every tick, keeps numbers for the round, for each team and for each object, counts timers down, places weapons and invisible zones on the map, writes rows on players' HUDs, changes traits and teams, and can end the round. Zone Control is written in it.
To use it, set the script's version to 2 and add a logic part. The file around the script stays the same: "version": 1 and "mode": "Slayer". Everything from version 1 still works, at the script's top level as before (initial, on_spawn, on_kill, on_death) and inside version 2's events, loops and functions. The host and every player need 0.8.14 or later.
A complete version 2 mode
Save this as timed-bonus.json in your Game Modes folder, or download it. It's Slayer with a bonus: every 10 seconds, each living player gets a point, and a HUD row shows their score as a bar toward 50. Hosts can change the 10 seconds, from 1 to 60, under Custom mode options.
{
"version": 1,
"name": "Timed Bonus",
"mode": "Slayer",
"rules": {
"score_to_win": 50,
"kill_points": 1,
"death_points": 0,
"suicide_points": 0,
"betrayal_points": 0
},
"options": {
"rounds.teams": 0,
"rounds.limit": 1,
"rounds.minutes": 10
},
"script": {
"version": 2,
"name": "Timed Bonus",
"initial": [],
"logic": {
"options": {
"interval": {"value": 10, "min": 1, "max": 60}
},
"on_round_start": [
{"op": "timer_set", "scope": "global", "slot": 0, "value": ["option.interval"]},
{"op": "timer_rate", "scope": "global", "slot": 0, "value": [-1]}
],
"on_tick": [
{"op": "if", "condition": ["global.timer.0", 0, "=="], "then": [
{"op": "for_each_player", "actions": [
{"op": "score", "when": ["self.alive"], "value": [1]},
{"op": "hud", "slot": 0, "text": "Score", "value": ["self.score"], "max": [50]}
]},
{"op": "timer_set", "scope": "global", "slot": 0, "value": ["option.interval"]}
]}
]
}
}
}
mode-check answers Valid custom mode: Timed Bonus (script version 2).
| Part | What it does |
|---|---|
"version": 2 in script | The version 2 language. The version at the top of the file stays 1. |
options | A setting hosts can change: interval, 10 unless the host picks another value from 1 to 60. "option.interval" reads it. |
on_round_start | Runs once as the round starts: sets the round's timer 0 to the interval and makes it count down, one second per second of play. |
on_tick | Runs on every tick of the game. When timer 0 reaches 0, it goes through every player, gives the living ones a point and updates their HUD row, then sets the timer back to the interval. The timer keeps counting down. |
What logic holds
logic is required in version 2, even empty: "logic": {}. Every part of it is optional.
| Part | What it holds |
|---|---|
globals | Starting values of the round's 16 numbers, like initial for players. Numbers you don't list start at 0. |
team_initial | Starting values of each team's 16 numbers. |
seed | Where random numbers start from: the same seed gives the same numbers every round. 0 or left out is seed 1. |
options | Settings hosts can change, by name. |
objects | Zones, weapons, characters and map objects the script works with, by name. |
functions | Lists of actions with a name, to call from anywhere. |
on_join, on_leave, on_round_start, on_tick, on_round_end | The round events: lists of actions, like on_spawn. |
Round events
| Event | Runs for | When |
|---|---|---|
on_join | The player who joined | A new player, before their first spawn. A player taking a slot someone left starts fresh. |
on_leave | The player who's leaving | Before they're removed, so their numbers can still be read. |
on_round_start | Nobody in particular | Once, when the round's players and map are known, before anyone spawns. |
on_tick | Nobody in particular | On every tick of the game, after that tick's deaths and spawns. Timers move on just before it. A paused game has no ticks. |
on_round_end | Nobody in particular | Once, when the round is over, before its objects are cleaned up. The score is final by then. |
Events that run for nobody in particular have no self: use for_each_player to go through the players. Each round starts with fresh numbers, timers and objects. The first on_join events of a round see the players as they arrive; make decisions about everyone in on_round_start or on_tick.
More numbers and values
Besides each player's numbers, version 2 has numbers for the round, for each team and for each object: 16 of each, plus 8 timers each. All work in expressions like self.0.
| Value | What it is |
|---|---|
"global.0" … "global.15" | The round's numbers, shared by everyone. Changed with set_global. |
"team.0" … "team.15" | The current team's numbers. Changed with set_team. |
"object.0" … "object.15" | The current object's numbers. Changed with set_object. |
"self.timer.0", "global.timer.0", "team.timer.0", "object.timer.0" | A timer's value in seconds, slots 0 to 7. |
"self.alive", "self.score", "self.ammo" | 1 while the player has a living body; their score this round, the script's points included; the ammo they have left. |
"self.x", "self.y", "self.z" | Where the player's body is on the map, in Forge units. |
"self.index", "self.team", "team.index" | The player's slot, their team (0 to 7) and the current team's number. −1 when there's none. |
"object.exists", "object.x", "object.y", "object.z" | 1 when the current object is on the map, and where it is. |
"object.distance", "object.contains" | How far the current player is from the current object, in Forge units, and 1 when a living player is inside its shape. |
"event.player", "event.killer", "event.victim" | The slots of the players the event is about, whatever loop the script is in. −1 when there's none. |
"game.time", "game.dt", "game.players" | Seconds played this round, seconds since the last tick, and how many players are in the game. |
"option.name" | The value of one of the mode's options. |
More operators: "%" (remainder), "and" and "or" (both values are worked out first), and the one-value operators "not", "abs", "floor", "ceil", "neg" and "sqrt", which take the top value only: ["self.0", "floor"] rounds slot 0 down. A remainder of a division by zero or the square root of a negative number stops the script, like a division by zero; guard them with an if.
Conditions, loops and functions
| Action | Fields | What it does |
|---|---|---|
if | condition, then, otherwise | Runs the then actions when the condition works out to more than 0, the otherwise actions (optional) when it doesn't. |
for_each_player | actions | Runs the actions once for each player, dead ones too, with self and team set to that player and their team. Check "self.alive" where it matters. |
for_each_team | actions | Once for each of the 8 teams, empty ones too. |
for_each_object | actions, label | Once for each of the mode's objects that's on the map, or only those with the label given. The current player stays selected, so "object.contains" works for them. |
with_player | index, actions | Runs the actions for the player in that slot, such as ["event.victim"]. Skipped when nobody's there. |
with_object | object, actions | Runs the actions for one object by name, even when it isn't on the map, so "object.exists" can decide whether to place it. |
call | function | Runs a function from logic.functions, for the same player, team and object. A function can't call itself, directly or through another. |
set_global, set_team, set_object | slot, value | Set a round, team or object number, like set does for players. |
Loops put back the player, team and object they started with when they finish. A slot number names whoever is in that slot now: if a player leaves and another joins, the newcomer gets the slot. when belongs to version 1's actions (set, scale, health, damage, weapon, ammo, add_ammo, lethal and score); put any other action inside an if.
Timers and random numbers
{"op": "timer_set", "scope": "global", "slot": 0, "value": [30]}sets a timer to 30 seconds.scopeisplayer,global,teamorobject; aplayertimer is the current player's, so set it in a player's event or loop.{"op": "timer_rate", …, "value": [-1]}makes it count down one second per second of play;1counts up and0stops it. Timers start at 0, stopped.- A timer counting down stops at 0. Nothing happens by itself when it gets there: check it in
on_tick, as Timed Bonus does. Setting a new value keeps its rate, so it counts down again. {"op": "random", "scope": "player", "slot": 0, "min": [1], "max": [3]}puts a whole number from 1 to 3 in the player's slot 0.logic.seeddecides the sequence.
Objects and zones
logic.objects names up to 64 things on the map for the script to work with. Each has a source, and optionally a position, a label for for_each_object, and a shape for zones.
"objects": {
"hill": {
"source": {"kind": "zone"},
"label": "objective",
"shape": {"kind": "cylinder", "radius": 8, "half_height": 3}
},
"prize": {"source": {"kind": "weapon", "weapon": "rocket_launcher"}}
}
| Source | What it is |
|---|---|
zone | An invisible area with a shape, there from the start of the round. Players can't see it or bump into it: it only answers "object.contains". Move it with move_object. |
weapon | One of the weapon names, not on the map until spawn_object places it. The map has to offer that weapon in multiplayer. |
biped | A character, such as {"kind": "biped", "biped": "marine"}: spartan, elite, marine, grunt, jackal, brute, elite_warrior or hunter. It stands still, without a mind of its own. Handing a player's control over to it takes a Megalo script. |
placement | An object of the map variant, such as a vehicle or crate, by its place in the saved map ("index": 42, 0 to 639). It works on that map only. A copy placed with spawn_object has its object's standard settings, not every Forge setting of the original. |
Shapes: sphere with a radius, box with half_extents [x, y, z], or cylinder with a radius and a half_height. They line up with the map's axes, and their edge counts as inside. Positions and sizes are in Forge units: one is ten feet.
| Action | Fields | What it does |
|---|---|---|
spawn_object | object, position | Places a named object that isn't on the map yet. |
move_object | position | Moves the current object. |
delete_object | none | Removes the current object. |
object_scale | value | The current object's size, from 0.1 to 3. A zone's area stays as its shape says. |
teleport | position | Moves the current player's living body. |
A position is three expressions, x, y and z, each in its own list. In on_spawn, this places the prize beside the first player to spawn, at half size, once:
{"op": "with_object", "object": "prize", "actions": [
{"op": "if", "condition": ["object.exists", "not"], "then": [
{"op": "spawn_object", "object": "prize", "position": [
["self.x", 2, "+"], ["self.y"], ["self.z", 1, "+"]
]},
{"op": "object_scale", "value": [0.5]}
]}
]}
- Zone Control puts its hill where the first player spawns with
move_object, so it works on every map. For a hill in a set place, give the zone aposition: in Forge, fly the monitor there and read X, Y and Z on the Forge menu's Map tab. - Objects the script placed are removed when the round ends. Moved or deleted map objects stay that way for the rest of the round, and the map may respawn a deleted one by its own rules.
- Players who join mid-game see the objects and their sizes as they are.
Traits, teams, round end and HUD
| Action | Fields | What it does |
|---|---|---|
traits | values | Sets the player's traits, as Game Options would: {"op": "traits", "values": {"speed": 7, "aura": 4}} is 125% speed with a white aura. Each traits action replaces the last one; "values": {} takes them all off. They stay through death and respawn until changed. |
team | value | Moves the player to team 0 to 7. Needs a team game ("rounds.teams": 1) and a team the map allows. |
end_round | none | Ends the round with the current player as the winner, or their team in a team game. Without a current player, the round ends without one. Points the script gave first still count. |
hud | slot, text, value, max | Writes row 0 to 3 of the player's HUD: text (up to 96 characters of plain text), then value if given, and a bar filled to value out of max if both are given. Empty text hides the row. Rows stay until changed. |
Trait values are the position of the choice in Game Options' list, counting from 0, not a percentage: speed 7 is 125% and 8 is 150%, camouflage 4 is invisible, aura 4 is white. 0 leaves the trait as the game type has it. The traits and their highest value:
| Trait | Up to | Trait | Up to |
|---|---|---|---|
resistance | 12 | recharge | 9 |
vampirism | 5 | headshots | 2 |
shields | 5 | damage | 12 |
grenade_regeneration | 2 | ammo | 3 |
pickup | 2 | speed | 10 |
gravity | 5 | vehicles | 3 |
camouflage | 4 | waypoint | 3 |
aura | 4 | radar | 4 |
radar_range | 7 |
To pick the winner yourself, select them first, for example in on_kill: {"op": "if", "condition": ["self.score", 30, ">="], "then": [{"op": "end_round"}]}. To show everyone the same row, write it in a for_each_player loop; to show one player something, use with_player, as with "event.victim" in on_kill.
Settings hosts can change
Each entry in logic.options is a setting with a name, a value and the range hosts may choose from: "interval": {"value": 10, "min": 1, "max": 60}. Hosts change them in the lobby under Game Options → Slayer rules → Custom mode options, and Save as variant keeps their choice. The script reads one as "option.interval". Up to 32 per mode, with names of letters, digits and underscores.
Version 2 limits
| Limit | Value |
|---|---|
| Script size | 32 KB |
| Actions in the whole script | 512, counting those inside if, loops and functions |
Depth of if, loops and calls | 16 |
| Steps per event | 16,384 actions and expression items |
| Items per expression | 64 |
| Numbers and timers | 16 numbers and 8 timers for each player, team, object and the round |
| Objects, functions, options | 64, 32 and 32 |
| HUD rows | 4 per player, 96 characters each |
| Changes to the world waiting at once | 256 |
| Timers | From 0 to 1,000,000 seconds |
When an event goes wrong while a game runs, everything that event did is taken back (numbers, timers, traits, HUD rows, points and its changes to the world) and the script stops until the next round, as in version 1. Changes made by earlier events stay. The host's log says why.
Want attachments, velocity, hidden or invincible objects, or players in other bodies? Those are in Megalo scripts, written in ReachVariantTool's language and compiled into a version 2 mode.
Recipes
Changes to the first three bundled modes, and two new modes, to copy from. Each change starts from the mode's file in Game Modes\Project Reclaimer; make a copy with a new name first, as in your first mode.
A shorter Gun Game
Gun Game keeps the player's stage in slot 0, from 0. on_spawn gives the stage's weapon. on_kill scores a point, moves up a stage and gives the next weapon, the first two only when the kill wasn't a melee strike. on_death handles the set-back: after a kill from behind it takes a point back, if the player has one to lose, and moves them down a stage, never below the first. Its script:
"script": {
"version": 1,
"name": "Gun Game",
"initial": [0],
"on_spawn": [
{"op": "weapon", "index": ["self.0"], "choices": [
"spartan_laser", "rocket_launcher", "shotgun", "missile_pod",
"sniper_rifle", "beam_rifle", "brute_shot", "fuel_rod_gun",
"mauler", "battle_rifle", "machine_gun_turret", "magnum",
"plasma_cannon", "carbine", "plasma_rifle", "spiker",
"assault_rifle", "flamethrower", "plasma_pistol", "smg",
"needler", "sentinel_beam", "gravity_hammer", "energy_sword"
]}
],
"on_kill": [
{"op": "score", "when": ["kill.melee", 0, "=="], "value": [1]},
{"op": "set", "slot": 0, "when": ["kill.melee", 0, "=="], "value": ["self.0", 1, "+", 23, "min"]},
{"op": "weapon", "index": ["self.0"], "choices": [ the same 24 weapons ]}
],
"on_death": [
{"op": "score", "when": ["kill.assassination", "self.0", 0, ">", "min"], "value": [-1]},
{"op": "set", "slot": 0, "when": ["kill.assassination"], "value": ["self.0", 1, "-", 0, "max"]}
]
}
For four stages, a Magnum, battle rifle, shotgun and energy sword:
- Change both weapon listsMake the
choicesinon_spawnand inon_killthe same four weapons. - Change the last stageStages count from 0, so four stages end at 3: in
on_kill'sset, the23becomes3. - Change the score to winSet
rules.score_to_winto4. Keepkill_pointsat 0 and nothing taken away for deaths, suicides or betrayals, so a player's score always matches their stage.
"on_spawn": [
{"op": "weapon", "index": ["self.0"],
"choices": ["magnum", "battle_rifle", "shotgun", "energy_sword"]}
],
"on_kill": [
{"op": "score", "when": ["kill.melee", 0, "=="], "value": [1]},
{"op": "set", "slot": 0, "when": ["kill.melee", 0, "=="], "value": ["self.0", 1, "+", 3, "min"]},
{"op": "weapon", "index": ["self.0"],
"choices": ["magnum", "battle_rifle", "shotgun", "energy_sword"]}
],
"on_death": [
{"op": "score", "when": ["kill.assassination", "self.0", 0, ">", "min"], "value": [-1]},
{"op": "set", "slot": 0, "when": ["kill.assassination"], "value": ["self.0", 1, "-", 0, "max"]}
]
The whole file is short-ladder.json: Gun Game with its ladder cut to four. For a ladder of any length, the last stage is one less than the number of weapons, and the score to win the same as the number of weapons.
- To leave the melee set-back out, delete both
on_deathactions. - To let melee kills move players up too, delete the
whenfrom the twoon_killactions that have one.
Tune Growth
| Setting | Where | In the file |
|---|---|---|
| Starting size | initial, and all four values in on_death | 0.1 |
| Growth per kill | on_kill's set. For half the victim's size, add 0.5, "*" after "victim.0", as in your first mode | All of the victim's size |
| Largest size | The number before "min" in the same set | 3 |
| Health | Every health action | ["self.0"], same as size |
| Damage | Every damage action | ["self.0"], same as size |
| Starting weapons | Game Options, or player.primary_weapon and player.secondary_weapon | Random |
- For growth that only changes size, remove the
healthanddamageactions from all three events. - To start everyone at normal size, as Growth did before 0.8.12, make
initialand the fouron_deathvalues1. With all of the victim's size added, two kills then reach the largest size, so add a share too. - Random starting weapons come from Halo 3's own starting-weapon list, which also holds a few campaign weapons, such as the Brute plasma rifle and the golf club. Pick fixed weapons in Game Options to leave them out.
- Change an effect in all three events together, or players keep the old one after a kill or a death. Keep the largest size at 3 or below and the smallest at 0.1 or above: sizes are kept between 0.1 and 3, and a stored size outside them would make those players worth more or less than they look.
Tune One in the Chamber
"script": {
"version": 1,
"name": "One in the Chamber",
"initial": [],
"on_spawn": [
{"op": "weapon", "index": [0], "choices": ["magnum"]},
{"op": "ammo", "value": [1]},
{"op": "lethal", "enabled": true}
],
"on_kill": [
{"op": "add_ammo", "value": [1]}
]
}
| You want | Change |
|---|---|
| Two bullets to start | The ammo value in on_spawn to [2] |
| Two bullets per kill | The add_ammo value in on_kill to [2] |
| Normal damage | The lethal action's enabled to false |
| Another weapon | The weapon in choices, one with a magazine |
| Other lives, time or score | Game Options, or respawn.lives, rounds.minutes and score_to_win in the file |
A new mode: Momentum
Every kill without dying adds a fifth to the damage you deal, up to five kills, double damage. Dying resets it. Slot 0 holds the streak.
"script": {
"version": 1,
"name": "Momentum",
"initial": [0],
"on_spawn": [
{"op": "damage", "value": [1, "self.0", 0.2, "*", "+"]}
],
"on_kill": [
{"op": "set", "slot": 0, "value": ["self.0", 1, "+", 5, "min"]},
{"op": "damage", "value": [1, "self.0", 0.2, "*", "+"]}
],
"on_death": [
{"op": "set", "slot": 0, "value": [0]},
{"op": "damage", "value": [1]}
]
}
The whole file is momentum.json. The damage expression is 1 plus a fifth of the streak: 1 with no kills, 2 at five. Build from this: make it the player's size instead with scale, or give the streak a weapon each with weapon.
A new mode: Style Points
A kill is worth a point, as in Slayer, but a kill from behind is worth three, and from the fifth kill in a row every kill is worth one more. Slot 0 holds the streak. Uses when, a comparison, kill.assassination and score.
"script": {
"version": 1,
"name": "Style Points",
"initial": [0],
"on_kill": [
{"op": "set", "slot": 0, "value": ["self.0", 1, "+", 10, "min"]},
{"op": "score", "when": ["kill.assassination"], "value": [2]},
{"op": "score", "when": ["self.0", 5, ">="], "value": [1]}
],
"on_death": [
{"op": "set", "slot": 0, "value": [0]}
]
}
The whole file is style-points.json, with 50 points to win. kill_points stays at 1, so every kill still scores its point, and the two score actions add the bonuses: 2 more for a kill from behind, 1 more once the streak reaches 5. The streak stops counting at 10, well within the limits, since the bonus doesn't need more.
Test before you share
mode-check finds mistakes in the file, but not in how the mode plays. Play it with at least one other player and check:
- The first spawn, several kills, and the respawn after each death.
- A suicide and a fall off the map: they should run only
on_death. - A second round, which should start everyone over.
- A player leaving and joining again.
- Growth-like modes: killing a bigger player, and playing at the largest and the smallest size in tight spaces.
- Melee rules: a melee strike from the front, one from behind, and a sword or hammer kill, which count as weapon kills, not melee strikes.
- A kill made after dying, with a grenade still in flight.
- Ammo modes: a miss, a melee kill, a reload, and dying with bullets saved.
- Weapon ladders: the last stage, and the round ending on the right kill.
- Modes that score: the score limit ending the round on the right point, and the next round starting everyone at 0.
- The score, lives and time limit ending the game the way you meant.
- Version 2 timers: one that should repeat fires more than once, and one in a player's slot keeps going after they die.
- Zones: an empty zone, one player inside, two players inside, and a player dying inside.
- Objects: a placed object on several maps, its size, and that it's gone in the next round.
- HUD rows and traits for a player who joins mid-game, and rows clearing when they should.
end_roundpicking the right winner, and the round ending without one when nobody's selected.
Limits and safety
Version 1's limits are below; version 2 has its own, and so do Megalo scripts.
| Limit | Value |
|---|---|
| Script size | 32 KB |
| Numbers per player | 16, slots 0 to 15 |
| Actions per event | 32 |
| Items per expression | 64 |
| Weapons per list | 1 to 32 |
Points per score action | −100 to 100, whole points |
| Values | Between −1,000,000 and 1,000,000 |
| Players | 16 |
If an event goes wrong while a game runs, such as a division by zero or a value past a million, that event changes nothing, and the script stops for the rest of the round: every player goes back to normal size, health and damage, and keeps the weapon they hold. The host's log says why. Fix the file and start a new game.
What a script can't do
A script is a list of allowed actions, not a program. Project Reclaimer checks every file against that list and turns away anything else, so a mode from someone you don't know can change how your game plays, and nothing more:
- No access to files, the network, other programs or the console, and no code of any kind: no Lua, JavaScript or Halo 3's own scripts.
- Version 1 has no loops or functions. Version 2's loops only go through players, teams and the mode's objects, its functions can't call themselves, and every event has a step limit, so no script can run forever. Megalo scripts are held to the same kind of limits.
- No text beyond the action, weapon and value names, and the plain text of HUD rows.
- Only the host runs it. Players receive what it does through the game, never the script.
A mode can still be unfair within those limits: try one before you host it for others.
Not yet
Version 1 has no random numbers, timers or way to pick a winner; version 2 adds all three. Neither has per-hit events. Of how a kill was made, both know only melee strikes and kills from behind: they can't tell a headshot from a body shot, or require that a kill was made with the weapon the script gave, so in Gun Game a rocket still in flight after moving up counts. Zones are invisible, with no boundary drawn on the map and no waypoint. Scripts can't play sounds or show icons, and there's no editor in the game. These may come in later versions.
Troubleshooting
| Problem | What to do |
|---|---|
| The bundled modes aren't under Slayer | Check you run 0.8.10 or later, and 0.8.14 or later for Zone Control. In the game type picker, choose Refresh and select the Slayer card to see its variants. |
| My mode isn't listed | Check the file ends in .json, not .json.txt (turn on file name extensions in File Explorer's View menu), that it's inside Game Modes, and run mode-check: files with mistakes are left out. |
| Two modes have the same name | Give your copy its own top-level name, or move the extra copy out of Game Modes. |
| My changes don't show | Save the file, choose Refresh, select the mode again, and start a new game. |
| A deleted mode came back | The game puts the bundled modes back when they're missing. To get an original back after you changed it, move your version out of Game Modes and choose Refresh. |
| Gun Game or Growth still has the old rules | You changed your copy, so the game kept it instead of updating it. Move it out of the Game Modes folder and choose Refresh: the game puts the new version back. |
| The lobby plays something else | A playlist chooses the games. Stop using it, or add your mode to it. |
Unknown expression token or missing operand | An operator comes before its values, as in ["self.0", "+", 1], or a word isn't a slot or operator, such as "self.size". Write ["self.0", 1, "+"]. |
Expression must leave exactly one value | An operator is missing: [1, 2] leaves two values. |
victim variables are only available in on_kill | victim.0 and the other victim slots only exist in on_kill. |
kill values are only available in on_kill and on_death | kill.melee and kill.assassination describe a death, so on_spawn can't use them. |
unknown variant `heal` | The action or weapon name is misspelled or doesn't exist. The message lists the ones that do. For score or one of the four weapons added in 0.8.12, the game checking it is older than 0.8.12: update it. |
unknown field `if`, expected `value` or `when` | A field name is misspelled or doesn't belong to that action; the message lists the ones that do. If it says unknown field `when`, the game checking the file is older than 0.8.12: update it. |
missing field `rules` or another field | The file needs the parts of a complete mode, not just a script. |
Version 2 requires a logic object | A script with "version": 2 needs a logic part, even an empty one: "logic": {}. |
unknown field `when`, expected one of `slot`, `text`, `value`, `max` | Only version 1's actions take a when. Put the action inside an if instead. |
Unknown function: award or Recursive function: award | The function isn't in logic.functions (check the spelling), or it calls itself, directly or through another function. |
Unknown object: hill | Name the object in logic.objects before using it in with_object or spawn_object. |
Unknown expression token: option.interval | The option isn't in logic.options, or its name is spelled differently there. |
Option interval is outside its range | The option's value has to be between its min and max. |
Trait speed must be 0..10 | Trait values are positions in Game Options' list; see the trait table for each trait's highest. |
Invalid object source, label, position or shape | Check the source's kind and weapon or character name, that shape sizes are above 0 and at most 10,000, that positions are within 100,000, and that the label has at most 32 characters. |
| I can't find Zone Control's hill | It's where the first player spawned this round, and it has no visible edge. Follow the Hill distance row on your HUD down to 0. |
| HUD rows don't show for a player | Rows need 0.8.14 or later on the player's side too. A row shows only after the script writes text to it, and empty text hides it again. |
| A timer fires once and never again | A timer counting down stops at 0. Set it again with timer_set when it gets there; it keeps counting down. |
| Points pour in every moment | on_tick runs many times a second. Give points when a timer reaches 0 or when something changes, not on every tick. |
| A kill gives no reward | Suicides, falls and betrayals don't run on_kill. Test by killing another player. An action with a when runs only when it works out to more than 0: in Gun Game, melee strikes are skipped on purpose. |
| The score doesn't match what the script gives | kill_points still scores every kill on top of the script's points. Set it to 0 when the script does all the scoring. |
| Players keep their size after dying | Reset the slot and the effects in on_death: numbers and effects stay until changed. |
| No weapon or ammo in game | The map may not have that weapon for multiplayer, or the weapon has no magazine. Give the weapon before its ammo. |
| The script stopped during a round | Look in the host's log for Custom game mode stopped and the reason, fix the file and start a new game. |
The client's log is Documents\My Games\Project Reclaimer\Logs\client.log; a dedicated server's is data/<server>/server.log. Lines about modes start with Custom mode or Custom game mode stopped, and files left out with Skipped content file. When you report a problem, include the mode's file, the map, and what happened in the game before it.
