OverPy is a high level language for the Overwatch workshop with a Python-like syntax, allowing you to code your gamemodes with modern development practices: multiple files, switches, dictionaries, macros, function macros, enums, built-in JS preprocessing...
It contains both a compiler, and a decompiler, to allow you to quickly convert your existing gamemodes to OverPy.
All in-game languages are supported, meaning you do not need to switch your in-game language to English to use it.
The VS Code extension includes syntax highlighting, autocompletion, and documentation:
Join the discord for help & feedback: https://workshop.codes/discord
Play around with the demo: https://zezombye.github.io/overpy/demo
Thanks to:
- Arxenix for parsing the workshop documentation (https://github.com/arxenix/owws-documentation/blob/master/workshop.json)
- The Overtool team for providing a way to datamine all translations
- CactusPuppy for converting OverPy to Typescript
- Install:
npm install
- Build in dev and test with the demo:
npm run dev
- Build
out/overpy_standalone.js
:npm run package
- Build .vsix for prod:
vsce package
(also builds overpy_standalone.js)
- Download and install VS Code: https://code.visualstudio.com/download
- In the sidebar, click the "Extensions" button, then search for "overpy" and install the extension:
- Make sure you have configured Windows to show file extensions.
- Create a new file in VS Code (File -> New Text File or Ctrl+N)
- Press Ctrl+S to save the untitled file, then save it wherever you want, but make sure it has an ".opy" extension.
- The file should now have the OverPy icon and an ".opy" (not .opy.txt) extension.
- Press Ctrl+Shift+P, type "overpy", then select the "Insert Template" command.
- You should now have the "OverPy starter pack" in your file. Press Ctrl+S to save it and to compile the starter pack to Workshop.
- The gamemode is now compiled and can be pasted directly into Overwatch.
-
If the text language of Overwatch is set to something other than English (not recommended for workshop) and you are unable to paste the gamemode, go to File -> Preferences -> Settings, type "overpy", then select the workshop language.
-
To import an existing gamemode, copy the Workshop code from Overwatch using the "copy settings" button.
- Then, in an empty .opy file (refer back to steps 4-5 to create one), press Ctrl+Shift+P, type "overpy", then select "Decompile".
- Your gamemode is now converted to OverPy and can be compiled by saving the file.
It is not recommended to constantly switch between Workshop and OverPy, as you will not be able to fully use OverPy to its full potential. Once you have decompiled your gamemode, you should not decompile it again.
You may get warnings when compiling; do not ignore them, as they can lead to bugs in your gamemode. If you are sure some warnings can be ignored, see below for how to disable them.
It is recommended to also decompile one of your existing gamemodes to get a better feel of the OverPy syntax.
# Line comments are made with "#".
# Multiline comments are made with /* */.
# A rule is declared with "rule" followed by the rule name and a colon ":".
# The contents of a rule MUST always be indented.
# The default indentation is 4 spaces.
# Do not change it, as a tab is considered equivalent to 4 spaces.
rule "setup":
disableInspector() #don't forget to add that to your gamemodes
# Rule metadata is specified with annotations, starting with "@".
# The @Event annotation specifies the event of the rule, same for @Team, @Hero and @Slot.
# The @Condition annotation specifies a rule condition. You can have multiple conditions.
rule "player has spawned":
@Event eachPlayer
@Condition eventPlayer.hasSpawned()
@Condition eventPlayer.A == 1
@Team 1
@Hero widowmaker
@Slot 11
# You can suppress a warning within the rule with the @SuppressWarnings annotation,
# followed by the warning names separated by spaces.
@SuppressWarnings w_player_closest_to_reticle w_wait_9999 #don't do this, it's just for the example
eventPlayer.B = eventPlayer.getPlayerClosestToReticle(Team.ALL)
wait(9999)
# A subroutine is declared with the "def" keyword, but follows the same format as a rule.
# Note that due to Workshop limitations, no parameters or return values can be specified.
def teleportPlayerToSpawn():
@Name "Subroutine: Teleport the player to spawn"
eventPlayer.teleport(vect(10, 0, 10))
# By default, optimization is enabled, meaning empty rules will not appear in the compilation output.
# To prevent this, you can add the @Delimiter annotation.
rule "---------- Boss fight code ----------":
@Delimiter
@Disabled
@NewPage #adds empty rules to put the rule at the top of a page
# You can declare variables using the globalvar/playervar keywords, followed by an optional index.
# Default variable names (A-Z, AA-DX) do not need to be declared.
globalvar gameStatus 2
playervar score #index is automatically calculated
# You can also directly initialize a variable.
globalvar spawnLocation = vect(10, 0, 10)
rule "functions":
@Event eachPlayer
# The vast majority of functions follow these rules:
# 1. The function name is camelCased.
# Create Effect() -> createEffect().
setMatchTime(3600)
# 2. Function names taking a player, array or string as a first argument are "member functions".
# Teleport(Event Player, Vector(10,0,10)) -> eventPlayer.teleport(vect(10,0,10))
eventPlayer.cancelPrimaryAction()
# 3. Functions to get constants such as Hero(), Map() or Gamemode() are replaced by enums.
# Map(Hanamura) -> Map.HANAMURA.
eventPlayer.startForcingHero(Hero.ANA)
# 4. Operators and some functions have significant syntax changes.
# You can expand the list below to see all such functions.
# Some functions also have default values.
wait() #Equivalent to wait(0.016, Wait.IGNORE_CONDITION)
# You can also specify individual arguments using the name=value syntax.
# hudHeader() is a macro for hudText() that only displays the header.
hudHeader(getAllPlayers(), "Some text", sortOrder=5, color=Color.YELLOW)
hudHeader(text="Some text")
List of functions with a significant syntax/name change (click to expand)
If a function is not in that list, then the name is the English name in camelCase; simply rely on the autocompletion to find it. (Also type a "." to check if it is a member function.)
Workshop function | OverPy function |
---|---|
Abort |
return |
Abort If |
if condition: return |
Abort If Condition Is False |
if not ruleCondition: return |
Abort If Condition Is True |
if ruleCondition: return |
Absolute Value |
abs() |
Add(A, 3) |
A + 3 |
And(A == 2, B == 4) |
A == 2 and B == 4 |
Append To Array(array, value) |
array.concat(value) |
Arccosine In Degrees |
acosDeg() |
Arccosine In Radians |
acos() |
Arcsine In Degrees |
asinDeg() |
Arcsine In Radians |
asin() |
Arctangent In Degrees |
atan2Deg() |
Arctangent In Radians |
atan2() |
Array(a, b, c) |
[a, b, c] |
Array Contains(array, value) |
value in array |
Array Slice(array, start, count) |
array.slice(start, count) |
Assist Count |
getNumberOfAssistIds() |
Attach Players |
player.attachTo() |
Backward |
Vector.BACKWARD |
Button(Interact) |
Button.INTERACT |
Call Subroutine(subroutine) |
subroutine() |
Char In String(str, index) |
str.charAt(index) |
Color(White) |
Color.WHITE |
Compare(a, ==, b) |
a == b (same for !=, <, etc) |
Cosine From Degrees |
cosDeg() |
Cosine From Radians |
cos() |
Count Of |
len() |
Current Array Element |
See Filtered Array |
Current Array Index |
See Filtered Array |
Custom Color |
rgb() |
Custom String("Score: {0}", Score Of(Event Player)) |
"Score: {}".format(eventPlayer.getScore()) See Strings for more info. |
Damage Modification Count |
getNumberOfDamageModificationIds() |
Damage Over Time Count |
getNumberOfDamageOverTimeIds() |
Detach Players |
player.detach() |
Disable Movement Collision With Environment |
player.disableEnvironmentCollision() |
Disable Movement Collision With Players |
player.disablePlayerCollision() |
Divide(A, 3) |
A / 3 |
Down |
Vector.DOWN |
Else |
else: |
Else If |
elif: |
Empty Array |
[] |
End |
Go back a level of indentation |
Entity Count |
getNumberOfEntityIds() |
Evaluate Once |
evalOnce() |
Filtered Array(array, Current Array Element == 2) Filtered Array(array, Current Array Element == 2 && Current Array Index > 4) |
[elem for elem in array if elem == 2] [elem for elem, index in array if elem == 2 and index > 4] The elem and index variables represent Current Array Element and Current Array Index respectively. You can name them however you like. |
First Of(array) |
array[0] |
For Global Variable(A, 0, 10, 1) For Global Variable(A, 1, 10, 1) For Global Variable(A, 0, 10, 2) |
for A in range(10): for A in range(1, 10): for A in range(0, 10, 2) The step can be omitted if it is 1, and the start can be omitted if it is 0 and step is 1. |
For Player Variable(Event Player, A, 1, 10, 2) |
for eventPlayer.A in range(1, 10, 2): |
Forward |
Vector.FORWARD |
Game Mode(Assault) |
Gamemode.ASSAULT |
Global.A |
A |
Global Variable(A) |
A |
Heal Over Time Count |
getNumberOfHealingOverTimeIds() |
Healing Modification Count |
getNumberOfHealingModificationIds() |
Hero(Ana) |
Hero.ANA |
Hero Being Duplicated |
player.getHeroOfDuplication() |
If(A == 2) |
if A == 2: |
If-Then-Else(A == 2, 3, 20) |
3 if A == 2 else 20 Note that the if/else operator has a low precedence. This means B + 3 if A == 2 else 20 will be parsed as (B + 3) if A == 2 else 20 . It is customary to wrap the if/else operator with parentheses. |
Index Of Array Value(array, value) |
array.index(value) |
Index Of String Char(str, char) |
str.strIndex(char) |
Is Button Held |
player.isHoldingButton() |
Is In Line of Sight |
isInLoS() |
Is Portrait On Fire |
player.isOnFire() |
Is True For All(array, Current Array Element == 2) |
all([elem == 2 for elem in array]) |
Is True For Any(array, Current Array Element == 2 && Current Array Index > 4) |
any([elem == 2 and idx > 4 for elem, idx in array]) Also see Filtered Array. |
Last Of(array) |
array.last() |
Left |
Vector.LEFT |
Log To Inspector |
printLog() |
Loop If(A == 2) |
if A == 2: loop() |
Loop If Condition Is False |
if not ruleCondition: loop() |
Loop If Condition Is True |
if ruleCondition: loop() |
Magnitude Of |
magnitude() |
Map(Workshop Island) |
Map.WORKSHOP_ISLAND |
Mapped Array(array, Current Array Element + 2) Mapped Array(array, Current Array Element * Current Array Index) |
[elem+2 for elem in array] [elem * idx for elem,idx in array] |
Modify Global Variable(A, Add, 2) |
A += 2 |
Modify Global Variable(A, Append To Array, 2) |
A.append(2) |
Modify Global Variable(A, Divide, 2) |
A /= 2 |
Modify Global Variable(A, Max, 2) |
A max= 2 |
Modify Global Variable(A, Min, 2) |
A min= 2 |
Modify Global Variable(A, Modulo, 2) |
A %= 2 |
Modify Global Variable(A, Raise To Power, 2) |
A **= 2 |
Modify Global Variable(A, Remove From Array By Index, 2) |
del A[2] |
Modify Global Variable(A, Remove From Array By Value, 2) |
A.remove(2) |
Modify Global Variable(A, Subtract, 2) |
A -= 2 |
Modify Global Variable At Index(A, 1, Append To Array, 2) |
A[1].append(2) |
Modify Global Variable At Index(A, 1, Remove From Array By Index, 2) |
del A[1][2] |
Modify Player Score |
player.addToScore() |
Modify Player Variable(Event Player, A, Max, 2) |
eventPlayer.A max= 2 |
Modify Player Variable At Index(Event Player, A, 1, Remove From Array By Value, 2) |
eventPlayer.A[1].remove(2) |
Modify Team Score |
addToTeamScore() |
Modulo(A, B) |
A % B |
Move Player to Team |
moveToTeam() |
Multiply(A, B) |
A * B |
Not(a) |
not a |
Number(1234) |
1234 |
Objective Index |
getCurrentObjective() |
Objective Position |
getObjectivePosition() |
Opposite Team Of(Team Of(Event Player)) |
getOppositeTeam(eventPlayer.getTeam()) Or for a player: eventPlayer.getOppositeTeam() |
Or(A == 2, B == 4) |
A == 2 or B == 4 |
Player Carrying Flag |
getFlagCarrier() |
Player Variable(Event Player, A) |
eventPlayer.A |
Press Button |
player.forceButtonPress() |
Raise To Power(A, 2) |
A ** 2 |
Random Integer |
random.randint() |
Random Real |
random.uniform() |
Random Value In Array |
random.choice() |
Randomized Array |
random.shuffle() |
Raycast Hit Normal(start, end, include, exclude, includePlayerObjects) |
raycast(start, end, include, exclude, includePlayersObjects).getNormal() |
Raycast Hit Player(start, end, include, exclude, includePlayerObjects) |
raycast(start, end, include, exclude, includePlayersObjects).getPlayerHit() |
Raycast Hit Position(start, end, include, exclude, includePlayerObjects) |
raycast(start, end, include, exclude, includePlayersObjects).getHitPosition() |
Remove From Array(array, 2) |
array.exclude(2) |
Remove Player |
player.removeFromGame() |
Right |
Vector.RIGHT |
Round To Integer(A, Up) |
ceil(A) |
Round To Integer(A, Down) |
floor(A) |
Round To Integer(A, To Nearest) |
round(A) |
Set Environment Credit Player |
player.setEnvironmentalKillCreditor() |
Set Global Variable(A, 2) |
A = 2 |
Set Global Variable At Index(A, 1, 2) |
A[1] = 2 |
Set Player Variable(Event Player, A, 2) |
eventPlayer.A = 2 |
Set Player Variable At Index(Event Player, A, 1, 2) |
eventPlayer.A[1] = 2 |
Sine From Degrees |
sinDeg() |
Sine From Radians |
sin() |
Skip(A + 3) |
goto loc+A+3 |
Skip(2) A = 1 B = 2 C = 3 |
goto label A = 1 B = 2 label: C = 3 If the argument of the Skip() action is a number, you can instead specify a label within the rule (followed by a colon ":") and OverPy will automatically calculate how many actions to jump. |
Skip If(A == 2, 4) |
if A == 2: |
Sorted Array(array, Current Array Element.score) |
sorted(array, key=lambda elem: elem.score) |
Sorted Array(array, -1 * Current Array Index) |
sorted(array, key=lambda elem, idx: -idx) Or in this specific case: array.reverse() The elem and idx variables represent the Current Array Element and Current Array Index values respectively. You can name them however you like. |
Square Root |
sqrt() |
Start Assist |
player.startGrantingAssistFor() |
Start Forcing Dummy Bot Name |
player.startForcingName() |
Start Holding Button |
player.startForcingButton() |
Start Modifying Hero Voice Lines |
player.startModifyingVoicelinePitch() |
Start Rule(subroutine, behavior) |
async(subroutine, behavior) |
Start Scaling Player |
player.startScalingSize() |
Stop Holding Button |
player.stopForcingButton() |
Stop Modifying Hero Voice Lines |
player.stopModifyingVoicelinePitch() |
String("Hello") |
l"Hello" Note: those are "localized strings" and should never be used. Use Custom String instead (don't prefix strings by "l"). |
String Contains |
strContains() |
String Length |
strLen() |
String Replace(Custom String("abc"), Custom String("a"), Custom String("d")) |
"abc".replace("a", "d") |
String Slice(Custom String("abc"), 1, 2) |
"abc".substring(1, 2) |
String Split(Custom String("abc"), Custom String("b")) |
"abc".split("b") |
Subtract(A, 2) |
A - 2 |
Tangent From Degrees |
tanDeg() |
Tangent From Radians |
tan() |
Team(Team 1) |
Team.1 |
Text Count |
getNumberOfTextIds() |
Up |
Vector.UP |
Value In Array(A, 3) |
A[3] |
Vector |
vect() |
Weapon |
player.getCurrentWeapon() |
While(A == 2) |
while A == 2: |
X Component Of(vector) |
vector.x |
Y Component Of(vector) |
vector.y |
Z Component Of(vector) |
vector.z |
OverPy supports including custom game settings, using the settings
keyword:
settings {
"main": {
"description": "Some awesome game mode"
},
"gamemodes": {
"skirmish": {
"enabledMaps": [
"workshopIsland"
]
},
"general": {
"heroLimit": "off",
"respawnTime%": 30
}
}
}
However, there are quite a lot of changes regarding the syntax, and it is recommended that you edit settings within Overwatch then use the decompile command to convert to OverPy.
Extensions are activated using the #!extensions
compiler directive.
An if/elif/else statement is simply represented by the following structure:
if A == 1:
B = 2
elif A == 2:
B = 3
elif A == 3:
B = 4
else:
B = 5
A while loop is represented by the following structure:
while A == 1:
B = 2
wait()
It compiles to the workshop's While
instruction.
A for loop is represented by the following structure:
globalvar i
rule "for loop":
for i in range(1, 5, 2):
B.append(i)
#B = [1,3,5]
Note that the range(start, stop, step)
function, that can only be used here, has other forms:
for i in range(1,5) -> for i in range(1,5,1)
for i in range(5) -> for i in range(0,5,1)
As such, you can use the range(start, stop)
and range(stop)
signatures.
Although not recommended to use, gotos can be used in conjunction with a label. They are the equivalent of the Workshop's Skip
instruction.
For example:
if A == 4:
goto lbl_0
B = 5
lbl_0:
C = 6
Labels are declared on their own line, and must include a colon at the end (but no additional indentation).
Due to the limitations of the workshop, labels must be in the same rule as the goto
instruction, and cannot be before it.
Additionally, dynamic gotos can be specified using the special keyword loc
:
goto loc+A
This is however not recommended and can lead to very confusing code; you should consider using switches instead.
These statements are compiled to Skip
or Skip If
instructions.
OverPy guarantees a fixed amount of instructions:
- Each logical line counts for one instruction, except for the
switch
statement which counts for 2 instructions. - Each unindent counts for one instruction.
This is true regardless of optimizations.
Strings are written with single or double quotes, using the backslash character to escape: "string \"with quotes\"\nand a newline"
.
To concatenate strings, the .format()
function must be used with {}
or {number}
placeholders, such as "You have {} money and deal {} damage".format(eventPlayer.money, eventPlayer.damage)
.
The Workshop applies a limit of 128 characters and 3 placeholders per string. OverPy automatically splits a string if it goes beyond that limit; you can therefore write your strings with as many characters and as many placeholders as you want.
Successive strings are concatenated: "string1""string2"
will resolve to "string1string2"
.
The escape sequence \n
can be used to make a newline. Strings are also multiline by default (no need for triple quotes), therefore a literal newline within a string will be compiled as a \n
.
The escape sequences \xHH
(byte escape) and \uHHHH
(unicode escape) are also supported, where H
is an hexadecimal digit. For example, "\u3000"
will output a fullwidth space.
String entities, using the escape sequence \&
, can also be used to not have to look up unicode characters: "\&black_square;"
is the same as "■"
.
The escape sequences \t
, \r
, and \z
map respectively to a tab, a carriage return, and a zero-width space.
String modifiers are placed just before the string, such as w"fullwidth string"
. There are currently 5 string modifiers:
- The
l
string modifier specifies a localized string (String
function in the workshop). Localized strings are not recommended as they are limited to a specific subset of predefined strings. They are only included for legacy gamemodes. - The
w
string modifier makes the string fullwidth. - The
b
string modifier forces, when possible, the use of the "big letters" font (Blizzard Global font). - The
c
string modifier makes the string case-sensitive, using special latin characters that are not uppercased. It however shows diacritics above most letters, and only works with the latin alphabet. - The
t
string modifier specifies that the string should be included in translations (see translations.)
The #!setupTags
directive can be used to use textures and colors in strings.
If this directive is used, OverPy will automatically generate the rule to obtain an unsanitized "<" character, and automatically replace it in strings if they are the start of a tag.
For color, use the <fgRRGGBBAA> tag, where RR/GG/BB are the hex color value, and AA is the hex transparency value (00 = transparent, FF = opaque).
Example: print('<fgFF0000FF>Red text</fg>')
For textures, use the <TX> standalone tag, with the texture id as seen in https://workshop.codes/wiki/articles/tx-reference-sheet.
Example: print('<TXC0000000002DD21>')
will display the mouse cursor texture.
Additionally, you can use the Texture
enum and OverPy will automatically optimize it.
For example: "Mouse cursor texture: {}".format(Texture.MOUSE_CURSOR)
OverPy will also replace '<tx1234>'
to the correct full texture id, but only if the entire tag is inside a string ('<tx{}>'.format(id)
will not work, but '<tx{}>'.format(1234)
will).
Compiler options begin with the #!
operator, not indented.
Inserts the text of the specified file. The file path can be relative; if so, it is relative to the main file. For example:
#!include "heroes/zenyatta.opy"
/* includes a file in the parent directory (".." is the parent directory) */
#!include "../main.opy"
/* will include all files in the "heroes" directory */
#!include "heroes/"
Specifies an .opy file as the main file (implying the current file is a module). This directive MUST be placed at the very beginning of the file. For example:
#!mainFile "../main.opy"
This is so that you can compile your gamemode from any file and OverPy knows the starting point to use.
Suppresses the specified warnings globally across the program. Warnings must be separated by a space. Example:
#!suppressWarnings w_type_check w_unsuitable_event
By default, OverPy automatically optimizes gamemodes for speed. This means useless code is converted, calculations are done when possible, and function patterns are replaced with builtin functions.
For example:
A = A * 1 + 0
will not be included, as multiplying by 1 and adding 0 does nothing, and it is an assignment to itself.A = 10*10+20
will compile toA = 120
.not eventPlayer.isDead()
will compile toeventPlayer.isAlive()
.
You can check here for a list of optimizations.
The #!disableOptimizations
directive can be used to disable all optimizations done by the compiler. Should be only used for debugging, if you suspect that OverPy has bugs in its optimizations.
The #!optimizeForSize
directive prioritizes lowering the number of elements over optimizing the runtime (see here for a list of optimizations).
Several compiler options are available to automatically replace some constants, in order to save elements:
#!replaceTeam1ByControlScoringTeam
#!replace0ByCapturePercentage
#!replace0ByPayloadProgressPercentage
#!replace0ByIsMatchComplete
#!replace1ByMatchRound
These replacements should only be used if the requirements for the replacement are met. They are detailed in the autocomplete description of each compiler option.
Size optimizations must be enabled for these replacements to work (with #!optimizeForSize
).
Preprocessing is an important part of OverPy and allows extending its power way beyond the limitations of the workshop.
Normal macros are declared with the #!define
directive:
#!define TEAM_HUMANS 1
#!define TEAM_ZOMBIES 2
This is primarly useful for declaring constants.
The #!defineMember
directive behaves exactly the same as #!define
, except the VS Code extension will put the autocompletion in the dot trigger. This is primarly useful for vectors: if you have a vector that stores 3 distinct numbers, you can do #!defineMember someVar x
to do vector.someVar
instead of vector.x
.
Function macros are declared with the #!define
directive, but have parameters for the macro:
#!define setUsefulVars(x) hasFirstInfectionPassed = x\
currentSection = x\
firstInfectionLoopIndex = x\
countdownProgress = x\
roundWinners = x
Note the usage of the backslashed lines to make a multi-line macro.
Warning: Always wrap macros and macro arguments with parentheses when possible, as macros are a literal replacement!
For example, given this function:
#!define sum(a,b) a+b
then sum(3, 4) * 3
will be resolved as 3 + 4 * 3
and sum(3, 4 if A else 3)
will be resolved as 3 + 4 if A else 3
.
This macro should be declared instead as #!define sum(a,b) ((a)+(b))
: parentheses around the whole macro definition, and around each argument.
You can do script macros with the special __script__
function. For example:
#!define addFive(x) __script__("addfive.js")
The content of addfive.js
is simply x+5
(no return
!)
For the technical details:
- Arguments are automatically inserted into the script (in this case,
var x = 123;
would be inserted at the top of the script) - A
vect()
function is automatically inserted, so thatvect(1,2,3)
returns an object with the correct x, y, and z properties andtoString()
function - The script is then evaluated using a JavaScript interpreter
Switches are a good way to transform an if/elif/else chain into something more optimized:
switch A:
case Hero.ANA:
B = 2
break
case Hero.ASHE:
B = 3
break
default:
B = 4
This is equivalent to:
if A == Hero.ANA:
B = 2
elif A == Hero.ASHE:
B = 3
else:
B = 4
However, the switch statement has the advantage of not recalculating A
.
Keep in mind that fallthrough is enabled, so if you don't have a break
instruction, the execution continues to the next case
statement.
In the above case, we always set the same variable. Therefore, a dictionary can be used:
B = {
0: 4,
Hero.ANA: 2,
Hero.ASHE: 3,
}[A]
If the key is not found in the dictionary, null
will be returned. However, you can specify a default
key for a default value.
This dictionary is internally converted to:
B = [4, 2, 3][[0, Hero.ANA, Hero.ASHE].index(A)]
Therefore, a dictionary cannot be declared alone; it must always be accessed.
Enums can be declared to avoid manually declaring several macros:
enum GameStatus:
GAME_NOT_STARTED = 1
GAME_IN_PROGRESS
GAME_FINISHED
enum Team:
HUMANS = Team.2
ZOMBIES = Team.1
rule "Kill zombies when game has finished":
@Condition gameStatus == GameStatus.GAME_FINISHED
kill(getPlayers(Team.ZOMBIES), null)
If an enum member is not given a value, it will take the previous value plus 1 (or 0 if it is the first value). You can also extend existing enums, such as the Team
enum in this example.
You can use len(GameStatus)
to have the amount of values in the enum and GameStatus.toArray()
to get an array of the values. This is useful to iterate on the values.
OverPy includes some built-in macros for commonly used patterns.
The createCasedProgressBarIwt
function can be used to overlay multiple texts and display pixel-perfect lowercase text. It can however only be used with the Blizzard Global font.
Returns a string made of spaces that is the same length as the provided string. The provided string must be a literal string.
NOTE: The displayed string MUST be in the Blizzard Global font (use the 'b' string modifier on the final string, unless using a progress bar). The casing of the string is also respected.
The spacesForLength
and strVisualLength
macros can also be used. spacesForString
is the combination of these two.
This is useful to do alignment tricks. For example, the following code displays a key:value attribute list:
#We use strVisualLength("M")*20 to specify a string that is longer than the longest key/value.
#!define dictLine(key, value) "{}{}{}{}".format(spacesForLength(strVisualLength("M")*20 - strVisualLength(key)), (key), (value), spacesForLength(strVisualLength("M")*20 - strVisualLength(value)))
rule "iwt":
createProgressBarInWorldText(text=b" \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n{}\n{}\n{}".format(
#If not using createCasedProgressBarIwt then you have to put the strings in uppercase
dictLine("ABILITY 1: ", "KILLAURA"),
dictLine("ABILITY 2: ", "WALLHACK"),
dictLine("ULTIMATE: ", "CRASH THE SERVER")
), position=vect(0,0,0), scale=2)
The ruleCondition
value compiles to all the conditions of the rule, joined with and
.
This is mostly useful in waitUntil
, so you can do the following:
rule "":
@Event eachPlayer
@Condition ...
@Condition ...
@Condition ...
eventPlayer.startHealingOverTime(null, Math.INFINITY, 30)
eventPlayer.hotId = getLastHealingOverTimeId()
waitUntil(not ruleCondition)
#cleanup
stopHealingOverTime(eventPlayer.hotId)
You can use the compressed()
function to store a large array of numbers or vectors.
For example:
mapData = compressed([vect(-65.048, -18.007, -80.036), vect(0.92, 12.58, -18.32), vect(194.041, 6.128, -74.097), ...])
OverPy will store the array in a string and automatically decompress it, which takes much fewer elements.
For more control over the compression (eg if you have separate arrays to compress), you can use the compress
and decompressNumbers
/decompressVectors
functions.
Maps an array of dictionaries to variables. For example:
splitDictArray({
hero: waveHeroes,
length: waveLengths
}, [
{hero: Hero.ANA, length: 3},
{length: 8, hero: Hero.SOLDIER},
{hero: Hero.HAMMOND}
])
Will yield the following:
waveHeroes = [Hero.ANA, Hero.SOLDIER, Hero.HAMMOND]
waveLengths = [3, 8, null]
If the third argument is specified and set to true
, string compression is automatically used for number and vector arrays.
The Workshop only allows you to directly assign to a 1d array, but thanks to Mira, assignments to 2d/3d arrays are supported:
array[i][j] = value
will be compiled toarray[i] = array[i].slice(0, j).concat([value]).concat(array[i].slice(j+1))
array[i][j][k] = value
will be compiled toarray[i] = array[i].slice(0, j).concat([array[i][j].slice(0, k).concat([value]).concat(array[i][j].slice(k+1))]).concat(array[i].slice(j+1))
The Math
enum contains useful constants, such as Math.INFINITY
and Math.FUCKTON_OF_NEWLINES
. See the autocompletion for more info.
Function | Description |
---|---|
Player.getOppositeTeam() |
Returns the opposite team of a player. |
Player.getEffectiveHero() |
Returns the hero the player is duplicating (if using Echo ultimate) or the normal hero of a player. |
getSign() |
Returns the sign of a number (-1, 0 or 1). |
hudHeader() , hudSubheader() , hudSubtext() |
Built-in macros for hudText() to reduce the number of arguments. You can specify HudPosition.ACTUALLY_LEFT to fix the bug where HUDs are centered relative to each other, but you have to specify it on each of the left HUDs. |
lineIntersectsSphere() |
Determines if a line intersects a sphere. Can be used to check if a player is looking at a specific point. |
print() |
Creates an orange HUD text for quick debugging of a value. |
debug() |
You will likely want to use this rather than print() . This function displays the value and also displays the arguments passed to it, so debug(A+2) will display A + 2 = 4 instead of just 4 . |
Array.reverse() |
Reverses the array. |
Array.unique() |
Removes duplicates from the array. |
timeToString() |
Converts a time (in seconds) to a H:MM:SS string. For example, timeToString(3600+120+37.65) will return 1:02:37.65 . |
arrayToString() |
Converts an array to a string (otherwise, only the first element is displayed). |
buttonToString() |
A better version of Input Binding String() which automatically adds brackets (but not if the button is a texture) and replaces LSHIFT/LCONTROL/LALT by SHIFT/CTRL/ALT. |
hsl() |
Specifies a color in HSL format. |
log() |
Calculates the natural logarithm of a given number. |
Feel free to open an issue if you'd like an additional macro to be added.
Use the #!translations
directive to setup the translation system. Arguments are the language codes separated by spaces. For example:
#!translations en fr es zh_cn
Only the es_mx
, es_es
, zh_cn
and zh_tw
languages can be specified fully. For the rest, you can only specify the first two letters.
To translate a string, wrap it with the _
function, such as _("You have \${} money").format(money)
. Note that the formatter has to be outside of the function. You can also use the "t" string modifier, such as t"\${} money".format(money)
.
If two strings are the same but have to be translated differently, you can add a context string as the first argument, such as _("the direction", "left")
.
Lastly, if a translated string is stored in a variable, you have to use the _
function when displaying it, such as hudHeader(text=_(someVariable))
. Else, "TLErr" will be displayed. Note that you also have to use the _
function when storing the string in the variable, else "0" will be displayed.
OverPy will generate and parse .po files for each language based on the name of the main file. You can then use an online editor to edit those files, such as https://pofile.net/free-po-editor or https://localise.biz/free/poeditor. Leading and trailing whitespace is automatically stripped from the string when put into translation files.
WARNING: A translated string cannot be used as a normal string when stored in a variable, as it becomes a string array. This means you cannot use .replace()
, .charAt()
, etc. When translating your gamemode, look out for these functions.
This also means that, when used in a variable, you cannot use a translated string as an argument of a string: "{}{}".format(t"string", 1234)
will not work. Instead, do t"string{}".format(1234)
. The translated string must always be top-level. You will also get "TLErr" if trying to use a translated string as an argument for another function.
Note: The way string formatting works is via the .replace() function and some constants. This means you cannot have the following in your translated strings if using formatters:
(0.00, 1.00, 0.00)
(Vector.UP
)(0.00, -1.00, 0.00)
(Vector.DOWN
)(1.00, 0.00, 0.00)
(Vector.LEFT
)(-1.00, 0.00, 0.00)
(Vector.RIGHT
)(0.00, 0.00, 1.00)
(Vector.FORWARD
)(0.00, 0.00, -1.00)
(Vector.BACKWARD
)1876650.25
1876651.25
1876652.25
1876653.25
1876654.25
1876655.25
1876656.25
1876657.25
1876658.25
1876659.25
Last, you can use the #!translateWithPlayerVar
directive to store the player's language in a variable and save on elements, but it is potentially invasive (although it should work with the vast majority of gamemodes).
In summary:
- Use the
#!translations
directive to setup translations, and#!translateWithPlayerVar
if you are short on elements. - If a string is within a display action (
bigMessage()
,hudText()
,createInWorldText()
, etc), simply prefix it witht
or wrap it with the_
function, and it will work. - Otherwise, if the string is assigned to a variable, you have to wrap the variable with the
_
function when displaying it, as well as check if the string is not used as a string formatter or that string operations aren't applied on it. If there are, you need to refactor your code.
Technical details
The way translations work is by casting a value to string (usually Color.WHITE
) and checking what is returned. The cast to string is done with the player's language, so it will return "White" if on English, "Blanc" if on French, etc.
However, this cast to string must be done client-side. You cannot do the following:
# Will not work properly, do not use!
eventPlayer.language = ["White", "Blanc"].index("{}".format(Color.WHITE))
This will appear to work, but this is evaluated server-side, and what it actually does is set the language to the language of the host (more precisely the language of whoever last imported or edited the gamemode). This means everyone will play as one single language, which defeats the whole point of translations.
When in a reevaluated HUD text, t"Some string"
resolves to:
["Some string", "Une chaîne"][["White", "Blanc"].index("{}").format(Color.WHITE)]
This works as reevaluated text is evaluated client-side. To check if something is evaluated client-side, check if inputBindingString
isn't 0 or localPlayer
isn't 0.
When in a variable, text = t"Some string"
just resolves to text = ["Some string", "Une chaîne"]
. We cannot access the array now as we do not know the player's language, and so we need to access it in the HUD text. So _(text)
will then resolve to text[["White", "Blanc"].index("{}").format(Color.WHITE)]
.
Since we are dealing with the array form of the translated string between declaration and display, we cannot perform any operations on it like we would on a normal string.
There is however a way of transferring data from client to server. Most reevaluated actions are evaluated client-side, and this includes .startFacing()
. By setting the player's facing direction to a specific angle based on the language, then getting the angle of the player's facing direction, we can know the language index. This is what #!translateWithPlayerVar
does, and this avoids repeating the language resolution formula at each translated string.
Note: I used arrays in the example. OverPy concatenates arrays to strings using .split()
and does various other tricks to save elements, but the principle is the same.
If you are still confused about something, or want to discuss a feature, please join the discord #hll-scripting :)