Compare commits

...

10 Commits

Author SHA1 Message Date
a8604946f6 add more levels 2025-12-20 15:51:02 +02:00
310a4aff55 add finale section 2025-12-20 15:32:58 +02:00
1382b14313 add level transitions 2025-12-20 14:41:59 +02:00
df027f34cd add doors and keys 2025-12-15 00:15:45 +02:00
73fbbafbfc add moveable pots 2025-12-14 21:19:02 +02:00
e5e4e429b6 integrate tiled 2025-12-14 20:21:36 +02:00
61e5edb8cf add tilemap 2025-12-14 14:56:30 +02:00
5be299ad4c add key repeat 2025-12-14 13:47:50 +02:00
c2e784bfb2 add canvas scaling 2025-12-13 23:47:35 +02:00
0174a3f3f0 add imgui and tracy 2025-12-13 17:47:07 +02:00
27 changed files with 4770 additions and 72 deletions

107
build.zig
View File

@ -1,17 +1,122 @@
const std = @import("std"); const std = @import("std");
const sokol = @import("sokol");
pub fn build(b: *std.Build) void { pub fn build(b: *std.Build) !void {
const target = b.standardTargetOptions(.{}); const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{}); const optimize = b.standardOptimizeOption(.{});
const has_imgui = b.option(bool, "imgui", "ImGui integration") orelse (optimize == .Debug);
const has_tracy = b.option(bool, "tracy", "Tracy integration") orelse (optimize == .Debug);
const exe = b.addExecutable(.{ const exe = b.addExecutable(.{
.name = "game_2025_12_13", .name = "game_2025_12_13",
.root_module = b.createModule(.{ .root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"), .root_source_file = b.path("src/main.zig"),
.target = target, .target = target,
.optimize = optimize, .optimize = optimize,
.link_libc = true
}), }),
}); });
const exe_mod = exe.root_module;
const tracy_dependency = b.dependency("tracy", .{
.target = target,
.optimize = optimize,
.tracy_enable = has_tracy,
.tracy_only_localhost = true
});
exe_mod.linkLibrary(tracy_dependency.artifact("tracy"));
exe_mod.addImport("tracy", tracy_dependency.module("tracy"));
const stb_dependency = b.dependency("stb", .{});
exe_mod.addIncludePath(stb_dependency.path("."));
const sokol_c_dependency = b.dependency("sokol_c", .{});
exe_mod.addIncludePath(sokol_c_dependency.path("util"));
const fontstash_dependency = b.dependency("fontstash", .{});
exe_mod.addIncludePath(fontstash_dependency.path("src"));
const libxml2_dependency = b.dependency("libxml2", .{
.target = target,
.optimize = optimize,
.linkage = .static,
});
{
const libtmx_dependency = b.dependency("libtmx", .{});
const libtmx = b.addLibrary(.{
.name = "tmx",
.root_module = b.createModule(.{
.target = target,
.optimize = optimize,
.link_libc = true
}),
});
libtmx.installHeader(libtmx_dependency.path("src/tmx.h"), "tmx.h");
libtmx.root_module.addCSourceFiles(.{
.root = libtmx_dependency.path("src"),
.files = &.{
"tmx.c",
"tmx_utils.c",
"tmx_err.c",
"tmx_xml.c",
"tmx_mem.c",
"tmx_hash.c"
},
.flags = &.{
"-fno-delete-null-pointer-checks"
}
});
libtmx.linkLibrary(libxml2_dependency.artifact("xml"));
exe_mod.linkLibrary(libtmx);
}
const sokol_dependency = b.dependency("sokol", .{
.target = target,
.optimize = optimize,
.with_sokol_imgui = has_imgui
});
exe_mod.addImport("sokol", sokol_dependency.module("sokol"));
exe_mod.linkLibrary(sokol_dependency.artifact("sokol_clib"));
var cflags_buffer: [64][]const u8 = undefined;
var cflags = std.ArrayListUnmanaged([]const u8).initBuffer(&cflags_buffer);
switch (sokol.resolveSokolBackend(.auto, target.result)) {
.d3d11 => try cflags.appendBounded("-DSOKOL_D3D11"),
.metal => try cflags.appendBounded("-DSOKOL_METAL"),
.gl => try cflags.appendBounded("-DSOKOL_GLCORE"),
.gles3 => try cflags.appendBounded("-DSOKOL_GLES3"),
.wgpu => try cflags.appendBounded("-DSOKOL_WGPU"),
else => @panic("unknown sokol backend"),
}
exe_mod.addIncludePath(b.path("src/libs"));
exe_mod.addCSourceFile(.{
.file = b.path("src/libs/sokol_fontstash_impl.c"),
.flags = cflags.items
});
exe_mod.addCSourceFile(.{
.file = b.path("src/libs/stb_image.c"),
.flags = &.{}
});
if (has_imgui) {
if (b.lazyDependency("cimgui", .{
.target = target,
.optimize = optimize,
})) |cimgui_dependency| {
sokol_dependency.artifact("sokol_clib").addIncludePath(cimgui_dependency.path("src"));
exe_mod.addImport("cimgui", cimgui_dependency.module("cimgui"));
}
}
var options = b.addOptions();
options.addOption(bool, "has_imgui", has_imgui);
options.addOption(bool, "has_tracy", has_tracy);
exe_mod.addOptions("options", options);
b.installArtifact(exe); b.installArtifact(exe);

View File

@ -1,81 +1,46 @@
.{ .{
// This is the default name used by packages depending on this one. For
// example, when a user runs `zig fetch --save <url>`, this field is used
// as the key in the `dependencies` table. Although the user can choose a
// different name, most users will stick with this provided value.
//
// It is redundant to include "zig" in this name because it is already
// within the Zig package namespace.
.name = .game_2025_12_13, .name = .game_2025_12_13,
// This is a [Semantic Version](https://semver.org/).
// In a future version of Zig it will be used for package deduplication.
.version = "0.0.0", .version = "0.0.0",
// Together with name, this represents a globally unique package .fingerprint = 0x5704f7ae3ffdd7f8,
// identifier. This field is generated by the Zig toolchain when the
// package is first created, and then *never changes*. This allows
// unambiguous detection of one package being an updated version of
// another.
//
// When forking a Zig project, this id should be regenerated (delete the
// field and run `zig build`) if the upstream project is still maintained.
// Otherwise, the fork is *hostile*, attempting to take control over the
// original project's identity. Thus it is recommended to leave the comment
// on the following line intact, so that it shows up in code reviews that
// modify the field.
.fingerprint = 0x5704f7ae3ffdd7f8, // Changing this has security and trust implications.
// Tracks the earliest Zig version that the package considers to be a
// supported use case.
.minimum_zig_version = "0.15.2", .minimum_zig_version = "0.15.2",
// This field is optional.
// Each dependency must either provide a `url` and `hash`, or a `path`.
// `zig build --fetch` can be used to fetch all dependencies of a package, recursively.
// Once all dependencies are fetched, `zig build` no longer requires
// internet connectivity.
.dependencies = .{ .dependencies = .{
// See `zig fetch --save <url>` for a command-line interface for adding dependencies. .sokol = .{
//.example = .{ .url = "git+https://github.com/floooh/sokol-zig.git#1e233203b41893a8bf9c1c91933eba98204b6ed8",
// // When updating this field to a new URL, be sure to delete the corresponding .hash = "sokol-0.1.0-pb1HK42FNgDb5sqnsadiO2qabkfUX8jXP_DheOZGcD1W",
// // `hash`, otherwise you are communicating that you expect to find the old hash at },
// // the new URL. If the contents of a URL change this will result in a hash mismatch .cimgui = .{
// // which will prevent zig from using it. .url = "git+https://github.com/floooh/dcimgui.git#33c99ef426b68030412b5a4b11487a23da9d4f13",
// .url = "https://example.com/foo.tar.gz", .hash = "cimgui-0.1.0-44ClkQRJlABdFMKRqIG8KDD6jy1eQbgPO335NziPYjmL",
// .lazy = true,
// // This is computed from the file contents of the directory of files that is },
// // obtained after fetching `url` and applying the inclusion rules given by .tracy = .{
// // `paths`. .url = "git+https://github.com/sagehane/zig-tracy.git#80933723efe9bf840fe749b0bfc0d610f1db1669",
// // .hash = "zig_tracy-0.0.5-aOIqsX1tAACKaRRB-sraMLuNiMASXi_y-4FtRuw4cTpx",
// // This field is the source of truth; packages do not come from a `url`; they },
// // come from a `hash`. `url` is just one of many possible mirrors for how to .stb = .{
// // obtain a package matching this `hash`. .url = "git+https://github.com/nothings/stb.git#f1c79c02822848a9bed4315b12c8c8f3761e1296",
// // .hash = "N-V-__8AABQ7TgCnPlp8MP4YA8znrjd6E-ZjpF1rvrS8J_2I",
// // Uses the [multihash](https://multiformats.io/multihash/) format. },
// .hash = "...", .sokol_c = .{
// .url = "git+https://github.com/floooh/sokol.git#c66a1f04e6495d635c5e913335ab2308281e0492",
// // When this is provided, the package is found in a directory relative to the .hash = "N-V-__8AAC3eYABB1DVLb4dkcEzq_xVeEZZugVfQ6DoNQBDN",
// // build root. In this case the package's hash is irrelevant and therefore not },
// // computed. This field and `url` are mutually exclusive. .fontstash = .{
// .path = "foo", .url = "git+https://github.com/memononen/fontstash.git#b5ddc9741061343740d85d636d782ed3e07cf7be",
// .hash = "N-V-__8AAA9xHgAxdLYPmlNTy6qzv9IYqiIePEHQUOPWYQ_6",
// // When this is set to `true`, a package is declared to be lazily },
// // fetched. This makes the dependency only get fetched if it is .libtmx = .{
// // actually used. .url = "git+https://github.com/baylej/tmx.git#11ffdcdc9bd65669f1a8dbd3a0362a324dda2e0c",
// .lazy = false, .hash = "N-V-__8AAKQvBQCTT3Q6_we7vTVX-MkAWDZ91YkUev040IRo",
//}, },
.libxml2 = .{
.url = "git+https://github.com/allyourcodebase/libxml2.git?ref=2.14.3-4#86c4742a9becd6c86dc79180f806ed344fd2a727",
.hash = "libxml2-2.14.3-4-qHdjhn9FAACpyisv_5DDFVQlegox6QE3mTpdlr44RcbT",
},
}, },
// Specifies the set of files and directories that are included in this package.
// Only files and directories listed here are included in the `hash` that
// is computed for this package. Only files listed here will remain on disk
// when using the zig package manager. As a rule of thumb, one should list
// files required for compilation plus any license(s).
// Paths are relative to the build root. Use the empty string (`""`) to refer to
// the build root itself.
// A directory listed here means that all files within, recursively, are included.
.paths = .{ .paths = .{
"build.zig", "build.zig",
"build.zig.zon", "build.zig.zon",
"src", "src",
// For example...
//"LICENSE",
//"README.md",
}, },
} }

View File

@ -0,0 +1,23 @@
Micro Roguelike (1.4)
Created/distributed by Kenney (www.kenney.nl)
Creation date: 01-11-2021
------------------------------
License: (Creative Commons Zero, CC0)
http://creativecommons.org/publicdomain/zero/1.0/
This content is free to use in personal, educational and commercial projects.
Support us by crediting Kenney or www.kenney.nl (this is not mandatory)
------------------------------
Donate: http://support.kenney.nl
Patreon: http://patreon.com/kenney/
Follow on Twitter for updates:
http://twitter.com/KenneyNL

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -0,0 +1,93 @@
Copyright 2011 The Roboto Project Authors (https://github.com/googlefonts/roboto-classic)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://openfontlicense.org
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.10" tiledversion="1.11.2" orientation="orthogonal" renderorder="right-down" width="20" height="15" tilewidth="8" tileheight="8" infinite="0" nextlayerid="5" nextobjectid="1">
<tileset firstgid="1" source="tileset.tsx"/>
<layer id="1" name="Actors" width="20" height="15">
<data encoding="csv">
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,5,0,0,0,0,0,0,53,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
</data>
</layer>
<layer id="2" name="Walls" width="20" height="15">
<properties>
<property name="solid" type="bool" value="true"/>
</properties>
<data encoding="csv">
0,0,0,0,0,70,70,70,0,0,0,0,0,0,70,0,0,0,0,0,
70,70,0,0,0,0,0,0,0,70,70,0,0,0,0,0,0,0,0,0,
0,0,0,0,1,68,2,68,2,2,2,67,2,2,2,4,0,0,0,0,
0,0,0,0,17,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0,
0,0,0,0,17,0,0,0,0,0,0,0,0,0,0,20,0,70,0,0,
0,70,70,0,17,0,0,0,0,0,0,0,0,0,0,20,0,70,70,0,
0,0,70,0,17,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0,
0,0,0,0,17,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0,
0,0,0,0,17,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0,
0,0,0,0,17,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0,
0,0,0,0,83,2,2,68,2,67,2,2,2,67,2,36,0,0,0,0,
0,70,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,70,0,0,0,0,0,0,0,0,70,70,70,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,70,0,0,0,0,0,0,0,0,0,0,0,
0,0,70,70,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
</data>
</layer>
<layer id="4" name="Decorations" width="20" height="15">
<data encoding="csv">
0,85,85,0,0,0,0,85,85,85,85,85,85,0,0,0,0,85,85,85,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,85,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,85,
0,0,85,0,0,0,0,0,0,0,0,0,0,0,70,0,0,0,0,85,
85,0,0,0,0,0,69,0,0,70,0,0,0,70,0,0,0,0,0,85,
85,0,0,0,0,0,0,0,0,0,0,0,69,0,0,0,0,0,0,85,
85,0,0,0,0,0,0,0,0,69,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,85,0,0,
0,85,0,0,0,0,69,0,0,0,70,0,69,0,0,0,0,85,0,0,
0,85,0,0,0,70,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
85,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,85,
0,0,0,0,85,0,0,0,0,0,0,0,0,0,85,0,85,0,0,85,
85,0,85,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,85,
0,0,0,0,0,0,85,85,85,85,85,85,0,0,0,0,0,0,0,85
</data>
</layer>
</map>

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.10" tiledversion="1.11.2" orientation="orthogonal" renderorder="right-down" width="20" height="15" tilewidth="8" tileheight="8" infinite="0" nextlayerid="2" nextobjectid="1">
<tileset firstgid="1" source="tileset.tsx"/>
<layer id="1" name="Tiles" width="20" height="15">
<data encoding="csv">
0,0,0,0,0,70,70,70,0,0,0,0,0,0,0,0,85,85,0,0,
0,85,85,85,0,0,0,70,0,0,1,2,2,2,4,0,0,85,70,85,
0,0,0,0,85,85,70,70,0,0,17,0,0,0,20,0,85,85,70,85,
0,0,0,85,85,0,0,0,0,0,17,0,53,0,20,0,0,0,70,0,
0,0,0,0,70,0,0,0,0,0,17,0,0,0,20,0,0,0,0,0,
0,85,0,70,70,0,1,2,2,2,2,2,40,2,2,66,0,0,0,0,
0,85,0,0,0,0,17,69,0,5,0,41,91,0,69,20,0,85,0,0,
0,85,85,0,0,0,17,69,1,2,2,4,0,0,69,20,0,85,85,0,
0,0,0,85,0,0,17,69,17,70,70,49,2,36,0,20,0,0,0,70,
0,0,0,85,0,0,17,0,17,0,0,0,0,0,0,20,0,0,70,70,
0,0,0,0,0,0,17,0,17,0,33,2,2,2,2,66,0,0,0,0,
0,0,85,0,0,0,17,0,17,0,0,0,0,0,0,20,0,0,0,0,
85,0,0,0,0,0,17,0,33,2,2,2,2,36,69,20,0,0,85,85,
85,0,0,0,0,0,17,70,70,0,0,0,0,69,69,20,0,70,85,0,
0,0,85,85,0,0,33,2,2,2,2,2,2,2,2,2,0,70,85,0
</data>
</layer>
</map>

View File

@ -0,0 +1,14 @@
{
"automappingRulesFile": "",
"commands": [
],
"compatibilityVersion": 1100,
"extensionsPath": "extensions",
"folders": [
"."
],
"properties": [
],
"propertyTypes": [
]
}

View File

@ -0,0 +1,61 @@
{
"Map/SizeTest": {
"height": 4300,
"width": 2
},
"activeFile": "",
"expandedProjectPaths": [
],
"fileStates": {
"first.tmx": {
"scale": 4,
"selectedLayer": 1,
"viewCenter": {
"x": 79.5,
"y": 71.5
}
},
"fourth.tmx": {
"scale": 5.24875,
"selectedLayer": 0,
"viewCenter": {
"x": 82.30531078828292,
"y": 78.49487973326983
}
},
"second.tmx": {
"scale": 4,
"selectedLayer": 2,
"viewCenter": {
"x": 67.25,
"y": 71.5
}
},
"third.tmx": {
"scale": 4,
"selectedLayer": 0,
"viewCenter": {
"x": 65.75,
"y": 49.5
}
}
},
"last.imagePath": "/home/rokas/code/games/game-2025-12-13/src/assets/kenney-micro-roguelike",
"map.height": 15,
"map.lastUsedFormat": "tmx",
"map.tileHeight": 8,
"map.tileWidth": 8,
"map.width": 20,
"openFiles": [
],
"project": "main.tiled-project",
"recentFiles": [
"first.tmx"
],
"textEdit.monospace": true,
"tileset.lastUsedFormat": "tsx",
"tileset.tileSize": {
"height": 8,
"width": 8
}
}

View File

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.10" tiledversion="1.11.2" orientation="orthogonal" renderorder="right-down" width="20" height="15" tilewidth="8" tileheight="8" infinite="0" nextlayerid="4" nextobjectid="1">
<tileset firstgid="1" source="tileset.tsx"/>
<layer id="3" name="Deco" width="20" height="15">
<data encoding="csv">
0,0,0,85,0,0,0,85,85,85,85,0,0,0,0,0,0,0,85,0,
0,0,85,85,0,0,0,0,0,0,0,0,0,0,0,0,0,0,85,85,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,85,0,0,0,0,0,0,0,0,0,0,0,85,0,0,
85,0,0,0,85,85,0,0,0,0,0,0,0,0,0,0,0,85,85,0,
85,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,85,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,85,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,85,0,0,85,
0,0,0,70,69,0,0,0,0,0,0,70,70,0,0,0,0,0,0,0,
85,0,0,0,0,0,0,0,69,0,69,0,70,0,0,0,0,0,85,0,
85,0,0,70,70,0,0,69,0,0,0,0,69,0,0,0,0,0,85,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,85,85,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,85,85,0,0,0,0,85,85,85,85,85,0,0,0,0,0,85
</data>
</layer>
<layer id="1" name="Actors" width="20" height="15">
<data encoding="csv">
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,53,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,40,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,91,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
</data>
</layer>
<layer id="2" name="Walls" width="20" height="15">
<data encoding="csv">
0,0,0,0,0,0,0,0,0,0,0,0,0,0,70,70,0,0,0,0,
0,0,0,0,70,70,0,0,0,1,2,2,2,4,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,17,0,0,0,20,0,0,0,0,70,0,
0,0,70,0,0,0,0,0,0,17,0,0,0,20,0,0,0,0,0,0,
0,0,70,0,0,0,0,0,0,17,0,0,0,20,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,65,2,0,2,66,0,0,0,0,0,0,
0,70,0,0,70,0,0,0,0,17,0,69,0,20,0,0,0,70,0,0,
0,70,0,0,0,0,0,0,0,17,0,69,0,20,0,0,0,70,0,0,
0,0,1,2,2,2,2,2,2,50,0,0,0,20,0,0,0,0,0,0,
0,0,17,0,0,0,0,0,0,0,0,0,0,20,0,70,0,0,0,0,
0,0,17,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0,0,0,
0,0,17,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0,0,0,
0,0,33,2,2,2,2,2,2,2,2,2,2,36,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,70,0,0,0,0,0,0,70,70,0,0,0,
0,0,0,0,0,0,70,70,0,0,0,0,0,0,0,0,0,0,0,0
</data>
</layer>
</map>

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.10" tiledversion="1.11.2" orientation="orthogonal" renderorder="right-down" width="20" height="15" tilewidth="8" tileheight="8" infinite="0" nextlayerid="2" nextobjectid="1">
<tileset firstgid="1" source="tileset.tsx"/>
<layer id="1" name="Tiles" width="20" height="15">
<data encoding="csv">
0,0,0,0,85,85,0,0,0,0,0,0,0,0,0,0,0,0,69,69,
0,85,85,85,85,85,0,70,70,0,85,85,85,85,85,0,0,0,0,69,
0,85,0,0,0,85,0,0,0,0,0,0,0,0,0,70,70,85,0,0,
0,0,0,0,0,70,70,0,0,0,0,0,0,0,0,70,70,85,85,0,
0,0,85,0,0,70,70,1,2,2,2,2,4,0,0,0,0,0,0,0,
0,0,85,85,0,0,0,17,0,69,69,0,20,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,17,0,0,0,0,49,2,2,4,0,0,85,85,
70,70,70,0,0,0,0,17,0,5,0,41,0,0,53,20,0,69,69,85,
0,70,70,0,0,0,0,17,69,0,70,0,1,2,2,36,0,0,69,0,
0,0,85,85,0,0,0,17,0,0,0,0,17,0,0,0,0,0,0,0,
0,0,85,85,0,0,0,33,2,2,2,2,50,0,0,0,0,85,85,0,
0,0,85,85,85,0,0,0,0,0,0,0,0,0,0,0,0,0,85,0,
0,69,69,0,0,0,85,85,70,70,0,0,0,0,0,0,70,0,0,0,
0,0,69,0,0,0,0,0,0,70,70,85,85,85,0,70,70,70,0,0,
0,85,0,85,85,85,85,0,0,0,0,85,0,0,0,70,69,69,69,0
</data>
</layer>
</map>

View File

@ -0,0 +1,139 @@
<?xml version="1.0" encoding="UTF-8"?>
<tileset version="1.10" tiledversion="1.11.2" name="tileset" tilewidth="8" tileheight="8" tilecount="160" columns="16">
<image source="../kenney-micro-roguelike/colored_tilemap_packed.png" width="128" height="80"/>
<tile id="0">
<properties>
<property name="type" value="solid"/>
</properties>
</tile>
<tile id="1">
<properties>
<property name="type" value="solid"/>
</properties>
</tile>
<tile id="2">
<properties>
<property name="type" value="solid"/>
</properties>
</tile>
<tile id="3">
<properties>
<property name="type" value="solid"/>
</properties>
</tile>
<tile id="4">
<properties>
<property name="type" value="player"/>
</properties>
</tile>
<tile id="16">
<properties>
<property name="type" value="solid"/>
</properties>
</tile>
<tile id="19">
<properties>
<property name="type" value="solid"/>
</properties>
</tile>
<tile id="32">
<properties>
<property name="type" value="solid"/>
</properties>
</tile>
<tile id="33">
<properties>
<property name="type" value="solid"/>
</properties>
</tile>
<tile id="34">
<properties>
<property name="type" value="solid"/>
</properties>
</tile>
<tile id="35">
<properties>
<property name="type" value="solid"/>
</properties>
</tile>
<tile id="39">
<properties>
<property name="type" value="locked_door"/>
</properties>
</tile>
<tile id="40">
<properties>
<property name="type" value="pot"/>
</properties>
</tile>
<tile id="48">
<properties>
<property name="type" value="solid"/>
</properties>
</tile>
<tile id="49">
<properties>
<property name="type" value="solid"/>
</properties>
</tile>
<tile id="50">
<properties>
<property name="type" value="solid"/>
</properties>
</tile>
<tile id="51">
<properties>
<property name="type" value="solid"/>
</properties>
</tile>
<tile id="52">
<properties>
<property name="type" value="staircase"/>
</properties>
</tile>
<tile id="64">
<properties>
<property name="type" value="solid"/>
</properties>
</tile>
<tile id="65">
<properties>
<property name="type" value="solid"/>
</properties>
</tile>
<tile id="66">
<properties>
<property name="type" value="solid"/>
</properties>
</tile>
<tile id="67">
<properties>
<property name="type" value="solid"/>
</properties>
</tile>
<tile id="80">
<properties>
<property name="type" value="solid"/>
</properties>
</tile>
<tile id="81">
<properties>
<property name="type" value="solid"/>
</properties>
</tile>
<tile id="82">
<properties>
<property name="type" value="solid"/>
</properties>
</tile>
<tile id="83">
<properties>
<property name="type" value="solid"/>
</properties>
</tile>
<tile id="90">
<properties>
<property name="type" value="key"/>
</properties>
</tile>
</tileset>

31
src/entity.zig Normal file
View File

@ -0,0 +1,31 @@
const GenerationalArrayList = @import("./generational_array_list.zig").GenerationalArrayList;
const Gfx = @import("./graphics.zig");
const Math = @import("./math.zig");
const Vec2 = Math.Vec2;
const Entity = @This();
pub const List = GenerationalArrayList(Entity);
pub const Id = List.Id;
pub const Type = enum {
nil,
player,
solid,
pot,
staircase,
door,
key,
};
type: Type,
position: Vec2,
locked: bool = false,
render_tile: ?union(enum) {
position: Vec2,
id: Gfx.TileId
} = null

556
src/game.zig Normal file
View File

@ -0,0 +1,556 @@
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.zig");
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();
const manager = try tiled.ResourceManager.init();
defer manager.deinit();
try manager.loadTilesetFromBuffer(@embedFile("assets/tiled/tileset.tsx"), "tileset.tsx");
try self.levels.append(gpa, try loadLevelFromEmbedFile(gpa, manager, "assets/tiled/first.tmx"));
try self.levels.append(gpa, try loadLevelFromEmbedFile(gpa, manager, "assets/tiled/second.tmx"));
try self.levels.append(gpa, try loadLevelFromEmbedFile(gpa, manager, "assets/tiled/third.tmx"));
try self.levels.append(gpa, try loadLevelFromEmbedFile(gpa, manager, "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 loadLevelFromEmbedFile(gpa: Allocator, manager: tiled.ResourceManager, comptime path: []const u8) !Level {
const map = try manager.loadMapFromBuffer(@embedFile(path));
defer map.deinit();
return try loadLevelFromTiled(gpa, map);
}
fn loadLevelFromTiled(gpa: Allocator, map: tiled.Map) !Level {
var level: Level = .empty;
errdefer level.deinit(gpa);
var layer_iter = map.iterLayers();
while (layer_iter.next()) |layer| {
if (layer.layer.visible == 0) {
continue;
}
if (layer.layer.type != @intFromEnum(tiled.Layer.Type.layer)) {
continue;
}
const map_width = map.map.width;
for (0..map.map.height) |y| {
for (0..map_width) |x| {
const tile = map.getTile(layer, x, y) orelse continue;
const tile_props = tiled.Properties{ .inner = tile.tile.properties };
const tile_type: []const u8 = std.mem.span(tile_props.getPropertyString("type") orelse "");
const tile_size = Vec2.init(8, 8);
var entity: Entity = .{
.type = .nil,
.position = Vec2.init(@floatFromInt(x), @floatFromInt(y)),
.render_tile = .{ .position = tile.getUpperLeft().divide(tile_size) },
};
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;
}
}

View File

@ -0,0 +1,480 @@
const std = @import("std");
const tracy = @import("tracy");
const builtin = @import("builtin");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const Index = u24;
const Generation = u8;
pub fn GenerationalArrayList(Item: type) type {
assert(@bitSizeOf(Generation) % 8 == 0);
assert(@bitSizeOf(Index) % 8 == 0);
return struct {
const Self = @This();
items: [*]Item,
generations: [*]Generation,
unused: [*]u8,
len: u32,
capacity: u32,
count: u32,
pub const empty = Self{
.items = &[_]Item{},
.generations = &[_]Generation{},
.unused = &[_]u8{},
.capacity = 0,
.len = 0,
.count = 0
};
pub const Id = packed struct {
generation: Generation,
index: Index,
// TODO: Maybe `Id.Optional` type should be created to ensure .wrap() and .toOptional()
pub const none = Id{
.generation = std.math.maxInt(Generation),
.index = std.math.maxInt(Index),
};
pub fn format(self: Id, writer: *std.Io.Writer) std.Io.Writer.Error!void {
if (self == Id.none) {
try writer.print("Id({s}){{ .none }}", .{ @typeName(Item) });
} else {
try writer.print("Id({s}){{ {}, {} }}", .{ @typeName(Item), self.index, self.generation });
}
}
pub fn asInt(self: Id) u32 {
return @bitCast(self);
}
};
pub const ItemWithId = struct {
id: Id,
item: *Item,
};
pub const Iterator = struct {
array_list: *Self,
index: Index,
pub fn nextId(self: *Iterator) ?Id {
while (self.index < self.array_list.len) {
const index = self.index;
self.index += 1;
// TODO: Inline the `byte_index` calculate for better speed.
// Probably not needed. Idk
if (self.array_list.isUnused(index)) {
continue;
}
return Id{
.index = @intCast(index),
.generation = self.array_list.generations[index]
};
}
return null;
}
pub fn nextItem(self: *Iterator) ?*Item {
if (self.nextId()) |id| {
return &self.array_list.items[id.index];
}
return null;
}
pub fn next(self: *Iterator) ?ItemWithId {
if (self.nextId()) |id| {
return ItemWithId{
.id = id,
.item = &self.array_list.items[id.index]
};
}
return null;
}
};
pub const Metadata = extern struct {
len: u32,
count: u32
};
fn divCeilGeneration(num: u32) u32 {
return std.math.divCeil(u32, num, @bitSizeOf(Generation)) catch unreachable;
}
fn divFloorGeneration(num: u32) u32 {
return @divFloor(num, @bitSizeOf(Generation));
}
pub fn ensureTotalCapacityPrecise(self: *Self, allocator: Allocator, new_capacity: u32) !void {
if (new_capacity > std.math.maxInt(Index)) {
return error.OutOfIndexSpace;
}
// TODO: Shrinking is not supported
assert(new_capacity >= self.capacity);
const unused_bit_array_len = divCeilGeneration(self.capacity);
const new_unused_bit_array_len = divCeilGeneration(new_capacity);
// TODO: Handle allocation failure case
const new_unused = try allocator.realloc(self.unused[0..unused_bit_array_len], new_unused_bit_array_len);
const new_items = try allocator.realloc(self.items[0..self.capacity], new_capacity);
const new_generations = try allocator.realloc(self.generations[0..self.capacity], new_capacity);
self.unused = new_unused.ptr;
self.items = new_items.ptr;
self.generations = new_generations.ptr;
self.capacity = new_capacity;
}
fn growCapacity(current: u32, minimum: u32) u32 {
const init_capacity = @as(comptime_int, @max(1, std.atomic.cache_line / @sizeOf(Item)));
var new = current;
while (true) {
new +|= new / 2 + init_capacity;
if (new >= minimum) {
return new;
}
}
}
pub fn ensureTotalCapacity(self: *Self, allocator: Allocator, new_capacity: u32) !void {
if (self.capacity >= new_capacity) return;
const better_capacity = Self.growCapacity(self.capacity, new_capacity);
try self.ensureTotalCapacityPrecise(allocator, better_capacity);
}
pub fn clearRetainingCapacity(self: *Self) void {
self.count = 0;
self.len = 0;
}
pub fn ensureUnusedCapacity(self: *Self, allocator: Allocator, unused_capacity: u32) !void {
try self.ensureTotalCapacity(allocator, self.len + unused_capacity);
}
fn findFirstUnused(self: *Self) ?Index {
for (0..divCeilGeneration(self.len)) |byte_index| {
if (self.unused[byte_index] != 0) {
const found = @ctz(self.unused[byte_index]) + byte_index * @bitSizeOf(Generation);
if (found < self.len) {
return @intCast(found);
} else {
return null;
}
}
}
return null;
}
fn markUnused(self: *Self, index: Index, unused: bool) void {
assert(index < self.len);
const byte_index = divFloorGeneration(index);
const bit_index = @mod(index, @bitSizeOf(Generation));
const bit_flag = @as(u8, 1) << @intCast(bit_index);
if (unused) {
self.unused[byte_index] |= bit_flag;
} else {
self.unused[byte_index] &= ~bit_flag;
}
}
fn isUnused(self: *Self, index: Index) bool {
assert(index < self.len);
const byte_index = divFloorGeneration(index);
const bit_index = @mod(index, @bitSizeOf(Generation));
const bit_flag = @as(u8, 1) << @intCast(bit_index);
return (self.unused[byte_index] & bit_flag) != 0;
}
pub fn insertUndefined(self: *Self, allocator: Allocator) !Id {
var unused_index: Index = undefined;
if (self.findFirstUnused()) |index| {
unused_index = index;
} else {
try self.ensureUnusedCapacity(allocator, 1);
unused_index = @intCast(self.len);
self.len += 1;
self.generations[unused_index] = 0;
}
self.markUnused(unused_index, false);
self.count += 1;
const id = Id{
.index = @intCast(unused_index),
.generation = self.generations[unused_index]
};
assert(id != Id.none);
return id;
}
pub fn insert(self: *Self, allocator: Allocator, item: Item) !Id {
const id = try self.insertUndefined(allocator);
const new_item_ptr = self.getAssumeExists(id);
new_item_ptr.* = item;
return id;
}
pub fn exists(self: *Self, id: Id) bool {
if (id.index >= self.len) {
return false;
}
if (self.isUnused(id.index)) {
return false;
}
if (self.generations[id.index] != id.generation) {
return false;
}
return true;
}
pub fn removeAssumeExists(self: *Self, id: Id) void {
assert(self.exists(id));
self.markUnused(id.index, true);
// TODO: Maybe a log should be shown when a wrap-around occurs?
self.generations[id.index] +%= 1;
self.count -= 1;
}
pub fn remove(self: *Self, id: Id) bool {
if (!self.exists(id)) {
return false;
}
self.removeAssumeExists(id);
return true;
}
pub fn getAssumeExists(self: *Self, id: Id) *Item {
assert(self.exists(id));
return &self.items[id.index];
}
pub fn get(self: *Self, id: Id) ?*Item {
if (self.exists(id)) {
return self.getAssumeExists(id);
} else {
return null;
}
}
pub fn iterator(self: *Self) Iterator {
return Iterator{
.array_list = self,
.index = 0
};
}
pub fn deinit(self: *Self, allocator: Allocator) void {
allocator.free(self.unused[0..divCeilGeneration(self.capacity)]);
allocator.free(self.generations[0..self.capacity]);
allocator.free(self.items[0..self.capacity]);
}
pub fn clone(self: *Self, allocator: Allocator) !Self {
const items = try allocator.dupe(Item, self.items[0..self.capacity]);
errdefer allocator.free(items);
const generations = try allocator.dupe(Generation, self.generations[0..self.capacity]);
errdefer allocator.free(generations);
const unused = try allocator.dupe(u8, self.unused[0..divCeilGeneration(self.capacity)]);
errdefer allocator.free(unused);
return Self{
.items = items.ptr,
.generations = generations.ptr,
.unused = unused.ptr,
.len = self.len,
.count = self.count,
.capacity = self.capacity
};
}
pub fn getMetadata(self: *Self) Metadata {
return Metadata{
.len = self.len,
.count = self.count
};
}
pub fn write(self: *Self, writer: *std.Io.Writer, endian: std.builtin.Endian) !void {
const zone = tracy.beginZone(@src(), .{ .name = "gen array list write" });
defer zone.end();
try writer.writeSliceEndian(Item, self.items[0..self.len], endian);
try writer.writeSliceEndian(Generation, self.generations[0..self.len], endian);
try writer.writeAll(self.unused[0..divCeilGeneration(self.len)]);
}
pub fn read(
self: *Self,
allocator: Allocator,
reader: *std.Io.Reader,
endian: std.builtin.Endian,
metadata: Metadata
) !void {
const zone = tracy.beginZone(@src(), .{ .name = "gen array list read" });
defer zone.end();
try self.ensureTotalCapacity(allocator, metadata.len);
try reader.readSliceEndian(Item, self.items[0..metadata.len], endian);
try reader.readSliceEndian(Generation, self.generations[0..metadata.len], endian);
try reader.readSliceAll(self.unused[0..divCeilGeneration(metadata.len)]);
self.len = metadata.len;
self.count = metadata.count;
}
};
}
const TestArray = GenerationalArrayList(u32);
test "insert & remove" {
const expect = std.testing.expect;
const gpa = std.testing.allocator;
var array_list: TestArray = .empty;
defer array_list.deinit(gpa);
const id1 = try array_list.insert(gpa, 10);
try expect(array_list.exists(id1));
try expect(array_list.remove(id1));
try expect(!array_list.exists(id1));
try expect(!array_list.remove(id1));
const id2 = try array_list.insert(gpa, 10);
try expect(array_list.exists(id2));
try expect(!array_list.exists(id1));
try expect(id1.index == id2.index);
}
test "generation wrap around" {
const expectEqual = std.testing.expectEqual;
const gpa = std.testing.allocator;
var array_list: TestArray = .empty;
defer array_list.deinit(gpa);
// Grow array list so that at least 1 slot exists
const id1 = try array_list.insert(gpa, 10);
array_list.removeAssumeExists(id1);
// Artificially increase generation count
array_list.generations[id1.index] = std.math.maxInt(Generation);
// Check if generation wraps around
const id2 = try array_list.insert(gpa, 10);
array_list.removeAssumeExists(id2);
try expectEqual(id1.index, id2.index);
try expectEqual(0, array_list.generations[id1.index]);
}
test "iterator" {
const expectEqual = std.testing.expectEqual;
const gpa = std.testing.allocator;
var array_list: TestArray = .empty;
defer array_list.deinit(gpa);
// Create array which has a hole
const id1 = try array_list.insert(gpa, 1);
const id2 = try array_list.insert(gpa, 2);
const id3 = try array_list.insert(gpa, 3);
array_list.removeAssumeExists(id2);
var iter = array_list.iterator();
try expectEqual(
TestArray.ItemWithId{
.id = id1,
.item = array_list.getAssumeExists(id1)
},
iter.next().?
);
try expectEqual(
TestArray.ItemWithId{
.id = id3,
.item = array_list.getAssumeExists(id3)
},
iter.next().?
);
try expectEqual(null, iter.next());
}
test "read & write" {
const expectEqual = std.testing.expectEqual;
const gpa = std.testing.allocator;
var array_list1: TestArray = .empty;
defer array_list1.deinit(gpa);
var array_list2: TestArray = .empty;
defer array_list2.deinit(gpa);
const id1 = try array_list1.insert(gpa, 1);
const id2 = try array_list1.insert(gpa, 2);
const id3 = try array_list1.insert(gpa, 3);
var buffer: [1024]u8 = undefined;
var writer = std.Io.Writer.fixed(&buffer);
const native_endian = builtin.cpu.arch.endian();
try array_list1.write(&writer, native_endian);
var reader = std.Io.Reader.fixed(writer.buffered());
try array_list2.read(gpa, &reader, native_endian, array_list1.getMetadata());
try expectEqual(array_list1.getAssumeExists(id1).*, array_list2.getAssumeExists(id1).*);
try expectEqual(array_list1.getAssumeExists(id2).*, array_list2.getAssumeExists(id2).*);
try expectEqual(array_list1.getAssumeExists(id3).*, array_list2.getAssumeExists(id3).*);
try expectEqual(array_list1.count, array_list2.count);
}
test "clear retaining capacity" {
const expect = std.testing.expect;
const expectEqual = std.testing.expectEqual;
const gpa = std.testing.allocator;
var array_list: TestArray = .empty;
defer array_list.deinit(gpa);
const id1 = try array_list.insert(gpa, 10);
try expect(array_list.exists(id1));
array_list.clearRetainingCapacity();
const id2 = try array_list.insert(gpa, 10);
try expect(array_list.exists(id2));
try expectEqual(id1, id2);
}

1135
src/graphics.zig Normal file

File diff suppressed because it is too large Load Diff

504
src/imgui.zig Normal file
View File

@ -0,0 +1,504 @@
const std = @import("std");
const Math = @import("./math.zig");
const options = @import("options");
pub const ig = @import("cimgui");
const Vec2 = Math.Vec2;
const Vec3 = Math.Vec3;
const Vec4 = Math.Vec4;
const sokol = @import("sokol");
const sapp = sokol.app;
const simgui = sokol.imgui;
const enabled = options.has_imgui;
var global_allocator: ?std.mem.Allocator = null;
pub const WindowOptions = struct {
name: [*c]const u8,
pos: ?Vec2 = null,
size: ?Vec2 = null,
collapsed: ?bool = null,
open: ?*bool = null
};
pub const SliderOptions = struct {
label: [*c]const u8,
value: *f32,
min: f32,
max: f32,
};
fn toImVec2(vec2: Vec2) ig.ImVec2 {
return ig.ImVec2{
.x = vec2.x,
.y = vec2.y,
};
}
inline fn structCast(T: type, value: anytype) T {
return @as(*T, @ptrFromInt(@intFromPtr(&value))).*;
}
pub fn setup(gpa: std.mem.Allocator, desc: simgui.Desc) void {
if (!enabled) {
return;
}
global_allocator = gpa;
simgui.setup(desc);
}
pub fn addFont(ttf_data: []const u8, font_size: f32) void {
if (!enabled) {
return;
}
var font_config: ig.ImFontConfig = .{};
font_config.FontDataOwnedByAtlas = false;
font_config.OversampleH = 2;
font_config.OversampleV = 2;
font_config.GlyphMaxAdvanceX = std.math.floatMax(f32);
font_config.RasterizerMultiply = 1.0;
font_config.RasterizerDensity = 1.0;
font_config.EllipsisChar = 0;
const io = ig.igGetIO();
_ = ig.ImFontAtlas_AddFontFromMemoryTTF(
io.*.Fonts,
@constCast(@ptrCast(ttf_data.ptr)),
@intCast(ttf_data.len),
font_size,
&font_config,
null
);
}
pub fn shutdown() void {
if (!enabled) {
return;
}
simgui.shutdown();
}
pub fn handleEvent(ev: sapp.Event) bool {
if (enabled) {
return simgui.handleEvent(ev);
} else {
return false;
}
}
pub fn newFrame(desc: simgui.FrameDesc) void {
if (!enabled) {
return;
}
simgui.newFrame(desc);
}
pub fn render() void {
if (!enabled) {
return;
}
simgui.render();
}
pub fn beginWindow(opts: WindowOptions) bool {
if (!enabled) {
return false;
}
if (opts.pos) |pos| {
ig.igSetNextWindowPos(toImVec2(pos), ig.ImGuiCond_Once);
}
if (opts.size) |size| {
ig.igSetNextWindowSize(toImVec2(size), ig.ImGuiCond_Once);
}
if (opts.collapsed) |collapsed| {
ig.igSetNextWindowCollapsed(collapsed, ig.ImGuiCond_Once);
}
ig.igSetNextWindowBgAlpha(1);
var open = ig.igBegin(opts.name, opts.open, ig.ImGuiWindowFlags_None);
if (opts.open) |opts_open| {
if (opts_open.* == false) {
open = false;
}
}
if (!open) {
endWindow();
}
return open;
}
pub fn endWindow() void {
if (!enabled) {
return;
}
ig.igEnd();
}
pub fn textFmt(comptime fmt: []const u8, args: anytype) void {
if (!enabled) {
return;
}
const gpa = global_allocator orelse return;
const formatted = std.fmt.allocPrintSentinel(gpa, fmt, args, 0) catch return;
defer gpa.free(formatted);
text(formatted);
}
pub fn text(text_z: [*c]const u8) void {
if (!enabled) {
return;
}
ig.igText("%s", text_z);
}
pub fn beginDisabled(disabled: bool) void {
if (!enabled) {
return;
}
ig.igBeginDisabled(disabled);
}
pub fn endDisabled() void {
if (!enabled) {
return;
}
ig.igEndDisabled();
}
pub fn button(label: [*c]const u8) bool {
if (!enabled) {
return false;
}
return ig.igButton(label);
}
pub fn slider(opts: SliderOptions) bool {
if (!enabled) {
return false;
}
return ig.igSliderFloat(opts.label, opts.value, opts.min, opts.max);
}
pub fn checkbox(label: [*c]const u8, value: *bool) bool {
if (!enabled) {
return false;
}
return ig.igCheckbox(label, value);
}
pub fn beginTabBar(id: [*c]const u8) bool {
if (!enabled) {
return false;
}
return ig.igBeginTabBar(id, ig.ImGuiTabBarFlags_None);
}
pub fn endTabBar() void {
if (!enabled) {
return;
}
ig.igEndTabBar();
}
pub fn beginTabItem(label: [*c]const u8) bool {
if (!enabled) {
return false;
}
return ig.igBeginTabItem(label, null, ig.ImGuiTabItemFlags_None);
}
pub fn endTabItem() void {
if (!enabled) {
return;
}
return ig.igEndTabItem();
}
pub fn beginGroup() void {
if (!enabled) {
return;
}
ig.igBeginGroup();
}
pub fn endGroup() void {
if (!enabled) {
return;
}
ig.igEndGroup();
}
pub fn sameLine() void {
if (!enabled) {
return;
}
ig.igSameLine();
}
pub fn beginTable(id: [*c]const u8, columns: u32, flags: ig.ImGuiTableFlags) bool {
if (!enabled) {
return false;
}
return ig.igBeginTable(id, @intCast(columns), flags);
}
pub fn endTable() void {
if (!enabled) {
return;
}
ig.igEndTable();
}
pub fn tableNextColumn() void {
if (!enabled) {
return;
}
_ = ig.igTableNextColumn();
}
pub fn tableNextRow() void {
if (!enabled) {
return;
}
_ = ig.igTableNextRow();
}
pub fn tableSetColumnIndex(index: usize) void {
if (!enabled) {
return;
}
_ = ig.igTableSetColumnIndex(@intCast(index));
}
pub fn tableSetupColumn(label: [*c]const u8, flags: ig.ImGuiTableColumnFlags) void {
if (!enabled) {
return;
}
ig.igTableSetupColumn(label, flags);
}
pub fn tableHeadersRow() void {
if (!enabled) {
return;
}
ig.igTableHeadersRow();
}
pub const ID = union(enum) {
string: []const u8,
int: i32
};
pub fn pushID(id: ID) void {
if (!enabled) {
return;
}
switch (id) {
.string => |str| ig.igPushIDStr(str.ptr, str.ptr + str.len),
.int => |int| ig.igPushIDInt(int)
}
}
pub fn popID() void {
if (!enabled) {
return;
}
ig.igPopID();
}
pub const TreeNodeFlags = packed struct {
selected: bool = false,
framed: bool = false,
allow_overlap: bool = false,
no_tree_pushOnOpen: bool = false,
no_auto_open_on_log: bool = false,
default_open: bool = false,
open_on_double_click: bool = false,
open_on_arrow: bool = false,
leaf: bool = false,
bullet: bool = false,
frame_padding: bool = false,
span_avail_width: bool = false,
span_full_width: bool = false,
span_label_width: bool = false,
span_all_columns: bool = false,
label_span_all_columns: bool = false,
nav_left_jumps_back_here: bool = false,
collapsing_header: bool = false,
fn toInt(self: TreeNodeFlags) u32 {
// TODO: Try using comptime to reduce this duplication.
// Would be great if `toInt()` could be replaced with just a @bitCast
//
// If the underlying C enum is exhaustive, maybe a bitcast could be performed?
// If the order of enums is correct
const flags = .{
.{ self.selected, ig.ImGuiTreeNodeFlags_Selected },
.{ self.framed, ig.ImGuiTreeNodeFlags_Framed },
.{ self.allow_overlap, ig.ImGuiTreeNodeFlags_AllowOverlap },
.{ self.no_tree_pushOnOpen, ig.ImGuiTreeNodeFlags_NoTreePushOnOpen },
.{ self.no_auto_open_on_log, ig.ImGuiTreeNodeFlags_NoAutoOpenOnLog },
.{ self.default_open, ig.ImGuiTreeNodeFlags_DefaultOpen },
.{ self.open_on_double_click, ig.ImGuiTreeNodeFlags_OpenOnDoubleClick },
.{ self.open_on_arrow, ig.ImGuiTreeNodeFlags_OpenOnArrow },
.{ self.leaf, ig.ImGuiTreeNodeFlags_Leaf },
.{ self.bullet, ig.ImGuiTreeNodeFlags_Bullet },
.{ self.frame_padding, ig.ImGuiTreeNodeFlags_FramePadding },
.{ self.span_avail_width, ig.ImGuiTreeNodeFlags_SpanAvailWidth },
.{ self.span_full_width, ig.ImGuiTreeNodeFlags_SpanFullWidth },
.{ self.span_label_width, ig.ImGuiTreeNodeFlags_SpanLabelWidth },
.{ self.span_all_columns, ig.ImGuiTreeNodeFlags_SpanAllColumns },
.{ self.label_span_all_columns, ig.ImGuiTreeNodeFlags_LabelSpanAllColumns },
.{ self.nav_left_jumps_back_here, ig.ImGuiTreeNodeFlags_NavLeftJumpsBackHere },
.{ self.collapsing_header, ig.ImGuiTreeNodeFlags_CollapsingHeader },
};
var sum: u32 = 0;
inline for (flags) |flag_pair| {
if (flag_pair[0]) {
sum += flag_pair[1];
}
}
return sum;
}
};
pub fn treeNode(label: [*c]const u8, flags: TreeNodeFlags) bool {
if (!enabled) {
return false;
}
return ig.igTreeNodeEx(label, @intCast(flags.toInt()));
}
pub fn treePop() void {
if (!enabled) {
return;
}
ig.igTreePop();
}
pub fn isItemClicked() bool {
if (!enabled) {
return false;
}
return ig.igIsItemClicked();
}
pub fn isItemToggledOpen() bool {
if (!enabled) {
return false;
}
return ig.igIsItemToggledOpen();
}
pub fn colorPicker4(label: [*c]const u8, color: *Vec4) bool {
if (!enabled) {
return false;
}
return ig.igColorPicker4(label, color.asArray().ptr, 0, null);
}
pub fn colorEdit4(label: [*c]const u8, color: *Vec4) bool {
if (!enabled) {
return false;
}
return ig.igColorEdit4(label, color.asArray().ptr, 0);
}
pub fn beginCombo(label: [*c]const u8, preview_value: [*c]const u8) bool {
if (!enabled) {
return false;
}
return ig.igBeginCombo(label, preview_value, 0);
}
pub fn endCombo() void {
if (!enabled) {
return;
}
ig.igEndCombo();
}
pub fn selectable(label: [*c]const u8, selected: bool) bool {
if (!enabled) {
return false;
}
return ig.igSelectableEx(label, selected, 0, .{ });
}
pub fn setItemDefaultFocus() void {
if (!enabled) {
return;
}
ig.igSetItemDefaultFocus();
}
pub fn combo(label: [*c]const u8, items: []const [*c]const u8, selected: *usize) void {
if (beginCombo(label, items[selected.*])) {
defer endCombo();
for (0.., items) |i, item| {
const is_selected = selected.* == i;
if (selectable(item, is_selected)) {
selected.* = i;
}
if (is_selected) {
setItemDefaultFocus();
}
}
}
}
pub fn separator() void {
if (!enabled) {
return;
}
ig.igSeparator();
}

View File

@ -0,0 +1,85 @@
#include "stdlib.h"
#include "stdio.h"
#include "stdbool.h"
#include <stdint.h>
#define FONTSTASH_IMPLEMENTATION
#include "fontstash.h"
#include "sokol/sokol_gfx.h"
#include "sokol/sokol_gl.h"
#define SOKOL_FONTSTASH_IMPL
#include "sokol_fontstash.h"
typedef struct FONSstate FONSstate;
// Expose private functions so that `getTextBoundsUtf8` could be implemented in zig
FONSstate* zig_fons__getState(FONScontext* stash)
{
return fons__getState(stash);
}
FONSfont* zig_getFont(FONScontext* stash, int index)
{
if (index < 0 || index >= stash->nfonts) {
return NULL;
}
FONSfont *font = stash->fonts[index];
if (font->data == NULL) {
return NULL;
}
return font;
}
bool zig_isTopLeft(FONScontext* stash)
{
return stash->params.flags & FONS_ZERO_TOPLEFT;
}
int zig_getGlyphIndex(FONSglyph* glyph)
{
return glyph->index;
}
float zig_fons__tt_getPixelHeightScale(FONSfont *font, float size)
{
return fons__tt_getPixelHeightScale(&font->font, size);
}
float zig_fons__getVertAlign(FONScontext* stash, FONSfont* font, int align, short isize)
{
return fons__getVertAlign(stash, font, align, isize);
}
FONSglyph* zig_fons__getGlyph(
FONScontext* stash,
FONSfont* font,
unsigned int codepoint,
short isize,
short iblur
)
{
return fons__getGlyph(stash, font, codepoint, isize, iblur);
}
void zig_fons__getQuad(
FONScontext* stash,
FONSfont* font,
int prevGlyphIndex, FONSglyph* glyph,
float scale,
float spacing,
float* x, float* y,
FONSquad* q
)
{
fons__getQuad(
stash,
font,
prevGlyphIndex,
glyph,
scale,
spacing,
x, y,
q
);
}

2
src/libs/stb_image.c Normal file
View File

@ -0,0 +1,2 @@
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"

View File

@ -1,6 +1,116 @@
const std = @import("std"); const std = @import("std");
const tracy = @import("tracy");
const Gfx = @import("./graphics.zig");
const builtin = @import("builtin");
const Window = @import("./window.zig");
const Event = Window.Event;
const MouseButton = Window.MouseButton;
const Math = @import("./math.zig");
const Vec2 = Math.Vec2;
const sokol = @import("sokol");
const slog = sokol.log;
const sg = sokol.gfx;
const sapp = sokol.app;
const sglue = sokol.glue;
const log = std.log.scoped(.main);
var gpa: std.mem.Allocator = undefined;
var window: Window = undefined;
var event_queue_full_shown = false;
fn signalHandler(sig: i32) callconv(.c) void {
_ = sig;
sapp.requestQuit();
}
export fn init() void {
var zone = tracy.initZone(@src(), .{ });
defer zone.deinit();
Window.init(&window, gpa) catch |e| {
log.err("init() failed: {}", .{e});
sapp.requestQuit();
};
}
export fn frame() void {
tracy.frameMark();
const zone = tracy.initZone(@src(), .{ });
defer zone.deinit();
window.frame() catch |e| {
log.err("frame() failed: {}", .{e});
if (@errorReturnTrace()) |trace| {
std.debug.dumpStackTrace(trace.*);
}
sapp.requestQuit();
};
}
export fn cleanup() void {
const zone = tracy.initZone(@src(), .{ });
defer zone.deinit();
window.deinit();
}
export fn event(e_ptr: [*c]const sapp.Event) void {
const zone = tracy.initZone(@src(), .{ });
defer zone.deinit();
const consumed_event = window.event(e_ptr) catch |e| switch (e) {
error.EventQueueFull => blk: {
if (!event_queue_full_shown) {
log.warn("Event queue is full! Frame is taking too long to process", .{});
event_queue_full_shown = true;
}
break :blk false;
},
};
if (consumed_event) {
event_queue_full_shown = false;
sapp.consumeEvent();
}
}
pub fn main() !void { pub fn main() !void {
// Prints to stderr, ignoring potential errors. var debug_allocator: std.heap.DebugAllocator(.{}) = .init;
std.debug.print("All your {s} are belong to us.\n", .{"codebase"}); defer _ = debug_allocator.deinit();
if (builtin.mode == .ReleaseFast) {
gpa = std.heap.smp_allocator;
} else {
gpa = debug_allocator.allocator();
} }
// TODO: Use tracy TracingAllocator
tracy.setThreadName("Main");
var sa: std.posix.Sigaction = .{
.handler = .{ .handler = signalHandler },
.mask = std.posix.sigemptyset(),
.flags = std.posix.SA.RESTART,
};
std.posix.sigaction(std.posix.SIG.INT, &sa, null);
sapp.run(.{
.init_cb = init,
.frame_cb = frame,
.cleanup_cb = cleanup,
.event_cb = event,
.width = 640,
.height = 480,
.icon = .{ .sokol_default = true },
.window_title = "Game",
.logger = .{ .func = Window.sokolLogCallback },
});
}

413
src/math.zig Normal file
View File

@ -0,0 +1,413 @@
const std = @import("std");
const assert = std.debug.assert;
pub const bytes_per_kib = 1024;
pub const bytes_per_mib = bytes_per_kib * 1024;
pub const bytes_per_gib = bytes_per_mib * 1024;
pub const bytes_per_kb = 1000;
pub const bytes_per_mb = bytes_per_kb * 1000;
pub const bytes_per_gb = bytes_per_mb * 1000;
pub const Vec2 = extern struct {
x: f32,
y: f32,
pub const zero = init(0, 0);
pub fn init(x: f32, y: f32) Vec2 {
return Vec2{
.x = x,
.y = y,
};
}
pub fn initAngle(angle: f32) Vec2 {
return Vec2{
.x = @cos(angle),
.y = @sin(angle),
};
}
pub fn rotateLeft90(self: Vec2) Vec2 {
return Vec2.init(self.y, -self.x);
}
pub fn rotateRight90(self: Vec2) Vec2 {
return Vec2.init(-self.y, self.x);
}
pub fn flip(self: Vec2) Vec2 {
return Vec2.init(-self.x, -self.y);
}
pub fn add(self: Vec2, other: Vec2) Vec2 {
return Vec2.init(
self.x + other.x,
self.y + other.y,
);
}
pub fn sub(self: Vec2, other: Vec2) Vec2 {
return Vec2.init(
self.x - other.x,
self.y - other.y,
);
}
pub fn multiplyScalar(self: Vec2, value: f32) Vec2 {
return Vec2.init(
self.x * value,
self.y * value,
);
}
pub fn multiply(self: Vec2, other: Vec2) Vec2 {
return Vec2.init(
self.x * other.x,
self.y * other.y,
);
}
pub fn divide(self: Vec2, other: Vec2) Vec2 {
return Vec2.init(
self.x / other.x,
self.y / other.y,
);
}
pub fn divideScalar(self: Vec2, value: f32) Vec2 {
return Vec2.init(
self.x / value,
self.y / value,
);
}
pub fn length(self: Vec2) f32 {
return @sqrt(self.x*self.x + self.y*self.y);
}
pub fn distance(self: Vec2, other: Vec2) f32 {
return self.sub(other).length();
}
pub fn limitLength(self: Vec2, max_length: f32) Vec2 {
const self_length = self.length();
if (self_length > max_length) {
return Vec2.init(self.x / self_length * max_length, self.y / self_length * max_length);
} else {
return self;
}
}
pub fn normalized(self: Vec2) Vec2 {
const self_length = self.length();
if (self_length == 0) {
return Vec2.init(0, 0);
}
return Vec2.init(self.x / self_length, self.y / self_length);
}
pub fn initScalar(value: f32) Vec2 {
return Vec2.init(value, value);
}
pub fn eql(self: Vec2, other: Vec2) bool {
return self.x == other.x and self.y == other.y;
}
pub fn format(self: Vec2, writer: *std.io.Writer) std.io.Writer.Error!void {
try writer.print("Vec2{{ {d}, {d} }}", .{ self.x, self.y });
}
};
pub const Vec3 = extern struct {
x: f32, y: f32, z: f32,
pub const zero = init(0, 0, 0);
pub fn init(x: f32, y: f32, z: f32) Vec3 {
return Vec3{
.x = x,
.y = y,
.z = z,
};
}
pub fn initScalar(value: f32) Vec3 {
return Vec3.init(value, value, value);
}
pub fn asArray(self: *Vec3) []f32 {
const ptr: [*]f32 = @alignCast(@ptrCast(@as(*anyopaque, @ptrCast(self))));
return ptr[0..3];
}
pub fn lerp(a: Vec3, b: Vec3, t: f32) Vec3 {
return Vec3.init(
std.math.lerp(a.x, b.x, t),
std.math.lerp(a.y, b.y, t),
std.math.lerp(a.z, b.z, t),
);
}
pub fn clamp(self: Vec3, min_value: f32, max_value: f32) Vec3 {
return Vec3.init(
std.math.clamp(self.x, min_value, max_value),
std.math.clamp(self.y, min_value, max_value),
std.math.clamp(self.z, min_value, max_value),
);
}
};
pub const Vec4 = extern struct {
x: f32, y: f32, z: f32, w: f32,
pub const zero = init(0, 0, 0, 0);
pub fn init(x: f32, y: f32, z: f32, w: f32) Vec4 {
return Vec4{
.x = x,
.y = y,
.z = z,
.w = w
};
}
pub fn initVec3XYZ(vec3: Vec3, w: f32) Vec4 {
return init(vec3.x, vec3.y, vec3.z, w);
}
pub fn initScalar(value: f32) Vec4 {
return Vec4.init(value, value, value, value);
}
pub fn multiplyMat4(left: Vec4, right: Mat4) Vec4 {
var result: Vec4 = undefined;
// TODO: SIMD
result.x = left.x * right.columns[0][0];
result.y = left.x * right.columns[0][1];
result.z = left.x * right.columns[0][2];
result.w = left.x * right.columns[0][3];
result.x += left.y * right.columns[1][0];
result.y += left.y * right.columns[1][1];
result.z += left.y * right.columns[1][2];
result.w += left.y * right.columns[1][3];
result.x += left.z * right.columns[2][0];
result.y += left.z * right.columns[2][1];
result.z += left.z * right.columns[2][2];
result.w += left.z * right.columns[2][3];
result.x += left.w * right.columns[3][0];
result.y += left.w * right.columns[3][1];
result.z += left.w * right.columns[3][2];
result.w += left.w * right.columns[3][3];
return result;
}
pub fn multiply(left: Vec4, right: Vec4) Vec4 {
return init(
left.x * right.x,
left.y * right.y,
left.z * right.z,
left.w * right.w
);
}
pub fn asArray(self: *Vec4) []f32 {
const ptr: [*]f32 = @alignCast(@ptrCast(@as(*anyopaque, @ptrCast(self))));
return ptr[0..4];
}
pub fn initArray(array: []const f32) Vec4 {
return Vec4.init(array[0], array[1], array[2], array[3]);
}
pub fn lerp(a: Vec4, b: Vec4, t: f32) Vec4 {
return Vec4.init(
std.math.lerp(a.x, b.x, t),
std.math.lerp(a.y, b.y, t),
std.math.lerp(a.z, b.z, t),
std.math.lerp(a.w, b.w, t),
);
}
pub fn clamp(self: Vec4, min_value: f32, max_value: f32) Vec4 {
return Vec4.init(
std.math.clamp(self.x, min_value, max_value),
std.math.clamp(self.y, min_value, max_value),
std.math.clamp(self.z, min_value, max_value),
std.math.clamp(self.w, min_value, max_value),
);
}
pub fn toVec3XYZ(self: Vec4) Vec3 {
return Vec3.init(self.x, self.y, self.z);
}
};
pub const Mat4 = extern struct {
columns: [4][4]f32,
pub fn initZero() Mat4 {
var self: Mat4 = undefined;
@memset(self.asArray(), 0);
return self;
}
pub fn initIdentity() Mat4 {
return Mat4.initDiagonal(1);
}
pub fn initDiagonal(value: f32) Mat4 {
var self = Mat4.initZero();
self.columns[0][0] = value;
self.columns[1][1] = value;
self.columns[2][2] = value;
self.columns[3][3] = value;
return self;
}
pub fn multiply(left: Mat4, right: Mat4) Mat4 {
var self: Mat4 = undefined;
inline for (.{ 0, 1, 2, 3 }) |i| {
var column = Vec4.initArray(&right.columns[i]).multiplyMat4(left);
@memcpy(&self.columns[i], column.asArray());
}
return self;
}
pub fn initScale(scale: Vec3) Mat4 {
var self = Mat4.initIdentity();
self.columns[0][0] = scale.x;
self.columns[1][1] = scale.y;
self.columns[2][2] = scale.z;
return self;
}
pub fn initTranslate(offset: Vec3) Mat4 {
var self = Mat4.initIdentity();
self.columns[3][0] = offset.x;
self.columns[3][1] = offset.y;
self.columns[3][2] = offset.z;
return self;
}
pub fn asArray(self: *Mat4) []f32 {
const ptr: [*]f32 = @alignCast(@ptrCast(@as(*anyopaque, @ptrCast(&self.columns))));
return ptr[0..16];
}
};
pub const Rect = struct {
pos: Vec2,
size: Vec2,
pub const zero = Rect{
.pos = Vec2.zero,
.size = Vec2.zero
};
pub fn init(x: f32, y: f32, width: f32, height: f32) Rect {
return Rect{
.pos = Vec2.init(x, y),
.size = Vec2.init(width, height)
};
}
pub fn clip(self: Rect, other: Rect) Rect {
const left_edge = @max(self.left(), other.left());
const right_edge = @min(self.right(), other.right());
const top_edge = @max(self.top(), other.top());
const bottom_edge = @min(self.bottom(), other.bottom());
return Rect.init(
left_edge,
top_edge,
right_edge - left_edge,
bottom_edge - top_edge
);
}
pub fn left(self: Rect) f32 {
return self.pos.x;
}
pub fn right(self: Rect) f32 {
return self.pos.x + self.size.x;
}
pub fn top(self: Rect) f32 {
return self.pos.y;
}
pub fn bottom(self: Rect) f32 {
return self.pos.y + self.size.y;
}
pub fn multiply(self: Rect, xy: Vec2) Rect {
return Rect{
.pos = self.pos.multiply(xy),
.size = self.size.multiply(xy),
};
}
pub fn divide(self: Rect, xy: Vec2) Rect {
return Rect{
.pos = self.pos.divide(xy),
.size = self.size.divide(xy),
};
}
pub fn isInside(self: Rect, pos: Vec2) bool {
const x_overlap = self.pos.x <= pos.x and pos.x < self.pos.x + self.size.x;
const y_overlap = self.pos.y <= pos.y and pos.y < self.pos.y + self.size.y;
return x_overlap and y_overlap;
}
};
pub const Line = struct {
p0: Vec2,
p1: Vec2
};
pub fn isInsideRect(rect_pos: Vec2, rect_size: Vec2, pos: Vec2) bool {
const rect = Rect{
.pos = rect_pos,
.size = rect_size
};
return rect.isInside(pos);
}
pub fn rgba(r: u8, g: u8, b: u8, a: f32) Vec4 {
assert(0 <= a and a <= 1);
return Vec4.init(
@as(f32, @floatFromInt(r)) / 255,
@as(f32, @floatFromInt(g)) / 255,
@as(f32, @floatFromInt(b)) / 255,
a,
);
}
pub fn rgb(r: u8, g: u8, b: u8) Vec4 {
return rgba(r, g, b, 1);
}
pub fn rgb_hex(text: []const u8) ?Vec4 {
if (text.len != 7) {
return null;
}
if (text[0] != '#') {
return null;
}
const r = std.fmt.parseInt(u8, text[1..3], 16) catch return null;
const g = std.fmt.parseInt(u8, text[3..5], 16) catch return null;
const b = std.fmt.parseInt(u8, text[5..7], 16) catch return null;
return rgb(r, g, b);
}

177
src/tiled.zig Normal file
View File

@ -0,0 +1,177 @@
const std = @import("std");
const c = @cImport({
@cInclude("stdlib.h");
@cInclude("tmx.h");
});
const math = @import("math.zig");
const Vec2 = math.Vec2;
const log = std.log.scoped(.tiled);
pub const TmxError = error {
MakeResourceManager,
LoadTileset,
LoadMap,
};
pub fn init() void {
c.tmx_alloc_func = c.realloc;
c.tmx_free_func = c.free;
}
pub const ResourceManager = struct {
manager: *anyopaque,
pub fn init() !ResourceManager {
const manager = c.tmx_make_resource_manager();
if (manager == null) {
log.err("tmx_make_resource_manager: {s}", .{c.tmx_strerr()});
return TmxError.MakeResourceManager;
}
return ResourceManager{
.manager = manager.?,
};
}
pub fn loadTilesetFromBuffer(self: *const ResourceManager, tileset: []const u8, key: [*:0]const u8) !void {
const success = c.tmx_load_tileset_buffer(self.manager, tileset.ptr, @intCast(tileset.len), key);
if (success != 1) {
log.err("tmx_load_tileset_buffer: {s}", .{c.tmx_strerr()});
return TmxError.LoadTileset;
}
}
pub fn loadMapFromBuffer(self: *const ResourceManager, map: []const u8) !Map {
const map_handle = c.tmx_rcmgr_load_buffer(self.manager, map.ptr, @intCast(map.len));
if (map_handle == null) {
log.err("tmx_rcmgr_load_buffer: {s}", .{c.tmx_strerr()});
return TmxError.LoadMap;
}
return Map{
.map = map_handle.?
};
}
pub fn deinit(self: *const ResourceManager) void {
c.tmx_free_resource_manager(self.manager);
}
};
pub const Tile = struct {
tile: *c.tmx_tile,
pub fn getUpperLeft(self: Tile) Vec2 {
return Vec2.init(
@floatFromInt(self.tile.ul_x),
@floatFromInt(self.tile.ul_y),
);
}
};
pub const TileWithFlags = struct {
tile: Tile,
flags: u32
};
pub const Map = struct {
map: *c.tmx_map,
pub fn deinit(self: *const Map) void {
c.tmx_map_free(self.map);
}
pub fn iterLayers(self: *const Map) Layer.Iterator {
return Layer.Iterator{
.current = self.map.ly_head
};
}
pub fn getTile(self: *const Map, layer: Layer, x: usize, y: usize) ?Tile {
if (self.getTileWithFlags(layer, x, y)) |tile_with_flags| {
return tile_with_flags.tile;
}
return null;
}
pub fn getTileWithFlags(self: *const Map, layer: Layer, x: usize, y: usize) ?TileWithFlags {
if (layer.layer.type != @intFromEnum(Layer.Type.layer)) {
return null;
}
const gid = layer.layer.content.gids[(y*self.map.width) + x];
const flags = gid & ~FLIP_BITS_REMOVAL;
const maybe_tile = self.map.tiles[gid & FLIP_BITS_REMOVAL];
if (maybe_tile == null) {
return null;
}
return TileWithFlags{
.tile = Tile{ .tile = maybe_tile.? },
.flags = flags
};
}
};
pub const Layer = struct {
layer: *c.tmx_layer,
pub const Type = enum(c_uint) {
none = c.L_NONE,
layer = c.L_LAYER,
object_group = c.L_OBJGR,
image = c.L_IMAGE,
group = c.L_GROUP
};
pub const Iterator = struct {
current: ?*c.tmx_layer,
pub fn next(self: *Iterator) ?Layer {
if (self.current) |current| {
self.current = current.next;
return Layer{ .layer = current };
}
return null;
}
};
};
pub const Properties = struct {
inner: ?*c.tmx_properties,
pub fn getPropertyString(self: Properties, key: [*:0]const u8) ?[*:0]const u8 {
const inner = self.inner orelse return null;
const maybe_prop = c.tmx_get_property(inner, key);
if (maybe_prop == null) {
return null;
}
const prop: *c.tmx_property = maybe_prop.?;
if (prop.type != c.PT_STRING) {
return null;
}
return prop.value.string;
}
pub fn getPropertyBool(self: Properties, key: [*:0]const u8) ?bool {
const inner = self.inner orelse return null;
const maybe_prop = c.tmx_get_property(inner, key);
if (maybe_prop == null) {
return null;
}
const prop: *c.tmx_property = maybe_prop.?;
if (prop.type != c.PT_BOOL) {
return null;
}
return prop.value.boolean != 0;
}
};
pub const FLIP_BITS_REMOVAL: u32 = c.TMX_FLIP_BITS_REMOVAL;

81
src/timer.zig Normal file
View File

@ -0,0 +1,81 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const Entity = @import("./entity.zig");
const GenerationalArrayList = @import("./generational_array_list.zig").GenerationalArrayList;
const Timer = @This();
const ArrayList = GenerationalArrayList(Timer);
pub const Id = ArrayList.Id;
pub const Options = struct {
duration: f64,
entity: ?Entity.Id = null
};
started_at: f64,
finishes_at: f64,
entity: ?Entity.Id,
pub const List = struct {
array_list: GenerationalArrayList(Timer),
now: f64,
pub const empty = List{
.array_list = .empty,
.now = 0
};
pub fn deinit(self: *List, gpa: Allocator) void {
self.array_list.deinit(gpa);
}
pub fn clone(self: *List, gpa: Allocator) !List {
const array_list = try self.array_list.clone(gpa);
errdefer array_list.deinit(gpa);
return List{
.now = self.now,
.array_list = array_list
};
}
pub fn start(self: *List, gpa: Allocator, opts: Options) !Id {
assert(opts.duration > 0);
return try self.array_list.insert(gpa, .{
.started_at = self.now,
.finishes_at = self.now + opts.duration,
.entity = opts.entity
});
}
pub fn stop(self: *List, id: Id) void {
_ = self.array_list.remove(id);
}
pub fn running(self: *List, id: Id) bool {
const timer = self.array_list.get(id) orelse return false;
return timer.finishes_at > self.now;
}
pub fn percent_passed(self: *List, id: Id) f32 {
const timer = self.array_list.get(id) orelse return 0;
const time_passed = self.now - timer.started_at;
const duration = timer.finishes_at - timer.started_at;
return @floatCast(std.math.clamp(time_passed / duration, 0, 1));
}
pub fn finished(self: *List, id: Id) bool {
const timer = self.array_list.get(id) orelse return false;
if (timer.finishes_at > self.now) {
return false;
}
self.array_list.removeAssumeExists(id);
return true;
}
};

553
src/window.zig Normal file
View File

@ -0,0 +1,553 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const sokol = @import("sokol");
const sapp = sokol.app;
const Gfx = @import("./graphics.zig");
const Tiled = @import("./tiled.zig");
const Math = @import("./math.zig");
const Vec2 = Math.Vec2;
const rgb = Math.rgb;
const Game = @import("./game.zig");
const Window = @This();
const log = std.log.scoped(.window);
pub const MouseButton = enum {
left,
right,
middle,
pub fn fromSokol(mouse_button: sokol.app.Mousebutton) ?MouseButton {
return switch(mouse_button) {
.LEFT => MouseButton.left,
.RIGHT => MouseButton.right,
.MIDDLE => MouseButton.middle,
else => null
};
}
};
pub const KeyCode = enum(std.math.IntFittingRange(0, sokol.app.max_keycodes-1)) {
SPACE = 32,
APOSTROPHE = 39,
COMMA = 44,
MINUS = 45,
PERIOD = 46,
SLASH = 47,
_0 = 48,
_1 = 49,
_2 = 50,
_3 = 51,
_4 = 52,
_5 = 53,
_6 = 54,
_7 = 55,
_8 = 56,
_9 = 57,
SEMICOLON = 59,
EQUAL = 61,
A = 65,
B = 66,
C = 67,
D = 68,
E = 69,
F = 70,
G = 71,
H = 72,
I = 73,
J = 74,
K = 75,
L = 76,
M = 77,
N = 78,
O = 79,
P = 80,
Q = 81,
R = 82,
S = 83,
T = 84,
U = 85,
V = 86,
W = 87,
X = 88,
Y = 89,
Z = 90,
LEFT_BRACKET = 91,
BACKSLASH = 92,
RIGHT_BRACKET = 93,
GRAVE_ACCENT = 96,
WORLD_1 = 161,
WORLD_2 = 162,
ESCAPE = 256,
ENTER = 257,
TAB = 258,
BACKSPACE = 259,
INSERT = 260,
DELETE = 261,
RIGHT = 262,
LEFT = 263,
DOWN = 264,
UP = 265,
PAGE_UP = 266,
PAGE_DOWN = 267,
HOME = 268,
END = 269,
CAPS_LOCK = 280,
SCROLL_LOCK = 281,
NUM_LOCK = 282,
PRINT_SCREEN = 283,
PAUSE = 284,
F1 = 290,
F2 = 291,
F3 = 292,
F4 = 293,
F5 = 294,
F6 = 295,
F7 = 296,
F8 = 297,
F9 = 298,
F10 = 299,
F11 = 300,
F12 = 301,
F13 = 302,
F14 = 303,
F15 = 304,
F16 = 305,
F17 = 306,
F18 = 307,
F19 = 308,
F20 = 309,
F21 = 310,
F22 = 311,
F23 = 312,
F24 = 313,
F25 = 314,
KP_0 = 320,
KP_1 = 321,
KP_2 = 322,
KP_3 = 323,
KP_4 = 324,
KP_5 = 325,
KP_6 = 326,
KP_7 = 327,
KP_8 = 328,
KP_9 = 329,
KP_DECIMAL = 330,
KP_DIVIDE = 331,
KP_MULTIPLY = 332,
KP_SUBTRACT = 333,
KP_ADD = 334,
KP_ENTER = 335,
KP_EQUAL = 336,
LEFT_SHIFT = 340,
LEFT_CONTROL = 341,
LEFT_ALT = 342,
LEFT_SUPER = 343,
RIGHT_SHIFT = 344,
RIGHT_CONTROL = 345,
RIGHT_ALT = 346,
RIGHT_SUPER = 347,
MENU = 348,
};
pub const Event = union(enum) {
mouse_pressed: struct {
button: MouseButton,
position: Vec2,
},
mouse_released: struct {
button: MouseButton,
position: Vec2,
},
mouse_move: Vec2,
mouse_enter: Vec2,
mouse_leave,
mouse_scroll: Vec2,
key_pressed: struct {
code: KeyCode,
repeat: bool
},
key_released: KeyCode,
window_resize,
char: u21,
};
pub const KeyState = struct {
down: bool,
pressed: bool,
released: bool,
down_duration: ?f64,
pub const RepeatOptions = struct {
first_at: f64 = 0,
period: f64
};
pub fn repeat(self: KeyState, last_repeat_at: *?f64, opts: RepeatOptions) bool {
if (!self.down) {
last_repeat_at.* = null;
return false;
}
const down_duration = self.down_duration.?;
if (last_repeat_at.* != null) {
if (down_duration >= last_repeat_at.*.? + opts.period) {
last_repeat_at.* = last_repeat_at.*.? + opts.period;
return true;
}
} else {
if (down_duration >= opts.first_at) {
last_repeat_at.* = opts.first_at;
return true;
}
}
return false;
}
};
const Nanoseconds = i128;
gpa: Allocator,
events: std.ArrayList(Event),
mouse_inside: bool = false,
game: Game,
last_frame_at_ns: Nanoseconds,
frame_dt_ns: Nanoseconds,
time_ns: Nanoseconds,
down_keys: std.EnumSet(KeyCode) = .initEmpty(),
pressed_keys: std.EnumSet(KeyCode) = .initEmpty(),
released_keys: std.EnumSet(KeyCode) = .initEmpty(),
pressed_keys_at: std.EnumMap(KeyCode, Nanoseconds) = .init(.{}),
pub fn init(self: *Window, gpa: Allocator) !void {
Tiled.init();
var events: std.ArrayList(Event) = .empty;
errdefer events.deinit(gpa);
try events.ensureTotalCapacityPrecise(gpa, 50);
try Gfx.init(.{
.allocator = gpa,
.logger = .{ .func = sokolLogCallback }
});
var game = try Game.init(gpa);
errdefer game.deinit();
self.* = Window{
.gpa = gpa,
.events = events,
.last_frame_at_ns = std.time.nanoTimestamp(),
.frame_dt_ns = 0,
.time_ns = 0,
.game = game
};
}
pub fn deinit(self: *Window) void {
const gpa = self.gpa;
self.game.deinit();
self.events.deinit(gpa);
Gfx.deinit();
}
pub fn frame(self: *Window) !void {
const now = std.time.nanoTimestamp();
self.frame_dt_ns = now - self.last_frame_at_ns;
self.last_frame_at_ns = now;
self.time_ns += self.frame_dt_ns;
Gfx.beginFrame();
defer Gfx.endFrame();
self.pressed_keys = .initEmpty();
self.released_keys = .initEmpty();
for (self.events.items) |e| {
switch (e) {
.key_pressed => |opts| {
if (!opts.repeat) {
self.pressed_keys_at.put(opts.code, self.time_ns);
self.pressed_keys.insert(opts.code);
self.down_keys.insert(opts.code);
}
},
.key_released => |key_code| {
self.down_keys.remove(key_code);
self.released_keys.insert(key_code);
self.pressed_keys_at.remove(key_code);
},
.mouse_leave => {
var iter = self.down_keys.iterator();
while (iter.next()) |key_code| {
self.released_keys.insert(key_code);
}
self.down_keys = .initEmpty();
self.pressed_keys_at = .init(.{});
},
else => {}
}
}
self.events.clearRetainingCapacity();
// TODO: Render to a lower resolution instead of scaling.
// To avoid pixel bleeding in spritesheet artifacts
const window_size: Vec2 = .init(sapp.widthf(), sapp.heightf());
const scale = @floor(@min(
window_size.x / self.game.canvas_size.x,
window_size.y / self.game.canvas_size.y,
));
var filler_size: Vec2 = Vec2.sub(window_size, self.game.canvas_size.multiplyScalar(scale)).multiplyScalar(0.5);
filler_size.x = @round(filler_size.x);
filler_size.y = @round(filler_size.y);
const input = self.game.getInput(self);
{
Gfx.pushTransform(filler_size, scale);
defer Gfx.popTransform();
try self.game.tick(input);
}
const bg_color = rgb(0, 0, 0);
Gfx.drawRectangle(
.init(0, 0),
.init(window_size.x, filler_size.y),
bg_color
);
Gfx.drawRectangle(
.init(0, window_size.y - filler_size.y),
.init(window_size.x, filler_size.y),
bg_color
);
Gfx.drawRectangle(
.init(0, 0),
.init(filler_size.x, window_size.y),
bg_color
);
Gfx.drawRectangle(
.init(window_size.x - filler_size.x, 0),
.init(filler_size.x, window_size.y),
bg_color
);
try self.game.debug();
}
pub fn isKeyDown(self: *Window, key_code: KeyCode) bool {
return self.down_keys.contains(key_code);
}
pub fn getKeyDownDuration(self: *Window, key_code: KeyCode) ?f64 {
if (!self.isKeyDown(key_code)) {
return null;
}
const pressed_at_ns = self.pressed_keys_at.get(key_code).?;
const duration_ns = self.time_ns - pressed_at_ns;
return @as(f64, @floatFromInt(duration_ns)) / std.time.ns_per_s;
}
pub fn isKeyPressed(self: *Window, key_code: KeyCode) bool {
return self.pressed_keys.contains(key_code);
}
pub fn isKeyReleased(self: *Window, key_code: KeyCode) bool {
return self.released_keys.contains(key_code);
}
pub fn getKeyState(self: *Window, key_code: KeyCode) KeyState {
return KeyState{
.down = self.isKeyDown(key_code),
.released = self.isKeyReleased(key_code),
.pressed = self.isKeyPressed(key_code),
.down_duration = self.getKeyDownDuration(key_code)
};
}
fn appendEvent(self: *Window, e: Event) !void {
self.events.appendBounded(e) catch return error.EventQueueFull;
}
pub fn event(self: *Window, e_ptr: [*c]const sapp.Event) !bool {
if (Gfx.event(e_ptr)) {
try self.appendEvent(Event{
.mouse_leave = {}
});
self.mouse_inside = false;
return true;
}
const e = e_ptr.*;
blk: switch (e.type) {
.MOUSE_DOWN => {
const mouse_button = Window.MouseButton.fromSokol(e.mouse_button) orelse break :blk;
try self.appendEvent(Event{
.mouse_pressed = .{
.button = mouse_button,
.position = Vec2.init(e.mouse_x, e.mouse_y)
}
});
return true;
},
.MOUSE_UP => {
const mouse_button = MouseButton.fromSokol(e.mouse_button) orelse break :blk;
try self.appendEvent(Event{
.mouse_released = .{
.button = mouse_button,
.position = Vec2.init(e.mouse_x, e.mouse_y)
}
});
return true;
},
.MOUSE_MOVE => {
if (!self.mouse_inside) {
try self.appendEvent(Event{
.mouse_enter = Vec2.init(e.mouse_x, e.mouse_y)
});
}
try self.appendEvent(Event{
.mouse_move = Vec2.init(e.mouse_x, e.mouse_y)
});
self.mouse_inside = true;
return true;
},
.MOUSE_ENTER => {
if (!self.mouse_inside) {
try self.appendEvent(Event{
.mouse_enter = Vec2.init(e.mouse_x, e.mouse_y)
});
}
self.mouse_inside = true;
return true;
},
.RESIZED => {
if (self.mouse_inside) {
try self.appendEvent(Event{
.mouse_leave = {}
});
}
try self.appendEvent(Event{
.window_resize = {}
});
self.mouse_inside = false;
return true;
},
.MOUSE_LEAVE => {
if (self.mouse_inside) {
try self.appendEvent(Event{
.mouse_leave = {}
});
}
self.mouse_inside = false;
return true;
},
.MOUSE_SCROLL => {
try self.appendEvent(Event{
.mouse_scroll = Vec2.init(e.scroll_x, e.scroll_y)
});
return true;
},
.KEY_DOWN => {
try self.appendEvent(Event{
.key_pressed = .{
.code = @enumFromInt(@intFromEnum(e.key_code)),
.repeat = e.key_repeat
}
});
return true;
},
.KEY_UP => {
try self.appendEvent(Event{
.key_released = @enumFromInt(@intFromEnum(e.key_code))
});
return true;
},
.CHAR => {
try self.appendEvent(Event{
.char = @intCast(e.char_code)
});
return true;
},
.QUIT_REQUESTED => {
// TODO: handle quit request. Maybe show confirmation window in certain cases.
},
else => {}
}
return false;
}
fn cStrToZig(c_str: [*c]const u8) [:0]const u8 {
return std.mem.span(c_str);
}
fn sokolLogFmt(log_level: u32, comptime format: []const u8, args: anytype) void {
const log_sokol = std.log.scoped(.sokol);
if (log_level == 0) {
log_sokol.err(format, args);
} else if (log_level == 1) {
log_sokol.err(format, args);
} else if (log_level == 2) {
log_sokol.warn(format, args);
} else {
log_sokol.info(format, args);
}
}
pub fn sokolLogCallback(tag: [*c]const u8, log_level: u32, log_item: u32, message: [*c]const u8, line_nr: u32, filename: [*c]const u8, user_data: ?*anyopaque) callconv(.c) void {
_ = user_data;
if (filename != null) {
const format = "[{s}][id:{}] {s}:{}: {s}";
const args = .{
cStrToZig(tag orelse "-"),
log_item,
std.fs.path.basename(cStrToZig(filename orelse "-")),
line_nr,
cStrToZig(message orelse "")
};
sokolLogFmt(log_level, format, args);
} else {
const format = "[{s}][id:{}] {s}";
const args = .{
cStrToZig(tag orelse "-"),
log_item,
cStrToZig(message orelse "")
};
sokolLogFmt(log_level, format, args);
}
}