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

KindLooks like
Nothingnull
True or falsetrue, 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, colorVec3(1.0, 0.0, 2.0), Rotation.from_euler(0, 90, 0), Color.from_hex("#ffcc00"): see Vectors, rotations and colors
Functionfn 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

OperatorsMeaning
+ - * / %Arithmetic. % is the remainder.
== != < <= > >=Comparison.
and or notLogic.

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.