From 1e79f2ffb2f30d522d6ed26147d9d4e4141f3ce2 Mon Sep 17 00:00:00 2001 From: Eric Wagoner Date: Sun, 23 Aug 2026 18:25:01 -0400 Subject: [PATCH] The terrain moves in: pits open downward, hedges stand waist-high, ooze is jello MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The flat token billboards give way to terrain that inhabits the room. Pits, slime, tacks, and the dimensional warp's rings are painted INTO the flagstones — the home-tile decal pass grows into a general per-cell overlay grid, one texture per cell, alpha-blended texel by texel. Thorn and rose hedges fill their square wall to wall at half height, drawn with a near-bias so the wizard standing among the thorns peeks over the top. The killer ooze rises as a full translucent cube of jello, bricks and bodies visible through it; dust billows; and each dimensional warp mouth throws an additive pillar of violet light up from its floor ring. Billboards learn aspect, translucency, glow, and draw-order bias to carry it all. Nine paintable starters ship — four floor decals under textures/, five standing volumes under terrain3d/ (spec updated for the artist, hedges wide, jello solid, warpglow as light) — shown in the token workshop, rehearsable in place with /?fpv&terrain=thornbush@3,7;jello@4,7. Co-Authored-By: Claude Fable 5 --- packages/web/public/terrain3d/dust.png | Bin 0 -> 15066 bytes packages/web/public/terrain3d/jello.png | Bin 0 -> 2560 bytes packages/web/public/terrain3d/rosebush.png | Bin 0 -> 9735 bytes packages/web/public/terrain3d/thornbush.png | Bin 0 -> 8915 bytes packages/web/public/terrain3d/warpglow.png | Bin 0 -> 3021 bytes packages/web/public/textures/dimwarp.png | Bin 0 -> 6398 bytes packages/web/public/textures/pit.png | Bin 0 -> 1921 bytes packages/web/public/textures/slime.png | Bin 0 -> 1721 bytes packages/web/public/textures/tacks.png | Bin 0 -> 1607 bytes packages/web/src/TokenGallery.svelte | 21 ++++- packages/web/src/fpv/FirstPerson.svelte | 85 ++++++++++++------- packages/web/src/fpv/FpvWorkshop.svelte | 9 ++ packages/web/src/fpv/raycast.ts | 56 ++++++++++--- packages/web/src/fpv/terrain3d.ts | 87 ++++++++++++++++++++ packages/web/src/fpv/textures.ts | 63 +++++++++++++- research/fx3d-sprite-spec.md | 18 ++++ 16 files changed, 296 insertions(+), 43 deletions(-) create mode 100644 packages/web/public/terrain3d/dust.png create mode 100644 packages/web/public/terrain3d/jello.png create mode 100644 packages/web/public/terrain3d/rosebush.png create mode 100644 packages/web/public/terrain3d/thornbush.png create mode 100644 packages/web/public/terrain3d/warpglow.png create mode 100644 packages/web/public/textures/dimwarp.png create mode 100644 packages/web/public/textures/pit.png create mode 100644 packages/web/public/textures/slime.png create mode 100644 packages/web/public/textures/tacks.png create mode 100644 packages/web/src/fpv/terrain3d.ts diff --git a/packages/web/public/terrain3d/dust.png b/packages/web/public/terrain3d/dust.png new file mode 100644 index 0000000000000000000000000000000000000000..fe67125cd83145a88d38939290322fbfacd5a501 GIT binary patch literal 15066 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 literal 0 HcmV?d00001 diff --git a/packages/web/public/terrain3d/jello.png b/packages/web/public/terrain3d/jello.png new file mode 100644 index 0000000000000000000000000000000000000000..694248b9dbe027553f8dd917d2f99fa80c5d9896 GIT binary patch literal 2560 zcmV+b3jg(qP)yo3m6>eX^#>U3D-IwXvBvV^c$$R8)@?3e6RGuJ7Ret6b@-y2twY#&^-P3O0!CZ{- z>U1@;23sI(E&|Tfs0GrIbk27^1U^F4FYsTb5kLY|+Wi+7NB|^2#Sy5u`8We00V<9_ z#m&bV00~fW1S)F2XEx3N9ugJJkpLBSyk|BNz(b;W{#fdCSq8h)l)i3FfRQ_hN@RY-s!A{9GF0#wZ5L35D+K}0Hcjs&Qf!-M7` z0fLBB>>LSDF^31uJs`lp|M%Zdn97SmBmsf~cwPfO7hk*(xLe zGgRdiL;{r4ugq2<0hpmGryvrboPK4t>TCl1_2XYI(@4w~px{{uoK1kUM#sjfBM>zJ z5+DkplGh^vO77q&^N;{h2$j4Z2~cteN12BNh(f63^+*6l2RpXYuyjkXjZKrWNg>3L z01<97HA{T{X{vnvaXNVUvgnUL&y=ch&FQ zi`r^xNRx+oFQj>{Rjh*}0Vqgqoo#*gRg)>zuf?Wji>s+Fy`B{G4-$alIDNiuQ>uK$ z8l#-A5l1Iy6X2is|C*$c^2W$E{nI{8AJ^H{x3<%u)$?9{HUW4y@KpVF?ykn#cDLKH zigG811faO2DK@v1s)ORDC@{+&G|ssI2|#gxB)4ozm9JRpwX6%~xx&|!ixVUO#SLw_ zWlMeHTuMC@*Xp%h|FUbqaZwWzAjEUSTSe>i+tx9WrdZ#O-Yd&yj9Z1*Pn_oquMKM+ zR}KjfftY&R*RH-(mhwo5L=i#GCBHI^mDL*m zJX5q8Tg(C4C7qA}Nh2EQPhYIt5H=1J`%V{Qt!wF);494N1PPEb(8~#1sF=v*vbb~> z0SwI&$3IV%FaLkOgapTiW{N3eTA1$Ps)k4*i^33j+-|mK`GaMgpv=hwt`}x&N><81 zaj#Q2o<1ylu!`oD@tngNKRkr(TsM_p@{Y5O6(RE=}3RSf|#}+GJU7 ztW5Fik1qpQ;~$fUu!sAX;rw}-xXy%Cv0}s!SI80v80#bgyiRT8DHb|7mf#5&WRBJ4 zT?V(P>Ep8J*skyD)aPb9>GgfFEinH~1YCp`d-xgT_tUAzClSEi?jqZ<$egP5Iqg=H zHT7W&*7sp{IH&*pw#}BXZFhvtei`i;;tREUJ=Tmt>nl!qq$7`Y)({(!iytg9$NEmQ z3?8yJR`}AbsoRM@#41;^UaMb(Iu@degKJq&({@_lClLUqwlHQ3BusiimPmIYw+Wg; zX_oI89`;yd;^+vo!05fYq)qQFN!W}?fGLlwAM7}Z0MKwiLZrkf-#rdqqq1~Mv@EAN z{ybM8%tL~s}!V021kj%?FWi|Spx@?Hq z-}^n?P9{L3*>H(bi@}YoOJu>MP2%L_;jiB6w(NTCw|m+0Mejf~%o{N8TvjFO>K#ng zoxT(+%p1oZsn@m#*L{8XPSKR>p57)aj&a(jA$u|bJY~8{#~hN`0B#F{*!0Z^o{XMj ztlQJU%ayC%8m>U^+{?lF>aC(RnswJJt3rM@0X&ex9Mnx$1kvu2C=OmG9YeYMvMYx*_+@t|FPI zDv0gUMIbQ(z?(7fECBb?eU;&<7p}1VGCbx7fy4*^Cc&?Pci)d@aDP>A`r6Hh;T)cN zp$-$D2qa1XFcAV8aDVM6Z3|`vD8s}r0*MoVUsF-DV+bGt_E6c8&^5fvpw0Al*}em? zXQuBE3GfDoy|#p>PcJI$2!{6=;5z_myd&^795)#f;LR1Y=T(=4vTJXruNk}}U@Xnn z>j`Inl2Qi=K#+8-O@kJ#G|0?AL`wS~2>`CG`7V4YsZ2rw5Ld&`Re|z4mPA%JiAZVx zBLUuAJGky?^=8et5(sPd5Yu=QDP@KPcylYfwDIoys~Lo|l`C1l`n|ePu6wz>ruC*$ zCK(bS05gQMWwm3%`wfArQl1u$0I-n*ezU{tXQlK=#zqx`03<@UAivWeet5owjU3}& z7X7DB6BRz-nB;5R8DoFaa7j1fo&a#AKYyMn+T3XG0xwr@ET;*ZJa~HXG-(H=-5>V^ zm_03J;%MJ4o**dj;wjDqK~&hzxh8<2nWDAcFzuUPP1l9~Vkeusl9%F|0G4huIUisl zvqW=ihwu6UIZS>8xF&$JiMhSn$$o#mAI{>PaZP|)y%u=v3kSHiw;?555^FsKxF$f{ zU<^d_Ji{<&Lx5`nbb80X(Ra)=c>DNRd0EQRvV&^^G=zq$Ty-Pk@vyyG8OiIkKq_;; zToV9*m({KrMgq7^T;{mF8gwjj;+_BiE|B2%!*C9J_}X2;hCTJLXaoBXVAT%R^Fc1{ zwA;G1#mlM#-xqqge;z)58mrT%{J-{jj@^YI02l~+=wXM^<$LvYPdzN#X1P{x8+P;Q zr^!H@8j@qX4fx_Gd;-o`=Kr-%z~|Id7y_JXU=r!OI^Xe5B=y^!2kSxg^}vW$%YvqeZ(+*Kg~7+m6RU1prN4!bPF&=+aT zFdLS(B0TqBZ_qz}5!Aa;un$cj0fHVpa^C6+nsMPj?@!0zaS3XThq01VEN2VIk8 zVMg{5@EW;sY3!Vq9w^~8@_k~5y<`7pw$sJ z!#iaKK`O?sxDrDG#Bc|MoFM{v_wj0a_i;3R|Htde=sR^;0?0b&qfU$kfsd;7ZY zA0q+w(ctJfD^-vHSs~5oI7k3a9g~$>NPw)6=5!n+0H==0N-ZQnR!DO?4ibP<$7H1z z5+Ez2IUNTHz?oyRRtE`?HR7C&hXmlvAz7<~1jrh3&c;ImaORM#)jr6MxHx%{?K`V zKO29=&^a3~96I(HuE*co<*(ZTHeNV<;>HVx{`=TH!@Din+ll$pfRBmrXV~(tu?vPD z+IYc+zaKk)_>X>Xwebv`_?ix|>HPi;l;Kd&q1! zckqIdbA~=Xa?ao`z%MAIf3=|;Es>YB7j~+^cXJvo48ZaYg4}C(I zKZs!m0F~4sB=u)KTPq!i2;C4mlxXl`TnsNko8MX*Kc?@d&Bu3l|JDZh|F~mAEU&0r zALAm|@&bp5khDcPG-WfrscwwJTNT^6`xmndhqu#bMsKFcGE<;2pk>0*5FiqCv=nG$ zjE}y2=wP{K@Fi)K_qAwA%C!#^v<2`Z@XY}pnb=3d!oc`%#Ft{ic`mT^zJAo ziXQ^4*&IM#UrH6DPDuV-Mo40TQ=V!Y;F~%w_?}*84Rmp`tfSE@>PZ8DKZI}dEs*TV|Wm52uIFIebIx=JiVztv0TrtQ5az?Hfm ze{klTF_EW*dPEr^ntxi2>l&%DC13a{k-QD8KLF@)sBoMxUzPOCPmbPs#Cf3q#>p#& z??)%YfK?MSe?*;fRiQPQBd7F?51le_T@;z0zSL}X}9d}jZoL6tO!u1TlV5PPWjOmZpA=Mx<{L=mt4atpQG(>99 zTr$Lz0rcZ_FiFhps7*7lz-DG*@b5`4YJq5p0E~J(-QB*JXJ!xoIyk`t#^+w(+tJ{) zvKXC2gWg7jpI71&^E*KMVk-0=+I;cor^jBk;U#w;bNch{*?#)3LBH9GTrX^Y@XKwd ze`su5|A?^UpS*uzDn6E>CHy_;eumD`a6Jj4bACvl8rX{sSleJ%&-Budy5|!xd_Rod z&djDPQJ|?X&1o)D+Jt3VwZJYu&j^2cUy!>aV%bz>D8h2tHGI@ggx?3X8V5`ug8^2J z0Nb|pXN)gb6Vt7G9-R3>yuHJjPIhIJOtRvI59BiSEEaCrre3zb4n@z_(^jvWxz+iN z+#!G9GEm0=t0w4fRsJeWrtc2S(v`grCvE~b}8~XiQhuCQUlVmO^sNSiWu{6O~@@tSmTddGFvcM^9y(a~Mb}~n8EPC%C`&U@iZh(2z(myI z2@uOGtfG-r$WrYK4$&06+QV_R}80z2=sm?>P1N2S4%X4@l_eO`BRtK<(}L&eAOr_2Y-Q zK^xgCwp>2`;FilbeP`?Co32H9{^s{=Zh2Ylds*Kotmzz70xEI=dDFp&stzC)Hn%W% zgr>T*7+@lV36VNRr62a0Oal8VL7pLQW1qU1tKptvPl1@Sk~T%^^_FAw2K2u6Y%z28QYkmYd~8`LP;3A2eQ3{-&_cFBJm)w z2*9Rc{qnj4)_EoB*C!6jy+>a*{_d@B80kly22L6n9zMD6wZkX(f5bO@18DkzENuf0 zSC0S|=7|WGMU?0r4Vx$BSxhMcf?4`$4-J?(Faifj7-KxwQ2AL1x(Z?9G_NRkzyUl) zW39D@8%Os!`HBP_AbHTs-}eRC`|W%0r9y>XDbZtRAgKli7(1rtl_chfJ@-%P*T#x? z^HOM6GlmqzP)H2-)^*g1Fny~+*m&W_^oCRVju|>-=-A;ihX<5?|F-io(AHIo*MVV< zc0O?Z6F(z17vWy>BsNzUK!0Be>ST3Y**aRKnquZRi68uM{FNhL!5!z{1wZNu)2pyk z^{cDi{5#=7ePqOg_Wer3cxf;&95YT@8;^39qGhKO=70?qVb_xRi3&n8Yq2huTOXbL`oQGkoxQW!&y4Pyd)?^$ux0aIijgx>l&*0a?fZ%Q zX1+{8sK&2)qtctf=)h)uRoYk2lO^5#CXDtM>%;Bi4_&wW3meWDJb(DqfqxF?@{izL z|1KN5i=yc6;nN1b|AGDo-d*2cr7pzJQJifthsaCV?dsS$(2B&AN2$Ah5@l8|p^#~6 zBOzjm>{z19zaZHkz~QL$aP5(XD3IWy)Vw~I1c{6q(?;m3LRqC0$VEAv#xQANE~GFE z#Aiat!c3Skd7^-c3N`{hjjlpKDJ?`vnz8DYrTQxQ?sP8v$B~1JH;g?s|Ly*nWe#OK z4f!MWO?K>{05kV?@7}G-t7p@md*)^uh)oE0z2~mb8fnDdNk@ye;Ur9 zn`Q~L2tanCkg}6uQt<|`(uwuEr~xNvOAQ1X_Ek*vZWU3mII6ke?q&^6U@#!ryrl@B zj(`bin;LM22VZ62jpXhE7`ZT0Bng2eup~qQ#zP)M>0Crq05R50x1WQokwsgh%$Nk( z&?exJDFs`wdL}yMNltvzlhCjh7pFO!$zYKA8sgKeJo1Uh??jBc7|kCCxMbDv^ETm) zCQD(M`4KMK*DUVMzLl6yaY8M!Nv~+{YMaUDDN5Hn!S2q&S0m4S3c4@9f;aC(mc_FR zlJExnT)hPBlv0S({Z2of>`heOz%-ILwy4HoCJsp>S&cIpCxMKiNhE@GNpKldXNU^c z?eZv(8zOc;#Hz_T0cypCkQfet^O6X?p`Qefg;4T1eZa3tJzkDxqnaoHLkH-iD?e2j zL{#Z5E`?nRg@v#?KU?fCOozQxvvn(dq3d^_ zz`|EUU*ERVp)ni$$GDAe7c1|0=(@*VJimMSgaZ&m@@q^K7<`1o{6Y2;}nM{dJ-MDp9O@Aw2Itdk zNg*(F28Ql|qovTg=_ryzRABN8vteI;sd#CuJBr9?1XzgE4hR0=Cm*}1_}%&M({gn0 zqqjY^9ZvDOhi`cN=E2hjUx;0+x9|)z_d+iNfI$V+`!F^_Fgs%dcj@}xz4Pad-F@)V z(R)8~?ET+5{uQS@jPTBD{-7i|@$6ibFoS+|C%X>I8B||0PMPeApw~r#5c8_)2KN9l zdea~ZoKQJ4CSYP1yIgN5*kkenH~}?zU^S&$XocVaj7La3NqIsA@|4#Ay(T9!QMx!A z_8`=^HSp^QCKOihwUPCstbP5^$^B{oCxuzz;$sfW5#Ty3XZlAbSlVlexccE}?7R&x z@jdz?G4&8r8`i0T6;nVBf~fnz(!|mYHS{sn?zAVo!r6p}MP83$)db^m@ySAmO*7{X z=DV|dm-`(&neSUZQ0UH27rVm6h#f4ToDVrf87)POV{-LnFQ+pdZZ}^>-tX|m0wZ0ZW6F!1% zCsZA~bjjHGt42=2UU{KdO5tABX{~fC;5E7fs?)!+@^Vv!O!l$mfoQUrE_xDRlYma1 z4avi0I{`>bQQBV6mNX?yEnax4Z=^kZyX)g#eiR*-Dm_6=c z-?SUgq!pms44j}3ccB3oyp0#|PSOU`Fj@j2PhQfn39w%c2q3$J&d6eSD=6-W)y((W zO%Lt^U2pS13a8H%pBU;^bmk)Pc_HXDKu`QVkdL&!LO4Y#aqozgw@(3sNJ=!Sse?FMGU=@znPQ$0G13=uU9DKWV&( zSvsAi@p+An7Z_NdTWBA>!II7qv_!R%n3*`>R3hW|z{z|Y(4xuPeFvh+6ApfNay#G; z>s4E6+qC4SR1h%`;Z5L(`9N;CcMHGe@sX}emqvs4J#l2$&L>7XA>k6ll zUBNM1WH#nP7-nUOOc%rPe=A1-l(L-?q&wv5__UZc9>poE<3Ue?)5A&Qr#y`uO!}{` zi5j0)Rz)%yl0TfF^u{FlZ64|6ysk;>szo%2^;H!#W#0*sGSPQ^!p^Ckm?mqQP%MRCy@zrt42n>?*=7sU=TA9+kz-L+ZJA9`BC!v^vtHd(5dZLb^M{jyy zh`#WnEp+qDv2^((8>nNh)Bs}=l~7vu11HcU0rJ|?LD>^}qYd82BuzVq&Kf!vd@`D? z2VlJBV(1=TCBEU1VWCymTME2Vo&K$UN_^g3T26hZ|kVnsshHTOzOIIB!{Dg;K(pOS&l4Fas+Ul&=2O2)+0~Cna#3_KE352Q&1pN5`vt=LFT- zZ-lT(nY1MNNUsomZ^Ie=Z!i+AOr%xUYW!Am%uL2`Zclq|cG47TJ1ejG?#!azLaEuK zH++S)E`;a`2C0hxLdlesRUKgX?7?>%Yj0zrRsWEanYS6@4& zQM6%3dFsZR|IXkm9KOM0L~8!nBHn-jO}R|MKH<=!SxT_|T>8n7>YjTtOLEJG34GGv zekh2!j;~d{5PFCLY_--LLexhqbs5EO8m#`OGyd6l`dT%P8*7enkL7%jG+hO4T$a8qTjwff-CBH2KX_ zhhfemTpP+bzfvRXuY6kSZdZt4^e`8-b`BUHGnqA>`94Y_pY6}Qd3(k7hiCq2cp~~{ z|1^Da{3&|xQTT0zaa|+v80Q2Lybl@09>Lwz;`-1k3SiwK3Bg5TjVx4aMSsFo)x{Vk zbi-K#A2%dtlxl~N7s;n2T1L_ZC-AE+3*>P~9<`N@A>;ERZs=23O_s}V2lz1tQ*|vw zX9o~htIuN8gQ+6U0|-=F0Bce%w5}U{Ux^=Nk&6{->VY7J$YglEk`F+Jdvd)25(! zGe9&b$MQNZCJ9O@C$*#lkV?toz5j2=_Rx<9%1eRdkGtmS{hRlc1T@EMVJTRztu7T` z7o?bQp@k2tw7{T3`WGk@#54!iGBfMn_UD1;6elK)Lo6goH0fZ&2|r=Z_bojP2iPfL zi4R_@q=$y_+cf-CMbLE>Z$yN$P?w&y9*oh}S@e6Zp4v3@)qTeeeR}>mAO4WH{fcv6 zc*L%um-b#cbV`qVt-W&LzTy zFe?f1vAzJ{1R68!ey@>QqG0amv5XVkxr7qdM(P0bLqyjdv!7o5l85NRWACMpj_sw% zM6n`OTXJpnY1h(Bw%1UNH87zFkcE+FG3jYQ)?_76z(CcZVYR&H^Bh*yNleg^B*Wzp zKU!dMRVlP+l1yOOOB*yTGX*`A(V7A~pDg77UD$>Fm{;aqvzwDI%yn1ie6&pmSI-`K zN}mm`mdNZHd|BVEQ6!ht^n=PpvxDt7?|IJPO?!3>e({MNgMW%053)=DG20b5B$i?Q zuJ*hvzC$Al@G_jft@ZAA!u7gz#BwIEwRO4yR;Hm)<1?;doT@H~T4;j~_k-EEE@LCR zdOU++smtVraVxG)96Nv6OYkfRDNI&nTpK24{W%iR00E!DcI2j+k58N{PQV-&LPftP z%);o{E7A*Nq?CVrKEvA!?J8W;w|-+dBirAmGH{X?Q879T7uZf$NA?OB|KgdUOj~5! zT029}ycRoN&wH@R6T*;+)dGn4T^qS2;+#!d-2SQA!`MWOqZBr#)gE@2$602szA2d= zkBo-*SUc!9R$CuBo=?&V^PRM1xoVE}HH4G_G)n<7b$U5si;J7y(wQmLT@RQqHQ>sh z9+j%)Tz{t2HwyOQ1O`y1*A?uydy>d_!d{8P#6=izUhgClQ3`nV0;Uo}QGnwJRILCQ zAOKB+QBdxMRKUYGi!@$ud%!p6F7!8@=h@{ba8G*4o$KZP7;Usmbw2uadPxxdSkfkB zy%K`M2@`njUMA6%=nHxigF#PXAn5T2yo70EJkT2hAX%H3MUS#Ps55|4@*$e8H39Ti z;&6(f-XG~pD)WWBVv zoa|4|q3^53+AoptUKjF7bk_@ubBZMrntIaDEjAg-k@on@)m`NX$izzE<)vG9JgN<8 zAPhVuK9F20ezkkT_07(IluC44_vGTQlS}iQ6S8Rvae+<2=)G{-9xn-Fd!(<3T#`mZ zRVWje9|J?F6ml=D=OkKc2X2>aQp6oOt{PTb|5N*Z*0LV{XV>J4(|~yoEz&>q?qB=@4FB`= zLP3}UlegWbKas$W511IL0k8v9(I}R5tvGrlM$+9bAQ&J$pa%dSdUERq-ZJ^=HAP(8 zlttGLYs^;TsRO9W%9E>a%rB$@=}V816tH@B>pIq2%akJ2SWBv{G0mDn^7D=;<}z2V zk$t}7sl`hMem(K3p?eN~q-#%h(iHXV)$iIpFijWNwf|1rv|QV>CwwfKE9!rC_DC{U z_~_shv)^x@$yG!}h~YLK_IIc>Vd^A9JzmHOgNqt5)Wu}M6M!aW!f4DWv|0lFh*rYf z2D3QBoILu6rKAhIB%qW^{gv=oj$R3O3}W05V&taiFFR^{xGM8#dYP6yeQU3hs)!9O zsk1r)W`~J6)MfggH8D{dKYAo__BY-APLP~3%IHb~?^Y#M3D^-;ydGiV2=LB_Z+>`w zxe0^ zG8+=5B%*VYwo_11Wm0p0St!E9vG3aTVi~8 zppBMowlGtif&*MzVWGblRB$MTy|kC~JN@=Vm*0_S_u8NgzrttcP8cassW&Yd#vuoa zp?gRP_Be`W$)?EIjF*OBTAjg97^a*McX?|&Gdg6(tVP_L6WqTj1!?XCOvHFJC*WmR z8AEBb@<0!?mJ?{GzZDK}ExsP!G<<;e!6~!{H;f_x=$&jn7WzSR$xvh*QqCssnpmW) zL~HE+lC+g<32B*2xERgA*ptPXaLSca#ffM!T#!ia*mwKH|EaVT;Vp{$8NWR>oEpIU zb$24;ci>;U4=gzl^dm)a02w^R)fWn#hCLL`k+p#+SS2nJzd zD5DLN$WTa7GIa})@jQEDyb&Af5Bh>G{G!$Yoa4L-4#Na+qpGF`000G~NklP zjB^5gQ|y6;&H(c>G196i6V0RRTr(j76LeK@8r7|5X%8VFWW-tM2zzqa4wG_(5MJ<00WN+;&qgQZeIY-89dAph87rFwDj;B3!Fp{ zK`l0u1wBiK4=O#^XGWAB)?Dn0D+kef6vFQ_!-W)>x0NnLQ$LpP>;A1fR$) z9Km8o3hIP8tRxF#qvqxW4!YIRN?%;YY`p7Drt4ZEd<8_Ygk-eG{#j^6F;)=8xwcntK&6d=>Uu{4Vb~%IM0R( z0K!}yFwTE!e7KRc8Uev{MBou)YSVRUwDGow-0Jaaan?YTxRjW=Ptt6yK=(QEM zM6WtPsaneS-nw_$lJ`0yzK_=VFp*&ThCQBVkJRAPY-rXPUc$m!8km`nN6R%QT0aeh zYp$oYsxxcUu|KWyg`Qi*fk;FVio=oO{nQ)>HJu9)8`jrGsyuMS61u)sp@+4N8x
    Y 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 Hu0mjf<%oiK literal 0 HcmV?d00001 diff --git a/packages/web/public/textures/slime.png b/packages/web/public/textures/slime.png new file mode 100644 index 0000000000000000000000000000000000000000..2427410259ebb682daccd7a54d3b936c2712663d GIT binary patch literal 1721 zcmV;q21fabP)K9KiQq$>FKsVojj~>Q&WypL$*0)1LE~ z{~G{!%oD&FWWXQ;9&-YNTVO2{;K{&p(AJa zj-y@x=@|e5CpZFjop_rrxF~YQ5%#MMq9*_dg1~h_cj^NK<2&_t+ujoZ1ksJ7GM+5r z_7RaGQU?;UU1wDQy!+YiF*Nb{&V%oN^ZKW=Gr>fj=I55*Y%YaksI?qz?C8AfZaZ}5 zts`U+w~q*WsRIcePRF1FqP zl0bQ2@5Zk=$(iB0UQ#}2)mHukF|Ivpn8f2xHz*ufYd)i^V zJ++VHn`vqBhm0vQ?+U>FFLq3H3S3?IpTC!Wm+PA6HIVOnqav=QUoGP(-Wi5TmMn$y z+$L(DX`ORJwawN`0gzpHB4;8yVW(>#5NN&OJ&XLhw?V}9@H&610FgQ3$O&3OG}Uaj zpW!wEg7&PCfh)>TdmC5-Fx_?WVR*zJ-T2(PS5Z5Pf+UE^uocY_oY+VSkXp@aMC~{L z*uq?JwH-x)^aFvjBAH`M+KS5K`@TP?v#|m|@Q%f;A{z>e0G6q1}+Xg?nX?Uw{ab4dN>+9)QAl#pC zO=fL@z(JXJOT>(U3s3@qbOi(=NZJu~5rLE5WrAleUcLGKLEThZ0A*6@=Yi=uS^#eBS_7zg zo_p)wtr=A{&)2SF>a2~O>Dg7eW4~VedFrP>ez>FRx9h*$wQ)Yrc6h`eRh4J6qRsG) zwQ>L}oT%V}x5+Ipt>42dwJO@^L!Fi}IScw00J}|)@=BZ+Bz%j*QNzfYu8SxgIR!d6RD1> zcHP$b5k;VUB&jDKQ20)uigx-<)+u_FYXPVW5Hx97Ptr{Ub?(!uK`=i7tLta$^qW8^ zdX#GceD~3%Yb;FgI7K~AfT#)rSw`CDt&?p7g4pspUFJQe}eL1k?XqJ^d}B00O@;y=V*yCX}_rG{>M)%01QY|>LG!U`ij#U zSp#6MbmD%8VCY{d0DTW32|nE<94%030L+^%k}O8OU&O(e`eo=)R{&TeU93+a5Im1- z{4!P709XhJr;lDa>Z~D%GQs0}B>Kf-pkw59-7D=3?J2LNnn zJ&Lx240sfG)h4Dv2H4Pg6m1{+4Cr=K{sjO4|NmoiZc+dM00v1!K~w_(x4Iv_hJ>di P00000NkvXXu0mjf`(`YX literal 0 HcmV?d00001 diff --git a/packages/web/public/textures/tacks.png b/packages/web/public/textures/tacks.png new file mode 100644 index 0000000000000000000000000000000000000000..5109d72cb406077d0fb85dafb5e68361fd75042d GIT binary patch literal 1607 zcmV-N2Dtf&P)Y z!4g^c8ZL_&la*xMpzGUqiTz+jbcumzL(H~zd<+NLN`dvh-UD3na!dQZg_ib(5}w?9 z?m74LoadhRR3Rve1q%VPWUI6KvVEPhBUjpl#fuvjlM&qBE z7K_seNH+gTApny0`~Ahi;M8u5#r2`Z((^8i@vzx?p0(MWJNWT<%1I%hqGHEXQ&Xe1 zqvO&zCE*K<#jrRm7U$Mbh;)QPs4kuf^S;79m^Vm-eCW{OfDc>{T%QSo^O)J}dW+ZbGRDE*o12@o=gzf^ z^E#=Vas+5Jfo-AC!)8Jrel86Z`WWM!sj2Bt=;KiYIU4l((;xf&e&X}_j>2O%l5NTn zkddL;==FNV@$rc#_@MjxekrwD%}?>;ybSLQlqAWEl(^96ZBvebnwsj@+S*#ToH)fnvS>-(>!rs{oOCzYki!#KvMG*T6$904qN2pU!r5&%FS#-I;vq=Y~ZuiD`- zEWBxg4MR#107uS%Y3F6cJ#t*tgT?>RyU{N*yo9F47(VR-3MR#Jxkb z&yv$AB7m;MmSAwYMHI2@<45iTqv`qge~@7hAh&mj=2K3mhyaW;@U3(Z%Hw3QTlGk?1n_regv56Bq$eM# zECEa1nc=Q1?^+!evIM}Lmk-lIolA1!Ao5=MX^I9@fI9WT=z_un;edxpT2j`m#cJ+)GlTM=&F88 z@ukUhbC1*!->1psdOUyR({U{YvfJ(1g5W#?FdChY!Wa)i#N7V=esN@E^hsV9Nv6kR zFF2i!Kx=Eu<6sjjVkSVN!8@m?XO0p=q8>NXz({+0dsc1jiA@lcPN$os?Jh4dVO`lty{ll+`j$S zqDP=~KC?}BtC#@3)R@dC04qCRDLDw?jR+xtpSDO8v4hQn7RHhd#RMn~RGdFQr1lTy zK^y!ILI9L

    - {#each ["wall", "rim", "stone", "door", "firewall", "warp", "floor", "ceiling", "home"] as name (name)} + {#each ["wall", "rim", "stone", "door", "firewall", "warp", "floor", "ceiling", "home", "pit", "slime", "tacks", "dimwarp"] as name (name)}
    {`${name}
    {name}
    @@ -120,6 +121,24 @@ {/each}
    +

    The standing terrain — first-person volumes

    +

    + Terrain that stands in the room: hedges filling a square to half + height, the killer ooze as a translucent cube, dust billowing, the + dimensional warp's pillar of light. Each lives at + public/terrain3d/<name>.png — transparent PNG, any + size; hedges render twice as wide as tall. Rehearse them in place with + /?fpv&terrain=thornbush@3,7;jello@4,7. +

    +
    + {#each TERRAIN3D as name (name)} +
    + {`${name} +
    {name}
    +
    + {/each} +
    +

    Walls & doors — as the maze draws them

    Wall-segment effects have never been tokens: locks picked, jammed, and diff --git a/packages/web/src/fpv/FirstPerson.svelte b/packages/web/src/fpv/FirstPerson.svelte index e4ed672..a2a90c9 100644 --- a/packages/web/src/fpv/FirstPerson.svelte +++ b/packages/web/src/fpv/FirstPerson.svelte @@ -6,6 +6,7 @@ import { castRay, billboards } from "./raycast"; import { materialTextures } from "./textures"; import { doorOpenness, fxFallback, type FpFx } from "./fx3d"; + import { terrainFallback, TERRAIN3D } from "./terrain3d"; import { tokenArt } from "../art"; import { PLAYER_COLORS } from "../colors"; import type { GameView } from "@wizwar/engine"; @@ -88,29 +89,42 @@ } let frame: ImageData | null = null; - // Home bases wear their owner's emblem on the floor: a per-view grid of - // owner tints, indexed by integer cell for the per-pixel pass. - const homeCache = new WeakMap(); - function homesOf(v: GameView) { - let h = homeCache.get(v); + // The floor wears decals: home emblems in their owners' colors, pits + // opening downward, slime, tacks, dimensional warp rings — a per-view + // grid mapping each cell to one overlay, read by the per-pixel pass. + interface Decal { tex: string; tint: [number, number, number] | null } + const decalCache = new WeakMap(); + function decalsOf(v: GameView) { + let h = decalCache.get(v); if (!h) { const bw = v.board.width, bh = v.board.height; const grid = new Int16Array(bw * bh).fill(-1); - const tints: [number, number, number][] = []; + const decals: Decal[] = []; + const put = (x: number, y: number, tex: string, tint: Decal["tint"] = null) => { + if (x < 0 || y < 0 || x >= bw || y >= bh) return; + decals.push({ tex, tint }); + grid[y * bw + x] = decals.length - 1; + }; for (const p of v.players) { if (!p.home) continue; const hex = PLAYER_COLORS[p.colorIndex] ?? "#888888"; - tints.push([ + put(p.home.x, p.home.y, "home", [ parseInt(hex.slice(1, 3), 16) / 255, parseInt(hex.slice(3, 5), 16) / 255, parseInt(hex.slice(5, 7), 16) / 255, ]); - if (p.home.x >= 0 && p.home.y >= 0 && p.home.x < bw && p.home.y < bh) { - grid[p.home.y * bw + p.home.x] = tints.length - 1; - } } - h = { grid, bw, bh, tints }; - homeCache.set(v, h); + for (const [k, content] of Object.entries(v.squareContents)) { + if (content.kind !== "pit" && content.kind !== "slime" && content.kind !== "tacks") continue; + const [cx, cy] = k.split(",").map(Number) as [number, number]; + put(cx, cy, content.kind); + } + for (const w of v.dimWarps) { + put(w.a.x, w.a.y, "dimwarp"); + put(w.b.x, w.b.y, "dimwarp"); + } + h = { grid, bw, bh, decals }; + decalCache.set(v, h); } return h; } @@ -162,8 +176,7 @@ continue; } const d = (H / 2) / dz; - const homes = below ? homesOf(view) : null; - const hp = below ? pixelsOf(textures.home!) : null; + const dec = below ? decalsOf(view) : null; const tex = below ? fl : ce; const tw = tex.width, th = tex.height; const tp = tex.data; @@ -184,17 +197,19 @@ let v = wy % 1; if (v < 0) v += 1; const ti = (((v * th) | 0) * tw + ((u * tw) | 0)) * 4; let r = tp[ti]!, g = tp[ti + 1]!, b = tp[ti + 2]!; - if (homes) { + if (dec) { const hx = (wx - u) | 0, hy = (wy - v) | 0; - if (hx >= 0 && hy >= 0 && hx < homes.bw && hy < homes.bh) { - const owner = homes.grid[hy * homes.bw + hx]!; - if (owner >= 0 && hp) { - // The emblem sits over the flagstones, wearing its - // owner's color. + if (hx >= 0 && hy >= 0 && hx < dec.bw && hy < dec.bh) { + const di = dec.grid[hy * dec.bw + hx]!; + if (di >= 0) { + // A decal lies over the flagstones, alpha-blended texel + // by texel; home emblems wear their owner's color. + const decal = dec.decals[di]!; + const hp = pixelsOf(textures[decal.tex] ?? textures.home!); const hi = (((v * hp.height) | 0) * hp.width + ((u * hp.width) | 0)) * 4; const ha = hp.data[hi + 3]! / 255; if (ha > 0) { - const [tr, tg, tb] = homes.tints[owner]!; + const [tr, tg, tb] = decal.tint ?? [1, 1, 1]; r = r * (1 - ha) + hp.data[hi]! * tr * ha; g = g * (1 - ha) + hp.data[hi + 1]! * tg * ha; b = b * (1 - ha) + hp.data[hi + 2]! * tb * ha; @@ -306,16 +321,21 @@ rise: 0.3, warped: f.kind === "projectile" ? f.warped : undefined, }, ex, ey); - if (s) sprites.push({ ...s, glow: true, alpha: f.kind === "impact" ? 1 - p : 1, fallback: f.art }); + if (s) sprites.push({ ...s, alpha: f.kind === "impact" ? 1 - p : 1, fallback: f.art }); } sprites.sort((a, b) => b.depth - a.depth); for (const s of sprites) { - const img = imageFor(s.src) ?? (s.fallback ? fxFallback(s.fallback) : null); + const img = imageFor(s.src) ?? + (s.fallback + ? (TERRAIN3D.includes(s.fallback as (typeof TERRAIN3D)[number]) + ? terrainFallback(s.fallback) : fxFallback(s.fallback)) + : null); const iw = img instanceof HTMLImageElement ? img.naturalWidth : (img?.width ?? 0); const ih = img instanceof HTMLImageElement ? img.naturalHeight : (img?.height ?? 0); - // Conjurations are painted art, not light sources: normal - // compositing keeps their inks true; only the fade is borrowed. - if (s.glow) ctx.globalAlpha = s.alpha ?? 1; + // Painted art composites normally (inks stay true); light glows + // additively. Either way translucency applies. + if (s.glow) ctx.globalCompositeOperation = "lighter"; + if (s.alpha !== undefined || s.glow) ctx.globalAlpha = s.alpha ?? 1; for (let col = Math.max(0, s.left | 0); col < Math.min(W, s.right); col++) { if (s.depth >= zbuf[col]!) continue; if ((s.warped ? 1 : 0) !== warpedCol[col]!) continue; @@ -336,7 +356,8 @@ ctx.fillRect(col, s.top, 1, s.bottom - s.top); } } - if (s.glow) ctx.globalAlpha = 1; + if (s.glow) ctx.globalCompositeOperation = "source-over"; + if (s.alpha !== undefined || s.glow) ctx.globalAlpha = 1; } // The ghosts of walls you know are lies: drawn over everything at @@ -386,7 +407,9 @@ fallback?: string; } function project( - b: { x: number; y: number; src: string; scale: number; rise: number; warped?: boolean }, + b: { x: number; y: number; src: string; scale: number; rise: number; + aspect?: number; alpha?: number; glow?: boolean; bias?: number; + fallback?: string; warped?: boolean }, ex: number, ey: number, ): Projected | null { const relX = b.x - ex, relY = b.y - ey; @@ -397,10 +420,12 @@ const screenX = W / 2 + (side / depth) * (W / 2) / Math.tan(FOV / 2); const wallH = H / depth; const size = wallH * b.scale; + const wide = size * (b.aspect ?? 1); const bottom = half + wallH / 2 - b.rise * wallH; return { - src: b.src, depth, warped: b.warped, - left: screenX - size / 2, right: screenX + size / 2, + src: b.src, depth: depth - (b.bias ?? 0), warped: b.warped, + alpha: b.alpha, glow: b.glow, fallback: b.fallback, + left: screenX - wide / 2, right: screenX + wide / 2, top: bottom - size, bottom, }; } diff --git a/packages/web/src/fpv/FpvWorkshop.svelte b/packages/web/src/fpv/FpvWorkshop.svelte index 843bff9..eed2de0 100644 --- a/packages/web/src/fpv/FpvWorkshop.svelte +++ b/packages/web/src/fpv/FpvWorkshop.svelte @@ -27,6 +27,15 @@ view.knownIllusionEdges.push(k); delete view.board.edges[k]; } + // ?terrain=pit@3,7;thornbush@4,7 — furnish squares for the renderer + // (and the artist) to rehearse. dimwarp@x,y opens a floor mouth. + for (const spec of (q.get("terrain") ?? "").split(";").filter(Boolean)) { + const [kind, at] = spec.split("@") as [string, string?]; + const [tx, ty] = (at ?? "").split(",").map(Number); + if (!Number.isFinite(tx) || !Number.isFinite(ty)) continue; + if (kind === "dimwarp") view.dimWarps.push({ a: { x: tx!, y: ty! }, b: { x: tx!, y: ty! } }); + else view.squareContents[`${tx},${ty}`] = { kind } as (typeof view.squareContents)[string]; + } const start = view.players.find((p) => p.id === povId)!.position; // ?x=&y=&dir= override the spawn — for standing the camera anywhere. diff --git a/packages/web/src/fpv/raycast.ts b/packages/web/src/fpv/raycast.ts index c8469d1..e84fdad 100644 --- a/packages/web/src/fpv/raycast.ts +++ b/packages/web/src/fpv/raycast.ts @@ -227,6 +227,17 @@ export interface Billboard { /** Lifted off the floor (0 = feet on the ground). */ rise: number; label: string; + /** Width as a multiple of height (1 = square; 2 = a cell-wide hedge). */ + aspect?: number; + /** Translucency (jello, dust); 1 or absent is opaque. */ + alpha?: number; + /** Drawn additively, as light (warp pillars). */ + glow?: boolean; + /** Pulled this much nearer for draw order: a hedge wins the tie + * against the wizard standing in its own square, who then peeks over. */ + bias?: number; + /** Procedural stand-in name while the painted file loads. */ + fallback?: string; /** A reflection seen through a warp mouth, not the thing itself. */ warped?: boolean; } @@ -298,19 +309,42 @@ export function billboards( scale: 0.4, rise: 0, label: "treasure", }); } - // The floor's furniture: bushes stand tall, hazards squat low. Stone is - // a wall to the rays and needs no sprite. - const TERRAIN_SCALE: Record = { - thornbush: 0.7, rosebush: 0.7, safe: 0.55, ooze: 0.35, slime: 0.3, - tacks: 0.25, pit: 0.35, dust: 0.6, - }; + // The floor's furniture, standing in the room. Bushes fill the square + // to half height (a wizard peeks over); the killer ooze is a whole + // translucent cube of jello; dust billows. Pits, slime, tacks, and + // dimensional warps are painted INTO the floor by the decal pass, and + // stone is a wall to the rays — none of those need a sprite. for (const [k, content] of Object.entries(view.squareContents)) { - if (content.kind === "stone") continue; - const file = TERRAIN_ART[content.kind]; - const scale = TERRAIN_SCALE[content.kind]; - if (!file || !scale) continue; const [tx, ty] = k.split(",").map(Number) as [number, number]; - out.push({ x: tx + 0.5, y: ty + 0.5, src: art(file, "terrain"), scale, rise: 0, label: content.kind }); + const at = { x: tx + 0.5, y: ty + 0.5 }; + if (content.kind === "thornbush" || content.kind === "rosebush") { + out.push({ + ...at, src: `/terrain3d/${content.kind}.png`, fallback: content.kind, + scale: 0.5, aspect: 2, rise: 0, bias: 0.18, label: content.kind, + }); + } else if (content.kind === "ooze") { + out.push({ + ...at, src: "/terrain3d/jello.png", fallback: "jello", + scale: 0.96, aspect: 1.04, rise: 0, alpha: 0.6, bias: 0.18, label: "ooze", + }); + } else if (content.kind === "dust") { + out.push({ + ...at, src: "/terrain3d/dust.png", fallback: "dust", + scale: 0.85, aspect: 1.1, rise: 0, alpha: 0.55, bias: 0.18, label: "dust", + }); + } else if (content.kind === "safe") { + out.push({ ...at, src: art(TERRAIN_ART["safe"]!, "terrain"), scale: 0.55, rise: 0, label: "safe" }); + } + } + // Dimensional warp mouths throw pillars of light from their floor rings. + for (const w of view.dimWarps) { + for (const cellAt of [w.a, w.b]) { + out.push({ + x: cellAt.x + 0.5, y: cellAt.y + 0.5, + src: "/terrain3d/warpglow.png", fallback: "warpglow", + scale: 0.95, aspect: 0.5, rise: 0, alpha: 0.6, glow: true, label: "dimwarp", + }); + } } for (const [k, cards] of Object.entries(view.groundObjects)) { const file = cards[0] ? objectArt(cards[0].cardId) : null; diff --git a/packages/web/src/fpv/terrain3d.ts b/packages/web/src/fpv/terrain3d.ts new file mode 100644 index 0000000..960c3dc --- /dev/null +++ b/packages/web/src/fpv/terrain3d.ts @@ -0,0 +1,87 @@ +// Terrain that stands in the room rather than lying on its floor: bushes +// filling a square to half height, the killer ooze as a translucent cube +// of jello, dust billowing, dimensional warps throwing light. Each is a +// paintable sprite at public/terrain3d/.png (transparent PNG, any +// size); the bakes below stand in until painted. + +export const TERRAIN3D = ["thornbush", "rosebush", "jello", "dust", "warpglow"] as const; + +const baked = new Map(); +export function terrainFallback(name: string): HTMLCanvasElement { + let t = baked.get(name); + if (t) return t; + t = document.createElement("canvas"); + t.width = 128; + t.height = 64; + const c = t.getContext("2d")!; + const h2 = (a: number, b: number) => { + const n = Math.sin(a * 127.1 + b * 311.7) * 43758.5453; + return n - Math.floor(n); + }; + if (name === "thornbush" || name === "rosebush") { + // A hedge of tangles wall to wall; roses get their blooms. + for (let i = 0; i < 40; i++) { + const x = h2(i, 1) * 128, y = 16 + h2(i, 3) * 46, r = 6 + h2(i, 5) * 9; + const g = 60 + (h2(i, 7) * 50 | 0); + c.fillStyle = `rgba(${g * 0.35 | 0},${g},${g * 0.3 | 0},0.9)`; + c.beginPath(); + c.arc(x, y, r, 0, Math.PI * 2); + c.fill(); + } + c.strokeStyle = "rgba(30,40,18,0.7)"; + for (let i = 0; i < 14; i++) { + c.beginPath(); + c.moveTo(h2(i, 11) * 128, 18 + h2(i, 13) * 40); + c.lineTo(h2(i, 11) * 128 + 10 - h2(i, 17) * 20, 8 + h2(i, 19) * 40); + c.stroke(); + } + if (name === "rosebush") { + for (let i = 0; i < 9; i++) { + c.fillStyle = "rgba(210,40,70,0.95)"; + c.beginPath(); + c.arc(6 + h2(i, 23) * 116, 14 + h2(i, 29) * 40, 3.5, 0, Math.PI * 2); + c.fill(); + } + } + } else if (name === "jello") { + // The gelatinous whole of the killer ooze, drawn as one cube face + // with a paler top edge — the renderer's alpha keeps it see-through. + t.height = 128; + const g2 = t.getContext("2d")!; + g2.fillStyle = "rgba(90,190,70,0.85)"; + g2.fillRect(4, 12, 120, 112); + g2.fillStyle = "rgba(170,240,140,0.9)"; + g2.fillRect(4, 12, 120, 10); + g2.fillStyle = "rgba(230,255,210,0.5)"; + g2.fillRect(16, 30, 22, 12); + for (let i = 0; i < 8; i++) { + g2.fillStyle = "rgba(40,120,30,0.5)"; + g2.beginPath(); + g2.arc(16 + h2(i, 31) * 96, 40 + h2(i, 37) * 72, 3 + h2(i, 41) * 3, 0, Math.PI * 2); + g2.fill(); + } + } else if (name === "dust") { + t.height = 128; + const g2 = t.getContext("2d")!; + for (let i = 0; i < 26; i++) { + const a = 0.12 + h2(i, 43) * 0.16; + g2.fillStyle = `rgba(200,190,170,${a})`; + g2.beginPath(); + g2.arc(14 + h2(i, 47) * 100, 14 + h2(i, 53) * 100, 12 + h2(i, 59) * 16, 0, Math.PI * 2); + g2.fill(); + } + } else { + // warpglow: a pillar of violet light, brightest at its core. + t.width = 64; + t.height = 128; + const g2 = t.getContext("2d")!; + const grad = g2.createLinearGradient(0, 0, 64, 0); + grad.addColorStop(0, "rgba(140,80,220,0)"); + grad.addColorStop(0.5, "rgba(200,160,255,0.75)"); + grad.addColorStop(1, "rgba(140,80,220,0)"); + g2.fillStyle = grad; + g2.fillRect(0, 0, 64, 128); + } + baked.set(name, t); + return t; +} diff --git a/packages/web/src/fpv/textures.ts b/packages/web/src/fpv/textures.ts index 882faee..d21cf06 100644 --- a/packages/web/src/fpv/textures.ts +++ b/packages/web/src/fpv/textures.ts @@ -42,7 +42,7 @@ function brickTexture(base: [number, number, number], rows: number, mortar: stri for (let r = 0; r <= rows; r++) c.fillRect(0, r * rh - 1, TEX, 2); }); } -export const MATERIALS = ["wall", "rim", "stone", "door", "firewall", "warp", "floor", "ceiling", "home"] as const; +export const MATERIALS = ["wall", "rim", "stone", "door", "firewall", "warp", "floor", "ceiling", "home", "pit", "slime", "tacks", "dimwarp"] as const; /** The built-in bake: what ships when no painted file overrides it. */ export function proceduralTextures(): Record { @@ -127,6 +127,67 @@ export function proceduralTextures(): Record { c.arc(TEX / 2, TEX / 2, 7, 0, Math.PI * 2); c.fill(); }), + pit: bake((c) => { + // A hole in the world: black heart, ragged broken-flag edge, a rim + // highlight on the lit side selling the depth. + c.fillStyle = "rgba(0,0,0,0.92)"; + c.beginPath(); + c.ellipse(TEX / 2, TEX / 2, TEX * 0.38, TEX * 0.34, 0, 0, Math.PI * 2); + c.fill(); + c.strokeStyle = "rgba(20,16,10,0.9)"; + c.lineWidth = 4; + c.stroke(); + c.strokeStyle = "rgba(180,168,140,0.5)"; + c.lineWidth = 2; + c.beginPath(); + c.ellipse(TEX / 2, TEX / 2 - 2, TEX * 0.38, TEX * 0.34, 0, Math.PI * 1.1, Math.PI * 1.9); + c.stroke(); + }), + slime: bake((c) => { + // A spill of green across the flags, glistening. + c.fillStyle = "rgba(70,140,40,0.55)"; + c.beginPath(); + c.ellipse(TEX * 0.5, TEX * 0.52, TEX * 0.4, TEX * 0.32, 0.4, 0, Math.PI * 2); + c.fill(); + c.fillStyle = "rgba(110,190,60,0.5)"; + for (let i = 0; i < 6; i++) { + c.beginPath(); + c.ellipse(10 + h2(i, 2) * 44, 12 + h2(i, 5) * 40, 4 + h2(i, 7) * 5, 3 + h2(i, 9) * 4, 0, 0, Math.PI * 2); + c.fill(); + } + c.fillStyle = "rgba(220,255,200,0.35)"; + c.fillRect(TEX * 0.32, TEX * 0.4, 5, 2); + }), + tacks: bake((c) => { + // A scatter of little iron cruelties. + for (let i = 0; i < 14; i++) { + const x = 8 + h2(i, 3) * 48, y = 8 + h2(i, 6) * 48; + c.fillStyle = "rgba(70,70,78,0.9)"; + c.beginPath(); + c.moveTo(x, y - 3); + c.lineTo(x + 3, y + 2); + c.lineTo(x - 3, y + 2); + c.closePath(); + c.fill(); + c.fillStyle = "rgba(190,190,200,0.8)"; + c.fillRect(x - 1, y - 4, 2, 2); + } + }), + dimwarp: bake((c) => { + // The dimensional warp's floor mouth: violet rings drawing inward. + for (let i = 5; i >= 0; i--) { + const a = 0.16 + i * 0.1; + c.strokeStyle = `rgba(${140 + i * 12},${80 + i * 14},${220},${a})`; + c.lineWidth = 3; + c.beginPath(); + c.arc(TEX / 2, TEX / 2, 5 + i * 5, 0, Math.PI * 2); + c.stroke(); + } + c.fillStyle = "rgba(240,225,255,0.8)"; + c.beginPath(); + c.arc(TEX / 2, TEX / 2, 3.5, 0, Math.PI * 2); + c.fill(); + }), warp: bake((c) => { const grad = c.createLinearGradient(0, 0, TEX, TEX); grad.addColorStop(0, "#2c1a4e"); diff --git a/research/fx3d-sprite-spec.md b/research/fx3d-sprite-spec.md index eb5b1bc..bf468da 100644 --- a/research/fx3d-sprite-spec.md +++ b/research/fx3d-sprite-spec.md @@ -51,6 +51,24 @@ door slide (doors animate by sliding `door.png` along its own edge — no separate art), and the jammed-lock seethe (a pulsing rust glow the renderer lays over the door) — those are renderer effects, not images. +## The terrain, in two families + +**Floor decals** — `public/textures/pit.png`, `slime.png`, `tacks.png`, +`dimwarp.png` — are painted INTO the flagstones by the floor caster, +alpha-blended texel by texel like the home tile: transparent wherever +plain floor should show. The pit can carry its own painted label if +wanted. Square, any size. + +**Standing volumes** — `public/terrain3d/thornbush.png`, `rosebush.png`, +`jello.png`, `dust.png`, `warpglow.png` — are billboards in the room. +The hedges render CELL-WIDE at half wall height (paint them twice as +wide as tall; a wizard in the square peeks over the top). The jello +cube fills the whole square and is drawn ~60% opaque — paint it solid, +the renderer supplies the translucency. Dust likewise. `warpglow` is a +pillar of light drawn ADDITIVELY (like the old glow sprites): bright +means bright, black vanishes. Rehearse any of them in place: +`/?fpv&terrain=thornbush@3,7;jello@4,7;dimwarp@2,7`. + ## One more paintable surface: the home tile `public/textures/home.png` is a floor overlay for home-base cells,