Compare commits

..

88 Commits

Author SHA1 Message Date
75890b1a71 /apps/tools/twndel: thingies 2025-03-15 07:59:55 +03:00
73db3e57dc various small tweaks 2025-03-13 03:08:35 +03:00
2975aa2dfb comparing with whole number floats is okay. 2025-03-13 02:54:40 +03:00
6726faf719 twn_textures: lock cache when adding requests 2025-03-13 01:18:07 +03:00
183dfa6be5 twn_lines: combine differently colored lines into same batch 2025-03-12 12:00:53 +03:00
e974194af0 /docs/wiki: G1. Trigonometry starter 2025-03-12 01:36:42 +03:00
8607aa48ec /apps/tools/twndel: face hovering 2025-03-11 07:40:18 +03:00
f6600dfbda norm is needed after all. 2025-03-11 07:09:55 +03:00
bdabd04388 /apps/tools/twndel: reversable triangulation, fix to point finding with triangles 2025-03-11 07:02:52 +03:00
0e075ec334 /apps/tools/twndel: texture painting, face selection 2025-03-11 04:58:44 +03:00
b256fc903a /apps/tools/twndel: triangluation, triangle render 2025-03-11 02:41:03 +03:00
b52ecaeaa0 remove logging 2025-03-11 01:54:36 +03:00
37e46e9a7e /apps/tools/twndel: initial texture projection work 2025-03-11 01:53:45 +03:00
a472e6af52 /apps/demos/scenery: simpler normal_at(), clean one bit 2025-03-11 01:28:08 +03:00
66b2f04d9d fix missing texture sizing, fix bordered repeated texture upload, make quads default to repeating for now 2025-03-10 22:10:53 +03:00
90f4097070 /apps/templates/zig: depend on /src/*.zig sources for rebuilding, add .gitignore 2025-03-10 10:11:10 +03:00
829ff4780c zig template 2025-03-10 09:58:28 +03:00
8e15c9ec3c twn_util: dont add empty list 2025-03-10 07:05:45 +03:00
474ea84a77 /apps/tools/twndel: some data 2025-03-10 06:54:23 +03:00
7b8b9416ba twn_util: file_read() :images 2025-03-10 06:54:10 +03:00
8ed8158ae6 twn_util: file_read() with :exists 2025-03-10 05:34:29 +03:00
48e3a4c233 /share/twn_api.json: add new logging funcs 2025-03-10 05:29:56 +03:00
56530f9864 twn_util: final cleaning up, introducton of powerful file_read() 2025-03-10 05:19:58 +03:00
f86f3dd41a /apps/tools/twndel: use roundf for snapping 2025-03-09 08:12:29 +03:00
adae6be7e5 /apps/tools/twndel: camera translation 2025-03-09 07:38:32 +03:00
cd3033f9c4 /apps/tools/twndel: add reference lines 2025-03-09 06:53:54 +03:00
e11e63f273 /apps/tools/twndel: more plane awareness 2025-03-09 06:24:05 +03:00
75737b738f /apps/tools/twndel: plane intersection based point move 2025-03-09 00:30:20 +03:00
ce2c2513aa /apps/tool/twndel: grab.png 2025-03-08 21:57:53 +03:00
36c0af9953 hopefully more portable way of packaging binary embeds 2025-03-08 18:11:39 +03:00
826622cd58 twn_draw: proper ortho unproject 2025-03-08 04:38:56 +03:00
78b6a26de9 twndel: fix y axis point move 2025-03-08 04:10:14 +03:00
5f7b8bac6d undo, axis editing 2025-03-08 02:20:31 +03:00
6d6230c6a1 twn_audio: fix ogg vorbis sample reuse 2025-03-08 01:38:38 +03:00
c07e16490e model tool progress: initial selection 2025-03-08 00:50:47 +03:00
f5e55bb997 /apps/tools/twndel: point selection, sounds and other assets 2025-03-07 20:56:19 +03:00
1e6e323fe1 twn_draw: remove logging 2025-03-07 20:21:36 +03:00
dbf9599fe5 twn_api.json: add draw_camera_unproject() 2025-03-07 19:40:56 +03:00
923cd81571 twn_draw: draw_camera_unproject() 2025-03-07 19:31:46 +03:00
733a1786ab twn_draw: multiply zoom by 0.1 to match projections 2025-03-07 11:16:31 +03:00
a03e1d885d twn_camera: fix aspect of ortho projection 2025-03-07 11:07:05 +03:00
67feb5974a /apps/tools/twndel: start the thing 2025-03-07 07:10:44 +03:00
5be4ed4645 twn_lines: bail on empty batch 2025-03-07 07:05:20 +03:00
4a41f47a58 twn_textures: fix freeing of missed texture 2025-03-07 06:19:44 +03:00
35bb26705a twn_text: use full ascii range 2025-03-07 05:16:46 +03:00
13bc71a28d twn_text: embed default font 2025-03-07 03:35:35 +03:00
b97a155de4 /bin: make utilities quit on error 2025-03-07 02:56:12 +03:00
5df80addeb /apps/demos/scenery: no drifting :( 2025-03-07 00:51:07 +03:00
787977b747 /apps/demos/scenery: drift ! steer ! 2025-03-05 05:26:05 +03:00
f90b973d86 /apps/demos/scenery: more than almost 2025-03-04 09:45:49 +03:00
32675c012c /apps/demos/scenery: ...almost...🚬 2025-03-04 03:30:01 +03:00
a97515e948 twn_textures.c: fix atlas packing, allow out of order population 2025-03-03 00:57:19 +03:00
ed8e826b94 /apps/demos/scenery: something... 2025-03-02 23:19:27 +03:00
4e5ff9433c /apps/demos/scenery: box is almost coherent... 2025-03-02 03:37:02 +03:00
55829a1bef /apps/demos/scenery: fix world origin relation 2025-03-02 01:04:09 +03:00
119bd52c51 /apps/demos/scenery: some friction 2025-03-02 00:02:32 +03:00
5abd1ced1c /apps/demos/scenery: vehicle... 2025-03-01 16:42:14 +03:00
80db96672d stb_ds.h: fix implicit casts to int, resulting in bogus bitshift 2025-03-01 12:15:41 +03:00
2f6f7852be twn_api.json: add draw_line_3d() 2025-03-01 03:59:55 +03:00
307d5552f6 twn_lines.c: 3d case 2025-03-01 03:46:11 +03:00
5911cbd980 take care of warnings 2025-03-01 01:06:32 +03:00
e47b761a2c twn_lines.c: introduction with proper impl 2025-02-28 23:50:12 +03:00
844283c2fb /apps/examples/cirlce-raster: update 2025-02-28 17:35:58 +03:00
09eac707c3 draw: use GLint in circle element buffer 2025-02-28 17:27:01 +03:00
5e89710458 rename twn_engine_api.h to twn_api.h 2025-02-28 16:42:33 +03:00
4bc1feb826 /apps/demos/scenery: fix ramp bug, increase gravity 2025-02-26 23:19:22 +03:00
1c3973c6a2 /apps/demos/scenery: narrower cull, new assets used 2025-02-26 19:57:38 +03:00
da5bdb4fae /apps/demos/scenery: increase walking speed a bit 2025-02-26 17:22:02 +03:00
ed2afec5a7 /apps/demos/scenery: culling 2025-02-26 17:08:45 +03:00
6812c7c13d add trees to scenery, disable mipmapping by default, increase index buffer size again 2025-02-26 16:17:44 +03:00
8c0f43ec34 draw: draw_distance for 3d spaces, proper positioning of skybox according to it, scenery demo on circle rasters 2025-02-26 15:53:59 +03:00
23fbd45564 increase quad element buffer size 2025-02-26 13:29:28 +03:00
a36459397e draw: increase far Z, separate path for space quads, fix billboard batching 2025-02-26 13:27:09 +03:00
5f3920fdba /apps/demos/scenery: use quads 2025-02-26 11:28:59 +03:00
f57525cea6 /apps/demos/scenery: remove title scene 2025-02-26 11:26:38 +03:00
6b2901be28 /apps/demos/crawl: cleanup, document 2025-02-25 22:20:35 +03:00
9f0d15b9f6 /apps/twnlua: add ---@meta annotation 2025-02-23 22:07:32 +03:00
b46331e08d no c brain 2025-02-23 17:34:00 +03:00
d2938da8e2 /apps/demos/crawl: dont scale by 2 2025-02-23 17:28:45 +03:00
9134e51817 /apps/demos/crawl: add LICENSES 2025-02-23 17:19:33 +03:00
d66eda1894 add texture border to atlas residents, actually make use of mipmapping 2025-02-23 17:14:05 +03:00
a88392b9e9 /docs/wiki: T1.4 Developing 2025-02-22 21:21:42 +03:00
05f85062e8 fix command lists 2025-02-22 20:50:38 +03:00
d5aec5e6e1 /bin/twn: devcompl command to generate clangd completions in root 2025-02-22 20:29:28 +03:00
62866d33ae add notes explaining emscripten considerations 2025-02-22 16:19:22 +03:00
ce7240d423 dont generate source maps for web as it crashes lua build 2025-02-22 01:39:04 +03:00
7a38f7bcf3 /bin/twnbuild: fix cmake cache for web target 2025-02-22 01:32:55 +03:00
affaf7f557 cleanup templates 2025-02-22 01:14:20 +03:00
130 changed files with 3483 additions and 678 deletions

1
.gitignore vendored
View File

@ -12,6 +12,7 @@
**/*.js **/*.js
**/*.wasm **/*.wasm
**/*.wasm.map **/*.wasm.map
**/*.data
**/*.so **/*.so
**/*.dll **/*.dll
**/*.7z **/*.7z

View File

@ -106,6 +106,7 @@ set(TWN_NONOPT_SOURCE_FILES
src/rendering/twn_circles.c src/rendering/twn_circles.c
src/rendering/twn_skybox.c src/rendering/twn_skybox.c
src/rendering/twn_models.c src/rendering/twn_models.c
src/rendering/twn_lines.c
) )
set(TWN_SOURCE_FILES set(TWN_SOURCE_FILES
@ -167,8 +168,7 @@ function(give_options_without_warnings target)
-g3 -g3
-gdwarf -gdwarf
-fno-omit-frame-pointer -fno-omit-frame-pointer
$<$<BOOL:${TWN_SANITIZE}>:-fstack-protector-all -fsanitize=undefined -fsanitize=address> $<$<BOOL:${TWN_SANITIZE}>:-fstack-protector-all -fsanitize=undefined -fsanitize=address>)
$<$<BOOL:${EMSCRIPTEN}>:-gsource-map>)
set(LINK_FLAGS set(LINK_FLAGS
-Bsymbolic-functions -Bsymbolic-functions
@ -287,6 +287,24 @@ function(link_deps target)
endfunction() endfunction()
function(put_townengine output_directory)
cmake_path(GET TWN_OUT_DIR STEM LAST_ONLY target)
add_executable(${target} ${TWN_ROOT_DIR}/src/twn_main.c)
target_link_options(${target} PRIVATE $<$<BOOL:${LINUX}>:-Wl,-rpath,$ORIGIN/>)
set_target_properties(${TWN_TARGET} PROPERTIES
LIBRARY_OUTPUT_DIRECTORY ${output_directory})
set_target_properties(${target} PROPERTIES
OUTPUT_NAME ${target}
LIBRARY_OUTPUT_DIRECTORY ${output_directory}
RUNTIME_OUTPUT_DIRECTORY ${output_directory})
give_options(${target})
include_deps(${target})
link_deps(${target})
target_link_libraries(${target} PUBLIC ${TWN_TARGET})
endfunction()
function(use_townengine sources output_directory) function(use_townengine sources output_directory)
cmake_path(GET TWN_OUT_DIR STEM LAST_ONLY target) cmake_path(GET TWN_OUT_DIR STEM LAST_ONLY target)
@ -354,5 +372,18 @@ link_deps(twn_third_parties)
give_options(${TWN_TARGET}) give_options(${TWN_TARGET})
include_deps(${TWN_TARGET}) include_deps(${TWN_TARGET})
link_deps(${TWN_TARGET}) link_deps(${TWN_TARGET})
target_link_libraries(${TWN_TARGET} PUBLIC twn_third_parties)
target_include_directories(${TWN_TARGET} PRIVATE ${TWN_ROOT_DIR}/src) target_include_directories(${TWN_TARGET} PRIVATE ${TWN_ROOT_DIR}/src)
target_link_libraries(${TWN_TARGET} PUBLIC
twn_third_parties
${CMAKE_CURRENT_BINARY_DIR}/font.o)
# embed resources
# TODO: think of a portable way to compress/decompress them
add_custom_command(
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/font.o
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
COMMAND ${CMAKE_COMMAND} -E env CMAKE_CURRENT_BINARY_DIR=${CMAKE_CURRENT_BINARY_DIR} sh bin/prep-embed.sh
DEPENDS share/assets/Dernyns256.ttf)
add_custom_target(asset-compilation ALL DEPENDS
${CMAKE_CURRENT_BINARY_DIR}/font.o)

View File

@ -1,3 +1,3 @@
[[deps]] [[deps]]
source = "../../../common-data" source = "../../../data"
name = "common-data" name = "common-data"

View File

@ -0,0 +1 @@
castledoors.png - https://opengameart.org/content/castle-door - CC-BY 3.0

View File

@ -1,4 +1,3 @@
require("string")
require("level") require("level")
require("render") require("render")
@ -29,19 +28,23 @@ function game_tick()
input_action { control = "S", name = "walk_backward" } input_action { control = "S", name = "walk_backward" }
if input_action_just_released { name = "turn_left" } then if input_action_just_released { name = "turn_left" } then
ctx.udata.player.direction = { x = ctx.udata.player.direction.z, y = ctx.udata.player.direction.y, z = -ctx.udata.player.direction.x } ctx.udata.player.direction = { x = ctx.udata.player.direction.z,
y = ctx.udata.player.direction.y,
z =-ctx.udata.player.direction.x }
end end
if input_action_just_released { name = "turn_right" } then if input_action_just_released { name = "turn_right" } then
ctx.udata.player.direction = { x = -ctx.udata.player.direction.z, y = ctx.udata.player.direction.y, z = ctx.udata.player.direction.x } ctx.udata.player.direction = { x =-ctx.udata.player.direction.z,
y = ctx.udata.player.direction.y,
z = ctx.udata.player.direction.x }
end end
local move = { x = 0, y = 0 } local move = { x = 0, y = 0 }
if input_action_just_released { name = "walk_forward" } then if input_action_just_released { name = "walk_forward" } then
move = { x = move.x + ctx.udata.player.direction.z, y = move.y + ctx.udata.player.direction.x } move = { x = move.x + ctx.udata.player.direction.x, y = move.y + ctx.udata.player.direction.z }
end end
if input_action_just_released { name = "walk_backward" } then if input_action_just_released { name = "walk_backward" } then
move = { x = move.x - ctx.udata.player.direction.z, y = move.y - ctx.udata.player.direction.x } move = { x = move.x - ctx.udata.player.direction.x, y = move.y - ctx.udata.player.direction.z }
end end
if ctx.udata.level.grid[ctx.udata.player.position.y + move.y][ctx.udata.player.position.x + move.x].solid ~= nil then if ctx.udata.level.grid[ctx.udata.player.position.y + move.y][ctx.udata.player.position.x + move.x].solid ~= nil then
@ -57,9 +60,9 @@ function game_tick()
draw_camera { draw_camera {
position = { position = {
x = ctx.udata.player.position_lerp.y * 2 + 1 - ctx.udata.player.direction.x / 2, x = ctx.udata.player.position_lerp.x + 0.5 - ctx.udata.player.direction.x / 2,
y = 1, y = 0.5,
z = ctx.udata.player.position_lerp.x * 2 + 1 - ctx.udata.player.direction.z / 2, z = ctx.udata.player.position_lerp.y + 0.5 - ctx.udata.player.direction.z / 2,
}, },
direction = ctx.udata.player.direction_lerp, direction = ctx.udata.player.direction_lerp,
} }

View File

@ -3,14 +3,20 @@ function load_level(file)
local f = file_read { file = file } local f = file_read { file = file }
local result = { local result = {
-- templates to fill the grid with
classes = { classes = {
-- predefined empty tile
void = { }, void = { },
}, },
-- symbol to class lookup table
glossary = { glossary = {
[" "] = "void", [" "] = "void",
}, },
-- grid consists of expanded classes, of size dimensions
grid = {}, grid = {},
-- map consists of original rows of symbols that the grid is constructed from
map = {}, map = {},
-- maximum extends of the map, unspecified tiles are filled with "void"
size = { x = 0, y = 0 }, size = { x = 0, y = 0 },
} }
@ -28,14 +34,10 @@ function load_level(file)
section = line:sub(2); subsection = "none" section = line:sub(2); subsection = "none"
-- decode map one line at a time -- decode map one line at a time
elseif section == "map" then elseif section == "map" then
local l = #result.map + 1
if result.size.x < #line then if result.size.x < #line then
result.size.x = #line result.size.x = #line
end end
result.map[l] = {} result.map[#result.map + 1] = line
for i = 1, #line do
result.map[l][i] = line:sub(i,i)
end
-- templates to expand -- templates to expand
elseif section == "classes" then elseif section == "classes" then
-- properties -- properties
@ -61,14 +63,16 @@ function load_level(file)
from = limit + 1 from = limit + 1
start, limit = string.find(f, "\n", from) start, limit = string.find(f, "\n", from)
end end
-- post process -- post process, expand map to grid
for y = 1, #result.map do for y = 1, #result.map do
result.grid[y] = {} result.grid[y] = {}
for x = 1, result.size.x do for x = 1, result.size.x do
-- past defined for line -- past defined for line
local symbol local symbol
if x > #result.map[y] then symbol = " " if x > #result.map[y] then
else symbol = result.map[y][x] symbol = " "
else
symbol = result.map[y]:sub(x,x)
end end
local class = result.classes[result.glossary[symbol]] local class = result.classes[result.glossary[symbol]]
if class["unique"] ~= nil then if class["unique"] ~= nil then

View File

@ -1,56 +1,57 @@
-- if this is too wasteful, one could check nerby tiles to see whether faces could be visible
-- more robust solution would be to travel the level from observer point of view
function render_dungeon(dungeon) function render_dungeon(dungeon)
for y = 1, dungeon.size.y do for y = 1, dungeon.size.y do
for x = 1, dungeon.size.x do for x = 1, dungeon.size.x do
if dungeon.grid[y][x].wall_texture ~= nil then if dungeon.grid[y][x].wall_texture ~= nil then
draw_quad { draw_quad {
texture = dungeon.grid[y][x].wall_texture, texture = dungeon.grid[y][x].wall_texture,
v3 = { x = y * 2, y = 2, z = x * 2 }, v3 = { x = x, y = 1, z = y },
v2 = { x = y * 2, y = 0, z = x * 2 }, v2 = { x = x, y = 0, z = y },
v1 = { x = y * 2 + 2, y = 0, z = x * 2 }, v1 = { x = x + 1, y = 0, z = y },
v0 = { x = y * 2 + 2, y = 2, z = x * 2 }, v0 = { x = x + 1, y = 1, z = y },
texture_region = { w = 128, h = 128 }, texture_region = { w = 128, h = 128 },
} }
draw_quad { draw_quad {
texture = dungeon.grid[y][x].wall_texture, texture = dungeon.grid[y][x].wall_texture,
v3 = { x = y * 2 + 2, y = 2, z = x * 2 }, v3 = { x = x + 1, y = 1, z = y },
v2 = { x = y * 2 + 2, y = 0, z = x * 2 }, v2 = { x = x + 1, y = 0, z = y },
v1 = { x = y * 2 + 2, y = 0, z = x * 2 + 2 }, v1 = { x = x + 1, y = 0, z = y + 1 },
v0 = { x = y * 2 + 2, y = 2, z = x * 2 + 2 }, v0 = { x = x + 1, y = 1, z = y + 1 },
texture_region = { w = 128, h = 128 }, texture_region = { w = 128, h = 128 },
} }
draw_quad { draw_quad {
texture = dungeon.grid[y][x].wall_texture, texture = dungeon.grid[y][x].wall_texture,
v3 = { x = y * 2 + 2, y = 2, z = x * 2 + 2 }, v3 = { x = x + 1, y = 1, z = y + 1 },
v2 = { x = y * 2 + 2, y = 0, z = x * 2 + 2 }, v2 = { x = x + 1, y = 0, z = y + 1 },
v1 = { x = y * 2, y = 0, z = x * 2 + 2 }, v1 = { x = x, y = 0, z = y + 1 },
v0 = { x = y * 2, y = 2, z = x * 2 + 2 }, v0 = { x = x, y = 1, z = y + 1 },
texture_region = { w = 128, h = 128 }, texture_region = { w = 128, h = 128 },
} }
draw_quad { draw_quad {
texture = dungeon.grid[y][x].wall_texture, texture = dungeon.grid[y][x].wall_texture,
v3 = { x = y * 2, y = 2, z = x * 2 + 2 }, v3 = { x = x, y = 1, z = y + 1 },
v2 = { x = y * 2, y = 0, z = x * 2 + 2 }, v2 = { x = x, y = 0, z = y + 1 },
v1 = { x = y * 2, y = 0, z = x * 2 }, v1 = { x = x, y = 0, z = y },
v0 = { x = y * 2, y = 2, z = x * 2 }, v0 = { x = x, y = 1, z = y },
texture_region = { w = 128, h = 128 }, texture_region = { w = 128, h = 128 },
} }
elseif dungeon.grid[y][x].tile_texture ~= nil then elseif dungeon.grid[y][x].tile_texture ~= nil then
draw_quad { draw_quad {
texture = dungeon.grid[y][x].tile_texture, texture = dungeon.grid[y][x].tile_texture,
v0 = { x = y * 2 + 2, y = 0, z = x * 2 }, v0 = { x = x + 1, y = 0, z = y },
v1 = { x = y * 2, y = 0, z = x * 2 }, v1 = { x = x, y = 0, z = y },
v2 = { x = y * 2, y = 0, z = x * 2 + 2 }, v2 = { x = x, y = 0, z = y + 1 },
v3 = { x = y * 2 + 2, y = 0, z = x * 2 + 2}, v3 = { x = x + 1, y = 0, z = y + 1},
texture_region = { w = 128, h = 128 }, texture_region = { w = 128, h = 128 },
} }
draw_quad { draw_quad {
texture = dungeon.grid[y][x].tile_texture, texture = dungeon.grid[y][x].tile_texture,
v3 = { x = y * 2 + 2, y = 2, z = x * 2 }, v3 = { x = x + 1, y = 1, z = y },
v2 = { x = y * 2, y = 2, z = x * 2 }, v2 = { x = x, y = 1, z = y },
v1 = { x = y * 2, y = 2, z = x * 2 + 2 }, v1 = { x = x, y = 1, z = y + 1 },
v0 = { x = y * 2 + 2, y = 2, z = x * 2 + 2}, v0 = { x = x + 1, y = 1, z = y + 1},
texture_region = { w = 128, h = 128 }, texture_region = { w = 128, h = 128 },
} }
end end
@ -59,25 +60,26 @@ function render_dungeon(dungeon)
if dungeon.grid[y][x].face == "horizon" then if dungeon.grid[y][x].face == "horizon" then
draw_quad { draw_quad {
texture = dungeon.grid[y][x].face_texture, texture = dungeon.grid[y][x].face_texture,
v3 = { x = y * 2, y = 2, z = x * 2 + 1 }, v3 = { x = x + 1, y = 1, z = y },
v2 = { x = y * 2, y = 0, z = x * 2 + 1 }, v2 = { x = x + 1, y = 0, z = y },
v1 = { x = y * 2 + 2, y = 0, z = x * 2 + 1 }, v1 = { x = x + 1, y = 0, z = y + 1 },
v0 = { x = y * 2 + 2, y = 2, z = x * 2 + 1 }, v0 = { x = x + 1, y = 1, z = y + 1 },
texture_region = { w = 64, h = 96 }, texture_region = { w = 64, h = 64 },
} }
draw_quad { draw_quad {
texture = dungeon.grid[y][x].face_texture, texture = dungeon.grid[y][x].face_texture,
v3 = { x = y * 2 + 2, y = 2, z = x * 2 + 1 }, v3 = { x = x, y = 1, z = y + 1 },
v2 = { x = y * 2 + 2, y = 0, z = x * 2 + 1 }, v2 = { x = x, y = 0, z = y + 1 },
v1 = { x = y * 2, y = 0, z = x * 2 + 1 }, v1 = { x = x, y = 0, z = y },
v0 = { x = y * 2, y = 2, z = x * 2 + 1 }, v0 = { x = x, y = 1, z = y },
texture_region = { w = 64, h = 96 }, texture_region = { w = 64, h = 64 },
} }
elseif dungeon.grid[y][x].face == "observer" then elseif dungeon.grid[y][x].face == "observer" then
draw_billboard { draw_billboard {
texture = dungeon.grid[y][x].face_texture, texture = dungeon.grid[y][x].face_texture,
position = { x = y * 2 + 1, y = 1, z = x * 2 + 1 }, position = { x = x + 0.5, y = 0.5, z = y + 0.5 },
size = { x = 1, y = 1 }, size = { x = 0.5, y = 0.5 },
} }
end end
end end

View File

@ -1,3 +1,3 @@
[[deps]] [[deps]]
source = "../../../common-data" # where does it come from, might be an url source = "../../../data" # where does it come from, might be an url
name = "common-data" # should be globally unique name = "common-data" # should be globally unique

View File

@ -29,10 +29,6 @@ void game_tick(void) {
ctx.debug = !ctx.debug; ctx.debug = !ctx.debug;
} }
if (input_action_just_pressed("debug_dump_atlases")) {
textures_dump_atlases();
}
state->scene->tick(state); state->scene->tick(state);
/* there's a scene switch pending, we can do it now that the tick is done */ /* there's a scene switch pending, we can do it now that the tick is done */

View File

@ -13,7 +13,6 @@ set(SOURCE_FILES
state.h state.h
scenes/scene.c scenes/scene.h scenes/scene.c scenes/scene.h
scenes/title.c scenes/title.h
scenes/ingame.c scenes/ingame.h scenes/ingame.c scenes/ingame.h
) )

View File

@ -1,3 +1,3 @@
[[deps]] [[deps]]
source = "../../../common-data" # where does it come from, might be an url source = "../../../data" # where does it come from, might be an url
name = "common-data" # should be globally unique name = "common-data" # should be globally unique

View File

@ -1,6 +1,5 @@
#include "state.h" #include "state.h"
#include "scenes/scene.h" #include "scenes/scene.h"
#include "scenes/title.h"
#include "scenes/ingame.h" #include "scenes/ingame.h"
#include "twn_game_api.h" #include "twn_game_api.h"
@ -18,7 +17,7 @@ void game_tick(void) {
State *state = ctx.udata; State *state = ctx.udata;
state->ctx = &ctx; state->ctx = &ctx;
state->scene = title_scene(state); state->scene = ingame_scene(state);
} }
} }
@ -31,10 +30,6 @@ void game_tick(void) {
ctx.debug = !ctx.debug; ctx.debug = !ctx.debug;
} }
if (input_action_just_pressed("debug_dump_atlases")) {
textures_dump_atlases();
}
state->scene->tick(state); state->scene->tick(state);
/* there's a scene switch pending, we can do it now that the tick is done */ /* there's a scene switch pending, we can do it now that the tick is done */

View File

@ -1,5 +1,4 @@
#include "ingame.h" #include "ingame.h"
#include "title.h"
#include "scene.h" #include "scene.h"
#include "twn_game_api.h" #include "twn_game_api.h"
@ -13,21 +12,298 @@
#define TERRAIN_FREQUENCY 0.15f #define TERRAIN_FREQUENCY 0.15f
#define TERRAIN_DISTANCE 64 #define TERRAIN_RADIUS 128
#define GRASS_RADIUS 16
#define TERRAIN_DISTANCE (TERRAIN_RADIUS * 2)
#define HALF_TERRAIN_DISTANCE ((float)TERRAIN_DISTANCE / 2) #define HALF_TERRAIN_DISTANCE ((float)TERRAIN_DISTANCE / 2)
#define PLAYER_HEIGHT 0.6f #define PLAYER_HEIGHT 0.6f
#define TREE_DENSITY 0.03f
#define G_CONST 10.0f
/* TODO: pregenerate grid of levels of detail */
static float heightmap[TERRAIN_DISTANCE][TERRAIN_DISTANCE]; static float heightmap[TERRAIN_DISTANCE][TERRAIN_DISTANCE];
/* vehicle sim ! */
/* https://www.youtube.com/watch?v=pwbwFdWBkU0 */
/* representation is a "jelly" box with spring configuration trying to make it coherent */
/* == springs == */
/* damped spring: F = -kx - cv */
/* x = length(p1-p0) - at_rest_length */
/* v = dot(v1 - v0, unit(p1-p0)) */
/* F = (-kx - cv) * unit(p1-p0) */
/* v += (F/m)*t */
/* one points gains positive F, other negative F, to come together */
/* == ground interaction == */
/* if point is under terrain, then apply this: */
/* x = y difference on point and ground */
/* -x(n) = x * normal */
/* -v(n) = dot(v, normal) */
/* -F = (-kx(n)-cv(n)) * normal */
/* == friction == */
/* v(o)/F(o) are perpendicular to slope (x - x(n)) */
/* if v(o) == 0, then */
/* -- at rest, static friction overcomes */
/* if F(o) <= f(s)*x(n), F(o) = 0 */
/* else, F(o) -= f(k) * x(n) */
/* else if length(v(o) + (F(o)/m)*t) <= (f(k)*x(n)*t), v(o) = 0 */
/* else, F = -unit(v(o)*f(k)*x(n)) */
#define VEHICLE_MASS 200.0f
#define VEHICLE_LENGTH 3.0f
#define VEHICLE_WIDTH 1.7f
#define VEHICLE_HEIGHT 1.3f
/* spring constant */
#define VEHICLE_SPRING_K 22000.0f
#define VEHICLE_SPRING_K_SHOCK 18000.0f
#define VEHICLE_SPRING_GK 70000.0f
/* damping constant */
#define VEHICLE_SPRING_C 800.0f
#define VEHICLE_SPRING_C_SHOCK 500.0f
#define VEHICLE_SPRING_GC 100.0f
#define VEHICLE_FRICTION_S 1000.0f
#define VEHICLE_FRICTION_K 110.0f
#define VEHICLE_FRICTION_V 4000.0f
/* TODO: shock springs, that are more loose, which are used to simulate the wheels */
/* initial, ideal corner positions */
static const Vec3 vbpi[8] = {
[0] = { 0, 0, 0 },
[1] = { VEHICLE_LENGTH, 0, 0 },
[2] = { VEHICLE_LENGTH, 0, VEHICLE_WIDTH },
[3] = { 0, 0, VEHICLE_WIDTH },
[4] = { 0, VEHICLE_HEIGHT, 0 },
[5] = { VEHICLE_LENGTH, VEHICLE_HEIGHT, 0 },
[6] = { VEHICLE_LENGTH, VEHICLE_HEIGHT, VEHICLE_WIDTH },
[7] = { 0, VEHICLE_HEIGHT, VEHICLE_WIDTH },
};
/* corner positions in simulation */
static Vec3 vbp[8] = { vbpi[0], vbpi[1], vbpi[2], vbpi[3],
vbpi[4], vbpi[5], vbpi[6], vbpi[7], };
/* corner velocities */
static Vec3 vbv[8];
/* springs */
static uint8_t vbs[28][2] = {
{0, 1}, {4, 5}, {0, 4},
{1, 2}, {5, 6}, {1, 5},
{2, 3}, {6, 7}, {2, 6},
{3, 0}, {7, 4}, {3, 7},
{0, 2}, {0, 5}, {0, 7},
{1, 3}, {1, 6}, {1, 4},
{4, 6}, {2, 7}, {2, 5},
{5, 7}, {3, 4}, {3, 6},
{0, 6}, {1, 7}, {2, 4}, {3, 5},
};
/* ackermann steering geometry */
static float vehicle_turning_extend;
static float vehicle_turning_speed = 0.12f;
static float vehicle_turning_extend_limit = VEHICLE_WIDTH * 1.75f;
static float height_at(SceneIngame *scn, Vec2 position);
static Vec3 normal_at(SceneIngame *scn, Vec2 position);
static inline float clampf(float f, float min, float max) {
const float t = f < min ? min : f;
return t > max ? max : t;
}
static void draw_vehicle(SceneIngame *scn) {
for (size_t i = 0; i < 12; ++i)
draw_line_3d(vbp[vbs[i][0]], vbp[vbs[i][1]], 1, (Color){255, 255, 255, 255});
for (size_t i = 12; i < 24; ++i)
draw_line_3d(vbp[vbs[i][0]], vbp[vbs[i][1]], 1, (Color){200, 200, 200, 255});
for (size_t i = 24; i < 28; ++i)
draw_line_3d(vbp[vbs[i][0]], vbp[vbs[i][1]], 1, (Color){255, 125, 125, 255});
}
static void process_vehicle(SceneIngame *scn) {
/* apply gravity */
Vec3 Facc[8] = {0};
/* steering */
bool steered = false;
if (input_action_pressed("player_left")) {
vehicle_turning_extend -= vehicle_turning_speed;
steered = true;
}
if (input_action_pressed("player_right")) {
vehicle_turning_extend += vehicle_turning_speed;
steered = true;
}
if (!steered)
vehicle_turning_extend -= copysignf(vehicle_turning_speed * 0.9f, vehicle_turning_extend);
vehicle_turning_extend = clampf(vehicle_turning_extend, -vehicle_turning_extend_limit, vehicle_turning_extend_limit);
if (fabsf(vehicle_turning_extend) <= 0.11f)
vehicle_turning_extend = 0;
Vec3 const Fg = { .y = -VEHICLE_MASS * G_CONST };
for (size_t i = 0; i < 8; ++i)
Facc[i] = vec3_add(Facc[i], Fg);
/* apply springs */
for (size_t i = 0; i < 28; ++i) {
Vec3 const p0 = vbp[vbs[i][0]];
Vec3 const p1 = vbp[vbs[i][1]];
Vec3 const v0 = vbv[vbs[i][0]];
Vec3 const v1 = vbv[vbs[i][1]];
Vec3 const pd = vec3_sub(p1, p0);
Vec3 const pn = vec3_norm(pd);
/* TODO: length at rest could be precalculated */
float const lar = vec3_length(vec3_sub(vbpi[vbs[i][1]], vbpi[vbs[i][0]]));
float const x = vec3_length(pd) - lar;
float const v = vec3_dot(vec3_sub(v1, v0), pn);
float const spring_k = i == 2 | i == 5 || i == 8 || i == 11 ? VEHICLE_SPRING_K_SHOCK : VEHICLE_SPRING_K;
float const spring_c = i == 2 | i == 5 || i == 8 || i == 11 ? VEHICLE_SPRING_C_SHOCK : VEHICLE_SPRING_C;
Vec3 const Fs = vec3_scale(pn, -spring_k * x - spring_c * v);
Facc[vbs[i][0]] = vec3_sub(Facc[vbs[i][0]], Fs);
Facc[vbs[i][1]] = vec3_add(Facc[vbs[i][1]], Fs);
}
/* spring and friction against the ground */
for (size_t i = 0; i < 8; ++i) {
Vec3 const p = vbp[i];
Vec3 const v = vbv[i];
float const h = height_at(scn, (Vec2){ p.x, p.z });
Vec3 const fwd = vec3_norm(vec3_sub(vbp[1], vbp[0]));
if (h >= p.y) {
/* back wheel processing: acceleration */
if (i == 0 || i == 3) {
float scale = 0;
if (scn->camera_mode == 2 && input_action_pressed("player_forward"))
scale += 1;
if (scn->camera_mode == 2 && input_action_pressed("player_backward"))
scale -= 1;
Facc[i] = vec3_add(Facc[i], vec3_scale(fwd, 6500 * scale));
}
/* normal force, for displacement */
Vec3 const n = normal_at(scn, (Vec2){ p.x, p.z });
float const xn = (h - p.y) * n.y;
float const vn = vec3_dot(v, n);
Vec3 const Fn = vec3_scale(n, -VEHICLE_SPRING_GK * xn - VEHICLE_SPRING_GC * vn);
Facc[i] = vec3_sub(Facc[i], Fn);
/* friction force, perpendicular to normal force */
/* TODO: is it right? aren't vn+vol should be = |v| */
Vec3 const von = vec3_norm(vec3_cross(n, vec3_cross(v, n)));
Vec3 const vo = vec3_scale(vec3_scale(von, vec3_length(v) - vn), -1);
float const vol = vec3_length(vo);
Vec3 const Fon = vec3_norm(vec3_cross(n, vec3_cross(Facc[i], n)));
Vec3 Fo = vec3_scale(vec3_scale(Fon, vec3_length(Facc[i]) - vec3_dot(Facc[i], n)), -1);
/* portion of total force along the surface */
float const Fol = vec3_length(Fo);
float const fkxn = VEHICLE_FRICTION_K * xn;
/* at rest, might want to start moving */
if (fabsf(0.0f - vol) <= 0.0001f) {
/* cannot overcome static friction, force along the surface is zeroed */
if (Fol <= VEHICLE_FRICTION_S * xn) { Fo = vec3_scale(Fo, -1);}
/* resist the force by friction, while starting to move */
else { Fo = vec3_sub(Fo, vec3_scale(von, fkxn));}
/* not at rest, stop accelerating along the surface */
} else if (vol + (Fol / VEHICLE_MASS) * ctx.frame_duration <= fkxn * ctx.frame_duration * 2) {
/* ugh ... */
vbv[i] = vec3_add(v, vo);
/* just apply friction */
} else {
Fo = vec3_scale(von, -fkxn * 400);
}
Facc[i] = vec3_add(Facc[i], Fo);
/* rear wheel friction */
if (i == 0 || i == 3) {
Vec3 const pn = vec3_cross(fwd, n);
Vec3 const Fp = vec3_scale(pn, vec3_dot(v, pn) * -VEHICLE_FRICTION_V);
Facc[i] = vec3_add(Facc[i], Fp);
}
/* front wheel processing */
if (i == 1 || i == 2) {
/* steering influences "center of turning", which is a point */
/* laying on line defined by rear axle */
/* front arms are rotated to be perpendicular to center of turning, */
/* which then are used to dissipate forces, thus providing control */
Vec3 const rear_bar = vec3_sub(vbp[0], vbp[3]);
Vec3 const rear_center = vec3_scale(vec3_add(vbp[0], vbp[3]), 0.5);
Vec3 a, b, r;
if (i == 1) {
a = vec3_sub(vbp[3], vbp[2]);
b = vec3_sub(rear_center, vbp[2]);
r = vbp[2];
} else {
a = vec3_sub(vbp[0], vbp[1]);
b = vec3_sub(rear_center, vbp[1]);
r = vbp[1];
}
float const arm_angle = vec3_angle(a, b);
Vec3 const turn_center = vec3_add(rear_center, vec3_scale(vec3_norm(rear_bar), vehicle_turning_extend));
Vec3 const arm = vec3_sub(r, turn_center);
Vec3 const n = vec3_norm(vec3_cross(a, b));
Vec3 const wheel = vec3_norm(vec3_rotate(arm, -arm_angle, n));
Vec3 const p = vec3_norm(vec3_cross(wheel, n));
draw_line_3d(r, vec3_add(r, p), 1, (Color){0,255,255,255});
Vec3 const Fp = vec3_scale(p, vec3_dot(v, p) * -VEHICLE_FRICTION_V);
Facc[i] = vec3_add(Facc[i], Fp);
}
}
Vec3 vd = vec3_scale(vec3_scale(Facc[i], (1.0f / VEHICLE_MASS)), ctx.frame_duration);
vbv[i] = vec3_add(vbv[i], vd);
vbp[i] = vec3_add(vbp[i], vec3_scale(vbv[i], ctx.frame_duration));
}
}
static void process_vehicle_mode(State *state) {
SceneIngame *scn = (SceneIngame *)state->scene;
Vec3 const top_center = vec3_sub(vbp[4], vec3_scale(vec3_sub(vbp[4], vbp[6]), 1.0f / 2.0f));
// Vec3 const front_center = vec3_add(vbp[4], vec3_scale(vec3_sub(vbp[4], vbp[7]), 1.0f / 2.0f));
// Vec3 const facing_direction = vec3_sub(top_center, front_center);
float yawc, yaws, pitchc, pitchs;
sincosf(scn->yaw + (float)M_PI_2, &yaws, &yawc);
sincosf(scn->pitch, &pitchs, &pitchc);
Vec3 const looking_direction = vec3_norm(((Vec3){
yawc * pitchc,
pitchs,
yaws * pitchc,
}));
Vec3 const orbit = vec3_sub(top_center, vec3_scale(looking_direction, 7.5));
draw_camera(orbit, looking_direction, (Vec3){0,1,0}, (float)M_PI_2 * 0.8f, 1, TERRAIN_RADIUS * sqrtf(3));
scn->looking_direction = looking_direction;
scn->pos = top_center;
}
static void process_fly_mode(State *state) { static void process_fly_mode(State *state) {
SceneIngame *scn = (SceneIngame *)state->scene; SceneIngame *scn = (SceneIngame *)state->scene;
DrawCameraFromPrincipalAxesResult dir_and_up = DrawCameraFromPrincipalAxesResult dir_and_up =
draw_camera_from_principal_axes(scn->pos, scn->roll, scn->pitch, scn->yaw, (float)M_PI_2 * 0.8f, 1); draw_camera_from_principal_axes(scn->pos, scn->roll, scn->pitch, scn->yaw, (float)M_PI_2 * 0.8f, 1, TERRAIN_RADIUS * sqrtf(3));
scn->looking_direction = dir_and_up.direction;
const Vec3 right = m_vec_norm(m_vec_cross(dir_and_up.direction, dir_and_up.up)); const Vec3 right = m_vec_norm(m_vec_cross(dir_and_up.direction, dir_and_up.up));
const float speed = 0.04f; /* TODO: put this in a better place */ const float speed = 0.1f; /* TODO: put this in a better place */
if (input_action_pressed("player_left")) if (input_action_pressed("player_left"))
scn->pos = vec3_sub(scn->pos, m_vec_scale(right, speed)); scn->pos = vec3_sub(scn->pos, m_vec_scale(right, speed));
@ -47,33 +323,39 @@ static void process_fly_mode(State *state) {
scn->pos.y -= speed; scn->pos.y -= speed;
} }
/* TODO: could be baked in map format */
static Vec3 normal_at(SceneIngame *scn, Vec2 position) {
int const x = (int)(floorf(position.x - scn->world_center.x));
int const y = (int)(floorf(position.y - scn->world_center.y));
float const height0 = heightmap[x][y];
float const height1 = heightmap[x + 1][y];
float const height2 = heightmap[x][y + 1];
Vec3 const a = { .x = 1, .y = height0 - height1, .z = 0 };
Vec3 const b = { .x = 0, .y = height0 - height2, .z = -1 };
return vec3_norm(vec3_cross(a, b));
}
/* TODO: don't operate on triangles, instead interpolate on quads */
static float height_at(SceneIngame *scn, Vec2 position) { static float height_at(SceneIngame *scn, Vec2 position) {
float height0, height1, height2, weight0, weight1, weight2; int const x = (int)(floorf(position.x - scn->world_center.x));
int const y = (int)(floorf(position.y - scn->world_center.y));
int const x = (int)(HALF_TERRAIN_DISTANCE + (position.x - scn->pos.x)); float const height0 = heightmap[x][y];
int const y = (int)(HALF_TERRAIN_DISTANCE + (position.y - scn->pos.z)); float const height1 = heightmap[x + 1][y];
float const height2 = heightmap[x][y + 1];
height0 = heightmap[x][y]; float const height3 = heightmap[x + 1][y + 1];
height1 = heightmap[x + 1][y + 1];
Vec2 incell = { position.x - floorf(position.x), position.y - floorf(position.y) }; Vec2 incell = { position.x - floorf(position.x), position.y - floorf(position.y) };
/* who needs barycentric coordinates, am i right? */ float const weight0 = (1 - incell.x) * (1 - incell.y);
weight0 = 1 / sqrtf(powf(incell.x, 2) + powf(incell.y, 2)); float const weight1 = ( incell.x) * (1 - incell.y);
weight1 = 1 / sqrtf(powf(1 - incell.x, 2) + powf(1 - incell.y, 2)); float const weight2 = (1 - incell.x) * ( incell.y);
float const weight3 = ( incell.x) * ( incell.y);
/* find which triangle we're directly under */ return (height0 * weight0 + height1 * weight1 + height2 * weight2 + height3 * weight3) / (weight0 + weight1 + weight2 + weight3);
/* for this manhattan distance is sufficient */
if (incell.x + (1 - incell.y) < (1 - incell.x) + incell.y) {
height2 = heightmap[x][y + 1];
weight2 = 1 / sqrtf(powf(incell.x, 2) + powf(1 - incell.y, 2));
} else {
height2 = heightmap[x + 1][y];
weight2 = 1 / sqrtf(powf(1 - incell.x, 2) + powf(incell.y, 2));
}
return (height0 * weight0 + height1 * weight1 + height2 * weight2) / (weight0 + weight1 + weight2);
} }
@ -81,13 +363,15 @@ static void process_ground_mode(State *state) {
SceneIngame *scn = (SceneIngame *)state->scene; SceneIngame *scn = (SceneIngame *)state->scene;
DrawCameraFromPrincipalAxesResult dir_and_up = DrawCameraFromPrincipalAxesResult dir_and_up =
draw_camera_from_principal_axes(scn->pos, scn->roll, scn->pitch, scn->yaw, (float)M_PI_2 * 0.8f, 1); draw_camera_from_principal_axes(scn->pos, scn->roll, scn->pitch, scn->yaw, (float)M_PI_2 * 0.8f, 1, TERRAIN_RADIUS * sqrtf(3));
scn->looking_direction = dir_and_up.direction;
dir_and_up.direction.y = 0; dir_and_up.direction.y = 0;
dir_and_up.direction = vec3_norm(dir_and_up.direction); dir_and_up.direction = vec3_norm(dir_and_up.direction);
const Vec3 right = m_vec_norm(m_vec_cross(dir_and_up.direction, dir_and_up.up)); const Vec3 right = m_vec_norm(m_vec_cross(dir_and_up.direction, dir_and_up.up));
const float speed = 0.18f; /* TODO: put this in a better place */ const float speed = 0.20f; /* TODO: put this in a better place */
Vec3 target = scn->pos; Vec3 target = scn->pos;
@ -96,7 +380,7 @@ static void process_ground_mode(State *state) {
float const height = height_at(scn, (Vec2){scn->pos.x, scn->pos.z}); float const height = height_at(scn, (Vec2){scn->pos.x, scn->pos.z});
if (target.y > height + PLAYER_HEIGHT) if (target.y > height + PLAYER_HEIGHT)
target.y = target.y - 0.4f; target.y = target.y - 0.6f;
if (target.y < height + PLAYER_HEIGHT) if (target.y < height + PLAYER_HEIGHT)
target.y = height + PLAYER_HEIGHT; target.y = height + PLAYER_HEIGHT;
@ -135,50 +419,99 @@ static void generate_terrain(SceneIngame *scn) {
float y = floorf(scn->pos.z - HALF_TERRAIN_DISTANCE + (float)ly); float y = floorf(scn->pos.z - HALF_TERRAIN_DISTANCE + (float)ly);
float height = stb_perlin_noise3((float)x * TERRAIN_FREQUENCY, (float)y * TERRAIN_FREQUENCY, 0, 0, 0, 0) * 3 - 1; float height = stb_perlin_noise3((float)x * TERRAIN_FREQUENCY, (float)y * TERRAIN_FREQUENCY, 0, 0, 0, 0) * 3 - 1;
height += stb_perlin_noise3((float)x * TERRAIN_FREQUENCY / 10, (float)y * TERRAIN_FREQUENCY / 10, 0, 0, 0, 0) * 10 - 1; height += stb_perlin_noise3((float)x * TERRAIN_FREQUENCY / 10, (float)y * TERRAIN_FREQUENCY / 10, 0, 0, 0, 0) * 20 - 1;
heightmap[lx][ly] = height; heightmap[lx][ly] = height;
} }
} }
scn->world_center = (Vec2){ floorf(scn->pos.x - HALF_TERRAIN_DISTANCE), floorf(scn->pos.z - HALF_TERRAIN_DISTANCE) };
}
static int32_t ceil_sqrt(int32_t const n) {
int32_t res = 1;
while(res * res < n)
res++;
return res;
}
static uint32_t adler32(const void *buf, size_t buflength) {
const uint8_t *buffer = (const uint8_t*)buf;
uint32_t s1 = 1;
uint32_t s2 = 0;
for (size_t n = 0; n < buflength; n++) {
s1 = (s1 + buffer[n]) % 65521;
s2 = (s2 + s1) % 65521;
}
return (s2 << 16) | s1;
} }
static void draw_terrain(SceneIngame *scn) { static void draw_terrain(SceneIngame *scn) {
for (int ly = TERRAIN_DISTANCE - 1; ly > 0; ly--) { /* used to cull invisible tiles over field of view (to horizon) */
for (int lx = 0; lx < TERRAIN_DISTANCE - 1; lx++) { Vec2 const d = vec2_norm((Vec2){ .x = scn->looking_direction.x, .y = scn->looking_direction.z });
int32_t x = (int32_t)(floorf(scn->pos.x - HALF_TERRAIN_DISTANCE + (float)lx)); float const c = cosf((float)M_PI_2 * 0.8f * 0.8f);
int32_t y = (int32_t)(floorf(scn->pos.z - HALF_TERRAIN_DISTANCE + (float)ly));
/* draw terrain in circle */
int32_t const rsi = (int32_t)TERRAIN_RADIUS * (int32_t)TERRAIN_RADIUS;
for (int32_t iy = -(int32_t)TERRAIN_RADIUS; iy <= (int32_t)TERRAIN_RADIUS - 1; ++iy) {
int32_t const dx = ceil_sqrt(rsi - (iy + (iy <= 0)) * (iy + (iy <= 0)));
for (int32_t ix = -dx; ix < dx - 1; ++ix) {
int32_t lx = ix + TERRAIN_RADIUS;
int32_t ly = iy + TERRAIN_RADIUS;
float x = (float)(floorf(scn->pos.x - HALF_TERRAIN_DISTANCE + (float)lx));
float y = (float)(floorf(scn->pos.z - HALF_TERRAIN_DISTANCE + (float)ly));
/* cull tiles outside of vision */
if (vec2_dot(vec2_norm((Vec2){x - scn->pos.x + d.x * 2, y - scn->pos.z + d.y * 2}), d) < c)
continue;
float d0 = heightmap[lx][ly]; float d0 = heightmap[lx][ly];
float d1 = heightmap[lx + 1][ly]; float d1 = heightmap[lx + 1][ly];
float d2 = heightmap[lx + 1][ly - 1]; float d2 = heightmap[lx + 1][ly - 1];
float d3 = heightmap[lx][ly - 1]; float d3 = heightmap[lx][ly - 1];
draw_triangle("/assets/grass.png", draw_quad("/assets/grass2.png",
(Vec3){ (float)x, d0, (float)y }, (Vec3){ (float)x, d0, (float)y },
(Vec3){ (float)x + 1, d1, (float)y },
(Vec3){ (float)x, d3, (float)y - 1 },
(Vec2){ 128, 128 },
(Vec2){ 128, 0 },
(Vec2){ 0, 128 },
(Color){255, 255, 255, 255},
(Color){255, 255, 255, 255},
(Color){255, 255, 255, 255});
draw_triangle("/assets/grass.png",
(Vec3){ (float)x + 1, d1, (float)y }, (Vec3){ (float)x + 1, d1, (float)y },
(Vec3){ (float)x + 1, d2, (float)y - 1 }, (Vec3){ (float)x + 1, d2, (float)y - 1 },
(Vec3){ (float)x, d3, (float)y - 1 }, (Vec3){ (float)x, d3, (float)y - 1 },
(Vec2){ 128, 0 }, (Rect){ .w = 128, .h = 128 },
(Vec2){ 0, 0 },
(Vec2){ 0, 128 },
(Color){255, 255, 255, 255},
(Color){255, 255, 255, 255},
(Color){255, 255, 255, 255}); (Color){255, 255, 255, 255});
draw_billboard("/assets/grasses/10.png", if (((float)(adler32(&((Vec2){x, y}), sizeof (Vec2)) % 100) / 100) <= TREE_DENSITY)
(Vec3){ (float)x, d0 + 0.15f, (float)y }, draw_billboard("/assets/trreez.png",
(Vec2){0.3f, 0.3f}, (Vec3){ (float)x, d0 + 1.95f, (float)y },
(Vec2){2.f, 2.f},
(Rect){0},
(Color){255, 255, 255, 255}, true);
}
}
int32_t const rsi_g = (int32_t)GRASS_RADIUS * (int32_t)GRASS_RADIUS;
for (int32_t iy = -(int32_t)GRASS_RADIUS; iy <= (int32_t)GRASS_RADIUS - 1; ++iy) {
int32_t const dx = ceil_sqrt(rsi_g - (iy + (iy <= 0)) * (iy + (iy <= 0)));
for (int32_t ix = -dx; ix < dx; ++ix) {
int32_t lx = ix + TERRAIN_RADIUS;
int32_t ly = iy + TERRAIN_RADIUS;
float x = (float)(floorf(scn->pos.x - HALF_TERRAIN_DISTANCE + (float)lx));
float y = (float)(floorf(scn->pos.z - HALF_TERRAIN_DISTANCE + (float)ly));
float d = heightmap[lx][ly];
draw_billboard("/assets/grasses/25.png",
(Vec3){
(float)x + (float)((adler32(&((Vec2){x, y}), sizeof (Vec2))) % 32) / 64.0f,
d + 0.2f,
(float)y + (float)((adler32(&((Vec2){y, x}), sizeof (Vec2))) % 32) / 64.0f
},
(Vec2){0.4f, 0.4f},
(Rect){0}, (Rect){0},
(Color){255, 255, 255, 255}, true); (Color){255, 255, 255, 255}, true);
} }
@ -198,24 +531,22 @@ static void ingame_tick(State *state) {
input_action("mouse_capture_toggle", "ESCAPE"); input_action("mouse_capture_toggle", "ESCAPE");
input_action("toggle_camera_mode", "C"); input_action("toggle_camera_mode", "C");
// draw_model("models/test.obj", (Vec3){0}, (Vec3){0,0,1}, (Vec3){1.f / 64,1.f / 64,1.f / 64});
// draw_model("models/test2.obj", (Vec3){0}, (Vec3){0,0,1}, (Vec3){1.f / 64,1.f / 64,1.f / 64});
// draw_model("models/bunny.obj", (Vec3){0}, (Vec3){0,0,1}, (Vec3){4.,4.,4.});
if (scn->mouse_captured) { if (scn->mouse_captured) {
const float sensitivity = 0.4f * (float)DEG2RAD; /* TODO: put this in a better place */ const float sensitivity = 0.4f * (float)(M_PI / 180); /* TODO: put this in a better place */
scn->yaw += (float)ctx.mouse_movement.x * sensitivity; scn->yaw += (float)ctx.mouse_movement.x * sensitivity;
scn->pitch -= (float)ctx.mouse_movement.y * sensitivity; scn->pitch -= (float)ctx.mouse_movement.y * sensitivity;
scn->pitch = clampf(scn->pitch, (float)-M_PI * 0.49f, (float)M_PI * 0.49f); scn->pitch = clampf(scn->pitch, (float)-M_PI * 0.49f, (float)M_PI * 0.49f);
} }
if (input_action_just_pressed("toggle_camera_mode")) if (input_action_just_pressed("toggle_camera_mode"))
scn->flying_camera = !scn->flying_camera; scn->camera_mode = scn->camera_mode == 2 ? 0 : scn->camera_mode + 1;
if (scn->flying_camera) { if (scn->camera_mode == 1) {
process_fly_mode(state); process_fly_mode(state);
} else { } else if (scn->camera_mode == 0) {
process_ground_mode(state); process_ground_mode(state);
} else if (scn->camera_mode) {
process_vehicle_mode(state);
} }
/* toggle mouse capture with end key */ /* toggle mouse capture with end key */
@ -225,12 +556,15 @@ static void ingame_tick(State *state) {
ctx.mouse_capture = scn->mouse_captured; ctx.mouse_capture = scn->mouse_captured;
generate_terrain(scn); generate_terrain(scn);
process_vehicle(scn);
draw_terrain(scn); draw_terrain(scn);
draw_vehicle(scn);
draw_skybox("/assets/miramar/miramar_*.tga"); draw_skybox("/assets/miramar/miramar_*.tga");
ctx.fog_color = (Color){ 140, 147, 160, 255 }; ctx.fog_color = (Color){ 140, 147, 160, 255 };
ctx.fog_density = 0.03f; ctx.fog_density = 0.015f;
} }
@ -248,7 +582,7 @@ Scene *ingame_scene(State *state) {
new_scene->mouse_captured = true; new_scene->mouse_captured = true;
m_audio(m_set(path, "music/mod65.xm"), m_audio(m_set(path, "music/woah.ogg"),
m_opt(channel, "soundtrack"), m_opt(channel, "soundtrack"),
m_opt(repeat, true)); m_opt(repeat, true));

View File

@ -12,13 +12,16 @@
typedef struct SceneIngame { typedef struct SceneIngame {
Scene base; Scene base;
Vec3 looking_direction;
Vec2 world_center;
Vec3 pos; Vec3 pos;
float yaw; float yaw;
float pitch; float pitch;
float roll; float roll;
bool mouse_captured; bool mouse_captured;
bool flying_camera; int camera_mode;
} SceneIngame; } SceneIngame;

View File

@ -1,61 +0,0 @@
#include "title.h"
#include "ingame.h"
#include "twn_game_api.h"
#include <stdio.h>
#include <stdlib.h>
static void title_tick(State *state) {
SceneTitle *scn = (SceneTitle *)state->scene;
(void)scn;
input_action("ui_accept", "RETURN");
if (input_action_just_pressed("ui_accept")) {
switch_to(state, ingame_scene);
return;
}
m_sprite("/assets/title.png", ((Rect) {
((float)ctx.resolution.x / 2) - ((float)320 / 2), 64, 320, 128 }));
/* draw the tick count as an example of dynamic text */
size_t text_str_len = snprintf(NULL, 0, "%llu", (unsigned long long)state->ctx->frame_number) + 1;
char *text_str = malloc(text_str_len);
snprintf(text_str, text_str_len, "%llu", (unsigned long long)state->ctx->frame_number);
const char *font = "/fonts/kenney-pixel.ttf";
float text_h = 32;
float text_w = draw_text_width(text_str, text_h, font);
draw_rectangle(
(Rect) {
.x = 0,
.y = 0,
.w = (float)text_w,
.h = (float)text_h,
},
(Color) { 0, 0, 0, 255 }
);
draw_text(text_str, (Vec2){ 0, 0 }, text_h, (Color) { 255, 255, 255, 255 }, font);
free(text_str);
}
static void title_end(State *state) {
free(state->scene);
}
Scene *title_scene(State *state) {
(void)state;
SceneTitle *new_scene = calloc(1, sizeof *new_scene);
new_scene->base.tick = title_tick;
new_scene->base.end = title_end;
return (Scene *)new_scene;
}

View File

@ -1,16 +0,0 @@
#ifndef TITLE_H
#define TITLE_H
#include "../state.h"
#include "scene.h"
typedef struct SceneTitle {
Scene base;
} SceneTitle;
Scene *title_scene(State *state);
#endif

View File

@ -13,4 +13,4 @@ set(SOURCE_FILES
state.h state.h
) )
use_townengine(${PROJECT_NAME} "${SOURCE_FILES}" ${CMAKE_CURRENT_SOURCE_DIR}) use_townengine("${SOURCE_FILES}" ${CMAKE_CURRENT_SOURCE_DIR})

View File

@ -116,11 +116,13 @@ void game_tick(void) {
input_action("up", "LCLICK"); input_action("up", "LCLICK");
input_action("down", "RCLICK"); input_action("down", "RCLICK");
if (input_action_just_pressed("up")) if (input_action_pressed("up"))
state->r += 1; state->r += 1;
if (input_action_just_pressed("down")) if (input_action_pressed("down") && state->r > 2)
state->r -= 1; state->r -= 1;
draw_circle(mouse_snap, state->r * 8, (Color){125, 125, 125, 125});
int32_t const rsi = (int32_t)state->r * (int32_t)state->r; int32_t const rsi = (int32_t)state->r * (int32_t)state->r;
int32_t acc = 1; int32_t acc = 1;
for (int32_t iy = (int32_t)state->r - 1; iy >= 0; --iy) { for (int32_t iy = (int32_t)state->r - 1; iy >= 0; --iy) {
@ -133,9 +135,8 @@ void game_tick(void) {
} }
} }
draw_circle(mouse_snap, state->r * 8, (Color){125, 125, 125, 125}); /* uncomment to see performance difference between variants */
// benchmark(state);
benchmark(state);
} }

BIN
apps/templates/c/data/twn.png (Stored with Git LFS)

Binary file not shown.

View File

@ -17,11 +17,6 @@ void game_tick(void) {
struct state *state = ctx.udata; struct state *state = ctx.udata;
++state->counter; ++state->counter;
m_sprite("twn.png",
(Rect) { .w = 128, .h = 64, },
m_opt(stretch, true)
);
} }

View File

@ -3,10 +3,15 @@
!*.* !*.*
!*/ !*/
*.so **/*.so
*.dll **/*.dll
*.exe **/*.exe
*.trace **/*.trace
**/*.js
**/*.wasm
**/*.wasm.map
**/*.data
**/*.html
data/scripts/twnapi.lua data/scripts/twnapi.lua
build/ build/

View File

@ -1,53 +0,0 @@
---@type { frame_number: number, frame_duration: number, fog_density: number, fog_color: { r: number, g: number, b: number, a: number }, resolution: { x: number, y: number }, mouse_position: { x: number, y: number }, mouse_movement: { x: number, y: number }, random_seed: number, debug: boolean, initialization_needed: boolean, mouse_capture: boolean, udata: table }
ctx = nil
---@alias Control '"A"'|'"B"'|'"C"'|'"D"'|'"E"'|'"F"'|'"G"'|'"H"'|'"I"'|'"J"'|'"K"'|'"L"'|'"M"'|'"N"'|'"O"'|'"P"'|'"Q"'|'"R"'|'"S"'|'"T"'|'"U"'|'"V"'|'"W"'|'"X"'|'"Y"'|'"Z"'|'"1"'|'"2"'|'"3"'|'"4"'|'"5"'|'"6"'|'"7"'|'"8"'|'"9"'|'"0"'|'"RETURN"'|'"ESCAPE"'|'"BACKSPACE"'|'"TAB"'|'"SPACE"'|'"MINUS"'|'"EQUALS"'|'"LEFTBRACKET"'|'"RIGHTBRACKET"'|'"BACKSLASH"'|'"NONUSHASH"'|'"SEMICOLON"'|'"APOSTROPHE"'|'"GRAVE"'|'"COMMA"'|'"PERIOD"'|'"SLASH"'|'"CAPSLOCK"'|'"F1"'|'"F2"'|'"F3"'|'"F4"'|'"F5"'|'"F6"'|'"F7"'|'"F8"'|'"F9"'|'"F10"'|'"F11"'|'"F12"'|'"PRINTSCREEN"'|'"SCROLLLOCK"'|'"PAUSE"'|'"INSERT"'|'"HOME"'|'"PAGEUP"'|'"DELETE"'|'"END"'|'"PAGEDOWN"'|'"RIGHT"'|'"LEFT"'|'"DOWN"'|'"UP"'|'"NUMLOCKCLEAR"'|'"KP_DIVIDE"'|'"KP_MULTIPLY"'|'"KP_MINUS"'|'"KP_PLUS"'|'"KP_ENTER"'|'"KP_1"'|'"KP_2"'|'"KP_3"'|'"KP_4"'|'"KP_5"'|'"KP_6"'|'"KP_7"'|'"KP_8"'|'"KP_9"'|'"KP_0"'|'"KP_PERIOD"'|'"NONUSBACKSLASH"'|'"APPLICATION"'|'"POWER"'|'"KP_EQUALS"'|'"F13"'|'"F14"'|'"F15"'|'"F16"'|'"F17"'|'"F18"'|'"F19"'|'"F20"'|'"F21"'|'"F22"'|'"F23"'|'"F24"'|'"EXECUTE"'|'"HELP"'|'"MENU"'|'"SELECT"'|'"STOP"'|'"AGAIN"'|'"UNDO"'|'"CUT"'|'"COPY"'|'"PASTE"'|'"FIND"'|'"MUTE"'|'"VOLUMEUP"'|'"VOLUMEDOWN"'|'"KP_COMMA"'|'"KP_EQUALSAS400"'|'"INTERNATIONAL1"'|'"INTERNATIONAL2"'|'"INTERNATIONAL3"'|'"INTERNATIONAL4"'|'"INTERNATIONAL5"'|'"INTERNATIONAL6"'|'"INTERNATIONAL7"'|'"INTERNATIONAL8"'|'"INTERNATIONAL9"'|'"LANG1"'|'"LANG2"'|'"LANG3"'|'"LANG4"'|'"LANG5"'|'"LANG6"'|'"LANG7"'|'"LANG8"'|'"LANG9"'|'"ALTERASE"'|'"SYSREQ"'|'"CANCEL"'|'"CLEAR"'|'"PRIOR"'|'"RETURN2"'|'"SEPARATOR"'|'"OUT"'|'"OPER"'|'"CLEARAGAIN"'|'"CRSEL"'|'"EXSEL"'|'"KP_00"'|'"KP_000"'|'"THOUSANDSSEPARATOR"'|'"DECIMALSEPARATOR"'|'"CURRENCYUNIT"'|'"CURRENCYSUBUNIT"'|'"KP_LEFTPAREN"'|'"KP_RIGHTPAREN"'|'"KP_LEFTBRACE"'|'"KP_RIGHTBRACE"'|'"KP_TAB"'|'"KP_BACKSPACE"'|'"KP_A"'|'"KP_B"'|'"KP_C"'|'"KP_D"'|'"KP_E"'|'"KP_F"'|'"KP_XOR"'|'"KP_POWER"'|'"KP_PERCENT"'|'"KP_LESS"'|'"KP_GREATER"'|'"KP_AMPERSAND"'|'"KP_DBLAMPERSAND"'|'"KP_VERTICALBAR"'|'"KP_DBLVERTICALBAR"'|'"KP_COLON"'|'"KP_HASH"'|'"KP_SPACE"'|'"KP_AT"'|'"KP_EXCLAM"'|'"KP_MEMSTORE"'|'"KP_MEMRECALL"'|'"KP_MEMCLEAR"'|'"KP_MEMADD"'|'"KP_MEMSUBTRACT"'|'"KP_MEMMULTIPLY"'|'"KP_MEMDIVIDE"'|'"KP_PLUSMINUS"'|'"KP_CLEAR"'|'"KP_CLEARENTRY"'|'"KP_BINARY"'|'"KP_OCTAL"'|'"KP_DECIMAL"'|'"KP_HEXADECIMAL"'|'"LCTRL"'|'"LSHIFT"'|'"LALT"'|'"LGUI"'|'"RCTRL"'|'"RSHIFT"'|'"RALT"'|'"RGUI"'|'"MODE"'|'"KBDILLUMTOGGLE"'|'"KBDILLUMDOWN"'|'"KBDILLUMUP"'|'"EJECT"'|'"SLEEP"'|'"APP1"'|'"APP2"'|'"AUDIOREWIND"'|'"AUDIOFASTFORWARD"'|'"SOFTLEFT"'|'"SOFTRIGHT"'|'"CALL"'|'"ENDCALL"'|'"LEFT_MOUSE"'|'"RIGHT_MOUSE"'|'"MIDDLE_MOUSE"'|'"X1"'|'"X2"'
---@param args { name: string, control: Control }
function input_action(args) end
---@param args { name: string }
function input_action_pressed(args) end
---@param args { name: string }
function input_action_just_pressed(args) end
---@param args { name: string }
function input_action_just_released(args) end
---@param args { name: string }
function input_action_position(args) end
---@param args { texture: string, rect: { x: number, y: number, w: number, h: number }, texture_region: { x: number, y: number, w: number, h: number }?, color: { r: number, g: number, b: number, a: number }?, rotation: number?, flip_x: boolean?, flip_y: boolean?, stretch: boolean? }
function draw_sprite(args) end
---@param args { rect: { x: number, y: number, w: number, h: number }, color: { r: number, g: number, b: number, a: number }? }
function draw_rectangle(args) end
---@param args { position: { x: number, y: number }, radius: number, color: { r: number, g: number, b: number, a: number }? }
function draw_circle(args) end
---@param args { string: string, position: { x: number, y: number }, height: number?, color: { r: number, g: number, b: number, a: number }?, font: string? }
function draw_text(args) end
---@param args { string: string, height: number?, font: string? }
function draw_text_width(args) end
---@param args { texture: string, corners: { x: number, y: number }, rect: { x: number, y: number, w: number, h: number }, border_thickness: number?, color: { r: number, g: number, b: number, a: number }? }
function draw_nine_slice(args) end
---@param args { start: { x: number, y: number }, finish: { x: number, y: number }, thickness: number?, color: { r: number, g: number, b: number, a: number }? }
function draw_line(args) end
---@param args { texture: string, v0: { x: number, y: number, z: number }, v1: { x: number, y: number, z: number }, v2: { x: number, y: number, z: number }, uv0: { x: number, y: number }, uv1: { x: number, y: number }, uv2: { x: number, y: number }, c0: { r: number, g: number, b: number, a: number }?, c1: { r: number, g: number, b: number, a: number }?, c2: { r: number, g: number, b: number, a: number }? }
function draw_triangle(args) end
---@param args { texture: string, v0: { x: number, y: number, z: number }, v1: { x: number, y: number, z: number }, v2: { x: number, y: number, z: number }, v3: { x: number, y: number, z: number }, texture_region: { x: number, y: number, w: number, h: number }, color: { r: number, g: number, b: number, a: number }? }
function draw_quad(args) end
---@param args { texture: string, position: { x: number, y: number, z: number }, size: { x: number, y: number }, color: { r: number, g: number, b: number, a: number }?, cylindrical: boolean? }
function draw_billboard(args) end
---@param args { position: { x: number, y: number, z: number }, direction: { x: number, y: number, z: number }?, up: { x: number, y: number, z: number }?, fov: number?, zoom: number? }
function draw_camera(args) end
---@param args { position: { x: number, y: number, z: number }, roll: number?, pitch: number?, yaw: number?, fov: number?, zoom: number? }
function draw_camera_from_principal_axes(args) end
---@param args { textures: string? }
function draw_skybox(args) end
---@param args { audio: string, channel: string?, loops: boolean?, volume: number?, panning: number? }
function audio_play(args) end
---@param args { channel: string, parameter: string, value: number }
function audio_parameter(args) end
---@param args { value: { x: number, y: number }, identity: string }
function log_vec2(args) end
---@param args { value: { x: number, y: number, z: number }, identity: string }
function log_vec3(args) end
---@param args { value: { x: number, y: number, w: number, h: number }, identity: string }
function log_rect(args) end
---@param args { profile: string }
function profile_start(args) end
---@param args { profile: string }
function profile_end(args) end

19
apps/templates/zig/.gitignore vendored Normal file
View File

@ -0,0 +1,19 @@
# ignore executables
*
!*.*
!*/
**/*.so
**/*.dll
**/*.exe
**/*.trace
**/*.js
**/*.wasm
**/*.wasm.map
**/*.data
**/*.html
data/scripts/twnapi.lua
build/
.zig-cache/
zig-out/

View File

@ -0,0 +1,26 @@
cmake_minimum_required(VERSION 3.21)
project(twngame LANGUAGES C)
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Debug)
endif()
add_subdirectory($ENV{TWNROOT} ${CMAKE_BINARY_DIR}/twn)
cmake_path(GET CMAKE_SOURCE_DIR STEM LAST_ONLY GAME_PROJECT_NAME)
put_townengine(${CMAKE_CURRENT_SOURCE_DIR})
file(GLOB_RECURSE zig-sources ${CMAKE_CURRENT_SOURCE_DIR}/src/*.zig)
# TODO: support static build
# TODO: propagate release switches
add_custom_command(
OUTPUT ${CMAKE_CURRENT_SOURCE_DIR}/libgame.so
COMMAND env zig build
DEPENDS ${TWN_TARGET} ${zig-sources}
)
add_custom_target(
zig-step ALL
DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/libgame.so
)

View File

@ -0,0 +1,58 @@
const std = @import("std");
// Although this function looks imperative, note that its job is to
// declaratively construct a build graph that will be executed by an external
// runner.
pub fn build(b: *std.Build) void {
// Standard target options allows the person running `zig build` to choose
// what target to build for. Here we do not override the defaults, which
// means any target is allowed, and the default is native. Other options
// for restricting supported target set are available.
const target = b.standardTargetOptions(.{});
// Standard optimization options allow the person running `zig build` to select
// between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not
// set a preferred release mode, allowing the user to decide how to optimize.
const optimize = b.standardOptimizeOption(.{});
// This creates a "module", which represents a collection of source files alongside
// some compilation options, such as optimization mode and linked system libraries.
// Every executable or library we compile will be based on one or more modules.
const lib_mod = b.createModule(.{
// `root_source_file` is the Zig "entry point" of the module. If a module
// only contains e.g. external object files, you can make this `null`.
// In this case the main source file is merely a path, however, in more
// complicated build scripts, this could be a generated file.
.root_source_file = b.path("src/root.zig"),
.target = target,
.optimize = optimize,
.link_libc = true,
});
lib_mod.addIncludePath(b.path("../../../"));
lib_mod.addIncludePath(b.path("../../../include/"));
lib_mod.addLibraryPath(b.path("./"));
// Now, we will create a static library based on the module we created above.
// This creates a `std.Build.Step.Compile`, which is the build step responsible
// for actually invoking the compiler.
const lib = b.addLibrary(.{
.linkage = .dynamic,
.name = "game",
.root_module = lib_mod,
});
lib.linkSystemLibrary("townengine");
// This declares intent for the library to be installed into the standard
// location when the user invokes the "install" step (the default step when
// running `zig build`).
const install_artifact = b.addInstallArtifact(lib, .{
.dest_dir = .{
.override = .{
.custom = "../",
},
},
});
b.getInstallStep().dependOn(&install_artifact.step);
}

View File

@ -0,0 +1,27 @@
# This file contains everything about the engine and your game that can be
# configured before it runs.
#
# Optional settings are commented out, with their default values shown.
# Invalid values in these settings will be ignored.
# Data about your game as an application
[about]
title = "Zig Awesomeness"
developer = "notwanp"
app_id = "yourzigthing"
dev_id = "definatelynotwanp"
# Game runtime details
[game]
resolution = [ 640, 480 ]
background_color = [ 255, 125, 0, 255 ]
#debug = true
# Engine tweaks. You probably don't need to change these
[engine]
#ticks_per_second = 60 # minimum of 8
#keybind_slots = 3 # minimum of 1
#texture_atlas_size = 2048 # minimum of 32
#font_texture_size = 2048 # minimum of 1024
#font_oversampling = 4 # minimum of 0
#font_filtering = "linear" # possible values: "nearest", "linear"

View File

@ -0,0 +1,24 @@
const std = @import("std");
const c = @cImport({
@cInclude("twn_game_api.h");
});
export fn game_tick() void {
tick() catch |err| {
std.log.err(@errorName(err));
};
}
export fn game_end() void {
end() catch |err| {
std.log.err(@errorName(err));
};
}
fn tick() !void {
if (c.ctx.initialization_needed) {
std.debug.print("lmao\n", .{});
}
}
fn end() !void {}

View File

@ -0,0 +1,16 @@
cmake_minimum_required(VERSION 3.21)
project(twndel LANGUAGES C)
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Debug)
endif()
add_subdirectory($ENV{TWNROOT} ${CMAKE_BINARY_DIR}/twn)
set(SOURCE_FILES
tool.c
state.h
)
cmake_path(GET CMAKE_SOURCE_DIR STEM LAST_ONLY GAME_PROJECT_NAME)
use_townengine("${SOURCE_FILES}" ${CMAKE_CURRENT_SOURCE_DIR})

BIN
apps/tools/twndel/data/bong.ogg (Stored with Git LFS) Normal file

Binary file not shown.

BIN
apps/tools/twndel/data/camera.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
apps/tools/twndel/data/center.png (Stored with Git LFS) Normal file

Binary file not shown.

Binary file not shown.

BIN
apps/tools/twndel/data/drop.ogg (Stored with Git LFS) Normal file

Binary file not shown.

BIN
apps/tools/twndel/data/grab.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
apps/tools/twndel/data/point.png (Stored with Git LFS) Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
apps/tools/twndel/data/selectin.ogg (Stored with Git LFS) Normal file

Binary file not shown.

BIN
apps/tools/twndel/data/selectout.ogg (Stored with Git LFS) Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
apps/tools/twndel/data/switch.ogg (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -0,0 +1,12 @@
[about]
title = "Townengine Modeling Tool"
developer = "twnteam"
app_id = "twndel"
dev_id = "twnteam"
# Game runtime details
[game]
resolution = [ 640, 480 ]
background_color = [ 255, 125, 0, 255 ]
[engine]

BIN
apps/tools/twndel/data/x.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
apps/tools/twndel/data/y.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
apps/tools/twndel/data/z.png (Stored with Git LFS) Normal file

Binary file not shown.

118
apps/tools/twndel/state.h Normal file
View File

@ -0,0 +1,118 @@
#ifndef STATE_H
#define STATE_H
#include "twn_game_api.h"
#define POINTS_PER_METER 128
#define UNDO_STACK_SIZE 32
#define CAMERA_FOV ((float)M_PI_2 * 0.75f)
#define POINT_LIMIT 65534
#define FACE_LIMIT 2048
#define OBJECT_LIMIT 16
#define TEXTURE_LIMIT 32
#define INVALID_POINT (POINT_LIMIT+1)
#define INVALID_FACE (FACE_LIMIT+1)
#define INVALID_OBJECT (OBJECT_LIMIT+1)
#define INVALID_TEXTURE (TEXTURE_LIMIT+1)
#define CAMERA_ROTATION_SPEED 0.04f
#define CAMERA_TRANSLATION_SPEED 0.04f
#define SELECTION_SPHERE_RADIUS 32
/* should be an odd number */
#define SNAP_LINES_SHOW 7
#define SNAP_LINES_WIDTH 1.0f
#define SNAP_LINES_COLOR ((Color){200,200,200,150})
typedef struct Operation {
enum {
OPERATION_MOVE_POINT,
OPERATION_SET_TEXTURE,
OPERATION_TRIANGULATE,
} kind;
union {
struct {
uint16_t point;
int16_t delta_x;
int16_t delta_y;
int16_t delta_z;
uint8_t object;
} move_point;
struct {
uint16_t face;
int16_t delta_texture;
uint8_t object;
} set_texture;
struct {
uint16_t old_face;
uint16_t new_face;
uint8_t object;
} triangulate;
} data;
bool chained;
} Operation;
typedef struct Point {
int16_t x, y, z;
} Point;
/* TODO: store topology in terms on edge connections? might be bad, as it's stateful */
/* triangles have p3 = INVALID_POINT */
/* lines have p2, p3 = INVALID_POINT */
/* billboards have p1, p2, p3 = INVALID_POINT */
typedef struct Face {
uint16_t p[4];
/* texture origin, as point on face plane, in absolute coordinates */
int16_t tex_x, tex_y;
uint8_t texture;
uint8_t tex_scale;
} Face;
typedef struct Object {
char *name;
bool is_invisible;
Point position;
char *textures[TEXTURE_LIMIT + 1];
uint8_t textures_sz;
Point rotation;
Face faces[FACE_LIMIT];
uint16_t faces_sz;
} Object;
typedef struct State {
Operation op_stack[UNDO_STACK_SIZE];
uint32_t op_stack_ptr;
bool op_active;
Vec3 active_center;
Vec3 camera_position;
Vec3 camera_direction;
float camera_zoom;
bool camera_is_orthographic;
/* defaults to wireframe */
bool solid_display_mode;
/* positions skipped */
uint8_t grid_snap_granularity;
Point points[POINT_LIMIT];
uint16_t points_sz;
Object objects[OBJECT_LIMIT];
uint8_t objects_sz;
/* which axes are blocked and which are not */
/* order: x, y, z */
bool axis_mask[3];
char *current_texture;
uint8_t current_hovered_obj;
uint16_t current_hovered_face;
} State;
#endif

972
apps/tools/twndel/tool.c Normal file
View File

@ -0,0 +1,972 @@
#include "twn_game_api.h"
#include "state.h"
#include "twn_vec.h"
#include <SDL.h>
/* planned features: */
/* grid-based, bounded space (65536 / POINTS_PER_METER meters), which allows for efficient storage */
/* 65534 point limit, 16 object limit, 2048 faces per object, 32 textures per object */
/* triangles and quads only */
/* support for billboards and flat two sided quads */
/* texture painting */
/* bones with mesh animations, snapping to grid, with no weights */
/* billboard render to specified angles */
/* 1 point light primitive lighting */
/* live edited textures are capped at 128x128 */
/* assumptions: */
/* up is always (0,1,0) */
/* preallocations everywhere */
static State state;
static bool init;
static uint8_t new_object(const char *name) {
if (state.objects_sz >= OBJECT_LIMIT)
return INVALID_OBJECT;
state.objects_sz++;
state.objects[state.objects_sz-1].name = SDL_strdup(name);
return state.objects_sz-1;
}
static uint16_t new_point(int16_t x, int16_t y, int16_t z) {
if (state.points_sz >= POINT_LIMIT)
return INVALID_POINT;
state.points_sz++;
state.points[state.points_sz-1] = (Point){x, y, z};
return state.points_sz-1;
}
static uint16_t push_face(uint8_t object,
uint16_t p0, uint16_t p1, uint16_t p2, uint16_t p3,
uint8_t texture, uint8_t tex_scale, int16_t tex_x, int16_t tex_y)
{
Object *o = &state.objects[object];
o->faces_sz++;
o->faces[o->faces_sz-1] = (Face) {
.p = {p0, p1, p2, p3},
.texture = texture,
.tex_scale = tex_scale,
.tex_x = tex_x,
.tex_y = tex_y,
};
return o->faces_sz-1;
}
static uint8_t push_texture(uint8_t object, char *texture) {
Object *o = &state.objects[object];
/* check whether it's already here */
for (uint8_t i = 0; i < o->textures_sz; ++i)
if (SDL_strcmp(o->textures[i], texture) == 0)
return i;
o->textures_sz++;
o->textures[o->textures_sz-1] = SDL_strdup(texture);
return o->textures_sz-1;
}
/* TODO: use tombstones instead? it would be easier to maintain, by a lot */
/* note: make sure nothing depends on none */
static void pop_face(uint8_t object, uint16_t face) {
Object *o = &state.objects[object];
if (face != o->faces_sz-1)
o->faces[face] = o->faces[o->faces_sz-1];
o->faces_sz--;
}
static void push_operation(Operation operation, bool active) {
state.op_stack_ptr++;
uint8_t op = state.op_stack_ptr % UNDO_STACK_SIZE;
state.op_stack[op] = operation;
state.op_active = active;
}
static void extend_operation(Operation operation) {
uint8_t op = state.op_stack_ptr % UNDO_STACK_SIZE;
Operation ext = state.op_stack[op];
ext.chained = true;
state.op_stack[op] = operation;
push_operation(ext, state.op_active);
}
static inline Vec3 point_to_vec3(uint8_t object, uint16_t point) {
Object *o = &state.objects[object];
return (Vec3){ (o->position.x + state.points[point].x) / (float)POINTS_PER_METER,
(o->position.y + state.points[point].y) / (float)POINTS_PER_METER,
(o->position.z + state.points[point].z) / (float)POINTS_PER_METER };
}
/* TODO: something is wrong, figure out how to introduce rotation to this. */
static inline Vec2 project_texture_coordinate(Vec2 origin, Vec3 plane, Vec3 point, float scale) {
Vec3 right = vec3_norm(vec3_cross(plane, fabsf(0.0f - plane.y) < 1e-9f ? (Vec3){0,1,0} : (Vec3){1,0,0}));
Vec3 up = vec3_norm(vec3_cross(right, plane));
Vec2 pp = { vec3_dot(point, right), vec3_dot(point, up) };
return vec2_scale(vec2_sub(origin, pp), scale);
}
static void render_object(uint8_t object) {
Object *o = &state.objects[object];
if (o->is_invisible)
return;
for (uint16_t fi = 0; fi < o->faces_sz; ++fi) {
Face *f = &o->faces[fi];
if (f->p[2] != INVALID_POINT) {
Vec3 p0 = point_to_vec3(object, f->p[0]);
Vec3 p1 = point_to_vec3(object, f->p[1]);
Vec3 p2 = point_to_vec3(object, f->p[2]);
if (state.solid_display_mode) {
Vec3 n = vec3_norm(vec3_cross(vec3_sub(p1, p0), vec3_sub(p2, p0)));
Vec2 to = { f->tex_x / (float)POINTS_PER_METER, f->tex_y / (float)POINTS_PER_METER, };
Vec2 tul = project_texture_coordinate(to, n, p0, (float)f->tex_scale);
Vec2 tdl = project_texture_coordinate(to, n, p1, (float)f->tex_scale);
Vec2 tdr = project_texture_coordinate(to, n, p2, (float)f->tex_scale);
draw_triangle(o->textures[f->texture],
p0, p1, p2,
tul, tdl, tdr,
(Color){255,255,255,255},
(Color){255,255,255,255},
(Color){255,255,255,255});
if (f->p[3] != INVALID_POINT) {
Vec3 p3 = point_to_vec3(object, f->p[3]);
Vec2 tur = project_texture_coordinate(to, n, p3, (float)f->tex_scale);
draw_triangle(o->textures[f->texture],
p2, p3, p0,
tdr, tur, tul,
(Color){255,255,255,255},
(Color){255,255,255,255},
(Color){255,255,255,255});
}
} else {
draw_line_3d(p0, p1, 1, (Color){255,255,255,255});
draw_line_3d(p1, p2, 1, (Color){255,255,255,255});
if (f->p[3] == INVALID_POINT)
draw_line_3d(p2, p0, 1, (Color){255,255,255,255});
else {
Vec3 p3 = point_to_vec3(object, f->p[3]);
draw_line_3d(p2, p3, 1, (Color){255,255,255,255});
draw_line_3d(p3, p0, 1, (Color){255,255,255,255});
}
}
} else
SDL_assert_always(false);
}
}
static uint8_t new_cube(Point pos, Point size) {
uint8_t object = new_object("cube");
uint16_t p0 = new_point(pos.x - size.x / 2, pos.y - size.y / 2, pos.z - size.z / 2);
uint16_t p1 = new_point(pos.x - size.x / 2, pos.y + size.y / 2, pos.z - size.z / 2);
uint16_t p2 = new_point(pos.x + size.x / 2, pos.y + size.y / 2, pos.z - size.z / 2);
uint16_t p3 = new_point(pos.x + size.x / 2, pos.y - size.y / 2, pos.z - size.z / 2);
uint16_t p4 = new_point(pos.x - size.x / 2, pos.y - size.y / 2, pos.z + size.z / 2);
uint16_t p5 = new_point(pos.x - size.x / 2, pos.y + size.y / 2, pos.z + size.z / 2);
uint16_t p6 = new_point(pos.x + size.x / 2, pos.y + size.y / 2, pos.z + size.z / 2);
uint16_t p7 = new_point(pos.x + size.x / 2, pos.y - size.y / 2, pos.z + size.z / 2);
uint8_t tex = push_texture(object, "/data/placeholder.png");
push_face(object, p2, p3, p0, p1, tex, 128, pos.x - size.x / 2, pos.y - size.y / 2);
push_face(object, p5, p4, p7, p6, tex, 128, pos.x - size.x / 2, pos.y - size.y / 2);
push_face(object, p1, p0, p4, p5, tex, 128, pos.x - size.x / 2, pos.y - size.y / 2);
push_face(object, p6, p7, p3, p2, tex, 128, pos.x - size.x / 2, pos.y - size.y / 2);
push_face(object, p2, p1, p5, p6, tex, 128, pos.x - size.x / 2, pos.y - size.y / 2);
push_face(object, p0, p3, p7, p4, tex, 128, pos.x - size.x / 2, pos.y - size.y / 2);
return object;
}
static void process_camera_rotation(void) {
float horizontal_rotation = 0;
float vertical_rotation = 0;
if (input_action_pressed("camera_rotate_left"))
horizontal_rotation -= CAMERA_ROTATION_SPEED;
if (input_action_pressed("camera_rotate_right"))
horizontal_rotation += CAMERA_ROTATION_SPEED;
if (input_action_pressed("camera_rotate_up"))
vertical_rotation -= CAMERA_ROTATION_SPEED;
if (input_action_pressed("camera_rotate_down"))
vertical_rotation += CAMERA_ROTATION_SPEED;
Vec3 front = vec3_cross(state.camera_direction, (Vec3){0,1,0});
Vec3 local_position = vec3_sub(state.active_center, state.camera_position);
Vec3 new_local_position = vec3_rotate(local_position, horizontal_rotation, (Vec3){0,1,0});
state.camera_direction = vec3_rotate(state.camera_direction, horizontal_rotation, (Vec3){0,1,0});
Vec3 new_rot = vec3_rotate(state.camera_direction, vertical_rotation, front);
/* only apply if it's in limits */
float d = vec3_dot(new_rot, (Vec3){0,-1,0});
if (fabsf(d) <= 0.999f) {
new_local_position = vec3_rotate(new_local_position, vertical_rotation, front);
state.camera_direction = new_rot;
}
state.camera_position = vec3_sub(state.active_center, new_local_position);
}
static void process_camera_translation(void) {
Vec3 right = vec3_norm(vec3_cross(state.camera_direction, (Vec3){0,1,0}));
Vec3 up = vec3_norm(vec3_cross(state.camera_direction, right));
Vec3 was = state.camera_position;
if (input_action_pressed("camera_rotate_left"))
state.camera_position = vec3_sub(state.camera_position, vec3_scale(right, CAMERA_TRANSLATION_SPEED));
if (input_action_pressed("camera_rotate_right"))
state.camera_position = vec3_add(state.camera_position, vec3_scale(right, CAMERA_TRANSLATION_SPEED));
if (input_action_pressed("camera_rotate_up"))
state.camera_position = vec3_sub(state.camera_position, vec3_scale(up, CAMERA_TRANSLATION_SPEED));
if (input_action_pressed("camera_rotate_down"))
state.camera_position = vec3_add(state.camera_position, vec3_scale(up, CAMERA_TRANSLATION_SPEED));
state.active_center = vec3_add(state.active_center, vec3_sub(state.camera_position, was));
draw_billboard("/data/camera.png",
vec3_add(state.camera_position, vec3_scale(state.camera_direction, vec3_length(state.camera_position))),
(Vec2){0.2f,0.2f},
(Rect){0},
(Color){255,255,255,255},
false);
/* show relation to origin */
draw_billboard("/data/center.png",
(Vec3){0},
(Vec2){0.1f,0.1f},
(Rect){0},
(Color){255,255,255,255},
false);
draw_line_3d((Vec3){0}, state.active_center, 1, (Color){255,255,255,255});
}
static void process_camera_movement(void) {
input_action("camera_rotate_left", "A");
input_action("camera_rotate_right", "D");
input_action("camera_rotate_up", "W");
input_action("camera_rotate_down", "S");
input_action("camera_lock_rotation", "SPACE");
if (input_action_pressed("camera_lock_rotation")) {
process_camera_translation();
} else {
process_camera_rotation();
}
}
static inline DrawCameraUnprojectResult unproject_point(Vec2 point) {
return draw_camera_unproject(
point,
state.camera_position,
state.camera_direction,
(Vec3){0, 1, 0},
state.camera_is_orthographic ? 0 : CAMERA_FOV,
state.camera_zoom,
100 );
}
static bool find_closest_point(uint8_t* object_result, uint16_t *point_result) {
DrawCameraUnprojectResult pos_and_ray = unproject_point(ctx.mouse_position);
/* step over every selectable object and find points closest to the view ray */
/* by constructing triangles and finding their height, from perpendicular */
uint16_t closest_point = INVALID_POINT;
uint8_t closest_obj = INVALID_OBJECT;
float closest_distance = INFINITY;
for (uint8_t obj = 0; obj < state.objects_sz; ++obj) {
Object *o = &state.objects[obj];
if (o->is_invisible)
continue;
/* TODO: is it possible to skip repeated points? does it matter? */
/* as we limit the point could we could actually have bool array preallocated for this */
for (uint16_t fi = 0; fi < o->faces_sz; ++fi) {
Face *f = &o->faces[fi];
for (uint16_t pi = 0; pi < 4; ++pi) {
if (f->p[pi] == INVALID_POINT) break;
Vec3 p = point_to_vec3(obj, f->p[pi]);
Vec3 d = vec3_sub(pos_and_ray.position, p);
Vec3 b = vec3_cross(d, pos_and_ray.direction);
float ray_dist = vec3_length(b);
if (ray_dist > ((float)SELECTION_SPHERE_RADIUS / POINTS_PER_METER))
continue;
float dist = vec3_length(vec3_sub(pos_and_ray.position, vec3_add(p, b)));
if (dist < closest_distance) {
closest_distance = dist;
closest_obj = obj;
closest_point = f->p[pi];
}
}
}
}
if (closest_point == INVALID_POINT)
return false;
if (object_result)
*object_result = closest_obj;
if (point_result)
*point_result = closest_point;
return true;
}
/* o = vector origin */
/* v = vector direction, normalized */
/* p = any point on plane */
/* n = normal of a plane */
static bool vector_plane_intersection(Vec3 o, Vec3 v, Vec3 p, Vec3 n, Vec3 *out) {
float dot = vec3_dot(n, v);
if (fabsf(dot) > FLT_EPSILON) {
Vec3 w = vec3_sub(o, p);
float fac = -vec3_dot(n, w) / dot;
*out = vec3_add(o, vec3_scale(v, fac));
return true;
}
/* vector and plane are perpendicular, assume that it lies exactly on it */
return false;
}
static bool find_closest_face(uint8_t* object_result, uint16_t *face_result) {
DrawCameraUnprojectResult cam = unproject_point(ctx.mouse_position);
uint8_t closest_obj = INVALID_OBJECT;
uint16_t closest_face = INVALID_FACE;
float closest_distance = INFINITY;
for (uint8_t oi = 0; oi < state.objects_sz; ++oi) {
Object *o = &state.objects[oi];
for (uint16_t fi = 0; fi < o->faces_sz; ++fi) {
Face *f = &o->faces[fi];
if (f->p[1] == INVALID_POINT) continue;
Vec3 p0 = point_to_vec3(oi, f->p[0]);
Vec3 p1 = point_to_vec3(oi, f->p[1]);
Vec3 p2 = point_to_vec3(oi, f->p[2]);
Vec3 n = vec3_norm(vec3_cross(vec3_sub(p1, p0), vec3_sub(p2, p0)));
/* culling */
if (vec3_dot(state.camera_direction, n) >= 0) continue;
Vec3 i;
if (!vector_plane_intersection(cam.position, cam.direction, p0, n, &i))
continue;
float dist = vec3_length(vec3_sub(p0, i));
if (dist >= closest_distance) continue;
/* left normals are used to determine whether point lies to the left for all forming lines */
Vec3 ln0 = vec3_norm(vec3_cross(n, vec3_sub(p1, p0)));
if (vec3_dot(ln0, vec3_sub(p0, i)) >= 0) continue;
Vec3 ln1 = vec3_norm(vec3_cross(n, vec3_sub(p2, p1)));
if (vec3_dot(ln1, vec3_sub(p1, i)) >= 0) continue;
if (f->p[3] == INVALID_POINT) {
/* triangle */
Vec3 ln2 = vec3_norm(vec3_cross(n, vec3_sub(p0, p2)));
if (vec3_dot(ln2, vec3_sub(p2, i)) >= 0) continue;
} else {
/* quad */
Vec3 p3 = point_to_vec3(oi, f->p[3]);
Vec3 ln2 = vec3_norm(vec3_cross(n, vec3_sub(p3, p2)));
if (vec3_dot(ln2, vec3_sub(p2, i)) >= 0) continue;
Vec3 ln3 = vec3_norm(vec3_cross(n, vec3_sub(p0, p3)));
if (vec3_dot(ln3, vec3_sub(p3, i)) >= 0) continue;
}
closest_distance = dist;
closest_face = fi;
closest_obj = oi;
}
}
if (closest_face == INVALID_FACE)
return false;
if (object_result)
*object_result = closest_obj;
if (face_result)
*face_result = closest_face;
return true;
}
static void show_snap_lines(Vec3 p) {
float step = 1.0f / ((float)POINTS_PER_METER / state.grid_snap_granularity);
int const lines_per_side = (SNAP_LINES_SHOW - 1) / 2;
for (int l = -lines_per_side; l <= lines_per_side; ++l) {
if (!state.axis_mask[0]) {
Vec3 c = vec3_add(p, vec3_scale((Vec3){1,0,0}, step * (float)l));
draw_line_3d(
vec3_add(c, vec3_scale((Vec3){0,0,1}, SNAP_LINES_WIDTH)),
vec3_add(c, vec3_scale((Vec3){0,0,1}, -SNAP_LINES_WIDTH)),
1,
SNAP_LINES_COLOR);
}
if (!state.axis_mask[1]) {
Vec3 axis = fabsf(vec3_dot(state.camera_direction, (Vec3){0,0,1})) >= 0.5f ? (Vec3){1,0,0} : (Vec3){0,0,1};
Vec3 c = vec3_add(p, vec3_scale((Vec3){0,1,0}, step * (float)l));
draw_line_3d(
vec3_add(c, vec3_scale(axis, SNAP_LINES_WIDTH)),
vec3_add(c, vec3_scale(axis, -SNAP_LINES_WIDTH)),
1,
SNAP_LINES_COLOR);
}
if (!state.axis_mask[2]) {
Vec3 c = vec3_add(p, vec3_scale((Vec3){0,0,1}, step * (float)l));
draw_line_3d(
vec3_add(c, vec3_scale((Vec3){1,0,0}, SNAP_LINES_WIDTH)),
vec3_add(c, vec3_scale((Vec3){1,0,0}, -SNAP_LINES_WIDTH)),
1,
SNAP_LINES_COLOR);
}
}
}
static bool process_operation_move_point(Operation *op) {
/* finish dragging around */
/* TODO: dont keep empty ops on stack? */
if (input_action_just_released("select")) {
state.op_active = false;
return false;
}
float size = sinf(ctx.frame_number / 10) * 0.05f + 0.05f;
draw_billboard("/data/grab.png",
point_to_vec3(op->data.move_point.object, op->data.move_point.point),
(Vec2){size, size},
(Rect){0},
(Color){255,255,255,255},
false);
DrawCameraUnprojectResult cam = unproject_point(ctx.mouse_position);
Vec3 p = point_to_vec3(op->data.move_point.object, op->data.move_point.point);
bool point_moved = false;
/* figure out which planes are angled acutely against the viewing direction */
bool y_closer_than_horizon = fabsf(vec3_dot(state.camera_direction, (Vec3){0,1,0})) >= 0.5f;
Vec3 plane_normal_x = y_closer_than_horizon ? (Vec3){0,1,0} : (Vec3){0,0,1};
Vec3 plane_normal_z = y_closer_than_horizon ? (Vec3){0,1,0} : (Vec3){1,0,0};
/* show snapping in lines */
show_snap_lines(p);
Vec3 s;
if (!state.axis_mask[0]) {
if (vector_plane_intersection(cam.position, cam.direction, p, plane_normal_x, &s)) {
int16_t xch = (int16_t)(roundf(vec3_dot(vec3_sub(s, p), (Vec3){1,0,0}) * (float)POINTS_PER_METER));
xch -= xch % state.grid_snap_granularity;
state.points[op->data.move_point.point].x += xch;
op->data.move_point.delta_x += xch;
if (xch != 0) point_moved = true;
}
}
if (!state.axis_mask[1]) {
if (vector_plane_intersection(cam.position, cam.direction, p, (Vec3){0,0,1}, &s)) {
int16_t ych = (int16_t)(roundf(vec3_dot(vec3_sub(s, p), (Vec3){0,1,0}) * (float)POINTS_PER_METER));
ych -= ych % state.grid_snap_granularity;
state.points[op->data.move_point.point].y += ych;
op->data.move_point.delta_y += ych;
if (ych != 0) point_moved = true;
}
}
if (!state.axis_mask[2]) {
if (vector_plane_intersection(cam.position, cam.direction, p, plane_normal_z, &s)) {
int16_t zch = (int16_t)(roundf(vec3_dot(vec3_sub(s, p), (Vec3){0,0,1}) * (float)POINTS_PER_METER));
zch -= zch % state.grid_snap_granularity;
state.points[op->data.move_point.point].z += zch;
op->data.move_point.delta_z += zch;
if (zch != 0) point_moved = true;
}
}
if (point_moved) {
audio_play("/data/bong.ogg", NULL, false, 0.12f, 0.0f);
return true;
}
return false;
}
static void reverse_operation_move_point(Operation *op) {
SDL_assert(op->kind == OPERATION_MOVE_POINT);
state.points[op->data.move_point.point].x -= op->data.move_point.delta_x;
state.points[op->data.move_point.point].y -= op->data.move_point.delta_y;
state.points[op->data.move_point.point].z -= op->data.move_point.delta_z;
audio_play("/data/drop.ogg", NULL, false, 0.4f, 0.0f);
}
static void reverse_operation_set_texture(Operation *op) {
SDL_assert(op->kind == OPERATION_SET_TEXTURE);
Object *o = &state.objects[op->data.set_texture.object];
o->faces[op->data.set_texture.face].texture += op->data.set_texture.delta_texture;
audio_play("/data/drop.ogg", NULL, false, 0.4f, 0.0f);
}
static void reverse_triangulation(Operation *op) {
SDL_assert(op->kind == OPERATION_TRIANGULATE);
Object *o = &state.objects[op->data.set_texture.object];
Face *fn = &o->faces[op->data.triangulate.new_face];
Face *fo = &o->faces[op->data.triangulate.old_face];
fo->p[3] = fo->p[2];
fo->p[2] = fn->p[1];
pop_face(op->data.set_texture.object, op->data.triangulate.new_face);
}
/* TODO: reverse of this */
static void try_subdividing_from_moving(uint8_t object, uint16_t point) {
Object *o = &state.objects[object];
bool not_first = false;
for (uint16_t fi = 0; fi < o->faces_sz; ++fi) {
Face *f = &o->faces[fi];
if (f->p[3] == INVALID_POINT) continue;
for (uint16_t pi = 0; pi < 4; ++pi) {
if (f->p[pi] == point) {
Face new0 = *f;
new0.p[0] = f->p[pi];
new0.p[1] = f->p[(pi + 1) % 4];
new0.p[2] = f->p[(pi + 3) % 4];
new0.p[3] = INVALID_POINT;
uint16_t newf = push_face(
object,
f->p[(pi + 1) % 4],
f->p[(pi + 2) % 4],
f->p[(pi + 3) % 4],
INVALID_POINT,
f->texture,
f->tex_scale,
f->tex_x,
f->tex_y);
*f = new0;
extend_operation((Operation){
.kind = OPERATION_TRIANGULATE,
.data = { .triangulate = { .new_face = newf, .old_face = fi, .object = object} },
.chained = not_first,
});
not_first = true;
}
}
}
}
static void process_undo(void) {
if (state.op_active)
state.op_active = false;
/* TODO: checks and defined limit */
Operation *op = &state.op_stack[state.op_stack_ptr % UNDO_STACK_SIZE];
state.op_stack_ptr--;
switch (op->kind) {
case OPERATION_MOVE_POINT: {
reverse_operation_move_point(op);
break;
}
case OPERATION_SET_TEXTURE: {
reverse_operation_set_texture(op);
break;
}
case OPERATION_TRIANGULATE: {
reverse_triangulation(op);
break;
}
default:
(void)0;
}
/* pop another if they're chained together */
if (op->chained) process_undo();
}
static void process_operations(void) {
if (input_action_just_pressed("undo"))
process_undo();
if (!state.op_active) {
/* point dragging */
if (!state.current_texture) {
uint16_t point_select; uint8_t obj_select;
if (find_closest_point(&obj_select, &point_select)) {
draw_billboard("/data/point.png",
point_to_vec3(obj_select, point_select),
(Vec2){0.05f, 0.05f},
(Rect){0},
(Color){255,255,255,255},
false);
if (input_action_just_pressed("select"))
push_operation((Operation){
.kind = OPERATION_MOVE_POINT,
.data = { .move_point = { .point = point_select, .object = obj_select } },
}, true );
}
/* texture setting */
} else {
uint8_t obj_select; uint16_t face_select;
if (find_closest_face(&obj_select, &face_select)) {
state.current_hovered_face = face_select;
state.current_hovered_obj = obj_select;
if (input_action_just_pressed("rotate")) {
Face *f = &state.objects[obj_select].faces[face_select];
int16_t tex_x = f->tex_x;
int16_t tex_y = f->tex_y;
f->tex_x = -tex_y;
f->tex_y = tex_x;
}
if (input_action_pressed("select") && state.current_texture) {
uint8_t new_tex = push_texture(obj_select, state.current_texture);
uint8_t cur_tex = state.objects[obj_select].faces[face_select].texture;
if (new_tex != cur_tex) {
state.objects[obj_select].faces[face_select].texture = new_tex;
audio_play("/data/bong.ogg", NULL, false, 0.5f, 0.0f);
push_operation((Operation){
.kind = OPERATION_SET_TEXTURE,
.data = { .set_texture = {
.face = face_select,
.object = obj_select,
.delta_texture = cur_tex - new_tex,
}},
}, false );
}
}
}
}
}
if (state.op_active) {
Operation *op = &state.op_stack[state.op_stack_ptr % UNDO_STACK_SIZE];
switch (op->kind) {
case OPERATION_MOVE_POINT: {
bool update = process_operation_move_point(op);
if (update)
try_subdividing_from_moving(op->data.move_point.object, op->data.move_point.point);
break;
}
case OPERATION_SET_TEXTURE:
case OPERATION_TRIANGULATE:
default:
(void)0;
}
}
}
static void draw_axes(void) {
/* axis helpers */
/* idea: double selection of axes for diagonal edits */
draw_line_3d(state.active_center, (Vec3){256,0,0}, 1, state.axis_mask[0] ? (Color){0,0,0,75} : (Color){255,0,0,125});
draw_billboard("/data/x.png",
vec3_add(state.active_center, (Vec3){2,0,0}),
(Vec2){0.1f, 0.1f},
(Rect){0},
state.axis_mask[0] ? (Color){0,0,0,255} : (Color){255,0,0,255},
false);
draw_line_3d(state.active_center, (Vec3){0,256,0}, 1, state.axis_mask[1] ? (Color){0,0,0,75} : (Color){75,125,25,125});
draw_billboard("/data/y.png",
vec3_add(state.active_center, (Vec3){0,1.5f,0}),
(Vec2){0.1f, 0.1f},
(Rect){0},
state.axis_mask[1] ? (Color){0,0,0,255} : (Color){75,125,25,255},
false);
draw_line_3d(state.active_center, (Vec3){0,0,256}, 1, state.axis_mask[2] ? (Color){0,0,0,75} : (Color){0,0,255,125});
draw_billboard("/data/z.png",
vec3_add(state.active_center, (Vec3){0,0,2}),
(Vec2){0.1f, 0.1f},
(Rect){0},
state.axis_mask[2] ? (Color){0,0,0,255} : (Color){0,0,255,255},
false);
}
static void draw_hovered_face_border(void) {
if (state.current_hovered_obj == INVALID_OBJECT || state.current_hovered_face == INVALID_FACE)
return;
Object *o = &state.objects[state.current_hovered_obj];
Face *f = &o->faces[state.current_hovered_face];
if (f->p[3] != INVALID_POINT) {
Vec3 p0 = point_to_vec3(state.current_hovered_obj, f->p[0]);
Vec3 p1 = point_to_vec3(state.current_hovered_obj, f->p[1]);
Vec3 p2 = point_to_vec3(state.current_hovered_obj, f->p[2]);
Vec3 p3 = point_to_vec3(state.current_hovered_obj, f->p[3]);
Vec3 center = vec3_scale(vec3_add(p2, p0), 0.5);
float size = sinf(ctx.frame_number / 10) * 0.05f + 0.1f;
Vec3 c0 = vec3_add(p0, vec3_scale(vec3_sub(p0, center), size));
Vec3 c1 = vec3_add(p1, vec3_scale(vec3_sub(p1, center), size));
Vec3 c2 = vec3_add(p2, vec3_scale(vec3_sub(p2, center), size));
Vec3 c3 = vec3_add(p3, vec3_scale(vec3_sub(p3, center), size));
draw_line_3d(c0, c1, 1, (Color){255,255,255,255});
draw_line_3d(c1, c2, 1, (Color){255,255,255,255});
draw_line_3d(c2, c3, 1, (Color){255,255,255,255});
draw_line_3d(c3, c0, 1, (Color){255,255,255,255});
} else {
Vec3 p0 = point_to_vec3(state.current_hovered_obj, f->p[0]);
Vec3 p1 = point_to_vec3(state.current_hovered_obj, f->p[1]);
Vec3 p2 = point_to_vec3(state.current_hovered_obj, f->p[2]);
Vec3 center = vec3_scale(vec3_add(p0, vec3_add(p1, p2)), 0.33f);
float size = sinf(ctx.frame_number / 10) * 0.05f + 0.1f;
Vec3 c0 = vec3_add(p0, vec3_scale(vec3_sub(p0, center), size));
Vec3 c1 = vec3_add(p1, vec3_scale(vec3_sub(p1, center), size));
Vec3 c2 = vec3_add(p2, vec3_scale(vec3_sub(p2, center), size));
draw_line_3d(c0, c1, 1, (Color){255,255,255,255});
draw_line_3d(c1, c2, 1, (Color){255,255,255,255});
draw_line_3d(c2, c0, 1, (Color){255,255,255,255});
}
}
static void display_texture_selection(void) {
String list = file_read("/data/assets/", ":images");
if (!list.data)
return;
draw_rectangle((Rect){0, 480 - 50, 640, 480}, (Color){0,0,0,126});
char *selected = NULL;
char *saveptr = NULL;
int count = 0;
char const *part = SDL_strtokr(list.data, "\n", &saveptr);
do {
Rect box = (Rect){(float)count * 50, 480 - 48, 48, 48};
draw_sprite(part,
box,
(Rect){0,0,64,64},
(Color){255,255,255,255},
0, false, false, true);
count++;
if (state.current_texture && SDL_strcmp(part, state.current_texture) == 0)
draw_box(box, 1, (Color){255,255,255,255});
if (rect_intersects(box, (Rect){ctx.mouse_position.x, ctx.mouse_position.y, 1, 1}))
selected = SDL_strdup(part);
} while ((part = SDL_strtokr(NULL, "\n", &saveptr)));
if (selected) {
draw_text(selected, (Vec2){0, 480 - 48 - 24}, 24, (Color){255,255,255,255}, NULL);
if (input_action_just_pressed("select")) {
if (state.current_texture) SDL_free(state.current_texture);
state.current_texture = selected;
} else
SDL_free(selected);
}
}
static void process_camera_inputs(void) {
if (input_action_just_pressed("toggle_display_mode")) {
audio_play("/data/click.wav", NULL, false, 0.7f, 0.0f);
state.solid_display_mode = !state.solid_display_mode;
if (state.current_texture) {
SDL_free(state.current_texture);
state.current_texture = NULL;
}
}
if (input_action_just_pressed("toggle_projection")) {
audio_play("/data/pop.wav", NULL, false, 0.8f, 0.0f);
state.camera_is_orthographic = !state.camera_is_orthographic;
}
if (input_action_just_pressed("toggle_x_axis")) {
state.axis_mask[0] = 0; state.axis_mask[1] = 1; state.axis_mask[2] = 1;
}
if (input_action_just_pressed("toggle_y_axis")) {
state.axis_mask[0] = 1; state.axis_mask[1] = 0; state.axis_mask[2] = 1;
}
if (input_action_just_pressed("toggle_z_axis")) {
state.axis_mask[0] = 1; state.axis_mask[1] = 1; state.axis_mask[2] = 0;
}
/* TODO: determine bounding box for this? */
/* TODO: no idea whether it's all correct in terms of directions. */
if (input_action_just_pressed("camera_front")) {
audio_play("/data/pop.wav", NULL, false, 0.8f, 0.0f);
state.camera_is_orthographic = true;
state.camera_direction = (Vec3){0,0,-1};
state.camera_position = vec3_add(state.active_center, (Vec3){0,0,2});
}
if (input_action_just_pressed("camera_back")) {
audio_play("/data/pop.wav", NULL, false, 0.8f, 0.0f);
state.camera_is_orthographic = true;
state.camera_direction = (Vec3){0,0,1};
state.camera_position = vec3_add(state.active_center, (Vec3){0,0,-2});
}
if (input_action_just_pressed("camera_left")) {
audio_play("/data/pop.wav", NULL, false, 0.8f, 0.0f);
state.camera_is_orthographic = true;
state.camera_direction = (Vec3){1,0,0};
state.camera_position = vec3_add(state.active_center, (Vec3){-2,0,0});
}
if (input_action_just_pressed("camera_right")) {
audio_play("/data/pop.wav", NULL, false, 0.8f, 0.0f);
state.camera_is_orthographic = true;
state.camera_direction = (Vec3){-1,0,0};
state.camera_position = vec3_add(state.active_center, (Vec3){2,0,0});
}
/* TODO: broken */
if (input_action_just_pressed("camera_above")) {
audio_play("/data/pop.wav", NULL, false, 0.8f, 0.0f);
state.camera_is_orthographic = true;
state.camera_direction = (Vec3){0,-1,0};
state.camera_position = vec3_add(state.active_center, (Vec3){0,2,0});
}
/* TODO: broken */
if (input_action_just_pressed("camera_below")) {
audio_play("/data/pop.wav", NULL, false, 0.8f, 0.0f);
state.camera_is_orthographic = true;
state.camera_direction = (Vec3){0,1,0};
state.camera_position = vec3_add(state.active_center, (Vec3){0,-2,0});
}
}
void game_tick(void) {
if (!init) {
/* default state */
new_cube((Point){0,0,0}, (Point){POINTS_PER_METER,POINTS_PER_METER,POINTS_PER_METER});
state.camera_position = (Vec3){2,1,2};
state.camera_direction = vec3_norm(((Vec3){-2,-1,-2}));
state.camera_zoom = 0.5f;
state.grid_snap_granularity = 16;
state.axis_mask[1] = 1; state.axis_mask[2] = 1;
init = true;
}
state.current_hovered_face = INVALID_FACE;
state.current_hovered_obj = INVALID_OBJECT;
input_action("toggle_display_mode", "Q");
input_action("toggle_projection", "TAB");
input_action("toggle_x_axis", "Z");
input_action("toggle_y_axis", "X");
input_action("toggle_z_axis", "C");
input_action("select", "LCLICK");
input_action("rotate", "R");
input_action("undo", "F");
input_action("camera_front", "KP0");
input_action("camera_back", "KP1");
input_action("camera_left", "KP2");
input_action("camera_right", "KP3");
input_action("camera_above", "KP4");
input_action("camera_below", "KP5");
process_camera_inputs();
process_camera_movement();
process_operations();
/* helpres */
draw_axes();
draw_hovered_face_border();
for (uint8_t obj = 0; obj < state.objects_sz; ++obj)
render_object(obj);
if (state.solid_display_mode)
display_texture_selection();
draw_text("twndel\x03", (Vec2){0, 2}, 32, (Color){255,255,255,200}, NULL);
draw_camera(
state.camera_position,
state.camera_direction,
(Vec3){0, 1, 0},
state.camera_is_orthographic ? 0 : CAMERA_FOV,
state.camera_zoom,
100
);
}
void game_end(void) {
for (uint8_t obj = 0; obj < state.objects_sz; ++obj) {
Object *o = &state.objects[obj];
for (uint8_t t = 0; t < o->textures_sz; ++t)
SDL_free(o->textures[t]);
SDL_free(o->name);
}
if (state.current_texture) {
SDL_free(state.current_texture);
state.current_texture = NULL;
}
}

View File

@ -9,10 +9,6 @@ endif()
add_subdirectory($ENV{TWNROOT} ${CMAKE_BINARY_DIR}/twn) add_subdirectory($ENV{TWNROOT} ${CMAKE_BINARY_DIR}/twn)
set(FLAGS
$<$<NOT:$<BOOL:${TWN_FEATURE_DYNLIB_GAME}>>:--no-dynlib-game>
)
add_custom_command( add_custom_command(
OUTPUT ${CMAKE_CURRENT_SOURCE_DIR}/game.c OUTPUT ${CMAKE_CURRENT_SOURCE_DIR}/game.c
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/bindgen.py $ENV{TWNROOT}/share/twn_api.json ${FLAGS} > ${CMAKE_CURRENT_SOURCE_DIR}/luabind.c COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/bindgen.py $ENV{TWNROOT}/share/twn_api.json ${FLAGS} > ${CMAKE_CURRENT_SOURCE_DIR}/luabind.c

View File

@ -112,6 +112,9 @@ for procedure, procedure_desc in api["procedures"].items():
binding += " lua_pushnumber(L, (float)(%s(%s)));\n" % (procedure, ", ".join(param["name"] for param in procedure_desc["params"])) binding += " lua_pushnumber(L, (float)(%s(%s)));\n" % (procedure, ", ".join(param["name"] for param in procedure_desc["params"]))
elif procedure_desc["return"] == "char *": elif procedure_desc["return"] == "char *":
binding += " lua_pushstring(L, %s(%s));\n" % (procedure, ", ".join(param["name"] for param in procedure_desc["params"])) binding += " lua_pushstring(L, %s(%s));\n" % (procedure, ", ".join(param["name"] for param in procedure_desc["params"]))
elif procedure_desc["return"] == "String":
binding += " String result = %s(%s);\n" % (procedure, ", ".join(param["name"] for param in procedure_desc["params"]))
binding += " lua_pushlstring(L, result.data, (int)result.length);\n"
elif type(procedure_desc["return"]) is dict or procedure_desc["return"] in api["types"]: elif type(procedure_desc["return"]) is dict or procedure_desc["return"] in api["types"]:
type_desc = procedure_desc["return"] if type(procedure_desc["return"]) is dict else api["types"][procedure_desc["return"]] type_desc = procedure_desc["return"] if type(procedure_desc["return"]) is dict else api["types"][procedure_desc["return"]]
binding += " %s result = %s(%s);\n" % (type_desc["c_type"], procedure, ", ".join(param["name"] for param in procedure_desc["params"])) binding += " %s result = %s(%s);\n" % (type_desc["c_type"], procedure, ", ".join(param["name"] for param in procedure_desc["params"]))

View File

@ -1,3 +1,3 @@
[[deps]] [[deps]]
source = "../../common-data" # where does it come from, might be an url source = "../../data" # where does it come from, might be an url
name = "common-data" # should be globally unique name = "common-data" # should be globally unique

View File

@ -29,8 +29,8 @@ def to_lua_type_annot(typedesc):
return "unknown" return "unknown"
# raise BaseException("Unhandled type for annotation: %s" % typedesc) # raise BaseException("Unhandled type for annotation: %s" % typedesc)
print("---@meta twn")
print("---@diagnostic disable") print("---@diagnostic disable")
print("error(\"townengine lua api file is not supposed to be imported!\")")
type_annotations = {} type_annotations = {}
type_annotations["ctx"] = r"{ %s, udata: table }" % \ type_annotations["ctx"] = r"{ %s, udata: table }" % \

View File

@ -26,7 +26,7 @@ static int physfs_loader(lua_State *L) {
static const char *name_breaker = NULL; static const char *name_breaker = NULL;
if (name_breaker && SDL_strcmp(name, name_breaker) == 0) { if (name_breaker && SDL_strcmp(name, name_breaker) == 0) {
log_critical("Recursive load on itself from lua module (%s)", name_breaker); log_string(name_breaker, "Recursive load on itself from lua module");
return 0; return 0;
} name_breaker = name; } name_breaker = name;
@ -40,26 +40,26 @@ static int physfs_loader(lua_State *L) {
SDL_asprintf(&final_path, "/scripts/%s.lua", path_copy); SDL_asprintf(&final_path, "/scripts/%s.lua", path_copy);
SDL_free(path_copy); SDL_free(path_copy);
if (!file_exists(final_path)) { if (SDL_strcmp(file_read(final_path, ":exists").data, "yes") == 0) {
char *error_message = NULL; char *error_message = NULL;
SDL_asprintf(&error_message, "could not find module %s in filesystem", name); SDL_asprintf(&error_message, "could not find module %s in filesystem", name);
lua_pushstring(L, error_message); lua_pushstring(L, error_message);
free(error_message); SDL_free(error_message);
free(final_path); SDL_free(final_path);
return 1; return 1;
} }
unsigned char *buf = NULL; char *final_path_binary = NULL;
int64_t buf_size = file_to_bytes(final_path, &buf); SDL_asprintf(&final_path_binary, "%s%s", final_path, ":binary");
free(final_path);
String file = file_read(final_path, ":binary");
SDL_free(final_path);
/* TODO: use reader interface for streaming instead */ /* TODO: use reader interface for streaming instead */
int const result = luaL_loadbuffer(L, (char *)buf, buf_size, name); int const result = luaL_loadbuffer(L, file.data, (size_t)file.length, name);
free(buf);
if (result != LUA_OK) if (result != LUA_OK)
log_critical("%s", lua_tostring(L, -1)); log_string(lua_tostring(L, -1), NULL);
return result == LUA_OK; return result == LUA_OK;
} }
@ -110,7 +110,7 @@ static void *custom_alloc(void *ud, void *ptr, size_t osize, size_t nsize) {
static void exchange_lua_states(lua_State *from, lua_State *to, int level, int index) { static void exchange_lua_states(lua_State *from, lua_State *to, int level, int index) {
if (level >= UDATA_NESTING_LIMIT) { if (level >= UDATA_NESTING_LIMIT) {
log_critical("ctx.udata nesting limit is reached (%u)", UDATA_NESTING_LIMIT); log_string("ctx.udata nesting limit is reached", NULL);
return; return;
} }
@ -143,7 +143,7 @@ static void exchange_lua_states(lua_State *from, lua_State *to, int level, int i
break; break;
default: default:
/* TODO: provide a path and type of it for better diagnostic */ /* TODO: provide a path and type of it for better diagnostic */
log_warn("Unserializable udata found and is ignored"); log_string("Unserializable udata found and is ignored", NULL);
break; break;
} }
} }
@ -167,7 +167,6 @@ void game_tick(void) {
SDL_assert(!lua_isnoneornil(state->L, -1)); SDL_assert(!lua_isnoneornil(state->L, -1));
SDL_assert(!lua_isnoneornil(state->L, -2)); SDL_assert(!lua_isnoneornil(state->L, -2));
if (!lua_isnoneornil(state->L, -1)) { if (!lua_isnoneornil(state->L, -1)) {
log_info("Exchanging lua states...");
lua_newtable(new_state); lua_newtable(new_state);
exchange_lua_states(state->L, new_state, 0, -1); exchange_lua_states(state->L, new_state, 0, -1);
lua_setfield(new_state, -2, "udata"); lua_setfield(new_state, -2, "udata");
@ -216,24 +215,21 @@ void game_tick(void) {
bindgen_load_twn(state->L); bindgen_load_twn(state->L);
/* now finally get to running the code */ /* now finally get to running the code */
unsigned char *game_buf = NULL; String file = file_read("/scripts/game.lua", ":binary");
size_t game_buf_size = file_to_bytes("/scripts/game.lua", &game_buf);
/* TODO: use reader interface for streaming instead */ /* TODO: use reader interface for streaming instead */
if (luaL_loadbuffer(state->L, (char *)game_buf, game_buf_size, "game.lua") == LUA_OK) { if (luaL_loadbuffer(state->L, file.data, (size_t)file.length, "game.lua") == LUA_OK) {
if (lua_pcall(state->L, 0, 0, 0) != LUA_OK) { if (lua_pcall(state->L, 0, 0, 0) != LUA_OK) {
log_critical("%s", lua_tostring(state->L, -1)); log_string(luaL_tolstring(state->L, -1, NULL), "Error executing /scripts/game.lua entry");
lua_pop(state->L, 1); lua_pop(state->L, 1);
} else } else
state->loaded_successfully = true; state->loaded_successfully = true;
} else { } else {
/* got some sort of error, it should be pushed on top of the stack */ /* got some sort of error, it should be pushed on top of the stack */
SDL_assert(lua_isstring(state->L, -1)); SDL_assert(lua_isstring(state->L, -1));
log_critical("Error loading /scripts/game.lua entry: %s", luaL_tolstring(state->L, -1, NULL)); log_string(luaL_tolstring(state->L, -1, NULL), "Error loading /scripts/game.lua entry");
lua_pop(state->L, 1); lua_pop(state->L, 1);
} }
SDL_free(game_buf);
/* from this point we have access to everything defined in lua */ /* from this point we have access to everything defined in lua */
} }
@ -251,7 +247,7 @@ void game_tick(void) {
lua_getglobal(state->L, "game_tick"); lua_getglobal(state->L, "game_tick");
if (lua_pcall(state->L, 0, 0, 0) != LUA_OK) { if (lua_pcall(state->L, 0, 0, 0) != LUA_OK) {
log_critical("%s", lua_tostring(state->L, -1)); log_string(luaL_tolstring(state->L, -1, NULL), "Error executing game_tick()");
lua_pop(state->L, 1); lua_pop(state->L, 1);
} }

View File

@ -1,6 +1,6 @@
#!/bin/env sh #!/bin/env sh
# single header api generator with clang # single header api generator with clang
set +e set -e
clang -I./ -P -E -nostdinc -nobuiltininc -DTWN_NOT_C $TWNROOT/include/twn_game_api.h 2> /dev/null | clang-format clang -I./ -P -E -nostdinc -nobuiltininc -DTWN_NOT_C $TWNROOT/include/twn_game_api.h 2> /dev/null | clang-format

8
bin/prep-embed.sh Normal file
View File

@ -0,0 +1,8 @@
#!/bin/env sh
set -e
# packages embedded resources with objcopy, so that it is more portable
# ld.lld on windows doesn't recognize --format binary, sadly
objdump=$(objdump -i)
bdfname=$(echo "$objdump" | sed -n 2p)
objcopy -I binary -O "$bdfname" share/assets/Dernyns256.ttf "$CMAKE_CURRENT_BINARY_DIR/font.o"

View File

@ -1,7 +1,7 @@
#!/bin/env sh #!/bin/env sh
# townengine tooling interface # townengine tooling interface
set +e set -e
exe="$(basename $PWD)" exe="$(basename $PWD)"
toolpath="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" toolpath="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")"
@ -57,6 +57,9 @@ case "$1" in
fi fi
;; ;;
devcompl ) (cd "$TWNROOT" && "$toolpath"/twnbuild "--build_dir=$TWNROOT/build" "${@:2}")
;;
* ) echo "Unknown command." * ) echo "Unknown command."
;; ;;
esac esac

View File

@ -5,6 +5,7 @@ from os import getcwd
from os.path import expandvars from os.path import expandvars
from pathlib import Path from pathlib import Path
from sys import argv from sys import argv
from functools import reduce
import tomllib import tomllib
#TODO: support for default pack override #TODO: support for default pack override
@ -16,15 +17,16 @@ has_clang = getoutput("command -v clang") != ""
target_web = "--target=web" in argv target_web = "--target=web" in argv
#TODO: infer what "native" means for current env #TODO: infer what "native" means for current env
build_dir = "build/web" if target_web else "build/native" build_dir_arg = reduce(lambda c, n: c if c.startswith("--build_dir=") else n, argv + [""], "")
build_dir += "/release" if "--release" in argv else "/debug" build_dir = "build/web" if target_web else build_dir_arg.split("=")[1] if build_dir_arg else "build/native"
build_dir += "" if build_dir_arg else "/release" if "--release" in argv else "/debug"
cmake = ["emcmake", "cmake"] if target_web else ["cmake"] cmake = ["emcmake", "cmake"] if target_web else ["cmake"]
# cmake configuration command # cmake configuration command
command = [] command = []
# check whether clang is around (it's just better) # check whether clang is around (it's just better)
if has_clang: if has_clang and not target_web:
command += ["-DCMAKE_C_COMPILER=clang"] command += ["-DCMAKE_C_COMPILER=clang"]
# check whether ninja is around (you better start running) # check whether ninja is around (you better start running)
if has_ninja: if has_ninja:

BIN
data/assets/dirt/1.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
data/assets/dirt/2.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
data/assets/grass2.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
data/assets/grasses/25.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
data/assets/trreez.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
data/music/woah.ogg (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -53,6 +53,7 @@
<li><b>T</b> for townengine; development and apis. <li><b>T</b> for townengine; development and apis.
<li><b>G</b> for gamedev; guides and FAQs on game making. <li><b>G</b> for gamedev; guides and FAQs on game making.
</ul> </ul>
<p>There's no reason not to pile up knowledge, refactoring is irrelevant.
</blockquote> </blockquote>
<p><a name="abi"></a><strong>T1.3 </strong><strong>Procedure Interface</strong> <p><a name="abi"></a><strong>T1.3 </strong><strong>Procedure Interface</strong>
<blockquote> <blockquote>
@ -76,5 +77,21 @@
<li>Returns must be restricted/closed on being constant over frame, parameters or asset frame. <li>Returns must be restricted/closed on being constant over frame, parameters or asset frame.
</ul> </ul>
</blockquote> </blockquote>
<p><a name="dev-on"></a><strong>T1.4 </strong><strong>Developing</strong>
<blockquote>
<p>One of hard choices we employ is requirement on having more or less the same tools on all platforms in order to compile.
If you're on Windows you need to get a MinGW environment. [cmake] and [python3] (>specify versions<) are also required.
[clang] is preferred over [gcc], as it has superior lto, which matters a great deal with our architecture (literal day and night difference in frame time).
[git] is used for version control. Git for Windows distribution could also be used for getting conforming cli environment, much recommended.
<p>Inside $TWNROOT/bin folder the [twn] script is used for engine instrumentation.
It could build and run your projects and even open this very wiki locally!
You could execute [source hooks] inside $TWNROOT to add [twn] to your $PATH temporarily. Symlinking of it is also supported.
Look inside the script to see what it can do, it's ever growing !
<p>[twn init &lt;template&gt; &lt;name&gt;] could be used to quickly initialize a project.
Look into $TWNROOT/apps/templates folder to see available templates.
<p>Support for [clangd] completions is provided with [twn devcompl] command, it generates compile_commands.json inside $TWNROOT/build folder.
Having workspace placed in $TWNROOT by your IDE should be enough to make it all work. Rerun [twn devcompl] if new files are added.
<p>Web target requires [emsdk] as of now. Use [twn build --target=web] to use it.
</blockquote>
</body> </body>
</html> </html>

View File

@ -9,7 +9,7 @@
<a>Awesomeness</a> <a>Awesomeness</a>
<table style="padding-top:1em"> <table style="padding-top:1em">
<tr><td>T1.</strong> <a href="#about-townengine">About Townengnine</a></td> <tr><td>T1.</strong> <a href="#about-townengine">About Townengnine</a></td>
<td>G1.</strong> <a href="#making-2dot5d-shooters">Making 2.5D Shooters</a></td> <td>G1.</strong> <a href="#trigonometry">Trigonometry</a></td>
</tr> </tr>
<tr><td>T2.</strong> <a href="#input-system">Input System</a></td> <tr><td>T2.</strong> <a href="#input-system">Input System</a></td>
</tr> </tr>
@ -21,6 +21,7 @@
<p style="margin:0">T1.1 <a href="about-townengine.html#introduction">Introduction</a></p> <p style="margin:0">T1.1 <a href="about-townengine.html#introduction">Introduction</a></p>
<p style="margin:0">T1.2 <a href="about-townengine.html#wiki">Wiki</a></p> <p style="margin:0">T1.2 <a href="about-townengine.html#wiki">Wiki</a></p>
<p style="margin:0">T1.3 <a href="about-townengine.html#abi">Procedure Interface</a></p> <p style="margin:0">T1.3 <a href="about-townengine.html#abi">Procedure Interface</a></p>
<p style="margin:0">T1.4 <a href="about-townengine.html#dev-on">Developing</a></p>
</blockquote> </blockquote>
<p style="margin-bottom:0"><a name="input-system"></a>T2. </strong><a href="input-system.html">Input System</strong></a></p> <p style="margin-bottom:0"><a name="input-system"></a>T2. </strong><a href="input-system.html">Input System</strong></a></p>
<blockquote style="margin-top:0"> <blockquote style="margin-top:0">
@ -31,7 +32,7 @@
<blockquote style="margin-top:0"> <blockquote style="margin-top:0">
<p style="margin:0">T3.1 <a href="packaging.html#overview">Overview</a></p> <p style="margin:0">T3.1 <a href="packaging.html#overview">Overview</a></p>
</blockquote> </blockquote>
<p style="margin-bottom:0"><a name="making-2dot5d-shooters"></a>G1. </strong><a href="making-2dot5d-shooters.html">Making 2.5D Shooters</strong></a></p> <p style="margin-bottom:0"><a name="trigonometry"></a>G1. </strong><a href="trigonometry.html">Trigonometry</strong></a></p>
<blockquote style="margin-top:0"> <blockquote style="margin-top:0">
</blockquote> </blockquote>
</body> </body>

View File

@ -6,7 +6,7 @@
</head> </head>
<body> <body>
<h1 style="margin-bottom:0in">X1. {About}<span style="float:right">{twn}</span></h1> <h1 style="margin-bottom:0in">X1. {About}<span style="float:right">{twn}</span></h1>
<a href="index.html">Go back <a href="index.html">Go back</a>
<p><a name="{Section}"></a><strong>X1.1 </strong><strong>{Section}</strong> <p><a name="{Section}"></a><strong>X1.1 </strong><strong>{Section}</strong>
<blockquote> <blockquote>
<p>Text <p>Text

View File

@ -0,0 +1,46 @@
<!doctype html>
<html>
<head>
<title>Townengine Wiki : Trigonometry</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1 style="margin-bottom:0in">G1. Trigonometry<span style="float:right">{twn}</span></h1>
<a href="index.html">Go back</a>
<blockquote>
<p>Truth is, most games are played in continuous space, where geometry matters.
Math classes turn out to be not so useless after all.
But even if you don't know your numbers well, you can compose functions by their meaning, and not number logic.
This page will try to help you with that, providing examples.
</blockquote>
<p><strong>the Hell are Vectors</strong>
<blockquote>
<p>There are distinct meanings they posses: <i>position in space</i>, <i>travel between points</i> and <i>direction</i>.
Often you need to juggle those representation around.
<p>For example, player and enemy positions by themselves don't tell you anything about their spatial relation.
For that you need to compute the <i>difference between them</i>, which is plain per component subtraction.
Resulted direction of travel depends on order of inputs, - it will point to position on the right side, the one you subtract with, but not from.
<p>It's important to understand that after subtraction the travel vector doesn't have relation with original positions,
it is effectively in a <i>subspace</i>, where (0,0,0) is at position you subtracted with to get it.
Operation is reversible by means of addition, resulting in position in space once again.
<p>I often forget what the direction is pointed at but here's a quick way to remind yourself: take two numbers, 10 and 0, and try subtracting them in different order.
Sign of result will tell you which part is pointed at, +10 -> direction is to the 10, -10 -> direction is to the 0. (Draw it in your head)
<p>Sometimes direction is more important than the travel, for which you need to drop the length of it.
This is called <i>normalization</i>, where result is called a <i>normal</i> (You probably heard those terms).
This is achieved by dividing every component by the length (magnitude) of vector itself.
Length is computed as <i>sqrt(x*x, y*y, z*z)</i>. This effectively creates a vector of length 1.
As every direction sized the same, when graphed they trace a circle.
</blockquote>
<p><strong>Center between two points</strong>
<blockquote>
<p><b>Method one. </b>Add two positions and divide them by two: (a + b) / 2. This is cheap, but harder to remember.
<p><b>Method two. </b>Find the travel between points, divide it by two and then translate the origin: (a + (a - b) / 2)
</blockquote>
</body>
</html>

View File

@ -1,13 +1,14 @@
#ifndef TWN_AUDIO_H #ifndef TWN_AUDIO_H
#define TWN_AUDIO_H #define TWN_AUDIO_H
#include "twn_engine_api.h" #include "twn_api.h"
#include <stdbool.h> #include <stdbool.h>
/* plays audio file at specified channel or at scratch channel if NULL is passed, without ability to refer to it later */ /* plays audio file at specified channel or at scratch channel if NULL is passed, without ability to refer to it later */
/* path path must contain valid file extension to infer which file format it is */ /* path path must contain valid file extension to infer which file format it is */
/* supported formats: .ogg, .xm */ /* supported formats: .ogg, .xm */
/* mono or stereo only */
TWN_API void audio_play(const char *audio, TWN_API void audio_play(const char *audio,
const char *channel, /* optional */ const char *channel, /* optional */
bool repeat, /* default: false */ bool repeat, /* default: false */

View File

@ -2,7 +2,7 @@
#define TWN_CONTEXT_H #define TWN_CONTEXT_H
#include "twn_types.h" #include "twn_types.h"
#include "twn_engine_api.h" #include "twn_api.h"
#include <stdbool.h> #include <stdbool.h>
#include <stdint.h> #include <stdint.h>

View File

@ -2,7 +2,7 @@
#define TWN_DRAW_H #define TWN_DRAW_H
#include "twn_types.h" #include "twn_types.h"
#include "twn_engine_api.h" #include "twn_api.h"
#include <stdbool.h> #include <stdbool.h>
@ -47,6 +47,13 @@ TWN_API void draw_line(Vec2 start,
float thickness, /* optional, default: 1 */ float thickness, /* optional, default: 1 */
Color color); /* optional, default: all 255 */ Color color); /* optional, default: all 255 */
/* intended for debugging and spatial reference */
TWN_API void draw_line_3d(Vec3 start,
Vec3 finish,
float thickness, /* optional, default: 1 */
Color color); /* optional, default: all 255 */
/* TODO: combine with draw_rectangle()? */ /* TODO: combine with draw_rectangle()? */
TWN_API void draw_box(Rect rect, TWN_API void draw_box(Rect rect,
float thickness, /* optional, default: 1 */ float thickness, /* optional, default: 1 */
@ -66,12 +73,12 @@ TWN_API void draw_triangle(char const *texture,
Color c2); /* optional, default: all 255 */ Color c2); /* optional, default: all 255 */
TWN_API void draw_quad(char const *texture, TWN_API void draw_quad(char const *texture,
Vec3 v0, /* upper-left */ Vec3 v0, /* upper-left */
Vec3 v1, /* bottom-left */ Vec3 v1, /* bottom-left */
Vec3 v2, /* bottom-right */ Vec3 v2, /* bottom-right */
Vec3 v3, /* upper-right */ Vec3 v3, /* upper-right */
Rect texture_region, Rect texture_region,
Color color); /* optional, default: all 255 */ Color color); /* optional, default: all 255 */
TWN_API void draw_billboard(char const *texture, TWN_API void draw_billboard(char const *texture,
Vec3 position, Vec3 position,
@ -86,11 +93,26 @@ TWN_API void draw_camera_2d(Vec2 position, /* optional, default: (0, 0) */
/* sets a perspective 3d camera to be used for all 3d commands */ /* sets a perspective 3d camera to be used for all 3d commands */
/* fov = 0 corresponds to orthographic projection */ /* fov = 0 corresponds to orthographic projection */
TWN_API void draw_camera(Vec3 position, TWN_API void draw_camera(Vec3 position, /* optional, default: (0, 0, 0) */
Vec3 direction, /* optional, default: (0, 0, -1) */ Vec3 direction, /* optional, default: (0, 0, -1) */
Vec3 up, /* optional, default: (0, 1, 0) */ Vec3 up, /* optional, default: (0, 1, 0) */
float fov, /* optional, default: PI / 6 * 3 (90 degrees) */ float fov, /* optional, default: PI / 6 * 3 (90 degrees) */
float zoom); /* optional, default: 1 */ float zoom, /* optional, default: 1 */
float draw_distance); /* optional, default: 100 */
/* find position and looking direction of a 2d point into 3d space */
/* make sure it's called with matching parameters to last draw_camera(), if you need them to match */
typedef struct DrawCameraUnprojectResult {
Vec3 position;
Vec3 direction;
} DrawCameraUnprojectResult;
TWN_API DrawCameraUnprojectResult draw_camera_unproject(Vec2 point,
Vec3 position,
Vec3 direction,
Vec3 up,
float fov,
float zoom,
float draw_distance);
/* same as draw_camera(), but with first person controller in mind */ /* same as draw_camera(), but with first person controller in mind */
/* direction and up vectors are inferred from roll, pitch and yaw parameters (in radians) */ /* direction and up vectors are inferred from roll, pitch and yaw parameters (in radians) */
@ -100,12 +122,13 @@ typedef struct DrawCameraFromPrincipalAxesResult {
Vec3 up; Vec3 up;
} DrawCameraFromPrincipalAxesResult; } DrawCameraFromPrincipalAxesResult;
TWN_API DrawCameraFromPrincipalAxesResult TWN_API DrawCameraFromPrincipalAxesResult
draw_camera_from_principal_axes(Vec3 position, draw_camera_from_principal_axes(Vec3 position, /* optional, default: (0, 0, 0) */
float roll, /* optional, default: 0 */ float roll, /* optional, default: 0 */
float pitch, /* optional, default: 0 */ float pitch, /* optional, default: 0 */
float yaw, /* optional, default: 0 */ float yaw, /* optional, default: 0 */
float fov, /* optional, default: PI / 6 * 3 (90 degrees) */ float fov, /* optional, default: PI / 6 * 3 (90 degrees) */
float zoom); /* optional, default: 1 */ float zoom, /* optional, default: 1 */
float draw_distance); /* optional, default: 100 */
/* expects '*' masks that will be expanded to 6 names: 'up', 'down', 'east', 'west', 'north' and 'south' */ /* expects '*' masks that will be expanded to 6 names: 'up', 'down', 'east', 'west', 'north' and 'south' */
TWN_API void draw_skybox(const char *textures); TWN_API void draw_skybox(const char *textures);

View File

@ -5,7 +5,7 @@
#include "twn_input.h" #include "twn_input.h"
#include "twn_draw.h" #include "twn_draw.h"
#include "twn_audio.h" #include "twn_audio.h"
#include "twn_engine_api.h" #include "twn_api.h"
#include "twn_util.h" #include "twn_util.h"
#include "twn_context.h" #include "twn_context.h"

View File

@ -1,7 +1,7 @@
#ifndef TWN_INPUT_H #ifndef TWN_INPUT_H
#define TWN_INPUT_H #define TWN_INPUT_H
#include "twn_engine_api.h" #include "twn_api.h"
#include "twn_types.h" #include "twn_types.h"
#include <stdbool.h> #include <stdbool.h>

View File

@ -40,4 +40,12 @@ typedef struct Rect {
} Rect; } Rect;
/* data packet exchanged between parties, assumed immutable */
/* length is whole number */
typedef struct String {
char *data;
float length;
} String;
#endif #endif

View File

@ -2,54 +2,43 @@
#define TWN_UTIL_H #define TWN_UTIL_H
#include "twn_types.h" #include "twn_types.h"
#include "twn_engine_api.h" #include "twn_api.h"
#include <stdint.h> #include <stdint.h>
#include <stddef.h> #include <stddef.h>
#include <stdbool.h> #include <stdbool.h>
/* here as it's not part of standard C */
#ifndef M_PI
#define M_PI 3.14159265358979323846264338327950288 /**< pi */
#endif
#ifndef TWN_NOT_C /* read data from virtual filesystem, with assumption that you know what that underlying data is */
#include <math.h> /* this routine supports commands, which define operations performed on filepaths */
/* empty result is returned when something goes wrong or file doesn't exist, with an error logged */
/* defined commands: */
/* <path:string> -- reads utf8 encoded file to null terminated string */
/* <path:binary> -- reads arbitrary binary file, resulted encoding depends on environment (could be base64, for example) */
/* <path:exists> -- returns "yes" or "no" ascii string */
/* <path:images> -- returns newline separated utf8 list of image files in given path */
TWN_API String file_read(char const *file, const char *operation);
#ifndef M_PI /* commit write to a file, which meaning depends on interpretation */
#define M_PI 3.14159265358979323846264338327950288 /**< pi */ /* file_read should not be affected for current frame by this, operation is pending */
#endif /* defined commands: */
/* <path:string> -- save utf8 file */
/* multiply by these to convert degrees <---> radians */ /* <path:binary> -- save binary file */
#define DEG2RAD (M_PI / 180) /* <path:image> -- write image data to a PNG file, potentially updating running textures used */
#define RAD2DEG (180 / M_PI) /* -- format is: [width:2b][height:2][alpha:1][data:(3+alpha)*width*height] */
TWN_API void file_write(char const *file, const char *operation, String string);
TWN_API void log_info(const char *restrict format, ...);
TWN_API void log_critical(const char *restrict format, ...);
TWN_API void log_warn(const char *restrict format, ...);
/* saves all texture atlases as BMP files in the write directory */
TWN_API void textures_dump_atlases(void);
/* TODO: this is why generics were invented. sorry, i'm tired today */
TWN_API float clampf(float f, float min, float max);
/* sets buf_out to a pointer to a byte buffer which must be freed. */
/* returns the size of this buffer. */
TWN_API int64_t file_to_bytes(const char *path, unsigned char **buf_out);
/* returns a pointer to a string which must be freed */
TWN_API char *file_to_str(const char *path);
/* returns true if the file exists in the filesystem */
TWN_API bool file_exists(const char *path);
#endif /* TWN_NOT_C */
/* read file to null terminated string, it is freed when the frame ends */
TWN_API char const *file_read(char const *file);
/* TODO: move to external templated lib */
/* calculates the overlap of two rectangles */ /* calculates the overlap of two rectangles */
TWN_API Rect rect_overlap(Rect a, Rect b); TWN_API Rect rect_overlap(Rect a, Rect b);
/* returns true if two rectangles are intersecting */ /* returns true if two rectangles are intersecting */
TWN_API bool rect_intersects(Rect a, Rect b); TWN_API bool rect_intersects(Rect a, Rect b);
/* TODO: move to external templated lib */
/* decrements a floating point second-based timer, stopping at 0.0f */ /* decrements a floating point second-based timer, stopping at 0.0f */
/* meant for poll based real time logic in game logic */ /* meant for poll based real time logic in game logic */
/* note that it should be decremented only on the next tick after its creation */ /* note that it should be decremented only on the next tick after its creation */
@ -60,10 +49,13 @@ typedef struct TimerElapseSecondsResult {
} TimerElapseSecondsResult; } TimerElapseSecondsResult;
TWN_API TimerElapseSecondsResult timer_elapse_seconds(float seconds_left, float interval); TWN_API TimerElapseSecondsResult timer_elapse_seconds(float seconds_left, float interval);
TWN_API void log_string(char const *value, char const *identity);
TWN_API void log_float(float value, char const *identity);
TWN_API void log_vec2(Vec2 value, char const *identity); TWN_API void log_vec2(Vec2 value, char const *identity);
TWN_API void log_vec3(Vec3 value, char const *identity); TWN_API void log_vec3(Vec3 value, char const *identity);
TWN_API void log_rect(Rect value, char const *identity); TWN_API void log_rect(Rect value, char const *identity);
/* IDEA: NES debugger style frame slices that show for how long and when certain profile range takes place */
TWN_API void profile_start(char const *profile); TWN_API void profile_start(char const *profile);
TWN_API void profile_end(char const *profile); TWN_API void profile_end(char const *profile);

View File

@ -29,10 +29,20 @@ static inline Vec2 vec2_scale(Vec2 a, float s) {
return (Vec2) { a.x * s, a.y * s }; return (Vec2) { a.x * s, a.y * s };
} }
static inline float vec2_dot(Vec2 a, Vec2 b) {
return a.x * b.x + a.y * b.y;
}
static inline float vec2_length(Vec2 a) { static inline float vec2_length(Vec2 a) {
return sqrtf(a.x * a.x + a.y * a.y); return sqrtf(a.x * a.x + a.y * a.y);
} }
static inline Vec2 vec2_norm(Vec2 a) {
const float n = sqrtf(vec2_dot(a, a));
/* TODO: do we need truncating over epsilon as cglm does? */
return vec2_scale(a, 1.0f / n);
}
static inline Vec3 vec3_add(Vec3 a, Vec3 b) { static inline Vec3 vec3_add(Vec3 a, Vec3 b) {
return (Vec3) { a.x + b.x, a.y + b.y, a.z + b.z }; return (Vec3) { a.x + b.x, a.y + b.y, a.z + b.z };
} }
@ -76,6 +86,10 @@ static inline Vec3 vec3_norm(Vec3 a) {
return vec3_scale(a, 1.0f / n); return vec3_scale(a, 1.0f / n);
} }
static inline float vec3_angle(Vec3 a, Vec3 b) {
return acosf(vec3_dot(vec3_norm(a), vec3_norm(b)));
}
static inline Vec3 vec3_rotate(Vec3 v, float angle, Vec3 axis) { static inline Vec3 vec3_rotate(Vec3 v, float angle, Vec3 axis) {
/* from cglm */ /* from cglm */
Vec3 v1, v2, k; Vec3 v1, v2, k;

BIN
share/assets/Dernyns256.ttf Normal file

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More