Scripting with Cog
The language
Cog is built to be read. It takes its structure from Rust (fn, for x in,
if as a value) and its surface from TypeScript and C# (var, name: type,
class), and it spells and, or and not as words: not grounded reads
better than !grounded.
Statements and comments
A statement ends at the end of its line; a ; puts two on one line. Blocks
are always in braces, even a block of one line. // starts a comment.
// Move up, then stop.
var height = 0.0
if height < 10.0 { height = height + 1.0 }Values
| Kind | Looks like |
|---|---|
| Nothing | null |
| True or false | true, false |
Whole number (int) | 3, -12 |
Number with a fraction (float) | 3.0, 0.25 |
Text (string) | "Hello" |
| Array | [1, 2, 3] |
| Object | { x: 1, y: 2 } |
| Vector, rotation, color | Vec3(1.0, 0.0, 2.0), Rotation.from_euler(0, 90, 0), Color.from_hex("#ffcc00"): see Vectors, rotations and colors |
| Function | fn and (x) => x * 2, below |
Convert between them with int(x), float(x) and str(x).
Variables
var speed = 5.0 // can change
const MAX_HEALTH = 100 // can't
var name: string = "Ada" // with a type, checked when it's declared
A type after the name is checked: var speed: float = "fast" is an error. The
types are int, float, string, bool and any.
Variables at the top of a script, outside any function, are the script’s state: they last as long as the object does. Variables inside a function last until it returns.
Text
Put a value inside text with braces:
var keys = 2
log.info("Keys: {keys} / 3") // Keys: 2 / 3
log.info("A brace: {{") // A brace: {
+ joins text, and joins a number onto text. Text has methods, such as
name.starts_with("Key_"), s.upper() and s.split(","); the full list is
in the Cog API.
Operators
| Operators | Meaning |
|---|---|
+ - * / % | Arithmetic. % is the remainder. |
== != < <= > >= | Comparison. |
and or not | Logic. |
and and or give back the value that decided them, so player or nearest() is the player if there is one, and otherwise nearest(), which
only runs if it’s needed.
Vectors take + and - with each other, and * and / with a number:
self.position + Vec3.up * height.
If
var keys = 2
var keys_needed = 3
var escaped = false
if keys >= keys_needed {
escaped = true
} else if keys > 0 {
log.info("not yet")
} else {
log.info("no keys")
}
if also gives a value, when both branches are there:
var x = 12
var size = if x > 10 { "big" } else { "small" }Loops
var lives = 3
fn play_round() { lives = lives - 1 }
while lives > 0 { play_round() }
for enemy in Scene.with_tag("enemy") { enemy.send("alert") }
for i in range(5) { log.info(i) } // 0 to 4
for i in range(2, 10, 2) { log.info(i) } // 2, 4, 6, 8
break leaves a loop, continue skips to its next turn.
Functions
var keys = 0
fn take_key() {
keys = keys + 1
}
fn distance_to(other) -> float {
return self.position.distance(other.position)
}
A function is a value too: you can keep one in a variable, pass one to a timer, or write one in place:
var double = (x) => x * 2
var hide = () => { Scene.canvas("HUD").set_visible("message", false) }
self.after(2.0, hide)
A function written in place can read and change the script’s variables, and one made inside a loop keeps that turn’s values:
for i in range(3) {
self.after(i * 1.0, () => log.info(i)) // 0, then 1, then 2: not 3, 3, 3
}
Types on parameters and after -> are for you and your tools; they aren’t
checked.
Arrays and objects
var items = ["sword", "shield"]
items.push("potion")
log.info(items[0]) // sword
log.info(items.length) // 3
var stats = { health: 100, speed: 5.0 }
stats.health = stats.health - 10
if stats.has("speed") { }
Arrays have length, push, pop, contains, index_of, remove, map,
filter and copy; objects have keys, values, has and copy.
Errors
fn open(door) {
if door == null { throw "no such door" }
door.send("open")
}
try {
open(Scene.find("Gate"))
} catch (e) {
log.warn("Couldn't open: {e}")
}
An error nothing catches stops the hook it happened in, and the Console shows it once, with the file and line. The next frame runs the hook again.
Mistakes caught before you press Play
When a script loads, Cog reads all of it for mistakes it can prove without running it, and the Console lists each with its line:
- a name nothing declares, with a guess:
'helth' is not defined, did you mean 'health'? - a name declared twice at the top of the script
- the wrong number of arguments to one of the script’s functions, or to a
built-in one:
math.sqrt takes 1 argument, got 2
A script with one of these doesn’t load until it’s fixed. Assigning to a name
you never declared with var is a warning: it works, but it’s usually a typo.
Classes and behaviors
For state that wants its own methods, write a class. A behavior is a set of fields and methods that classes can share:
behavior Damageable {
var health: int = 100
fn hurt(n) { self.health = self.health - n }
}
class Turret with Damageable {
var range: float = 8.0
fn can_see(from, target) -> bool {
return from.distance(target.position) < self.range
}
}
fn fire() { log.info("Fire!") }
var turret = Turret()
turret.hurt(10)
if turret.can_see(self.position, Scene.find("Player")) { fire() }
Inside a method, self is the instance, and its fields are self.health,
not health. Most scripts don’t need classes; they’re here for the ones that
do.