pax_global_header00006660000000000000000000000064151223535240014514gustar00rootroot0000000000000052 comment=a8a3b29e493b8bfd6b9770d8ec9154e7c78277ed tomo-game/000077500000000000000000000000001512235352400127455ustar00rootroot00000000000000tomo-game/.gitignore000066400000000000000000000000171512235352400147330ustar00rootroot00000000000000.* !.gitignore tomo-game/CHANGES.md000066400000000000000000000001361512235352400143370ustar00rootroot00000000000000# Version History ## v2025-12-22 - Updated to latest Tomo syntax. ## v1.0 Initial version tomo-game/Makefile000066400000000000000000000003041512235352400144020ustar00rootroot00000000000000 game: game.tm box.tm color.tm player.tm world.tm tomo -e game.tm # Disable built-in makefile rules: %: %.c %.o: %.c %: %.o clean: rm -vf game *.tm.* play: game ./game .PHONY: play, clean tomo-game/README.md000066400000000000000000000007731512235352400142330ustar00rootroot00000000000000# Example Game This is a simple example game written in [Tomo](https://tomo.bruce-hill.com) that uses [raylib](https://www.raylib.com/) to demonstrate a project that spans multiple files and showcases some game programming concepts used in idiomatic Tomo code. It also showcases how to interact with an external C library. To run the game: ```bash tomo game.tm ``` An example [Makefile](Makefile) is also provided if you want to use `make` to build the game and `make clean` to clean up the built files. tomo-game/box.tm000066400000000000000000000003441512235352400141000ustar00rootroot00000000000000# Defines a struct representing boxes on the terrain use ./world.tm use ./raylib.tm struct Box(pos:Vector2, size=Vector2(50, 50), color=Color(0x80,0x80,0x80)) func draw(b:Box) DrawRectangleV(b.pos, b.size, b.color) tomo-game/game.tm000066400000000000000000000013421512235352400142200ustar00rootroot00000000000000# This game demo uses Raylib to present a simple maze-type game use ./raylib.tm use ./world.tm func main(map=(./map.txt)) InitWindow(1600, 900, CString("raylib [core] example - 2d camera")) map_contents := map.read() or exit("Could not find the game map: $map") world := @World( player=@Player(Vector2(0,0), Vector2(0,0)), goal=@Box(Vector2(0,0), Vector2(50,50), color=Color(0x10,0xa0,0x10)), boxes=@[], ) world.load_map(map_contents) SetTargetFPS(60) while not WindowShouldClose() dt := GetFrameTime() world.update(dt) BeginDrawing() ClearBackground(Color(0xCC, 0xCC, 0xCC, 0xFF)) world.draw() EndDrawing() CloseWindow() tomo-game/map.txt000066400000000000000000000011221512235352400142570ustar00rootroot00000000000000################################ #@ # # # # #### #### # # #### ##### # # # # # # # ####### # ####### # # ### # # # # # # # # #### ########## # #### # # # # # # # # #### # # # # # # # # ## # # # # # # # # ####### ########## # ######## # # # # # # ########## #### # # # # # # # # # # # # # # ####### # # ########## # # # # # # # # ? # # # # # # # # ################################ tomo-game/modules.ini000066400000000000000000000000001512235352400151040ustar00rootroot00000000000000tomo-game/player.tm000066400000000000000000000016151512235352400146060ustar00rootroot00000000000000# Defines a struct representing the player, which is controlled by WASD keys use ./world.tm use ./raylib.tm struct Player(pos,prev_pos:Vector2) WALK_SPEED := Num32(500.) ACCEL := Num32(0.3) FRICTION := Num32(0.99) SIZE := Vector2(30, 30) COLOR := Color(0x60, 0x60, 0xbF) func update(p:@Player) target_x := C_code:Num32` (Num32_t)((IsKeyDown(KEY_A) ? -1 : 0) + (IsKeyDown(KEY_D) ? 1 : 0)) ` target_y := C_code:Num32` (Num32_t)((IsKeyDown(KEY_W) ? -1 : 0) + (IsKeyDown(KEY_S) ? 1 : 0)) ` target_vel := Vector2(target_x, target_y).norm() * Player.WALK_SPEED vel := (p.pos - p.prev_pos)/World.DT vel *= Player.FRICTION vel = vel.mix(target_vel, Player.ACCEL) p.prev_pos, p.pos = p.pos, p.pos + World.DT*vel func draw(p:Player) DrawRectangleV(p.pos, Player.SIZE, Player.COLOR) tomo-game/raylib.tm000066400000000000000000000062071512235352400145760ustar00rootroot00000000000000# Raylib wrapper for some functions and structs use -lraylib use use struct Color(r,g,b:Byte,a=Byte(255)) struct Rectangle(x,y,width,height:Num32) func draw(r:Rectangle, color:Color) DrawRectangleRec(r, color) struct Vector2(x,y:Num32) ZERO := Vector2(0, 0) func plus(a,b:Vector2->Vector2; inline) return Vector2(a.x+b.x, a.y+b.y) func minus(a,b:Vector2->Vector2; inline) return Vector2(a.x-b.x, a.y-b.y) func times(a,b:Vector2->Vector2; inline) return Vector2(a.x*b.x, a.y*b.y) func negative(v:Vector2->Vector2; inline) return Vector2(-v.x, -v.y) func dot(a,b:Vector2->Num32; inline) return ((a.x-b.x)*(a.x-b.x) + (a.y-b.y)*(a.y-b.y)) func cross(a,b:Vector2->Num32; inline) return a.x*b.y - a.y*b.x func scaled_by(v:Vector2, k:Num32->Vector2; inline) return Vector2(v.x*k, v.y*k) func divided_by(v:Vector2, divisor:Num32->Vector2; inline) return Vector2(v.x/divisor, v.y/divisor) func length(v:Vector2->Num32; inline) return (v.x*v.x + v.y*v.y).sqrt() func dist(a,b:Vector2->Num32; inline) return a.minus(b).length() func angle(v:Vector2->Num32; inline) return Num32.atan2(v.y, v.x) func norm(v:Vector2->Vector2; inline) if v.x == 0 and v.y == 0 return v len := v.length() return Vector2(v.x/len, v.y/len) func rotated(v:Vector2, radians:Num32 -> Vector2) cos := radians.cos() or return v sin := radians.sin() or return v return Vector2(cos*v.x - sin*v.y, sin*v.x + cos*v.y) func mix(a,b:Vector2, amount:Num32 -> Vector2) return Vector2( amount.mix(a.x, b.x), amount.mix(a.y, b.y), ) func InitWindow(width:Int32, height:Int32, title:CString) C_code `InitWindow(@width, @height, @title);` func SetTargetFPS(fps:Int32) C_code `SetTargetFPS(@fps);` func WindowShouldClose(->Bool) return C_code:Bool`WindowShouldClose()` func GetFrameTime(->Num32) return C_code:Num32`GetFrameTime()` func BeginDrawing() C_code `BeginDrawing();` func EndDrawing() C_code `EndDrawing();` func CloseWindow() C_code `CloseWindow();` func ClearBackground(color:Color) C_code `ClearBackground((Color){@color.r, @color.g, @color.b, @color.a});` func DrawRectangle(x,y,width,height:Int32, color:Color) C_code `DrawRectangleRec((Rectangle){@x,@y,@width,@height},(Color){@color.r, @color.g, @color.b, @color.a});` func DrawRectangleRec(rec:Rectangle, color:Color) C_code `DrawRectangleRec((Rectangle){@rec.x,@rec.y,@rec.width,@rec.height},(Color){@color.r, @color.g, @color.b, @color.a});` func DrawRectangleV(pos:Vector2, size:Vector2, color:Color) C_code `DrawRectangleV((Vector2){@pos.x,@pos.y}, (Vector2){@size.x,@size.y},(Color){@color.r, @color.g, @color.b, @color.a});` func DrawText(text:CString, x,y:Int32, text_height:Int32, color:Color) C_code `DrawText(@text, @x, @y, @text_height, (Color){@color.r, @color.g, @color.b, @color.a});` func GetScreenWidth(->Int32) return C_code:Int32`GetScreenWidth()` func GetScreenHeight(->Int32) return C_code:Int32`GetScreenHeight()` tomo-game/world.tm000066400000000000000000000061151512235352400144410ustar00rootroot00000000000000# This file defines a World object for keeping track of everything, as well # as the collision logic. use ./player.tm use ./raylib.tm use ./box.tm # Return a displacement relative to `a` that will push it out of `b` func solve_overlap(a_pos:Vector2, a_size:Vector2, b_pos:Vector2, b_size:Vector2 -> Vector2) a_left := a_pos.x a_right := a_pos.x + a_size.x a_top := a_pos.y a_bottom := a_pos.y + a_size.y b_left := b_pos.x b_right := b_pos.x + b_size.x b_top := b_pos.y b_bottom := b_pos.y + b_size.y # Calculate the overlap in each dimension overlap_x := (a_right _min_ b_right) - (a_left _max_ b_left) overlap_y := (a_bottom _min_ b_bottom) - (a_top _max_ b_top) # If either axis is not overlapping, then there is no collision: if overlap_x <= 0 or overlap_y <= 0 return Vector2(0, 0) if overlap_x < overlap_y if a_right > b_left and a_right < b_right return Vector2(-(overlap_x), 0) else if a_left < b_right and a_left > b_left return Vector2(overlap_x, 0) else if a_top < b_bottom and a_top > b_top return Vector2(0, overlap_y) else if a_bottom > b_top and a_bottom < b_bottom return Vector2(0, -overlap_y) return Vector2(0, 0) struct World(player:@Player, goal:@Box, boxes:@[@Box], dt_accum=Num32(0.0), won=no) DT := (Num32(1.)/Num32(60.)) STIFFNESS := Num32(0.3) func update(w:@World, dt:Num32) w.dt_accum += dt while w.dt_accum > 0 w.update_once() w.dt_accum -= World.DT func update_once(w:@World) w.player.update() if solve_overlap(w.player.pos, Player.SIZE, w.goal.pos, w.goal.size) != Vector2(0,0) w.won = yes # Resolve player overlapping with any boxes: for i in 3 for b in w.boxes w.player.pos += World.STIFFNESS * solve_overlap(w.player.pos, Player.SIZE, b.pos, b.size) func draw(w:@World) for b in w.boxes b.draw() w.goal.draw() w.player.draw() if w.won DrawText(CString("WINNER"), GetScreenWidth()/Int32(2)-Int32(48*3), GetScreenHeight()/Int32(2)-Int32(24), 48, Color(0,0,0)) func load_map(w:@World, map:Text) if map.has("[]") map = map.translate({"[]":"#", "@ ":"@", " ":" "}) w.boxes = @[] box_size := Vector2(50., 50.) for y,line in map.lines() for x,cell in line.split() if cell == "#" pos := Vector2((Num32(x)-1) * box_size.x, (Num32(y)-1) * box_size.y) box := @Box(pos, size=box_size, color=Color(0x80,0x80,0x80)) w.boxes.insert(box) else if cell == "@" pos := Vector2((Num32(x)-1) * box_size.x, (Num32(y)-1) * box_size.y) pos += box_size/Num32(2) - Player.SIZE/Num32(2) w.player = @Player(pos,pos) else if cell == "?" pos := Vector2((Num32(x)-1) * box_size.x, (Num32(y)-1) * box_size.y) w.goal.pos = pos