Compare commits

..

10 Commits

Author SHA1 Message Date
a5d824817c adjust waves 2026-02-01 06:45:32 +02:00
f96a8b4b78 sort enemy draw 2026-02-01 05:45:50 +02:00
0b3f4486c0 add animations 2026-02-01 05:19:00 +02:00
a249dc3cf9 flip sprites 2026-02-01 04:16:02 +02:00
a4d803fc57 add background 2026-02-01 03:48:44 +02:00
beeacdc701 add sprites 2026-02-01 02:22:13 +02:00
d2ecfcafe9 implement cluster enemy 2026-01-31 23:20:34 +02:00
11be4f21ac implement snake enemy 2026-01-31 21:58:39 +02:00
c9543c72ad add bomb 2026-01-31 19:23:48 +02:00
ba264d7bc5 add laser weapon 2026-01-31 18:19:15 +02:00
82 changed files with 1735 additions and 495 deletions

View File

@ -13,6 +13,7 @@ pub fn build(b: *std.Build) !void {
const has_console = b.option(bool, "console", "Show console (Window only)") orelse (optimize == .Debug);
const isWasm = target.result.cpu.arch.isWasm();
const isWindows = target.result.os.tag == .windows;
if (isWasm) {
has_tracy = false;
@ -25,7 +26,12 @@ pub fn build(b: *std.Build) !void {
.link_libc = true,
});
const dep_sokol = b.dependency("sokol", .{ .target = target, .optimize = optimize, .with_sokol_imgui = has_imgui, .vulkan = true });
const dep_sokol = b.dependency("sokol", .{
.target = target,
.optimize = optimize,
.with_sokol_imgui = has_imgui,
.vulkan = if (isWasm or isWindows) null else true
});
mod_main.linkLibrary(dep_sokol.artifact("sokol_clib"));
mod_main.addImport("sokol", dep_sokol.module("sokol"));
@ -62,7 +68,8 @@ pub fn build(b: *std.Build) !void {
{
var cflags_buffer: [64][]const u8 = undefined;
var cflags = std.ArrayListUnmanaged([]const u8).initBuffer(&cflags_buffer);
switch (sokol.resolveSokolBackend(.vulkan, target.result)) {
const backend: sokol.SokolBackend = if (isWasm or isWindows) .auto else .vulkan;
switch (sokol.resolveSokolBackend(backend, target.result)) {
.d3d11 => try cflags.appendBounded("-DSOKOL_D3D11"),
.metal => try cflags.appendBounded("-DSOKOL_METAL"),
.gl => try cflags.appendBounded("-DSOKOL_GLCORE"),
@ -118,8 +125,8 @@ fn buildNative(b: *std.Build, name: []const u8, mod: *std.Build.Module, has_cons
.root_source_file = b.path("tools/png-to-icon.zig"),
}),
});
const dep_stb_image = b.dependency("stb_image", .{});
png_to_icon_tool.root_module.addImport("stb_image", dep_stb_image.module("stb_image"));
const dep_stb = b.dependency("stb", .{});
png_to_icon_tool.root_module.addImport("stb_image", dep_stb.module("stb_image"));
const png_to_icon_step = b.addRunArtifact(png_to_icon_tool);
png_to_icon_step.addFileArg(b.path("src/assets/icon.png"));
@ -209,7 +216,11 @@ fn buildWasm(b: *std.Build, opts: BuildWasmOptions) !void {
const dep_emsdk = opts.dep_sokol.builder.dependency("emsdk", .{});
patchWasmIncludeDirs(opts.mod_main, dep_emsdk.path("upstream/emscripten/cache/sysroot/include"), &(opts.dep_sokol.artifact("sokol_clib").step));
patchWasmIncludeDirs(
opts.mod_main,
dep_emsdk.path("upstream/emscripten/cache/sysroot/include"),
&(opts.dep_sokol.artifact("sokol_clib").step)
);
// create a build step which invokes the Emscripten linker
const link_step = try sokol.emLinkStep(b, .{
@ -221,6 +232,7 @@ fn buildWasm(b: *std.Build, opts: BuildWasmOptions) !void {
.use_emmalloc = true,
.use_filesystem = false,
.shell_file_path = b.path("src/engine/shell.html"),
.extra_args = &.{ "-sASSERTIONS=1", "-sALLOW_MEMORY_GROWTH=1" }
});
// attach to default target
b.getInstallStep().dependOn(&link_step.step);

View File

@ -136,6 +136,7 @@ pub const TileVariant = struct {
const encoded_tiles = try lexer.nextExpectText();
const tiles = try parseEncoding(encoding, scratch.allocator(), encoded_tiles);
try temp_tiles.appendSlice(scratch.allocator(), tiles.items);
continue;
} else if (node.isTag("chunk")) {
const chunk = try initChunkDataFromXml(arena, scratch, lexer, encoding);
@ -158,7 +159,6 @@ pub const TileVariant = struct {
.fixed = try arena.dupe(u32, temp_tiles.items)
};
}
}
fn parseEncoding(

View File

@ -73,6 +73,9 @@ pub const TilesetReference = struct {
pub const Tile = struct {
tileset: *const Tileset,
id: u32,
flip_horizontal: bool,
flip_vertical: bool,
flip_diagonal: bool,
pub fn getProperties(self: Tile) Property.List {
return self.tileset.getTileProperties(self.id) orelse .empty;
@ -216,13 +219,19 @@ fn getTilesetByGid(self: *const Tilemap, gid: u32) ?TilesetReference {
}
pub fn getTile(self: *const Tilemap, tilesets: Tileset.List, gid: u32) ?Tile {
const tileset_ref = self.getTilesetByGid(gid & GlobalTileId.Flag.clear) orelse return null;
const Flag = GlobalTileId.Flag;
const gid_without_flags = gid & Flag.clear;
const tileset_ref = self.getTilesetByGid(gid_without_flags) orelse return null;
const tileset = tilesets.get(tileset_ref.source) orelse return null;
const id = gid - tileset_ref.first_gid;
const id = gid_without_flags - tileset_ref.first_gid;
return Tile{
.tileset = tileset,
.id = id
.id = id,
.flip_horizontal = (gid & @intFromEnum(Flag.flipped_horizontally)) > 0,
.flip_vertical = (gid & @intFromEnum(Flag.flipped_vertically)) > 0,
.flip_diagonal = (gid & @intFromEnum(Flag.flipped_diagonally)) > 0
};
}

View File

@ -24,7 +24,6 @@ pub const Tilemap = struct {
texture: Gfx.TextureId,
tile_size: Engine.Vec2,
pub fn getTileUV(self: Tilemap, tile_x: f32, tile_y: f32) Rect {
const texture_info = Engine.Graphics.getTextureInfo(self.texture);
const tilemap_size = Vec2.initFromInt(u32, texture_info.width, texture_info.height);
@ -39,14 +38,19 @@ pub const Tilemap = struct {
arena: std.heap.ArenaAllocator,
font_id: FontName.EnumArray,
wood01: Audio.Data.Id,
pistol_mask: Gfx.TextureId,
bomb_mask: Gfx.TextureId,
laser_mask: Gfx.TextureId,
bullet: Gfx.TextureId,
shadow: Gfx.TextureId,
dungeon_tilemap: Tilemap,
skeleton: Gfx.Sprite,
snake: Gfx.Sprite,
fire_spirit: Gfx.Sprite,
map: tiled.Tilemap,
tilesets: tiled.Tileset.List,
move_sound: []Audio.Data.Id,
terrain_tilemap: Tilemap,
players_tilemap: Tilemap,
weapons_tilemap: Tilemap,
pub fn init(gpa: std.mem.Allocator) !Assets {
var arena = std.heap.ArenaAllocator.init(gpa);
@ -58,11 +62,88 @@ pub fn init(gpa: std.mem.Allocator) !Assets {
.italic = try Gfx.addFont("italic", @embedFile("assets/roboto-font/Roboto-Italic.ttf")),
});
const wood01 = try Audio.load(.{
.format = .vorbis,
.data = @embedFile("assets/wood01.ogg"),
const pistol_mask_image = try STBImage.load(@embedFile("assets/pistol-mask.png"));
defer pistol_mask_image.deinit();
const pistol_mask_texture = try Gfx.addTexture(&.{
.{
.width = pistol_mask_image.width,
.height = pistol_mask_image.height,
.rgba = pistol_mask_image.rgba8_pixels
}
});
const bomb_mask_image = try STBImage.load(@embedFile("assets/bomb-mask.png"));
defer bomb_mask_image.deinit();
const bomb_mask_texture = try Gfx.addTexture(&.{
.{
.width = bomb_mask_image.width,
.height = bomb_mask_image.height,
.rgba = bomb_mask_image.rgba8_pixels
}
});
const laser_mask_image = try STBImage.load(@embedFile("assets/laser-mask.png"));
defer laser_mask_image.deinit();
const laser_mask_texture = try Gfx.addTexture(&.{
.{
.width = laser_mask_image.width,
.height = laser_mask_image.height,
.rgba = laser_mask_image.rgba8_pixels
}
});
const skeleton_image = try STBImage.load(@embedFile("assets/skeleton.png"));
defer skeleton_image.deinit();
const skeleton_texture = try Gfx.addTexture(&.{
.{
.width = skeleton_image.width,
.height = skeleton_image.height,
.rgba = skeleton_image.rgba8_pixels
}
});
const snake_image = try STBImage.load(@embedFile("assets/snake.png"));
defer snake_image.deinit();
const snake_texture = try Gfx.addTexture(&.{
.{
.width = snake_image.width,
.height = snake_image.height,
.rgba = snake_image.rgba8_pixels
}
});
const fire_spirit_image = try STBImage.load(@embedFile("assets/fire-spirit.png"));
defer fire_spirit_image.deinit();
const fire_spirit_texture = try Gfx.addTexture(&.{
.{
.width = fire_spirit_image.width,
.height = fire_spirit_image.height,
.rgba = fire_spirit_image.rgba8_pixels
}
});
const bullet_image = try STBImage.load(@embedFile("assets/bullet.png"));
defer bullet_image.deinit();
const bullet_texture = try Gfx.addTexture(&.{
.{
.width = bullet_image.width,
.height = bullet_image.height,
.rgba = bullet_image.rgba8_pixels
}
});
const snake = Gfx.Sprite{
.texture = snake_texture,
.uv = .unit
};
const skeleton = Gfx.Sprite{
.texture = skeleton_texture,
.uv = .unit
};
const fire_spirit = Gfx.Sprite{
.texture = fire_spirit_texture,
.uv = .unit
};
var scratch = std.heap.ArenaAllocator.init(gpa);
defer scratch.deinit();
@ -80,76 +161,54 @@ pub fn init(gpa: std.mem.Allocator) !Assets {
gpa,
&scratch,
&xml_buffers,
@embedFile("assets/tileset.tsx")
@embedFile("assets/dungeon.tsx")
);
var tilesets: tiled.Tileset.List = .empty;
try tilesets.add(gpa, "tilemap.tsx", tileset);
try tilesets.add(gpa, "dungeon.tsx", tileset);
const players_image = try STBImage.load(@embedFile("assets/kenney_desert-shooter-pack_1.0/PNG/Players/tilemap_packed.png"));
defer players_image.deinit();
const players_texture = try Gfx.addTexture(&.{
const dungeon_image = try STBImage.load(@embedFile("assets/tiny-dungeon/tilemap_packed.png"));
defer dungeon_image.deinit();
const dungeon_texture = try Gfx.addTexture(&.{
.{
.width = players_image.width,
.height = players_image.height,
.rgba = players_image.rgba8_pixels
.width = dungeon_image.width,
.height = dungeon_image.height,
.rgba = dungeon_image.rgba8_pixels
}
});
const tileset_image = try STBImage.load(@embedFile("assets/kenney_desert-shooter-pack_1.0/PNG/Tiles/tilemap_packed.png"));
defer tileset_image.deinit();
const tileset_texture = try Gfx.addTexture(&.{
const shadow_image = try STBImage.load(@embedFile("assets/shadow.png"));
defer shadow_image.deinit();
const shadow_texture = try Gfx.addTexture(&.{
.{
.width = tileset_image.width,
.height = tileset_image.height,
.rgba = tileset_image.rgba8_pixels
.width = shadow_image.width,
.height = shadow_image.height,
.rgba = shadow_image.rgba8_pixels
}
});
const weapons_tileset = try STBImage.load(@embedFile("assets/kenney_desert-shooter-pack_1.0/PNG/Weapons/tilemap_packed.png"));
defer weapons_tileset.deinit();
const weapons_texture = try Gfx.addTexture(&.{
.{
.width = weapons_tileset.width,
.height = weapons_tileset.height,
.rgba = weapons_tileset.rgba8_pixels
}
});
const move_c = try Audio.load(.{
.data = @embedFile("assets/kenney_desert-shooter-pack_1.0/Sounds/move-c.ogg"),
.format = .vorbis,
});
const move_d = try Audio.load(.{
.data = @embedFile("assets/kenney_desert-shooter-pack_1.0/Sounds/move-d.ogg"),
.format = .vorbis,
});
const move_sound = try arena.allocator().dupe(Audio.Data.Id, &.{ move_c, move_d });
return Assets{
.arena = arena,
.font_id = font_id_array,
.wood01 = wood01,
.pistol_mask = pistol_mask_texture,
.bomb_mask = bomb_mask_texture,
.laser_mask = laser_mask_texture,
.snake = snake,
.skeleton = skeleton,
.fire_spirit = fire_spirit,
.map = map,
.tilesets = tilesets,
.move_sound = move_sound,
.terrain_tilemap = .{
.texture = tileset_texture,
.tile_size = .initFromInt(u32, tileset.tile_width, tileset.tile_height)
},
.players_tilemap = .{
.texture = players_texture,
.tile_size = .init(24, 24)
},
.weapons_tilemap = .{
.texture = weapons_texture,
.tile_size = .init(24, 24)
},
.bullet = bullet_texture,
.shadow = shadow_texture,
.dungeon_tilemap = .{
.texture = dungeon_texture,
.tile_size = .init(16, 16)
}
};
}
pub fn deinit(self: *Assets, gpa: std.mem.Allocator) void {
self.map.deinit();
self.tilesets.deinit(gpa);
self.map.deinit();
self.arena.deinit();
}

BIN
src/assets/bomb-mask.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 529 B

BIN
src/assets/bullet.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 B

4
src/assets/dungeon.tsx Normal file
View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<tileset version="1.10" tiledversion="1.11.2" name="dungeon" tilewidth="16" tileheight="16" tilecount="132" columns="12">
<image source="tiny-dungeon/tilemap_packed.png" width="192" height="176"/>
</tileset>

BIN
src/assets/fire-spirit.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 320 B

View File

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

View File

@ -1,9 +0,0 @@
Tilesheet information:
Tile size • 24px × 24px
Space between tiles • 1px × 1px
---
Total tiles (horizontal) • 4 tiles
Total tiles (vertical) • 4 tiles
---
Total tiles in sheet • 16 tiles

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -1,9 +0,0 @@
Tilesheet information:
Tile size • 24px × 24px
Space between tiles • 1px × 1px
---
Total tiles (horizontal) • 4 tiles
Total tiles (vertical) • 4 tiles
---
Total tiles in sheet • 16 tiles

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1,9 +0,0 @@
Tilesheet information:
Tile size • 16px × 16px
Space between tiles • 1px × 1px
---
Total tiles (horizontal) • 18 tiles
Total tiles (vertical) • 13 tiles
---
Total tiles in sheet • 234 tiles

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

View File

@ -1,9 +0,0 @@
Tilesheet information:
Tile size • 24px × 24px
Space between tiles • 1px × 1px
---
Total tiles (horizontal) • 10 tiles
Total tiles (vertical) • 4 tiles
---
Total tiles in sheet • 40 tiles

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

BIN
src/assets/laser-mask.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 557 B

View File

@ -1,225 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.10" tiledversion="1.11.2" orientation="orthogonal" renderorder="right-down" width="30" height="20" tilewidth="16" tileheight="16" infinite="1" nextlayerid="5" nextobjectid="2">
<tileset firstgid="1" source="tilemap.tsx"/>
<layer id="1" name="ground" width="30" height="20">
<map version="1.10" tiledversion="1.11.2" orientation="orthogonal" renderorder="right-down" width="20" height="15" tilewidth="16" tileheight="16" infinite="0" nextlayerid="2" nextobjectid="1">
<tileset firstgid="1" source="dungeon.tsx"/>
<layer id="1" name="Tile Layer 1" width="20" height="15">
<data encoding="csv">
<chunk x="-32" y="-32" width="16" height="16">
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,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,109,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,109
</chunk>
<chunk x="-16" y="-32" width="16" height="16">
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,65,65,0,0,0,0,0,0,
92,92,92,92,92,92,92,93,65,65,0,0,0,0,0,0,
110,20,110,110,110,110,110,112,110,65,0,0,0,0,0,0,
110,110,110,110,20,110,110,110,110,65,65,0,0,0,0,0
</chunk>
<chunk x="-32" y="-16" width="16" height="16">
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,109,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,109,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,127,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
</chunk>
<chunk x="-16" y="-16" width="16" height="16">
110,110,20,110,110,110,110,94,110,65,65,0,0,0,65,0,
110,20,110,110,110,110,20,111,65,65,65,0,0,0,65,0,
128,128,128,128,128,128,128,129,65,65,65,0,65,65,65,0,
0,0,0,65,65,65,65,65,65,65,65,65,195,65,65,0,
0,0,0,65,65,65,65,65,65,65,65,65,65,65,65,0,
0,0,65,65,65,65,65,65,65,65,65,65,66,65,65,65,
0,0,65,65,65,65,66,65,65,66,65,65,65,65,65,65,
0,0,0,65,65,65,65,65,65,65,66,65,65,65,65,65,
0,0,0,65,65,195,65,65,65,65,65,65,65,65,65,65,
0,0,0,65,65,65,65,66,65,65,65,65,65,65,65,65,
0,0,0,65,65,65,65,65,65,66,65,65,195,65,65,65,
0,0,0,65,65,65,65,65,65,65,65,65,65,65,66,65,
0,65,65,65,65,65,66,65,65,65,65,65,65,65,65,65,
0,0,65,65,65,65,65,65,65,65,65,65,65,65,65,65,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
</chunk>
<chunk x="0" y="-16" width="16" height="16">
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
65,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
65,65,65,65,65,65,0,0,0,0,0,0,0,0,0,0,
65,65,65,65,65,65,0,0,0,0,0,0,0,0,0,0,
65,65,65,65,65,65,65,0,0,0,0,0,0,0,0,0,
65,65,65,65,65,65,65,0,0,0,0,0,0,0,0,0,
65,65,65,65,65,65,0,0,0,0,0,0,0,0,0,0,
65,65,65,65,65,0,0,0,0,0,0,0,0,0,0,0,
65,65,65,0,0,0,0,0,0,0,0,0,0,0,0,0,
65,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
</chunk>
</data>
2147483701,51,51,51,52,52,51,51,51,51,51,52,52,52,51,51,51,51,51,53,
536870963,49,49,49,49,49,49,49,49,50,50,50,50,50,49,49,49,49,49,2684354611,
1610612788,49,43,43,49,49,49,49,49,50,50,50,50,50,49,49,43,43,49,2684354611,
1610612788,49,43,43,49,49,50,49,49,49,50,50,50,50,49,49,43,43,50,2684354611,
1610612788,50,50,49,49,49,50,50,49,49,49,49,49,49,49,49,49,50,50,3758096436,
1610612788,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,50,3758096436,
1610612788,49,49,49,49,49,49,50,50,50,50,49,49,50,49,49,49,49,49,3758096436,
536870963,49,49,49,49,49,49,49,50,50,50,50,50,50,49,49,49,49,49,2684354611,
536870963,49,50,50,49,49,49,49,49,49,50,50,50,50,49,49,49,50,50,3758096436,
536870963,49,50,50,49,49,49,49,49,49,49,49,49,49,49,49,50,50,50,3758096436,
1610612788,50,50,49,49,49,49,50,50,49,49,49,49,49,49,50,50,50,49,2684354611,
1610612788,49,43,43,49,49,50,50,50,50,50,49,49,49,49,50,43,43,49,2684354611,
536870963,49,43,43,49,50,50,50,49,49,49,49,49,49,49,50,43,43,49,3758096436,
536870963,49,49,49,49,50,50,49,49,49,49,49,49,49,49,49,49,49,49,2684354611,
3221225525,1073741875,1073741875,1073741875,1073741875,1073741876,1073741876,1073741875,1073741875,1073741875,1073741875,1073741875,1073741875,1073741876,1073741876,1073741875,1073741875,1073741875,1073741876,1073741877
</data>
</layer>
<layer id="3" name="walls" width="30" height="20">
<properties>
<property name="solid" type="bool" value="true"/>
</properties>
<data encoding="csv">
<chunk x="-16" y="-32" width="16" height="16">
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,1,2,2,2,2,3,0,0,
0,0,0,0,0,0,0,0,19,110,20,110,20,21,0,0,
0,0,0,0,0,0,0,0,19,110,110,20,20,21,0,0,
0,0,0,0,0,0,0,0,37,38,38,38,38,39,0,0,
0,0,0,0,0,0,0,0,73,56,174,174,174,57,0,0,
0,0,0,0,0,0,0,0,73,174,174,56,174,57,0,0,
0,0,0,0,0,0,0,0,73,174,174,174,174,57,0,0
</chunk>
<chunk x="-16" y="-16" width="16" height="16">
0,0,0,0,0,0,0,0,73,174,56,56,174,57,0,0,
0,0,0,0,0,0,0,0,73,174,174,174,56,57,0,0,
0,0,0,0,0,0,0,0,73,74,74,74,74,75,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
</chunk>
</data>
</layer>
<layer id="4" name="walls 2" width="30" height="20">
<properties>
<property name="solid" type="bool" value="true"/>
</properties>
<data encoding="csv">
<chunk x="-16" y="-32" width="16" height="16">
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,1,2,2,
0,0,0,0,0,0,0,0,0,0,0,0,0,37,38,38
</chunk>
<chunk x="0" y="-32" width="16" height="16">
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
2,2,2,3,0,0,0,0,0,0,0,0,0,0,0,0,
38,38,38,39,0,0,0,0,0,0,0,0,0,0,0,0
</chunk>
<chunk x="-16" y="-16" width="16" height="16">
0,0,0,0,0,0,0,0,0,0,0,0,0,73,174,174,
0,0,0,0,0,0,0,0,0,0,0,0,0,73,174,174,
0,0,0,0,0,0,0,0,0,0,0,0,0,73,56,174,
0,0,0,0,0,0,0,0,0,0,0,0,0,73,56,56,
0,0,0,0,0,0,0,0,0,0,0,0,0,73,74,74,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
</chunk>
<chunk x="0" y="-16" width="16" height="16">
56,56,174,75,0,0,0,0,0,0,0,0,0,0,0,0,
174,174,174,75,0,0,0,0,0,0,0,0,0,0,0,0,
174,174,56,75,0,0,0,0,0,0,0,0,0,0,0,0,
174,56,56,75,0,0,0,0,0,0,0,0,0,0,0,0,
74,74,74,75,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
</chunk>
</data>
</layer>
<objectgroup id="2" name="Object Layer 1">
<object id="1" name="spawnpoint" x="-118" y="-120.5">
<point/>
</object>
</objectgroup>
</map>

BIN
src/assets/pistol-mask.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 552 B

BIN
src/assets/shadow.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 B

BIN
src/assets/skeleton.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 413 B

BIN
src/assets/snake.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

View File

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<tileset version="1.10" tiledversion="1.11.2" name="tilemap" tilewidth="16" tileheight="16" tilecount="234" columns="18">
<image source="kenney_desert-shooter-pack_1.0/PNG/Tiles/tilemap_packed.png" width="288" height="208"/>
</tileset>

View File

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<tileset version="1.10" tiledversion="1.11.2" name="tilemap" tilewidth="16" tileheight="16" tilecount="234" columns="18">
<image source="kenney_desert-shooter-pack_1.0/PNG/Tiles/Tilemap/tilemap_packed.png" width="288" height="208"/>
</tileset>

View File

@ -0,0 +1,23 @@
Tiny Creatures (1.0)
Created by Clint Bellanger (clintbellanger.net)
Creation date: 03-18-2024
------------------------------
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 my work by crediting Clint Bellanger (this is not mandatory)
------------------------------
This set is an expansion of Kenney's Tiny Dungeon and Tiny Town:
https://kenney.nl/assets/tiny-dungeon
https://kenney.nl/assets/tiny-town
The included sample Tiled map and example images contain works by Kenney from these sets.
Made with Kenney's permission

View File

@ -0,0 +1,13 @@
Tilesheet information:
Tile size • 16px × 16px
Space between tiles • 1px × 1px
---
Total tiles (horizontal) • 10 tiles
Total tiles (vertical) • 18 tiles
---
Total tiles in sheet • 180 tiles
Note: I've set these creatures to all face right (except where symmetrical).
You may choose to horizonally-flip these sprites when creatures are facing left.

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -1,10 +1,9 @@
Desert Shooter Pack (1.0)
Tiny Dungeon (1.0)
Created/distributed by Kenney (www.kenney.nl)
Sponsored by: GameMaker (www.gamemaker.io)
Creation date: 24-04-2024
Creation date: 05-07-2022
------------------------------

View File

@ -3,7 +3,7 @@ Tilesheet information:
Tile size • 16px × 16px
Space between tiles • 1px × 1px
---
Total tiles (horizontal) • 18 tiles
Total tiles (horizontal) • 12 tiles
Total tiles (vertical) • 11 tiles
---
Total tiles in sheet • 198 tiles
Total tiles in sheet • 132 tiles

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -230,6 +230,16 @@ pub fn popScissor(self: *Frame) void {
});
}
pub fn drawCircle(self: *Frame, center: Vec2, radius: f32, color: Vec4) void {
self.pushGraphicsCommand(.{
.draw_circle = .{
.center = center,
.radius = radius,
.color = color,
}
});
}
pub fn drawRectangle(self: *Frame, opts: GraphicsCommand.DrawRectangle) void {
self.pushGraphicsCommand(.{ .draw_rectangle = opts });
}

View File

@ -7,6 +7,7 @@ const simgui = sokol.imgui;
const sgl = sokol.gl;
const Math = @import("./math.zig");
const Line = Math.Line;
const Vec2 = Math.Vec2;
const Vec4 = Math.Vec4;
const rgb = Math.rgb;
@ -45,6 +46,15 @@ pub const Command = union(enum) {
sprite: ?Sprite = null,
rotation: f32 = 0,
origin: Vec2 = .init(0, 0),
uv_flip_diagonal: bool = false,
uv_flip_horizontal: bool = false,
uv_flip_vertical: bool = false
};
pub const DrawCircle = struct {
center: Vec2,
radius: f32,
color: Vec4,
};
set_scissor: Rect,
@ -59,6 +69,7 @@ pub const Command = union(enum) {
},
push_transformation: struct { translation: Vec2, scale: Vec2 },
pop_transformation: void,
draw_circle: DrawCircle
};
const Texture = struct {
@ -80,6 +91,23 @@ pub const TextureInfo = Texture.Info;
pub const Sprite = struct {
texture: TextureId,
uv: Rect,
pub fn flipHorizontal(self: Sprite) Sprite {
var uv = self.uv;
uv.pos.x += uv.size.x;
uv.size.x *= -1;
return Sprite{
.texture = self.texture,
.uv = uv
};
}
pub fn getSize(self: Sprite) Vec2 {
const texture_info = getTextureInfo(self.texture);
const texture_size = Vec2.initFromInt(u32, texture_info.width, texture_info.height);
return self.uv.size.multiply(texture_size);
}
};
var gpa: std.mem.Allocator = undefined;
@ -205,6 +233,25 @@ pub fn drawCommand(command: Command) void {
font_context.setColor(color);
font_context.drawText(opts.pos.x * font_resolution_scale.x, opts.pos.y * font_resolution_scale.y, opts.text);
},
.draw_circle => |opts| {
sgl.beginTriangleStrip();
defer sgl.end();
const angle_step: f32 = std.math.pi * 2.0 / 32.0;
sgl.c4f(opts.color.x, opts.color.y, opts.color.z, opts.color.w);
sgl.v2f(opts.center.x, opts.center.y);
var angle: f32 = 0;
while (angle < std.math.pi * 2) {
const point_on_circle = Vec2.initAngle(angle).multiplyScalar(opts.radius).add(opts.center);
sgl.v2f(point_on_circle.x, point_on_circle.y);
sgl.v2f(opts.center.x, opts.center.y);
angle += angle_step;
}
sgl.v2f(opts.center.x + opts.radius, opts.center.y);
}
}
}
@ -306,7 +353,41 @@ fn drawRectangle(opts: Command.DrawRectangle) void {
if (opts.sprite) |sprite| {
const uv = sprite.uv;
const quad = [4]Vertex{ .{ .pos = pos.add(top_left), .uv = .init(uv.left(), uv.top()) }, .{ .pos = pos.add(top_right), .uv = .init(uv.right(), uv.top()) }, .{ .pos = pos.add(bottom_right), .uv = .init(uv.right(), uv.bottom()) }, .{ .pos = pos.add(bottom_left), .uv = .init(uv.left(), uv.bottom()) } };
var uv_top_left = Vec2.init(uv.left(), uv.top());
var uv_top_right = Vec2.init(uv.right(), uv.top());
var uv_bottom_right = Vec2.init(uv.right(), uv.bottom());
var uv_bottom_left = Vec2.init(uv.left(), uv.bottom());
if (opts.uv_flip_diagonal) {
std.mem.swap(Vec2, &uv_bottom_left, &uv_top_right);
}
if (opts.uv_flip_horizontal) {
std.mem.swap(Vec2, &uv_top_left, &uv_top_right);
std.mem.swap(Vec2, &uv_bottom_left, &uv_bottom_right);
}
if (opts.uv_flip_vertical) {
std.mem.swap(Vec2, &uv_top_left, &uv_bottom_left);
std.mem.swap(Vec2, &uv_top_right, &uv_bottom_right);
}
const quad = [4]Vertex{
.{
.pos = pos.add(top_left),
.uv = uv_top_left
},
.{
.pos = pos.add(top_right),
.uv = uv_top_right,
},
.{
.pos = pos.add(bottom_right),
.uv = uv_bottom_right
},
.{
.pos = pos.add(bottom_left),
.uv = uv_bottom_left
}
};
drawQuad(quad, opts.color, sprite.texture);
} else {
const quad = .{ pos.add(top_left), pos.add(top_right), pos.add(bottom_right), pos.add(bottom_left) };
@ -315,14 +396,22 @@ fn drawRectangle(opts: Command.DrawRectangle) void {
}
fn drawLine(from: Vec2, to: Vec2, color: Vec4, width: f32) void {
const step = to.sub(from).normalized().multiplyScalar(width / 2);
const line = Line{
.p0 = from,
.p1 = to,
};
const top_left = from.add(step.rotateLeft90());
const bottom_left = from.add(step.rotateRight90());
const top_right = to.add(step.rotateLeft90());
const bottom_right = to.add(step.rotateRight90());
const quad = line.getQuad(width);
drawQuadNoUVs(.{ top_right, top_left, bottom_left, bottom_right }, color);
drawQuadNoUVs(
.{
quad.top_right,
quad.top_left,
quad.bottom_left,
quad.bottom_right
},
color
);
}
pub fn addFont(name: [*c]const u8, data: []const u8) !Font.Id {
@ -378,3 +467,8 @@ pub fn getTextureInfo(id: TextureId) TextureInfo {
const texture = textures.items[@intFromEnum(id)];
return texture.info;
}
pub fn getTextureSize(id: TextureId) Vec2 {
const texture_info = getTextureInfo(id);
return Vec2.initFromInt(u32, texture_info.width, texture_info.height);
}

View File

@ -368,6 +368,11 @@ pub const Rect = struct {
pos: Vec2,
size: Vec2,
pub const unit = Rect{
.pos = .zero,
.size = .init(1, 1),
};
pub const zero = Rect{
.pos = Vec2.zero,
.size = Vec2.zero
@ -397,6 +402,33 @@ pub const Rect = struct {
);
}
pub fn getTopEdge(self: Rect) Line {
return Line{
.p0 = .init(self.left(), self.top()),
.p1 = .init(self.right(), self.top()),
};
}
pub fn getBottomEdge(self: Rect) Line {
return Line{
.p0 = .init(self.left(), self.bottom()),
.p1 = .init(self.right(), self.bottom()),
};
}
pub fn getLeftEdge(self: Rect) Line {
return Line{
.p0 = .init(self.left(), self.top()),
.p1 = .init(self.left(), self.bottom()),
};
}
pub fn getRightEdge(self: Rect) Line {
return Line{
.p0 = .init(self.right(), self.top()),
.p1 = .init(self.right(), self.bottom()),
};
}
pub fn left(self: Rect) f32 {
return self.pos.x;
}
@ -431,12 +463,57 @@ pub const Rect = struct {
};
}
pub fn isInside(self: Rect, pos: Vec2) bool {
pub fn isPointInside(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 fn checkEdgeLineOverlap(self: Rect, line: Line) bool {
const left_overlap = line.hasOverlap(self.getLeftEdge());
const right_overlap = line.hasOverlap(self.getRightEdge());
const top_overlap = line.hasOverlap(self.getTopEdge());
const bottom_overlap = line.hasOverlap(self.getBottomEdge());
return left_overlap or right_overlap or top_overlap or bottom_overlap;
}
pub fn checkCircleOverlap(self: Rect, circle_center: Vec2, circle_radius: f32) bool {
const cx = circle_center.x;
const cy = circle_center.y;
const rx = self.pos.x;
const ry = self.pos.y;
const rw = self.size.x;
const rh = self.size.y;
// temporary variables to set edges for testing
var testX = cx;
var testY = cy;
// which edge is closest?
if (cx < rx) {
testX = rx; // test left edge
} else if (cx > rx+rw) {
testX = rx+rw; // right edge
}
if (cy < ry) {
testY = ry; // top edge
} else if (cy > ry+rh) {
testY = ry+rh; // bottom edge
}
// get distance from closest edges
const distX = cx-testX;
const distY = cy-testY;
const distance = @sqrt( (distX*distX) + (distY*distY) );
// if the distance is less than the radius, collision!
if (distance <= circle_radius) {
return true;
}
return false;
}
pub fn hasOverlap(lhs: Rect, rhs: Rect) bool {
return (lhs.left() < rhs.right() and lhs.right() > rhs.left()) and
(lhs.top() < rhs.bottom() and lhs.bottom() > rhs.top());
@ -461,9 +538,133 @@ pub const Rect = struct {
}
};
pub const Quad = struct {
top_left: Vec2,
bottom_left: Vec2,
top_right: Vec2,
bottom_right: Vec2,
pub fn isRectOverlap(self: Quad, rect: Rect) bool {
const vertices = [4]Vec2{
self.top_right,
self.top_left,
self.bottom_left,
self.bottom_right,
};
const polygon = Polygon{
.vertices = &vertices
};
return polygon.isRectOverlap(rect);
}
};
pub const Polygon = struct {
vertices: []const Vec2,
pub fn isPointInside(self: Polygon, point: Vec2) bool {
var collision = false;
const py = point.y;
const px = point.x;
for (0..self.vertices.len) |i| {
const next_i = @mod(i + 1, self.vertices.len);
// get the PVectors at our current position
// this makes our if statement a little cleaner
const vc = self.vertices[i]; // c for "current"
const vn = self.vertices[next_i]; // n for "next"
// compare position, flip 'collision' variable
// back and forth
if (
((vc.y > py and vn.y < py) or (vc.y < py and vn.y > py))
and
(px < (vn.x-vc.x)*(py-vc.y) / (vn.y-vc.y)+vc.x)
) {
collision = !collision;
}
}
return collision;
}
pub fn isRectOverlap(self: Polygon, rect: Rect) bool {
// go through each of the vertices, plus the next
// vertex in the list
for (0..self.vertices.len) |i| {
const next_i = @mod(i + 1, self.vertices.len);
// get the PVectors at our current position
// this makes our if statement a little cleaner
const vc = self.vertices[i]; // c for "current"
const vn = self.vertices[next_i]; // n for "next"
const polygon_edge = Line{
.p0 = vc,
.p1 = vn,
};
// check against all four sides of the rectangle
if (rect.checkEdgeLineOverlap(polygon_edge)) {
return true;
}
}
// optional: test if the rectangle is INSIDE the polygon
// note that this iterates all sides of the polygon
// again, so only use this if you need to
if (self.isPointInside(rect.pos)) {
return true;
}
return false;
}
};
pub const Line = struct {
p0: Vec2,
p1: Vec2
p1: Vec2,
pub fn hasOverlap(lhs: Line, rhs: Line) bool {
const x1 = lhs.p0.x;
const y1 = lhs.p0.y;
const x2 = lhs.p1.x;
const y2 = lhs.p1.y;
const x3 = rhs.p0.x;
const y3 = rhs.p0.y;
const x4 = rhs.p1.x;
const y4 = rhs.p1.y;
// calculate the direction of the lines
const uA = ((x4-x3)*(y1-y3) - (y4-y3)*(x1-x3)) / ((y4-y3)*(x2-x1) - (x4-x3)*(y2-y1));
const uB = ((x2-x1)*(y1-y3) - (y2-y1)*(x1-x3)) / ((y4-y3)*(x2-x1) - (x4-x3)*(y2-y1));
// if uA and uB are between 0-1, lines are colliding
if (uA >= 0 and uA <= 1 and uB >= 0 and uB <= 1) {
return true;
}
return false;
}
pub fn getQuad(self: Line, width: f32) Quad {
const to = self.p1;
const from = self.p0;
const step = to.sub(from).normalized().multiplyScalar(width / 2);
const top_left = from.add(step.rotateLeft90());
const bottom_left = from.add(step.rotateRight90());
const top_right = to.add(step.rotateLeft90());
const bottom_right = to.add(step.rotateRight90());
return Quad{
.top_left = top_left,
.bottom_left = bottom_left,
.top_right = top_right,
.bottom_right = bottom_right
};
}
};
pub fn isInsideRect(rect_pos: Vec2, rect_size: Vec2, pos: Vec2) bool {
@ -471,7 +672,7 @@ pub fn isInsideRect(rect_pos: Vec2, rect_size: Vec2, pos: Vec2) bool {
.pos = rect_pos,
.size = rect_size
};
return rect.isInside(pos);
return rect.isPointInside(pos);
}
pub fn rgba(r: u8, g: u8, b: u8, a: f32) Vec4 {

View File

@ -124,14 +124,14 @@ pub fn debug(self: *Game) !void {
}
defer imgui.endWindow();
if (imgui.button("Spawn enemy")) {
try self.combat_screen.spawnEnemy();
}
if (imgui.button("Restart")) {
try self.restartAndShowCombatScreen();
}
if (imgui.button("Toggle colliders")) {
self.combat_screen.show_colliders = !self.combat_screen.show_colliders;
}
if (self.show_shop) {
if (imgui.button("Swap to combat")) {
self.show_shop = false;
@ -144,10 +144,7 @@ pub fn debug(self: *Game) !void {
const screen = &self.combat_screen;
const time_left_til_pickup = screen.next_pickup_spawn_at - screen.wave_timer;
imgui.textFmt("Waves: {}\n", .{screen.waves.items.len});
imgui.textFmt("Bullets: {}\n", .{screen.bullets.items.len});
imgui.textFmt("Enemies: {}\n", .{screen.enemies.items.len});
imgui.textFmt("Time until next pickup: {d:.2}s\n", .{@as(f32, @floatFromInt(time_left_til_pickup)) / std.time.ns_per_s});
imgui.textFmt("Enemies: {}\n", .{screen.enemies.count});
}

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);
}

View File

@ -134,7 +134,7 @@ pub fn tick(self: *ShopScreen, state: *State, frame: *Frame) !TickResult {
for (self.nodes.items) |*node| {
const node_rect = Rect.initCentered(node.pos.x, node.pos.y, node_size.x, node_size.y);
const is_mouse_inside = mouse != null and node_rect.isInside(mouse.?);
const is_mouse_inside = mouse != null and node_rect.isPointInside(mouse.?);
const has_enough_money = state.money >= node.money_cost;
if (has_enough_money and is_mouse_inside and frame.isMousePressed(.left)) {
@ -173,7 +173,7 @@ pub fn tick(self: *ShopScreen, state: *State, frame: *Frame) !TickResult {
.color = rgb(255, 255, 255)
});
if (frame.input.mouse_position != null and back_rect.isInside(frame.input.mouse_position.?) and frame.isMousePressed(.left)) {
if (frame.input.mouse_position != null and back_rect.isPointInside(frame.input.mouse_position.?) and frame.isMousePressed(.left)) {
result.back_to_combat = true;
}