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 0000000..7bf61f7 Binary files /dev/null and b/assets/textures/__TB_empty.tga differ diff --git a/assets/textures/fonts/oldschool-cga-8x8.tga b/assets/textures/fonts/oldschool-cga-8x8.tga new file mode 100644 index 0000000..d342b01 Binary files /dev/null and b/assets/textures/fonts/oldschool-cga-8x8.tga differ diff --git a/assets/textures/fonts/oldschool-ega-8x14.tga b/assets/textures/fonts/oldschool-ega-8x14.tga new file mode 100644 index 0000000..b66fd00 Binary files /dev/null and b/assets/textures/fonts/oldschool-ega-8x14.tga differ diff --git a/assets/textures/placeholder.tga b/assets/textures/placeholder.tga new file mode 100644 index 0000000..a9e2a1a Binary files /dev/null and b/assets/textures/placeholder.tga differ diff --git a/assets/textures/ui/cursors.tga b/assets/textures/ui/cursors.tga new file mode 100644 index 0000000..ceeb5dc Binary files /dev/null and b/assets/textures/ui/cursors.tga differ diff --git a/assets/textures/ui/panel_ornaments.tga b/assets/textures/ui/panel_ornaments.tga new file mode 100644 index 0000000..a8fa494 Binary files /dev/null and b/assets/textures/ui/panel_ornaments.tga differ diff --git a/src/engine/map.rs b/src/engine/map.rs new file mode 100644 index 0000000..e71beee --- /dev/null +++ b/src/engine/map.rs @@ -0,0 +1,179 @@ +//! Quake-`.map`-Parser (Standard-Format, wie TrenchBroom es mit dem +//! Generic-Game exportiert). +//! +//! Struktur: eine Datei ist eine Liste von Entities `{...}`. Eine Entity +//! hält Key/Value-Properties (`"key" "value"`) und null oder mehr Brushes +//! `{...}`. Ein Brush ist eine Liste von Faces; jede Face ist eine Ebene +//! aus drei Punkten plus Textur-Ausrichtung: +//! +//! ( x y z ) ( x y z ) ( x y z ) TEXTUR offX offY rot scaleX scaleY +//! +//! Hier wird nur geparst — Geometrie bleibt im Quake-Koordinatensystem +//! (Z-up, Brush = Schnitt der Halbräume); die Umrechnung ins Engine-System +//! und der Halbraum-Schnitt sind Sache des Konsumenten (render::brush). + +use std::collections::HashMap; +use std::fs::read_to_string; + +pub struct Map { + pub entities: Vec, +} + +#[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); + } +}