SKILL.md
Declarations
my_variable: int = 42; // Explicit type
my_variable := 42; // Type inferred
my_variable: int; // Zero-initialized
Integer literals coerce to float, but not the reverse.
Struct fields cannot have inline defaults.
Constants
Define const with :: Must be compile-time constant.
Global variable initializers must be compile-time constants. Zero-initialized file-scope vars are allowed for game-wide handles/registries, then assign them in aostart or aobeforesceneload for runtime init. Do not use globals for per-player state; store that on the player/component.
PI is already defined as a global.
Types
Primitive Types
- Signed integers:
s8,s16,s32,s64 - Unsigned integers:
u8,u16,u32,u64 - Booleans:
bool - Floats:
f32,f64 - Vector types:
v2,v3,v4(float fields.x,.y,.z,.w; constructed withv2{10, 20}) string,typeid,any
Strings and Template Strings
"..." strings support backslash escapes (\n, \t, \\, ...). Backtick strings are raw (no backslash escapes, newlines allowed) and support {expr} interpolation:
name := "Ada";
count := 0;
msg := `Hi {name}, you have {count + 1} new messages`; // any expression works inside {}
- Interpolating templates compile to a
format_stringcall, so they needimport "core:basic"and are not compile-time constants. - A backtick string with no
{expr}stays a plain raw string constant. - Literal braces:
{{and}}. A single}in text is a compile error. - Backtick strings cannot nest inside
{}(use"..."strings there).
Structs
Structs are value types (shallow-copied on assignment/pass).
Food_Definition :: struct {
name: string;
food_value: int;
}
Classes: reference types allocated with new
Foo :: class {
value: int;
position: v2;
}
foo := new(Foo);
foo: Foo = new();
foo: Foo = new(Foo);
Inheritance
Dog :: class : Animal {
breed: string;
}
Procedures
add :: proc(a: int, b: int) -> int {
return a + b;
}
Methods
Dog :: class {
name: string;
bark :: method() {
log_info(`{name} says bark!`); // implicit this.name
}
}
dog := new(Dog);
dog.bark();
Arrays
- Fixed:
[N]Tinitialized with{...} - Slice:
[]Ta view into array data (common for parameters) - Dynamic:
[..]Tresizable list (.count,.capacity); implicitly converts to[]T
fixed: [4]int = {1, 2, 3, 4};
spawn_points: [3]v2 = {{0, 0}, {5, 0}, {0, 5}};
dyn: [..]int;
view: []int = dyn;
hit := Damage_Desc{amount=10, knockback={2, 1}}; // named fields use = not :
Dynamic Arrays
numbers: [..]int;
numbers.append(10);
numbers.pop();
numbers.clear();
numbers.reserve(64);
numbers.unordered_remove_by_value(10);
numbers.ordered_remove_by_value(999, .ALL);
numbers.unordered_remove_by_index(0);
numbers.ordered_remove_by_index(0);
Control Flow
Conditions normally omit parentheses. Enum values use . prefix in switch:
Compound literals in control-flow conditions
A compound literal immediately before a control-flow body is ambiguous. This is the exception to the normal no-parentheses style: disambiguate with .{}, Type.{}, or parentheses.
if value == .{} {}
if value == v2.{} {}
if (value == v2{}) {}
switch tier {
case .COMMON: return {0.7, 0.7, 0.7, 1.0};
case .RARE: return {0.3, 0.5, 1.0, 1.0};
default: return {1.0, 1.0, 1.0, 1.0};
}
Cases support multiple values (comma-separated) and ranges (.., inclusive):
switch level {
case 1, 2, 3: tier = .BEGINNER;
case 4..10: tier = .INTERMEDIATE;
case 11..20, 25, 30..50: tier = .ADVANCED;
default: tier = .UNKNOWN;
}
Multi-statement bodies use braces
switch tier {
case .COMMON, .UNCOMMON: {
color = {0.7, 0.7, 0.7, 1.0};
label = "Common";
}
default: {
color = {1.0, 1.0, 1.0, 1.0};
label = "Unknown";
}
}
Do not write C-style fallthrough logic
for also handles custom iterators: for player: componentiterator(MyPlayer) { } Numeric loops use for i: 0..3 { } (inclusive) or for i: 0..<3 { } (half-open), never for i in ....
Custom iterator-based for loops require a next :: method() -> bool and a current field.
Item_Tier :: enum {
COMMON;
RARE;
}
if !#alive(target) return;
UI.push_screen_draw_context();
defer UI.pop_draw_context();
#alive(expr) checks whether a class reference is still valid and MUST be used before accessing to prevent crashes. defer runs a statement when the current scope exits.
Type Casting
Use expr.(T) syntax: b := 123.4.(int);
Parameter Passing: ref
Mark both the parameter and callsite with ref. When forwarding a ref param use ref again:
update_health :: proc(health: ref int, damage: int) {
health -= damage;
}
hp := 100;
update_health(ref hp, 25);
Polymorphic Procedures
$T on a parameter deduces the type from the callsite. $T is only used when defining polymorphic procs — callers always pass concrete types:
min :: proc(a: $T, b: T) -> T {
if a < b return a;
return b;
}
result := min(3, 5); // T is deduced as int
Tis a type only inside a polymorphic declaration that binds$T; it is not a generic placeholder elsewhere. In concrete code use the actual type name, such ascomponent_iterator(Enemy)or[..]Enemy.
Function Pointers and Callbacks
CSL has no closures. Proc literals cannot capture enclosing parameters or locals. Pair a callback field with a userdata: Object field. Any class instance can be stored as Object and cast back:
on_death_userdata: Object;
on_death: proc(player: Player, userdata: Object);
// Setting the callback:
player.on_death_userdata = this;
player.on_death = proc(player: Player, userdata: Object) {
tracker := userdata.(Death_Tracker);
tracker.death_count += 1;
};
Using Keyword - access fields without a selector:
using position: v3;
x = 123; // Instead of position.x
Runtime Type Checking
Use .#type to get the runtime type of a class instance:
if effect.#type == Slow_Effect {
slow := effect.(Slow_Effect);
}
Multiple Return Values
get_thing :: proc() -> Thing, bool {
return g_thing, true;
}
thing, ok := get_thing();
if thing, ok := get_thing(); ok { }