Repository navigation
Game modes
A game mode is a plugin's own rules for ProLeak's local server. The mode sets who holds each
lane, switches whole systems of the game off, replaces single rules (policies), reacts to what
happens on the server, and drives the game through a server-side game object. Everything
it does not change plays as the official game.
Modes run on the local server only, which for now needs developer mode
(dev: true in Settings).
Contents
- A first mode
- Client scope and server scope
- Lanes
- Systems
- Policies
- Server events
- The game
- Corridor gates
- Randomness and time
- Chat commands
- Budgets and faults
from proleak import modes
@modes.mode("half", title="Half damage", subtitle="Every hit does half its damage.")
def setup(game):
"""Runs on the server once, before the player joins."""
game.lanes[1].human()
game.lanes[2].bot(modes.bots.basic())
game.lanes[5].idle()
game.lanes[6].idle()
game.mercenaries.enabled = False
game.waves.build_seconds = lambda wave, seconds: 20
@modes.on("unit.damaged", pre=True)
def halve(ev):
ev.damage *= 0.5
@modes.on("wave.ended")
def bonus(ev):
for player in ev.game.players:
player.gold += 10The game's Solo & Co-op screen shows each mode as a card after the game's own cards. The card
takes these from modes.mode:
| Argument | The card shows | |
|---|---|---|
title |
required | its name |
subtitle |
required | the line under the name |
image |
a PNG or JPG of at most 2 MB in the plugin's folder, such as "art/card.png"; without one, ProLeak's helmet |
|
tooltip |
a text over the card while the pointer is on it | |
hue |
the colour of the glow round the card under the pointer, in degrees on the colour wheel (0 red, 120 green, 240 blue); purple (275) without one | |
info |
with info_popup
|
a link under the card, as the game's Ranked FAQ |
info_popup |
with info
|
the text the link opens in a popup: a blank line starts a paragraph, and a paragraph whose lines all start with - is a list |
@modes.mode("tower_rush", title="Tower rush", subtitle="Twice the gold, half the time.",
tooltip="Build phases last 15 seconds.", hue=20, info="Rules",
info_popup="Every wave pays double.\n\n- 15 s build phases\n- No mercenaries")
def setup(game):
...The cards wrap into rows once they no longer fit in one. modes.play("half") starts a mode from
the plugin's own code; it raises ModeError with the reason when the game cannot start.
A mode plugin runs code in two places of the same game process:
-
Client scope is the rest of the plugin:
proleak.on,ui,camera,actionsand the others, under the permissions of a local server. It changes what this player sees and sends. -
Server scope is the code ProLeak calls for the mode: the
setupgiven tomodes.mode, the handlers given tomodes.on, and the policies and timers they set. It changes the game itself.
Only the server scope of the mode the server is playing reaches the server. A call from client
scope, from another plugin, or with no mode playing raises NotAllowedHere. Server handlers
receive server units and players, never the client's world objects.
In setup, game.lanes[slot] (slots 1 to 8; 1 to 4 are the west team) takes one of:
| Call | The lane |
|---|---|
human() |
the player at this computer (slot 1) |
bot(modes.bots.basic()) |
ProLeak's bot (see below) |
idle() |
plays: its waves come and nobody builds |
closed() |
is no lane at all |
A team with nobody on it has no king. The lanes are fixed once the game starts.
ProLeak's basic bot plays the Mastermind with a random playstyle. Each build phase it builds
toward the wave's recommended value, upgrades and buys workers, sends mercenaries for income,
and buys King Regen after a wave that hurt its king. modes.bots.basic(king_heal=0.2) makes it
heal only after a wave that took more than a fifth of the king's life (by default, after any
damage).
Each system has a switch, game.<system>.enabled, on by default. A switch can change at any
time from server scope.
| System | Switched off |
|---|---|
economy |
no income, no bounty, no mythium gathered |
building |
placing and upgrading fighters are refused |
waves |
no wave comes; each build phase ends as if its wave were cleared |
movement |
units stand where they are |
combat |
nobody attacks |
mercenaries |
hiring is refused |
spells |
no legion spell is offered |
notices |
no banners, pings or music |
bots |
bots do nothing |
gates |
no corridor gate is placed (2v2 map): the waves walk straight to their king |
A policy is one decision of the server, replaced by a function:
game.combat.damage = fn. A policy that returns None lets the server decide as the official
game does, so it can change only the cases it cares about. Setting a policy to None restores
the official rule. Units are server units, players Player.
| System | Policy | Arguments | Answer |
|---|---|---|---|
| economy | income |
player, the income it would pay | the gold paid when a wave is cleared |
| economy | bounty |
dead unit, killer, the bounty | the gold the kill pays |
| economy | mythium_rate |
player, its gather rate | the workers' rate (1 is the game's) |
| building | can_build |
player, unit type, x, z | whether it may be placed there (True lifts the lane and grid rules) |
| building | cost |
player, unit type, its gold cost | what placing it costs |
| waves | units |
wave, lane | the unit types that come down the lane, in order |
| waves | build_seconds |
wave, its build seconds | the build phase before it |
| waves | next_wave |
the wave cleared, the next one | the next wave; 0 ends the game, won by the player |
| movement | route |
unit | the points it walks instead of its lane's route |
| combat | damage |
attacker, target, the damage | the damage dealt |
| combat | target |
unit, the units it could attack | the one it attacks |
| mercenaries | stock |
player, mercenary type, its stock | how many can be hired now |
| mercenaries | delivery |
player, the lane it would go to | the lane a mercenary goes to |
| spells | offer |
player, the legion spells offered | the ones offered instead |
| gates | holds |
unit | whether a gate waits for it and sends it on when it opens (default: every wave unit the mode neither controls nor routes) |
| gates | opens |
side (west, east), the units the gate waits for |
whether it opens now (default: once they are all through the corridor top) |
A wave number or lane a policy names that the game does not have stops the mode.
modes.on(event, pre=False, priority=0) subscribes a server handler. ev.game is the game.
Pre handlers may change the editable fields or cancel.
| Event | Fields | Pre |
|---|---|---|
game.started |
||
game.ended |
won_side (west, east), reason
|
|
game.tick |
tick, the server clock in milliseconds |
|
wave.started |
wave |
|
wave.ended |
wave |
|
unit.spawned |
unit, type, owner, lane
|
|
unit.died |
unit, killer
|
|
unit.damaged |
unit, source, damage (editable) |
yes, cancellable |
unit.attacking |
unit, target; raised as a unit is about to attack a new target |
yes, cancellable |
player.income |
player, amount (editable) |
yes |
lane.leaked |
lane, wave; once a wave, when the lane's fighters have all fallen |
|
gate.opened |
side, reason (wave, mode or broken) |
game.tick and unit.damaged are raised only while a handler is subscribed.
A cancelled unit.attacking holds that swing only. The unit keeps the target, so the event is
raised again on the next server tick (every 100 ms) for as long as the target stays the unit's
best. To keep a unit off a target, call unit.ignore(target): the unit then picks another
target, and the event is not raised for that pair while the ignore holds. The
target policy chooses among every candidate instead.
| Member | |
|---|---|
game.lanes[slot] |
the lanes, set in setup
|
game.players |
each lane's player: slot, gold, mythium, income, workers (settable, shown at once), value
|
game.units(owner=, lane=, type=) |
the units alive, filtered |
game.unit(id) |
one unit |
game.wave, game.tick
|
the wave, the server clock in milliseconds |
game.random(n), game.random_float()
|
the game's own seeded stream |
game.after(ms, fn), game.every(ms, fn), game.cancel(timer)
|
timers on the server clock; none runs after the game ends |
game.spawn(type, owner, x, z) |
a new unit, which stands until ordered |
game.gate(side) |
a corridor gate, west or east (see below) |
A unit has id, type, owner, lane, x, z, alive, life (settable), and:
-
controlled = True: the mode drives it; it stops, leaves its route and attacks only what it is ordered to. -
order_move(x, z),order_attack(unit). An attack order lifts an ignore on that unit. -
ignore(unit),unignore(unit): it never takesunitas its own target, and drops it at once if it is attacking it. The ignore holds for that unit only: a later unit given the same id is not ignored. -
kill(): it dies at once and nobody earns its bounty.
On the 2v2 map each side's wave waits behind a gate at the bottom of its corridor; by default
the gate opens once the wave is through the corridor top, and closes again when the next wave
has come. A mode changes this with the gates system and its policies, or drives a gate
directly:
-
gate.is_open,gate.open(),gate.close(). -
gate.life = 500on a closed gate: the gate becomes a target of the wave waiting behind it (armoured, without an attack) and opens when its life is gone. A gate with a life ignores the default opening; theopenspolicy andgate.open()still open it.
@modes.on("wave.started")
def sturdy_gates(ev):
for side in ("west", "east"):
ev.game.gate(side).life = 400Use game.random, game.after, game.every and game.tick in a mode rather than Python's
random and time modules. They follow the game's own seed and clock, so the same game played
again from its commands gives the same result. A recording replays exactly whatever the mode
does.
The server answers the game's chat commands: a line that starts with -, in any case, such as
-gold 500. The cheats among them work only while the game's debug mode is on, and the mode
decides when that is with game.commands.cheats:
| Value | Debug mode |
|---|---|
"off" (the default) |
never: -debug is refused, and each cheat tells its player that debug mode is off |
"debug" |
once a player types -debug, for the rest of the game, as in an official custom game |
"on" |
from the start |
game.commands.allow(fn) decides who may cheat and type -debug: fn gets each Player and
returns True or False. game.commands.disable(name) switches a command off (typed, it does
nothing, as an unknown command) and enable(name) brings it back. A game without a mode has its
cheats on in a sandbox and off otherwise. The game's Codex leaves out -ratings, -next, -info
and the type and wave kind commands; the server answers them all the same.
| Command | Cheat | What it does |
|---|---|---|
-debug |
turns debug mode on, when the mode lets the player | |
-ready |
votes to start wave 1 early | |
-ff |
votes to forfeit | |
-abandon |
leaves the game, lost | |
-keep 1 2 3 4 5 6 |
keeps those of a Mastermind roll | |
-kills, -dps, -lifetime
|
in the build phase, shows over each fighter its kills this game, its damage per second in the last fight, or how long it lasted; once each 8 s | |
-ratings |
answers that the command is disabled, as the game always does | |
-next, -info X
|
shows the next wave, or wave X: its units with their life, types and gold, its total gold and its recommended value | |
-pierce (or -piercing), -impact, -magic, -pure
|
shows what the attack type does to each defense, and the waves that attack with it | |
-immaterial, -swift, -natural, -arcane, -fortified
|
shows what each attack type does to the defense, and the waves that defend with it | |
-ranged, -air (or -flying), -boss, -miniboss
|
lists the waves of that kind | |
-builder X |
yes, once a legion is picked | picks legion X |
-greedisgood |
yes | +10000 gold and mythium, supply cap at the sandbox's |
-gold X, -mythium X, -income X
|
yes | sets the player's gold, mythium or income to X |
-goldall X, -mythiumall X
|
yes | sets every player's gold or mythium to X |
-supply X |
yes | sets the supply cap to X, at most the sandbox's |
-workers X |
yes | sets the player's workers to X |
-nodouble |
yes | switches double workers on or off (each town trains a double worker once wave 15 is cleared) |
-wave X |
yes | sets the wave to X |
-start |
yes | starts the wave now |
-clear |
yes | clears the wave, once it has finished spawning |
-clearwest, -cleareast
|
yes | clears one side's wave at any time: its fighters warp to their catch area and its gate opens |
-win, -lose
|
yes | sends thirty Dire Toads at the east king or the west one |
-heal |
yes | fills both kings' life and mana |
-healwest, -healeast
|
yes | fills one king's life |
-autoheal |
yes | switches on or off a king about to die being healed whole |
-smite X |
yes | sets the selected unit's life to X percent; 0 kills it, paying nothing |
-powerup |
yes | offers every legion spell, at any time; the player may pick again |
-maphack |
yes | shows the whole map for the rest of the game |
-mana X |
yes | sets the selected unit's mana to X percent |
-lockwave |
yes | the wave comes again with the same sends and resources until typed again |
Each handler, policy and timer has the plugin's handler budget (50 ms by default), and all of a mode's server code shares 20 ms in each server tick. A mode that raises, runs past either budget, or loses its plugin (turned off or reloaded) stops: the game ends at once with the reason across the screen.
Using ProLeak
Features
Tutorial
- Your first plugin
- Plugin basics
- Handling events
- Plugin settings
- User interface
- Players, units and game data
- Purchases, signals and pings
- Chat, camera, input and more
- Cosmetics
- Game modes
- Game updates
- Publishing plugins
Reference