const std = @import("std"); const Allocator = std.mem.Allocator; const assert = std.debug.assert; const Engine = @import("./engine/root.zig"); const imgui = Engine.imgui; const Gfx = Engine.Graphics; const Vec2 = Engine.Math.Vec2; const Vec4 = Engine.Math.Vec4; const rgb = Engine.Math.rgb; const rgb_hex = Engine.Math.rgb_hex; const Timer = @import("./timer.zig"); const Entity = @import("./entity.zig"); const Assets = @import("./assets.zig"); const tiled = @import("tiled"); const Game = @This(); const KeyState = Engine.Input.KeyState; pub const Input = struct { dt: f64, move_up: KeyState, move_down: KeyState, move_left: KeyState, move_right: KeyState, restart: bool }; pub const Level = struct { entities: Entity.List, timers: Timer.List, pub const empty = Level{ .entities = .empty, .timers = .empty }; pub fn clone(self: *Level, gpa: Allocator) !Level { var entities = try self.entities.clone(gpa); errdefer entities.deinit(gpa); var timers = try self.timers.clone(gpa); errdefer timers.deinit(gpa); return Level{ .entities = entities, .timers = timers }; } pub fn deinit(self: *Level, gpa: Allocator) void { self.entities.deinit(gpa); self.timers.deinit(gpa); } }; const key = "XXXXX-XXXXX-XXXXX"; gpa: Allocator, canvas_size: Vec2, level: Level, assets: *Assets, current_level: u32, levels: std.ArrayList(Level), last_up_repeat_at: ?f64 = null, last_down_repeat_at: ?f64 = null, last_left_repeat_at: ?f64 = null, last_right_repeat_at: ?f64 = null, show_grid: bool = false, timers: Timer.List = .empty, level_exit_transition: ?Timer.Id = null, level_enter_transition: ?Timer.Id = null, finale: bool = false, finale_timer: ?Timer.Id = null, finale_counter: u32 = 0, pub fn init(gpa: Allocator, assets: *Assets) !Game { var self = Game{ .gpa = gpa, .canvas_size = (Vec2.init(20, 15)), .level = .empty, .levels = .empty, .current_level = 0, .assets = assets }; errdefer self.deinit(); var scratch = std.heap.ArenaAllocator.init(gpa); defer scratch.deinit(); var xml_buffers = tiled.xml.Lexer.Buffers.init(gpa); defer xml_buffers.deinit(); var tilesets: tiled.Tileset.List = .empty; defer tilesets.deinit(gpa); try tilesets.add( gpa, "tileset.tsx", try tiled.Tileset.initFromBuffer( gpa, &scratch, &xml_buffers, @embedFile("assets/tiled/tileset.tsx") ) ); try self.levels.append(gpa, try loadLevelFromEmbedTiled(gpa, tilesets, "assets/tiled/first.tmx")); try self.levels.append(gpa, try loadLevelFromEmbedTiled(gpa, tilesets, "assets/tiled/second.tmx")); try self.levels.append(gpa, try loadLevelFromEmbedTiled(gpa, tilesets, "assets/tiled/third.tmx")); try self.levels.append(gpa, try loadLevelFromEmbedTiled(gpa, tilesets, "assets/tiled/fourth.tmx")); try self.restartLevel(); return self; } fn restartLevel(self: *Game) !void { const level_copy = try self.levels.items[self.current_level].clone(self.gpa); errdefer level_copy.deinit(self.gpa); self.level.deinit(self.gpa); self.level = level_copy; } fn nextLevel(self: *Game) !void { if (self.level_exit_transition != null) { return; } if (self.current_level < self.levels.items.len) { self.level_exit_transition = try self.timers.start(self.gpa, .{ .duration = 1 }); } } fn loadLevelFromEmbedTiled(gpa: Allocator, tilesets: tiled.Tileset.List, comptime path: []const u8) !Level { var scratch = std.heap.ArenaAllocator.init(gpa); defer scratch.deinit(); var xml_buffers = tiled.xml.Lexer.Buffers.init(gpa); defer xml_buffers.deinit(); const map = try tiled.Tilemap.initFromBuffer( gpa, &scratch, &xml_buffers, @embedFile(path) ); defer map.deinit(); var level: Level = .empty; errdefer level.deinit(gpa); for (map.layers) |*layer| { if (!layer.visible) { continue; } if (layer.variant != .tile) { continue; } for (0..map.height) |y| { for (0..map.width) |x| { const tile = map.getTile(layer, tilesets, x, y) orelse continue; const tile_props = tile.getProperties(); const tile_type = tile_props.getString("type") orelse ""; const tile_width: f32 = @floatFromInt(tile.tileset.tile_width); const tile_height: f32 = @floatFromInt(tile.tileset.tile_height); const tile_position_in_image = tile.tileset.getTilePositionInImage(tile.id).?; var entity: Entity = .{ .type = .nil, .position = Vec2.init(@floatFromInt(x), @floatFromInt(y)), .render_tile = .{ .position = .{ .x = tile_position_in_image.x / tile_width, .y = tile_position_in_image.y / tile_height, } }, }; if (std.mem.eql(u8, tile_type, "player")) { entity.type = .player; } else if (std.mem.eql(u8, tile_type, "key")) { entity.type = .key; } else if (std.mem.eql(u8, tile_type, "locked_door")) { entity.type = .door; entity.locked = true; } else if (std.mem.eql(u8, tile_type, "pot")) { entity.type = .pot; } else if (std.mem.eql(u8, tile_type, "staircase")) { entity.type = .staircase; } else if (std.mem.eql(u8, tile_type, "solid")) { entity.type = .solid; } _ = try level.entities.insert(gpa, entity); } } } return level; } pub fn deinit(self: *Game) void { self.timers.deinit(self.gpa); self.level.deinit(self.gpa); for (self.levels.items) |*level| { level.deinit(self.gpa); } self.levels.deinit(self.gpa); } fn drawGrid(self: *Game, size: Vec2, color: Vec4, line_width: f32) void { var x: f32 = 0; while (x < self.canvas_size.x) { x += size.x; Gfx.drawLine( .init(x, 0), .init(x, self.canvas_size.y), color, line_width ); } var y: f32 = 0; while (y < self.canvas_size.y) { y += size.y; Gfx.drawLine( .init(0, y), .init(self.canvas_size.x, y), color, line_width ); } } fn getEntityAt(self: *Game, pos: Vec2) ?Entity.Id { var iter = self.level.entities.iterator(); while (iter.next()) |tuple| { const entity = tuple.item; if (entity.type == .nil) { continue; } if (entity.position.eql(pos)) { return tuple.id; } } return null; } fn isSolidAt(self: *Game, pos: Vec2) bool { if (self.getEntityAt(pos)) |entity_id| { const entity = self.level.entities.getAssumeExists(entity_id); return entity.type == .solid; } return false; } fn canMove(self: *Game, entity: *Entity, dir: Vec2) bool { const next_pos = entity.position.add(dir); if (self.isSolidAt(next_pos)) { return false; } if (next_pos.x < 0 or next_pos.x >= self.canvas_size.x) { return false; } if (next_pos.y < 0 or next_pos.y >= self.canvas_size.y) { return false; } return true; } fn moveEntity(self: *Game, entity_id: Entity.Id, dir: Vec2) bool { const entity = self.level.entities.get(entity_id) orelse return true; if (entity.type == .solid) { return false; } if (entity.type == .nil) { return true; } if (dir.x == 0 and dir.y == 0) { return true; } const next_pos = entity.position.add(dir); if (self.getEntityAt(next_pos)) |next_entity_id| { const next_entity = self.level.entities.getAssumeExists(next_entity_id); if (next_entity.type == .door and next_entity.locked and entity.type == .key) { _ = self.level.entities.removeAssumeExists(entity_id); next_entity.locked = false; return true; } if (next_entity.type == .pot or next_entity.type == .key) { if (!self.moveEntity(next_entity_id, dir)) { return false; } } else if (next_entity.type == .door) { if (next_entity.locked) { return false; } } else if (next_entity.type == .solid) { return false; } } entity.position = next_pos; return true; } pub fn getInput(self: *Game, frame: Engine.Frame) Input { _ = self; // autofix const input = frame.input; return Input{ .dt = frame.dt, .move_up = input.getKeyState(frame, .W), .move_down = input.getKeyState(frame, .S), .move_left = input.getKeyState(frame, .A), .move_right = input.getKeyState(frame, .D), .restart = input.isKeyPressed(.R) }; } fn drawEntity(self: *Game, entity: *Entity) void { _ = self; // autofix if (entity.render_tile) |render_tile| { var tile_coord = switch (render_tile) { .id => |tile_id| Gfx.getTileCoords(tile_id), .position => |position| position }; if (entity.type == .door) { if (entity.locked) { tile_coord = Gfx.getTileCoords(.locked_door); } else { tile_coord = Gfx.getTileCoords(.open_door); } } Gfx.drawTile(tile_coord, entity.position, .init(1,1), rgb(255, 255, 255)); } } fn hasStaricaseAt(self: *Game, position: Vec2) bool { var iter = self.level.entities.iterator(); while (iter.nextItem()) |entity| { if (entity.type == .staircase and entity.position.eql(position)) { return true; } } return false; } pub fn tickLevel(self: *Game, input: Input) !void { const bg_color = rgb_hex("#222323").?; if (input.restart) { try self.restartLevel(); } self.level.timers.now += input.dt; var cover_opacity: f32 = 0; var can_move = true; if (self.level_exit_transition) |timer| { can_move = false; cover_opacity = self.timers.percent_passed(timer); if (self.timers.finished(timer)) { self.current_level += 1; if (self.current_level == self.levels.items.len) { self.finale = true; return; } else { try self.restartLevel(); self.level_exit_transition = null; self.level_enter_transition = try self.timers.start(self.gpa, .{ .duration = 1 }); } } } if (self.level_enter_transition) |timer| { cover_opacity = 1 - self.timers.percent_passed(timer); if (self.timers.finished(timer)) { self.level_enter_transition = null; } } var move: Vec2 = .init(0, 0); if (can_move) { const repeat_options = KeyState.RepeatOptions{ .first_at = 0.3, .period = 0.1 }; if (input.move_up.pressed or input.move_up.repeat(&self.last_up_repeat_at, repeat_options)) { move.y -= 1; } if (input.move_down.pressed or input.move_down.repeat(&self.last_down_repeat_at, repeat_options)) { move.y += 1; } if (input.move_left.pressed or input.move_left.repeat(&self.last_left_repeat_at, repeat_options)) { move.x -= 1; } if (input.move_right.pressed or input.move_right.repeat(&self.last_right_repeat_at, repeat_options)) { move.x += 1; } } var iter = self.level.entities.iterator(); while (iter.next()) |tuple| { const entity = tuple.item; const entity_id = tuple.id; if (entity.type == .player) { _ = self.moveEntity(entity_id, move); if (self.hasStaricaseAt(entity.position)) { try self.nextLevel(); } } } var top_layer: std.ArrayList(Entity.Id) = .empty; defer top_layer.deinit(self.gpa); var bottom_layer: std.ArrayList(Entity.Id) = .empty; defer bottom_layer.deinit(self.gpa); iter = self.level.entities.iterator(); while (iter.next()) |tuple| { const entity = tuple.item; const entity_id = tuple.id; if (entity.type == .player or entity.type == .key or entity.type == .pot) { try top_layer.append(self.gpa, entity_id); } else { try bottom_layer.append(self.gpa, entity_id); } } for (bottom_layer.items) |entity_id| { const entity = self.level.entities.getAssumeExists(entity_id); self.drawEntity(entity); } for (top_layer.items) |entity_id| { const entity = self.level.entities.getAssumeExists(entity_id); self.drawEntity(entity); } if (cover_opacity != 0) { Gfx.drawRectangle(.init(0, 0), .init(self.canvas_size.x, self.canvas_size.y), bg_color.multiply(Vec4.init(1, 1, 1, cover_opacity))); } } pub fn tickFinale(self: *Game) !void { const color = rgb(200, 200, 200); const Line = struct { pos: Vec2, font: Gfx.Font, text: []const u8, }; const regular_font = Gfx.Font{ .id = self.assets.font_map.get(.regular), .size = 64 }; const bold_font = Gfx.Font{ .id = self.assets.font_map.get(.bold), .size = 64 }; const lines = [5]Line{ .{ .pos = Vec2.init(1, 1), .font = regular_font, .text = "Congratulations scientist" }, .{ .pos = Vec2.init(1, 2), .font = regular_font, .text = "You have passed the entrance exam" }, .{ .pos = Vec2.init(1, 3), .font = regular_font, .text = "Here is your entry code" }, .{ .pos = Vec2.init(1, 5), .font = bold_font, .text = key }, .{ .pos = Vec2.init(1, 7), .font = regular_font, .text = "I'll meet you at the lab" } }; if (self.finale_timer == null) { if (self.finale_counter < lines.len) { self.finale_timer = try self.timers.start(self.gpa, .{ .duration = 2, }); } } if (self.finale_timer) |timer| { if (self.timers.finished(timer)) { self.finale_timer = null; self.finale_counter += 1; } } for (0..self.finale_counter) |i| { const line = lines[i]; try Gfx.drawText(self.gpa, line.pos, line.font, color, line.text); } if (self.finale_counter < lines.len) { var opacity: f32 = 0; if (self.finale_timer) |timer| { opacity = self.timers.percent_passed(timer); } const line = lines[self.finale_counter]; try Gfx.drawText(self.gpa, line.pos, line.font, color.multiply(Vec4.init(1, 1, 1, opacity)), line.text); } } pub fn tick(self: *Game, frame: Engine.Frame) !void { const input = self.getInput(frame); const bg_color = rgb_hex("#222323").?; Gfx.drawRectangle(.init(0, 0), .init(self.canvas_size.x, self.canvas_size.y), bg_color); if (self.show_grid) { self.drawGrid(.init(1, 1), rgb(20, 20, 20), 0.1); } self.timers.now += input.dt; if (self.finale) { try self.tickFinale(); } else { try self.tickLevel(input); } } pub fn debug(self: *Game) !void { if (!imgui.beginWindow(.{ .name = "Debug", .pos = Vec2.init(20, 20), .size = Vec2.init(400, 200), })) { return; } defer imgui.endWindow(); imgui.textFmt("Entities: {}", .{self.level.entities.len}); imgui.textFmt("Timers: {}", .{self.level.timers.array_list.len}); _ = imgui.checkbox("Show grid", &self.show_grid); if (imgui.button("Skip level")) { try self.nextLevel(); } if (imgui.button("Finale")) { self.finale = true; } }