const std = @import("std"); pub const panic = std.debug.FullPanic(wasmPanic); const emscripten = std.os.emscripten; const builtin = @import("builtin"); fn wasmPanic(msg: []const u8, ret_addr: ?usize) noreturn { _ = msg; _ = ret_addr; @trap(); } const expect = std.testing.expect; //imports const rl = @import("raylib"); // pub const screen_width: f32 = 1200; pub const screen_height: f32 = 800; pub const player_width: f32 = 60; pub const player_height: f32 = 40; pub const player_start_y: f32 = screen_height - player_height - 10; pub const player_base_speed: f32 = 600.0; pub const bullet_width: f32 = 6; pub const bullet_height: f32 = 26; pub const bullet_speed: f32 = 1200; pub const bullet_interval: f32 = 0.25; pub const bullet_limit: f32 = 255; pub const shield_padding: f32 = @ceil((player_width + bullet_width) / 2); pub const shield_start_x: f32 = shield_padding; pub const shield_start_y: f32 = screen_height - 175; pub const num_shields: f32 = 7; pub const shield_cols: f32 = @trunc((screen_width - shield_padding * 2) / shield_spacing) - (num_shields - 1); pub const shield_rows: f32 = 4; pub const shields_per_section: f32 = @trunc(shield_cols / num_shields); pub const shield_count: f32 = shield_rows * shield_cols; pub const shield_spacing: f32 = bullet_width * 3; pub const invader_start_x: f32 = @trunc((screen_width - (invader_cols * (invader_width + invader_spacing))) / 2); pub const invader_start_y: f32 = invader_height; pub const invader_width: f32 = player_width; pub const invader_height: f32 = player_height; pub const invader_spacing: f32 = bullet_width * 3; pub const invader_rows: f32 = 5; pub const invader_cols: f32 = @trunc((screen_width / (invader_width + invader_spacing)) - 2); pub const invader_count: f32 = @trunc(invader_rows * invader_cols); pub const invader_start_speed: f32 = 50; pub const max_invader_speed: f32 = 500; pub const invader_acceleration: f32 = 1.04; pub const invader_interval: f32 = 0.5; pub const ent_count: f32 = 1 + invader_count + shield_count + bullet_limit; var invader_speed: f32 = invader_start_speed; // //breaks down the map by .{start_index, end_index} //note: for bullets and shields we'll use the number of rows and columns //plust these indices to iterate over them as a map even though they var //are technically just arrays in a struct pub const legend = .{ .player = .{ .start = 0, .end = 0 }, .shield = .{ .start = 1, .end = @as(usize, @intFromFloat(shield_count)), }, .invader = .{ .start = (1 + @as(usize, @intFromFloat(shield_count))), .end = (@as(usize, @intFromFloat(shield_count + invader_count))), }, .bullet = .{ .start = (1 + @as(usize, @intFromFloat(invader_count + shield_count))), .end = @as( usize, @intFromFloat(shield_count + invader_count + bullet_limit), ), }, .len = @as(usize, @intFromFloat(shield_count + invader_count + bullet_limit + 1)), }; test "legend" { try expect(legend.bullet.end == ent_count - 1); } pub const Map: type = struct { width: [ent_count]f32, height: [ent_count]f32, x: [ent_count]f32, y: [ent_count]f32, px: [ent_count]f32, py: [ent_count]f32, speed: [ent_count]f32, alive: [ent_count]bool, collision_type: [ent_count]CollisionType, color: [ent_count]u32, row: [ent_count]usize = [_]usize{0} ** ent_count, col: [ent_count]usize = [_]usize{0} ** ent_count, }; pub const color = .{ .player = @as(u32, 0x4d9be6ff), .shield = @as(u32, 0x6b3e75ff), .plr_bullet = @as(u32, 0xa884f3ff), .inv_bullet = 0xc7dcd0, .inv_1 = @as(u32, 0x92a924ff), .inv_2 = @as(u32, 0xb2ba90ff), .inv_3 = @as(u32, 0x0b6e65ff), .inv_4 = @as(u32, 0x0b8a8fff), .inv_5 = @as(u32, 0x0eaf9bff), .inv_6 = @as(u32, 0x1ebc73ff), .inv_7 = @as(u32, 0x91db79ff), .inv_8 = @as(u32, 0xea4f36ff), .inv_9 = @as(u32, 0xb33831ff), .inv_10 = @as(u32, 0xc7dcd0ff), }; pub const CollisionType = enum { Shield, Bullet, Invader, Player, None, }; //the entire game state basically, time notwithstanding var map: Map = buildMap(); fn buildMap() Map { var stoa: Map = undefined; //player stoa.width[0] = player_width; stoa.height[0] = player_height; stoa.x[0] = (screen_width / 2) - (player_width / 2); stoa.y[0] = player_start_y; stoa.px[0] = stoa.x[0]; stoa.py[0] = stoa.y[0]; stoa.speed[0] = player_base_speed; stoa.alive[0] = true; stoa.collision_type[0] = .Player; stoa.color[0] = color.player; //shields for ( legend.shield.start..legend.shield.end + 1, 0.., ) |shld, count| { const row = @trunc(@as(f32, @floatFromInt(count)) / shield_cols); const col = @as(f32, @floatFromInt(count % @as(usize, shield_cols))); stoa.width[shld] = shield_spacing; stoa.height[shld] = shield_spacing; stoa.x[shld] = shield_start_x + shield_spacing * col + @trunc(col / (shields_per_section + 1)) * shield_spacing; stoa.y[shld] = shield_start_y + shield_spacing * row; stoa.px[shld] = stoa.x[shld]; stoa.py[shld] = stoa.y[shld]; stoa.speed[shld] = 0; stoa.alive[shld] = true; stoa.collision_type[shld] = .Shield; stoa.color[shld] = color.shield; stoa.row[shld] = @as(usize, @intFromFloat(row)); stoa.col[shld] = @as(usize, @intFromFloat(col)); } //invaders for ( legend.invader.start..legend.invader.end + 1, 0.., ) |inv, count| { const row = @trunc(@as(f32, @floatFromInt(count)) / invader_cols); const col = @as(f32, @floatFromInt((count % @as(usize, invader_cols)))); stoa.width[inv] = invader_width; stoa.height[inv] = invader_height; stoa.x[inv] = invader_start_x + (invader_spacing + invader_width) * col; stoa.y[inv] = invader_start_y + (invader_spacing + invader_height) * row; stoa.px[inv] = stoa.x[inv]; stoa.py[inv] = stoa.y[inv]; stoa.speed[inv] = invader_start_speed; stoa.alive[inv] = true; stoa.collision_type[inv] = .Invader; stoa.color[inv] = switch (@as(usize, @intFromFloat(row))) { 0 => color.inv_1, 1 => color.inv_2, 2 => color.inv_3, 3 => color.inv_4, 4 => color.inv_5, 5 => color.inv_6, 6 => color.inv_7, 7 => color.inv_8, 9 => color.inv_9, else => color.inv_10, }; stoa.row[inv] = @as(usize, @intFromFloat(row)); stoa.col[inv] = @as(usize, @intFromFloat(col)); } //bullets for (legend.bullet.start..legend.bullet.end + 1) |blt| { stoa.width[blt] = bullet_width; stoa.height[blt] = bullet_height; stoa.x[blt] = 0; stoa.y[blt] = 0; stoa.px[blt] = 0; stoa.px[blt] = 0; stoa.speed[blt] = 0; stoa.alive[blt] = false; stoa.collision_type[blt] = .Bullet; stoa.color[blt] = 0; } return stoa; } //buildMap tests test "player assignment" { try expect(map.width[0] == player_width); } test "mapping" { //assuming invader_cols = 7 const lgs1 = 1; const lgs2 = 23; try expect((lgs1 - legend.invader.start) / 7 == 0); try expect((lgs1 - legend.invader.start) % 7 == 0); try expect((lgs2 - legend.invader.start) / 7 == 3); try expect((lgs2 - legend.invader.start) % 7 == 1); } test "map length" { try expect(legend.bullet.end + 1 == map.alive.len); try expect((legend.invader.end) + (legend.shield.end - legend.invader.end) + (legend.bullet.end - legend.shield.end) + 1 == map.alive.len); } //input const Input: type = struct { quit: bool = false, left: bool = false, right: bool = false, shoot: bool = false, pause: bool = false, }; pub fn getInput() Input { var npt: Input = .{}; if (rl.isKeyDown(rl.KeyboardKey.q)) npt.quit = true; if (rl.isKeyDown(rl.KeyboardKey.h)) npt.left = true; if (rl.isKeyDown(rl.KeyboardKey.l)) npt.right = true; if (rl.isKeyDown(rl.KeyboardKey.space)) npt.shoot = true; if (rl.isKeyDown(rl.KeyboardKey.p)) npt.pause = true; return npt; } //moiton and physics pub fn playerVelocity(dt: f32, input: Input) struct { f32, f32 } { var x: f32 = 0; const y: f32 = 0; if (input.left == true) x -= player_base_speed * dt; if (input.right == true) x += player_base_speed * dt; if (map.x[0] + map.width[0] + x >= screen_width) { x = (screen_width - player_width) - map.x[0]; } if (map.x[0] + x <= 0) x = 0 - map.x[0]; return .{ x, y }; } pub fn invaderVelocity(dt: f32, form: *Formation) struct { f32, f32 } { //calculated based on topmost, leftmost invader var x: f32 = 0; var y: f32 = 0; const form_width = @as(f32, @floatFromInt((form.*.high_col) - (form.*.low_col) + 1)) * (invader_width + invader_spacing); const true_spacing: f32 = invader_width + invader_spacing; const form_xl: f32 = map.x[legend.invader.start] + @as(f32, @floatFromInt(form.*.low_col)) * true_spacing; const form_xr: f32 = form_xl + form_width - invader_spacing; if (form_xr >= screen_width - 1 and invader_speed > 0) { x = screen_width - 1 - form_xr; y = @ceil(invader_height / 2); invader_speed *= -1; return .{ x, y }; } else if (form_xl <= 1 and invader_speed < 0) { x = 1 - form_xl; y = @ceil(invader_height / 2); invader_speed *= -1; return .{ x, y }; } else { x = invader_speed * dt; return .{ x, y }; } } //state and data flow pub const Formation: type = struct { high_col: usize, low_col: usize, high_row: usize, low_row: usize, }; pub fn buildFormation( shooters: *std.ArrayList(usize), live_invaders: *std.ArrayList(usize), ) Formation { for (legend.invader.start..legend.invader.end + 1) |inv| { live_invaders.appendAssumeCapacity(inv); if (map.row[inv] == invader_rows - 1) shooters.appendAssumeCapacity(inv); } return Formation{ .high_col = invader_cols - 1, .low_col = 0, .high_row = 0, .low_row = invader_rows - 1, }; } pub fn invaderFormation( shooters: *std.ArrayList(usize), live_invaders: *std.ArrayList(usize), main_menu: *bool, ) Formation { shooters.clearRetainingCapacity(); live_invaders.clearRetainingCapacity(); var high_col: usize = 0; var low_col: usize = @intFromFloat(invader_cols); var high_row: usize = @intFromFloat(invader_rows); var low_row: usize = 0; main_menu.* = true; for (legend.invader.start..legend.invader.end + 1) |inv| { if (map.alive[inv] == true) { high_col = @max(high_col, map.col[inv]); low_col = @min(low_col, map.col[inv]); high_row = @min(high_row, map.row[inv]); low_row = @max(low_row, map.row[inv]); live_invaders.appendAssumeCapacity(inv); main_menu.* = false; } } if (main_menu.* == true) return Formation{ .high_col = 0, .low_col = 0, .high_row = 0, .low_row = 0, }; for (low_col..high_col + 1) |col| { for (high_row..low_row + 1) |row| { const next_lowest: usize = legend.invader.start + col + (low_row - row) * @as(usize, @intFromFloat(invader_cols)); if (map.alive[next_lowest]) { shooters.appendAssumeCapacity(next_lowest); break; } } } return Formation{ .high_col = high_col, .low_col = low_col, .high_row = high_row, .low_row = low_row, }; } pub fn getLiveShields(live_shields: *std.ArrayList(usize)) void { live_shields.clearRetainingCapacity(); for (legend.shield.start..legend.shield.end + 1) |shld| { if (map.alive[shld]) live_shields.appendAssumeCapacity(shld); } } pub fn update( plr_v: struct { f32, f32 }, inv_v: struct { f32, f32 }, dt: f32, ) void { map.px[0] = map.x[0]; map.py[0] = map.y[0]; map.x[0] += plr_v.@"0"; map.y[0] += plr_v.@"1"; for (legend.invader.start..legend.invader.end + 1) |inv| { map.px[inv] = map.x[inv]; map.py[inv] = map.y[inv]; map.x[inv] += inv_v.@"0"; map.y[inv] += inv_v.@"1"; if (map.y[inv] + map.height[inv] >= map.y[0]) map.alive[0] = false; } for (legend.bullet.start..legend.bullet.end + 1) |blt| { map.py[blt] = map.y[blt]; map.y[blt] -= map.speed[blt] * dt; if (map.y[blt] - bullet_height <= 0 or map.y[blt] - bullet_height >= screen_height) map.alive[blt] = false; } } //spawn and despawn pub fn spawnBullets( blt_t: *f32, inv_t: *f32, input: Input, shooters: *std.ArrayList(usize), ) void { var plr_blt: bool = false; var inv_blt: bool = false; if ((input.shoot == true) and (blt_t.* >= bullet_interval)) { plr_blt = true; blt_t.* = 0; } if (inv_t.* >= invader_interval) { inv_blt = true; inv_t.* = 0; } for (legend.bullet.start..legend.bullet.end + 1) |blt_ndx| { if (map.alive[blt_ndx] == false and plr_blt == true) { map.x[blt_ndx] = map.x[0] + (player_width - bullet_width) / 2; map.y[blt_ndx] = map.y[0] - (bullet_height + 1); map.px[blt_ndx] = map.x[blt_ndx]; map.py[blt_ndx] = map.y[blt_ndx]; map.alive[blt_ndx] = true; map.speed[blt_ndx] = bullet_speed; map.color[blt_ndx] = color.plr_bullet; plr_blt = false; } if (map.alive[blt_ndx] == false and inv_blt == true) { const random = rl.getRandomValue( 0, @as(i32, @intCast(shooters.items.len)) - 1, ); map.x[blt_ndx] = map.x[shooters.items[@intCast(random)]] + (invader_width - bullet_width) / 2; map.y[blt_ndx] = map.y[shooters.items[@intCast(random)]] + invader_height + 1; map.px[blt_ndx] = map.x[blt_ndx]; map.py[blt_ndx] = map.y[blt_ndx]; map.alive[blt_ndx] = true; map.speed[blt_ndx] = -bullet_speed / 2; map.color[blt_ndx] = color.inv_bullet; inv_blt = false; } if (plr_blt == false and inv_blt == false) break; } } pub fn despawnEntity( ent: usize, formation: *Formation, shooters: *std.ArrayList(usize), live_invaders: *std.ArrayList(usize), live_shields: *std.ArrayList(usize), main_menu: *bool, ) void { if (map.collision_type[ent] == CollisionType.Player or map.collision_type[ent] == CollisionType.Bullet) { map.alive[ent] = false; return; } if (map.collision_type[ent] == CollisionType.Shield) { map.alive[ent] = false; getLiveShields(live_shields); return; } if (map.collision_type[ent] == CollisionType.Invader) { map.alive[ent] = false; formation.* = invaderFormation( shooters, live_invaders, main_menu, ); invader_speed *= invader_acceleration; } } //collisions //2 main edge cases: tunnelling and multiple collisions //so we're gonna do swept aabb to find any collisions in a given frame //then we are going to collect and compare for any objects that had multiple //collisions and ignore any but the first const Collision = struct { ent_a: usize, ent_b: usize, toi: f32, }; pub fn detectCollisions( formation: *Formation, shooters: *std.ArrayList(usize), live_invaders: *std.ArrayList(usize), live_shields: *std.ArrayList(usize), main_menu: *bool, ) void { var buf: [128]Collision = undefined; var collision_list: std.ArrayList(Collision) = .initBuffer(&buf); for (legend.bullet.start..legend.bullet.end + 1) |blt| { if (!map.alive[blt]) continue; { const clsn = checkCollision(blt, 0); if (clsn != null) collision_list.appendAssumeCapacity(clsn.?); } for (live_shields.items) |*shld| { const clsn = checkCollision(blt, shld.*); if (clsn != null) collision_list.appendAssumeCapacity(clsn.?); } for (live_invaders.items) |*inv| { const clsn = checkCollision(blt, inv.*); if (clsn != null) collision_list.appendAssumeCapacity(clsn.?); } } sortCollisions(collision_list.items[0..]); for (collision_list.items) |clsn| { if (map.alive[clsn.ent_a] == false or map.alive[clsn.ent_b] == false) { continue; } despawnEntity( clsn.ent_a, formation, shooters, live_invaders, live_shields, main_menu, ); despawnEntity( clsn.ent_b, formation, shooters, live_invaders, live_shields, main_menu, ); } } pub fn checkCollision(blt: usize, ent: usize) ?Collision { //swept aabb collision //get relative velocity (technically displacement) on each axis const rvx = (map.x[blt] - map.px[blt]) - (map.x[ent] - map.px[ent]); const rvy = (map.y[blt] - map.py[blt]) - (map.y[ent] - map.py[ent]); //I need the distance between closest (entry) and furthest (exit) sides var x_entry: f32 = undefined; var x_exit: f32 = undefined; var y_entry: f32 = undefined; var y_exit: f32 = undefined; if (rvx > 0) { x_entry = map.px[ent] - (map.px[blt] + map.width[blt]); x_exit = (map.px[ent] + map.width[ent]) - map.px[blt]; } else { x_entry = (map.px[ent] + map.width[ent]) - map.px[blt]; x_exit = map.px[ent] - (map.px[blt] + map.width[blt]); } if (rvy > 0) { y_entry = map.py[ent] - (map.py[blt] + map.height[blt]); y_exit = (map.py[ent] + map.height[ent]) - map.py[blt]; } else { y_entry = (map.py[ent] + map.height[ent]) - map.py[blt]; y_exit = map.py[ent] - (map.py[blt] + map.height[blt]); } //find time of impact var t_x_entry: f32 = undefined; var t_x_exit: f32 = undefined; var t_y_entry: f32 = undefined; var t_y_exit: f32 = undefined; if (rvx > 0) { t_x_entry = (map.px[ent] - (map.px[blt] + map.width[blt])) / rvx; t_x_exit = ((map.px[ent] + map.width[ent]) - map.px[blt]) / rvx; } else if (rvx < 0) { t_x_entry = ((map.px[ent] + map.width[ent]) - map.px[blt]) / rvx; t_x_exit = (map.px[ent] - (map.px[blt] + map.width[blt])) / rvx; } else { if (map.px[blt] + map.width[blt] <= map.px[ent] or map.px[ent] + map.width[ent] <= map.px[blt]) return null; t_x_entry = -std.math.inf(f32); t_x_exit = std.math.inf(f32); } if (rvy > 0) { t_y_entry = (map.py[ent] - (map.py[blt] + map.height[blt])) / rvy; t_y_exit = ((map.py[ent] + map.height[ent]) - map.py[blt]) / rvy; } else if (rvy < 0) { t_y_entry = ((map.py[ent] + map.height[ent]) - map.py[blt]) / rvy; t_y_exit = (map.py[ent] - (map.py[blt] + map.height[blt])) / rvy; } else { if (map.py[blt] + map.height[blt] <= map.py[ent] or map.py[ent] + map.height[ent] <= map.py[blt]) return null; t_y_entry = -std.math.inf(f32); t_y_exit = std.math.inf(f32); } //actual time of collision const t_entry = @max(t_x_entry, t_y_entry); const t_exit = @min(t_x_exit, t_y_exit); if (t_entry > t_exit or t_entry < 0 or t_entry > 1) { return null; } return Collision{ .ent_a = blt, .ent_b = ent, .toi = t_entry, }; } pub fn sortCollisions(cl: []Collision) void { if (cl.len <= 1) return; var i: usize = 0; const pvt = cl.len - 1; for (0..cl.len) |j| { if (cl[j].toi < cl[pvt].toi) { const swap = cl[j]; cl[j] = cl[i]; cl[i] = swap; i += 1; } } cl[cl.len - 1] = cl[i]; cl[i] = cl[pvt]; sortCollisions(cl[0..i]); sortCollisions(cl[i + 1 ..]); } //rendering pub fn drawEntities() void { for (0..map.alive.len) |ent| { if (map.alive[ent] == true) { rl.drawRectangle( @intFromFloat(map.x[ent]), @intFromFloat(map.y[ent]), @intFromFloat(map.width[ent]), @intFromFloat(map.height[ent]), rl.Color.fromInt(map.color[ent]), ); } } } fn executeFrame(state_pointers: *StatePtrs) void { const state = state_pointers.*; const delta_time = rl.getFrameTime(); state.bullet_timer.* += delta_time; state.invader_timer.* += delta_time; rl.beginDrawing(); defer rl.endDrawing(); rl.clearBackground(rl.Color.fromInt(0x323353)); if (state.main_menu.*) { rl.drawText( "ZigInvaders", 40, 40, 40, rl.Color.green, ); rl.drawText( "Start Game?", 60, 120, 30, rl.Color.green, ); if (builtin.os.tag == .emscripten) { rl.drawText( "(Y)es", 80, 160, 30, rl.Color.dark_green, ); } else { rl.drawText( "(Y)es\n(N)o", 80, 160, 30, rl.Color.dark_green, ); } if (rl.isKeyPressed(rl.KeyboardKey.y)) { try resetGame(state); state.main_menu.* = false; } if (rl.isKeyPressed(rl.KeyboardKey.n)) { if (builtin.os.tag == .emscripten) {} else std.process.exit(0); } return; } drawEntities(); rl.drawText( "(Y)es\n(N)o", 80, 160, 30, rl.Color.dark_green, ); rl.drawText( "H: Move Left. L: Move Right. Space: Shoot", 120, 10, 20, rl.Color.green, ); rl.drawFPS(10, 10); const input = getInput(); if (input.quit) { state.main_menu.* = true; return; } const plr_v = playerVelocity( delta_time, input, ); const inv_v = invaderVelocity( delta_time, state.formation, ); update(plr_v, inv_v, delta_time); detectCollisions( state.formation, state.shooters, state.live_invaders, state.live_shields, state.main_menu, ); if (map.alive[0] == false) state.main_menu.* = true; spawnBullets( state.bullet_timer, state.invader_timer, input, state.shooters, ); } const StatePtrs = struct { bullet_timer: *f32, invader_timer: *f32, formation: *Formation, shooters: *std.ArrayList(usize), live_invaders: *std.ArrayList(usize), live_shields: *std.ArrayList(usize), main_menu: *bool, }; export fn executeFrameWrapper(arg: ?*anyopaque) callconv(.c) void { const state_ptrs: *StatePtrs = @ptrCast(@alignCast(arg)); executeFrame(state_ptrs); } fn resetGame(state_ptrs: StatePtrs) !void { state_ptrs.shooters.clearRetainingCapacity(); state_ptrs.live_invaders.clearRetainingCapacity(); state_ptrs.live_shields.clearRetainingCapacity(); map = buildMap(); invader_speed = invader_start_speed; getLiveShields(state_ptrs.live_shields); state_ptrs.formation.* = buildFormation( state_ptrs.shooters, state_ptrs.live_invaders, ); state_ptrs.bullet_timer.* = bullet_interval; state_ptrs.invader_timer.* = 0; } //main pub fn main(init: std.process.Init) !void { std.log.info("{d} env vars", .{init.environ_map.count()}); var buf_shtr: [invader_cols]usize = undefined; var shooters: std.ArrayList(usize) = .initBuffer(&buf_shtr); var buf_lvinv: [invader_rows * invader_cols]usize = undefined; var live_invaders: std.ArrayList(usize) = .initBuffer(&buf_lvinv); var buf_lvshld: [shield_count]usize = undefined; var live_shields: std.ArrayList(usize) = .initBuffer(&buf_lvshld); var formation: Formation = undefined; var bullet_timer: f32 = bullet_interval; var invader_timer: f32 = 0; var main_menu = true; var state_ptrs = StatePtrs{ .bullet_timer = &bullet_timer, .invader_timer = &invader_timer, .formation = &formation, .shooters = &shooters, .live_invaders = &live_invaders, .live_shields = &live_shields, .main_menu = &main_menu, }; //open and defer closing the window rl.initWindow( screen_width, screen_height, "Zig Invaders", ); if (builtin.os.tag == .emscripten) { emscripten.emscripten_set_main_loop_arg( executeFrameWrapper, @ptrCast(&state_ptrs), 0, 1, ); } else { rl.setTargetFPS(60); while (!rl.windowShouldClose()) { executeFrame( &state_ptrs, ); } } }