From 6bb9b009c48e3bf435b2df79b419f21d5aecdfa1 Mon Sep 17 00:00:00 2001 From: irrlicht Date: Sat, 13 Jun 2026 18:18:17 +0200 Subject: [PATCH] idk shitload of stuff --- assets/maps/autosave/test.1.map | 284 +++++++++++++ assets/maps/autosave/test.2.map | 293 +++++++++++++ assets/maps/autosave/test.3.map | 303 ++++++++++++++ assets/maps/test.map | 293 +++++++++++++ assets/maps/test02.map | 84 ++++ assets/textures/__TB_empty.tga | Bin 0 -> 16428 bytes assets/textures/fonts/oldschool-cga-8x8.tga | Bin 0 -> 65580 bytes assets/textures/fonts/oldschool-ega-8x14.tga | Bin 0 -> 114732 bytes assets/textures/placeholder.tga | Bin 0 -> 262188 bytes assets/textures/ui/cursors.tga | Bin 0 -> 4140 bytes assets/textures/ui/panel_ornaments.tga | Bin 0 -> 4140 bytes src/engine/map.rs | 179 ++++++++ src/engine/mod.rs | 7 + src/engine/tga.rs | 150 +++++++ src/render/brush.rs | 417 +++++++++++++++++++ src/render/camera.rs | 102 +++++ src/render/font.rs | 168 ++++++++ src/render/gpu.rs | 58 ++- src/render/mod.rs | 268 ++++++++++-- src/render/scene.rs | 199 ++++++--- src/render/scene.wgsl | 29 +- src/render/sprite.rs | 207 +++++++++ src/render/sprite.wgsl | 45 ++ src/render/ui.rs | 254 +++++++++++ 24 files changed, 3221 insertions(+), 119 deletions(-) create mode 100644 assets/maps/autosave/test.1.map create mode 100644 assets/maps/autosave/test.2.map create mode 100644 assets/maps/autosave/test.3.map create mode 100644 assets/maps/test.map create mode 100644 assets/maps/test02.map create mode 100644 assets/textures/__TB_empty.tga create mode 100644 assets/textures/fonts/oldschool-cga-8x8.tga create mode 100644 assets/textures/fonts/oldschool-ega-8x14.tga create mode 100644 assets/textures/placeholder.tga create mode 100644 assets/textures/ui/cursors.tga create mode 100644 assets/textures/ui/panel_ornaments.tga create mode 100644 src/engine/map.rs create mode 100644 src/engine/tga.rs create mode 100644 src/render/brush.rs create mode 100644 src/render/camera.rs create mode 100644 src/render/font.rs create mode 100644 src/render/sprite.rs create mode 100644 src/render/sprite.wgsl create mode 100644 src/render/ui.rs diff --git a/assets/maps/autosave/test.1.map b/assets/maps/autosave/test.1.map new file mode 100644 index 0000000..86e5bc9 --- /dev/null +++ b/assets/maps/autosave/test.1.map @@ -0,0 +1,284 @@ +// Game: Generic +// Format: Standard +// entity 0 +{ +"classname" "worldspawn" +"wad" "" +// brush 0 +{ +( -112 -672 -80 ) ( -112 -720 -80 ) ( -112 -720 -112 ) placeholder -224 -64 0 0.2 0.2 +( -112 -720 -80 ) ( -64 -720 -80 ) ( -64 -720 -112 ) placeholder 64 -64 0 0.2 0.2 +( -64 -720 -112 ) ( -64 -672 -112 ) ( -112 -672 -112 ) placeholder 64 224 0 0.2 0.2 +( -112 -672 -80 ) ( -64 -720 -80 ) ( -112 -720 -80 ) placeholder 64 224 0 0.2 0.2 +( -64 -672 -112 ) ( -64 -672 -96 ) ( -112 -672 -80 ) placeholder 64 -64 0 0.2 0.2 +( -112 -672 -80 ) ( -64 -672 -96 ) ( -64 -720 -80 ) placeholder 64 224 0 0.2 0.2 +( -64 -720 -80 ) ( -64 -672 -96 ) ( -64 -672 -112 ) placeholder -224 -64 0 0.2 0.2 +} +// brush 1 +{ +( 0 -128 -16 ) ( 0 -127 -16 ) ( 0 -128 -15 ) placeholder 0 0 0 1 1 +( 0 -128 -16 ) ( 0 -128 -15 ) ( 1 -128 -16 ) placeholder -32 0 0 1 1 +( 0 -128 -16 ) ( 1 -128 -16 ) ( 0 -127 -16 ) placeholder -32 0 0 1 1 +( 64 -96 0 ) ( 64 -95 0 ) ( 65 -96 0 ) placeholder -32 0 0 1 1 +( 64 -96 0 ) ( 65 -96 0 ) ( 64 -96 1 ) placeholder -32 0 0 1 1 +( 64 -96 0 ) ( 64 -96 1 ) ( 64 -95 0 ) placeholder 0 0 0 1 1 +} +// brush 2 +{ +( 64 -144 -32 ) ( 64 -143 -32 ) ( 64 -144 -31 ) placeholder -16 -16 0 1 1 +( 64 -144 -32 ) ( 64 -144 -31 ) ( 65 -144 -32 ) placeholder -32 -16 0 1 1 +( 64 -144 -32 ) ( 65 -144 -32 ) ( 64 -143 -32 ) placeholder -32 16 0 1 1 +( 192 -80 -16 ) ( 192 -79 -16 ) ( 193 -80 -16 ) placeholder -32 16 0 1 1 +( 192 -80 -16 ) ( 193 -80 -16 ) ( 192 -80 -15 ) placeholder -32 -16 0 1 1 +( 192 -80 -16 ) ( 192 -80 -15 ) ( 192 -79 -16 ) placeholder -16 -16 0 1 1 +} +// brush 3 +{ +( 80 16 32 ) ( 80 15 32 ) ( 80 16 31 ) placeholder 0 -16 180 1 1 +( 144 -96 16 ) ( 145 -96 16 ) ( 144 -96 15 ) placeholder 16 -16 0 1 -1 +( 144 -96 16 ) ( 144 -97 16 ) ( 145 -96 16 ) placeholder 16 0 0 1 -1 +( 80 16 32 ) ( 81 16 32 ) ( 80 15 32 ) placeholder 16 0 0 1 -1 +( 80 16 32 ) ( 80 16 31 ) ( 81 16 32 ) placeholder 16 -16 0 1 -1 +( 144 -96 16 ) ( 144 -96 15 ) ( 144 -97 16 ) placeholder 0 -16 180 1 1 +} +// brush 4 +{ +( 224 -48 -80 ) ( 224 16 -80 ) ( 224 -96 -56 ) placeholder -128 -60 0 1 2 +( 224 -96 -56 ) ( 288 -96 -80 ) ( 224 -48 -80 ) placeholder -32 8 0 1 -2 +( 224 16 -80 ) ( 272 -32 -56 ) ( 224 -96 -56 ) placeholder -134.26993 -44.949593 0 1 2 +( 272 -32 -56 ) ( 288 -96 -40 ) ( 224 -96 -56 ) placeholder -32 112 0 1 1 +( 224 -96 -56 ) ( 288 -96 -40 ) ( 288 -96 -80 ) placeholder -32 -16.941177 0 1 -2 +( 288 -96 -80 ) ( 288 16 -80 ) ( 224 16 -80 ) placeholder -32 144 0 1 1 +( 272 -32 -56 ) ( 304 -32 -56 ) ( 288 -96 -40 ) placeholder -32 112 0 1 1 +( 288 16 -80 ) ( 304 -32 -56 ) ( 272 -32 -56 ) placeholder -32 -24 0 1 -2 +( 288 -96 -80 ) ( 304 -32 -56 ) ( 288 16 -80 ) placeholder 144 -52 0 1 2 +( 288 -96 -40 ) ( 304 -32 -56 ) ( 288 -96 -80 ) placeholder 118.95169 -69.728745 0 1 2 +} +// brush 5 +{ +( -144 -560 -80 ) ( -144 -559 -80 ) ( -144 -560 -79 ) placeholder -80 96 0 0.2 0.2 +( -144 -320 -80 ) ( -144 -320 -79 ) ( -143 -320 -80 ) placeholder -32 96 0 0.2 0.2 +( -144 -560 -80 ) ( -143 -560 -80 ) ( -144 -559 -80 ) placeholder -32 80 0 0.2 0.2 +( -16 -432 16 ) ( -16 -431 16 ) ( -15 -432 16 ) placeholder -32 80 0 0.2 0.2 +( -16 -304 -48 ) ( -15 -304 -48 ) ( -16 -304 -47 ) placeholder -32 96 0 0.2 0.2 +( 304 -432 -48 ) ( 304 -432 -47 ) ( 304 -431 -48 ) placeholder -80 96 0 0.2 0.2 +} +// brush 6 +{ +( -160 -240 -16 ) ( -160 -241 -16 ) ( -160 -240 -17 ) placeholder -240 96 180 1 1 +( -64 -304 -32 ) ( -63 -304 -32 ) ( -64 -304 -33 ) placeholder 208 96 0 1 -1 +( -64 -352 -80 ) ( -64 -353 -80 ) ( -63 -352 -80 ) placeholder 208 240 0 1 -1 +( -160 -240 16 ) ( -159 -240 16 ) ( -160 -241 16 ) placeholder 208 240 0 1 -1 +( -160 192 -16 ) ( -160 192 -17 ) ( -159 192 -16 ) placeholder 208 96 0 1 -1 +( -144 -352 -32 ) ( -144 -352 -33 ) ( -144 -353 -32 ) placeholder -240 96 180 1 1 +} +// brush 7 +{ +( 162.74516600406096 -112 -80 ) ( 163.4522727852475 -111.29289321881345 -80 ) ( 162.74516600406096 -112 -79 ) placeholder 190.39192 0 0 0.70710677 1 +( 162.74516600406096 -112 -80 ) ( 162.74516600406096 -112 -79 ) ( 163.45227278524752 -112.70710678118652 -80 ) placeholder -38.156433 0 0 0.70710677 1 +( 162.74516600406096 -112 -80 ) ( 163.45227278524752 -112.70710678118652 -80 ) ( 163.4522727852475 -111.29289321881345 -80 ) placeholder -2.27417 3.882248 315 1 1 +( 253.25483399593907 -112 -48 ) ( 253.96194077712562 -111.29289321881353 -48 ) ( 253.9619407771256 -112.70710678118664 -48 ) placeholder -2.27417 3.882248 315 1 1 +( 253.25483399593907 -112 -64 ) ( 253.25483399593907 -112 -63 ) ( 253.96194077712562 -111.29289321881353 -64 ) placeholder -6.156433 0 0 0.70710677 1 +( 253.25483399593907 -112 -64 ) ( 253.9619407771256 -112.70710678118664 -64 ) ( 253.25483399593907 -112 -63 ) placeholder 97.60808 0 180 0.70710677 -1 +} +// brush 8 +{ +( -32 96 -80 ) ( -32 97 -80 ) ( -32 96 -79 ) placeholder 0 0 0 1 1 +( 96 272 -64 ) ( 97 272.625 -64 ) ( 96 272 -63 ) placeholder 0 0 0 1 1 +( -32 96 -80 ) ( -31 96.625 -80 ) ( -32 97 -80 ) placeholder -73.70788 15.668304 32.00539 1.1792476 0.87100387 +( 96 272 -32 ) ( 96 273 -32 ) ( 97 272.625 -32 ) placeholder -73.70788 15.668304 32.00539 1.1792476 0.87100387 +( -32 96 -80 ) ( -32 96 -79 ) ( -31 96.625 -80 ) placeholder 0 0 0 1 1 +( 96 272 -64 ) ( 96 272 -63 ) ( 96 273 -64 ) placeholder -80 0 0 1 1 +} +// brush 9 +{ +( -64 -672 -96 ) ( -64 -720 -80 ) ( -64 -720 -112 ) placeholder -224 -64 0 0.2 0.2 +( -64 -672 -96 ) ( -16 -672 -80 ) ( -64 -720 -80 ) placeholder 80 224 0 0.2 0.2 +( -64 -720 -80 ) ( -16 -720 -80 ) ( -16 -720 -112 ) placeholder 80 -64 0 0.2 0.2 +( -16 -720 -112 ) ( -16 -672 -112 ) ( -64 -672 -112 ) placeholder 80 224 0 0.2 0.2 +( -64 -720 -80 ) ( -16 -672 -80 ) ( -16 -720 -80 ) placeholder 80 224 0 0.2 0.2 +( -16 -672 -112 ) ( -16 -672 -80 ) ( -64 -672 -96 ) placeholder 80 -64 0 0.2 0.2 +( -16 -720 -80 ) ( -16 -672 -80 ) ( -16 -672 -112 ) placeholder -224 -64 0 0.2 0.2 +} +// brush 10 +{ +( -16 -672 -80 ) ( -16 -720 -80 ) ( -16 -720 -112 ) placeholder -224 -64 0 0.2 0.2 +( -16 -672 -80 ) ( 32 -672 -48 ) ( -16 -720 -80 ) placeholder -160 224 0 0.2 0.2 +( -16 -720 -80 ) ( 32 -720 -80 ) ( 32 -720 -112 ) placeholder -160 -64 0 0.2 0.2 +( -16 -720 -80 ) ( 32 -672 -48 ) ( 32 -720 -80 ) placeholder -160 224 0 0.2 0.2 +( 32 -720 -112 ) ( 32 -672 -112 ) ( -16 -672 -112 ) placeholder -160 224 0 0.2 0.2 +( 32 -672 -112 ) ( 32 -672 -48 ) ( -16 -672 -80 ) placeholder -160 -64 0 0.2 0.2 +( 32 -720 -80 ) ( 32 -672 -48 ) ( 32 -672 -112 ) placeholder -224 -64 0 0.2 0.2 +} +// brush 11 +{ +( 32 -672 -48 ) ( 32 -720 -80 ) ( 32 -720 -112 ) placeholder -224 -64 0 0.2 0.2 +( 32 -720 -80 ) ( 80 -720 -80 ) ( 80 -720 -112 ) placeholder -144 -64 0 0.2 0.2 +( 32 -672 -48 ) ( 80 -720 -80 ) ( 32 -720 -80 ) placeholder -144 224 0 0.2 0.2 +( 80 -720 -112 ) ( 80 -672 -112 ) ( 32 -672 -112 ) placeholder -144 224 0 0.2 0.2 +( 80 -672 -112 ) ( 80 -672 -80 ) ( 32 -672 -48 ) placeholder -144 -64 0 0.2 0.2 +( 32 -672 -48 ) ( 80 -672 -80 ) ( 80 -720 -80 ) placeholder -144 224 0 0.2 0.2 +( 80 -720 -80 ) ( 80 -672 -80 ) ( 80 -672 -112 ) placeholder -224 -64 0 0.2 0.2 +} +// brush 12 +{ +( -112 -624 -80 ) ( -112 -672 -80 ) ( -112 -672 -112 ) placeholder -208 -64 0 0.2 0.2 +( -112 -672 -80 ) ( -64 -672 -96 ) ( -64 -672 -112 ) placeholder 64 -64 0 0.2 0.2 +( -64 -672 -112 ) ( -64 -624 -112 ) ( -112 -624 -112 ) placeholder 64 208 0 0.2 0.2 +( -112 -624 -80 ) ( -64 -624 -80 ) ( -112 -672 -80 ) placeholder 64 208 0 0.2 0.2 +( -64 -624 -112 ) ( -64 -624 -80 ) ( -112 -624 -80 ) placeholder 64 -64 0 0.2 0.2 +( -112 -672 -80 ) ( -64 -624 -80 ) ( -64 -672 -96 ) placeholder 64 208 0 0.2 0.2 +( -64 -672 -96 ) ( -64 -624 -80 ) ( -64 -624 -112 ) placeholder -208 -64 0 0.2 0.2 +} +// brush 13 +{ +( -112 -384 -112 ) ( -112 -383 -112 ) ( -112 -384 -111 ) placeholder -192 -64 0 0.2 0.2 +( -112 -624 -112 ) ( -112 -624 -111 ) ( -111 -624 -112 ) placeholder 64 -64 0 0.2 0.2 +( -112 -384 -112 ) ( -111 -384 -112 ) ( -112 -383 -112 ) placeholder 64 192 0 0.2 0.2 +( 16 -256 -80 ) ( 16 -255 -80 ) ( 17 -256 -80 ) placeholder 64 192 0 0.2 0.2 +( 16 -576 -80 ) ( 17 -576 -80 ) ( 16 -576 -79 ) placeholder 64 -64 0 0.2 0.2 +( -64 -256 -80 ) ( -64 -256 -79 ) ( -64 -255 -80 ) placeholder -192 -64 0 0.2 0.2 +} +// brush 14 +{ +( -64 -624 -80 ) ( -64 -672 -96 ) ( -64 -672 -112 ) placeholder -208 -64 0 0.2 0.2 +( -64 -624 -80 ) ( -16 -672 -80 ) ( -64 -672 -96 ) placeholder 80 208 0 0.2 0.2 +( -64 -672 -96 ) ( -16 -672 -80 ) ( -16 -672 -112 ) placeholder 80 -64 0 0.2 0.2 +( -16 -672 -112 ) ( -16 -624 -112 ) ( -64 -624 -112 ) placeholder 80 208 0 0.2 0.2 +( -64 -624 -80 ) ( -16 -624 -80 ) ( -16 -672 -80 ) placeholder 80 208 0 0.2 0.2 +( -16 -624 -112 ) ( -16 -624 -80 ) ( -64 -624 -80 ) placeholder 80 -64 0 0.2 0.2 +( -16 -672 -80 ) ( -16 -624 -80 ) ( -16 -624 -112 ) placeholder -208 -64 0 0.2 0.2 +} +// brush 15 +{ +( -112 -528 -80 ) ( -112 -576 -80 ) ( -112 -576 -112 ) placeholder -176 -64 0 0.2 0.2 +( -112 -528 -80 ) ( -64 -528 -48 ) ( -112 -576 -80 ) placeholder 64 176 0 0.2 0.2 +( -112 -576 -80 ) ( -64 -576 -80 ) ( -64 -576 -112 ) placeholder 64 -64 0 0.2 0.2 +( -112 -576 -80 ) ( -64 -528 -48 ) ( -64 -576 -80 ) placeholder 64 176 0 0.2 0.2 +( -64 -576 -112 ) ( -64 -528 -112 ) ( -112 -528 -112 ) placeholder 64 176 0 0.2 0.2 +( -64 -528 -112 ) ( -64 -528 -48 ) ( -112 -528 -80 ) placeholder 64 -64 0 0.2 0.2 +( -64 -576 -80 ) ( -64 -528 -48 ) ( -64 -528 -112 ) placeholder -176 -64 0 0.2 0.2 +} +// brush 16 +{ +( -64 -384 -112 ) ( -64 -383 -112 ) ( -64 -384 -111 ) placeholder -192 -64 0 0.2 0.2 +( -64 -624 -112 ) ( -64 -624 -111 ) ( -63 -624 -112 ) placeholder 80 -64 0 0.2 0.2 +( -64 -384 -112 ) ( -63 -384 -112 ) ( -64 -383 -112 ) placeholder 80 192 0 0.2 0.2 +( 64 -256 -80 ) ( 64 -255 -80 ) ( 65 -256 -80 ) placeholder 80 192 0 0.2 0.2 +( 64 -576 -80 ) ( 65 -576 -80 ) ( 64 -576 -79 ) placeholder 80 -64 0 0.2 0.2 +( -16 -256 -80 ) ( -16 -256 -79 ) ( -16 -255 -80 ) placeholder -192 -64 0 0.2 0.2 +} +// brush 17 +{ +( -16 -624 -80 ) ( -16 -672 -80 ) ( -16 -672 -112 ) placeholder -208 -64 0 0.2 0.2 +( -16 -624 -80 ) ( 32 -672 -48 ) ( -16 -672 -80 ) placeholder -160 208 0 0.2 0.2 +( -16 -672 -80 ) ( 32 -672 -48 ) ( 32 -672 -112 ) placeholder -160 -64 0 0.2 0.2 +( 32 -672 -112 ) ( 32 -624 -112 ) ( -16 -624 -112 ) placeholder -160 208 0 0.2 0.2 +( -16 -624 -80 ) ( 32 -624 -80 ) ( 32 -672 -48 ) placeholder -160 208 0 0.2 0.2 +( 32 -624 -112 ) ( 32 -624 -80 ) ( -16 -624 -80 ) placeholder -160 -64 0 0.2 0.2 +( 32 -672 -48 ) ( 32 -624 -80 ) ( 32 -624 -112 ) placeholder -208 -64 0 0.2 0.2 +} +// brush 18 +{ +( -112 -480 -80 ) ( -112 -528 -80 ) ( -112 -528 -112 ) placeholder -160 -64 0 0.2 0.2 +( -112 -480 -80 ) ( -64 -528 -48 ) ( -112 -528 -80 ) placeholder 64 160 0 0.2 0.2 +( -112 -528 -80 ) ( -64 -528 -48 ) ( -64 -528 -112 ) placeholder 64 -64 0 0.2 0.2 +( -64 -528 -112 ) ( -64 -480 -112 ) ( -112 -480 -112 ) placeholder 64 160 0 0.2 0.2 +( -112 -480 -80 ) ( -64 -480 -80 ) ( -64 -528 -48 ) placeholder 64 160 0 0.2 0.2 +( -64 -480 -112 ) ( -64 -480 -80 ) ( -112 -480 -80 ) placeholder 64 -64 0 0.2 0.2 +( -64 -528 -48 ) ( -64 -480 -80 ) ( -64 -480 -112 ) placeholder -160 -64 0 0.2 0.2 +} +// brush 19 +{ +( -64 -528 -48 ) ( -64 -576 -80 ) ( -64 -576 -112 ) placeholder -176 -64 0 0.2 0.2 +( -64 -576 -80 ) ( -16 -576 -80 ) ( -16 -576 -112 ) placeholder 80 -64 0 0.2 0.2 +( -64 -528 -48 ) ( -16 -576 -80 ) ( -64 -576 -80 ) placeholder 80 176 0 0.2 0.2 +( -16 -576 -112 ) ( -16 -528 -112 ) ( -64 -528 -112 ) placeholder 80 176 0 0.2 0.2 +( -16 -528 -112 ) ( -16 -528 -80 ) ( -64 -528 -48 ) placeholder 80 -64 0 0.2 0.2 +( -64 -528 -48 ) ( -16 -528 -80 ) ( -16 -576 -80 ) placeholder 80 176 0 0.2 0.2 +( -16 -576 -80 ) ( -16 -528 -80 ) ( -16 -528 -112 ) placeholder -176 -64 0 0.2 0.2 +} +// brush 20 +{ +( -16 -384 -112 ) ( -16 -383 -112 ) ( -16 -384 -111 ) placeholder -192 -64 0 0.2 0.2 +( -16 -624 -112 ) ( -16 -624 -111 ) ( -15 -624 -112 ) placeholder -160 -64 0 0.2 0.2 +( -16 -384 -112 ) ( -15 -384 -112 ) ( -16 -383 -112 ) placeholder -160 192 0 0.2 0.2 +( 112 -256 -80 ) ( 112 -255 -80 ) ( 113 -256 -80 ) placeholder -160 192 0 0.2 0.2 +( 112 -576 -80 ) ( 113 -576 -80 ) ( 112 -576 -79 ) placeholder -160 -64 0 0.2 0.2 +( 32 -256 -80 ) ( 32 -256 -79 ) ( 32 -255 -80 ) placeholder -192 -64 0 0.2 0.2 +} +// brush 21 +{ +( 32 -624 -80 ) ( 32 -672 -48 ) ( 32 -672 -112 ) placeholder -208 -64 0 0.2 0.2 +( 32 -672 -48 ) ( 80 -672 -80 ) ( 80 -672 -112 ) placeholder -144 -64 0 0.2 0.2 +( 80 -672 -112 ) ( 80 -624 -112 ) ( 32 -624 -112 ) placeholder -144 208 0 0.2 0.2 +( 32 -624 -80 ) ( 80 -624 -80 ) ( 32 -672 -48 ) placeholder -144 208 0 0.2 0.2 +( 80 -624 -112 ) ( 80 -624 -80 ) ( 32 -624 -80 ) placeholder -144 -64 0 0.2 0.2 +( 32 -672 -48 ) ( 80 -624 -80 ) ( 80 -672 -80 ) placeholder -144 208 0 0.2 0.2 +( 80 -672 -80 ) ( 80 -624 -80 ) ( 80 -624 -112 ) placeholder -208 -64 0 0.2 0.2 +} +// brush 22 +{ +( -64 -480 -80 ) ( -64 -528 -48 ) ( -64 -528 -112 ) placeholder -160 -64 0 0.2 0.2 +( -64 -528 -48 ) ( -16 -528 -80 ) ( -16 -528 -112 ) placeholder 80 -64 0 0.2 0.2 +( -16 -528 -112 ) ( -16 -480 -112 ) ( -64 -480 -112 ) placeholder 80 160 0 0.2 0.2 +( -64 -480 -80 ) ( -16 -480 -80 ) ( -64 -528 -48 ) placeholder 80 160 0 0.2 0.2 +( -16 -480 -112 ) ( -16 -480 -80 ) ( -64 -480 -80 ) placeholder 80 -64 0 0.2 0.2 +( -64 -528 -48 ) ( -16 -480 -80 ) ( -16 -528 -80 ) placeholder 80 160 0 0.2 0.2 +( -16 -528 -80 ) ( -16 -480 -80 ) ( -16 -480 -112 ) placeholder -160 -64 0 0.2 0.2 +} +// brush 23 +{ +( -16 -528 -80 ) ( -16 -576 -80 ) ( -16 -576 -112 ) placeholder -176 -64 0 0.2 0.2 +( -16 -576 -80 ) ( 32 -576 -80 ) ( 32 -576 -112 ) placeholder -160 -64 0 0.2 0.2 +( 32 -576 -112 ) ( 32 -528 -112 ) ( -16 -528 -112 ) placeholder -160 176 0 0.2 0.2 +( -16 -528 -80 ) ( 32 -576 -80 ) ( -16 -576 -80 ) placeholder -160 176 0 0.2 0.2 +( 32 -528 -112 ) ( 32 -528 -96 ) ( -16 -528 -80 ) placeholder -160 -64 0 0.2 0.2 +( -16 -528 -80 ) ( 32 -528 -96 ) ( 32 -576 -80 ) placeholder -160 176 0 0.2 0.2 +( 32 -576 -80 ) ( 32 -528 -96 ) ( 32 -528 -112 ) placeholder -176 -64 0 0.2 0.2 +} +// brush 24 +{ +( 32 -384 -112 ) ( 32 -383 -112 ) ( 32 -384 -111 ) placeholder -192 -64 0 0.2 0.2 +( 32 -624 -112 ) ( 32 -624 -111 ) ( 33 -624 -112 ) placeholder -144 -64 0 0.2 0.2 +( 32 -384 -112 ) ( 33 -384 -112 ) ( 32 -383 -112 ) placeholder -144 192 0 0.2 0.2 +( 160 -256 -80 ) ( 160 -255 -80 ) ( 161 -256 -80 ) placeholder -144 192 0 0.2 0.2 +( 160 -576 -80 ) ( 161 -576 -80 ) ( 160 -576 -79 ) placeholder -144 -64 0 0.2 0.2 +( 80 -256 -80 ) ( 80 -256 -79 ) ( 80 -255 -80 ) placeholder -192 -64 0 0.2 0.2 +} +// brush 25 +{ +( -16 -480 -80 ) ( -16 -528 -80 ) ( -16 -528 -112 ) placeholder -160 -64 0 0.2 0.2 +( -16 -528 -80 ) ( 32 -528 -96 ) ( 32 -528 -112 ) placeholder -160 -64 0 0.2 0.2 +( 32 -528 -112 ) ( 32 -480 -112 ) ( -16 -480 -112 ) placeholder -160 160 0 0.2 0.2 +( -16 -480 -80 ) ( 32 -480 -80 ) ( -16 -528 -80 ) placeholder -160 160 0 0.2 0.2 +( 32 -480 -112 ) ( 32 -480 -80 ) ( -16 -480 -80 ) placeholder -160 -64 0 0.2 0.2 +( -16 -528 -80 ) ( 32 -480 -80 ) ( 32 -528 -96 ) placeholder -160 160 0 0.2 0.2 +( 32 -528 -96 ) ( 32 -480 -80 ) ( 32 -480 -112 ) placeholder -160 -64 0 0.2 0.2 +} +// brush 26 +{ +( 32 -528 -96 ) ( 32 -576 -80 ) ( 32 -576 -112 ) placeholder -176 -64 0 0.2 0.2 +( 32 -528 -96 ) ( 80 -528 -80 ) ( 32 -576 -80 ) placeholder -144 176 0 0.2 0.2 +( 32 -576 -80 ) ( 80 -576 -80 ) ( 80 -576 -112 ) placeholder -144 -64 0 0.2 0.2 +( 80 -576 -112 ) ( 80 -528 -112 ) ( 32 -528 -112 ) placeholder -144 176 0 0.2 0.2 +( 32 -576 -80 ) ( 80 -528 -80 ) ( 80 -576 -80 ) placeholder -144 176 0 0.2 0.2 +( 80 -528 -112 ) ( 80 -528 -80 ) ( 32 -528 -96 ) placeholder -144 -64 0 0.2 0.2 +( 80 -576 -80 ) ( 80 -528 -80 ) ( 80 -528 -112 ) placeholder -176 -64 0 0.2 0.2 +} +// brush 27 +{ +( 32 -480 -80 ) ( 32 -528 -96 ) ( 32 -528 -112 ) placeholder -160 -64 0 0.2 0.2 +( 32 -480 -80 ) ( 80 -528 -80 ) ( 32 -528 -96 ) placeholder -144 160 0 0.2 0.2 +( 32 -528 -96 ) ( 80 -528 -80 ) ( 80 -528 -112 ) placeholder -144 -64 0 0.2 0.2 +( 80 -528 -112 ) ( 80 -480 -112 ) ( 32 -480 -112 ) placeholder -144 160 0 0.2 0.2 +( 32 -480 -80 ) ( 80 -480 -80 ) ( 80 -528 -80 ) placeholder -144 160 0 0.2 0.2 +( 80 -480 -112 ) ( 80 -480 -80 ) ( 32 -480 -80 ) placeholder -144 -64 0 0.2 0.2 +( 80 -528 -80 ) ( 80 -480 -80 ) ( 80 -480 -112 ) placeholder -160 -64 0 0.2 0.2 +} +} +// entity 1 +{ +"classname" "info_player_start" +"origin" "112 -48 56" +} diff --git a/assets/maps/autosave/test.2.map b/assets/maps/autosave/test.2.map new file mode 100644 index 0000000..0ff1645 --- /dev/null +++ b/assets/maps/autosave/test.2.map @@ -0,0 +1,293 @@ +// Game: Generic +// Format: Standard +// entity 0 +{ +"classname" "worldspawn" +"wad" "" +// brush 0 +{ +( -112 -672 -80 ) ( -112 -720 -80 ) ( -112 -720 -112 ) placeholder -224 -64 0 0.2 0.2 +( -112 -720 -80 ) ( -64 -720 -80 ) ( -64 -720 -112 ) placeholder 64 -64 0 0.2 0.2 +( -64 -720 -112 ) ( -64 -672 -112 ) ( -112 -672 -112 ) placeholder 64 224 0 0.2 0.2 +( -112 -672 -80 ) ( -64 -720 -80 ) ( -112 -720 -80 ) placeholder 64 224 0 0.2 0.2 +( -64 -672 -112 ) ( -64 -672 -96 ) ( -112 -672 -80 ) placeholder 64 -64 0 0.2 0.2 +( -112 -672 -80 ) ( -64 -672 -96 ) ( -64 -720 -80 ) placeholder 64 224 0 0.2 0.2 +( -64 -720 -80 ) ( -64 -672 -96 ) ( -64 -672 -112 ) placeholder -224 -64 0 0.2 0.2 +} +// brush 1 +{ +( 0 -128 -16 ) ( 0 -127 -16 ) ( 0 -128 -15 ) placeholder 0 0 0 1 1 +( 0 -128 -16 ) ( 0 -128 -15 ) ( 1 -128 -16 ) placeholder -32 0 0 1 1 +( 0 -128 -16 ) ( 1 -128 -16 ) ( 0 -127 -16 ) placeholder -32 0 0 1 1 +( 64 -96 0 ) ( 64 -95 0 ) ( 65 -96 0 ) placeholder -32 0 0 1 1 +( 64 -96 0 ) ( 65 -96 0 ) ( 64 -96 1 ) placeholder -32 0 0 1 1 +( 64 -96 0 ) ( 64 -96 1 ) ( 64 -95 0 ) placeholder 0 0 0 1 1 +} +// brush 2 +{ +( 64 -144 -32 ) ( 64 -143 -32 ) ( 64 -144 -31 ) placeholder -16 -16 0 1 1 +( 64 -144 -32 ) ( 64 -144 -31 ) ( 65 -144 -32 ) placeholder -32 -16 0 1 1 +( 64 -144 -32 ) ( 65 -144 -32 ) ( 64 -143 -32 ) placeholder -32 16 0 1 1 +( 192 -80 -16 ) ( 192 -79 -16 ) ( 193 -80 -16 ) placeholder -32 16 0 1 1 +( 192 -80 -16 ) ( 193 -80 -16 ) ( 192 -80 -15 ) placeholder -32 -16 0 1 1 +( 192 -80 -16 ) ( 192 -80 -15 ) ( 192 -79 -16 ) placeholder -16 -16 0 1 1 +} +// brush 3 +{ +( 80 16 32 ) ( 80 15 32 ) ( 80 16 31 ) placeholder 0 -16 180 1 1 +( 144 -96 16 ) ( 145 -96 16 ) ( 144 -96 15 ) placeholder 16 -16 0 1 -1 +( 144 -96 16 ) ( 144 -97 16 ) ( 145 -96 16 ) placeholder 16 0 0 1 -1 +( 80 16 32 ) ( 81 16 32 ) ( 80 15 32 ) placeholder 16 0 0 1 -1 +( 80 16 32 ) ( 80 16 31 ) ( 81 16 32 ) placeholder 16 -16 0 1 -1 +( 144 -96 16 ) ( 144 -96 15 ) ( 144 -97 16 ) placeholder 0 -16 180 1 1 +} +// brush 4 +{ +( 224 -48 -80 ) ( 224 16 -80 ) ( 224 -96 -56 ) placeholder -128 -60 0 1 2 +( 224 -96 -56 ) ( 288 -96 -80 ) ( 224 -48 -80 ) placeholder -32 8 0 1 -2 +( 224 16 -80 ) ( 272 -32 -56 ) ( 224 -96 -56 ) placeholder -134.26993 -44.949593 0 1 2 +( 272 -32 -56 ) ( 288 -96 -40 ) ( 224 -96 -56 ) placeholder -32 112 0 1 1 +( 224 -96 -56 ) ( 288 -96 -40 ) ( 288 -96 -80 ) placeholder -32 -16.941177 0 1 -2 +( 288 -96 -80 ) ( 288 16 -80 ) ( 224 16 -80 ) placeholder -32 144 0 1 1 +( 272 -32 -56 ) ( 304 -32 -56 ) ( 288 -96 -40 ) placeholder -32 112 0 1 1 +( 288 16 -80 ) ( 304 -32 -56 ) ( 272 -32 -56 ) placeholder -32 -24 0 1 -2 +( 288 -96 -80 ) ( 304 -32 -56 ) ( 288 16 -80 ) placeholder 144 -52 0 1 2 +( 288 -96 -40 ) ( 304 -32 -56 ) ( 288 -96 -80 ) placeholder 118.95169 -69.728745 0 1 2 +} +// brush 5 +{ +( -144 -560 -80 ) ( -144 -559 -80 ) ( -144 -560 -79 ) placeholder -80 96 0 0.2 0.2 +( -144 -320 -80 ) ( -144 -320 -79 ) ( -143 -320 -80 ) placeholder -32 96 0 0.2 0.2 +( -144 -560 -80 ) ( -143 -560 -80 ) ( -144 -559 -80 ) placeholder -32 80 0 0.2 0.2 +( -16 -432 16 ) ( -16 -431 16 ) ( -15 -432 16 ) placeholder -32 80 0 0.2 0.2 +( -16 -304 -48 ) ( -15 -304 -48 ) ( -16 -304 -47 ) placeholder -32 96 0 0.2 0.2 +( 304 -432 -48 ) ( 304 -432 -47 ) ( 304 -431 -48 ) placeholder -80 96 0 0.2 0.2 +} +// brush 6 +{ +( -160 -240 -16 ) ( -160 -241 -16 ) ( -160 -240 -17 ) placeholder -240 96 180 1 1 +( -64 -304 -32 ) ( -63 -304 -32 ) ( -64 -304 -33 ) placeholder 208 96 0 1 -1 +( -64 -352 -80 ) ( -64 -353 -80 ) ( -63 -352 -80 ) placeholder 208 240 0 1 -1 +( -160 -240 16 ) ( -159 -240 16 ) ( -160 -241 16 ) placeholder 208 240 0 1 -1 +( -160 192 -16 ) ( -160 192 -17 ) ( -159 192 -16 ) placeholder 208 96 0 1 -1 +( -144 -352 -32 ) ( -144 -352 -33 ) ( -144 -353 -32 ) placeholder -240 96 180 1 1 +} +// brush 7 +{ +( 162.74516600406096 -112 -80 ) ( 163.4522727852475 -111.29289321881345 -80 ) ( 162.74516600406096 -112 -79 ) placeholder 190.39192 0 0 0.70710677 1 +( 162.74516600406096 -112 -80 ) ( 162.74516600406096 -112 -79 ) ( 163.45227278524752 -112.70710678118652 -80 ) placeholder -38.156433 0 0 0.70710677 1 +( 162.74516600406096 -112 -80 ) ( 163.45227278524752 -112.70710678118652 -80 ) ( 163.4522727852475 -111.29289321881345 -80 ) placeholder -2.27417 3.882248 315 1 1 +( 253.25483399593907 -112 -48 ) ( 253.96194077712562 -111.29289321881353 -48 ) ( 253.9619407771256 -112.70710678118664 -48 ) placeholder -2.27417 3.882248 315 1 1 +( 253.25483399593907 -112 -64 ) ( 253.25483399593907 -112 -63 ) ( 253.96194077712562 -111.29289321881353 -64 ) placeholder -6.156433 0 0 0.70710677 1 +( 253.25483399593907 -112 -64 ) ( 253.9619407771256 -112.70710678118664 -64 ) ( 253.25483399593907 -112 -63 ) placeholder 97.60808 0 180 0.70710677 -1 +} +// brush 8 +{ +( -32 96 -80 ) ( -32 97 -80 ) ( -32 96 -79 ) placeholder 0 0 0 1 1 +( 96 272 -64 ) ( 97 272.625 -64 ) ( 96 272 -63 ) placeholder 0 0 0 1 1 +( -32 96 -80 ) ( -31 96.625 -80 ) ( -32 97 -80 ) placeholder -73.70788 15.668304 32.00539 1.1792476 0.87100387 +( 96 272 -32 ) ( 96 273 -32 ) ( 97 272.625 -32 ) placeholder -73.70788 15.668304 32.00539 1.1792476 0.87100387 +( -32 96 -80 ) ( -32 96 -79 ) ( -31 96.625 -80 ) placeholder 0 0 0 1 1 +( 96 272 -64 ) ( 96 272 -63 ) ( 96 273 -64 ) placeholder -80 0 0 1 1 +} +// brush 9 +{ +( -64 -672 -96 ) ( -64 -720 -80 ) ( -64 -720 -112 ) placeholder -224 -64 0 0.2 0.2 +( -64 -672 -96 ) ( -16 -672 -80 ) ( -64 -720 -80 ) placeholder 80 224 0 0.2 0.2 +( -64 -720 -80 ) ( -16 -720 -80 ) ( -16 -720 -112 ) placeholder 80 -64 0 0.2 0.2 +( -16 -720 -112 ) ( -16 -672 -112 ) ( -64 -672 -112 ) placeholder 80 224 0 0.2 0.2 +( -64 -720 -80 ) ( -16 -672 -80 ) ( -16 -720 -80 ) placeholder 80 224 0 0.2 0.2 +( -16 -672 -112 ) ( -16 -672 -80 ) ( -64 -672 -96 ) placeholder 80 -64 0 0.2 0.2 +( -16 -720 -80 ) ( -16 -672 -80 ) ( -16 -672 -112 ) placeholder -224 -64 0 0.2 0.2 +} +// brush 10 +{ +( -16 -672 -80 ) ( -16 -720 -80 ) ( -16 -720 -112 ) placeholder -224 -64 0 0.2 0.2 +( -16 -672 -80 ) ( 32 -672 -48 ) ( -16 -720 -80 ) placeholder -160 224 0 0.2 0.2 +( -16 -720 -80 ) ( 32 -720 -80 ) ( 32 -720 -112 ) placeholder -160 -64 0 0.2 0.2 +( -16 -720 -80 ) ( 32 -672 -48 ) ( 32 -720 -80 ) placeholder -160 224 0 0.2 0.2 +( 32 -720 -112 ) ( 32 -672 -112 ) ( -16 -672 -112 ) placeholder -160 224 0 0.2 0.2 +( 32 -672 -112 ) ( 32 -672 -48 ) ( -16 -672 -80 ) placeholder -160 -64 0 0.2 0.2 +( 32 -720 -80 ) ( 32 -672 -48 ) ( 32 -672 -112 ) placeholder -224 -64 0 0.2 0.2 +} +// brush 11 +{ +( 32 -672 -48 ) ( 32 -720 -80 ) ( 32 -720 -112 ) placeholder -224 -64 0 0.2 0.2 +( 32 -720 -80 ) ( 80 -720 -80 ) ( 80 -720 -112 ) placeholder -144 -64 0 0.2 0.2 +( 32 -672 -48 ) ( 80 -720 -80 ) ( 32 -720 -80 ) placeholder -144 224 0 0.2 0.2 +( 80 -720 -112 ) ( 80 -672 -112 ) ( 32 -672 -112 ) placeholder -144 224 0 0.2 0.2 +( 80 -672 -112 ) ( 80 -672 -80 ) ( 32 -672 -48 ) placeholder -144 -64 0 0.2 0.2 +( 32 -672 -48 ) ( 80 -672 -80 ) ( 80 -720 -80 ) placeholder -144 224 0 0.2 0.2 +( 80 -720 -80 ) ( 80 -672 -80 ) ( 80 -672 -112 ) placeholder -224 -64 0 0.2 0.2 +} +// brush 12 +{ +( -112 -624 -80 ) ( -112 -672 -80 ) ( -112 -672 -112 ) placeholder -208 -64 0 0.2 0.2 +( -112 -672 -80 ) ( -64 -672 -96 ) ( -64 -672 -112 ) placeholder 64 -64 0 0.2 0.2 +( -64 -672 -112 ) ( -64 -624 -112 ) ( -112 -624 -112 ) placeholder 64 208 0 0.2 0.2 +( -112 -624 -80 ) ( -64 -624 -80 ) ( -112 -672 -80 ) placeholder 64 208 0 0.2 0.2 +( -64 -624 -112 ) ( -64 -624 -80 ) ( -112 -624 -80 ) placeholder 64 -64 0 0.2 0.2 +( -112 -672 -80 ) ( -64 -624 -80 ) ( -64 -672 -96 ) placeholder 64 208 0 0.2 0.2 +( -64 -672 -96 ) ( -64 -624 -80 ) ( -64 -624 -112 ) placeholder -208 -64 0 0.2 0.2 +} +// brush 13 +{ +( -112 -384 -112 ) ( -112 -383 -112 ) ( -112 -384 -111 ) placeholder -192 -64 0 0.2 0.2 +( -112 -624 -112 ) ( -112 -624 -111 ) ( -111 -624 -112 ) placeholder 64 -64 0 0.2 0.2 +( -112 -384 -112 ) ( -111 -384 -112 ) ( -112 -383 -112 ) placeholder 64 192 0 0.2 0.2 +( 16 -256 -80 ) ( 16 -255 -80 ) ( 17 -256 -80 ) placeholder 64 192 0 0.2 0.2 +( 16 -576 -80 ) ( 17 -576 -80 ) ( 16 -576 -79 ) placeholder 64 -64 0 0.2 0.2 +( -64 -256 -80 ) ( -64 -256 -79 ) ( -64 -255 -80 ) placeholder -192 -64 0 0.2 0.2 +} +// brush 14 +{ +( -64 -624 -80 ) ( -64 -672 -96 ) ( -64 -672 -112 ) placeholder -208 -64 0 0.2 0.2 +( -64 -624 -80 ) ( -16 -672 -80 ) ( -64 -672 -96 ) placeholder 80 208 0 0.2 0.2 +( -64 -672 -96 ) ( -16 -672 -80 ) ( -16 -672 -112 ) placeholder 80 -64 0 0.2 0.2 +( -16 -672 -112 ) ( -16 -624 -112 ) ( -64 -624 -112 ) placeholder 80 208 0 0.2 0.2 +( -64 -624 -80 ) ( -16 -624 -80 ) ( -16 -672 -80 ) placeholder 80 208 0 0.2 0.2 +( -16 -624 -112 ) ( -16 -624 -80 ) ( -64 -624 -80 ) placeholder 80 -64 0 0.2 0.2 +( -16 -672 -80 ) ( -16 -624 -80 ) ( -16 -624 -112 ) placeholder -208 -64 0 0.2 0.2 +} +// brush 15 +{ +( -112 -528 -80 ) ( -112 -576 -80 ) ( -112 -576 -112 ) placeholder -176 -64 0 0.2 0.2 +( -112 -528 -80 ) ( -64 -528 -48 ) ( -112 -576 -80 ) placeholder 64 176 0 0.2 0.2 +( -112 -576 -80 ) ( -64 -576 -80 ) ( -64 -576 -112 ) placeholder 64 -64 0 0.2 0.2 +( -112 -576 -80 ) ( -64 -528 -48 ) ( -64 -576 -80 ) placeholder 64 176 0 0.2 0.2 +( -64 -576 -112 ) ( -64 -528 -112 ) ( -112 -528 -112 ) placeholder 64 176 0 0.2 0.2 +( -64 -528 -112 ) ( -64 -528 -48 ) ( -112 -528 -80 ) placeholder 64 -64 0 0.2 0.2 +( -64 -576 -80 ) ( -64 -528 -48 ) ( -64 -528 -112 ) placeholder -176 -64 0 0.2 0.2 +} +// brush 16 +{ +( -64 -384 -112 ) ( -64 -383 -112 ) ( -64 -384 -111 ) placeholder -192 -64 0 0.2 0.2 +( -64 -624 -112 ) ( -64 -624 -111 ) ( -63 -624 -112 ) placeholder 80 -64 0 0.2 0.2 +( -64 -384 -112 ) ( -63 -384 -112 ) ( -64 -383 -112 ) placeholder 80 192 0 0.2 0.2 +( 64 -256 -80 ) ( 64 -255 -80 ) ( 65 -256 -80 ) placeholder 80 192 0 0.2 0.2 +( 64 -576 -80 ) ( 65 -576 -80 ) ( 64 -576 -79 ) placeholder 80 -64 0 0.2 0.2 +( -16 -256 -80 ) ( -16 -256 -79 ) ( -16 -255 -80 ) placeholder -192 -64 0 0.2 0.2 +} +// brush 17 +{ +( -16 -624 -80 ) ( -16 -672 -80 ) ( -16 -672 -112 ) placeholder -208 -64 0 0.2 0.2 +( -16 -624 -80 ) ( 32 -672 -48 ) ( -16 -672 -80 ) placeholder -160 208 0 0.2 0.2 +( -16 -672 -80 ) ( 32 -672 -48 ) ( 32 -672 -112 ) placeholder -160 -64 0 0.2 0.2 +( 32 -672 -112 ) ( 32 -624 -112 ) ( -16 -624 -112 ) placeholder -160 208 0 0.2 0.2 +( -16 -624 -80 ) ( 32 -624 -80 ) ( 32 -672 -48 ) placeholder -160 208 0 0.2 0.2 +( 32 -624 -112 ) ( 32 -624 -80 ) ( -16 -624 -80 ) placeholder -160 -64 0 0.2 0.2 +( 32 -672 -48 ) ( 32 -624 -80 ) ( 32 -624 -112 ) placeholder -208 -64 0 0.2 0.2 +} +// brush 18 +{ +( -112 -480 -80 ) ( -112 -528 -80 ) ( -112 -528 -112 ) placeholder -160 -64 0 0.2 0.2 +( -112 -480 -80 ) ( -64 -528 -48 ) ( -112 -528 -80 ) placeholder 64 160 0 0.2 0.2 +( -112 -528 -80 ) ( -64 -528 -48 ) ( -64 -528 -112 ) placeholder 64 -64 0 0.2 0.2 +( -64 -528 -112 ) ( -64 -480 -112 ) ( -112 -480 -112 ) placeholder 64 160 0 0.2 0.2 +( -112 -480 -80 ) ( -64 -480 -80 ) ( -64 -528 -48 ) placeholder 64 160 0 0.2 0.2 +( -64 -480 -112 ) ( -64 -480 -80 ) ( -112 -480 -80 ) placeholder 64 -64 0 0.2 0.2 +( -64 -528 -48 ) ( -64 -480 -80 ) ( -64 -480 -112 ) placeholder -160 -64 0 0.2 0.2 +} +// brush 19 +{ +( -64 -528 -48 ) ( -64 -576 -80 ) ( -64 -576 -112 ) placeholder -176 -64 0 0.2 0.2 +( -64 -576 -80 ) ( -16 -576 -80 ) ( -16 -576 -112 ) placeholder 80 -64 0 0.2 0.2 +( -64 -528 -48 ) ( -16 -576 -80 ) ( -64 -576 -80 ) placeholder 80 176 0 0.2 0.2 +( -16 -576 -112 ) ( -16 -528 -112 ) ( -64 -528 -112 ) placeholder 80 176 0 0.2 0.2 +( -16 -528 -112 ) ( -16 -528 -80 ) ( -64 -528 -48 ) placeholder 80 -64 0 0.2 0.2 +( -64 -528 -48 ) ( -16 -528 -80 ) ( -16 -576 -80 ) placeholder 80 176 0 0.2 0.2 +( -16 -576 -80 ) ( -16 -528 -80 ) ( -16 -528 -112 ) placeholder -176 -64 0 0.2 0.2 +} +// brush 20 +{ +( -16 -384 -112 ) ( -16 -383 -112 ) ( -16 -384 -111 ) placeholder -192 -64 0 0.2 0.2 +( -16 -624 -112 ) ( -16 -624 -111 ) ( -15 -624 -112 ) placeholder -160 -64 0 0.2 0.2 +( -16 -384 -112 ) ( -15 -384 -112 ) ( -16 -383 -112 ) placeholder -160 192 0 0.2 0.2 +( 112 -256 -80 ) ( 112 -255 -80 ) ( 113 -256 -80 ) placeholder -160 192 0 0.2 0.2 +( 112 -576 -80 ) ( 113 -576 -80 ) ( 112 -576 -79 ) placeholder -160 -64 0 0.2 0.2 +( 32 -256 -80 ) ( 32 -256 -79 ) ( 32 -255 -80 ) placeholder -192 -64 0 0.2 0.2 +} +// brush 21 +{ +( 32 -624 -80 ) ( 32 -672 -48 ) ( 32 -672 -112 ) placeholder -208 -64 0 0.2 0.2 +( 32 -672 -48 ) ( 80 -672 -80 ) ( 80 -672 -112 ) placeholder -144 -64 0 0.2 0.2 +( 80 -672 -112 ) ( 80 -624 -112 ) ( 32 -624 -112 ) placeholder -144 208 0 0.2 0.2 +( 32 -624 -80 ) ( 80 -624 -80 ) ( 32 -672 -48 ) placeholder -144 208 0 0.2 0.2 +( 80 -624 -112 ) ( 80 -624 -80 ) ( 32 -624 -80 ) placeholder -144 -64 0 0.2 0.2 +( 32 -672 -48 ) ( 80 -624 -80 ) ( 80 -672 -80 ) placeholder -144 208 0 0.2 0.2 +( 80 -672 -80 ) ( 80 -624 -80 ) ( 80 -624 -112 ) placeholder -208 -64 0 0.2 0.2 +} +// brush 22 +{ +( -64 -480 -80 ) ( -64 -528 -48 ) ( -64 -528 -112 ) placeholder -160 -64 0 0.2 0.2 +( -64 -528 -48 ) ( -16 -528 -80 ) ( -16 -528 -112 ) placeholder 80 -64 0 0.2 0.2 +( -16 -528 -112 ) ( -16 -480 -112 ) ( -64 -480 -112 ) placeholder 80 160 0 0.2 0.2 +( -64 -480 -80 ) ( -16 -480 -80 ) ( -64 -528 -48 ) placeholder 80 160 0 0.2 0.2 +( -16 -480 -112 ) ( -16 -480 -80 ) ( -64 -480 -80 ) placeholder 80 -64 0 0.2 0.2 +( -64 -528 -48 ) ( -16 -480 -80 ) ( -16 -528 -80 ) placeholder 80 160 0 0.2 0.2 +( -16 -528 -80 ) ( -16 -480 -80 ) ( -16 -480 -112 ) placeholder -160 -64 0 0.2 0.2 +} +// brush 23 +{ +( -16 -528 -80 ) ( -16 -576 -80 ) ( -16 -576 -112 ) placeholder -176 -64 0 0.2 0.2 +( -16 -576 -80 ) ( 32 -576 -80 ) ( 32 -576 -112 ) placeholder -160 -64 0 0.2 0.2 +( 32 -576 -112 ) ( 32 -528 -112 ) ( -16 -528 -112 ) placeholder -160 176 0 0.2 0.2 +( -16 -528 -80 ) ( 32 -576 -80 ) ( -16 -576 -80 ) placeholder -160 176 0 0.2 0.2 +( 32 -528 -112 ) ( 32 -528 -96 ) ( -16 -528 -80 ) placeholder -160 -64 0 0.2 0.2 +( -16 -528 -80 ) ( 32 -528 -96 ) ( 32 -576 -80 ) placeholder -160 176 0 0.2 0.2 +( 32 -576 -80 ) ( 32 -528 -96 ) ( 32 -528 -112 ) placeholder -176 -64 0 0.2 0.2 +} +// brush 24 +{ +( 32 -384 -112 ) ( 32 -383 -112 ) ( 32 -384 -111 ) placeholder -192 -64 0 0.2 0.2 +( 32 -624 -112 ) ( 32 -624 -111 ) ( 33 -624 -112 ) placeholder -144 -64 0 0.2 0.2 +( 32 -384 -112 ) ( 33 -384 -112 ) ( 32 -383 -112 ) placeholder -144 192 0 0.2 0.2 +( 160 -256 -80 ) ( 160 -255 -80 ) ( 161 -256 -80 ) placeholder -144 192 0 0.2 0.2 +( 160 -576 -80 ) ( 161 -576 -80 ) ( 160 -576 -79 ) placeholder -144 -64 0 0.2 0.2 +( 80 -256 -80 ) ( 80 -256 -79 ) ( 80 -255 -80 ) placeholder -192 -64 0 0.2 0.2 +} +// brush 25 +{ +( -16 -480 -80 ) ( -16 -528 -80 ) ( -16 -528 -112 ) placeholder -160 -64 0 0.2 0.2 +( -16 -528 -80 ) ( 32 -528 -96 ) ( 32 -528 -112 ) placeholder -160 -64 0 0.2 0.2 +( 32 -528 -112 ) ( 32 -480 -112 ) ( -16 -480 -112 ) placeholder -160 160 0 0.2 0.2 +( -16 -480 -80 ) ( 32 -480 -80 ) ( -16 -528 -80 ) placeholder -160 160 0 0.2 0.2 +( 32 -480 -112 ) ( 32 -480 -80 ) ( -16 -480 -80 ) placeholder -160 -64 0 0.2 0.2 +( -16 -528 -80 ) ( 32 -480 -80 ) ( 32 -528 -96 ) placeholder -160 160 0 0.2 0.2 +( 32 -528 -96 ) ( 32 -480 -80 ) ( 32 -480 -112 ) placeholder -160 -64 0 0.2 0.2 +} +// brush 26 +{ +( 32 -528 -96 ) ( 32 -576 -80 ) ( 32 -576 -112 ) placeholder -176 -64 0 0.2 0.2 +( 32 -528 -96 ) ( 80 -528 -80 ) ( 32 -576 -80 ) placeholder -144 176 0 0.2 0.2 +( 32 -576 -80 ) ( 80 -576 -80 ) ( 80 -576 -112 ) placeholder -144 -64 0 0.2 0.2 +( 80 -576 -112 ) ( 80 -528 -112 ) ( 32 -528 -112 ) placeholder -144 176 0 0.2 0.2 +( 32 -576 -80 ) ( 80 -528 -80 ) ( 80 -576 -80 ) placeholder -144 176 0 0.2 0.2 +( 80 -528 -112 ) ( 80 -528 -80 ) ( 32 -528 -96 ) placeholder -144 -64 0 0.2 0.2 +( 80 -576 -80 ) ( 80 -528 -80 ) ( 80 -528 -112 ) placeholder -176 -64 0 0.2 0.2 +} +// brush 27 +{ +( 32 -480 -80 ) ( 32 -528 -96 ) ( 32 -528 -112 ) placeholder -160 -64 0 0.2 0.2 +( 32 -480 -80 ) ( 80 -528 -80 ) ( 32 -528 -96 ) placeholder -144 160 0 0.2 0.2 +( 32 -528 -96 ) ( 80 -528 -80 ) ( 80 -528 -112 ) placeholder -144 -64 0 0.2 0.2 +( 80 -528 -112 ) ( 80 -480 -112 ) ( 32 -480 -112 ) placeholder -144 160 0 0.2 0.2 +( 32 -480 -80 ) ( 80 -480 -80 ) ( 80 -528 -80 ) placeholder -144 160 0 0.2 0.2 +( 80 -480 -112 ) ( 80 -480 -80 ) ( 32 -480 -80 ) placeholder -144 -64 0 0.2 0.2 +( 80 -528 -80 ) ( 80 -480 -80 ) ( 80 -480 -112 ) placeholder -160 -64 0 0.2 0.2 +} +// brush 28 +{ +( 224 256 112 ) ( 224 257 112 ) ( 224 256 113 ) placeholder 170.66669 8 0 -0.375 0.4 +( 224 256 112 ) ( 224 256 113 ) ( 225 256 112 ) placeholder 224 -256 0 0.4 0.4 +( 224 256 112 ) ( 225 256 112 ) ( 224 257 112 ) placeholder 47.999992 138.99998 0 -0.4 0.4 +( 320 336 208 ) ( 320 337 208 ) ( 321 336 208 ) placeholder 208 112 0 0.4 0.4 +( 320 352 128 ) ( 321 352 128 ) ( 320 352 129 ) placeholder 48 -256 0 -0.4 0.4 +( 320 336 128 ) ( 320 336 129 ) ( 320 337 128 ) placeholder 128 -256 0 0.4 0.4 +} +} +// entity 1 +{ +"classname" "info_player_start" +"origin" "112 -48 56" +} diff --git a/assets/maps/autosave/test.3.map b/assets/maps/autosave/test.3.map new file mode 100644 index 0000000..720b246 --- /dev/null +++ b/assets/maps/autosave/test.3.map @@ -0,0 +1,303 @@ +// Game: Generic +// Format: Standard +// entity 0 +{ +"classname" "worldspawn" +"wad" "" +// brush 0 +{ +( -112 -672 -80 ) ( -112 -720 -80 ) ( -112 -720 -112 ) placeholder -224 -64 0 0.2 0.2 +( -112 -720 -80 ) ( -64 -720 -80 ) ( -64 -720 -112 ) placeholder 64 -64 0 0.2 0.2 +( -64 -720 -112 ) ( -64 -672 -112 ) ( -112 -672 -112 ) placeholder 64 224 0 0.2 0.2 +( -112 -672 -80 ) ( -64 -720 -80 ) ( -112 -720 -80 ) placeholder 64 224 0 0.2 0.2 +( -64 -672 -112 ) ( -64 -672 -96 ) ( -112 -672 -80 ) placeholder 64 -64 0 0.2 0.2 +( -112 -672 -80 ) ( -64 -672 -96 ) ( -64 -720 -80 ) placeholder 64 224 0 0.2 0.2 +( -64 -720 -80 ) ( -64 -672 -96 ) ( -64 -672 -112 ) placeholder -224 -64 0 0.2 0.2 +} +// brush 1 +{ +( 0 -128 -16 ) ( 0 -127 -16 ) ( 0 -128 -15 ) placeholder 0 0 0 1 1 +( 0 -128 -16 ) ( 0 -128 -15 ) ( 1 -128 -16 ) placeholder -32 0 0 1 1 +( 0 -128 -16 ) ( 1 -128 -16 ) ( 0 -127 -16 ) placeholder -32 0 0 1 1 +( 64 -96 0 ) ( 64 -95 0 ) ( 65 -96 0 ) placeholder -32 0 0 1 1 +( 64 -96 0 ) ( 65 -96 0 ) ( 64 -96 1 ) placeholder -32 0 0 1 1 +( 64 -96 0 ) ( 64 -96 1 ) ( 64 -95 0 ) placeholder 0 0 0 1 1 +} +// brush 2 +{ +( 64 -144 -32 ) ( 64 -143 -32 ) ( 64 -144 -31 ) placeholder -16 -16 0 1 1 +( 64 -144 -32 ) ( 64 -144 -31 ) ( 65 -144 -32 ) placeholder -32 -16 0 1 1 +( 64 -144 -32 ) ( 65 -144 -32 ) ( 64 -143 -32 ) placeholder -32 16 0 1 1 +( 192 -80 -16 ) ( 192 -79 -16 ) ( 193 -80 -16 ) placeholder -32 16 0 1 1 +( 192 -80 -16 ) ( 193 -80 -16 ) ( 192 -80 -15 ) placeholder -32 -16 0 1 1 +( 192 -80 -16 ) ( 192 -80 -15 ) ( 192 -79 -16 ) placeholder -16 -16 0 1 1 +} +// brush 3 +{ +( 80 16 32 ) ( 80 15 32 ) ( 80 16 31 ) placeholder 0 -16 180 1 1 +( 144 -96 16 ) ( 145 -96 16 ) ( 144 -96 15 ) placeholder 16 -16 0 1 -1 +( 144 -96 16 ) ( 144 -97 16 ) ( 145 -96 16 ) placeholder 16 0 0 1 -1 +( 80 16 32 ) ( 81 16 32 ) ( 80 15 32 ) placeholder 16 0 0 1 -1 +( 80 16 32 ) ( 80 16 31 ) ( 81 16 32 ) placeholder 16 -16 0 1 -1 +( 144 -96 16 ) ( 144 -96 15 ) ( 144 -97 16 ) placeholder 0 -16 180 1 1 +} +// brush 4 +{ +( 240 -64 -80 ) ( 240 0 -80 ) ( 240 -112 -56 ) __TB_empty -128 -60 0 1 2 +( 240 -112 -56 ) ( 304 -112 -80 ) ( 240 -64 -80 ) __TB_empty -32 8 0 1 -2 +( 240 0 -80 ) ( 288 -48 -56 ) ( 240 -112 -56 ) __TB_empty -80.41865 55.557785 323.1301 1.1743635 1.1509081 +( 288 -48 -56 ) ( 304 -112 -40 ) ( 240 -112 -56 ) __TB_empty -32 112 0 1 1 +( 240 -112 -56 ) ( 304 -112 -40 ) ( 304 -112 -80 ) __TB_empty -32 -16.941177 0 1 -2 +( 304 -112 -80 ) ( 304 0 -80 ) ( 240 0 -80 ) __TB_empty -32 144 0 1 1 +( 288 -48 -56 ) ( 320 -48 -56 ) ( 304 -112 -40 ) __TB_empty -32 112 0 1 1 +( 304 0 -80 ) ( 320 -48 -56 ) ( 288 -48 -56 ) __TB_empty -32 -32 0 1 -1 +( 304 -112 -80 ) ( 320 -48 -56 ) ( 304 0 -80 ) __TB_empty 144 -52 0 1 2 +( 304 -112 -40 ) ( 320 -48 -56 ) ( 304 -112 -80 ) __TB_empty 118.95169 -69.728745 0 1 1.1 +} +// brush 5 +{ +( -144 -560 -80 ) ( -144 -559 -80 ) ( -144 -560 -79 ) placeholder -80 96 0 0.2 0.2 +( -144 -320 -80 ) ( -144 -320 -79 ) ( -143 -320 -80 ) placeholder -32 96 0 0.2 0.2 +( -144 -560 -80 ) ( -143 -560 -80 ) ( -144 -559 -80 ) placeholder -32 80 0 0.2 0.2 +( -16 -432 16 ) ( -16 -431 16 ) ( -15 -432 16 ) placeholder -32 80 0 0.2 0.2 +( -16 -304 -48 ) ( -15 -304 -48 ) ( -16 -304 -47 ) placeholder -32 96 0 0.2 0.2 +( 304 -432 -48 ) ( 304 -432 -47 ) ( 304 -431 -48 ) placeholder -80 96 0 0.2 0.2 +} +// brush 6 +{ +( -160 -240 -16 ) ( -160 -241 -16 ) ( -160 -240 -17 ) placeholder -240 96 180 1 1 +( -64 -304 -32 ) ( -63 -304 -32 ) ( -64 -304 -33 ) placeholder 208 96 0 1 -1 +( -64 -352 -80 ) ( -64 -353 -80 ) ( -63 -352 -80 ) placeholder 208 240 0 1 -1 +( -160 -240 16 ) ( -159 -240 16 ) ( -160 -241 16 ) placeholder 208 240 0 1 -1 +( -160 192 -16 ) ( -160 192 -17 ) ( -159 192 -16 ) placeholder 208 96 0 1 -1 +( -144 -352 -32 ) ( -144 -352 -33 ) ( -144 -353 -32 ) placeholder -240 96 180 1 1 +} +// brush 7 +{ +( 162.74516600406096 -112 -80 ) ( 163.4522727852475 -111.29289321881345 -80 ) ( 162.74516600406096 -112 -79 ) placeholder 190.39192 0 0 0.70710677 1 +( 162.74516600406096 -112 -80 ) ( 162.74516600406096 -112 -79 ) ( 163.45227278524752 -112.70710678118652 -80 ) placeholder -38.156433 0 0 0.70710677 1 +( 162.74516600406096 -112 -80 ) ( 163.45227278524752 -112.70710678118652 -80 ) ( 163.4522727852475 -111.29289321881345 -80 ) placeholder -2.27417 3.882248 315 1 1 +( 253.25483399593907 -112 -48 ) ( 253.96194077712562 -111.29289321881353 -48 ) ( 253.9619407771256 -112.70710678118664 -48 ) placeholder 163.08612 199.7645 315 0.3 0.3 +( 253.25483399593907 -112 -64 ) ( 253.25483399593907 -112 -63 ) ( 253.96194077712562 -111.29289321881353 -64 ) placeholder -6.156433 0 0 0.70710677 1 +( 253.25483399593907 -112 -64 ) ( 253.9619407771256 -112.70710678118664 -64 ) ( 253.25483399593907 -112 -63 ) placeholder 97.60808 0 180 0.70710677 -1 +} +// brush 8 +{ +( -32 96 -80 ) ( -32 97 -80 ) ( -32 96 -79 ) placeholder 0 0 0 1 1 +( 96 272 -64 ) ( 97 272.625 -64 ) ( 96 272 -63 ) placeholder 0 0 0 1 1 +( -32 96 -80 ) ( -31 96.625 -80 ) ( -32 97 -80 ) placeholder -73.70788 15.668304 32.00539 1.1792476 0.87100387 +( 96 272 -32 ) ( 96 273 -32 ) ( 97 272.625 -32 ) placeholder -73.70788 15.668304 32.00539 1.1792476 0.87100387 +( -32 96 -80 ) ( -32 96 -79 ) ( -31 96.625 -80 ) placeholder 0 0 0 1 1 +( 96 272 -64 ) ( 96 272 -63 ) ( 96 273 -64 ) placeholder -80 0 0 1 1 +} +// brush 9 +{ +( -64 -672 -96 ) ( -64 -720 -80 ) ( -64 -720 -112 ) placeholder -224 -64 0 0.2 0.2 +( -64 -672 -96 ) ( -16 -672 -80 ) ( -64 -720 -80 ) placeholder 80 224 0 0.2 0.2 +( -64 -720 -80 ) ( -16 -720 -80 ) ( -16 -720 -112 ) placeholder 80 -64 0 0.2 0.2 +( -16 -720 -112 ) ( -16 -672 -112 ) ( -64 -672 -112 ) placeholder 80 224 0 0.2 0.2 +( -64 -720 -80 ) ( -16 -672 -80 ) ( -16 -720 -80 ) placeholder 80 224 0 0.2 0.2 +( -16 -672 -112 ) ( -16 -672 -80 ) ( -64 -672 -96 ) placeholder 80 -64 0 0.2 0.2 +( -16 -720 -80 ) ( -16 -672 -80 ) ( -16 -672 -112 ) placeholder -224 -64 0 0.2 0.2 +} +// brush 10 +{ +( -16 -672 -80 ) ( -16 -720 -80 ) ( -16 -720 -112 ) placeholder -224 -64 0 0.2 0.2 +( -16 -672 -80 ) ( 32 -672 -48 ) ( -16 -720 -80 ) placeholder -160 224 0 0.2 0.2 +( -16 -720 -80 ) ( 32 -720 -80 ) ( 32 -720 -112 ) placeholder -160 -64 0 0.2 0.2 +( -16 -720 -80 ) ( 32 -672 -48 ) ( 32 -720 -80 ) placeholder -160 224 0 0.2 0.2 +( 32 -720 -112 ) ( 32 -672 -112 ) ( -16 -672 -112 ) placeholder -160 224 0 0.2 0.2 +( 32 -672 -112 ) ( 32 -672 -48 ) ( -16 -672 -80 ) placeholder -160 -64 0 0.2 0.2 +( 32 -720 -80 ) ( 32 -672 -48 ) ( 32 -672 -112 ) placeholder -224 -64 0 0.2 0.2 +} +// brush 11 +{ +( 32 -672 -48 ) ( 32 -720 -80 ) ( 32 -720 -112 ) placeholder -224 -64 0 0.2 0.2 +( 32 -720 -80 ) ( 80 -720 -80 ) ( 80 -720 -112 ) placeholder -144 -64 0 0.2 0.2 +( 32 -672 -48 ) ( 80 -720 -80 ) ( 32 -720 -80 ) placeholder -144 224 0 0.2 0.2 +( 80 -720 -112 ) ( 80 -672 -112 ) ( 32 -672 -112 ) placeholder -144 224 0 0.2 0.2 +( 80 -672 -112 ) ( 80 -672 -80 ) ( 32 -672 -48 ) placeholder -144 -64 0 0.2 0.2 +( 32 -672 -48 ) ( 80 -672 -80 ) ( 80 -720 -80 ) placeholder -144 224 0 0.2 0.2 +( 80 -720 -80 ) ( 80 -672 -80 ) ( 80 -672 -112 ) placeholder -224 -64 0 0.2 0.2 +} +// brush 12 +{ +( -112 -624 -80 ) ( -112 -672 -80 ) ( -112 -672 -112 ) placeholder -208 -64 0 0.2 0.2 +( -112 -672 -80 ) ( -64 -672 -96 ) ( -64 -672 -112 ) placeholder 64 -64 0 0.2 0.2 +( -64 -672 -112 ) ( -64 -624 -112 ) ( -112 -624 -112 ) placeholder 64 208 0 0.2 0.2 +( -112 -624 -80 ) ( -64 -624 -80 ) ( -112 -672 -80 ) placeholder 64 208 0 0.2 0.2 +( -64 -624 -112 ) ( -64 -624 -80 ) ( -112 -624 -80 ) placeholder 64 -64 0 0.2 0.2 +( -112 -672 -80 ) ( -64 -624 -80 ) ( -64 -672 -96 ) placeholder 64 208 0 0.2 0.2 +( -64 -672 -96 ) ( -64 -624 -80 ) ( -64 -624 -112 ) placeholder -208 -64 0 0.2 0.2 +} +// brush 13 +{ +( -112 -384 -112 ) ( -112 -383 -112 ) ( -112 -384 -111 ) placeholder -192 -64 0 0.2 0.2 +( -112 -624 -112 ) ( -112 -624 -111 ) ( -111 -624 -112 ) placeholder 64 -64 0 0.2 0.2 +( -112 -384 -112 ) ( -111 -384 -112 ) ( -112 -383 -112 ) placeholder 64 192 0 0.2 0.2 +( 16 -256 -80 ) ( 16 -255 -80 ) ( 17 -256 -80 ) placeholder 64 192 0 0.2 0.2 +( 16 -576 -80 ) ( 17 -576 -80 ) ( 16 -576 -79 ) placeholder 64 -64 0 0.2 0.2 +( -64 -256 -80 ) ( -64 -256 -79 ) ( -64 -255 -80 ) placeholder -192 -64 0 0.2 0.2 +} +// brush 14 +{ +( -64 -624 -80 ) ( -64 -672 -96 ) ( -64 -672 -112 ) placeholder -208 -64 0 0.2 0.2 +( -64 -624 -80 ) ( -16 -672 -80 ) ( -64 -672 -96 ) placeholder 80 208 0 0.2 0.2 +( -64 -672 -96 ) ( -16 -672 -80 ) ( -16 -672 -112 ) placeholder 80 -64 0 0.2 0.2 +( -16 -672 -112 ) ( -16 -624 -112 ) ( -64 -624 -112 ) placeholder 80 208 0 0.2 0.2 +( -64 -624 -80 ) ( -16 -624 -80 ) ( -16 -672 -80 ) placeholder 80 208 0 0.2 0.2 +( -16 -624 -112 ) ( -16 -624 -80 ) ( -64 -624 -80 ) placeholder 80 -64 0 0.2 0.2 +( -16 -672 -80 ) ( -16 -624 -80 ) ( -16 -624 -112 ) placeholder -208 -64 0 0.2 0.2 +} +// brush 15 +{ +( -112 -528 -80 ) ( -112 -576 -80 ) ( -112 -576 -112 ) placeholder -176 -64 0 0.2 0.2 +( -112 -528 -80 ) ( -64 -528 -48 ) ( -112 -576 -80 ) placeholder 64 176 0 0.2 0.2 +( -112 -576 -80 ) ( -64 -576 -80 ) ( -64 -576 -112 ) placeholder 64 -64 0 0.2 0.2 +( -112 -576 -80 ) ( -64 -528 -48 ) ( -64 -576 -80 ) placeholder 64 176 0 0.2 0.2 +( -64 -576 -112 ) ( -64 -528 -112 ) ( -112 -528 -112 ) placeholder 64 176 0 0.2 0.2 +( -64 -528 -112 ) ( -64 -528 -48 ) ( -112 -528 -80 ) placeholder 64 -64 0 0.2 0.2 +( -64 -576 -80 ) ( -64 -528 -48 ) ( -64 -528 -112 ) placeholder -176 -64 0 0.2 0.2 +} +// brush 16 +{ +( -64 -384 -112 ) ( -64 -383 -112 ) ( -64 -384 -111 ) placeholder -192 -64 0 0.2 0.2 +( -64 -624 -112 ) ( -64 -624 -111 ) ( -63 -624 -112 ) placeholder 80 -64 0 0.2 0.2 +( -64 -384 -112 ) ( -63 -384 -112 ) ( -64 -383 -112 ) placeholder 80 192 0 0.2 0.2 +( 64 -256 -80 ) ( 64 -255 -80 ) ( 65 -256 -80 ) placeholder 80 192 0 0.2 0.2 +( 64 -576 -80 ) ( 65 -576 -80 ) ( 64 -576 -79 ) placeholder 80 -64 0 0.2 0.2 +( -16 -256 -80 ) ( -16 -256 -79 ) ( -16 -255 -80 ) placeholder -192 -64 0 0.2 0.2 +} +// brush 17 +{ +( -16 -624 -80 ) ( -16 -672 -80 ) ( -16 -672 -112 ) placeholder -208 -64 0 0.2 0.2 +( -16 -624 -80 ) ( 32 -672 -48 ) ( -16 -672 -80 ) placeholder -160 208 0 0.2 0.2 +( -16 -672 -80 ) ( 32 -672 -48 ) ( 32 -672 -112 ) placeholder -160 -64 0 0.2 0.2 +( 32 -672 -112 ) ( 32 -624 -112 ) ( -16 -624 -112 ) placeholder -160 208 0 0.2 0.2 +( -16 -624 -80 ) ( 32 -624 -80 ) ( 32 -672 -48 ) placeholder -160 208 0 0.2 0.2 +( 32 -624 -112 ) ( 32 -624 -80 ) ( -16 -624 -80 ) placeholder -160 -64 0 0.2 0.2 +( 32 -672 -48 ) ( 32 -624 -80 ) ( 32 -624 -112 ) placeholder -208 -64 0 0.2 0.2 +} +// brush 18 +{ +( -112 -480 -80 ) ( -112 -528 -80 ) ( -112 -528 -112 ) placeholder -160 -64 0 0.2 0.2 +( -112 -480 -80 ) ( -64 -528 -48 ) ( -112 -528 -80 ) placeholder 64 160 0 0.2 0.2 +( -112 -528 -80 ) ( -64 -528 -48 ) ( -64 -528 -112 ) placeholder 64 -64 0 0.2 0.2 +( -64 -528 -112 ) ( -64 -480 -112 ) ( -112 -480 -112 ) placeholder 64 160 0 0.2 0.2 +( -112 -480 -80 ) ( -64 -480 -80 ) ( -64 -528 -48 ) placeholder 64 160 0 0.2 0.2 +( -64 -480 -112 ) ( -64 -480 -80 ) ( -112 -480 -80 ) placeholder 64 -64 0 0.2 0.2 +( -64 -528 -48 ) ( -64 -480 -80 ) ( -64 -480 -112 ) placeholder -160 -64 0 0.2 0.2 +} +// brush 19 +{ +( -64 -528 -48 ) ( -64 -576 -80 ) ( -64 -576 -112 ) placeholder -176 -64 0 0.2 0.2 +( -64 -576 -80 ) ( -16 -576 -80 ) ( -16 -576 -112 ) placeholder 80 -64 0 0.2 0.2 +( -64 -528 -48 ) ( -16 -576 -80 ) ( -64 -576 -80 ) placeholder 80 176 0 0.2 0.2 +( -16 -576 -112 ) ( -16 -528 -112 ) ( -64 -528 -112 ) placeholder 80 176 0 0.2 0.2 +( -16 -528 -112 ) ( -16 -528 -80 ) ( -64 -528 -48 ) placeholder 80 -64 0 0.2 0.2 +( -64 -528 -48 ) ( -16 -528 -80 ) ( -16 -576 -80 ) placeholder 80 176 0 0.2 0.2 +( -16 -576 -80 ) ( -16 -528 -80 ) ( -16 -528 -112 ) placeholder -176 -64 0 0.2 0.2 +} +// brush 20 +{ +( -16 -384 -112 ) ( -16 -383 -112 ) ( -16 -384 -111 ) placeholder -192 -64 0 0.2 0.2 +( -16 -624 -112 ) ( -16 -624 -111 ) ( -15 -624 -112 ) placeholder -160 -64 0 0.2 0.2 +( -16 -384 -112 ) ( -15 -384 -112 ) ( -16 -383 -112 ) placeholder -160 192 0 0.2 0.2 +( 112 -256 -80 ) ( 112 -255 -80 ) ( 113 -256 -80 ) placeholder -160 192 0 0.2 0.2 +( 112 -576 -80 ) ( 113 -576 -80 ) ( 112 -576 -79 ) placeholder -160 -64 0 0.2 0.2 +( 32 -256 -80 ) ( 32 -256 -79 ) ( 32 -255 -80 ) placeholder -192 -64 0 0.2 0.2 +} +// brush 21 +{ +( 32 -624 -80 ) ( 32 -672 -48 ) ( 32 -672 -112 ) placeholder -208 -64 0 0.2 0.2 +( 32 -672 -48 ) ( 80 -672 -80 ) ( 80 -672 -112 ) placeholder -144 -64 0 0.2 0.2 +( 80 -672 -112 ) ( 80 -624 -112 ) ( 32 -624 -112 ) placeholder -144 208 0 0.2 0.2 +( 32 -624 -80 ) ( 80 -624 -80 ) ( 32 -672 -48 ) placeholder -144 208 0 0.2 0.2 +( 80 -624 -112 ) ( 80 -624 -80 ) ( 32 -624 -80 ) placeholder -144 -64 0 0.2 0.2 +( 32 -672 -48 ) ( 80 -624 -80 ) ( 80 -672 -80 ) placeholder -144 208 0 0.2 0.2 +( 80 -672 -80 ) ( 80 -624 -80 ) ( 80 -624 -112 ) placeholder -208 -64 0 0.2 0.2 +} +// brush 22 +{ +( -64 -480 -80 ) ( -64 -528 -48 ) ( -64 -528 -112 ) placeholder -160 -64 0 0.2 0.2 +( -64 -528 -48 ) ( -16 -528 -80 ) ( -16 -528 -112 ) placeholder 80 -64 0 0.2 0.2 +( -16 -528 -112 ) ( -16 -480 -112 ) ( -64 -480 -112 ) placeholder 80 160 0 0.2 0.2 +( -64 -480 -80 ) ( -16 -480 -80 ) ( -64 -528 -48 ) placeholder 80 160 0 0.2 0.2 +( -16 -480 -112 ) ( -16 -480 -80 ) ( -64 -480 -80 ) placeholder 80 -64 0 0.2 0.2 +( -64 -528 -48 ) ( -16 -480 -80 ) ( -16 -528 -80 ) placeholder 80 160 0 0.2 0.2 +( -16 -528 -80 ) ( -16 -480 -80 ) ( -16 -480 -112 ) placeholder -160 -64 0 0.2 0.2 +} +// brush 23 +{ +( -16 -528 -80 ) ( -16 -576 -80 ) ( -16 -576 -112 ) placeholder -176 -64 0 0.2 0.2 +( -16 -576 -80 ) ( 32 -576 -80 ) ( 32 -576 -112 ) placeholder -160 -64 0 0.2 0.2 +( 32 -576 -112 ) ( 32 -528 -112 ) ( -16 -528 -112 ) placeholder -160 176 0 0.2 0.2 +( -16 -528 -80 ) ( 32 -576 -80 ) ( -16 -576 -80 ) placeholder -160 176 0 0.2 0.2 +( 32 -528 -112 ) ( 32 -528 -96 ) ( -16 -528 -80 ) placeholder -160 -64 0 0.2 0.2 +( -16 -528 -80 ) ( 32 -528 -96 ) ( 32 -576 -80 ) placeholder -160 176 0 0.2 0.2 +( 32 -576 -80 ) ( 32 -528 -96 ) ( 32 -528 -112 ) placeholder -176 -64 0 0.2 0.2 +} +// brush 24 +{ +( 32 -384 -112 ) ( 32 -383 -112 ) ( 32 -384 -111 ) placeholder -192 -64 0 0.2 0.2 +( 32 -624 -112 ) ( 32 -624 -111 ) ( 33 -624 -112 ) placeholder -144 -64 0 0.2 0.2 +( 32 -384 -112 ) ( 33 -384 -112 ) ( 32 -383 -112 ) placeholder -144 192 0 0.2 0.2 +( 160 -256 -80 ) ( 160 -255 -80 ) ( 161 -256 -80 ) placeholder -144 192 0 0.2 0.2 +( 160 -576 -80 ) ( 161 -576 -80 ) ( 160 -576 -79 ) placeholder -144 -64 0 0.2 0.2 +( 80 -256 -80 ) ( 80 -256 -79 ) ( 80 -255 -80 ) placeholder -192 -64 0 0.2 0.2 +} +// brush 25 +{ +( -16 -480 -80 ) ( -16 -528 -80 ) ( -16 -528 -112 ) placeholder -160 -64 0 0.2 0.2 +( -16 -528 -80 ) ( 32 -528 -96 ) ( 32 -528 -112 ) placeholder -160 -64 0 0.2 0.2 +( 32 -528 -112 ) ( 32 -480 -112 ) ( -16 -480 -112 ) placeholder -160 160 0 0.2 0.2 +( -16 -480 -80 ) ( 32 -480 -80 ) ( -16 -528 -80 ) placeholder -160 160 0 0.2 0.2 +( 32 -480 -112 ) ( 32 -480 -80 ) ( -16 -480 -80 ) placeholder -160 -64 0 0.2 0.2 +( -16 -528 -80 ) ( 32 -480 -80 ) ( 32 -528 -96 ) placeholder -160 160 0 0.2 0.2 +( 32 -528 -96 ) ( 32 -480 -80 ) ( 32 -480 -112 ) placeholder -160 -64 0 0.2 0.2 +} +// brush 26 +{ +( 32 -528 -96 ) ( 32 -576 -80 ) ( 32 -576 -112 ) placeholder -176 -64 0 0.2 0.2 +( 32 -528 -96 ) ( 80 -528 -80 ) ( 32 -576 -80 ) placeholder -144 176 0 0.2 0.2 +( 32 -576 -80 ) ( 80 -576 -80 ) ( 80 -576 -112 ) placeholder -144 -64 0 0.2 0.2 +( 80 -576 -112 ) ( 80 -528 -112 ) ( 32 -528 -112 ) placeholder -144 176 0 0.2 0.2 +( 32 -576 -80 ) ( 80 -528 -80 ) ( 80 -576 -80 ) placeholder -144 176 0 0.2 0.2 +( 80 -528 -112 ) ( 80 -528 -80 ) ( 32 -528 -96 ) placeholder -144 -64 0 0.2 0.2 +( 80 -576 -80 ) ( 80 -528 -80 ) ( 80 -528 -112 ) placeholder -176 -64 0 0.2 0.2 +} +// brush 27 +{ +( 32 -480 -80 ) ( 32 -528 -96 ) ( 32 -528 -112 ) placeholder -160 -64 0 0.2 0.2 +( 32 -480 -80 ) ( 80 -528 -80 ) ( 32 -528 -96 ) placeholder -144 160 0 0.2 0.2 +( 32 -528 -96 ) ( 80 -528 -80 ) ( 80 -528 -112 ) placeholder -144 -64 0 0.2 0.2 +( 80 -528 -112 ) ( 80 -480 -112 ) ( 32 -480 -112 ) placeholder -144 160 0 0.2 0.2 +( 32 -480 -80 ) ( 80 -480 -80 ) ( 80 -528 -80 ) placeholder -144 160 0 0.2 0.2 +( 80 -480 -112 ) ( 80 -480 -80 ) ( 32 -480 -80 ) placeholder -144 -64 0 0.2 0.2 +( 80 -528 -80 ) ( 80 -480 -80 ) ( 80 -480 -112 ) placeholder -160 -64 0 0.2 0.2 +} +// brush 28 +{ +( 224 256 112 ) ( 224 257 112 ) ( 224 256 113 ) placeholder 170.66669 8 0 -0.375 0.4 +( 224 256 112 ) ( 224 256 113 ) ( 225 256 112 ) placeholder 224 -256 0 0.4 0.4 +( 224 256 112 ) ( 225 256 112 ) ( 224 257 112 ) placeholder 47.999992 138.99998 0 -0.4 0.4 +( 320 336 208 ) ( 320 337 208 ) ( 321 336 208 ) placeholder 208 112 0 0.4 0.4 +( 320 352 128 ) ( 321 352 128 ) ( 320 352 129 ) placeholder 48 -256 0 -0.4 0.4 +( 320 336 128 ) ( 320 336 129 ) ( 320 337 128 ) placeholder 128 -256 0 0.4 0.4 +} +// brush 29 +{ +( 0 -144 -32 ) ( 16 -304 -64 ) ( 16 -144 -64 ) placeholder 0 0 0 1 1 +( 32 -144 -16 ) ( 0 -304 -32 ) ( 0 -144 -32 ) placeholder 0 0 0 1 1 +( 64 -304 -32 ) ( 0 -304 -32 ) ( 32 -304 -16 ) placeholder 0 0 0 1 1 +( 64 -144 -64 ) ( 16 -304 -64 ) ( 64 -304 -64 ) placeholder 0 0 0 1 1 +( 64 -144 -32 ) ( 16 -144 -64 ) ( 64 -144 -64 ) placeholder 0 0 0 1 1 +( 64 -144 -32 ) ( 32 -304 -16 ) ( 32 -144 -16 ) placeholder 0 0 0 1 1 +( 64 -144 -32 ) ( 64 -304 -64 ) ( 64 -304 -32 ) placeholder 0 0 0 1 1 +} +} +// entity 1 +{ +"classname" "info_player_start" +"origin" "112 -48 56" +} diff --git a/assets/maps/test.map b/assets/maps/test.map new file mode 100644 index 0000000..06e8c2d --- /dev/null +++ b/assets/maps/test.map @@ -0,0 +1,293 @@ +// Game: Generic +// Format: Standard +// entity 0 +{ +"classname" "worldspawn" +"wad" "" +// brush 0 +{ +( -112 -672 -80 ) ( -112 -720 -80 ) ( -112 -720 -112 ) placeholder -224 -64 0 0.2 0.2 +( -112 -720 -80 ) ( -64 -720 -80 ) ( -64 -720 -112 ) placeholder 64 -64 0 0.2 0.2 +( -64 -720 -112 ) ( -64 -672 -112 ) ( -112 -672 -112 ) placeholder 64 224 0 0.2 0.2 +( -112 -672 -80 ) ( -64 -720 -80 ) ( -112 -720 -80 ) placeholder 64 224 0 0.2 0.2 +( -64 -672 -112 ) ( -64 -672 -96 ) ( -112 -672 -80 ) placeholder 64 -64 0 0.2 0.2 +( -112 -672 -80 ) ( -64 -672 -96 ) ( -64 -720 -80 ) placeholder 64 224 0 0.2 0.2 +( -64 -720 -80 ) ( -64 -672 -96 ) ( -64 -672 -112 ) placeholder -224 -64 0 0.2 0.2 +} +// brush 1 +{ +( 0 -128 -16 ) ( 0 -127 -16 ) ( 0 -128 -15 ) placeholder 0 0 0 1 1 +( 0 -128 -16 ) ( 0 -128 -15 ) ( 1 -128 -16 ) placeholder -32 0 0 1 1 +( 0 -128 -16 ) ( 1 -128 -16 ) ( 0 -127 -16 ) placeholder -32 0 0 1 1 +( 64 -96 0 ) ( 64 -95 0 ) ( 65 -96 0 ) placeholder -32 0 0 1 1 +( 64 -96 0 ) ( 65 -96 0 ) ( 64 -96 1 ) placeholder -32 0 0 1 1 +( 64 -96 0 ) ( 64 -96 1 ) ( 64 -95 0 ) placeholder 0 0 0 1 1 +} +// brush 2 +{ +( 64 -144 -32 ) ( 64 -143 -32 ) ( 64 -144 -31 ) placeholder -16 -16 0 1 1 +( 64 -144 -32 ) ( 64 -144 -31 ) ( 65 -144 -32 ) placeholder -32 -16 0 1 1 +( 64 -144 -32 ) ( 65 -144 -32 ) ( 64 -143 -32 ) placeholder -32 16 0 1 1 +( 192 -80 -16 ) ( 192 -79 -16 ) ( 193 -80 -16 ) placeholder -32 16 0 1 1 +( 192 -80 -16 ) ( 193 -80 -16 ) ( 192 -80 -15 ) placeholder -32 -16 0 1 1 +( 192 -80 -16 ) ( 192 -80 -15 ) ( 192 -79 -16 ) placeholder -16 -16 0 1 1 +} +// brush 3 +{ +( 80 16 32 ) ( 80 15 32 ) ( 80 16 31 ) placeholder 0 -16 180 1 1 +( 144 -96 16 ) ( 145 -96 16 ) ( 144 -96 15 ) placeholder 16 -16 0 1 -1 +( 144 -96 16 ) ( 144 -97 16 ) ( 145 -96 16 ) placeholder 16 0 0 1 -1 +( 80 16 32 ) ( 81 16 32 ) ( 80 15 32 ) placeholder 16 0 0 1 -1 +( 80 16 32 ) ( 80 16 31 ) ( 81 16 32 ) placeholder 16 -16 0 1 -1 +( 144 -96 16 ) ( 144 -96 15 ) ( 144 -97 16 ) placeholder 0 -16 180 1 1 +} +// brush 4 +{ +( 224 -48 -80 ) ( 224 16 -80 ) ( 224 -96 -56 ) __TB_empty -128 -60 0 1 2 +( 224 -96 -56 ) ( 288 -96 -80 ) ( 224 -48 -80 ) __TB_empty -32 8 0 1 -2 +( 224 16 -80 ) ( 272 -32 -56 ) ( 224 -96 -56 ) __TB_empty -134.26993 -44.949593 0 1 2 +( 272 -32 -56 ) ( 288 -96 -40 ) ( 224 -96 -56 ) __TB_empty -32 112 0 1 1 +( 224 -96 -56 ) ( 288 -96 -40 ) ( 288 -96 -80 ) __TB_empty -32 -16.941177 0 1 -2 +( 288 -96 -80 ) ( 288 16 -80 ) ( 224 16 -80 ) __TB_empty -32 144 0 1 1 +( 272 -32 -56 ) ( 304 -32 -56 ) ( 288 -96 -40 ) __TB_empty -32 112 0 1 1 +( 288 16 -80 ) ( 304 -32 -56 ) ( 272 -32 -56 ) __TB_empty -32 -24 0 1 -2 +( 288 -96 -80 ) ( 304 -32 -56 ) ( 288 16 -80 ) __TB_empty 144 -52 0 1 2 +( 288 -96 -40 ) ( 304 -32 -56 ) ( 288 -96 -80 ) __TB_empty 118.95169 -69.728745 0 1 2 +} +// brush 5 +{ +( -144 -560 -80 ) ( -144 -559 -80 ) ( -144 -560 -79 ) placeholder -80 96 0 0.2 0.2 +( -144 -320 -80 ) ( -144 -320 -79 ) ( -143 -320 -80 ) placeholder -32 96 0 0.2 0.2 +( -144 -560 -80 ) ( -143 -560 -80 ) ( -144 -559 -80 ) placeholder -32 80 0 0.2 0.2 +( -16 -432 16 ) ( -16 -431 16 ) ( -15 -432 16 ) placeholder -32 80 0 0.2 0.2 +( -16 -304 -48 ) ( -15 -304 -48 ) ( -16 -304 -47 ) placeholder -32 96 0 0.2 0.2 +( 304 -432 -48 ) ( 304 -432 -47 ) ( 304 -431 -48 ) placeholder -80 96 0 0.2 0.2 +} +// brush 6 +{ +( -160 -240 -16 ) ( -160 -241 -16 ) ( -160 -240 -17 ) placeholder -240 96 180 1 1 +( -64 -304 -32 ) ( -63 -304 -32 ) ( -64 -304 -33 ) placeholder 208 96 0 1 -1 +( -64 -352 -80 ) ( -64 -353 -80 ) ( -63 -352 -80 ) placeholder 208 240 0 1 -1 +( -160 -240 16 ) ( -159 -240 16 ) ( -160 -241 16 ) placeholder 208 240 0 1 -1 +( -160 192 -16 ) ( -160 192 -17 ) ( -159 192 -16 ) placeholder 208 96 0 1 -1 +( -144 -352 -32 ) ( -144 -352 -33 ) ( -144 -353 -32 ) placeholder -240 96 180 1 1 +} +// brush 7 +{ +( 162.74516600406096 -112 -80 ) ( 163.4522727852475 -111.29289321881345 -80 ) ( 162.74516600406096 -112 -79 ) placeholder 190.39192 0 0 0.70710677 1 +( 162.74516600406096 -112 -80 ) ( 162.74516600406096 -112 -79 ) ( 163.45227278524752 -112.70710678118652 -80 ) placeholder -38.156433 0 0 0.70710677 1 +( 162.74516600406096 -112 -80 ) ( 163.45227278524752 -112.70710678118652 -80 ) ( 163.4522727852475 -111.29289321881345 -80 ) placeholder -2.27417 3.882248 315 1 1 +( 253.25483399593907 -112 -48 ) ( 253.96194077712562 -111.29289321881353 -48 ) ( 253.9619407771256 -112.70710678118664 -48 ) placeholder -2.27417 3.882248 315 1 1 +( 253.25483399593907 -112 -64 ) ( 253.25483399593907 -112 -63 ) ( 253.96194077712562 -111.29289321881353 -64 ) placeholder -6.156433 0 0 0.70710677 1 +( 253.25483399593907 -112 -64 ) ( 253.9619407771256 -112.70710678118664 -64 ) ( 253.25483399593907 -112 -63 ) placeholder 97.60808 0 180 0.70710677 -1 +} +// brush 8 +{ +( -32 96 -80 ) ( -32 97 -80 ) ( -32 96 -79 ) placeholder 0 0 0 1 1 +( 96 272 -64 ) ( 97 272.625 -64 ) ( 96 272 -63 ) placeholder 0 0 0 1 1 +( -32 96 -80 ) ( -31 96.625 -80 ) ( -32 97 -80 ) placeholder -73.70788 15.668304 32.00539 1.1792476 0.87100387 +( 96 272 -32 ) ( 96 273 -32 ) ( 97 272.625 -32 ) placeholder -73.70788 15.668304 32.00539 1.1792476 0.87100387 +( -32 96 -80 ) ( -32 96 -79 ) ( -31 96.625 -80 ) placeholder 0 0 0 1 1 +( 96 272 -64 ) ( 96 272 -63 ) ( 96 273 -64 ) placeholder -80 0 0 1 1 +} +// brush 9 +{ +( -64 -672 -96 ) ( -64 -720 -80 ) ( -64 -720 -112 ) placeholder -224 -64 0 0.2 0.2 +( -64 -672 -96 ) ( -16 -672 -80 ) ( -64 -720 -80 ) placeholder 80 224 0 0.2 0.2 +( -64 -720 -80 ) ( -16 -720 -80 ) ( -16 -720 -112 ) placeholder 80 -64 0 0.2 0.2 +( -16 -720 -112 ) ( -16 -672 -112 ) ( -64 -672 -112 ) placeholder 80 224 0 0.2 0.2 +( -64 -720 -80 ) ( -16 -672 -80 ) ( -16 -720 -80 ) placeholder 80 224 0 0.2 0.2 +( -16 -672 -112 ) ( -16 -672 -80 ) ( -64 -672 -96 ) placeholder 80 -64 0 0.2 0.2 +( -16 -720 -80 ) ( -16 -672 -80 ) ( -16 -672 -112 ) placeholder -224 -64 0 0.2 0.2 +} +// brush 10 +{ +( -16 -672 -80 ) ( -16 -720 -80 ) ( -16 -720 -112 ) placeholder -224 -64 0 0.2 0.2 +( -16 -672 -80 ) ( 32 -672 -48 ) ( -16 -720 -80 ) placeholder -160 224 0 0.2 0.2 +( -16 -720 -80 ) ( 32 -720 -80 ) ( 32 -720 -112 ) placeholder -160 -64 0 0.2 0.2 +( -16 -720 -80 ) ( 32 -672 -48 ) ( 32 -720 -80 ) placeholder -160 224 0 0.2 0.2 +( 32 -720 -112 ) ( 32 -672 -112 ) ( -16 -672 -112 ) placeholder -160 224 0 0.2 0.2 +( 32 -672 -112 ) ( 32 -672 -48 ) ( -16 -672 -80 ) placeholder -160 -64 0 0.2 0.2 +( 32 -720 -80 ) ( 32 -672 -48 ) ( 32 -672 -112 ) placeholder -224 -64 0 0.2 0.2 +} +// brush 11 +{ +( 32 -672 -48 ) ( 32 -720 -80 ) ( 32 -720 -112 ) placeholder -224 -64 0 0.2 0.2 +( 32 -720 -80 ) ( 80 -720 -80 ) ( 80 -720 -112 ) placeholder -144 -64 0 0.2 0.2 +( 32 -672 -48 ) ( 80 -720 -80 ) ( 32 -720 -80 ) placeholder -144 224 0 0.2 0.2 +( 80 -720 -112 ) ( 80 -672 -112 ) ( 32 -672 -112 ) placeholder -144 224 0 0.2 0.2 +( 80 -672 -112 ) ( 80 -672 -80 ) ( 32 -672 -48 ) placeholder -144 -64 0 0.2 0.2 +( 32 -672 -48 ) ( 80 -672 -80 ) ( 80 -720 -80 ) placeholder -144 224 0 0.2 0.2 +( 80 -720 -80 ) ( 80 -672 -80 ) ( 80 -672 -112 ) placeholder -224 -64 0 0.2 0.2 +} +// brush 12 +{ +( -112 -624 -80 ) ( -112 -672 -80 ) ( -112 -672 -112 ) placeholder -208 -64 0 0.2 0.2 +( -112 -672 -80 ) ( -64 -672 -96 ) ( -64 -672 -112 ) placeholder 64 -64 0 0.2 0.2 +( -64 -672 -112 ) ( -64 -624 -112 ) ( -112 -624 -112 ) placeholder 64 208 0 0.2 0.2 +( -112 -624 -80 ) ( -64 -624 -80 ) ( -112 -672 -80 ) placeholder 64 208 0 0.2 0.2 +( -64 -624 -112 ) ( -64 -624 -80 ) ( -112 -624 -80 ) placeholder 64 -64 0 0.2 0.2 +( -112 -672 -80 ) ( -64 -624 -80 ) ( -64 -672 -96 ) placeholder 64 208 0 0.2 0.2 +( -64 -672 -96 ) ( -64 -624 -80 ) ( -64 -624 -112 ) placeholder -208 -64 0 0.2 0.2 +} +// brush 13 +{ +( -112 -384 -112 ) ( -112 -383 -112 ) ( -112 -384 -111 ) placeholder -192 -64 0 0.2 0.2 +( -112 -624 -112 ) ( -112 -624 -111 ) ( -111 -624 -112 ) placeholder 64 -64 0 0.2 0.2 +( -112 -384 -112 ) ( -111 -384 -112 ) ( -112 -383 -112 ) placeholder 64 192 0 0.2 0.2 +( 16 -256 -80 ) ( 16 -255 -80 ) ( 17 -256 -80 ) placeholder 64 192 0 0.2 0.2 +( 16 -576 -80 ) ( 17 -576 -80 ) ( 16 -576 -79 ) placeholder 64 -64 0 0.2 0.2 +( -64 -256 -80 ) ( -64 -256 -79 ) ( -64 -255 -80 ) placeholder -192 -64 0 0.2 0.2 +} +// brush 14 +{ +( -64 -624 -80 ) ( -64 -672 -96 ) ( -64 -672 -112 ) placeholder -208 -64 0 0.2 0.2 +( -64 -624 -80 ) ( -16 -672 -80 ) ( -64 -672 -96 ) placeholder 80 208 0 0.2 0.2 +( -64 -672 -96 ) ( -16 -672 -80 ) ( -16 -672 -112 ) placeholder 80 -64 0 0.2 0.2 +( -16 -672 -112 ) ( -16 -624 -112 ) ( -64 -624 -112 ) placeholder 80 208 0 0.2 0.2 +( -64 -624 -80 ) ( -16 -624 -80 ) ( -16 -672 -80 ) placeholder 80 208 0 0.2 0.2 +( -16 -624 -112 ) ( -16 -624 -80 ) ( -64 -624 -80 ) placeholder 80 -64 0 0.2 0.2 +( -16 -672 -80 ) ( -16 -624 -80 ) ( -16 -624 -112 ) placeholder -208 -64 0 0.2 0.2 +} +// brush 15 +{ +( -112 -528 -80 ) ( -112 -576 -80 ) ( -112 -576 -112 ) placeholder -176 -64 0 0.2 0.2 +( -112 -528 -80 ) ( -64 -528 -48 ) ( -112 -576 -80 ) placeholder 64 176 0 0.2 0.2 +( -112 -576 -80 ) ( -64 -576 -80 ) ( -64 -576 -112 ) placeholder 64 -64 0 0.2 0.2 +( -112 -576 -80 ) ( -64 -528 -48 ) ( -64 -576 -80 ) placeholder 64 176 0 0.2 0.2 +( -64 -576 -112 ) ( -64 -528 -112 ) ( -112 -528 -112 ) placeholder 64 176 0 0.2 0.2 +( -64 -528 -112 ) ( -64 -528 -48 ) ( -112 -528 -80 ) placeholder 64 -64 0 0.2 0.2 +( -64 -576 -80 ) ( -64 -528 -48 ) ( -64 -528 -112 ) placeholder -176 -64 0 0.2 0.2 +} +// brush 16 +{ +( -64 -384 -112 ) ( -64 -383 -112 ) ( -64 -384 -111 ) placeholder -192 -64 0 0.2 0.2 +( -64 -624 -112 ) ( -64 -624 -111 ) ( -63 -624 -112 ) placeholder 80 -64 0 0.2 0.2 +( -64 -384 -112 ) ( -63 -384 -112 ) ( -64 -383 -112 ) placeholder 80 192 0 0.2 0.2 +( 64 -256 -80 ) ( 64 -255 -80 ) ( 65 -256 -80 ) placeholder 80 192 0 0.2 0.2 +( 64 -576 -80 ) ( 65 -576 -80 ) ( 64 -576 -79 ) placeholder 80 -64 0 0.2 0.2 +( -16 -256 -80 ) ( -16 -256 -79 ) ( -16 -255 -80 ) placeholder -192 -64 0 0.2 0.2 +} +// brush 17 +{ +( -16 -624 -80 ) ( -16 -672 -80 ) ( -16 -672 -112 ) placeholder -208 -64 0 0.2 0.2 +( -16 -624 -80 ) ( 32 -672 -48 ) ( -16 -672 -80 ) placeholder -160 208 0 0.2 0.2 +( -16 -672 -80 ) ( 32 -672 -48 ) ( 32 -672 -112 ) placeholder -160 -64 0 0.2 0.2 +( 32 -672 -112 ) ( 32 -624 -112 ) ( -16 -624 -112 ) placeholder -160 208 0 0.2 0.2 +( -16 -624 -80 ) ( 32 -624 -80 ) ( 32 -672 -48 ) placeholder -160 208 0 0.2 0.2 +( 32 -624 -112 ) ( 32 -624 -80 ) ( -16 -624 -80 ) placeholder -160 -64 0 0.2 0.2 +( 32 -672 -48 ) ( 32 -624 -80 ) ( 32 -624 -112 ) placeholder -208 -64 0 0.2 0.2 +} +// brush 18 +{ +( -112 -480 -80 ) ( -112 -528 -80 ) ( -112 -528 -112 ) placeholder -160 -64 0 0.2 0.2 +( -112 -480 -80 ) ( -64 -528 -48 ) ( -112 -528 -80 ) placeholder 64 160 0 0.2 0.2 +( -112 -528 -80 ) ( -64 -528 -48 ) ( -64 -528 -112 ) placeholder 64 -64 0 0.2 0.2 +( -64 -528 -112 ) ( -64 -480 -112 ) ( -112 -480 -112 ) placeholder 64 160 0 0.2 0.2 +( -112 -480 -80 ) ( -64 -480 -80 ) ( -64 -528 -48 ) placeholder 64 160 0 0.2 0.2 +( -64 -480 -112 ) ( -64 -480 -80 ) ( -112 -480 -80 ) placeholder 64 -64 0 0.2 0.2 +( -64 -528 -48 ) ( -64 -480 -80 ) ( -64 -480 -112 ) placeholder -160 -64 0 0.2 0.2 +} +// brush 19 +{ +( -64 -528 -48 ) ( -64 -576 -80 ) ( -64 -576 -112 ) placeholder -176 -64 0 0.2 0.2 +( -64 -576 -80 ) ( -16 -576 -80 ) ( -16 -576 -112 ) placeholder 80 -64 0 0.2 0.2 +( -64 -528 -48 ) ( -16 -576 -80 ) ( -64 -576 -80 ) placeholder 80 176 0 0.2 0.2 +( -16 -576 -112 ) ( -16 -528 -112 ) ( -64 -528 -112 ) placeholder 80 176 0 0.2 0.2 +( -16 -528 -112 ) ( -16 -528 -80 ) ( -64 -528 -48 ) placeholder 80 -64 0 0.2 0.2 +( -64 -528 -48 ) ( -16 -528 -80 ) ( -16 -576 -80 ) placeholder 80 176 0 0.2 0.2 +( -16 -576 -80 ) ( -16 -528 -80 ) ( -16 -528 -112 ) placeholder -176 -64 0 0.2 0.2 +} +// brush 20 +{ +( -16 -384 -112 ) ( -16 -383 -112 ) ( -16 -384 -111 ) placeholder -192 -64 0 0.2 0.2 +( -16 -624 -112 ) ( -16 -624 -111 ) ( -15 -624 -112 ) placeholder -160 -64 0 0.2 0.2 +( -16 -384 -112 ) ( -15 -384 -112 ) ( -16 -383 -112 ) placeholder -160 192 0 0.2 0.2 +( 112 -256 -80 ) ( 112 -255 -80 ) ( 113 -256 -80 ) placeholder -160 192 0 0.2 0.2 +( 112 -576 -80 ) ( 113 -576 -80 ) ( 112 -576 -79 ) placeholder -160 -64 0 0.2 0.2 +( 32 -256 -80 ) ( 32 -256 -79 ) ( 32 -255 -80 ) placeholder -192 -64 0 0.2 0.2 +} +// brush 21 +{ +( 32 -624 -80 ) ( 32 -672 -48 ) ( 32 -672 -112 ) placeholder -208 -64 0 0.2 0.2 +( 32 -672 -48 ) ( 80 -672 -80 ) ( 80 -672 -112 ) placeholder -144 -64 0 0.2 0.2 +( 80 -672 -112 ) ( 80 -624 -112 ) ( 32 -624 -112 ) placeholder -144 208 0 0.2 0.2 +( 32 -624 -80 ) ( 80 -624 -80 ) ( 32 -672 -48 ) placeholder -144 208 0 0.2 0.2 +( 80 -624 -112 ) ( 80 -624 -80 ) ( 32 -624 -80 ) placeholder -144 -64 0 0.2 0.2 +( 32 -672 -48 ) ( 80 -624 -80 ) ( 80 -672 -80 ) placeholder -144 208 0 0.2 0.2 +( 80 -672 -80 ) ( 80 -624 -80 ) ( 80 -624 -112 ) placeholder -208 -64 0 0.2 0.2 +} +// brush 22 +{ +( -64 -480 -80 ) ( -64 -528 -48 ) ( -64 -528 -112 ) placeholder -160 -64 0 0.2 0.2 +( -64 -528 -48 ) ( -16 -528 -80 ) ( -16 -528 -112 ) placeholder 80 -64 0 0.2 0.2 +( -16 -528 -112 ) ( -16 -480 -112 ) ( -64 -480 -112 ) placeholder 80 160 0 0.2 0.2 +( -64 -480 -80 ) ( -16 -480 -80 ) ( -64 -528 -48 ) placeholder 80 160 0 0.2 0.2 +( -16 -480 -112 ) ( -16 -480 -80 ) ( -64 -480 -80 ) placeholder 80 -64 0 0.2 0.2 +( -64 -528 -48 ) ( -16 -480 -80 ) ( -16 -528 -80 ) placeholder 80 160 0 0.2 0.2 +( -16 -528 -80 ) ( -16 -480 -80 ) ( -16 -480 -112 ) placeholder -160 -64 0 0.2 0.2 +} +// brush 23 +{ +( -16 -528 -80 ) ( -16 -576 -80 ) ( -16 -576 -112 ) placeholder -176 -64 0 0.2 0.2 +( -16 -576 -80 ) ( 32 -576 -80 ) ( 32 -576 -112 ) placeholder -160 -64 0 0.2 0.2 +( 32 -576 -112 ) ( 32 -528 -112 ) ( -16 -528 -112 ) placeholder -160 176 0 0.2 0.2 +( -16 -528 -80 ) ( 32 -576 -80 ) ( -16 -576 -80 ) placeholder -160 176 0 0.2 0.2 +( 32 -528 -112 ) ( 32 -528 -96 ) ( -16 -528 -80 ) placeholder -160 -64 0 0.2 0.2 +( -16 -528 -80 ) ( 32 -528 -96 ) ( 32 -576 -80 ) placeholder -160 176 0 0.2 0.2 +( 32 -576 -80 ) ( 32 -528 -96 ) ( 32 -528 -112 ) placeholder -176 -64 0 0.2 0.2 +} +// brush 24 +{ +( 32 -384 -112 ) ( 32 -383 -112 ) ( 32 -384 -111 ) placeholder -192 -64 0 0.2 0.2 +( 32 -624 -112 ) ( 32 -624 -111 ) ( 33 -624 -112 ) placeholder -144 -64 0 0.2 0.2 +( 32 -384 -112 ) ( 33 -384 -112 ) ( 32 -383 -112 ) placeholder -144 192 0 0.2 0.2 +( 160 -256 -80 ) ( 160 -255 -80 ) ( 161 -256 -80 ) placeholder -144 192 0 0.2 0.2 +( 160 -576 -80 ) ( 161 -576 -80 ) ( 160 -576 -79 ) placeholder -144 -64 0 0.2 0.2 +( 80 -256 -80 ) ( 80 -256 -79 ) ( 80 -255 -80 ) placeholder -192 -64 0 0.2 0.2 +} +// brush 25 +{ +( -16 -480 -80 ) ( -16 -528 -80 ) ( -16 -528 -112 ) placeholder -160 -64 0 0.2 0.2 +( -16 -528 -80 ) ( 32 -528 -96 ) ( 32 -528 -112 ) placeholder -160 -64 0 0.2 0.2 +( 32 -528 -112 ) ( 32 -480 -112 ) ( -16 -480 -112 ) placeholder -160 160 0 0.2 0.2 +( -16 -480 -80 ) ( 32 -480 -80 ) ( -16 -528 -80 ) placeholder -160 160 0 0.2 0.2 +( 32 -480 -112 ) ( 32 -480 -80 ) ( -16 -480 -80 ) placeholder -160 -64 0 0.2 0.2 +( -16 -528 -80 ) ( 32 -480 -80 ) ( 32 -528 -96 ) placeholder -160 160 0 0.2 0.2 +( 32 -528 -96 ) ( 32 -480 -80 ) ( 32 -480 -112 ) placeholder -160 -64 0 0.2 0.2 +} +// brush 26 +{ +( 32 -528 -96 ) ( 32 -576 -80 ) ( 32 -576 -112 ) placeholder -176 -64 0 0.2 0.2 +( 32 -528 -96 ) ( 80 -528 -80 ) ( 32 -576 -80 ) placeholder -144 176 0 0.2 0.2 +( 32 -576 -80 ) ( 80 -576 -80 ) ( 80 -576 -112 ) placeholder -144 -64 0 0.2 0.2 +( 80 -576 -112 ) ( 80 -528 -112 ) ( 32 -528 -112 ) placeholder -144 176 0 0.2 0.2 +( 32 -576 -80 ) ( 80 -528 -80 ) ( 80 -576 -80 ) placeholder -144 176 0 0.2 0.2 +( 80 -528 -112 ) ( 80 -528 -80 ) ( 32 -528 -96 ) placeholder -144 -64 0 0.2 0.2 +( 80 -576 -80 ) ( 80 -528 -80 ) ( 80 -528 -112 ) placeholder -176 -64 0 0.2 0.2 +} +// brush 27 +{ +( 32 -480 -80 ) ( 32 -528 -96 ) ( 32 -528 -112 ) placeholder -160 -64 0 0.2 0.2 +( 32 -480 -80 ) ( 80 -528 -80 ) ( 32 -528 -96 ) placeholder -144 160 0 0.2 0.2 +( 32 -528 -96 ) ( 80 -528 -80 ) ( 80 -528 -112 ) placeholder -144 -64 0 0.2 0.2 +( 80 -528 -112 ) ( 80 -480 -112 ) ( 32 -480 -112 ) placeholder -144 160 0 0.2 0.2 +( 32 -480 -80 ) ( 80 -480 -80 ) ( 80 -528 -80 ) placeholder -144 160 0 0.2 0.2 +( 80 -480 -112 ) ( 80 -480 -80 ) ( 32 -480 -80 ) placeholder -144 -64 0 0.2 0.2 +( 80 -528 -80 ) ( 80 -480 -80 ) ( 80 -480 -112 ) placeholder -160 -64 0 0.2 0.2 +} +// brush 28 +{ +( 224 256 112 ) ( 224 257 112 ) ( 224 256 113 ) placeholder 170.66669 8 0 -0.375 0.4 +( 224 256 112 ) ( 224 256 113 ) ( 225 256 112 ) placeholder 224 -256 0 0.4 0.4 +( 224 256 112 ) ( 225 256 112 ) ( 224 257 112 ) placeholder 47.999992 138.99998 0 -0.4 0.4 +( 320 336 208 ) ( 320 337 208 ) ( 321 336 208 ) placeholder 208 112 0 0.4 0.4 +( 320 352 128 ) ( 321 352 128 ) ( 320 352 129 ) placeholder 48 -256 0 -0.4 0.4 +( 320 336 128 ) ( 320 336 129 ) ( 320 337 128 ) placeholder 128 -256 0 0.4 0.4 +} +} +// entity 1 +{ +"classname" "info_player_start" +"origin" "112 -48 56" +} diff --git a/assets/maps/test02.map b/assets/maps/test02.map new file mode 100644 index 0000000..4b3a328 --- /dev/null +++ b/assets/maps/test02.map @@ -0,0 +1,84 @@ +// Game: Generic +// Format: Standard +// entity 0 +{ +"classname" "worldspawn" +"wad" "" +// brush 0 +{ +( -144 -64 -112 ) ( -144 -63 -112 ) ( -144 -64 -111 ) placeholder 0 -64 0 0.2 0.2 +( -144 -304 -112 ) ( -144 -304 -111 ) ( -143 -304 -112 ) placeholder 224 -64 0 0.2 0.2 +( -144 -64 -112 ) ( -143 -64 -112 ) ( -144 -63 -112 ) placeholder 224 0 0 0.2 0.2 +( -16 64 -80 ) ( -16 65 -80 ) ( -15 64 -80 ) placeholder 224 0 0 0.2 0.2 +( -16 192 -80 ) ( -15 192 -80 ) ( -16 192 -79 ) placeholder 224 -64 0 0.2 0.2 +( 304 64 -80 ) ( 304 64 -79 ) ( 304 65 -80 ) placeholder 0 -64 0 0.2 0.2 +} +// brush 1 +{ +( 0 -128 -16 ) ( 0 -127 -16 ) ( 0 -128 -15 ) placeholder 0 0 0 1 1 +( 0 -128 -16 ) ( 0 -128 -15 ) ( 1 -128 -16 ) placeholder -32 0 0 1 1 +( 0 -128 -16 ) ( 1 -128 -16 ) ( 0 -127 -16 ) placeholder -32 0 0 1 1 +( 64 -96 0 ) ( 64 -95 0 ) ( 65 -96 0 ) placeholder -32 0 0 1 1 +( 64 -96 0 ) ( 65 -96 0 ) ( 64 -96 1 ) placeholder -32 0 0 1 1 +( 64 -96 0 ) ( 64 -96 1 ) ( 64 -95 0 ) placeholder 0 0 0 1 1 +} +// brush 2 +{ +( 64 -144 -32 ) ( 64 -143 -32 ) ( 64 -144 -31 ) placeholder -16 -16 0 1 1 +( 64 -144 -32 ) ( 64 -144 -31 ) ( 65 -144 -32 ) placeholder -32 -16 0 1 1 +( 64 -144 -32 ) ( 65 -144 -32 ) ( 64 -143 -32 ) placeholder -32 16 0 1 1 +( 192 -80 -16 ) ( 192 -79 -16 ) ( 193 -80 -16 ) placeholder -32 16 0 1 1 +( 192 -80 -16 ) ( 193 -80 -16 ) ( 192 -80 -15 ) placeholder -32 -16 0 1 1 +( 192 -80 -16 ) ( 192 -80 -15 ) ( 192 -79 -16 ) placeholder -16 -16 0 1 1 +} +// brush 3 +{ +( 80 16 32 ) ( 80 15 32 ) ( 80 16 31 ) placeholder 0 -16 180 1 1 +( 144 -96 16 ) ( 145 -96 16 ) ( 144 -96 15 ) placeholder 16 -16 0 1 -1 +( 144 -96 16 ) ( 144 -97 16 ) ( 145 -96 16 ) placeholder 16 0 0 1 -1 +( 80 16 32 ) ( 81 16 32 ) ( 80 15 32 ) placeholder 16 0 0 1 -1 +( 80 16 32 ) ( 80 16 31 ) ( 81 16 32 ) placeholder 16 -16 0 1 -1 +( 144 -96 16 ) ( 144 -96 15 ) ( 144 -97 16 ) placeholder 0 -16 180 1 1 +} +// brush 4 +{ +( 224 -48 -80 ) ( 224 -47 -80 ) ( 224 -48 -79 ) placeholder -144 -96 0 1 1 +( 224 -48 -80 ) ( 224 -48 -79 ) ( 225 -48 -80 ) placeholder -32 -96 0 1 1 +( 224 -48 -80 ) ( 225 -48 -80 ) ( 224 -47 -80 ) placeholder -32 144 0 1 1 +( 288 16 -64 ) ( 288 17 -64 ) ( 289 16 -64 ) placeholder -32 144 0 1 1 +( 288 16 -64 ) ( 289 16 -64 ) ( 288 16 -63 ) placeholder -32 -96 0 1 1 +( 288 16 -64 ) ( 288 16 -63 ) ( 288 17 -64 ) placeholder -144 -96 0 1 1 +} +// brush 5 +{ +( -144 -560 -80 ) ( -144 -559 -80 ) ( -144 -560 -79 ) placeholder -80 96 0 0.2 0.2 +( -144 -320 -80 ) ( -144 -320 -79 ) ( -143 -320 -80 ) placeholder -32 96 0 0.2 0.2 +( -144 -560 -80 ) ( -143 -560 -80 ) ( -144 -559 -80 ) placeholder -32 80 0 0.2 0.2 +( -16 -432 16 ) ( -16 -431 16 ) ( -15 -432 16 ) placeholder -32 80 0 0.2 0.2 +( -16 -304 -48 ) ( -15 -304 -48 ) ( -16 -304 -47 ) placeholder -32 96 0 0.2 0.2 +( 304 -432 -48 ) ( 304 -432 -47 ) ( 304 -431 -48 ) placeholder -80 96 0 0.2 0.2 +} +// brush 6 +{ +( -160 -240 -16 ) ( -160 -241 -16 ) ( -160 -240 -17 ) placeholder -240 96 180 1 1 +( -64 -304 -32 ) ( -63 -304 -32 ) ( -64 -304 -33 ) placeholder 208 96 0 1 -1 +( -64 -352 -80 ) ( -64 -353 -80 ) ( -63 -352 -80 ) placeholder 208 240 0 1 -1 +( -160 -240 16 ) ( -159 -240 16 ) ( -160 -241 16 ) placeholder 208 240 0 1 -1 +( -160 192 -16 ) ( -160 192 -17 ) ( -159 192 -16 ) placeholder 208 96 0 1 -1 +( -144 -352 -32 ) ( -144 -352 -33 ) ( -144 -353 -32 ) placeholder -240 96 180 1 1 +} +// brush 7 +{ +( 176 -144 -80 ) ( 176 -143 -80 ) ( 176 -144 -79 ) placeholder 176 0 0 1 1 +( 176 -144 -80 ) ( 176 -144 -79 ) ( 177 -144 -80 ) placeholder 16 0 0 1 1 +( 176 -144 -80 ) ( 177 -144 -80 ) ( 176 -143 -80 ) placeholder 16 -176 0 1 1 +( 240 -80 -48 ) ( 240 -79 -48 ) ( 241 -80 -48 ) placeholder 16 -176 0 1 1 +( 240 -80 -64 ) ( 241 -80 -64 ) ( 240 -80 -63 ) placeholder 16 0 0 1 1 +( 240 -80 -64 ) ( 240 -80 -63 ) ( 240 -79 -64 ) placeholder 176 0 0 1 1 +} +} +// entity 1 +{ +"classname" "info_player_start" +"origin" "112 -48 56" +} diff --git a/assets/textures/__TB_empty.tga b/assets/textures/__TB_empty.tga new file mode 100644 index 0000000000000000000000000000000000000000..7bf61f7a1f6e879b3fb1b1197841b805cd43f6d2 GIT binary patch literal 16428 zcmeI2T~5O=5QHsG!Xqzm240F(34z3)xc>pTLWvbwbxe1%Q_XIK+M+0~cRD+t?ZN{o z<)+N9$5MZ8?@Boww!D6({##yKve({Qve(`|{xa#ttN(1R_3!=T|H0pQEKX~_ea^mL z`TgI4zh|&4zCUB#v$ov1wf5PXxA(pE^<*41m2;e{@B8_Fd`jEqS`Odc;;i%ST4(Ni zwH(adVmoG-vv@rFEL)5{8AlD`uUe}<`pjBxztn4VD%0AW#n;dGW8|miVf|3Hj2fF) ztuzo08Mvcv@)@LxH9%>%e z4`s`!v3b?{3`W#L&BOYkY#B8+uUenMhoXWp4>b?#hq7hV*t}|e z1|#aB=3)I%wu~B^SFO)rL_O3ztRKpjQDgI}^%;z)hnk1=L)kKFY+kiKgAw&m^RRv> z+a=W4^-}A%USl;6>v!e%|Nl8+t=Roi>m!&LPc;wg$7GGrvU%0|2qwl;&BOXJStGP; zUbQ}giSbnPuzpO|2rZjet&d=0Jk>m`AComg%jTW+?OCgO@1NhX=6lU@9KpnRF7q$5 z*|nbAGq!!1tPxt9(av0-Z8~1P&YJHv%W(t~<9R-R;~4k9twF2fJCij+dp>{T9QiA6 zt6}!qJA#SvJdb~!%j?TzjnH~?M(HA$7|$p!uP>7|LhH>LrCVb<{w`TQb6)4yKI^GR zad~~$GWML-c+CUVy*ZhZu%=ey*}PPcVGMW P{p-v9_tXA)_fUQTx_{T8 literal 0 HcmV?d00001 diff --git a/assets/textures/fonts/oldschool-cga-8x8.tga b/assets/textures/fonts/oldschool-cga-8x8.tga new file mode 100644 index 0000000000000000000000000000000000000000..d342b014a8b1d67d4aa6946da3ffa346c66ed194 GIT binary patch literal 65580 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 literal 0 HcmV?d00001 diff --git a/assets/textures/fonts/oldschool-ega-8x14.tga b/assets/textures/fonts/oldschool-ega-8x14.tga new file mode 100644 index 0000000000000000000000000000000000000000..b66fd0022900871d8ce3c19f96d7faacb6bfb800 GIT binary patch literal 114732 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 x;`02?JH7Y2gF`2Dw^{{7|S*Y7`n`yW33b({bI literal 0 HcmV?d00001 diff --git a/assets/textures/ui/cursors.tga b/assets/textures/ui/cursors.tga new file mode 100644 index 0000000000000000000000000000000000000000..ceeb5dc72daa52371a19582e7bf74e251c2e1316 GIT binary patch literal 4140 zcmeH|-3r1m3`T=b;-xqG7844h;2*yKd&q1jXOxmbo4MgdI_B5LoP3!?71_vlZDqK2 z@zne6pMlSS&ww@qeOy|+HkSB&)pF#)a&FaM@u^O$cglxDTWbsjvKI>vW9cEY9=Fu9 zXloCnY1~-+G^V8oa_K{>w^r;QdRXgO-N*lr=FC9+uDtWxQVYJ7ExGbsQx68MVaQ1g zdsK_@o~HPsTFz8V^U`8dJaLP6VoR<(Q-4Xt6J65FYx{5Ij2Vci$o=~0p5YcQ=l$y# HPTfI1s6ffd literal 0 HcmV?d00001 diff --git a/assets/textures/ui/panel_ornaments.tga b/assets/textures/ui/panel_ornaments.tga new file mode 100644 index 0000000000000000000000000000000000000000..a8fa4945a86129a100516cd245ad1611f8ffc38b GIT binary patch literal 4140 zcmeHKQEtN^3`ILhKliiVqLU&`o3ypl?thLbnIG|EggLYXwC)2}mcf|!j3Fd-?!hf` zaQ^owy;*cj6u#E7#WPORG{-8$$j6FL^DODFIKZboo?rQ92>i?keYR^X@RbwzZ2SBz zf9zv!do8hOEq;up%DB{D($8VYSh~-wi?0#pwp8S8;EC&JzZpYo#9J6yAF*Jm-~_SC z>I{l6i&s0Y`I!97wU)!0Pi#J85<>~=XN(LquK}^u-fjJzKis{2|Cb8%^RIU^pV9vO z_xrE)8Q|IakN2;>6aVD, +} + +#[derive(Default)] +pub struct Entity { + pub props: HashMap, + pub brushes: Vec, +} + +impl Entity { + // Wird beim Entity-/Metadaten-Schritt gebraucht (worldspawn vs. Props, + // Signal-Bindung); bis dahin nur in Tests verwendet. + #[allow(dead_code)] + pub fn classname(&self) -> Option<&str> { + self.props.get("classname").map(String::as_str) + } +} + +pub struct Brush { + pub faces: Vec, +} + +/// Eine Brush-Face: Ebene aus drei Punkten (Quake-Reihenfolge, im +/// Uhrzeigersinn von vorn gesehen) plus Textur-Ausrichtung. Die +/// Ausrichtungsfelder werden erst vom Textur-Schritt gebraucht. +pub struct Face { + pub plane: [[f32; 3]; 3], + pub texture: String, + #[allow(dead_code)] + pub offset: [f32; 2], + #[allow(dead_code)] + pub rotation: f32, + #[allow(dead_code)] + pub scale: [f32; 2], +} + +pub fn load(path: &str) -> Map { + let src = read_to_string(path).unwrap_or_else(|e| panic!("map load {path}: {e}")); + parse(&src) +} + +pub fn parse(src: &str) -> Map { + let mut entities = Vec::new(); + let mut cur_entity: Option = None; + let mut cur_brush: Option = None; + + for raw in src.lines() { + // Kommentar (`// …`) abschneiden, dann trimmen. + let line = match raw.split_once("//") { + Some((code, _)) => code.trim(), + None => raw.trim(), + }; + if line.is_empty() { continue; } + + match line { + "{" => { + // Erstes `{` öffnet eine Entity, ein weiteres einen Brush. + if cur_entity.is_none() { + cur_entity = Some(Entity::default()); + } else { + cur_brush = Some(Brush { faces: Vec::new() }); + } + } + "}" => { + if let Some(brush) = cur_brush.take() { + cur_entity.as_mut().unwrap().brushes.push(brush); + } else if let Some(entity) = cur_entity.take() { + entities.push(entity); + } + } + _ => { + if let Some(brush) = cur_brush.as_mut() { + if let Some(face) = parse_face(line) { + brush.faces.push(face); + } + } else if let Some(entity) = cur_entity.as_mut() { + if let Some((k, v)) = parse_property(line) { + entity.props.insert(k, v); + } + } + } + } + } + Map { entities } +} + +/// `"key" "value"` — Wert darf leer sein. +fn parse_property(line: &str) -> Option<(String, String)> { + let mut it = line.split('"'); + it.next()?; // vor dem ersten " + let key = it.next()?; // key + it.next()?; // zwischen den Paaren + let val = it.next()?; // value + Some((key.to_string(), val.to_string())) +} + +/// `( x y z ) ( x y z ) ( x y z ) TEX offX offY rot sx sy` +fn parse_face(line: &str) -> Option { + let t: Vec<&str> = line.split_whitespace().collect(); + let mut i = 0; + let mut plane = [[0.0f32; 3]; 3]; + for p in 0..3 { + if *t.get(i)? != "(" { return None; } + for k in 0..3 { + plane[p][k] = t.get(i + 1 + k)?.parse().ok()?; + } + if *t.get(i + 4)? != ")" { return None; } + i += 5; + } + let texture = (*t.get(i)?).to_string(); + let nums: Option> = t[i + 1..i + 6].iter().map(|s| s.parse().ok()).collect(); + let n = nums?; + Some(Face { + plane, + texture, + offset: [n[0], n[1]], + rotation: n[2], + scale: [n[3], n[4]], + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + const SAMPLE: &str = r#" +// entity 0 +{ +"classname" "worldspawn" +"wad" "" +// brush 0 +{ +( -48 -64 -48 ) ( -48 -63 -48 ) ( -48 -64 -47 ) placeholder 32 -32 0 1 1 +( -48 -64 -48 ) ( -48 -64 -47 ) ( -47 -64 -48 ) placeholder -16 -32 0 1 1 +( -48 -64 -48 ) ( -47 -64 -48 ) ( -48 -63 -48 ) placeholder -16 -32 0 1 1 +( 80 64 -16 ) ( 80 65 -16 ) ( 81 64 -16 ) placeholder -16 -32 0 1 1 +( 80 64 -16 ) ( 81 64 -16 ) ( 80 64 -15 ) placeholder -16 -32 0 1 1 +( 80 64 -16 ) ( 80 64 -15 ) ( 80 65 -16 ) placeholder 32 -32 0 1 1 +} +} +"#; + + #[test] + fn parses_entity_brush_and_face() { + let m = parse(SAMPLE); + assert_eq!(m.entities.len(), 1); + let e = &m.entities[0]; + assert_eq!(e.classname(), Some("worldspawn")); + assert_eq!(e.props.get("wad").map(String::as_str), Some("")); + assert_eq!(e.brushes.len(), 1); + let b = &e.brushes[0]; + assert_eq!(b.faces.len(), 6); + + let f = &b.faces[0]; + assert_eq!(f.texture, "placeholder"); + assert_eq!(f.plane[0], [-48.0, -64.0, -48.0]); + assert_eq!(f.plane[2], [-48.0, -64.0, -47.0]); + assert_eq!(f.offset, [32.0, -32.0]); + assert_eq!(f.scale, [1.0, 1.0]); + } +} diff --git a/src/engine/mod.rs b/src/engine/mod.rs index c2755dc..1a49c51 100644 --- a/src/engine/mod.rs +++ b/src/engine/mod.rs @@ -15,10 +15,17 @@ //! Bitte azyklisch halten: neue Querverbindungen lieber über Rückgabewerte //! an den Aufrufer lösen (so wie story_ctrl Tags zurückgibt, statt selbst //! signals::dispatch zu rufen). +//! +//! `map` und `tga` sind reine Decoder (Bytes → owned Daten, hängen an +//! nichts) — die geteilte Heimat für Format-Dekodierung, die jedes Frontend +//! per Pull konsumiert. `map` ist zugleich der Anfang des headless +//! Datenmodells (Phase 2 des Renderer-Plans). pub mod assets; pub mod game; pub mod ink; pub mod kv; +pub mod map; pub mod signals; pub mod story_ctrl; +pub mod tga; diff --git a/src/engine/tga.rs b/src/engine/tga.rs new file mode 100644 index 0000000..19af8e9 --- /dev/null +++ b/src/engine/tga.rs @@ -0,0 +1,150 @@ +//! TGA-Decoder → fertig dekodiertes RGBA8-`Image`. +//! +//! Neutral: kennt weder GPU noch UI noch Fonts. Liefert *immer* RGBA8, +//! Ursprung oben-links, vollständig konvertiert — der Aufrufer macht keine +//! Nachbearbeitung. (Anders als irl3d, wo der Loader BGR zurückgab und die +//! RGB-Umrechnung jedem Konsumenten einzeln überließ.) Konsumenten ziehen +//! ein `Image` und legen es ab, wie sie es brauchen: Renderer → GPU-Textur, +//! UI/Font → eigene Interpretation. Die UV-/V-Achsen-Konvention ist damit +//! Sache des Konsumenten beim Sampling, nicht des Loaders. +//! +//! Unterstützt True-Color unkomprimiert (Typ 2) und RLE (Typ 10) mit 24 +//! oder 32 bpp, sowie Graustufen (Typ 3/11, 8 bpp → zu RGBA expandiert) für +//! Font-Maps. Paletten-TGAs gibt es nicht mehr (wds ist 15-bit True-Color). + +pub struct Image { + pub width: u32, + pub height: u32, + /// `width * height * 4`, Ursprung oben-links. + pub rgba: Vec, +} + +pub fn load(path: &str) -> Image { + let bytes = std::fs::read(path).unwrap_or_else(|e| panic!("tga load {path}: {e}")); + decode(&bytes).unwrap_or_else(|e| panic!("tga decode {path}: {e}")) +} + +pub fn decode(d: &[u8]) -> Result { + if d.len() < 18 { return Err("Header zu kurz".into()); } + let id_len = d[0] as usize; + let cmap_type = d[1]; + let image_type = d[2]; + let width = u16::from_le_bytes([d[12], d[13]]) as u32; + let height = u16::from_le_bytes([d[14], d[15]]) as u32; + let depth = d[16]; + let descriptor = d[17]; + + if cmap_type != 0 { + return Err("Color-Map-TGAs nicht unterstützt (True-Color only)".into()); + } + // Pixeldaten beginnen nach Header + ID-Feld (Color-Map ist leer). + let off = 18 + id_len; + + let channels = match (image_type, depth) { + (2 | 10, 24) => 3, + (2 | 10, 32) => 4, + (3 | 11, 8) => 1, + (t, b) => return Err(format!("nicht unterstützt: Typ {t}, {b} bpp")), + }; + let n = (width as usize) * (height as usize); + if n == 0 { return Err("Nullgröße".into()); } + + // Rohpixel (channels Bytes je Pixel), bei RLE entpackt. + let raw_len = n * channels; + let raw: Vec = match image_type { + 2 | 3 => d.get(off..off + raw_len).ok_or("Pixeldaten zu kurz")?.to_vec(), + 10 | 11 => decode_rle(d.get(off..).ok_or("RLE-Daten fehlen")?, raw_len, channels)?, + _ => unreachable!(), + }; + + // → RGBA8. True-Color liegt im TGA als BGR(A) vor; Graustufen replizieren. + let mut rgba = vec![0u8; n * 4]; + for i in 0..n { + let (r, g, b, a) = match channels { + 1 => { let v = raw[i]; (v, v, v, 255) } + 3 => (raw[i * 3 + 2], raw[i * 3 + 1], raw[i * 3], 255), + 4 => (raw[i * 4 + 2], raw[i * 4 + 1], raw[i * 4], raw[i * 4 + 3]), + _ => unreachable!(), + }; + let o = i * 4; + rgba[o] = r; rgba[o + 1] = g; rgba[o + 2] = b; rgba[o + 3] = a; + } + + // Ursprung oben-links erzwingen. Descriptor-Bit 5 gesetzt = top-origin. + if (descriptor >> 5) & 1 == 0 { + flip_vertical(&mut rgba, width as usize, height as usize); + } + Ok(Image { width, height, rgba }) +} + +/// TGA-RLE: Pakete operieren auf ganzen Pixeln (`ch` Bytes). +fn decode_rle(src: &[u8], raw_len: usize, ch: usize) -> Result, String> { + let mut out = Vec::with_capacity(raw_len); + let mut si = 0; + while out.len() < raw_len { + let header = *src.get(si).ok_or("RLE: Quelle zu kurz")?; + si += 1; + let count = (header & 0x7f) as usize + 1; + if header & 0x80 != 0 { + // RLE-Paket: ein Pixel count-mal. + let px = src.get(si..si + ch).ok_or("RLE: Pixel zu kurz")?; + si += ch; + for _ in 0..count { out.extend_from_slice(px); } + } else { + // Raw-Paket: count Pixel am Stück. + let bytes = count * ch; + let chunk = src.get(si..si + bytes).ok_or("RLE: Chunk zu kurz")?; + si += bytes; + out.extend_from_slice(chunk); + } + } + out.truncate(raw_len); + Ok(out) +} + +fn flip_vertical(rgba: &mut [u8], w: usize, h: usize) { + let row = w * 4; + for y in 0..h / 2 { + let (a, b) = (y * row, (h - 1 - y) * row); + for x in 0..row { rgba.swap(a + x, b + x); } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn bgr_to_rgba_uncompressed() { + // Minimal-TGA: Typ 2, 2×1, 24 bpp, bottom-origin (descriptor 0). + // Zwei Pixel BGR (10,20,30) und (40,50,60). + let mut d = vec![0u8; 18]; + d[2] = 2; d[12] = 2; d[14] = 1; d[16] = 24; + d.extend_from_slice(&[10, 20, 30, 40, 50, 60]); + let img = decode(&d).unwrap(); + assert_eq!((img.width, img.height), (2, 1)); + assert_eq!(&img.rgba[0..4], &[30, 20, 10, 255]); + assert_eq!(&img.rgba[4..8], &[60, 50, 40, 255]); + } + + #[test] + fn rle_truecolor_expands() { + // Typ 10, 4×1, 24 bpp: ein RLE-Paket (4× BGR 1,2,3). + let mut d = vec![0u8; 18]; + d[2] = 10; d[12] = 4; d[14] = 1; d[16] = 24; + d.push(0x80 | 3); // RLE, count = 4 + d.extend_from_slice(&[1, 2, 3]); // ein Pixel + let img = decode(&d).unwrap(); + assert_eq!(img.rgba.len(), 4 * 4); + assert!(img.rgba.chunks(4).all(|p| p == [3, 2, 1, 255])); + } + + #[test] + fn decodes_real_placeholder() { + let bytes = std::fs::read("assets/textures/placeholder.tga").unwrap(); + let img = decode(&bytes).unwrap(); + assert_eq!((img.width, img.height), (256, 256)); + assert_eq!(img.rgba.len(), 256 * 256 * 4); + assert!(img.rgba.chunks(4).all(|p| p[3] == 255)); // 24bpp → Alpha 255 + } +} diff --git a/src/render/brush.rs b/src/render/brush.rs new file mode 100644 index 0000000..9509612 --- /dev/null +++ b/src/render/brush.rs @@ -0,0 +1,417 @@ +//! Brush → Render-Geometrie. +//! +//! Ein Quake-Brush ist der Schnitt der Halbräume seiner Face-Ebenen. Aus +//! den Ebenen rekonstruieren wir das konvexe Polyeder (Standard-Verfahren: +//! alle Ebenen-Tripel schneiden, nur Punkte behalten, die in *allen* +//! Halbräumen liegen), bilden pro Face das Polygon und fächern es zu +//! Dreiecken. Indizes werden nach Textur gruppiert (ein Draw-Batch je +//! Textur). +//! +//! Koplanar-Elimination: stoßen zwei Brushes aneinander, teilen sie eine +//! Fläche — beide Faces liegen auf derselben Ebene, mit entgegengesetzten +//! Normalen, und sind nie sichtbar. Wir verwerfen jede Face, die von einer +//! solchen gegenüberliegenden Face vollständig überdeckt wird (hinter ihr +//! liegt dann garantiert solides Brush-Inneres). Zusammen mit dem Back-face +//! Culling der Pipeline bleibt so nur die Außenhaut übrig — ohne BSP. +//! +//! UVs nach dem Standard-Map-Format: die Textur-Achsen ergeben sich aus der +//! dominanten Flächennormale (Quake-`baseaxis`-Tabelle), dann Rotation, +//! Skalierung und Offset der Face. Das erzeugt das klassische Quake- +//! Verziehen auf schrägen Flächen — gewollt fürs PS1-Gefühl. +//! +//! Achsen: Quake ist Z-up, unsere Engine Y-up mit Blick -Z. UVs werden im +//! Quake-System gerechnet (so sind Offsets/Scale definiert), die Position +//! danach umgerechnet: `(qx, qy, qz) → (qx, qz, -qy)`, mal `MAP_SCALE`. + +use crate::engine::map::{Face, Map}; +use crate::render::scene::{Batch, Mesh, Vertex}; + +/// Quake-Einheiten pro Engine-Einheit (32 ≈ klassische „1 Meter"-Annahme). +const MAP_SCALE: f32 = 1.0 / 32.0; +/// Punkt liegt „auf" einer Ebene / „innerhalb" eines Halbraums (Quake-Units). +const ON_EPS: f32 = 1e-2; + +struct Plane { + n: [f32; 3], // nach außen, im Quake-System + d: f32, // n · x = d +} + +/// Ein fertig rekonstruiertes Face-Polygon (Quake-Koords, CCW um `n`), +/// vor der UV-/Engine-Umrechnung. Hält die Ebene für die Koplanar-Prüfung +/// und einen Verweis auf die Quell-Face (Textur, Offsets, Skalierung). +struct FacePoly<'a> { + n: [f32; 3], + d: f32, + poly: Vec<[f32; 3]>, + face: &'a Face, +} + +/// Distinkte Texturnamen in der Map (sortiert) — die Indizes hier sind die +/// Textur-Indizes, auf die `Batch::texture` und der Bilder-Vec verweisen. +pub fn texture_names(map: &Map) -> Vec { + let mut names: Vec = map.entities.iter() + .flat_map(|e| &e.brushes) + .flat_map(|b| &b.faces) + .map(|f| f.texture.clone()) + .collect(); + names.sort_unstable(); + names.dedup(); + names +} + +/// Alle Brushes zu einem Mesh. `tex_names` (aus [`texture_names`]) gibt die +/// Textur-Indizes, `dims` die Pixelmaße je Textur (für UV-Normalisierung). +pub fn build(map: &Map, tex_names: &[String], dims: &[(u32, u32)]) -> Mesh { + // 1) Alle Face-Polygone rekonstruieren (noch in Quake-Koords). + let mut faces: Vec = Vec::new(); + for ent in &map.entities { + for brush in &ent.brushes { + collect_faces(&brush.faces, &mut faces); + } + } + + // 2) Koplanar verdeckte Faces markieren. + let hidden = mark_hidden(&faces); + + // 3) Überlebende mit UV in den Vertex-Buffer ausgeben, nach Textur + // gruppiert. + let mut verts = Vec::new(); + let mut per_tex: Vec> = vec![Vec::new(); tex_names.len()]; + for (i, fp) in faces.iter().enumerate() { + if hidden[i] { continue; } + emit_face(fp, tex_names, dims, &mut verts, &mut per_tex); + } + + // Pro-Textur-Indexlisten zu einem Buffer + Batches verflachen. + let mut indices = Vec::new(); + let mut batches = Vec::new(); + for (ti, list) in per_tex.into_iter().enumerate() { + if list.is_empty() { continue; } + batches.push(Batch { texture: ti, start: indices.len() as u32, count: list.len() as u32 }); + indices.extend_from_slice(&list); + } + Mesh { verts, indices, batches } +} + +/// Die Face-Polygone *eines* Brushes rekonstruieren und an `out` anhängen. +fn collect_faces<'a>(faces: &'a [Face], out: &mut Vec>) { + let planes: Vec = faces.iter().map(|f| plane(&f.plane)).collect(); + let n = planes.len(); + + // Gültige Eckpunkte: Schnitt je dreier Ebenen, sofern in allen + // Halbräumen. + let mut corners: Vec<[f32; 3]> = Vec::new(); + for i in 0..n { + for j in (i + 1)..n { + for k in (j + 1)..n { + let Some(p) = intersect(&planes[i], &planes[j], &planes[k]) else { continue; }; + if planes.iter().all(|pl| dot(pl.n, p) <= pl.d + ON_EPS) + && !corners.iter().any(|q| dist2(*q, p) < ON_EPS * ON_EPS) + { + corners.push(p); + } + } + } + } + + // Pro Face: Eckpunkte auf der Ebene sammeln, CCW um die Normale sortieren. + for (fi, face) in faces.iter().enumerate() { + let pl = &planes[fi]; + let mut poly: Vec<[f32; 3]> = corners.iter().copied() + .filter(|&p| (dot(pl.n, p) - pl.d).abs() < ON_EPS) + .collect(); + if poly.len() < 3 { continue; } + + let center = average(&poly); + let axis_u = any_perp(pl.n); + let axis_v = cross(pl.n, axis_u); + poly.sort_by(|a, b| { + angle(*a, center, axis_u, axis_v) + .partial_cmp(&angle(*b, center, axis_u, axis_v)) + .unwrap_or(std::cmp::Ordering::Equal) + }); + + out.push(FacePoly { n: pl.n, d: pl.d, poly, face }); + } +} + +/// Markiert Faces, die von einer gegenüberliegenden, deckungsgleichen Face +/// (anderer Brush) vollständig überdeckt werden — also nie sichtbar sind. +fn mark_hidden(faces: &[FacePoly]) -> Vec { + let mut hidden = vec![false; faces.len()]; + for i in 0..faces.len() { + let a = &faces[i]; + for (j, b) in faces.iter().enumerate() { + if i == j { continue; } + // Gegenüberliegende, koplanare Ebene? (n_a ≈ -n_b und d_a ≈ -d_b, + // denn n_b·x = d_b ⇒ n_a·x = -d_b muss a.d treffen.) + if dot(a.n, b.n) > -0.999 { continue; } + if (a.d + b.d).abs() > ON_EPS { continue; } + if covered_by(&a.poly, &b.poly, a.n) { + hidden[i] = true; + break; + } + } + } + hidden +} + +/// Liegt jeder Eckpunkt von `inner` innerhalb des Polygons `outer`? Beide +/// liegen (per Aufruf) auf derselben Ebene mit Normale `n`; wir projizieren +/// in deren 2D-Basis. Testpunkte werden minimal zur Polygonmitte gezogen, +/// damit deckungsgleiche Ränder/Ecken eindeutig als „innen" zählen. +fn covered_by(inner: &[[f32; 3]], outer: &[[f32; 3]], n: [f32; 3]) -> bool { + let u = any_perp(n); + let v = cross(n, u); + let c = average(inner); + let proj = |p: [f32; 3]| -> [f32; 2] { let d = sub(p, c); [dot(d, u), dot(d, v)] }; + let outer2: Vec<[f32; 2]> = outer.iter().map(|&p| proj(p)).collect(); + inner.iter().all(|&p| { + let pp = [ + p[0] + 1e-3 * (c[0] - p[0]), + p[1] + 1e-3 * (c[1] - p[1]), + p[2] + 1e-3 * (c[2] - p[2]), + ]; + in_poly_2d(proj(pp), &outer2) + }) +} + +/// Crossing-Number-Punkt-in-Polygon in 2D (orientierungsunabhängig). +fn in_poly_2d(p: [f32; 2], poly: &[[f32; 2]]) -> bool { + let mut inside = false; + let mut j = poly.len() - 1; + for i in 0..poly.len() { + let (a, b) = (poly[i], poly[j]); + if (a[1] > p[1]) != (b[1] > p[1]) { + let t = (p[1] - a[1]) / (b[1] - a[1]); + if p[0] < a[0] + t * (b[0] - a[0]) { inside = !inside; } + } + j = i; + } + inside +} + +/// Ein Face-Polygon mit UV in den Vertex-Buffer fächern. +fn emit_face( + fp: &FacePoly, + tex_names: &[String], + dims: &[(u32, u32)], + verts: &mut Vec, + per_tex: &mut [Vec], +) { + let Some(ti) = tex_names.iter().position(|nm| nm == &fp.face.texture) else { return; }; + let (tw, th) = dims[ti]; + let (su, sv) = tex_axes(fp.n, fp.face); // skalierte Textur-Achsen (Quake) + let base = verts.len() as u32; + for p in &fp.poly { + // UV in Texeln, dann auf 0..1 normalisiert. + let u = (dot(*p, su) + fp.face.offset[0]) / tw as f32; + let v = (dot(*p, sv) + fp.face.offset[1]) / th as f32; + verts.push(Vertex { pos: to_engine(*p), uv: [u, v] }); + } + for t in 1..(fp.poly.len() as u32 - 1) { + per_tex[ti].extend([base, base + t, base + t + 1]); + } +} + +/// Ebene aus drei Face-Punkten. Quake listet sie im Uhrzeigersinn von vorn, +/// damit `cross(c-a, b-a)` nach außen zeigt. +fn plane(p: &[[f32; 3]; 3]) -> Plane { + let n = normalize(cross(sub(p[2], p[0]), sub(p[1], p[0]))); + Plane { n, d: dot(n, p[0]) } +} + +/// Schnittpunkt dreier Ebenen (Cramer); `None`, wenn sie ~parallel sind. +fn intersect(a: &Plane, b: &Plane, c: &Plane) -> Option<[f32; 3]> { + let denom = dot(a.n, cross(b.n, c.n)); + if denom.abs() < 1e-6 { return None; } + let mut x = scale(cross(b.n, c.n), a.d); + x = add(x, scale(cross(c.n, a.n), b.d)); + x = add(x, scale(cross(a.n, b.n), c.d)); + Some(scale(x, 1.0 / denom)) +} + +/// Textur-Achsen (skaliert) im Quake-System nach dem Standard-Map-Format: +/// dominante Achse aus der Normale, dann Rotation und Skalierung. +fn tex_axes(n: [f32; 3], face: &Face) -> ([f32; 3], [f32; 3]) { + let (mut u, mut v) = base_axes(n); + + let (sinv, cosv) = face.rotation.to_radians().sin_cos(); + // Basis-Achsen sind achsen-ausgerichtet (genau eine Komponente ≠ 0); + // wir rotieren ihre beiden in-plane-Komponenten. + let su = nonzero_axis(u); + let sv = nonzero_axis(v); + for vec in [&mut u, &mut v] { + let a = cosv * vec[su] - sinv * vec[sv]; + let b = sinv * vec[su] + cosv * vec[sv]; + vec[su] = a; + vec[sv] = b; + } + + let sx = if face.scale[0] == 0.0 { 1.0 } else { face.scale[0] }; + let sy = if face.scale[1] == 0.0 { 1.0 } else { face.scale[1] }; + (scale(u, 1.0 / sx), scale(v, 1.0 / sy)) +} + +/// Quake-`baseaxis`-Tabelle: (Normalen-Kandidat, U-Achse, V-Achse) × 6. +/// Die U/V-Achsen der am besten passenden Normale bilden die Textur-Ebene. +fn base_axes(n: [f32; 3]) -> ([f32; 3], [f32; 3]) { + const BA: [[[f32; 3]; 3]; 6] = [ + [[0.0, 0.0, 1.0], [1.0, 0.0, 0.0], [0.0, -1.0, 0.0]], // Boden (+Z) + [[0.0, 0.0, -1.0], [1.0, 0.0, 0.0], [0.0, -1.0, 0.0]], // Decke (-Z) + [[ 1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, -1.0]], // West (+X) + [[-1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, -1.0]], // Ost (-X) + [[0.0, 1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, -1.0]], // Süd (+Y) + [[0.0, -1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, -1.0]], // Nord (-Y) + ]; + let mut best = -1.0; + let mut bi = 0; + for (i, ax) in BA.iter().enumerate() { + let d = dot(n, ax[0]); + if d > best { best = d; bi = i; } + } + (BA[bi][1], BA[bi][2]) +} + +fn to_engine(p: [f32; 3]) -> [f32; 3] { + [p[0] * MAP_SCALE, p[2] * MAP_SCALE, -p[1] * MAP_SCALE] +} + +// --- kleine Vektor-Helfer auf [f32; 3] --------------------------------------- + +fn sub(a: [f32; 3], b: [f32; 3]) -> [f32; 3] { [a[0] - b[0], a[1] - b[1], a[2] - b[2]] } +fn add(a: [f32; 3], b: [f32; 3]) -> [f32; 3] { [a[0] + b[0], a[1] + b[1], a[2] + b[2]] } +fn scale(a: [f32; 3], s: f32) -> [f32; 3] { [a[0] * s, a[1] * s, a[2] * s] } +fn dot(a: [f32; 3], b: [f32; 3]) -> f32 { a[0] * b[0] + a[1] * b[1] + a[2] * b[2] } +fn dist2(a: [f32; 3], b: [f32; 3]) -> f32 { let d = sub(a, b); dot(d, d) } + +fn cross(a: [f32; 3], b: [f32; 3]) -> [f32; 3] { + [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]] +} + +fn normalize(a: [f32; 3]) -> [f32; 3] { + let len = dot(a, a).sqrt(); + if len > 0.0 { scale(a, 1.0 / len) } else { a } +} + +fn average(pts: &[[f32; 3]]) -> [f32; 3] { + let mut c = [0.0; 3]; + for p in pts { c = add(c, *p); } + scale(c, 1.0 / pts.len() as f32) +} + +/// Index der (einzigen) Nicht-Null-Komponente einer achsen-ausgerichteten Achse. +fn nonzero_axis(a: [f32; 3]) -> usize { + if a[0] != 0.0 { 0 } else if a[1] != 0.0 { 1 } else { 2 } +} + +/// Achse in der Ebene, möglichst weit weg von der Normale, als Sortier-Basis. +fn any_perp(n: [f32; 3]) -> [f32; 3] { + let a = if n[0].abs() < 0.9 { [1.0, 0.0, 0.0] } else { [0.0, 1.0, 0.0] }; + normalize(cross(n, a)) +} + +/// Winkel von `p` um `center` in der (u, v)-Basis — für CCW-Sortierung. +fn angle(p: [f32; 3], center: [f32; 3], u: [f32; 3], v: [f32; 3]) -> f32 { + let d = sub(p, center); + dot(d, v).atan2(dot(d, u)) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::engine::map; + + // Ein achsenparalleler Quader-Brush (x[-48,80], y[-64,64], z[-48,-16]). + const BOX: &str = r#" +{ +"classname" "worldspawn" +{ +( -48 -64 -48 ) ( -48 -63 -48 ) ( -48 -64 -47 ) t 0 0 0 1 1 +( -48 -64 -48 ) ( -48 -64 -47 ) ( -47 -64 -48 ) t 0 0 0 1 1 +( -48 -64 -48 ) ( -47 -64 -48 ) ( -48 -63 -48 ) t 0 0 0 1 1 +( 80 64 -16 ) ( 80 65 -16 ) ( 81 64 -16 ) t 0 0 0 1 1 +( 80 64 -16 ) ( 81 64 -16 ) ( 80 64 -15 ) t 0 0 0 1 1 +( 80 64 -16 ) ( 80 64 -15 ) ( 80 65 -16 ) t 0 0 0 1 1 +} +} +"#; + + #[test] + fn box_brush_has_cuboid_geometry() { + let m = map::parse(BOX); + let names = texture_names(&m); + assert_eq!(names, vec!["t".to_string()]); + let mesh = build(&m, &names, &[(64, 64)]); + + // Quader: 6 Faces · 4 Ecken = 24 Vertices, 6 · 2 Dreiecke · 3 = 36 Indizes. + assert_eq!(mesh.verts.len(), 24); + assert_eq!(mesh.indices.len(), 36); + // Eine Textur → ein Batch über alle Indizes. + assert_eq!(mesh.batches.len(), 1); + assert_eq!((mesh.batches[0].texture, mesh.batches[0].start, mesh.batches[0].count), (0, 0, 36)); + + // Engine-Bounds: x[-1.5,2.5], y(=quake z)[-1.5,-0.5], z(=-quake y)[-2,2]. + let (mut mn, mut mx) = ([f32::MAX; 3], [f32::MIN; 3]); + for v in &mesh.verts { + for k in 0..3 { + mn[k] = mn[k].min(v.pos[k]); + mx[k] = mx[k].max(v.pos[k]); + } + } + let approx = |a: f32, b: f32| (a - b).abs() < 1e-4; + assert!(approx(mn[0], -1.5) && approx(mx[0], 2.5), "x: {:?}..{:?}", mn[0], mx[0]); + assert!(approx(mn[1], -1.5) && approx(mx[1], -0.5), "y: {:?}..{:?}", mn[1], mx[1]); + assert!(approx(mn[2], -2.0) && approx(mx[2], 2.0), "z: {:?}..{:?}", mn[2], mx[2]); + } + + // Zwei achsenparallele Würfel, die sich die Ebene x=64 teilen: + // A = x[0,64], B = x[64,128], beide y[0,64] z[0,64]. + const TWO_BOXES: &str = r#" +{ +"classname" "worldspawn" +{ +( 0 0 0 ) ( 0 1 0 ) ( 0 0 1 ) t 0 0 0 1 1 +( 0 0 0 ) ( 0 0 1 ) ( 1 0 0 ) t 0 0 0 1 1 +( 0 0 0 ) ( 1 0 0 ) ( 0 1 0 ) t 0 0 0 1 1 +( 64 64 64 ) ( 64 65 64 ) ( 65 64 64 ) t 0 0 0 1 1 +( 64 64 64 ) ( 65 64 64 ) ( 64 64 65 ) t 0 0 0 1 1 +( 64 64 64 ) ( 64 64 65 ) ( 64 65 64 ) t 0 0 0 1 1 +} +{ +( 64 0 0 ) ( 64 1 0 ) ( 64 0 1 ) t 0 0 0 1 1 +( 64 0 0 ) ( 64 0 1 ) ( 65 0 0 ) t 0 0 0 1 1 +( 64 0 0 ) ( 65 0 0 ) ( 64 1 0 ) t 0 0 0 1 1 +( 128 64 64 ) ( 128 65 64 ) ( 129 64 64 ) t 0 0 0 1 1 +( 128 64 64 ) ( 129 64 64 ) ( 128 64 65 ) t 0 0 0 1 1 +( 128 64 64 ) ( 128 64 65 ) ( 128 65 64 ) t 0 0 0 1 1 +} +} +"#; + + #[test] + fn shared_faces_are_eliminated() { + let m = map::parse(TWO_BOXES); + let names = texture_names(&m); + let mesh = build(&m, &names, &[(64, 64)]); + + // 2 Würfel · 6 Faces = 12, minus die 2 deckungsgleichen an x=64 → 10. + // 10 Faces · 4 Ecken = 40 Vertices, 10 · 2 Dreiecke · 3 = 60 Indizes. + // (Ohne Elimination wären es 48 Vertices / 72 Indizes.) + assert_eq!(mesh.verts.len(), 40); + assert_eq!(mesh.indices.len(), 60); + } + + #[test] + fn uvs_scale_with_texture_size() { + // Auf der +Z-Boden-Face (baseaxis U=+X, V=-Y) ist U = qx/tw, V = -qy/th + // bei Offset 0, Scale 1. Prüft, dass UVs in Texeln/Größe gerechnet werden. + let m = map::parse(BOX); + let names = texture_names(&m); + let mesh = build(&m, &names, &[(64, 64)]); + // Alle UVs endlich und im erwarteten groben Bereich (Brush ~128 units + // breit / 64 Texel → bis ~2). + assert!(mesh.verts.iter().all(|v| v.uv[0].is_finite() && v.uv[1].is_finite())); + assert!(mesh.verts.iter().any(|v| v.uv[0].abs() > 0.5)); + } +} diff --git a/src/render/camera.rs b/src/render/camera.rs new file mode 100644 index 0000000..a3856ce --- /dev/null +++ b/src/render/camera.rs @@ -0,0 +1,102 @@ +//! Flycam: freie Debug-/Entwicklungskamera (Schritt 4). Position, Yaw und +//! Pitch in f32 — Port der irl3d-Kamera-Idee, aber ohne Q16.16: an dieser +//! Kamera hängt keine Physik, und der Renderer rechnet ohnehin f32. +//! +//! Konvention wie `math::Mat4::view`: yaw=0 blickt -Z, positiver Yaw dreht +//! nach links, positiver Pitch hebt den Blick. + +use crate::render::math::Mat4; + +/// Knapp unter 90°: hält den Blick aus der Senkrechten, wo Yaw bedeutungslos +/// würde (und `right`/`forward` zusammenfielen). +const PITCH_LIMIT: f32 = 1.55; + +pub struct Camera { + pub pos: [f32; 3], + pub yaw: f32, + pub pitch: f32, +} + +impl Camera { + pub fn new(pos: [f32; 3]) -> Self { + Self { pos, yaw: 0.0, pitch: 0.0 } + } + + /// Roh-Maus-Delta (Pixel) → Yaw/Pitch. `sens` in Radiant pro Pixel. + /// Maus nach rechts dreht den Blick nach rechts (Yaw sinkt), Maus nach + /// unten senkt ihn; Pitch wird geclamped. + pub fn apply_mouse(&mut self, dx: f32, dy: f32, sens: f32) { + self.yaw -= dx * sens; + self.pitch = (self.pitch - dy * sens).clamp(-PITCH_LIMIT, PITCH_LIMIT); + } + + /// Blickrichtung inkl. Pitch — W/S bewegen sich hier entlang. + fn forward(&self) -> [f32; 3] { + let (sy, cy) = self.yaw.sin_cos(); + let (sp, cp) = self.pitch.sin_cos(); + [-sy * cp, sp, -cy * cp] + } + + /// Horizontales Rechts (ohne Pitch) — A/D strafen hier entlang, ohne + /// dass der Blick nach oben/unten die Bewegung kippt. + fn right(&self) -> [f32; 3] { + let (sy, cy) = self.yaw.sin_cos(); + [cy, 0.0, -sy] + } + + /// Bewegung integrieren. `fwd`/`right`/`up` je in [-1, 1] (Tastenachsen), + /// `dist` ist die Strecke diesen Frame (Speed · dt). Hoch/runter ist + /// Welt-Y, damit Auf-/Absteigen unabhängig vom Blickwinkel bleibt. + pub fn translate(&mut self, fwd: f32, right: f32, up: f32, dist: f32) { + let f = self.forward(); + let r = self.right(); + for i in 0..3 { + self.pos[i] += (f[i] * fwd + r[i] * right) * dist; + } + self.pos[1] += up * dist; + } + + pub fn view(&self) -> Mat4 { + Mat4::view(self.pos, self.yaw, self.pitch) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Die Bewegungs-Basis (`forward`) muss exakt zu der Richtung passen, + /// die `view()` als „vor der Kamera" definiert — sonst driften + /// Steuerung und Bild auseinander. Ein Punkt `d` vor der Kamera muss + /// in View-Space auf (0, 0, -d) landen. + #[test] + fn forward_matches_view() { + for &(yaw, pitch) in &[(0.0, 0.0), (0.7, 0.0), (0.0, 0.5), (1.2, -0.4)] { + let c = Camera { pos: [1.0, 2.0, 3.0], yaw, pitch }; + let f = c.forward(); + let ahead = [ + c.pos[0] + f[0] * 5.0, + c.pos[1] + f[1] * 5.0, + c.pos[2] + f[2] * 5.0, + 1.0, + ]; + let v = c.view().transform(ahead); + assert!( + (v[0]).abs() < 1e-4 && (v[1]).abs() < 1e-4 && (v[2] + 5.0).abs() < 1e-4, + "yaw={yaw} pitch={pitch}: {v:?}", + ); + } + } + + /// `right` muss horizontal sein und im View-Space nach +X zeigen + /// (sonst wären A/D vertauscht oder gekippt). + #[test] + fn right_is_horizontal_and_points_view_right() { + let c = Camera { pos: [0.0, 0.0, 0.0], yaw: 0.6, pitch: 0.3 }; + let r = c.right(); + assert!(r[1].abs() < 1e-6, "right darf kein Y haben: {r:?}"); + let p = [r[0] * 5.0, r[1] * 5.0, r[2] * 5.0, 1.0]; + let v = c.view().transform(p); + assert!(v[0] > 4.0, "right sollte nach View-+X zeigen: {v:?}"); + } +} diff --git a/src/render/font.rs b/src/render/font.rs new file mode 100644 index 0000000..95bc5e5 --- /dev/null +++ b/src/render/font.rs @@ -0,0 +1,168 @@ +//! CP437-Bitmap-Fonts mit proportionaler (variabler) Laufweite. +//! +//! Alle Atlanten sind nach demselben Schema aufgebaut: 32 Spalten × 8 Zeilen, +//! row-major mit Codepoint = Index (`A` an 65, `a` an 97 → col = c%32, +//! row = c/32). Die Glyphen liegen weiß auf schwarz *ohne* Alpha; wir leiten +//! die Maske beim Laden aus der Luminanz ab (`ui::key_luminance`, geteilt mit +//! Cursor- und Ornament-Sprites). +//! +//! Proportionale Breite: statt jeder Glyph-Zelle ihre volle Breite zu geben, +//! **messen** wir beim Laden aus der Alpha-Maske die Tinten-Spannweite jedes +//! Glyphs (linkeste/rechteste nicht-leere Spalte). Gezeichnet wird tight +//! gecroppt, der Cursor rückt nur um Ink-Breite + Tracking vor; leere Glyphen +//! (Space) bekommen eine feste Breite. So entsteht kein Sonderfall im Shader — +//! nur die UV- und Advance-Rechnung wird pro Glyph variabel. +//! +//! Ein [`Font`] wird zur Laufzeit aus dem geladenen Atlas gebaut und kennt +//! seinen GPU-Textur-Index, damit `ui::Ui::text` ohne Zusatzargumente damit +//! zeichnen kann. + +use crate::engine::tga::Image; + +const COLS: u32 = 32; + +/// Ein geladener Glyph-Atlas: Maße, gemessene Glyph-Breiten und der zugehörige +/// Sprite-Textur-Index. +pub(crate) struct Font { + pub(crate) glyph_h: f32, + glyph_w: f32, + atlas_w: f32, + atlas_h: f32, + tex: usize, + /// Ink-Startspalte je Codepoint (innerhalb der Zelle, 0..glyph_w). + left: [u8; 256], + /// Ink-Breite je Codepoint (0 = leer/Space). + width: [u8; 256], + tracking: f32, + space_adv: f32, +} + +/// Die im Projekt geladenen Fonts, gebaut in render::run und an `ui::layout` +/// gereicht. +pub(crate) struct Fonts { + pub(crate) ega: Font, + pub(crate) cga: Font, +} + +impl Font { + /// EGA 8×14 (`assets/textures/fonts/oldschool-ega-8x14.tga`, 256×112). + pub(crate) fn ega(tex: usize, atlas: &Image) -> Font { + Font::measure(8.0, 14.0, 256.0, 112.0, tex, atlas) + } + /// CGA 8×8 (`assets/textures/fonts/oldschool-cga-8x8.tga`, 256×64). + pub(crate) fn cga(tex: usize, atlas: &Image) -> Font { + Font::measure(8.0, 8.0, 256.0, 64.0, tex, atlas) + } + + /// Glyph-Breiten aus der Alpha-Maske des Atlas messen. + fn measure(glyph_w: f32, glyph_h: f32, atlas_w: f32, atlas_h: f32, tex: usize, atlas: &Image) -> Font { + let (gw, gh, aw) = (glyph_w as usize, glyph_h as usize, atlas_w as usize); + let mut left = [0u8; 256]; + let mut width = [0u8; 256]; + for code in 0..256usize { + let col = (code as u32 % COLS) as usize; + let row = (code as u32 / COLS) as usize; + let (l, w) = ink_bounds(&atlas.rgba, aw, col * gw, row * gh, gw, gh); + left[code] = l; + width[code] = w; + } + Font { + glyph_h, glyph_w, atlas_w, atlas_h, tex, left, width, + // 1px Abstand zwischen Glyphen; Space = halbe Zellbreite. + tracking: 1.0, + space_adv: (glyph_w * 0.5).round(), + } + } + + pub(crate) fn tex(&self) -> usize { self.tex } + + /// UV-Rechteck + Zeichenbreite (Pixel) eines Glyphs, tight gecroppt. + /// `None` für leere Glyphen (Space) — dort wird nichts gezeichnet. + pub(crate) fn glyph_quad(&self, code: u8) -> Option<([f32; 4], f32)> { + let w = self.width[code as usize]; + if w == 0 { return None; } + let col = (code as u32 % COLS) as f32; + let row = (code as u32 / COLS) as f32; + let x0 = col * self.glyph_w + self.left[code as usize] as f32; + let y0 = row * self.glyph_h; + let uv = [ + x0 / self.atlas_w, y0 / self.atlas_h, + (x0 + w as f32) / self.atlas_w, (y0 + self.glyph_h) / self.atlas_h, + ]; + Some((uv, w as f32)) + } + + /// Wie weit der Cursor nach diesem Glyph vorrückt. + pub(crate) fn advance(&self, code: u8) -> f32 { + match self.width[code as usize] { + 0 => self.space_adv, + w => w as f32 + self.tracking, + } + } + + /// Breite einer Zeichenkette in Pixeln (Summe der Advances). + pub(crate) fn text_width(&self, s: &str) -> f32 { + s.chars().map(|ch| self.advance(cp437(ch))).sum() + } +} + +/// Linkeste Ink-Spalte und Ink-Breite einer Glyph-Zelle in der Alpha-Maske. +/// `(0, 0)` wenn die Zelle leer ist. +fn ink_bounds(rgba: &[u8], atlas_w: usize, x0: usize, y0: usize, gw: usize, gh: usize) -> (u8, u8) { + let (mut lo, mut hi) = (gw, 0usize); + let mut any = false; + for y in 0..gh { + for x in 0..gw { + let a = rgba[((y0 + y) * atlas_w + (x0 + x)) * 4 + 3]; + if a > 0 { + any = true; + if x < lo { lo = x; } + if x > hi { hi = x; } + } + } + } + if any { (lo as u8, (hi - lo + 1) as u8) } else { (0, 0) } +} + +/// Ein Unicode-Zeichen auf seinen CP437-Codepoint abbilden. ASCII ist +/// identisch; für die im Projekt nötigen deutschen Sonderzeichen gibt es +/// feste Plätze, alles andere fällt auf '?' zurück. +pub(crate) fn cp437(ch: char) -> u8 { + match ch { + c if (c as u32) < 128 => c as u8, + 'Ç' => 128, 'ü' => 129, 'é' => 130, 'ä' => 132, 'à' => 133, + 'å' => 134, 'ç' => 135, 'ë' => 137, 'è' => 138, 'ï' => 139, + 'î' => 140, 'ì' => 141, 'Ä' => 142, 'Å' => 143, 'É' => 144, + 'ö' => 148, 'ò' => 149, 'û' => 150, 'ù' => 151, 'Ö' => 153, + 'Ü' => 154, 'á' => 160, 'í' => 161, 'ó' => 162, 'ú' => 163, + 'ñ' => 164, 'Ñ' => 165, 'ß' => 225, '°' => 248, + _ => b'?', + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn ink_bounds_finds_span_and_blank() { + // 4px-breite "Zelle" (atlas_w=4, 1 Zeile): Ink in Spalten 1..=2. + // rgba: x0 leer, x1 voll, x2 voll, x3 leer (nur Alpha relevant). + let rgba = vec![ + 0,0,0,0, 255,255,255,255, 255,255,255,255, 0,0,0,0, + ]; + assert_eq!(ink_bounds(&rgba, 4, 0, 0, 4, 1), (1, 2)); + // Komplett leere Zelle. + let blank = vec![0u8; 4 * 4]; + assert_eq!(ink_bounds(&blank, 4, 0, 0, 4, 1), (0, 0)); + } + + #[test] + fn cp437_maps_ascii_and_umlauts() { + assert_eq!(cp437('A'), 65); + assert_eq!(cp437('a'), 97); + assert_eq!(cp437('ä'), 132); + assert_eq!(cp437('ß'), 225); + assert_eq!(cp437('世'), b'?'); // außerhalb CP437 → Fallback + } +} diff --git a/src/render/gpu.rs b/src/render/gpu.rs index ddeb397..bacd699 100644 --- a/src/render/gpu.rs +++ b/src/render/gpu.rs @@ -10,11 +10,14 @@ use std::sync::Arc; use winit::window::Window; +use crate::engine::tga::Image; use crate::render::math::Mat4; -use crate::render::scene::ScenePass; +use crate::render::scene::{Mesh, ScenePass}; +use crate::render::sprite::SpritePass; +use crate::render::ui::Ui; -pub const INTERNAL_W: u32 = 480; -pub const INTERNAL_H: u32 = 360; +pub const INTERNAL_W: u32 = 320; +pub const INTERNAL_H: u32 = 240; /// D16 reicht für PS1-Geometrieskalen und ist das älteste, überall /// (auch GL-Fallback) unterstützte Depth-Format. @@ -31,12 +34,19 @@ pub struct Gpu { depth_view: wgpu::TextureView, scene: ScenePass, + sprite: SpritePass, blit_pipeline: wgpu::RenderPipeline, blit_bind: wgpu::BindGroup, } impl Gpu { - pub fn new(window: Arc, display: winit::event_loop::OwnedDisplayHandle) -> Self { + pub fn new( + window: Arc, + display: winit::event_loop::OwnedDisplayHandle, + mesh: &Mesh, + images: &[Image], + ui_textures: &[Image], + ) -> Self { let size = window.inner_size(); // Display-Handle mitgeben: für den GL-Fallback (v.a. Wayland) // Pflicht; Vulkan ignoriert es. `with_env` erlaubt Overrides wie @@ -86,8 +96,15 @@ impl Gpu { let (internal_view, depth_view) = make_internal_targets(&device); - // Pass 1: die Szene. - let scene = ScenePass::new(&device, INTERNAL_FORMAT, DEPTH_FORMAT); + // Pass 1: die Szene. Geometrie + Bilder kommen fertig vom Aufrufer + // (Decode in render::run); hier wird nur hochgeladen. + let scene = ScenePass::new(&device, &queue, INTERNAL_FORMAT, DEPTH_FORMAT, mesh, images); + + // Overlay-Pass: zeichnet nach der Szene ins selbe interne Target. + let sprite = SpritePass::new( + &device, &queue, INTERNAL_FORMAT, DEPTH_FORMAT, + [INTERNAL_W as f32, INTERNAL_H as f32], ui_textures, + ); // Pass 2: internes Target nearest-gesampelt auf die Surface. let blit_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { @@ -139,7 +156,7 @@ impl Gpu { Self { surface, device, queue, config, internal_view, depth_view, - scene, blit_pipeline, blit_bind, + scene, sprite, blit_pipeline, blit_bind, } } @@ -149,20 +166,27 @@ impl Gpu { self.surface.configure(&self.device, &self.config); } - /// Einen Frame rendern. `t` = Sekunden seit Start (treibt vorerst - /// die Orbit-Kamera; ab Schritt 4 kommt die Kamera vom Aufrufer). - pub fn frame(&mut self, t: f32) { - // Hartkodierte Orbit-Kamera um den Testwürfel — macht Vertex-Snap - // und Dither in Bewegung sichtbar. Die Flycam ersetzt sie. - let yaw = t * 0.4; - let eye = [2.2 * yaw.sin(), 1.3, 2.2 * yaw.cos()]; - let view = Mat4::view(eye, yaw, -0.5); + /// Fenster-Pixel (physisch) → interne Pixel, über dieselbe Letterbox wie + /// der Blit. Außerhalb des Bildbereichs auf die Ränder geklemmt. + pub fn map_cursor(&self, win: [f32; 2]) -> [f32; 2] { + let (vx, vy, vw, vh) = letterbox(self.config.width, self.config.height); + let ix = (win[0] - vx) / vw * INTERNAL_W as f32; + let iy = (win[1] - vy) / vh * INTERNAL_H as f32; + [ix.clamp(0.0, INTERNAL_W as f32), iy.clamp(0.0, INTERNAL_H as f32)] + } + + /// Einen Frame rendern. Die View-Matrix kommt vom Aufrufer (Flycam); + /// die Projektion gehört hierher, weil sie an der internen Auflösung + /// hängt. + pub fn frame(&mut self, view: &Mat4, ui: &Ui) { let proj = Mat4::perspective( 60f32.to_radians(), INTERNAL_W as f32 / INTERNAL_H as f32, 0.1, 100.0, ); - self.scene.prepare(&self.queue, &proj.mul(&view)); + let half_res = [INTERNAL_W as f32 * 0.5, INTERNAL_H as f32 * 0.5]; + self.scene.prepare(&self.queue, &proj.mul(view), half_res); + self.sprite.prepare(&self.queue, &ui.verts); use wgpu::CurrentSurfaceTexture as Cst; let frame = match self.surface.get_current_texture() { @@ -206,6 +230,8 @@ impl Gpu { multiview_mask: None, }); self.scene.draw(&mut pass); + // Overlay obenauf, im selben Pass (kein Tiefentest). + self.sprite.draw(&mut pass, &ui.batches); } // Pass 2: Letterbox-Blit aufs Fenster. diff --git a/src/render/mod.rs b/src/render/mod.rs index 8dddb3d..28107b5 100644 --- a/src/render/mod.rs +++ b/src/render/mod.rs @@ -1,41 +1,88 @@ //! Fenster-Frontend: winit-Loop um den wgpu-Renderer. //! -//! Stand: Schritt 3 des Renderer-Plans (notes/renderer-plan.md) — -//! internes 320×240-Target, PS1-Szenen-Pass (Pixel-Snap, affine -//! Interpolation, RGB555+Bayer) am Testwürfel, Letterbox-Blit. -//! Als Nächstes: Flycam (Schritt 4), dann OBJ-Szene (Schritt 5). +//! Stand: Schritt 4 des Renderer-Plans (notes/renderer-plan.md) — Flycam +//! über dem PS1-Szenen-Pass. WASD bewegt, Maus blickt (Klick fängt den +//! Cursor, Escape gibt ihn frei bzw. beendet), Space/Shift hoch/runter. +//! Als Nächstes: OBJ-Szene (Schritt 5). //! //! Steuert dieselbe [`Session`] wie die CLI: das Fenster *besitzt* sie, //! Terminal-Befehle kommen über einen Channel von einem stdin-Thread und //! werden pro Loop-Durchlauf eingespielt. Im Dialog-Modus pausiert die -//! Welt (die Sim-Uhr läuft nicht weiter) — so sieht das Fenster nie einen +//! Welt (Sim-Uhr und Kamera stehen) — so sieht das Fenster nie einen //! anderen Zustand als die Konsole. //! //! Schicht-Regel wie `cli`: Geschwister von `engine`/`session`, konsumiert //! deren Schnittstellen — nie umgekehrt. +mod brush; +mod camera; +mod font; mod gpu; mod math; mod scene; +mod sprite; +mod ui; use std::sync::mpsc::{self, Receiver}; use std::sync::Arc; use std::time::Instant; use winit::application::ApplicationHandler; -use winit::event::{ElementState, KeyEvent, WindowEvent}; +use winit::event::{DeviceEvent, DeviceId, ElementState, KeyEvent, MouseButton, WindowEvent}; use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop}; use winit::keyboard::{KeyCode, PhysicalKey}; -use winit::window::{Window, WindowId}; +use winit::window::{CursorGrabMode, Window, WindowId}; +use crate::engine::tga::Image; +use crate::engine::{assets, map, tga}; use crate::session::{Mode, Session}; +use camera::Camera; use gpu::Gpu; +use scene::Mesh; + +/// Radiant pro Maus-Pixel. +const MOUSE_SENS: f32 = 0.0025; +/// Welt-Einheiten pro Sekunde. +const MOVE_SPEED: f32 = 4.0; pub fn run(mut session: Session) { // Init-Signal feuern, bevor das Fenster steht (kann bereits einen // Dialog öffnen — dann startet die Welt eben pausiert). for line in session.start() { println!("{line}"); } + // CPU-Assets dekodieren (Pull über die engine-Decoder). Das Hochladen + // auf die GPU macht später `Gpu` — Decode (CPU) und Upload (GPU) bleiben + // getrennt. + let world = map::load(&assets::path("assets/maps/test.map")); + let tex_names = brush::texture_names(&world); + let images: Vec = tex_names.iter() + .map(|n| tga::load(&assets::path(&format!("assets/textures/{n}.tga")))) + .collect(); + let dims: Vec<(u32, u32)> = images.iter().map(|i| (i.width, i.height)).collect(); + let mesh = brush::build(&world, &tex_names, &dims); + report_map(&world, &tex_names, &mesh); + + // UI-Texturen, Reihenfolge = die Index-Konstanten in ui (WHITE, FONT_EGA, + // FONT_CGA, CURSORS, ORN). Decode (CPU) bleibt in run(); Fonts, Cursor und + // Ornament sind weiß-auf-schwarz und bekommen ihre Alpha-Maske aus der + // Luminanz. + let load_keyed = |p: &str| ui::key_luminance(&tga::load(&assets::path(p))); + // Font-Atlanten zuerst keyen, daraus die Glyph-Breiten messen (Font), + // dann dieselben Bilder in die GPU-Texturliste übernehmen. + let ega_img = load_keyed("assets/textures/fonts/oldschool-ega-8x14.tga"); + let cga_img = load_keyed("assets/textures/fonts/oldschool-cga-8x8.tga"); + let fonts = font::Fonts { + ega: font::Font::ega(ui::FONT_EGA, &ega_img), + cga: font::Font::cga(ui::FONT_CGA, &cga_img), + }; + let ui_textures: Vec = vec![ + ui::white_pixel(), + ega_img, + cga_img, + load_keyed("assets/textures/ui/cursors.tga"), + load_keyed("assets/textures/ui/panel_ornaments.tga"), + ]; + // stdin auf einem eigenen Thread: er darf blockieren, der Main-Thread // (winit) nicht. Es queren nur Strings die Thread-Grenze, kein Zustand // — der bleibt allein auf dem Main-Thread. @@ -58,25 +105,101 @@ pub fn run(mut session: Session) { // Poll statt Wait: wir rendern kontinuierlich (Spiel, kein Editor) und // pollen nebenbei den Befehls-Channel. event_loop.set_control_flow(ControlFlow::Poll); - let mut app = App::new(session, rx); + let mut app = App::new(session, rx, mesh, images, ui_textures, fonts); event_loop.run_app(&mut app).expect("winit: run"); } +/// Kurzer Lade-Report: Map-Inhalt und resultierende Geometriegröße. +fn report_map(world: &map::Map, tex_names: &[String], mesh: &Mesh) { + let brushes: usize = world.entities.iter().map(|e| e.brushes.len()).sum(); + let faces: usize = world.entities.iter() + .flat_map(|e| &e.brushes) + .map(|b| b.faces.len()) + .sum(); + println!( + "[map] {} Entities, {} Brushes, {} Faces, Texturen {:?} → {} Vertices, {} Dreiecke", + world.entities.len(), brushes, faces, tex_names, + mesh.verts.len(), mesh.indices.len() / 3, + ); +} + +/// Aktueller Eingabezustand. Tasten als gehaltene Flags (nicht Events), +/// damit die Bewegung pro Frame mit dt skaliert; Maus-Delta wird zwischen +/// den Frames akkumuliert und nach dem Anwenden genullt. +#[derive(Default)] +struct Input { + fwd: bool, + back: bool, + left: bool, + right: bool, + up: bool, + down: bool, + mouse_dx: f32, + mouse_dy: f32, + /// Cursor gefangen → Maus-Look aktiv. + grabbed: bool, +} + struct App { window: Option>, gpu: Option, session: Session, - /// Befehlszeilen vom stdin-Thread. rx: Receiver, - /// Sim-Zeit (Sekunden), läuft nur im `Free`-Modus weiter → Welt-Pause - /// im Dialog. Treibt aktuell die Orbit-Kamera (bis Flycam, Schritt 4). - sim_t: f32, + camera: Camera, + input: Input, last: Option, + /// Letzte Mausposition in Fenster-Pixeln (für den UI-Cursor). + cursor_win: [f32; 2], + /// CPU-seitige Welt-Geometrie + Texturen, in `resumed` einmal auf die + /// GPU geladen. + mesh: Mesh, + images: Vec, + ui_textures: Vec, + fonts: font::Fonts, } impl App { - fn new(session: Session, rx: Receiver) -> Self { - Self { window: None, gpu: None, session, rx, sim_t: 0.0, last: None } + fn new( + session: Session, + rx: Receiver, + mesh: Mesh, + images: Vec, + ui_textures: Vec, + fonts: font::Fonts, + ) -> Self { + Self { + window: None, + gpu: None, + session, + rx, + // Etwas zurück und erhöht, Blick Richtung Welt-Ursprung. + camera: Camera::new([0.0, 1.0, 4.0]), + input: Input::default(), + last: None, + cursor_win: [0.0, 0.0], + mesh, + images, + ui_textures, + fonts, + } + } + + // Der OS-Cursor bleibt immer versteckt (in `resumed` einmal gesetzt) — + // wir zeichnen stets ein eigenes Sprite. `set_grab` schaltet nur die + // Greif-Art: Locked (Flycam, relativer Blick) ↔ None (freie Maus, UI). + fn set_grab(&mut self, grab: bool) { + let Some(window) = &self.window else { return; }; + if grab { + // Locked (Cursor fixiert) ist am angenehmsten, wird aber nicht + // überall unterstützt → auf Confined zurückfallen. + let ok = window.set_cursor_grab(CursorGrabMode::Locked) + .or_else(|_| window.set_cursor_grab(CursorGrabMode::Confined)) + .is_ok(); + if ok { self.input.grabbed = true; } + } else { + let _ = window.set_cursor_grab(CursorGrabMode::None); + self.input.grabbed = false; + } } } @@ -87,14 +210,33 @@ impl ApplicationHandler for App { if self.window.is_some() { return; } let attrs = Window::default_attributes() .with_title("wds") - // 3× interne Auflösung als Startgröße; frei resizebar. + // 2× interne Auflösung als Startgröße; frei resizebar. .with_inner_size(winit::dpi::LogicalSize::new( - gpu::INTERNAL_W * 3, gpu::INTERNAL_H * 3)); + gpu::INTERNAL_W * 2, gpu::INTERNAL_H * 2)); let window = Arc::new(event_loop.create_window(attrs).expect("winit: Fenster")); - self.gpu = Some(Gpu::new(window.clone(), event_loop.owned_display_handle())); + // OS-Cursor dauerhaft aus — das Overlay zeichnet immer einen eigenen. + window.set_cursor_visible(false); + self.gpu = Some(Gpu::new( + window.clone(), + event_loop.owned_display_handle(), + &self.mesh, + &self.images, + &self.ui_textures, + )); self.window = Some(window); } + /// Rohe Maus-Bewegung (unabhängig von Cursor-Position) — nur sinnvoll, + /// solange der Cursor gefangen ist. + fn device_event(&mut self, _el: &ActiveEventLoop, _id: DeviceId, event: DeviceEvent) { + if let DeviceEvent::MouseMotion { delta } = event { + if self.input.grabbed { + self.input.mouse_dx += delta.0 as f32; + self.input.mouse_dy += delta.1 as f32; + } + } + } + /// Läuft einmal pro Loop-Durchlauf, nachdem die Events abgearbeitet /// sind: Konsolen-Befehle einspielen und den nächsten Frame anfordern. fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) { @@ -109,29 +251,87 @@ impl ApplicationHandler for App { fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) { match event { WindowEvent::CloseRequested => event_loop.exit(), - WindowEvent::KeyboardInput { - event: KeyEvent { - physical_key: PhysicalKey::Code(KeyCode::Escape), - state: ElementState::Pressed, .. - }, .. - } => event_loop.exit(), + WindowEvent::CursorMoved { position, .. } => { + self.cursor_win = [position.x as f32, position.y as f32]; + } + WindowEvent::MouseInput { state: ElementState::Pressed, button: MouseButton::Left, .. } => { + // Nur im freien Modus greift der Klick die Maus (Flycam). Im + // UI-/Dialog-Modus bleibt die Maus frei (für Klicks aufs UI). + if matches!(self.session.mode, Mode::Free) { self.set_grab(true); } + } + WindowEvent::KeyboardInput { event: key, .. } => self.on_key(event_loop, key), WindowEvent::Resized(size) => { if let Some(gpu) = &mut self.gpu { gpu.resize(size.width, size.height); } } - WindowEvent::RedrawRequested => { - // dt messen; im Dialog die Sim-Uhr anhalten → Welt pausiert, - // während die Konsole den Dialog treibt. - let now = Instant::now(); - let dt = self.last.replace(now) - .map_or(0.0, |prev| (now - prev).as_secs_f32()); - if matches!(self.session.mode, Mode::Free) { - self.sim_t += dt; - } - if let Some(gpu) = &mut self.gpu { gpu.frame(self.sim_t); } - } + WindowEvent::RedrawRequested => self.redraw(), _ => {} } } } + +impl App { + fn on_key(&mut self, event_loop: &ActiveEventLoop, key: KeyEvent) { + let PhysicalKey::Code(code) = key.physical_key else { return; }; + let down = key.state == ElementState::Pressed; + + // Escape: erst den Cursor freigeben, sonst beenden. + if code == KeyCode::Escape && down { + if self.input.grabbed { self.set_grab(false); } else { event_loop.exit(); } + return; + } + + match code { + KeyCode::KeyW => self.input.fwd = down, + KeyCode::KeyS => self.input.back = down, + KeyCode::KeyA => self.input.left = down, + KeyCode::KeyD => self.input.right = down, + KeyCode::Space => self.input.up = down, + KeyCode::ShiftLeft => self.input.down = down, + _ => {} + } + } + + fn redraw(&mut self) { + // dt messen; im Dialog Kamera und Sim anhalten → Welt pausiert, + // während die Konsole den Dialog treibt. + let now = Instant::now(); + let dt = self.last.replace(now).map_or(0.0, |prev| (now - prev).as_secs_f32()); + + if matches!(self.session.mode, Mode::Free) { + self.camera.apply_mouse(self.input.mouse_dx, self.input.mouse_dy, MOUSE_SENS); + let fwd = axis(self.input.fwd, self.input.back); + let right = axis(self.input.right, self.input.left); + let up = axis(self.input.up, self.input.down); + self.camera.translate(fwd, right, up, MOVE_SPEED * dt); + } else if self.input.grabbed { + // Nicht-Free (Dialog/Menü) → Maus freigeben, damit das UI klickbar + // ist und der Maus-Cursor statt des HUD-Pointers erscheint. + self.set_grab(false); + } + // Delta immer verwerfen, auch im Dialog — sonst springt der Blick + // beim Fortsetzen um die aufgestaute Bewegung. + self.input.mouse_dx = 0.0; + self.input.mouse_dy = 0.0; + + let view = self.camera.view(); + // Mausposition über die Letterbox in interne Pixel mappen. + let cursor = ui::Cursor { + pos: self.gpu.as_ref() + .map(|g| g.map_cursor(self.cursor_win)) + .unwrap_or([0.0, 0.0]), + grabbed: self.input.grabbed, + }; + // Overlay state-driven bauen (Stage 3: Demo-Panel + Cursor). + let overlay = ui::layout( + [gpu::INTERNAL_W as f32, gpu::INTERNAL_H as f32], &self.fonts, &cursor, + ); + if let Some(gpu) = &mut self.gpu { gpu.frame(&view, &overlay); } + } +} + +/// Zwei gegensätzliche Tasten zu einer Achse in {-1, 0, 1}. +fn axis(positive: bool, negative: bool) -> f32 { + (positive as i32 - negative as i32) as f32 +} diff --git a/src/render/scene.rs b/src/render/scene.rs index 4c74b07..0d2f600 100644 --- a/src/render/scene.rs +++ b/src/render/scene.rs @@ -1,40 +1,74 @@ //! Szenen-Pass: zeichnet die 3D-Welt ins interne Target. //! -//! Stand Schritt 3: ein hartkodierter Testwürfel mit Vertex-Colors — -//! Meshes aus OBJ und Texturen kommen in Schritt 5. Die Shader -//! (scene.wgsl) sind dagegen schon die echten PS1-Shader. +//! Stand Schritt 5: Geometrie + Texturen kommen vom Aufrufer (Brush-Welt +//! aus der `.map`, siehe render::brush; Bilder aus engine::tga). Pro Textur +//! eine Bind-Group + ein Draw-Batch (Material-Batching „pro Textur ein +//! Draw" wie im Plan). Die Shader (scene.wgsl) sind die echten PS1-Shader. use wgpu::util::DeviceExt; +use crate::engine::tga::Image; use crate::render::math::Mat4; #[repr(C)] #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)] -struct Vertex { - pos: [f32; 3], - color: [f32; 3], +pub(crate) struct Vertex { + pub(crate) pos: [f32; 3], + pub(crate) uv: [f32; 2], +} + +/// Ein Draw-Batch: ein zusammenhängender Index-Bereich, der mit *einer* +/// Textur gezeichnet wird. +#[derive(Clone, Copy)] +pub(crate) struct Batch { + pub(crate) texture: usize, + pub(crate) start: u32, + pub(crate) count: u32, +} + +/// CPU-seitige Welt-Geometrie, fertig zum Hochladen. Die Indizes sind nach +/// Textur gruppiert; `batches` zeigt in diese Reihenfolge. +pub(crate) struct Mesh { + pub(crate) verts: Vec, + pub(crate) indices: Vec, + pub(crate) batches: Vec, +} + +/// Spiegelt das `Uniforms`-Struct in scene.wgsl. `_pad` rundet die Größe +/// auf 80 Byte (16er-Vielfaches), wie es die Uniform-Adressraum-Regeln +/// von WGSL verlangen. +#[repr(C)] +#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)] +struct Uniforms { + mvp: [[f32; 4]; 4], + half_res: [f32; 2], + _pad: [f32; 2], } const VERTEX_LAYOUT: wgpu::VertexBufferLayout<'static> = wgpu::VertexBufferLayout { array_stride: size_of::() as u64, step_mode: wgpu::VertexStepMode::Vertex, - attributes: &wgpu::vertex_attr_array![0 => Float32x3, 1 => Float32x3], + attributes: &wgpu::vertex_attr_array![0 => Float32x3, 1 => Float32x2], }; pub struct ScenePass { - pipeline: wgpu::RenderPipeline, - vbuf: wgpu::Buffer, - ibuf: wgpu::Buffer, - ubuf: wgpu::Buffer, - bind: wgpu::BindGroup, - index_count: u32, + pipeline: wgpu::RenderPipeline, + vbuf: wgpu::Buffer, + ibuf: wgpu::Buffer, + ubuf: wgpu::Buffer, + uniform_bind: wgpu::BindGroup, // group 0 + tex_binds: Vec, // group 1, pro Textur + batches: Vec, } impl ScenePass { pub fn new( device: &wgpu::Device, + queue: &wgpu::Queue, color_format: wgpu::TextureFormat, depth_format: wgpu::TextureFormat, + mesh: &Mesh, + images: &[Image], ) -> Self { let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { label: Some("scene"), @@ -55,10 +89,16 @@ impl ScenePass { compilation_options: Default::default(), targets: &[Some(color_format.into())], }), - // Cull aus: der Z-Buffer sortiert auch so korrekt, und die - // irl3d-Materialien sind teils two-sided. Entscheidung pro - // Material fällt mit dem Szenen-Loader (Schritt 5). - primitive: wgpu::PrimitiveState::default(), + // Back-face Culling: Brush-Polygone werden CCW um ihre nach + // außen zeigende Normale gewickelt (siehe render::brush), und + // die Z-up→Y-up-Drehung erhält die Orientierung (det +1). Also + // sind die Außenflächen front-facing — Rückseiten und alle + // verdeckten Innenflächen zwischen Brushes fallen weg. + // (Front-Face bleibt der wgpu-Default CCW.) + primitive: wgpu::PrimitiveState { + cull_mode: Some(wgpu::Face::Back), + ..Default::default() + }, depth_stencil: Some(wgpu::DepthStencilState { format: depth_format, depth_write_enabled: Some(true), @@ -71,25 +111,24 @@ impl ScenePass { cache: None, }); - let (verts, indices) = cube(); let vbuf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("cube vertices"), - contents: bytemuck::cast_slice(&verts), + label: Some("scene vertices"), + contents: bytemuck::cast_slice(&mesh.verts), usage: wgpu::BufferUsages::VERTEX, }); let ibuf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("cube indices"), - contents: bytemuck::cast_slice(&indices), + label: Some("scene indices"), + contents: bytemuck::cast_slice(&mesh.indices), usage: wgpu::BufferUsages::INDEX, }); let ubuf = device.create_buffer(&wgpu::BufferDescriptor { label: Some("scene uniforms"), - size: size_of::() as u64, + size: size_of::() as u64, usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, mapped_at_creation: false, }); - let bind = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("scene"), + let uniform_bind = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("scene uniforms"), layout: &pipeline.get_bind_group_layout(0), entries: &[wgpu::BindGroupEntry { binding: 0, @@ -97,46 +136,92 @@ impl ScenePass { }], }); - Self { pipeline, vbuf, ibuf, ubuf, bind, index_count: indices.len() as u32 } + // Nearest-Sampler: harte Texel, kein Filtering — PS1. + let sampler = device.create_sampler(&wgpu::SamplerDescriptor { + label: Some("scene nearest"), + address_mode_u: wgpu::AddressMode::Repeat, + address_mode_v: wgpu::AddressMode::Repeat, + mag_filter: wgpu::FilterMode::Nearest, + min_filter: wgpu::FilterMode::Nearest, + ..Default::default() + }); + let tex_layout = pipeline.get_bind_group_layout(1); + let tex_binds = images.iter() + .map(|img| upload_texture(device, queue, &tex_layout, &sampler, img)) + .collect(); + + Self { + pipeline, vbuf, ibuf, ubuf, uniform_bind, tex_binds, + batches: mesh.batches.clone(), + } } /// Uniforms für diesen Frame hochladen — vor dem Render-Pass rufen. - pub fn prepare(&self, queue: &wgpu::Queue, mvp: &Mat4) { - queue.write_buffer(&self.ubuf, 0, bytemuck::bytes_of(mvp)); + /// `half_res` = halbe interne Auflösung (fürs Pixel-Snap im Shader). + pub fn prepare(&self, queue: &wgpu::Queue, mvp: &Mat4, half_res: [f32; 2]) { + let u = Uniforms { mvp: mvp.0, half_res, _pad: [0.0; 2] }; + queue.write_buffer(&self.ubuf, 0, bytemuck::bytes_of(&u)); } pub fn draw(&self, pass: &mut wgpu::RenderPass) { pass.set_pipeline(&self.pipeline); - pass.set_bind_group(0, &self.bind, &[]); + pass.set_bind_group(0, &self.uniform_bind, &[]); pass.set_vertex_buffer(0, self.vbuf.slice(..)); - pass.set_index_buffer(self.ibuf.slice(..), wgpu::IndexFormat::Uint16); - pass.draw_indexed(0..self.index_count, 0, 0..1); - } -} - -/// Einheitswürfel um den Ursprung, jede Seite eine Farbe. Der -/// Helligkeitsverlauf über die Ecken erzeugt Gradienten, an denen -/// Dither und affine Interpolation sichtbar werden. -fn cube() -> (Vec, Vec) { - const S: f32 = 0.5; - let faces: [([f32; 3], [[f32; 3]; 4]); 6] = [ - ([0.9, 0.2, 0.2], [[ S, -S, -S], [ S, S, -S], [ S, S, S], [ S, -S, S]]), // +X - ([0.2, 0.9, 0.9], [[-S, -S, -S], [-S, S, -S], [-S, S, S], [-S, -S, S]]), // -X - ([0.2, 0.9, 0.2], [[-S, S, -S], [ S, S, -S], [ S, S, S], [-S, S, S]]), // +Y - ([0.9, 0.2, 0.9], [[-S, -S, -S], [ S, -S, -S], [ S, -S, S], [-S, -S, S]]), // -Y - ([0.3, 0.3, 0.9], [[-S, -S, S], [ S, -S, S], [ S, S, S], [-S, S, S]]), // +Z - ([0.9, 0.8, 0.2], [[-S, -S, -S], [ S, -S, -S], [ S, S, -S], [-S, S, -S]]), // -Z - ]; - const SHADE: [f32; 4] = [1.0, 0.65, 0.4, 0.65]; - - let mut verts = Vec::with_capacity(24); - let mut idx: Vec = Vec::with_capacity(36); - for (base, corners) in faces { - let b = verts.len() as u16; - for (i, pos) in corners.into_iter().enumerate() { - verts.push(Vertex { pos, color: base.map(|c| c * SHADE[i]) }); + pass.set_index_buffer(self.ibuf.slice(..), wgpu::IndexFormat::Uint32); + for b in &self.batches { + pass.set_bind_group(1, &self.tex_binds[b.texture], &[]); + pass.draw_indexed(b.start..b.start + b.count, 0, 0..1); } - idx.extend([b, b + 1, b + 2, b, b + 2, b + 3]); } - (verts, idx) +} + +/// Ein RGBA8-`Image` als GPU-Textur hochladen und die zugehörige +/// Bind-Group (Textur + Sampler, group 1) bauen. +fn upload_texture( + device: &wgpu::Device, + queue: &wgpu::Queue, + layout: &wgpu::BindGroupLayout, + sampler: &wgpu::Sampler, + img: &Image, +) -> wgpu::BindGroup { + let size = wgpu::Extent3d { + width: img.width, + height: img.height, + depth_or_array_layers: 1, + }; + let texture = device.create_texture(&wgpu::TextureDescriptor { + label: Some("scene texture"), + size, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + // Nicht-sRGB: die Quantisierung im Shader erwartet rohe Werte. + format: wgpu::TextureFormat::Rgba8Unorm, + usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, + view_formats: &[], + }); + queue.write_texture( + wgpu::TexelCopyTextureInfo { + texture: &texture, + mip_level: 0, + origin: wgpu::Origin3d::ZERO, + aspect: wgpu::TextureAspect::All, + }, + &img.rgba, + wgpu::TexelCopyBufferLayout { + offset: 0, + bytes_per_row: Some(img.width * 4), + rows_per_image: Some(img.height), + }, + size, + ); + let view = texture.create_view(&Default::default()); + device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("scene texture"), + layout, + entries: &[ + wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&view) }, + wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(sampler) }, + ], + }) } diff --git a/src/render/scene.wgsl b/src/render/scene.wgsl index f9c1696..c9cfcd1 100644 --- a/src/render/scene.wgsl +++ b/src/render/scene.wgsl @@ -1,38 +1,40 @@ // Szenen-Pipeline: die PS1-Eigenheiten aus dem Renderer-Plan, authentisch // gerechnet statt nachgestellt: // - Pixel-Snap im Vertex-Shader → Vertex-Jitter -// - @interpolate(linear) → affine (nicht perspektivkorrigierte) Varyings +// - @interpolate(linear) → affine (nicht perspektivkorrigierte) UVs // - RGB555-Quantisierung + 4×4-Bayer-Dither im Fragment-Shader +// - 1-Bit-Alpha-Test (Cutouts), kein Blending struct Uniforms { mvp: mat4x4f, + // Halbe interne Auflösung (NDC läuft -1..1, Spanne 2 → Skalierung mit + // der halben Auflösung trifft das Pixelraster). Kommt aus dem Renderer. + half_res: vec2f, }; @group(0) @binding(0) var u: Uniforms; +@group(1) @binding(0) var tex: texture_2d; +@group(1) @binding(1) var smp: sampler; + struct VsOut { @builtin(position) pos: vec4f, - // linear = affin interpoliert; Vertex-Colors (und später UVs) - // wobbeln dadurch wie auf der PS1. - @location(0) @interpolate(linear) color: vec3f, + // linear = affin interpoliert → das Textur-Wobbeln der PS1. + @location(0) @interpolate(linear) uv: vec2f, }; -// Halbe interne Auflösung (320×240). Bei umschaltbarer Auflösung später -// in die Uniforms verschieben. -const HALF_RES = vec2f(160.0, 120.0); - @vertex -fn vs_main(@location(0) pos: vec3f, @location(1) color: vec3f) -> VsOut { +fn vs_main(@location(0) pos: vec3f, @location(1) uv: vec2f) -> VsOut { var clip = u.mvp * vec4f(pos, 1.0); // Pixel-Snap: xy nach der Projektion aufs interne Raster runden und // zurück in den Clip-Raum. Nur vor der Kamera (w>0) — dahinter würde // die Division Unsinn liefern, das Hardware-Clipping übernimmt. if clip.w > 0.0 { let ndc = clip.xy / clip.w; - clip = vec4f(round(ndc * HALF_RES) / HALF_RES * clip.w, clip.zw); + clip = vec4f(round(ndc * u.half_res) / u.half_res * clip.w, clip.zw); } var out: VsOut; out.pos = clip; - out.color = color; + out.uv = uv; return out; } @@ -49,9 +51,12 @@ fn bayer4(px: vec2u) -> f32 { @fragment fn fs_main(in: VsOut) -> @location(0) vec4f { + let texel = textureSample(tex, smp, in.uv); + if texel.a < 0.5 { discard; } // 1-Bit-Alpha (Cutouts), noch ungenutzt + // RGB555: 31 Stufen pro Kanal. Bayer-Schwelle vor dem Abrunden → // Verläufe zerfallen ins typische Dither-Muster. let t = (bayer4(vec2u(in.pos.xy)) + 0.5) / 16.0; - let c = clamp(in.color, vec3f(0.0), vec3f(1.0)); + let c = clamp(texel.rgb, vec3f(0.0), vec3f(1.0)); return vec4f(floor(c * 31.0 + t) / 31.0, 1.0); } diff --git a/src/render/sprite.rs b/src/render/sprite.rs new file mode 100644 index 0000000..86264cd --- /dev/null +++ b/src/render/sprite.rs @@ -0,0 +1,207 @@ +//! Sprite-Pass: das 2D-Overlay (UI) als getintete Textur-Quads. +//! +//! Immediate-Mode: der Vertex-Buffer wird jeden Frame neu befüllt (kleine +//! Datenmenge). Geometrie kommt von [`crate::render::ui`] als fertige +//! [`SpriteVertex`]-Liste + [`SpriteBatch`]es (ein Batch je Textur). Die +//! Texturen (weißer Pixel, Font-Atlas, …) werden einmal beim Bau +//! hochgeladen — wie beim Szenen-Pass: Decode (CPU) ≠ Upload (GPU). + +use crate::engine::tga::Image; + +#[repr(C)] +#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)] +pub(crate) struct SpriteVertex { + /// Interne Pixel, Ursprung oben-links. + pub(crate) pos: [f32; 2], + pub(crate) uv: [f32; 2], + /// Tint, mit dem Texel multipliziert (RGBA, 0..1). + pub(crate) color: [f32; 4], +} + +/// Ein zusammenhängender Vertex-Bereich, der mit *einer* Textur gezeichnet +/// wird (zwei Dreiecke je Quad, also Vielfache von 6). +#[derive(Clone, Copy)] +pub(crate) struct SpriteBatch { + pub(crate) texture: usize, + pub(crate) start: u32, + pub(crate) count: u32, +} + +#[repr(C)] +#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)] +struct Uniforms { + inv_res: [f32; 2], + _pad: [f32; 2], +} + +/// Obergrenze des dynamischen Vertex-Buffers. 8192 Verts = ~1365 Quads pro +/// Frame — reichlich für Dialog-Panels, Text und Cursor. +const MAX_VERTS: u64 = 8192; + +const VERTEX_LAYOUT: wgpu::VertexBufferLayout<'static> = wgpu::VertexBufferLayout { + array_stride: size_of::() as u64, + step_mode: wgpu::VertexStepMode::Vertex, + attributes: &wgpu::vertex_attr_array![0 => Float32x2, 1 => Float32x2, 2 => Float32x4], +}; + +pub struct SpritePass { + pipeline: wgpu::RenderPipeline, + vbuf: wgpu::Buffer, + uniform_bind: wgpu::BindGroup, // group 0 + tex_binds: Vec, // group 1, pro Textur + /// Im aktuellen Frame hochgeladene Vertex-Anzahl (für `draw`-Schutz). + loaded: u32, +} + +impl SpritePass { + pub fn new( + device: &wgpu::Device, + queue: &wgpu::Queue, + color_format: wgpu::TextureFormat, + depth_format: wgpu::TextureFormat, + internal: [f32; 2], + textures: &[Image], + ) -> Self { + let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("sprite"), + source: wgpu::ShaderSource::Wgsl(include_str!("sprite.wgsl").into()), + }); + let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("sprite"), + layout: None, + vertex: wgpu::VertexState { + module: &shader, + entry_point: Some("vs_main"), + compilation_options: Default::default(), + buffers: &[VERTEX_LAYOUT], + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: Some("fs_main"), + compilation_options: Default::default(), + targets: &[Some(wgpu::ColorTargetState { + format: color_format, + // Standard-Alpha-Blending fürs Overlay. + blend: Some(wgpu::BlendState::ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })], + }), + primitive: wgpu::PrimitiveState::default(), + // Der interne Pass hat ein Tiefen-Attachment → die Pipeline muss + // ein kompatibles Format deklarieren. UI testet/schreibt aber + // keine Tiefe (Always, kein Write) → liegt immer obenauf. + depth_stencil: Some(wgpu::DepthStencilState { + format: depth_format, + depth_write_enabled: Some(false), + depth_compare: Some(wgpu::CompareFunction::Always), + stencil: wgpu::StencilState::default(), + bias: wgpu::DepthBiasState::default(), + }), + multisample: wgpu::MultisampleState::default(), + multiview_mask: None, + cache: None, + }); + + let vbuf = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("sprite vertices"), + size: MAX_VERTS * size_of::() as u64, + usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + let ubuf = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("sprite uniforms"), + size: size_of::() as u64, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + let u = Uniforms { inv_res: [1.0 / internal[0], 1.0 / internal[1]], _pad: [0.0; 2] }; + queue.write_buffer(&ubuf, 0, bytemuck::bytes_of(&u)); + let uniform_bind = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("sprite uniforms"), + layout: &pipeline.get_bind_group_layout(0), + entries: &[wgpu::BindGroupEntry { binding: 0, resource: ubuf.as_entire_binding() }], + }); + + // Nearest + Clamp: harte Pixel, kein Wrap an den Atlas-Rändern. + let sampler = device.create_sampler(&wgpu::SamplerDescriptor { + label: Some("sprite nearest"), + address_mode_u: wgpu::AddressMode::ClampToEdge, + address_mode_v: wgpu::AddressMode::ClampToEdge, + mag_filter: wgpu::FilterMode::Nearest, + min_filter: wgpu::FilterMode::Nearest, + ..Default::default() + }); + let tex_layout = pipeline.get_bind_group_layout(1); + let tex_binds = textures.iter() + .map(|img| upload_texture(device, queue, &tex_layout, &sampler, img)) + .collect(); + + Self { pipeline, vbuf, uniform_bind, tex_binds, loaded: 0 } + } + + /// Vertex-Daten dieses Frames hochladen — vor dem Render-Pass rufen. + pub fn prepare(&mut self, queue: &wgpu::Queue, verts: &[SpriteVertex]) { + let n = (verts.len() as u64).min(MAX_VERTS) as usize; + queue.write_buffer(&self.vbuf, 0, bytemuck::cast_slice(&verts[..n])); + self.loaded = n as u32; + } + + pub fn draw(&self, pass: &mut wgpu::RenderPass, batches: &[SpriteBatch]) { + if self.loaded == 0 { return; } + pass.set_pipeline(&self.pipeline); + pass.set_bind_group(0, &self.uniform_bind, &[]); + pass.set_vertex_buffer(0, self.vbuf.slice(..)); + for b in batches { + if b.start + b.count > self.loaded { continue; } + pass.set_bind_group(1, &self.tex_binds[b.texture], &[]); + pass.draw(b.start..b.start + b.count, 0..1); + } + } +} + +/// Ein RGBA8-`Image` als GPU-Textur hochladen und die Bind-Group (Textur + +/// Sampler, group 1) bauen. +fn upload_texture( + device: &wgpu::Device, + queue: &wgpu::Queue, + layout: &wgpu::BindGroupLayout, + sampler: &wgpu::Sampler, + img: &Image, +) -> wgpu::BindGroup { + let size = wgpu::Extent3d { width: img.width, height: img.height, depth_or_array_layers: 1 }; + let texture = device.create_texture(&wgpu::TextureDescriptor { + label: Some("sprite texture"), + size, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Rgba8Unorm, + usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, + view_formats: &[], + }); + queue.write_texture( + wgpu::TexelCopyTextureInfo { + texture: &texture, + mip_level: 0, + origin: wgpu::Origin3d::ZERO, + aspect: wgpu::TextureAspect::All, + }, + &img.rgba, + wgpu::TexelCopyBufferLayout { + offset: 0, + bytes_per_row: Some(img.width * 4), + rows_per_image: Some(img.height), + }, + size, + ); + let view = texture.create_view(&Default::default()); + device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("sprite texture"), + layout, + entries: &[ + wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&view) }, + wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(sampler) }, + ], + }) +} diff --git a/src/render/sprite.wgsl b/src/render/sprite.wgsl new file mode 100644 index 0000000..cb181c6 --- /dev/null +++ b/src/render/sprite.wgsl @@ -0,0 +1,45 @@ +// 2D-Sprite-Pass für das UI-Overlay: texturierte Quads in internen Pixeln +// (Ursprung oben-links), getintet. Zeichnet ins interne Target *nach* der +// Szene, ohne Tiefentest → liegt immer obenauf. Echtes Alpha-Blending +// (anders als der 1-Bit-Alphatest der Welt), damit Panels und Glyph-Ränder +// weich überblenden. + +struct Uniforms { + inv_res: vec2f, // 1/Breite, 1/Höhe des internen Targets + _pad: vec2f, +}; +@group(0) @binding(0) var u: Uniforms; + +@group(1) @binding(0) var tex: texture_2d; +@group(1) @binding(1) var smp: sampler; + +struct VsOut { + @builtin(position) pos: vec4f, + @location(0) uv: vec2f, + @location(1) color: vec4f, +}; + +@vertex +fn vs_main( + @location(0) pos: vec2f, + @location(1) uv: vec2f, + @location(2) color: vec4f, +) -> VsOut { + var o: VsOut; + // Pixel (oben-links) → NDC (y nach oben). + let ndc = vec2f( + pos.x * u.inv_res.x * 2.0 - 1.0, + 1.0 - pos.y * u.inv_res.y * 2.0, + ); + o.pos = vec4f(ndc, 0.0, 1.0); + o.uv = uv; + o.color = color; + return o; +} + +@fragment +fn fs_main(in: VsOut) -> @location(0) vec4f { + // Textur × Tint. Für Solid-Fills zeigt die UV auf einen weißen Texel, + // dann ist die Farbe rein der Tint. + return textureSample(tex, smp, in.uv) * in.color; +} diff --git a/src/render/ui.rs b/src/render/ui.rs new file mode 100644 index 0000000..b4deffa --- /dev/null +++ b/src/render/ui.rs @@ -0,0 +1,254 @@ +//! UI-Overlay: **state-driven** und **immediate-mode**. +//! +//! [`layout`] ist eine reine Funktion des [`Session`]-Zustands — es hält +//! keinen eigenen mutierbaren Zustand (anders als irl3ds Widget-Bäume, die +//! neben dem Spielzustand desyncen konnten). Jeden Frame wird aus dem +//! aktuellen [`Mode`] neu gebaut: Dialog → Panel + Choice-Buttons, Menü → +//! Menü-Buttons, usw. Klicks erzeugen denselben Eingabe-String, den auch +//! CLI und stdin liefern, und laufen durch dasselbe `Session::exec` — ein +//! einziger Trichter in den State, kein UI-Backdoor. +//! +//! Koordinaten sind interne Pixel (das Overlay wird vor dem Blit gezeichnet, +//! teilt also den Lo-Fi-Look). Gezeichnet wird über [`crate::render::sprite`]. + +use crate::engine::tga::Image; +use crate::render::font::{self, Font, Fonts}; +use crate::render::sprite::{SpriteBatch, SpriteVertex}; + +/// Feste Textur-Indizes im Sprite-Pass — Reihenfolge = `ui_textures` in +/// render::run. `WHITE` ist ein 1×1-Weiß-Texel für Solid-Fills (Farbe kommt +/// rein aus dem Tint), `FONT_*` die CP437-Glyph-Atlanten, `CURSORS` das +/// 2×2-Raster der 16×16-Cursor, `ORN` der 9-Slice-Ornamentrahmen. +pub(crate) const WHITE: usize = 0; +pub(crate) const FONT_EGA: usize = 1; +pub(crate) const FONT_CGA: usize = 2; +pub(crate) const CURSORS: usize = 3; +pub(crate) const ORN: usize = 4; + +/// Cursor-Indizes im 2×2-Raster (Pivot mittig je 16×16-Zelle). +pub(crate) const CUR_MOUSE: usize = 0; // Maus, normal +pub(crate) const CUR_HUD: usize = 2; // First-Person-Pointer, normal +// Die „interagierbar"-Varianten (1, 3) kommen mit dem Hit-Testing in Stage 4. +#[allow(dead_code)] pub(crate) const CUR_MOUSE_INTERACT: usize = 1; +#[allow(dead_code)] pub(crate) const CUR_HUD_INTERACT: usize = 3; + +const CURSOR_PX: f32 = 16.0; +const ORN_CORNER: f32 = 8.0; // Eckgröße im Ornament-Atlas (und im Panel) + +/// 1×1 weißes RGBA8-`Image` für getintete Solid-Flächen. +pub(crate) fn white_pixel() -> Image { + Image { width: 1, height: 1, rgba: vec![255, 255, 255, 255] } +} + +/// Weiß-auf-schwarz-Sprite (Font, Cursor, Ornament) → getintbare Maske: +/// rgb = weiß, alpha = Luminanz. So blendet der Sprite-Pass den schwarzen +/// Hintergrund weg und der Tint setzt die Farbe. Reine CPU-Nachbearbeitung +/// beim Laden. +pub(crate) fn key_luminance(img: &Image) -> Image { + let mut rgba = img.rgba.clone(); + for px in rgba.chunks_mut(4) { + let lum = px[0].max(px[1]).max(px[2]); + px[0] = 255; px[1] = 255; px[2] = 255; px[3] = lum; + } + Image { width: img.width, height: img.height, rgba } +} + +/// Cursor, wie ihn die App pro Frame an [`layout`] gibt. +pub(crate) struct Cursor { + /// Position in internen Pixeln (über die Letterbox gemappt). + pub(crate) pos: [f32; 2], + /// Maus gefangen (Flycam aktiv) → First-Person-Pointer mittig statt + /// Maus-Cursor an `pos`. + pub(crate) grabbed: bool, +} + +/// Akkumulierte Overlay-Geometrie eines Frames, fertig für den Sprite-Pass. +#[derive(Default)] +pub(crate) struct Ui { + pub(crate) verts: Vec, + pub(crate) batches: Vec, +} + +impl Ui { + /// Ein getintetes Quad. `rect` = (x, y, w, h) in internen Pixeln + /// (oben-links), `uv` = (u0, v0, u1, v1) in 0..1. Aufeinanderfolgende + /// Quads derselben Textur landen im selben Batch. + pub(crate) fn quad(&mut self, tex: usize, rect: [f32; 4], uv: [f32; 4], color: [f32; 4]) { + let [x, y, w, h] = rect; + let [u0, v0, u1, v1] = uv; + let base = self.verts.len() as u32; + // Zwei Dreiecke (TL, BL, BR / TL, BR, TR). + let tl = SpriteVertex { pos: [x, y ], uv: [u0, v0], color }; + let bl = SpriteVertex { pos: [x, y + h], uv: [u0, v1], color }; + let br = SpriteVertex { pos: [x + w, y + h], uv: [u1, v1], color }; + let tr = SpriteVertex { pos: [x + w, y ], uv: [u1, v0], color }; + self.verts.extend([tl, bl, br, tl, br, tr]); + + // An letzten Batch anhängen, wenn gleiche Textur, sonst neuen öffnen. + match self.batches.last_mut() { + Some(b) if b.texture == tex => b.count += 6, + _ => self.batches.push(SpriteBatch { texture: tex, start: base, count: 6 }), + } + } + + /// Getintetes Solid-Rechteck (weißer Texel). + pub(crate) fn fill(&mut self, rect: [f32; 4], color: [f32; 4]) { + self.quad(WHITE, rect, [0.0, 0.0, 1.0, 1.0], color); + } + + /// Eine Textzeile ab (x, y) oben-links in `font`, proportional gesetzt + /// (variable Laufweite, leere Glyphen werden übersprungen). `\n` springt + /// eine Zeile weiter. Gibt die x-Endposition zurück. + pub(crate) fn text(&mut self, font: &Font, x: f32, y: f32, s: &str, color: [f32; 4]) -> f32 { + let (mut cx, mut cy) = (x, y); + for ch in s.chars() { + if ch == '\n' { cx = x; cy += font.glyph_h; continue; } + let code = font::cp437(ch); + if let Some((uv, w)) = font.glyph_quad(code) { + self.quad(font.tex(), [cx, cy, w, font.glyph_h], uv, color); + } + cx += font.advance(code); + } + cx + } + + /// Ein Cursor-Sprite (16×16, Pivot mittig) an `pos`, getintet. + pub(crate) fn cursor(&mut self, idx: usize, pos: [f32; 2], color: [f32; 4]) { + let rect = [pos[0] - CURSOR_PX * 0.5, pos[1] - CURSOR_PX * 0.5, CURSOR_PX, CURSOR_PX]; + self.quad(CURSORS, rect, cursor_uv(idx), color); + } + + /// Opakes Panel mit Ornamentrahmen. `rect` = Außenbounds. Erst der + /// Hintergrund (Solid, getintet), dann der 9-Slice-Rahmen darüber: feste + /// 8×8-Ecken, die Kanten dazwischen gekachelt bis zur nächsten Ecke. + pub(crate) fn panel(&mut self, rect: [f32; 4], bg: [f32; 4], frame: [f32; 4]) { + self.fill(rect, bg); + self.ornament_frame(rect, frame); + } + + fn ornament_frame(&mut self, rect: [f32; 4], color: [f32; 4]) { + let [x, y, w, h] = rect; + let c = ORN_CORNER; + let a = ORN_ATLAS; + // Vier Ecken (1:1 aus dem Atlas). + self.quad(ORN, [x, y, c, c], orn_uv(0.0, 0.0, c, c), color); + self.quad(ORN, [x + w - c, y, c, c], orn_uv(a - c, 0.0, c, c), color); + self.quad(ORN, [x, y + h - c, c, c], orn_uv(0.0, a - c, c, c), color); + self.quad(ORN, [x + w - c, y + h - c, c, c], orn_uv(a - c, a - c, c, c), color); + // Kanten zwischen den Ecken kacheln. + self.tile_h(x + c, x + w - c, y, c, [c, 0.0], color); // oben + self.tile_h(x + c, x + w - c, y + h - c, c, [c, a - c], color); // unten + self.tile_v(y + c, y + h - c, x, c, [0.0, c], color); // links + self.tile_v(y + c, y + h - c, x + w - c, c, [a - c, c], color); // rechts + } + + /// Horizontale Kante von `x0` bis `x1` bei `y` (Höhe `c`) mit dem + /// `c`-breiten Atlas-Sample ab `src` (Pixel) kacheln; letzte Kachel wird + /// passend beschnitten. + fn tile_h(&mut self, x0: f32, x1: f32, y: f32, c: f32, src: [f32; 2], color: [f32; 4]) { + let mut cx = x0; + while cx < x1 - 0.5 { + let seg = (x1 - cx).min(ORN_EDGE); + self.quad(ORN, [cx, y, seg, c], orn_uv(src[0], src[1], seg, c), color); + cx += seg; + } + } + + fn tile_v(&mut self, y0: f32, y1: f32, x: f32, c: f32, src: [f32; 2], color: [f32; 4]) { + let mut cy = y0; + while cy < y1 - 0.5 { + let seg = (y1 - cy).min(ORN_EDGE); + self.quad(ORN, [x, cy, c, seg], orn_uv(src[0], src[1], c, seg), color); + cy += seg; + } + } +} + +const ORN_ATLAS: f32 = 32.0; // Ornament-Atlasgröße (32×32) +const ORN_EDGE: f32 = 16.0; // Kanten-Sample-Länge (zwischen den 8px-Ecken) + +/// Atlas-Pixel-Rechteck → UV (0..1) im 32×32-Ornamentatlas. +fn orn_uv(x: f32, y: f32, w: f32, h: f32) -> [f32; 4] { + [x / ORN_ATLAS, y / ORN_ATLAS, (x + w) / ORN_ATLAS, (y + h) / ORN_ATLAS] +} + +/// UV-Rechteck einer Cursor-Zelle im 2×2-Raster (16×16 je Zelle, Atlas 32×32). +fn cursor_uv(idx: usize) -> [f32; 4] { + let col = (idx % 2) as f32; + let row = (idx / 2) as f32; + let (x0, y0) = (col * CURSOR_PX, row * CURSOR_PX); + [x0 / 32.0, y0 / 32.0, (x0 + CURSOR_PX) / 32.0, (y0 + CURSOR_PX) / 32.0] +} + +/// Das Overlay für den aktuellen Frame bauen. +// +// Stage 3: Demo-Panel mit Ornamentrahmen + Text, plus der Cursor. Ab Stage 4 +// verzweigt das hier nach `session.mode` (Dialog-Panel, Menü, HUD). +pub(crate) fn layout(internal: [f32; 2], fonts: &Fonts, cur: &Cursor) -> Ui { + let mut ui = Ui::default(); + + // Demo-Panel (opak) mit Ornamentrahmen + beide Fonts zum Vergleich. + ui.panel([8.0, 8.0, 216.0, 58.0], [0.06, 0.05, 0.10, 1.0], [0.85, 0.75, 0.95, 1.0]); + ui.text(&fonts.ega, 16.0, 16.0, "EGA 8x14 — Schöne Grüße!", [0.9, 0.9, 1.0, 1.0]); + ui.text(&fonts.cga, 16.0, 34.0, "CGA 8x8 — abcABC 0123 äöüß", [0.6, 1.0, 0.7, 1.0]); + + // Cursor zuletzt → liegt über allem. Gefangen (Flycam) → HUD-Pointer + // mittig; sonst Maus-Cursor an der Mausposition. + if cur.grabbed { + ui.cursor(CUR_HUD, [internal[0] * 0.5, internal[1] * 0.5], [1.0; 4]); + } else { + ui.cursor(CUR_MOUSE, cur.pos, [1.0; 4]); + } + + ui +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn same_texture_quads_merge_into_one_batch() { + let mut ui = Ui::default(); + ui.fill([0.0, 0.0, 10.0, 10.0], [1.0; 4]); + ui.fill([20.0, 0.0, 10.0, 10.0], [1.0; 4]); + assert_eq!(ui.verts.len(), 12); // 2 Quads · 6 Verts + assert_eq!(ui.batches.len(), 1); // gleiche Textur → ein Batch + assert_eq!(ui.batches[0].count, 12); + } + + #[test] + fn different_textures_split_batches() { + let mut ui = Ui::default(); + ui.fill([0.0, 0.0, 10.0, 10.0], [1.0; 4]); // WHITE + ui.quad(1, [0.0, 0.0, 10.0, 10.0], [0.0, 0.0, 1.0, 1.0], [1.0; 4]); // Textur 1 + ui.fill([0.0, 0.0, 10.0, 10.0], [1.0; 4]); // WHITE wieder + assert_eq!(ui.batches.len(), 3); + assert_eq!((ui.batches[0].start, ui.batches[1].start, ui.batches[2].start), (0, 6, 12)); + } + + #[test] + fn key_luminance_white_opaque_black_transparent() { + let img = Image { width: 2, height: 1, rgba: vec![255, 255, 255, 255, 0, 0, 0, 255] }; + let k = key_luminance(&img); + assert_eq!(&k.rgba[0..4], &[255, 255, 255, 255]); // weiß → opak + assert_eq!(&k.rgba[4..8], &[255, 255, 255, 0]); // schwarz → transparent + } + + #[test] + fn cursor_uv_picks_quadrant() { + // idx 3 = (col 1, row 1) → rechte untere 16×16-Zelle. + assert_eq!(cursor_uv(3), [0.5, 0.5, 1.0, 1.0]); + assert_eq!(cursor_uv(0), [0.0, 0.0, 0.5, 0.5]); + } + + #[test] + fn ornament_frame_tiles_edges_and_places_corners() { + let mut ui = Ui::default(); + // 48×48-Panel: Kanten je 48-16=32 px → 32/16 = 2 Kacheln pro Seite. + // Erwartung: 4 Ecken + 4·2 Kanten = 12 Ornament-Quads (nach dem fill). + ui.panel([0.0, 0.0, 48.0, 48.0], [0.0; 4], [1.0; 4]); + let orn_quads = ui.verts.len() / 6 - 1; // minus das Hintergrund-fill + assert_eq!(orn_quads, 12); + } +}