Useful code snippets
The following code snippets are likely to be useful when making new gametypes from scratch. Note that if a code snippet requires the use of a variable, you will probably have to ensure that your own script doesn't use the same variable for anything important.
These snippets include both "common" behaviors, which can be omitted from very specialized gametypes (e.g. chess does not need to award DLC achievements for kills), and "standard" behaviors, which the user would expect all gametypes to have (e.g. ending the round when the round timer runs out, which, yes, you have to do manually).
- Announce game start
- DLC Achievements
- Loadout palette code
- Identifying Red and Blue Teams across rounds
- Moving one object to another
- Round timer code (basic)
- Round timer code (with Sudden Death)
- Super Shields
Announce game start
Most official gametypes announce their names at the start of each round. They do this by sending an incident: they use the send_incident function to tell the game that some event (such as the start of a round) has occurred. Each base gametype has its own incident.
alias announced_game_start = allocate player.number alias announce_start_timer = allocate player.timer declare player.announced_game_start with network priority low = 0 declare player.announce_start_timer = 5 for each player do if current_player.announced_game_start == 0 and current_player.announce_start_timer.is_zero() then send_incident(race_game_start, current_player, no_player) current_player.announced_game_start = 1 end end
In addition to announcing the game start, you may want to display explanatory text at the start of the round. You can do this by calling the player.set_objective_text function. Commonly, gametypes will display the score to win in the "round card," unless the player has turned off the score limit, and this can be accomplished using format strings.
alias announced_game_start = allocate player.number alias announce_start_timer = allocate player.timer declare player.announced_game_start with network priority low = 0 declare player.announce_start_timer = 5 for each player do if game.score_to_win != 0 then current_player.set_objective_text("Collect flags for your team.\r\n%n points to win.", game.score_to_win) end if game.score_to_win == 0 then current_player.set_objective_text("Collect flags for your team.") end if current_player.announced_game_start == 0 and current_player.announce_start_timer.is_zero() then send_incident(stockpile_game_start, current_player, no_player) current_player.announced_game_start = 1 end end
DLC Achievements
Several DLC achievements are awarded by Megalo script instead of being hardcoded into the game. These include:
- Dive Bomber
- Assassinate an enemy player while using a Jetpack.
- From Hell's Heart
- After being stuck with a plasma grenade, make sure that the player who stuck you dies in the blast with you.
- Top Shot
- Score three headshot kills in a row without dying.
- License to Kill
- Splatter five enemy players during a single match.
- Paper Beats Rock
- Assassinate an enemy player no more than three seconds after they stop using Armor Lock.
This code was taken from official gametypes. In some cases, small adjustments were made in order to avoid dependencies that you can't easily copy and paste (e.g. using the object.is_of_type condition instead of using object.has_forge_label with unnamed Forge labels, as 343i does).
alias ach_top_shot_count = allocate player.number alias ach_license_to_kill_count = allocate player.number alias ach_paper_beats_rock_vuln_timer = allocate player.timer declare player.ach_top_shot_count with network priority low declare player.ach_license_to_kill_count with network priority low for each player do -- award Dive Bomber achievement as appropriate alias killer = allocate temporary player alias killer_aa = allocate temporary object alias death_mod = allocate temporary number if current_player.killer_type_is(kill) then killer = current_player.get_killer() death_mod = current_player.get_death_damage_mod() if death_mod == enums.damage_reporting_modifier.assassination then killer_aa = killer.get_armor_ability() if killer_aa.is_of_type(jetpack) and killer_aa.is_in_use() then send_incident(dlc_achieve_2, killer, killer, 65) end end end end for each player do -- award From Hell's Heart achievement as appropriate alias death_mod = allocate temporary number alias killer = allocate temporary player if current_player.killer_type_is(kill) then death_mod = current_player.get_death_damage_mod() if death_mod == enums.damage_reporting_modifier.sticky then killer = current_player.get_killer() if killer.killer_type_is(suicide) then send_incident(dlc_achieve_2, current_player, current_player, 68) end end end end for each player do -- manage and award Top Shot achievement as appropriate alias killer = allocate temporary player alias death_mod = allocate temporary number if current_player.killer_type_is(guardians | suicide | kill | betrayal | quit) then current_player.ach_top_shot_count = 0 if current_player.killer_type_is(kill) then killer = current_player.get_killer() death_mod = current_player.get_death_damage_mod() if death_mod != enums.damage_reporting_modifier.headshot then killer.ach_top_shot_count = 0 end if death_mod == enums.damage_reporting_modifier.headshot then killer.ach_top_shot_count += 1 end if killer.ach_top_shot_count > 2 then send_incident(dlc_achieve_2, killer, killer, 62) end end end end for each player do -- manage and award License To Kill achievement as appropriate alias killer = allocate temporary player alias death_mod = allocate temporary number if current_player.killer_type_is(kill) then killer = current_player.get_killer() death_mod = current_player.get_death_damage_mod() if death_mod == enums.damage_reporting_modifier.splatter then killer.ach_license_to_kill_count += 1 if killer.ach_license_to_kill_count > 4 then send_incident(dlc_achieve_2, killer, killer, 66) end end end end for each player do -- manage timing for the Paper Beats Rock achievement alias current_ability = allocate temporary object -- current_ability = current_player.get_armor_ability() if current_ability.is_of_type(armor_lock) and current_ability.is_in_use() then current_player.ach_paper_beats_rock_vuln_timer = 3 current_player.ach_paper_beats_rock_vuln_timer.set_rate(-100%) end end for each player do -- award Paper Beats Rock achievement as appropriate alias killer = allocate temporary player alias death_mod = allocate temporary number if current_player.killer_type_is(kill) and not current_player.ach_paper_beats_rock_vuln_timer.is_zero() then death_mod = current_player.get_death_damage_mod() if death_mod == enums.damage_reporting_modifier.assassination then killer = current_player.get_killer() send_incident(dlc_achieve_2, killer, killer, 60) end end end
Loadout palette code
Players will not have access to any loadout palette unless you give them access to loadout palettes. Standard gametypes grant players access to the Spartan Tier 1 and Elite Tier 1 palettes depending on their species.
for each player do -- loadout palettes if current_player.is_elite() then current_player.set_loadout_palette(elite_tier_1) end if not current_player.is_elite() then current_player.set_loadout_palette(spartan_tier_1) end end
Identifying Red and Blue Teams across rounds
As explained here, Megalo scripts
commonly refer to teams by a number, and with each round, Red and Blue Team swap
their numbers. This makes it easy to implement asymmetric gametypes: you can assume
that team[0]
is always Defense and team[1]
is always
Offense. The thing is, what if you actually need to know which team is Red Team
and which is Blue Team?
343 Industries' "Freeze Tag" game mode lightens players' armor colors whenever they become "frozen." It does this by applying a set of player traits with a forced color, but this means that Red Team and Blue Team need different traits, to force their colors to pink and teal, respectively — and it means that the gametype needs to know a player's team color, so that it can apply the right set of traits.
alias red_team = allocate global.team alias blue_team = allocate global.team declare red_team with network priority high = team[0] declare blue_team with network priority high = team[1] on init: do alias is_odd = allocate temporary number -- -- We need to know which team is red and which team is blue, so we can apply the -- appropriate visuals to players that are frozen. However, that's trickier than -- you might expect: teams alternate each round, but their indices are remapped. -- On round 1, Red Team is team[0], but on round 2, it's team[1]. -- is_odd = game.current_round is_odd %= 2 if is_odd == 0 then -- even-numbered round red_team = team[0] blue_team = team[1] end if is_odd != 0 then -- odd-numbered round red_team = team[1] blue_team = team[0] end end
Moving one object to another
Megalo doesn't have a "move to" function that can be used to move one object to another. Instead, you must take the object to be moved, attach it to the object you wish to move it to, and then detach it.
alias subject = global.object[0] alias target = global.object[1] subject.attach_to(target, 0, 0, 0, relative) subject.detach()
When attaching an object, you can optionally provide a position offset, and you can decide whether this should be relative to the target's rotation or not. This can be used to fine-tune object positions:
alias subject = global.object[0] alias target = global.object[1] -- Move (subject) 10.0 Forge units above (target): subject.attach_to(target, 0, 0, 100, relative) subject.detach()
Each coordinate in the attachment offset is limited to the range [-128, 127], where one unit in Forge is ten in Megalo. If you need to move an object further than 12.7 Forge units away from some target, you can do so by creating a second object along the way.
alias subject = global.object[0] alias target = global.object[1] alias temporary = global.object[2] -- Move (subject) 20 Forge units (200 Megalo units) above (target), by creating -- a Hill Marker 10 Forge units above (target) and moving (subject) 10 Forge -- units above that marker: temporary = target.place_at_me(hill_marker, none, none, 0, 0, 100, none) subject.attach_to(temporary, 0, 0, 100, relative) subject.detach() temporary.delete() -- make sure to delete the temporary Hill Marker afterward!
Round timer code (basic)
Halo: Reach does not automatically end the round when the round timer expires. Instead, it's up to Megalo script. The round timer likely behaves this way so that Invasion can use it for individual phases.
if game.round_time_limit > 0 and game.round_timer.is_zero() then game.end_round() end
Round timer code (with Sudden Death)
The code for a round timer with Sudden Death is a bit more complicated.
Typically, Sudden Death will activate if, at the end of the round, a player is in a position where they can attempt to complete the objective. For example, during CTF, Sudden Death will activate if a player is standing very close to a flag that they can pick up and capture.
Typically, the Grace Period is used to complement Sudden Death. Sudden Death activates when a player is able to further an objective match; for example, it will enable if any player is standing close to a flag that they can pick up and score. In that scenario, if Sudden Death begins and then the conditions for it are no longer met (the player stops being near the flag), then the match will end after the Grace Period ends. Sudden Death never resets (if a player comes near the flag again, the timer will continue from where it left off), but the Grace Period does.
alias sudden_death_enabled = allocate global.number -- set this to 1 when you want Sudden Death to be active, or 0 otherwise alias announced_sudden_death = allocate global.number -- only announce the start of Sudden Death once if not game.round_timer.is_zero() then game.grace_period_timer = 0 end if game.round_time_limit > 0 then if not game.round_timer.is_zero() then announced_sudden_death = 0 end if game.round_timer.is_zero() then if sudden_death_enabled == 1 then game.sudden_death_timer.set_rate(-100%) game.grace_period_timer.reset() if announced_sudden_death == 0 then send_incident(sudden_death, all_players, all_players) announced_sudden_death = 1 end if game.sudden_death_time > 0 and game.grace_period_timer > game.sudden_death_timer then game.grace_period_timer = game.sudden_death_timer end end if sudden_death_enabled == 0 then game.grace_period_timer.set_rate(-100%) if game.grace_period_timer.is_zero() then game.end_round() end end if game.sudden_death_timer.is_zero() then game.end_round() end end end
Super Shields
Like the original Halo: Combat Evolved, Halo: Reach doesn't display any extra graphics on players that have overshields. The "TU" versions of many gametypes include a new option called "Super Shields" which works around this, by spawning fire particles on these players.
alias opt_super_shields = script_option[12] alias lifespan = allocate object.timer -- This should be the index of a nameless Forge label whose object type -- is the fire particle emitter: alias all_fire_particles = 7 declare object.lifespan = 1 for each player do -- handle super shields VFX if opt_super_shields == 1 then alias current_shields = allocate temporary number alias outcome = allocate temporary number -- current_shields = 0 current_shields = current_player.biped.shields if current_shields > 100 then outcome = 0 outcome = rand(10) if outcome <= 2 then -- 30% chance to spawn a particle emitter current_player.biped.place_at_me(particle_emitter_fire, none, never_garbage_collect, 0, 0, 0, none) end end end end for each object with label all_fire_particles do -- clean up super shields VFX current_object.lifespan.set_rate(-100%) if current_object.lifespan.is_zero() then current_object.delete() end end