Compare commits
4 Commits
802f2bd34b
...
6bb9b009c4
| Author | SHA1 | Date | |
|---|---|---|---|
| 6bb9b009c4 | |||
| 48c499e356 | |||
| 0a8861a5b1 | |||
| 2dd338c890 |
Generated
+2218
-6
File diff suppressed because it is too large
Load Diff
@@ -5,3 +5,7 @@ edition = "2024"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bladeink = "1.2.5"
|
bladeink = "1.2.5"
|
||||||
|
bytemuck = { version = "1.25.0", features = ["derive"] }
|
||||||
|
pollster = "0.4.0"
|
||||||
|
wgpu = "29.0.3"
|
||||||
|
winit = "0.30.13"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 64 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 112 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 256 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 4.0 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 4.0 KiB |
+44
@@ -0,0 +1,44 @@
|
|||||||
|
//! Konsolen-REPL — Headless-Treiber für Tests und Betrieb ohne GPU.
|
||||||
|
//!
|
||||||
|
//! Liest Zeilen von stdin, gibt sie an `Session::exec` und druckt die
|
||||||
|
//! Ausgabe. Befehls- und Dialoglogik liegen in `session`, damit das
|
||||||
|
//! Fenster-Frontend dieselbe Session ohne Duplikat steuern kann.
|
||||||
|
|
||||||
|
use std::io::{self, Write};
|
||||||
|
|
||||||
|
use crate::session::{Mode, Session};
|
||||||
|
|
||||||
|
pub fn run(mut session: Session, signals_path: &str) {
|
||||||
|
println!("wds-Konsole — {} Signale aus {}. `help` für Befehle.",
|
||||||
|
session.game.signals.len(), signals_path);
|
||||||
|
|
||||||
|
// Reserviertes `[init]`-Signal: KV-Defaults setzen, bevor Stories laufen.
|
||||||
|
for line in session.start() { println!("{line}"); }
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let Some(input) = read_line(prompt_for(&session.mode)) else { break };
|
||||||
|
let r = session.exec(input.trim());
|
||||||
|
for line in r.output { println!("{line}"); }
|
||||||
|
if r.quit { break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prompt je nach Zustand: Befehl, Dialog-Weiterblättern oder Choice-Wahl.
|
||||||
|
fn prompt_for(mode: &Mode) -> &'static str {
|
||||||
|
match mode {
|
||||||
|
Mode::Dialog(d) if !d.choices.is_empty() => " wahl> ",
|
||||||
|
Mode::Dialog(_) => " [Enter] ",
|
||||||
|
Mode::Free => "> ",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Zeile von stdin lesen; `None` bei EOF (Ctrl-D) oder Lesefehler.
|
||||||
|
fn read_line(prompt: &str) -> Option<String> {
|
||||||
|
print!("{prompt}");
|
||||||
|
io::stdout().flush().ok();
|
||||||
|
let mut buf = String::new();
|
||||||
|
match io::stdin().read_line(&mut buf) {
|
||||||
|
Ok(0) | Err(_) => None,
|
||||||
|
Ok(_) => Some(buf.trim_end().to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,9 +11,15 @@
|
|||||||
//! sie, die Engine führt sie später aus. So bleibt der Kern headless
|
//! sie, die Engine führt sie später aus. So bleibt der Kern headless
|
||||||
//! testbar und kennt keines der Subsysteme.
|
//! testbar und kennt keines der Subsysteme.
|
||||||
|
|
||||||
use crate::ink;
|
use std::collections::HashMap;
|
||||||
use crate::kv::Store;
|
|
||||||
use crate::signals::Signals;
|
use crate::engine::ink;
|
||||||
|
use crate::engine::kv::Store;
|
||||||
|
|
||||||
|
/// Signal-Table aus `assets/signals.toml`: Signal-Name → Action-Strings.
|
||||||
|
/// Geladen/geparst von [`crate::engine::signals`]; definiert ist der Typ
|
||||||
|
/// hier beim Zustand, damit `game` ← `signals` eine Einbahnstraße bleibt.
|
||||||
|
pub type Signals = HashMap<String, Vec<String>>;
|
||||||
|
|
||||||
/// Action, die der Kern nicht selbst ausführt, sondern ans Frontend
|
/// Action, die der Kern nicht selbst ausführt, sondern ans Frontend
|
||||||
/// weiterreicht. Die Queue in `Game::actions` gehört nach jedem Dispatch
|
/// weiterreicht. Die Queue in `Game::actions` gehört nach jedem Dispatch
|
||||||
@@ -15,7 +15,7 @@ use std::fs::read_to_string;
|
|||||||
|
|
||||||
use bladeink::story::Story as BladeStory;
|
use bladeink::story::Story as BladeStory;
|
||||||
|
|
||||||
use crate::kv::Store;
|
use crate::engine::kv::Store;
|
||||||
|
|
||||||
pub enum StoryState {
|
pub enum StoryState {
|
||||||
Text(String),
|
Text(String),
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
//! Single Source of Truth für alle Spielvariablen (Quest-Flags, Inventar-
|
//! Single Source of Truth für alle Spielvariablen (Quest-Flags, Inventar-
|
||||||
//! Counter, Story-Beats). Ink-Skripte haben eigene `variables_state`-Stores;
|
//! Counter, Story-Beats). Ink-Skripte haben eigene `variables_state`-Stores;
|
||||||
//! die werden bei jedem `cont()` mit diesem KV synchronisiert (siehe
|
//! die werden bei jedem `cont()` mit diesem KV synchronisiert (siehe
|
||||||
//! [`crate::ink`]). Damit der KV authoritativ bleibt: externe Actions
|
//! [`crate::engine::ink`]). Damit der KV authoritativ bleibt: externe Actions
|
||||||
//! (`set`, `inc`, `clear`) ändern *immer* den KV, niemals direkt die Story.
|
//! (`set`, `inc`, `clear`) ändern *immer* den KV, niemals direkt die Story.
|
||||||
//!
|
//!
|
||||||
//! Wir nutzen `bladeink::ValueType` als Value, damit die Sync-Konvertierung
|
//! Wir nutzen `bladeink::ValueType` als Value, damit die Sync-Konvertierung
|
||||||
@@ -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<Entity>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct Entity {
|
||||||
|
pub props: HashMap<String, String>,
|
||||||
|
pub brushes: Vec<Brush>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<Face>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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<Entity> = None;
|
||||||
|
let mut cur_brush: Option<Brush> = 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<Face> {
|
||||||
|
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<Vec<f32>> = 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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
//! Headless-Kern: alles, was läuft, ohne dass ein Frontend existiert.
|
||||||
|
//!
|
||||||
|
//! Schicht-Regel: `engine` kennt keine Frontends. Nach außen kommuniziert
|
||||||
|
//! der Kern nur über Rückgabewerte (`StoryState`, Tags) und die
|
||||||
|
//! [`game::Action`]-Queue — niemals über Aufrufe in Frontend-Module.
|
||||||
|
//! Frontends (CLI-REPL jetzt, Renderer später) sind Geschwister-Module,
|
||||||
|
//! die `engine` konsumieren; die Richtung kehrt sich nie um.
|
||||||
|
//!
|
||||||
|
//! Interner Abhängigkeitsgraph (azyklisch, Pfeil = „benutzt"):
|
||||||
|
//!
|
||||||
|
//! kv ← ink ← game ← story_ctrl ← signals
|
||||||
|
//! ↑ ↑
|
||||||
|
//! kv assets
|
||||||
|
//!
|
||||||
|
//! 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;
|
||||||
@@ -30,11 +30,9 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fs::read_to_string;
|
use std::fs::read_to_string;
|
||||||
|
|
||||||
use crate::game::{Action, ActionCtx};
|
use crate::engine::game::{Action, ActionCtx, Signals};
|
||||||
use crate::kv;
|
use crate::engine::kv;
|
||||||
use crate::story_ctrl;
|
use crate::engine::story_ctrl;
|
||||||
|
|
||||||
pub type Signals = HashMap<String, Vec<String>>;
|
|
||||||
|
|
||||||
/// Strippt Blenders Duplikat-Suffix (`.NNN` mit reinen Ziffern am Ende),
|
/// Strippt Blenders Duplikat-Suffix (`.NNN` mit reinen Ziffern am Ende),
|
||||||
/// damit `Mushroom.001`, `Mushroom.042` etc. alle auf `[Mushroom]` in
|
/// damit `Mushroom.001`, `Mushroom.042` etc. alle auf `[Mushroom]` in
|
||||||
@@ -129,7 +127,7 @@ pub fn load_signals(path: &str) -> Signals {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::game::Game;
|
use crate::engine::game::Game;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn signal_key_strips_blender_suffix() {
|
fn signal_key_strips_blender_suffix() {
|
||||||
@@ -7,9 +7,9 @@
|
|||||||
//! Aufrufer entscheidet, ob/wie er sie via `signals::dispatch` weiterreicht
|
//! Aufrufer entscheidet, ob/wie er sie via `signals::dispatch` weiterreicht
|
||||||
//! (vermeidet den Modul-Zyklus signals → story_ctrl → signals).
|
//! (vermeidet den Modul-Zyklus signals → story_ctrl → signals).
|
||||||
|
|
||||||
use crate::assets;
|
use crate::engine::assets;
|
||||||
use crate::game::ActionCtx;
|
use crate::engine::game::ActionCtx;
|
||||||
use crate::ink::{Story, StoryState};
|
use crate::engine::ink::{Story, StoryState};
|
||||||
|
|
||||||
/// Lädt ein Ink-Skript (Pfad relativ zu `assets/interactions/`) in den
|
/// Lädt ein Ink-Skript (Pfad relativ zu `assets/interactions/`) in den
|
||||||
/// Story-Slot. Der erste Schritt passiert noch nicht — das Frontend ruft
|
/// Story-Slot. Der erste Schritt passiert noch nicht — das Frontend ruft
|
||||||
@@ -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<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<Image, String> {
|
||||||
|
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<u8> = 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<Vec<u8>, 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
|
||||||
|
}
|
||||||
|
}
|
||||||
+16
-135
@@ -1,144 +1,25 @@
|
|||||||
//! wds — Weirdcore Dating Simulator.
|
//! wds — Weirdcore Dating Simulator.
|
||||||
//!
|
//!
|
||||||
//! CLI-Phase: Die Kern-Systeme (Signal/Action-Dispatcher, KV-Store,
|
//! Aufbau: `engine/` ist der headless Kern (Signal/Action-Dispatcher,
|
||||||
//! Ink-Stories) laufen headless; diese REPL ist ihr erstes Frontend.
|
//! KV-Store, Ink-Stories); `session` hält den geteilten Anwendungszustand
|
||||||
//! Der Renderer kommt später dazu (siehe notes/renderer-plan.md) und
|
//! darüber (Game + Dialog-Modus + Befehls-Interpreter). Beide Frontends
|
||||||
//! konsumiert dieselben Schnittstellen — insbesondere `StoryState` und
|
//! steuern dieselbe Session: das Fenster (`render`, Default) und die
|
||||||
//! die `Action`-Queue.
|
//! Konsolen-REPL (`cli`, via `--cli`).
|
||||||
|
|
||||||
use std::io::{self, Write};
|
mod cli;
|
||||||
|
mod engine;
|
||||||
|
mod render;
|
||||||
|
mod session;
|
||||||
|
|
||||||
mod assets;
|
use engine::assets;
|
||||||
mod game;
|
use session::Session;
|
||||||
mod ink;
|
|
||||||
mod kv;
|
|
||||||
mod signals;
|
|
||||||
mod story_ctrl;
|
|
||||||
|
|
||||||
use game::{Action, Game};
|
|
||||||
use ink::StoryState;
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let signals_path = assets::path("assets/signals.toml");
|
let signals_path = assets::path("assets/signals.toml");
|
||||||
let signal_table = signals::load_signals(&signals_path);
|
let session = Session::new(signals_path.clone());
|
||||||
println!("wds-Konsole — {} Signale aus {}. `help` für Befehle.",
|
if std::env::args().any(|a| a == "--cli") {
|
||||||
signal_table.len(), signals_path);
|
cli::run(session, &signals_path);
|
||||||
let mut game = Game::new(signal_table);
|
} else {
|
||||||
|
render::run(session);
|
||||||
// Reserviertes `[init]`-Signal: KV-Defaults setzen, bevor Stories laufen.
|
|
||||||
fire(&mut game, "init", None);
|
|
||||||
|
|
||||||
while let Some(line) = read_line("> ") {
|
|
||||||
let line = line.trim();
|
|
||||||
match line {
|
|
||||||
"" => {}
|
|
||||||
"quit" | "exit" => break,
|
|
||||||
"help" => print_help(),
|
|
||||||
"kv" => dump_kv(&game),
|
|
||||||
"reload" => {
|
|
||||||
game.signals = signals::load_signals(&signals_path);
|
|
||||||
println!("{} Signale geladen.", game.signals.len());
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
if let Some(sig) = line.strip_prefix("signal ") {
|
|
||||||
fire(&mut game, sig.trim(), None);
|
|
||||||
} else if let Some(name) = line.strip_prefix("use ") {
|
|
||||||
// Objekt-Interaktion simulieren: Signal ist der gestrippte
|
|
||||||
// Name, $self der volle — wie der LMB-Klick-Pfad in irl3d.
|
|
||||||
let name = name.trim();
|
|
||||||
let key = signals::signal_key(name).to_string();
|
|
||||||
fire(&mut game, &key, Some(name.to_string()));
|
|
||||||
} else {
|
|
||||||
println!("unbekannter Befehl: {line:?} — `help` für Befehle");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn print_help() {
|
|
||||||
println!(" signal <s> Signal feuern oder Action direkt ausführen");
|
|
||||||
println!(" (z.B. `signal Cube`, `signal set has_key true`)");
|
|
||||||
println!(" use <instance> Objekt-Interaktion simulieren — `use Mushroom.005`");
|
|
||||||
println!(" feuert Signal `Mushroom` mit $self = Mushroom.005");
|
|
||||||
println!(" kv KV-Store anzeigen");
|
|
||||||
println!(" reload signals.toml neu laden");
|
|
||||||
println!(" quit beenden");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Signal dispatchen, deferred Actions ausgeben und — falls eine Action
|
|
||||||
/// eine Story gestartet hat — den Dialog-Loop fahren.
|
|
||||||
fn fire(game: &mut Game, signal: &str, instance: Option<String>) {
|
|
||||||
{
|
|
||||||
let mut ctx = game.action_ctx(instance);
|
|
||||||
signals::dispatch(signal, &mut ctx);
|
|
||||||
}
|
|
||||||
drain_actions(game);
|
|
||||||
if game.story.is_some() {
|
|
||||||
run_story(game);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Dialog-Loop: Story schrittweise treiben, Tags zurück in den Dispatcher,
|
|
||||||
/// Choices über nummerierte Eingabe. Läuft bis `End` (oder EOF).
|
|
||||||
fn run_story(game: &mut Game) {
|
|
||||||
let mut sel: Option<usize> = None;
|
|
||||||
loop {
|
|
||||||
let state = {
|
|
||||||
let mut ctx = game.action_ctx(None);
|
|
||||||
let (state, tags) = story_ctrl::advance(sel, &mut ctx);
|
|
||||||
for t in &tags { signals::dispatch(t, &mut ctx); }
|
|
||||||
state
|
|
||||||
};
|
|
||||||
drain_actions(game);
|
|
||||||
match state {
|
|
||||||
None | Some(StoryState::End) => break,
|
|
||||||
Some(StoryState::Text(text)) => {
|
|
||||||
println!("\n{text}");
|
|
||||||
if read_line(" [Enter] ").is_none() { break; }
|
|
||||||
sel = None;
|
|
||||||
}
|
|
||||||
Some(StoryState::Choice { prompt, options }) => {
|
|
||||||
if !prompt.is_empty() { println!("\n{prompt}"); }
|
|
||||||
for (i, o) in options.iter().enumerate() {
|
|
||||||
println!(" {}) {o}", i + 1);
|
|
||||||
}
|
|
||||||
sel = loop {
|
|
||||||
let Some(line) = read_line(" wahl> ") else { return; };
|
|
||||||
if let Ok(n) = line.trim().parse::<usize>() {
|
|
||||||
if (1..=options.len()).contains(&n) { break Some(n - 1); }
|
|
||||||
}
|
|
||||||
println!(" (1..{})", options.len());
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn drain_actions(game: &mut Game) {
|
|
||||||
for a in game.actions.drain(..) {
|
|
||||||
match a {
|
|
||||||
Action::HideObject(n) => println!("[action] hide_object {n}"),
|
|
||||||
Action::PlaySound(n) => println!("[action] play_sound {n}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn dump_kv(game: &Game) {
|
|
||||||
let mut keys: Vec<&String> = game.kv.keys().collect();
|
|
||||||
keys.sort();
|
|
||||||
for k in keys {
|
|
||||||
println!(" {k} = {}", kv::format_value(&game.kv[k]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Zeile von stdin lesen; `None` bei EOF (Ctrl-D) oder Lesefehler.
|
|
||||||
fn read_line(prompt: &str) -> Option<String> {
|
|
||||||
print!("{prompt}");
|
|
||||||
io::stdout().flush().ok();
|
|
||||||
let mut buf = String::new();
|
|
||||||
match io::stdin().read_line(&mut buf) {
|
|
||||||
Ok(0) | Err(_) => None,
|
|
||||||
Ok(_) => Some(buf.trim_end().to_string()),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
// Upscale-Pass: internes 320×240-Target nearest-gesampelt auf die
|
||||||
|
// Surface. Das 4:3-Letterbox-Rechteck setzt der Rust-Code als Viewport;
|
||||||
|
// hier ist es ein simples Fullscreen-Dreieck mit UVs.
|
||||||
|
|
||||||
|
struct VsOut {
|
||||||
|
@builtin(position) pos: vec4f,
|
||||||
|
@location(0) uv: vec2f,
|
||||||
|
};
|
||||||
|
|
||||||
|
@vertex
|
||||||
|
fn vs_main(@builtin(vertex_index) i: u32) -> VsOut {
|
||||||
|
var pos = array<vec2f, 3>(
|
||||||
|
vec2f(-1.0, -1.0), vec2f(3.0, -1.0), vec2f(-1.0, 3.0),
|
||||||
|
);
|
||||||
|
var out: VsOut;
|
||||||
|
out.pos = vec4f(pos[i], 0.0, 1.0);
|
||||||
|
// NDC (y hoch) → UV (v runter).
|
||||||
|
out.uv = vec2f(pos[i].x * 0.5 + 0.5, 0.5 - pos[i].y * 0.5);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@group(0) @binding(0) var internal_tex: texture_2d<f32>;
|
||||||
|
@group(0) @binding(1) var internal_smp: sampler;
|
||||||
|
|
||||||
|
@fragment
|
||||||
|
fn fs_main(in: VsOut) -> @location(0) vec4f {
|
||||||
|
return textureSample(internal_tex, internal_smp, in.uv);
|
||||||
|
}
|
||||||
@@ -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<String> {
|
||||||
|
let mut names: Vec<String> = 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<FacePoly> = 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<u32>> = 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<FacePoly<'a>>) {
|
||||||
|
let planes: Vec<Plane> = 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<bool> {
|
||||||
|
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<Vertex>,
|
||||||
|
per_tex: &mut [Vec<u32>],
|
||||||
|
) {
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,304 @@
|
|||||||
|
//! wgpu-Zustand: Surface, Device und der zweistufige Render-Pfad
|
||||||
|
//! aus dem Renderer-Plan:
|
||||||
|
//!
|
||||||
|
//! Pass 1 (intern): 320×240 RGBA8 + Depth — hier entsteht das Bild
|
||||||
|
//! ([`ScenePass`], PS1-Shader).
|
||||||
|
//! Pass 2 (Fenster): Nearest-Upscale des internen Targets mit
|
||||||
|
//! 4:3-Letterbox (via Viewport) auf die Surface.
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use winit::window::Window;
|
||||||
|
|
||||||
|
use crate::engine::tga::Image;
|
||||||
|
use crate::render::math::Mat4;
|
||||||
|
use crate::render::scene::{Mesh, ScenePass};
|
||||||
|
use crate::render::sprite::SpritePass;
|
||||||
|
use crate::render::ui::Ui;
|
||||||
|
|
||||||
|
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.
|
||||||
|
const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth16Unorm;
|
||||||
|
const INTERNAL_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
|
||||||
|
|
||||||
|
pub struct Gpu {
|
||||||
|
surface: wgpu::Surface<'static>,
|
||||||
|
device: wgpu::Device,
|
||||||
|
queue: wgpu::Queue,
|
||||||
|
config: wgpu::SurfaceConfiguration,
|
||||||
|
|
||||||
|
internal_view: wgpu::TextureView,
|
||||||
|
depth_view: wgpu::TextureView,
|
||||||
|
|
||||||
|
scene: ScenePass,
|
||||||
|
sprite: SpritePass,
|
||||||
|
blit_pipeline: wgpu::RenderPipeline,
|
||||||
|
blit_bind: wgpu::BindGroup,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Gpu {
|
||||||
|
pub fn new(
|
||||||
|
window: Arc<Window>,
|
||||||
|
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
|
||||||
|
// WGPU_BACKEND=gl zum Testen des Fallback-Pfads.
|
||||||
|
let instance = wgpu::Instance::new(
|
||||||
|
wgpu::InstanceDescriptor::new_with_display_handle(Box::new(display)).with_env(),
|
||||||
|
);
|
||||||
|
let surface = instance.create_surface(window).expect("wgpu: Surface");
|
||||||
|
|
||||||
|
let adapter = pollster::block_on(instance.request_adapter(
|
||||||
|
&wgpu::RequestAdapterOptions {
|
||||||
|
compatible_surface: Some(&surface),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)).expect("wgpu: kein Adapter");
|
||||||
|
let info = adapter.get_info();
|
||||||
|
println!("[gpu] {} ({:?}, {:?})", info.name, info.backend, info.device_type);
|
||||||
|
|
||||||
|
// Baseline-Limits/-Features: alles, was der Plan braucht, ist
|
||||||
|
// WebGPU-Kern — nichts anfordern, dann läuft es auch auf HD 5500.
|
||||||
|
let (device, queue) = pollster::block_on(
|
||||||
|
adapter.request_device(&wgpu::DeviceDescriptor::default()),
|
||||||
|
).expect("wgpu: Device");
|
||||||
|
|
||||||
|
// Nicht-sRGB-8-Bit-Surface bevorzugen: der Fragment-Shader
|
||||||
|
// quantisiert später selbst auf RGB555 — die Werte sollen
|
||||||
|
// unverändert auf den Schirm, ohne Gamma-Umkodierung beim Blit.
|
||||||
|
// Explizite Liste statt „erstes nicht-sRGB": Treiber bieten auch
|
||||||
|
// 16-Bit-Formate an, die extra Device-Features bräuchten.
|
||||||
|
let caps = surface.get_capabilities(&adapter);
|
||||||
|
let format = [wgpu::TextureFormat::Bgra8Unorm, wgpu::TextureFormat::Rgba8Unorm]
|
||||||
|
.into_iter()
|
||||||
|
.find(|f| caps.formats.contains(f))
|
||||||
|
.unwrap_or(caps.formats[0]);
|
||||||
|
println!("[gpu] Surface-Format {format:?}");
|
||||||
|
let config = wgpu::SurfaceConfiguration {
|
||||||
|
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||||
|
format,
|
||||||
|
width: size.width.max(1),
|
||||||
|
height: size.height.max(1),
|
||||||
|
present_mode: wgpu::PresentMode::AutoVsync,
|
||||||
|
alpha_mode: caps.alpha_modes[0],
|
||||||
|
view_formats: vec![],
|
||||||
|
desired_maximum_frame_latency: 2,
|
||||||
|
};
|
||||||
|
surface.configure(&device, &config);
|
||||||
|
|
||||||
|
let (internal_view, depth_view) = make_internal_targets(&device);
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
label: Some("blit"),
|
||||||
|
source: wgpu::ShaderSource::Wgsl(include_str!("blit.wgsl").into()),
|
||||||
|
});
|
||||||
|
let blit_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||||
|
label: Some("blit"),
|
||||||
|
layout: None,
|
||||||
|
vertex: wgpu::VertexState {
|
||||||
|
module: &blit_shader,
|
||||||
|
entry_point: Some("vs_main"),
|
||||||
|
compilation_options: Default::default(),
|
||||||
|
buffers: &[],
|
||||||
|
},
|
||||||
|
fragment: Some(wgpu::FragmentState {
|
||||||
|
module: &blit_shader,
|
||||||
|
entry_point: Some("fs_main"),
|
||||||
|
compilation_options: Default::default(),
|
||||||
|
targets: &[Some(config.format.into())],
|
||||||
|
}),
|
||||||
|
primitive: wgpu::PrimitiveState::default(),
|
||||||
|
depth_stencil: None,
|
||||||
|
multisample: wgpu::MultisampleState::default(),
|
||||||
|
multiview_mask: None,
|
||||||
|
cache: None,
|
||||||
|
});
|
||||||
|
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
|
||||||
|
label: Some("blit nearest"),
|
||||||
|
mag_filter: wgpu::FilterMode::Nearest,
|
||||||
|
min_filter: wgpu::FilterMode::Nearest,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
let blit_bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||||
|
label: Some("blit"),
|
||||||
|
layout: &blit_pipeline.get_bind_group_layout(0),
|
||||||
|
entries: &[
|
||||||
|
wgpu::BindGroupEntry {
|
||||||
|
binding: 0,
|
||||||
|
resource: wgpu::BindingResource::TextureView(&internal_view),
|
||||||
|
},
|
||||||
|
wgpu::BindGroupEntry {
|
||||||
|
binding: 1,
|
||||||
|
resource: wgpu::BindingResource::Sampler(&sampler),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
Self {
|
||||||
|
surface, device, queue, config,
|
||||||
|
internal_view, depth_view,
|
||||||
|
scene, sprite, blit_pipeline, blit_bind,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resize(&mut self, width: u32, height: u32) {
|
||||||
|
self.config.width = width.max(1);
|
||||||
|
self.config.height = height.max(1);
|
||||||
|
self.surface.configure(&self.device, &self.config);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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,
|
||||||
|
);
|
||||||
|
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() {
|
||||||
|
Cst::Success(f) | Cst::Suboptimal(f) => f,
|
||||||
|
// Surface veraltet (Resize/Compositor): neu konfigurieren,
|
||||||
|
// diesen Frame auslassen.
|
||||||
|
Cst::Outdated | Cst::Lost => {
|
||||||
|
self.surface.configure(&self.device, &self.config);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Fenster verdeckt/minimiert oder Treiber-Timeout: auslassen.
|
||||||
|
Cst::Timeout | Cst::Occluded => return,
|
||||||
|
Cst::Validation => panic!("wgpu: Surface-Validation-Fehler"),
|
||||||
|
};
|
||||||
|
let surface_view = frame.texture.create_view(&Default::default());
|
||||||
|
let mut enc = self.device.create_command_encoder(&Default::default());
|
||||||
|
|
||||||
|
// Pass 1: intern.
|
||||||
|
{
|
||||||
|
let mut pass = enc.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||||
|
label: Some("internal"),
|
||||||
|
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||||
|
view: &self.internal_view,
|
||||||
|
depth_slice: None,
|
||||||
|
resolve_target: None,
|
||||||
|
ops: wgpu::Operations {
|
||||||
|
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
|
||||||
|
store: wgpu::StoreOp::Store,
|
||||||
|
},
|
||||||
|
})],
|
||||||
|
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
|
||||||
|
view: &self.depth_view,
|
||||||
|
depth_ops: Some(wgpu::Operations {
|
||||||
|
load: wgpu::LoadOp::Clear(1.0),
|
||||||
|
store: wgpu::StoreOp::Store,
|
||||||
|
}),
|
||||||
|
stencil_ops: None,
|
||||||
|
}),
|
||||||
|
timestamp_writes: None,
|
||||||
|
occlusion_query_set: None,
|
||||||
|
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.
|
||||||
|
{
|
||||||
|
let mut pass = enc.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||||
|
label: Some("blit"),
|
||||||
|
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||||
|
view: &surface_view,
|
||||||
|
depth_slice: None,
|
||||||
|
resolve_target: None,
|
||||||
|
ops: wgpu::Operations {
|
||||||
|
// Schwarz = die Letterbox-Balken.
|
||||||
|
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
|
||||||
|
store: wgpu::StoreOp::Store,
|
||||||
|
},
|
||||||
|
})],
|
||||||
|
depth_stencil_attachment: None,
|
||||||
|
timestamp_writes: None,
|
||||||
|
occlusion_query_set: None,
|
||||||
|
multiview_mask: None,
|
||||||
|
});
|
||||||
|
let (vx, vy, vw, vh) = letterbox(self.config.width, self.config.height);
|
||||||
|
pass.set_viewport(vx, vy, vw, vh, 0.0, 1.0);
|
||||||
|
pass.set_pipeline(&self.blit_pipeline);
|
||||||
|
pass.set_bind_group(0, &self.blit_bind, &[]);
|
||||||
|
pass.draw(0..3, 0..1);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.queue.submit([enc.finish()]);
|
||||||
|
frame.present();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_internal_targets(device: &wgpu::Device) -> (wgpu::TextureView, wgpu::TextureView) {
|
||||||
|
let size = wgpu::Extent3d {
|
||||||
|
width: INTERNAL_W, height: INTERNAL_H, depth_or_array_layers: 1,
|
||||||
|
};
|
||||||
|
let color = device.create_texture(&wgpu::TextureDescriptor {
|
||||||
|
label: Some("internal color"),
|
||||||
|
size,
|
||||||
|
mip_level_count: 1,
|
||||||
|
sample_count: 1,
|
||||||
|
dimension: wgpu::TextureDimension::D2,
|
||||||
|
format: INTERNAL_FORMAT,
|
||||||
|
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
|
||||||
|
view_formats: &[],
|
||||||
|
});
|
||||||
|
let depth = device.create_texture(&wgpu::TextureDescriptor {
|
||||||
|
label: Some("internal depth"),
|
||||||
|
size,
|
||||||
|
mip_level_count: 1,
|
||||||
|
sample_count: 1,
|
||||||
|
dimension: wgpu::TextureDimension::D2,
|
||||||
|
format: DEPTH_FORMAT,
|
||||||
|
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||||
|
view_formats: &[],
|
||||||
|
});
|
||||||
|
(color.create_view(&Default::default()), depth.create_view(&Default::default()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Größtes 4:3-Rechteck, das ins Fenster passt, zentriert.
|
||||||
|
/// Nicht-ganzzahlige Skalierung ist gewollt (volle Fensterausnutzung);
|
||||||
|
/// die leicht ungleichen Pixel passen zum CRT-Vorbild.
|
||||||
|
fn letterbox(win_w: u32, win_h: u32) -> (f32, f32, f32, f32) {
|
||||||
|
let (w, h) = (win_w as f32, win_h as f32);
|
||||||
|
let scale = (w / INTERNAL_W as f32).min(h / INTERNAL_H as f32);
|
||||||
|
let vw = INTERNAL_W as f32 * scale;
|
||||||
|
let vh = INTERNAL_H as f32 * scale;
|
||||||
|
((w - vw) * 0.5, (h - vh) * 0.5, vw, vh)
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
//! Minimale f32-Matrix-Mathematik für den Renderer — bewusst handgerollt
|
||||||
|
//! statt glam: PS1-Style braucht nur Multiply, View und Perspective.
|
||||||
|
//! (Die Spiellogik bekommt später ihr eigenes Q16.16 wie irl3d; das hier
|
||||||
|
//! ist nur der GPU-Pfad.)
|
||||||
|
//!
|
||||||
|
//! Konventionen:
|
||||||
|
//! - rechtshändig, Kamera blickt -Z, +Y oben (wie Blender-OBJ-Export)
|
||||||
|
//! - Spaltenvektoren, Speicher column-major — `Mat4.0[spalte][zeile]`,
|
||||||
|
//! bytemuck-kompatibel zu WGSL `mat4x4f`
|
||||||
|
//! - Clip-Z in [0,1] (wgpu/WebGPU, nicht GL-[-1,1])
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Clone, Copy, Debug, bytemuck::Pod, bytemuck::Zeroable)]
|
||||||
|
pub struct Mat4(pub [[f32; 4]; 4]);
|
||||||
|
|
||||||
|
impl Mat4 {
|
||||||
|
pub const IDENT: Mat4 = Mat4([
|
||||||
|
[1.0, 0.0, 0.0, 0.0],
|
||||||
|
[0.0, 1.0, 0.0, 0.0],
|
||||||
|
[0.0, 0.0, 1.0, 0.0],
|
||||||
|
[0.0, 0.0, 0.0, 1.0],
|
||||||
|
]);
|
||||||
|
|
||||||
|
pub fn mul(&self, rhs: &Mat4) -> Mat4 {
|
||||||
|
let mut out = [[0.0f32; 4]; 4];
|
||||||
|
for c in 0..4 {
|
||||||
|
for r in 0..4 {
|
||||||
|
out[c][r] = (0..4).map(|k| self.0[k][r] * rhs.0[c][k]).sum();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Mat4(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Punkt-Transformation auf der CPU — bisher nur von den Tests
|
||||||
|
/// gebraucht; der Renderer transformiert auf der GPU.
|
||||||
|
#[cfg(test)]
|
||||||
|
pub fn transform(&self, v: [f32; 4]) -> [f32; 4] {
|
||||||
|
let mut out = [0.0f32; 4];
|
||||||
|
for r in 0..4 {
|
||||||
|
out[r] = (0..4).map(|k| self.0[k][r] * v[k]).sum();
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn translate(x: f32, y: f32, z: f32) -> Mat4 {
|
||||||
|
let mut m = Mat4::IDENT;
|
||||||
|
m.0[3] = [x, y, z, 1.0];
|
||||||
|
m
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rot_x(a: f32) -> Mat4 {
|
||||||
|
let (s, c) = a.sin_cos();
|
||||||
|
let mut m = Mat4::IDENT;
|
||||||
|
m.0[1] = [0.0, c, s, 0.0];
|
||||||
|
m.0[2] = [0.0, -s, c, 0.0];
|
||||||
|
m
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rot_y(a: f32) -> Mat4 {
|
||||||
|
let (s, c) = a.sin_cos();
|
||||||
|
let mut m = Mat4::IDENT;
|
||||||
|
m.0[0] = [c, 0.0, -s, 0.0];
|
||||||
|
m.0[2] = [s, 0.0, c, 0.0];
|
||||||
|
m
|
||||||
|
}
|
||||||
|
|
||||||
|
/// FPS-View: erst Kamera-Position abziehen, dann Yaw, dann Pitch
|
||||||
|
/// herausdrehen. Yaw/Pitch wie die irl3d-Kamera: yaw=0 blickt -Z,
|
||||||
|
/// positiver Pitch hebt den Blick.
|
||||||
|
pub fn view(pos: [f32; 3], yaw: f32, pitch: f32) -> Mat4 {
|
||||||
|
Mat4::rot_x(-pitch)
|
||||||
|
.mul(&Mat4::rot_y(-yaw))
|
||||||
|
.mul(&Mat4::translate(-pos[0], -pos[1], -pos[2]))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Perspektive mit Clip-Z in [0,1]. `fovy` in Radiant.
|
||||||
|
pub fn perspective(fovy: f32, aspect: f32, near: f32, far: f32) -> Mat4 {
|
||||||
|
let f = 1.0 / (fovy * 0.5).tan();
|
||||||
|
Mat4([
|
||||||
|
[f / aspect, 0.0, 0.0, 0.0],
|
||||||
|
[0.0, f, 0.0, 0.0],
|
||||||
|
[0.0, 0.0, far / (near - far), -1.0],
|
||||||
|
[0.0, 0.0, near * far / (near - far), 0.0],
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn close(a: [f32; 4], b: [f32; 4]) -> bool {
|
||||||
|
a.iter().zip(b).all(|(x, y)| (x - y).abs() < 1e-5)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn view_translates_origin_in_front() {
|
||||||
|
// Kamera bei z=+5, Blick -Z → Origin liegt 5 vor der Kamera.
|
||||||
|
let v = Mat4::view([0.0, 0.0, 5.0], 0.0, 0.0);
|
||||||
|
assert!(close(v.transform([0.0, 0.0, 0.0, 1.0]), [0.0, 0.0, -5.0, 1.0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn view_yaw_quarter_turn_looks_minus_x() {
|
||||||
|
// yaw=90°: Blick Richtung -X (irl3d-Konvention) — ein Punkt auf
|
||||||
|
// -X liegt dann vor der Kamera.
|
||||||
|
let v = Mat4::view([0.0, 0.0, 0.0], std::f32::consts::FRAC_PI_2, 0.0);
|
||||||
|
assert!(close(v.transform([-2.0, 0.0, 0.0, 1.0]), [0.0, 0.0, -2.0, 1.0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn view_pitch_up_looks_plus_y() {
|
||||||
|
let v = Mat4::view([0.0, 0.0, 0.0], 0.0, std::f32::consts::FRAC_PI_2);
|
||||||
|
assert!(close(v.transform([0.0, 3.0, 0.0, 1.0]), [0.0, 0.0, -3.0, 1.0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn perspective_maps_near_far_to_0_1() {
|
||||||
|
let p = Mat4::perspective(1.0, 4.0 / 3.0, 0.1, 100.0);
|
||||||
|
let n = p.transform([0.0, 0.0, -0.1, 1.0]);
|
||||||
|
let f = p.transform([0.0, 0.0, -100.0, 1.0]);
|
||||||
|
assert!((n[2] / n[3] - 0.0).abs() < 1e-5);
|
||||||
|
assert!((f[2] / f[3] - 1.0).abs() < 1e-4);
|
||||||
|
// w = Abstand vor der Kamera
|
||||||
|
assert!((n[3] - 0.1).abs() < 1e-6 && (f[3] - 100.0).abs() < 1e-4);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,337 @@
|
|||||||
|
//! Fenster-Frontend: winit-Loop um den wgpu-Renderer.
|
||||||
|
//!
|
||||||
|
//! 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 (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::{DeviceEvent, DeviceId, ElementState, KeyEvent, MouseButton, WindowEvent};
|
||||||
|
use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
|
||||||
|
use winit::keyboard::{KeyCode, PhysicalKey};
|
||||||
|
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<Image> = 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<Image> = 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.
|
||||||
|
let (tx, rx) = mpsc::channel::<String>();
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
let stdin = std::io::stdin();
|
||||||
|
let mut buf = String::new();
|
||||||
|
loop {
|
||||||
|
buf.clear();
|
||||||
|
match stdin.read_line(&mut buf) {
|
||||||
|
Ok(0) | Err(_) => break, // EOF (Ctrl-D) → Thread endet
|
||||||
|
Ok(_) => {
|
||||||
|
if tx.send(buf.trim_end().to_string()).is_err() { break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let event_loop = EventLoop::new().expect("winit: Event-Loop");
|
||||||
|
// 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, 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<Arc<Window>>,
|
||||||
|
gpu: Option<Gpu>,
|
||||||
|
session: Session,
|
||||||
|
rx: Receiver<String>,
|
||||||
|
camera: Camera,
|
||||||
|
input: Input,
|
||||||
|
last: Option<Instant>,
|
||||||
|
/// 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<Image>,
|
||||||
|
ui_textures: Vec<Image>,
|
||||||
|
fonts: font::Fonts,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl App {
|
||||||
|
fn new(
|
||||||
|
session: Session,
|
||||||
|
rx: Receiver<String>,
|
||||||
|
mesh: Mesh,
|
||||||
|
images: Vec<Image>,
|
||||||
|
ui_textures: Vec<Image>,
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ApplicationHandler for App {
|
||||||
|
/// winit liefert das Fenster erst hier, nicht beim Loop-Start —
|
||||||
|
/// deshalb sind `window`/`gpu` Options statt Konstruktor-Felder.
|
||||||
|
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
||||||
|
if self.window.is_some() { return; }
|
||||||
|
let attrs = Window::default_attributes()
|
||||||
|
.with_title("wds")
|
||||||
|
// 2× interne Auflösung als Startgröße; frei resizebar.
|
||||||
|
.with_inner_size(winit::dpi::LogicalSize::new(
|
||||||
|
gpu::INTERNAL_W * 2, gpu::INTERNAL_H * 2));
|
||||||
|
let window = Arc::new(event_loop.create_window(attrs).expect("winit: Fenster"));
|
||||||
|
// 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) {
|
||||||
|
while let Ok(line) = self.rx.try_recv() {
|
||||||
|
let r = self.session.exec(&line);
|
||||||
|
for out in r.output { println!("{out}"); }
|
||||||
|
if r.quit { event_loop.exit(); }
|
||||||
|
}
|
||||||
|
if let Some(w) = &self.window { w.request_redraw(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
|
||||||
|
match event {
|
||||||
|
WindowEvent::CloseRequested => 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 => 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
|
||||||
|
}
|
||||||
@@ -0,0 +1,227 @@
|
|||||||
|
//! Szenen-Pass: zeichnet die 3D-Welt ins interne Target.
|
||||||
|
//!
|
||||||
|
//! 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)]
|
||||||
|
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<Vertex>,
|
||||||
|
pub(crate) indices: Vec<u32>,
|
||||||
|
pub(crate) batches: Vec<Batch>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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::<Vertex>() as u64,
|
||||||
|
step_mode: wgpu::VertexStepMode::Vertex,
|
||||||
|
attributes: &wgpu::vertex_attr_array![0 => Float32x3, 1 => Float32x2],
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct ScenePass {
|
||||||
|
pipeline: wgpu::RenderPipeline,
|
||||||
|
vbuf: wgpu::Buffer,
|
||||||
|
ibuf: wgpu::Buffer,
|
||||||
|
ubuf: wgpu::Buffer,
|
||||||
|
uniform_bind: wgpu::BindGroup, // group 0
|
||||||
|
tex_binds: Vec<wgpu::BindGroup>, // group 1, pro Textur
|
||||||
|
batches: Vec<Batch>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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"),
|
||||||
|
source: wgpu::ShaderSource::Wgsl(include_str!("scene.wgsl").into()),
|
||||||
|
});
|
||||||
|
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||||
|
label: Some("scene"),
|
||||||
|
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(color_format.into())],
|
||||||
|
}),
|
||||||
|
// 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),
|
||||||
|
depth_compare: Some(wgpu::CompareFunction::Less),
|
||||||
|
stencil: wgpu::StencilState::default(),
|
||||||
|
bias: wgpu::DepthBiasState::default(),
|
||||||
|
}),
|
||||||
|
multisample: wgpu::MultisampleState::default(),
|
||||||
|
multiview_mask: None,
|
||||||
|
cache: None,
|
||||||
|
});
|
||||||
|
|
||||||
|
let vbuf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||||
|
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("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::<Uniforms>() as u64,
|
||||||
|
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
||||||
|
mapped_at_creation: false,
|
||||||
|
});
|
||||||
|
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,
|
||||||
|
resource: ubuf.as_entire_binding(),
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
/// `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.uniform_bind, &[]);
|
||||||
|
pass.set_vertex_buffer(0, self.vbuf.slice(..));
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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) },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
// 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) 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<uniform> u: Uniforms;
|
||||||
|
|
||||||
|
@group(1) @binding(0) var tex: texture_2d<f32>;
|
||||||
|
@group(1) @binding(1) var smp: sampler;
|
||||||
|
|
||||||
|
struct VsOut {
|
||||||
|
@builtin(position) pos: vec4f,
|
||||||
|
// linear = affin interpoliert → das Textur-Wobbeln der PS1.
|
||||||
|
@location(0) @interpolate(linear) uv: vec2f,
|
||||||
|
};
|
||||||
|
|
||||||
|
@vertex
|
||||||
|
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 * u.half_res) / u.half_res * clip.w, clip.zw);
|
||||||
|
}
|
||||||
|
var out: VsOut;
|
||||||
|
out.pos = clip;
|
||||||
|
out.uv = uv;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4×4-Bayer-Matrix (Werte 0..15) für Ordered Dithering.
|
||||||
|
fn bayer4(px: vec2u) -> f32 {
|
||||||
|
var m = array<u32, 16>(
|
||||||
|
0u, 8u, 2u, 10u,
|
||||||
|
12u, 4u, 14u, 6u,
|
||||||
|
3u, 11u, 1u, 9u,
|
||||||
|
15u, 7u, 13u, 5u,
|
||||||
|
);
|
||||||
|
return f32(m[(px.y % 4u) * 4u + (px.x % 4u)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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(texel.rgb, vec3f(0.0), vec3f(1.0));
|
||||||
|
return vec4f(floor(c * 31.0 + t) / 31.0, 1.0);
|
||||||
|
}
|
||||||
@@ -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::<SpriteVertex>() 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<wgpu::BindGroup>, // 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::<SpriteVertex>() 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::<Uniforms>() 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) },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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<uniform> u: Uniforms;
|
||||||
|
|
||||||
|
@group(1) @binding(0) var tex: texture_2d<f32>;
|
||||||
|
@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;
|
||||||
|
}
|
||||||
@@ -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<SpriteVertex>,
|
||||||
|
pub(crate) batches: Vec<SpriteBatch>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+228
@@ -0,0 +1,228 @@
|
|||||||
|
//! Session: der geteilte Anwendungszustand über allen Frontends.
|
||||||
|
//!
|
||||||
|
//! Es existiert genau eine `Session` pro Lauf. CLI und Fenster steuern
|
||||||
|
//! dieselbe — das Fenster besitzt sie, die Terminal-Eingabe schickt nur
|
||||||
|
//! Befehlszeilen hinein. Dadurch kann es keinen Mismatch geben: ein
|
||||||
|
//! `Game`-Zustand, ein `Mode`, beide Frontends stellen ihn nur dar.
|
||||||
|
//!
|
||||||
|
//! Schicht: `engine ← session ← { cli, render }`. Die Session kennt kein
|
||||||
|
//! Frontend — `exec` nimmt eine Befehlszeile und *gibt* Ausgabezeilen
|
||||||
|
//! zurück (statt zu drucken), damit jedes Frontend sie frei darstellt
|
||||||
|
//! (Terminal jetzt, In-Fenster-Konsole später).
|
||||||
|
|
||||||
|
use crate::engine::game::{Action, Game};
|
||||||
|
use crate::engine::ink::StoryState;
|
||||||
|
use crate::engine::{kv, signals, story_ctrl};
|
||||||
|
|
||||||
|
/// Was die Frontends gerade darstellen sollen. Geteilt, damit CLI und GUI
|
||||||
|
/// nie uneins sind, ob ein Dialog läuft.
|
||||||
|
pub enum Mode {
|
||||||
|
/// Freie Welt — das Fenster rendert die Szene, die Konsole nimmt
|
||||||
|
/// Engine-Befehle.
|
||||||
|
Free,
|
||||||
|
/// Ein Dialog läuft — das Fenster pausiert die Welt und zeigt (sobald
|
||||||
|
/// Text-Rendering existiert) Panels; Eingaben treiben den Dialog statt
|
||||||
|
/// Befehle.
|
||||||
|
Dialog(Dialog),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Aktueller Dialogschritt, frontend-neutral. `choices` leer = reiner Text,
|
||||||
|
/// der auf „weiter" (Leerzeile/Enter) wartet.
|
||||||
|
pub struct Dialog {
|
||||||
|
// Wird vom Panel-Renderer der UI-Phase gelesen; bis dahin läuft der
|
||||||
|
// Dialogtext über die Konsolen-Ausgabe (siehe `step`).
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub text: String,
|
||||||
|
pub choices: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Session {
|
||||||
|
pub game: Game,
|
||||||
|
pub mode: Mode,
|
||||||
|
signals_path: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ergebnis einer Eingabezeile: Ausgabezeilen plus, ob das Frontend
|
||||||
|
/// beenden soll (`quit`/`exit`).
|
||||||
|
pub struct ExecResult {
|
||||||
|
pub output: Vec<String>,
|
||||||
|
pub quit: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExecResult {
|
||||||
|
fn lines(output: Vec<String>) -> Self { Self { output, quit: false } }
|
||||||
|
fn empty() -> Self { Self { output: Vec::new(), quit: false } }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Session {
|
||||||
|
pub fn new(signals_path: String) -> Self {
|
||||||
|
let game = Game::new(signals::load_signals(&signals_path));
|
||||||
|
Self { game, mode: Mode::Free, signals_path }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reserviertes `[init]`-Signal feuern (KV-Defaults, bevor etwas läuft).
|
||||||
|
/// Eigene Methode statt im Konstruktor, damit das Frontend die Ausgabe
|
||||||
|
/// darstellen kann — und damit `init` bereits einen Dialog öffnen darf.
|
||||||
|
pub fn start(&mut self) -> Vec<String> {
|
||||||
|
self.fire("init", None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Eine Eingabezeile verarbeiten. Im Dialog treibt sie den Dialog,
|
||||||
|
/// sonst ist sie ein Engine-Befehl. Dieselbe Eingabequelle, je nach
|
||||||
|
/// `mode` unterschiedlich gedeutet — so steuert Terminal *und* (später)
|
||||||
|
/// Klick denselben Dialog ohne Sonderpfad.
|
||||||
|
pub fn exec(&mut self, line: &str) -> ExecResult {
|
||||||
|
match self.mode {
|
||||||
|
Mode::Dialog(_) => self.dialog_input(line),
|
||||||
|
Mode::Free => self.command(line),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn command(&mut self, line: &str) -> ExecResult {
|
||||||
|
let line = line.trim();
|
||||||
|
match line {
|
||||||
|
"" => ExecResult::empty(),
|
||||||
|
"quit" | "exit" => ExecResult { output: Vec::new(), quit: true },
|
||||||
|
"help" => ExecResult::lines(help()),
|
||||||
|
"kv" => ExecResult::lines(self.dump_kv()),
|
||||||
|
"reload" => {
|
||||||
|
self.game.signals = signals::load_signals(&self.signals_path);
|
||||||
|
ExecResult::lines(vec![
|
||||||
|
format!("{} Signale geladen.", self.game.signals.len()),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
if let Some(sig) = line.strip_prefix("signal ") {
|
||||||
|
ExecResult::lines(self.fire(sig.trim(), None))
|
||||||
|
} else if let Some(name) = line.strip_prefix("use ") {
|
||||||
|
// Objekt-Interaktion simulieren: Signal ist der gestrippte
|
||||||
|
// Name, $self der volle — wie der LMB-Klick-Pfad in irl3d.
|
||||||
|
let name = name.trim();
|
||||||
|
let key = signals::signal_key(name).to_string();
|
||||||
|
ExecResult::lines(self.fire(&key, Some(name.to_string())))
|
||||||
|
} else {
|
||||||
|
ExecResult::lines(vec![
|
||||||
|
format!("unbekannter Befehl: {line:?} — `help` für Befehle"),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dialog_input(&mut self, line: &str) -> ExecResult {
|
||||||
|
let Mode::Dialog(d) = &self.mode else { return ExecResult::empty(); };
|
||||||
|
let sel = if d.choices.is_empty() {
|
||||||
|
None // reiner Text → Leerzeile/Enter blättert weiter
|
||||||
|
} else {
|
||||||
|
match line.trim().parse::<usize>() {
|
||||||
|
Ok(n) if (1..=d.choices.len()).contains(&n) => Some(n - 1),
|
||||||
|
_ => return ExecResult::lines(vec![format!(" (1..{})", d.choices.len())]),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
ExecResult::lines(self.step(sel))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Signal dispatchen, deferred Actions einsammeln und — falls dadurch
|
||||||
|
/// eine Story startete — den ersten Dialogschritt ziehen.
|
||||||
|
fn fire(&mut self, signal: &str, instance: Option<String>) -> Vec<String> {
|
||||||
|
{
|
||||||
|
let mut ctx = self.game.action_ctx(instance);
|
||||||
|
signals::dispatch(signal, &mut ctx);
|
||||||
|
}
|
||||||
|
let mut out = self.drain_actions();
|
||||||
|
if self.game.story.is_some() {
|
||||||
|
out.extend(self.step(None));
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Einen Story-Schritt treiben und `mode` daran anpassen. `sel` ist die
|
||||||
|
/// Choice-Auswahl bzw. `None` für ersten Schritt / Text-Weiterblättern.
|
||||||
|
/// Tags gehen wie gehabt zurück in den Dispatcher (siehe story_ctrl).
|
||||||
|
fn step(&mut self, sel: Option<usize>) -> Vec<String> {
|
||||||
|
let state = {
|
||||||
|
let mut ctx = self.game.action_ctx(None);
|
||||||
|
let (state, tags) = story_ctrl::advance(sel, &mut ctx);
|
||||||
|
for t in &tags { signals::dispatch(t, &mut ctx); }
|
||||||
|
state
|
||||||
|
};
|
||||||
|
let mut out = self.drain_actions();
|
||||||
|
match state {
|
||||||
|
None | Some(StoryState::End) => {
|
||||||
|
self.mode = Mode::Free;
|
||||||
|
}
|
||||||
|
Some(StoryState::Text(text)) => {
|
||||||
|
out.push(text.clone());
|
||||||
|
self.mode = Mode::Dialog(Dialog { text, choices: Vec::new() });
|
||||||
|
}
|
||||||
|
Some(StoryState::Choice { prompt, options }) => {
|
||||||
|
if !prompt.is_empty() { out.push(prompt.clone()); }
|
||||||
|
for (i, o) in options.iter().enumerate() {
|
||||||
|
out.push(format!(" {}) {o}", i + 1));
|
||||||
|
}
|
||||||
|
self.mode = Mode::Dialog(Dialog { text: prompt, choices: options });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn drain_actions(&mut self) -> Vec<String> {
|
||||||
|
self.game.actions.drain(..).map(|a| match a {
|
||||||
|
Action::HideObject(n) => format!("[action] hide_object {n}"),
|
||||||
|
Action::PlaySound(n) => format!("[action] play_sound {n}"),
|
||||||
|
}).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dump_kv(&self) -> Vec<String> {
|
||||||
|
let mut keys: Vec<&String> = self.game.kv.keys().collect();
|
||||||
|
keys.sort();
|
||||||
|
keys.into_iter()
|
||||||
|
.map(|k| format!(" {k} = {}", kv::format_value(&self.game.kv[k])))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn help() -> Vec<String> {
|
||||||
|
vec![
|
||||||
|
" signal <s> Signal feuern oder Action direkt ausführen".into(),
|
||||||
|
" (z.B. `signal Cube`, `signal set has_key true`)".into(),
|
||||||
|
" use <instance> Objekt-Interaktion simulieren — `use Mushroom.005`".into(),
|
||||||
|
" feuert Signal `Mushroom` mit $self = Mushroom.005".into(),
|
||||||
|
" kv KV-Store anzeigen".into(),
|
||||||
|
" reload signals.toml neu laden".into(),
|
||||||
|
" quit beenden".into(),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
// Fehlender Pfad → leere Signal-Table (load_signals schluckt den Fehler),
|
||||||
|
// also keine Asset-Abhängigkeit für diese Tests.
|
||||||
|
fn empty_session() -> Session { Session::new("/nonexistent/signals.toml".into()) }
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn quit_sets_flag() {
|
||||||
|
let mut s = empty_session();
|
||||||
|
assert!(s.exec("quit").quit);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn builtin_command_mutates_kv_and_stays_free() {
|
||||||
|
let mut s = empty_session();
|
||||||
|
// Unbekanntes Signal fällt auf den Builtin-Pfad durch → `set`.
|
||||||
|
let r = s.exec("signal set has_key true");
|
||||||
|
assert!(!r.quit);
|
||||||
|
assert!(s.game.kv["has_key"].coerce_to_bool().unwrap());
|
||||||
|
assert!(matches!(s.mode, Mode::Free));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unknown_command_reports_and_stays_free() {
|
||||||
|
let mut s = empty_session();
|
||||||
|
let r = s.exec("frobnicate");
|
||||||
|
assert_eq!(r.output.len(), 1);
|
||||||
|
assert!(matches!(s.mode, Mode::Free));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user