583 lines
16 KiB
Zig
583 lines
16 KiB
Zig
const std = @import("std");
|
|
const Allocator = std.mem.Allocator;
|
|
const assert = std.debug.assert;
|
|
|
|
const Math = @import("./math.zig");
|
|
const Vec2 = Math.Vec2;
|
|
const Vec4 = Math.Vec4;
|
|
const rgb = Math.rgb;
|
|
const rgb_hex = Math.rgb_hex;
|
|
|
|
const Timer = @import("./timer.zig");
|
|
const Window = @import("./window.zig");
|
|
const imgui = @import("./imgui.zig");
|
|
const Gfx = @import("./graphics.zig");
|
|
const Entity = @import("./entity.zig");
|
|
|
|
const tiled = @import("tiled");
|
|
|
|
const Game = @This();
|
|
|
|
pub const Input = struct {
|
|
dt: f64,
|
|
move_up: Window.KeyState,
|
|
move_down: Window.KeyState,
|
|
move_left: Window.KeyState,
|
|
move_right: Window.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,
|
|
|
|
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) !Game {
|
|
var self = Game{
|
|
.gpa = gpa,
|
|
.canvas_size = (Vec2.init(20, 15)),
|
|
.level = .empty,
|
|
.levels = .empty,
|
|
.current_level = 0,
|
|
};
|
|
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, window: *Window) Input {
|
|
_ = self; // autofix
|
|
const dt = @as(f32, @floatFromInt(window.frame_dt_ns)) / std.time.ns_per_s;
|
|
|
|
return Input{
|
|
.dt = dt,
|
|
.move_up = window.getKeyState(.W),
|
|
.move_down = window.getKeyState(.S),
|
|
.move_left = window.getKeyState(.A),
|
|
.move_right = window.getKeyState(.D),
|
|
.restart = window.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 = Window.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 lines = [5]Line{
|
|
.{
|
|
.pos = Vec2.init(1, 1),
|
|
.font = Gfx.Font.default,
|
|
.text = "Congratulations scientist"
|
|
},
|
|
.{
|
|
.pos = Vec2.init(1, 2),
|
|
.font = Gfx.Font.default,
|
|
.text = "You have passed the entrance exam"
|
|
},
|
|
.{
|
|
.pos = Vec2.init(1, 3),
|
|
.font = Gfx.Font.default,
|
|
.text = "Here is your entry code"
|
|
},
|
|
.{
|
|
.pos = Vec2.init(1, 5),
|
|
.font = Gfx.Font.bold,
|
|
.text = key
|
|
},
|
|
.{
|
|
.pos = Vec2.init(1, 7),
|
|
.font = Gfx.Font.default,
|
|
.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, input: Input) !void {
|
|
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;
|
|
}
|
|
}
|