From f6081de0a88a865701985cd8f0b45c46949de68c Mon Sep 17 00:00:00 2001 From: Brian Fertig Date: Fri, 21 Aug 2026 17:54:36 -0600 Subject: [PATCH] Add real door textures with sliding-see-through rendering The raycaster's door geometry now takes the live `doors[]` state instead of a boolean flag, so partially-open door cells let render rays pass through the already-slid portion to show what's genuinely beyond, rather than painting a flat "socket" fill. `intersectDoorMidplane` returns a slide-shifted `textureX` so the door texture visibly translates into the wall as it opens. `WolfensteinView` samples the new `wolfenstein-doors` sheet the same way it does walls (source-texel column, vertical crop, multiply shading), falling back to the flat tan placeholder. Added the doors.png/.psd assets and a `sheets.doors` entry in the art manifest. Updated sprites.md to document the new sheet, the see-through behavior, and that this stays render-only (gameplay raycasts are unaffected). --- assets/images/wolfenstein/doors.png | Bin 0 -> 10521 bytes assets/images/wolfenstein/doors.psd | Bin 0 -> 33892 bytes data/wolfenstein-artwork.json | 3 +- src/games/wolfenstein/WolfensteinRaycaster.js | 83 +++++++++------- src/games/wolfenstein/WolfensteinView.js | 75 ++++++++------- src/games/wolfenstein/sprites.md | 90 +++++++++++------- 6 files changed, 150 insertions(+), 101 deletions(-) create mode 100644 assets/images/wolfenstein/doors.png create mode 100644 assets/images/wolfenstein/doors.psd diff --git a/assets/images/wolfenstein/doors.png b/assets/images/wolfenstein/doors.png new file mode 100644 index 0000000000000000000000000000000000000000..ebb855ee6cc43b266045167430d93fd1b9f633d7 GIT binary patch literal 10521 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 TNVb3NjVS8M+Ddf_R^k5#;#nHQ literal 0 HcmV?d00001 diff --git a/assets/images/wolfenstein/doors.psd b/assets/images/wolfenstein/doors.psd new file mode 100644 index 0000000000000000000000000000000000000000..cbbf73d7393ace2130da7c316ae9609e24f1566f GIT binary patch literal 33892 zcmeHvcT`+wmS>Tbvus&`AlX*1B$Vw|LODm2tSms}Oi@%uAfT$qBB!bXD(4~=R4!ym z1fhT&+LmRhXL_f1XXf`wRT9-DgJdmr-Hy{C82uHKyEKiWKA)l2tx^Y>nT#gC8n zoH|4Ep-uew)5uNOoka7SI27>wqdkX@`~9BWn?*YW?>;o&AKvkt|D@OFNMYYyG#ain z8hLISZuy;n7u1nAvHHNf2R8jS&=$~qzW?(3-_T|qIdJ#@&Bwm4o{Px^=3+KfB$^%q0OEIhUfYDET#F*_VJtT^Zh;A zLJWf64`BmWXtaPpKmSQVaBl4^3{Zf-k56C_c?Nv<_45yyy*_YG*y-J!L38ItW&Z2@ zWg8-+^V{M2cfe^sSby^47ZB*<3#S&(#)$ldB7h2;J!k#y@1N7AfdRg={bti5XfAK- zKV7UY1NYx%S|{Y>!p zvebABpuAA!AizIBo6))Auan=Pm=Xwx!XO?b=8ENVD4rxBmdi1va{xqV9>ENUvhswxgQL# z(`jdES+pox8jViNfcGp~ENsWn*sptXA$g?^Ua@m$!TP%R(ikZzu!o&LDLRTCdm<_| z_U!(T&%%BH?Hr)R0v7%RMyE5A1Vk2ti~p8Mjt^I7z?*R~<%JbFTGZ1&m2)Yz27wAhmw z*@@|CKUn7bml60${C{s`zVoWaKu( zNg8c7tl-c0-;;YZ+D&wB%Mh%d?}^vPeth#MuOs6DgBf1x3(M*oY z1Tsqf-`6R7qGFQcv#@^1yrwYH5&>fYtW#)vpju;S$+UQ=vve8*s`e&&kit&93Foo0 ze8_!z3}&3HG;%*C1?v*4nf$?HajdKi*ufnC#m|I%MjRYOd4JreC!_+t&mGt&@ncd` z$hw1F9vu5W)X6b+*5RL|b9zb?J2vY~Y<6~HTKo?M;`3J%8UUb^VqTZ->vHw^X*K9n|AYc z<^NSn$Y+J)H$y{X(-^5}#h#S3~m?^2bP`6irIA$kB~o8;}TO+ zPG&^KBxbY!%IP02gZEwJN>-&0>G!OSSe3J8VpYz%hFSbcRqlH&`Tty%LB}w8q3g-& zn5f8kfDuGH1{KLB$GuP*jBe!lAFGiZQBo*YBjxJDc>Gd*|M}JD0~I#0;Mn&wE(72H zybDBOCvEqCEZdcp`_hYs#p98uZRWz_48T?VbTqf@8VX!Nq? zR;8@IvAM0Wy0)sOqO7#+j;KITT#=WXTX;K%$;uO@re&}h@d*i;@z;YRD%7^T;wG7e zA#Sl)Sq)9?CVF|Lww7O7s>@`F%DU;y{K|d?lULiF%wjdzuV-aTy3Zx1D4H+E#djo~ z3yCOgKfRIPRJ8AcR9d{_dVN#DhHIte2_I(V7o7}G6XhM)lAWKkdutXaW6L%sn|WY+ zCOu};@#K`44JYCg_O3V+98n?@vFaKd3!9}9DNoqg+?*w-EWMpoP$=N076^qIN%=+m zfnyhnJDbrl)%o{NpvLmqMnOexgk-YzC#$)>cUa zA@LQKiil!A&xf~uVnMm?Yjt`#v&ZhNCPUH&& z=(~TlKu{0Y2cXviNZHh4*uY=#gYV_~@EBgsRERp4o0*(alN-Op|LxFa@17)zCJQQB z#1as1s3e5b&|zp{aJ{4$JV!WDE|}}XXAsg+-#e z(k5{=$Pog6$hSb~75S4XARvWr7vJLXz;ia2$6c!h7_8<|A(rjFu|qq2!0?hJvB8hWl2zUskKU2R3spZ=9ib& z6bVEXLQyfgi`L@?A@T!v_+H*zh#xzL1*MPr`Dv-D(v%Q?UQOAtOrkIVAcXbRgfI|- z1#;kpFW7{(`xo85#pikXbGR^;fgpzkuB0cY6`hL;4&s%b+{7;^C?tx4L?tC4A%aFD z!2DSlLQw&a&(8%J$Q@WPl@HoE{G6wd0BzFDXDnU`902<0yPy&|+O@yo%V5WfY5cyot${~2;+?jk1 zlL^VjkaKNySz-R{(#((I2*Dx@ji?e!R0Lgyodz`mn?e{|zU6+)^J6nvEP@>fxlY`7 zBV;>(}B3~#7g0x;ROIRah zb0D?gFZMM|f^g^aGa|trHX~Ui__T9t*vbq}&ds!?{_JXXX#r6LU5))f2B4;h-~>W9 zaNs`h9cKZ?{>aut94;d}p}78qX8U_#Y3$~*tgwajO6`?Yh`Rt{ha(W!U0PcRT?m5@ z$A`@s=Ptz3;PPU2o#OJCjKt#F7gbxAg&p9uXWk5*mRYnToPMhiatDH5!F=HzVIGDI zRl_f!pbO*JTF@8F#4^EP$+o4TYi^!+KPq$@UC7SN<=wtrSRgDY!qK->h^paQg6ofN z!+Rj!i6CkOZx)xClUR~#Dc`m%WCJ}XB`b7(MiC4dd;y=IpT}e82}CsoTmct^2;Ouw z8iye+j-((xhs$NGbV*g0VfGRmAxIwTeb9S%3Yptyj;$;XI;@5?|x zjwW0f%)tOGJ3ccfJFz0azX1H-$hMTFg+-*@KJmdda=<`uxwk+V05Iz0LChO;CrSed z6g@j9uD-A*e%p$Wjm(o9uPvRIk{@*(th{wAFCW_&%;4qY01h`1ESaDQH!u+MuynW_ zMr;#5>0Ias!QnaShvI^LlL|Oq*8IHuJQ#vN1mlkCxR^pO*B6cv+yz*+D9&bQ$JZTO z8@yokk_|a5Mn>4=l+uCZR5mL&Ki3O7Xq<0hC3ut3V;qRtTrb3KHbjV-lyK$hiuXg7 zY+^Wy;=`sV=N(!T1LMxETvSJZCSE?gg1>m@Lo>KMxQk)v3H~fLn-#x#)zZ+V@2=Rw zII;ff(!k{Wq$_}rx!N#cbne6;2AWAfFlHbRW|N+=n8^uOKUlTugUyW0bBW7*Q}Wpa z3x|3ycM=Ffm_Z|fgFkc%!SjW@6a0B(;&NFmHj8;}{RgWyGZ@+FOM?=M-9^kCIvXy; zTP56e2u=Wr0UI=sUIeks2HSC9g;20_va{*e)~wvb>Z?f&n-rgy{i_?C47}68tqxO* zg#y3dLZ$oh`E>AVE}NB+1BY4c96CKcBa^{8el>J?Jn!I!OdcH_Bx@6Mg(U`WaPxzA zJj3JVKnNi=90mj8#^JDt>;O9b;+m@=QxbSNjO?6jHdF$b4(GUhg5wJ!5Jxa)5(x3Q zIdFKKGc%LP1Z*yYNvCJQOjz)wSfM)~9){_+2<|LY55BS4SQ%)|zjNpEcr0*(1v;6j zbl8NQ9D?rWC`$>Rk(kffcOxy4A%xT5D#4opN?A;ZI|K1QysX&Ju+x(4vj%Dy{wrK8yGN{**Q7aqLe9@LY94Se$V3Ya|ux? zWIWla*`OWn0Q_4h&4cH3kbnk-n@ zMka${Xn;XiU&&&M3_=dKr27t=Ro7Wa&k%NGXR@lz^w_Az+8fa~n>m*jN1RM8+H)b9 z$K0QoD5M`wN+?f`WMr3|PUPeXk6g*Qm3ttCQ@}Zr&J^bC%gW5lJCKl4$T|=eQyzaZ z`ewz6lS?8ZV^=M|kZ@_$wuGd#@I!G)=a-($rpK&|Win5${WyoUckN9EYtyHW0Q8icPw=J>5Ka}AB;S^Yg^>GGy8TEXV-4of8pcPTaJHx zF>>>vi&sAW==8-)$3HrK?);&RXV0GByY1Zh3p;mTI3KxrUu5LTO$Wk!LW7A#K8qI< z3%&T}UF5fT(IR*YT(}V47r{~ACBcjFk`Qn3H2nH)Nie(yhlI?5DT5zBzQYH2^27Zu z_^6MQ1Rs$T1o(j;Kl0;IT(-ki+u$9KZijcAQoxCW2=eT1kim%sJToy>u$!D}n3yQQ zD{-j*ki2Ar~y(H57~<5B1h z&Td($s$Ze6P<6NURjCc-HlbEy9Jtlmu5z;K>RL_wnp(Ahw}mUv z+ar(e>@9S;GLN)&^~M}%wrb8DZR>449oeog+H*#&tvMFi*2arSu5U=(m0nYObTeah zM9Wi)YVhI1-p6BOPqdcN$B(4_9?;&`+pmRr46C%EtFNQ6QD-nz$#r^teQ|qhTXADu zV*^)IRb9p?Tr%79hskI(7+^IT z%^JCZ&`wb(Tk317tCh`Vs{_QNO?ktk#AAOz7<))O^|KE+9N$^TZ6RiBe}50kp}e`< zq&Kv+soN#B^>wwi&BDBO@3+5j2rZ8vKYIM^(@)1n#-2WLIBj;P!v>oBt)RTW*J|zW zGJI#8(`(Yp+hlS{LrqO>ZC&m84>{`>pI69Z#ZSkcjy?PI+1Qh3PoKI59Cn)%q~VRQ zz759rTl;!VCNRL-)v4EN4LWgsU5&Tauj*FGnkhR=Pp{4$d@}az*%?tFAA>Xj zX`2Kvn796ZuXRC3r->8?+l>ZTjqUYy^@W!T!lxW8+yB8a`IFHnV^4`^0S`VI9RRa! z-@)pGl2dF}dk?s7GAnf7=_eUA4R!VP4K-`uT6Xln@+^zZ=6?EY?CArWZ4gv}(c_?d z^28;QLI;9DhSr8gV{3i5f5@8kn==W(^+2J5-0)pHVOxwI+iiBzVL*W`TPNr= znUzqm;~?JN*d)pnY?%Ju{?nK8F(&sMqhk-e=&6nNdiAmPdu`JIrpw&XVKQs=1_Pm= z3=SJ5%{R8*-sr!oM5i|s)_{K7J@+UG`-2**6+yx<>ab(8^>=o4c669c1{Bxpv1Y|B zEq5B$`E&Y5u$qYe6&~k((Bc7e!D^@xa$_ReeGq2Ml-Zz$Mj1B*nj|e&Wy^*c4fcW? zt$ls{{eAt`q5BTP?h9gGThME7A>NDnx;s!5w3|&jSPdGmpruKi6<54|dW$3BsL|2~ z?Es2gP*@I-o&yQC;{gbt&E9QGYa=+O|y#nk)&2tH5)VaNLD(SpfpN+fLSsWg=i; zxz{wyXwb@=WD12+sncmLoe^!A-r!=K)m!@daU5`X#vx@OO6J>Z4X~Ke-f{ChEES_k z-&$W%Ra0N9&}mDKh&K5&4tOoTFnD=wUXbv!J76@kO|?Rp^)L{b%^jd}9w0!pWF?9W zdU8%|qEe^TwQro(IN-AM_JdY*n6Ufda4>H3b>HhEjD+6b09gW8=YX)Oot1T|JmTQ? zEfQU~wte%gMwf~qgDit$@D*OZwFL~=JD_O^<7B;FXTY*HnF!--uwIg#w7vXF#Exwe zeS1OWmT9%UM|W#`A=W5KfaYn~OD@ant|d%CP*tS-xN)XQ%86Uw*RyTQwpRU>kMg(p zH8|BRge3rC4b?!{CV}lx)_42#-x)$7BSxrZNU_n}oDRjQ7IQ9~b(Vkd&h}QlqO57N zf1~GaPcLEdhvu@`zq3uT_7A|A4P9U~dJVp4KNvml`pGDRPAbm1bmiXdRg2e2wB5$` zEz@dxBw3BUU^uu9gGZ3_LAPEH!>!jy(-THtXe7e)meE*nING3>iCLGg+>2kia9zB% zG_Q2i)auR?d)hjBEg((y*W3Y12i&5_I}MnNRcX?DP4mG8y%Fk}eKk^fb=AW42a9iP z5pJAT)29@h^_^Xo-oEZW!Wv|=TP!FInKFf8g@7Gk3%nUk&=6uV^YXFcYpWK8A8%9C zOE>yd_Y#JgdW&Ay(P_|k*=*o-KM4DnjN@h>Ow=NS7J@b*E(7uFr^>E=uxR~hrCP1n zJio@$SKFq;atA{@d;7cehHe9>_A#2g<|!snX*6hz;KR6amRQ`#yn3SI!pcSKPOCbb zn>Pnk8V>%d075j~YzFg;9T4me9V(rHbqRHXz*^WCH_kSwl=bP+r%U&(TC(<}^1!a! z8)sB>3De+))M@JM1TP`nJ|-PuqCsJM&4ES`g-V5_fp&vd)0i52>D=K(!E25y8saK8 zOfT=&Xmwg`7Yuk1S}&wYtAha>^b+RDCY?r4h8|p*ZB!|nQ=&G9Etns?`h-F*mu{L@ z+1p*%3^D8K1P8II3`QL!Ie_dZGhv!!G^jN?veM1aCn}{hWB>MDOBO9y87VW=-Ptg` zLU&+WDOt@ZPZ%eej0y$zqWMoIUzN(>1uJG7m9mC{10Sthw|>)($hH$Z^EOVc=&HG` zg+joG5U*(}L`|&(G0d|U^ajAlqlHo4G#8Gk8f)uBdFi`$?b&^*y@gQ;@$S}Yw3>f6 z(jdcuCYb`$Iu3#XaC0_lwFC?ZP%SzQK0B7=9ofC>WLsNn^QNh#z4sLwc;o>!Mgrs9 zD))l=S(qan!QiO`YD}lo61o`*abr`nSg=3hgzTQWabr-qHtCQ^->M&n?0};Irgj-9 zfo_^8FT(VWQEo7SthS_9tJZ2XZSd7WLsMJ)<=hQ{rG}^j61~i8n2eo(s$_BkG}Fvv zUt!vmc%SpvYHMmVgw|Irmk?5a31dfI_~cTPLMB%zpfU-=G>nkBqg{^gV1^fjW@ zD#fijwOXT4%0OOvueE;d%rbq^l@dw4vLEXZB1afk8w~PxGItZtA@C$+FsR^pRHN6( z&A7^Echey9+iC)6r62rG#88S=(r zjl8W*+Ac0HWf!lWvxgJ${^hDVDR>3ay1G_9_`4E{g4_qY0Z|S&DuqU?Rw~-sWngek zYkR9)Q6iGoRICf#TNtzaL}7iIS_=SLb+Jru00-63$Xcj9oeJW<2*zKnQVrh9os28Vu(A$MlILqv?`rS zr35QlWi>DzFJu&~oU%S+@B0y~+Pca{sOISsP@;k)gY{rAfrns6*)EYPWY8P!3a@G^ zSkO=e^YCruqLp*zuUNL|VtG|frAVPtYE&wjT1BV>l?n)yMonl#)N%+yt5_n|Dq5SG z>TAR*jjE+tT2TUX`UOn-a{u=~+`RQVQS~8wXDLxBAhB(7i3FTgz>OU&)~e-7g;Ltq z(yVD~Xl|;ml|c3yt7?S>g~i2t!qZl~{obDa`)^iO-l@D(aYrJ9j|a}--h3zfyJ%+yc*7At!d0D5kOZi#n@7&?i zqibszSxNPc^s7b9b@U7Q%1Zi~vQ|O%#p>o9PGoIO5%Xe6IVa~zVNq5}l0cAgIc<5w zje^yivdZWij#XBQw%)i?eRE}OU0ufJ0!ho&?R=H^^w!&rwYzp!H&?9LR#nYkd9b7` zY1Qfc+vk@j7Tu0moAiFf^~?)D83!^Rd zhWUnu!Yi>fDD2&LmxhIgg%QgFU~Ad3cZsC|%ieqU-DQ6{9f05RPkiXdzYxH`Bf!4| zz=;6-ivrxozY&=DOMr>L62P-~bmAJ6-v(DqEEC7J!?B$(F@Ta2SN=E^K;kXSg4R|k zRVpYK9Slj3awYu2SlA|OZI>z9rLFk=@Y}78)phlaWy0#3>ME$6(o#Xuty^5?uNJER z%Qt`j_kZ{D)o*_D5|?j2|N5&hpMUYyXP-ZV4@qI8BcqQ-2Zx6r-1mIqxjS&z0na`5 z0eIpz8&xW`rcJUlLj6T+=kV9sF7wx4+iksHeJL2}8XIeLIv?5^rPd*pLe~F@v{%vZ z%yqZGC!Gg19i58da;2)-UIn!+soWi*{46Tk`E_4>#mjGG7aPC)s^a73$B#0jx<0v= ze$_Z^iH`1mV7Q*#<0y$t?zcOPTp2&5+V!aigU+`cSopxGi?3!^XEfozHRWU z7vFRbKYet+Z}7p8-FSa!sMGk#1CPOAce)il4yRKi?&#>0Xp|alWple+BC6ZFNcGLg zv*%wufAICa?(YVh2A{w9`sg6}EzWj!GHTCfrNIx1Gc{nmM{O!;p zNS@p6v^xjy4!9g(h*7Cf$l-H`yjHp|D6-|!{<<$-yc&NQ`0C}WSC1ZzdWR>EJRBMV z?+1s${#hQk%kyCHuFHx(xO!DeLJU#CzNjFu@U5R}!UcDe*`TyqCt8ZS6kN7^t2y08pyr`{!=CdTEDf_|fPSNcgK?KYaMb z@Cby;JLEq+40-c{^xGpYw+nSTNKvQD-Ko*^t2Bq^t=@g`s9@yb@W|NLFJFBzI{M{A z+-8AdNZCUS-4LXHSfPSzTegPHU zpy(iiU^Q6o_PA_Lc!3QBaBIv5r^_F3vU@<|5UgK(MvT%R_Ouc2kRR$EA6`6i|1MOF z%QY|nW;+HT|26kM(6_Jh;a!<<;xgF%ST@W`Xb-iP0TEHONTj2IXI-33x^!s82N zLwKeRSY2+L(_$8VEIsgj{~!yr2fg^ItUqmTvE3Ru`HXg=6Vxcxj(cZ8efg2|f! zH_QZBJG#1hj?7hl`oICjG5i?ip~r?Ej)L+4^!VWj@i1g`cnC~&gXds5R!)yW?-}s8 zUbz<&|<7uu)!C$y~DmwhTSk#1M|)>$KrqhK+n$gc<#DAde))M+}-QeM0Pmu zRH}~!$?sjh@??zjV62u6gvB9Jk$|Wkzh1TwJwmf2ZAH={_DP zb5#mVFgOj*pXiU_;lYv5?pnzi9vEHhUMEa)5$?Ana3ffZo~^X<$E z+3^`N&-E)k1JJ0DKZrUpIPdX24>{%H0JSj2K-ydmsC%CQh~6w`myWY@3wPJbutU+3 zw9^s@-H6pZV1tu`KA$`whQgtNAK(t7VeF@}^N|k{u6g!BYrn_iuni0kKe=lgM0pn!>r@yVosfclxBKp0VqlKP-J#a7_H2oY zSh@5-QlHgoIk7-7WNhoQJ8k_?BhI_`hahyHK*c$oglh`)I9P9YIs2e)=M4;aJQ|IP z6|v>UwiV0vC->9=D}O zmbGj1m381il4ak=4M(TR?ums^UobA-bvZHNV7iTP`{S(ze1K-REp(7$7CGivWoi3% z>^m5;Y+qcD_E!60-?sZOw-)%x-A@MKPDD8UT~4db3Gz7LfV@9UU1Qx7o;d>^ORp+5 zVpVAHl4W}mySsWij!u<*qO3MK?E`o3g0L$9m0E4%AP1Ja-H!e~>o|Ju22p+H#_;uF zVIez{yJTX;;mPgRBWK#c+5tCI37GDhYwRGC1#J#6@anPjIWcfBX7_ZoS6n={Z}0wt zXOqm4=j#tomETu2LYP6q!1rDW|;auxwy5qgm>xkF1a5% zsP%!zY`6BnpaL?GZimxtFhi{XfOFg(44E46IC`8O51fRvz1nuUT32`NVtm)2L3eCY zyEX1co73z7uW7JC++4;E$R6SHbHi9qxEJGq*W&_lyQ5BqbC?bjd_q@gJFjNd9+@I_ zWnb%b^+3j41jsLUyNw+;^C+-2^yNSpQ2_Cs%9Yve`5H7MIA1}K{Z>c{Tr z>xFrIm?XCMg1oCAzOd_quLXL#I=Uh0T9wIcu}GQ7M5z4pvI&?DljIV_(D&(E5O<+K_ zplJI$n@${w%z{t;QgN%c8@|YeX%Lm7&u;5BLkhqtfbHp4=@jyQeXCN@+OCCJ8ks~{ zUt8DMbm+jX9T6KpzI2r-5yK*B(;K0lHByPR6=o3)9TsC>Zx`4C&*CbDyjuy!Tbo;; z=irOFntJe|G`(=ssRi-;E`#0wg7GSj53>1>u<9>wRkwa27a%bMa-8+t0z z;>9{XJyj}C%#Uwxxy49pY)Ip0-l@9E%B`)vk(&Rjh`5R^M{hUrcgM6yD~~Xw?J3(> zt*zO|t8~h^vo-zdE0K5PjYrQ(6;(UVv`C7!T&u0k+lufxrC8#b(8w`nuEyLr>5 z&6_rC+_-7oy6_DfH*8+_4$1cM0ruGb|5$d>_QC!x*vF$gfK9v~)_t(-g&BfvaCAGl zkJsUuU9jSnxQEMLkio0*_)gfzxre>*x(AL;=t0CEuip=-{=Y0gYXpAC9p)v1%+x-} z7-oDsnQhD-<`0kJf`2rEvqg9m=a;ZPunI8Gy8&Z6Y)^1lCi1?Ytec6fdy@d*)uGN%wh-k4X2!W6lM2tk=&h#(X> zh6q8GXNVY7xrSsssPPR+Gv1tYNQ%E1?+`_)aSuuN-;jTZvJ^Rp2tk#Hh!|A3h!Y{8 z#z#a9%A7h#!k0JaCZaq=ej-9p<0z5{RC$UBLY1qC7?k;n2tt{&h#-`CiwHuI zyND2P%wI%dsvJhdpvq$;<3NGSi1HNqj0o}OoJN$U%xgptO58@$2MYW~l&8pXBq6Bq z97zC5Tt`Hp%6CKzYMe(zpv-$j5X#&~1fj@(L zC@^d|Fd|T61tS6#W-#dk6?QNJP-F;C*g%aXj2IM|!U#c)EsO{h8N&!ci8YJ>l$gT^ zK$$&ETJeSqVw9!EB1VKaWfGIxDX@uAo)V)N0VuJG5r7)A7!jzlixGn|!x%x{kY$Xb z)R@MIK$&ffAe0%$q$Lzt$B03dd5jno*~g?SR2j$!LYak(Ae5QN2tt{Sj3AU5$s`C> zR`P@;6q(5g;`hevWR(78GVYfM{4Ga-DnA~3h%!f>91E!O7exlL!>}_9#!8bB`dDdG`oHk$X=DgA)H95h!!;5ri@ipY(+y7auXG z@bOWeA}1dqDDv_Vf+{y3F(~u%5ri^FA3><{^bvz9S06Db^YsygDrX-tsPXm@fdY3Q z#VPXlNeifP_z{94j~^i@a`_R0DxV)QsB-!dgEFrlL8x&1QJxyV9}%c={1Jf?&mRFO zbNvy7BHte&D0BW1gfj0RL8x;75rZ=SA3-Sp4*&$A{C@yQ5UT$RfCQoX{{To3s{a#! u1flwW0T6`Z{{}#azvX$IUwr>10?+}p0opIGKUW0uX}>JLMBr~X0{;u24`{^z literal 0 HcmV?d00001 diff --git a/data/wolfenstein-artwork.json b/data/wolfenstein-artwork.json index a429348..6887189 100644 --- a/data/wolfenstein-artwork.json +++ b/data/wolfenstein-artwork.json @@ -2,7 +2,8 @@ "_readme": "Drop-in art manifest. Every entry has path: null until painted — the game renders procedurally via WolfensteinArt.js until then. Fill in a path and it lazy-loads automatically via assetManifest.js's wolfenstein entry; no code changes needed. sheets.walls expects a 4-column strip (one column per wall type in WolfensteinArt.WALL_COLORS) so WolfensteinView's per-column drawImage can sample a texture-x slice per wall type.", "sheets": { "walls": { "key": "wolfenstein-walls", "path": "assets/images/wolfenstein/walls.png", "frameWidth": 64, "frameHeight": 64 }, - "guard": { "key": "wolfenstein-guard-sheet", "path": null, "frameWidth": 128, "frameHeight": 128 } + "guard": { "key": "wolfenstein-guard-sheet", "path": null, "frameWidth": 128, "frameHeight": 128 }, + "doors": { "key": "wolfenstein-doors", "path": "assets/images/wolfenstein/doors.png", "frameWidth": 64, "frameHeight": 64 } }, "artwork": [ { "key": "wolfenstein-item-pistol", "path": null }, diff --git a/src/games/wolfenstein/WolfensteinRaycaster.js b/src/games/wolfenstein/WolfensteinRaycaster.js index c2a3941..4b67d5b 100644 --- a/src/games/wolfenstein/WolfensteinRaycaster.js +++ b/src/games/wolfenstein/WolfensteinRaycaster.js @@ -13,11 +13,14 @@ // "exactly at the point dir was aimed at"). // // Door cells get real recessed-mid-plane geometry (see intersectDoorMidplane) -// when a caller opts in via `doorAware` — WolfensteinView's render raycast -// does; collision/line-of-sight/bullet callers in WolfensteinLogic don't, so -// a door stays a simple full-cell solid for gameplay purposes exactly as -// before (see stepDoors' note on why passability only flips at slide===1). -// This is a rendering-only enhancement, not a change to what's walkable. +// when a caller opts in by passing `doors` (WolfensteinView's render raycast +// does, with state.doors — the slide-open portion genuinely isn't a hit, so +// the DDA carries on through to render whatever's actually beyond); a +// collision/line-of-sight/bullet caller in WolfensteinLogic omits it, so a +// door stays a simple full-cell solid for gameplay purposes exactly as +// before (see stepDoors' note on why passability only flips at slide===1) — +// this is rendering-only, not a change to what's walkable/shootable/visible +// to AI, even though the player can now visually see further than that. const DEFAULT_MAX_STEPS = 256; @@ -41,31 +44,41 @@ export function isWallCell(map, cellX, cellY) { * walled off), not the doors[] `orientation` field, which nothing actually * sets to anything but 'vertical'. * - * Returns null when the ray's own trajectory would clear the cell sideways - * (cross into a neighboring cell) before ever reaching that mid-plane — the - * caller should then keep stepping the DDA as if this cell were transparent - * rather than treat it as a miss; stepping on lets the DDA reach the actual - * flanking wall cell and hit it at its own true (nearer, unrecessed) - * distance, which is what makes that wall's inward face visible at grazing - * angles — the two verticals of the "H," rendered as ordinary wall hits, - * not anything faked here. + * Returns null in two cases the caller treats identically — keep stepping + * the DDA as if this cell were transparent, rather than a miss: + * - the ray's own trajectory clears the cell sideways (crosses into a + * neighboring cell) before ever reaching the mid-plane at all — letting + * the DDA carry on reaches the actual flanking wall cell and hits it at + * its own true (nearer, unrecessed) distance, which is what makes that + * wall's inward face visible at grazing angles: the two verticals of the + * "H," rendered as ordinary wall hits, not anything faked here; + * - the plane crossing falls in the portion of the door that has already + * slid open (`along >= slide`, the panel having translated `slide` + * cell-widths into its pocket) — there's genuinely nothing solid there + * now, so the DDA carries on to whatever's actually beyond the doorway. + * + * For the still-covered portion, `textureX` comes back pre-shifted by + * `-slide` — the same panel-translation math, so sampling straight from it + * (see WolfensteinView._drawDoorColumn) makes the door texture visibly + * slide sideways into the wall as it opens, not just get cropped in place. */ -function intersectDoorMidplane(map, x, y, dirX, dirY, cellX, cellY) { +function intersectDoorMidplane(map, x, y, dirX, dirY, cellX, cellY, slide) { const blocksEW = isWallCell(map, cellX, cellY - 1) && isWallCell(map, cellX, cellY + 1); + let t, along; if (blocksEW) { if (Math.abs(dirX) < 1e-9) return null; - const t = (cellX + 0.5 - x) / dirX; + t = (cellX + 0.5 - x) / dirX; if (t <= 0) return null; - const along = y + t * dirY - cellY; - if (along < 0 || along > 1) return null; - return { perpDist: t, side: 0, textureX: along }; + along = y + t * dirY - cellY; + } else { + if (Math.abs(dirY) < 1e-9) return null; + t = (cellY + 0.5 - y) / dirY; + if (t <= 0) return null; + along = x + t * dirX - cellX; } - if (Math.abs(dirY) < 1e-9) return null; - const t = (cellY + 0.5 - y) / dirY; - if (t <= 0) return null; - const along = x + t * dirX - cellX; - if (along < 0 || along > 1) return null; - return { perpDist: t, side: 1, textureX: along }; + if (along < 0 || along > 1) return null; // clears the cell sideways + if (along < slide) return null; // this point has already slid open (opens from along=0 outward) + return { perpDist: t, side: blocksEW ? 0 : 1, textureX: along - slide }; } /** @@ -73,11 +86,12 @@ function intersectDoorMidplane(map, x, y, dirX, dirY, cellX, cellY) { * length; distances/`perpDist` come out scaled to that vector's own length — * until it exits the first solid cell. Returns null if it runs off the map * edge or exceeds maxSteps without a hit (should never happen inside a - * validated closed level). `doorAware` opts into recessed door-plane - * geometry (see intersectDoorMidplane) instead of treating a door cell as an - * ordinary flush solid. + * validated closed level). Passing `doors` (an array of `{x, y, slide}`, as + * on WolfensteinLogic's sim state) opts a door cell into recessed, + * partially-passable mid-plane geometry (see intersectDoorMidplane) instead + * of treating it as an ordinary flush full-cell solid. */ -export function castRay(map, x, y, dirX, dirY, maxSteps = DEFAULT_MAX_STEPS, doorAware = false) { +export function castRay(map, x, y, dirX, dirY, maxSteps = DEFAULT_MAX_STEPS, doors = null) { let mapX = Math.floor(x); let mapY = Math.floor(y); @@ -100,10 +114,11 @@ export function castRay(map, x, y, dirX, dirY, maxSteps = DEFAULT_MAX_STEPS, doo const wallType = map.walls[mapY][mapX]; if (wallType === 0) continue; - if (doorAware && wallType === DOOR_WALL_TYPE) { - const doorHit = intersectDoorMidplane(map, x, y, dirX, dirY, mapX, mapY); + if (doors && wallType === DOOR_WALL_TYPE) { + const door = doors.find((d) => d.x === mapX && d.y === mapY); + const doorHit = intersectDoorMidplane(map, x, y, dirX, dirY, mapX, mapY, door?.slide ?? 0); if (doorHit) return { ...doorHit, mapX, mapY, wallType }; - continue; // ray grazes past this door cell without reaching its mid-plane + continue; // ray grazes past this door cell, or passes through its already-open portion } const perpDist = side === 0 ? (sideDistX - deltaDistX) : (sideDistY - deltaDistY); @@ -125,8 +140,8 @@ export function makeCamera(x, y, angle, fov) { return { x, y, angle, dirX, dirY, planeX: -dirY * planeScale, planeY: dirX * planeScale }; } -/** One castRay per screen column, camera-space (fisheye-free). `doorAware` — see castRay — is what WolfensteinView passes to get recessed door geometry. */ -export function castColumns(map, camera, numColumns, doorAware = false) { +/** One castRay per screen column, camera-space (fisheye-free). `doors` — see castRay — is what WolfensteinView passes (state.doors) to get recessed, slide-aware door geometry. */ +export function castColumns(map, camera, numColumns, doors = null) { const { x, y, dirX, dirY, planeX, planeY } = camera; const maxSteps = fullMapSteps(map); const out = new Array(numColumns); @@ -134,7 +149,7 @@ export function castColumns(map, camera, numColumns, doorAware = false) { const cameraX = (2 * col) / numColumns - 1; const rdx = dirX + planeX * cameraX; const rdy = dirY + planeY * cameraX; - out[col] = castRay(map, x, y, rdx, rdy, maxSteps, doorAware); + out[col] = castRay(map, x, y, rdx, rdy, maxSteps, doors); } return out; } diff --git a/src/games/wolfenstein/WolfensteinView.js b/src/games/wolfenstein/WolfensteinView.js index 9f3b398..1a136d1 100644 --- a/src/games/wolfenstein/WolfensteinView.js +++ b/src/games/wolfenstein/WolfensteinView.js @@ -27,17 +27,12 @@ export const VIEW_H = 940; export const NUM_COLUMNS = 480; // wallType -> frame index in the `wolfenstein-walls` sheet, matching -// WolfensteinArt.WALL_COLORS' order. No entry for type 9 (door) — doors are -// never texture-sampled, they get their own recessed-slide treatment in -// _drawDoorColumn regardless of whether painted wall art exists. +// WolfensteinArt.WALL_COLORS' order. No entry for type 9 (door) — doors +// sample their own `wolfenstein-doors` sheet (frame 0) via _drawDoorColumn, +// which also handles their recessed/sliding geometry — not part of the +// generic per-column wall-texture path below at all. const WALL_FRAME = { 1: 0, 2: 1, 3: 2, 4: 3 }; -// A dark, unshaded fill for the portion of a door that's already slid past — -// meant to read as a shadowed pocket the panel has receded into, not more -// wall. Kept flat (no side/fog shading) so it reads as a distinct material, -// not just a darker version of the door itself. -const DOOR_SOCKET_COLOR = '#141018'; - export default class WolfensteinView { constructor(scene, rules) { this.scene = scene; @@ -57,6 +52,8 @@ export default class WolfensteinView { this.wallTexture = scene.textures.exists('wolfenstein-walls') ? scene.textures.get('wolfenstein-walls') : null; + this.doorTexture = scene.textures.exists('wolfenstein-doors') + ? scene.textures.get('wolfenstein-doors') : null; } render(state, camera) { @@ -71,7 +68,7 @@ export default class WolfensteinView { ctx.fillStyle = '#4a4a4a'; ctx.fillRect(0, VIEW_H / 2, VIEW_W, VIEW_H / 2); - const cols = castColumns(map, camera, NUM_COLUMNS, true); + const cols = castColumns(map, camera, NUM_COLUMNS, doors); for (let i = 0; i < NUM_COLUMNS; i++) { const hit = cols[i]; this.depthBuffer[i] = hit ? hit.perpDist : Infinity; @@ -88,7 +85,7 @@ export default class WolfensteinView { const h = Math.ceil(drawEnd - drawStart); if (hit.wallType === DOOR_WALL_TYPE) { - this._drawDoorColumn(ctx, x, y, w, h, hit, doors); + this._drawDoorColumn(ctx, x, y, w, h, hit); continue; } @@ -117,29 +114,43 @@ export default class WolfensteinView { } /** - * hit.perpDist already IS the recessed mid-plane distance here — the - * render raycast opts into door-aware geometry (see castColumns(..., - * true) above and WolfensteinRaycaster's intersectDoorMidplane), so this - * column's (x,y,w,h), computed by the caller exactly like any other wall, - * is already correctly smaller/farther than a flush wall would be. A - * grazing column whose ray clears the door cell sideways instead hits the - * real flanking wall cell at ITS true distance via the normal wall path - * above — that's what makes the wall's inward face visible next to the - * door, the two verticals of the "H." Nothing extra to fake here. - * - * `textureX` is where this column's ray crosses the door's face (0..1 - * along it, computed at the mid-plane); a door slides such that the low- - * textureX edge stays put and the far edge recedes first, so as `slide` - * climbs, columns flip from "still there" to "already receded" in - * textureX order — the door visibly sliding sideways into its socket. + * By the time a column gets here, the raycaster (see castColumns(..., + * doors) above and WolfensteinRaycaster's intersectDoorMidplane) has + * already resolved everything door-specific: this hit only exists because + * that particular point on the door is still covering the doorway, at its + * true recessed mid-plane distance — an already-open point along the same + * ray instead passes straight through to whatever's actually beyond + * (rendered by the ordinary wall path, not this method at all), which is + * how you end up able to see past the door as it slides open. `textureX` + * comes back already shifted by `-slide`, so sampling it straight makes + * the door texture visibly slide sideways into the wall, not just crop in + * place. So this is really just "draw a wall column, but from the + * `wolfenstein-doors` sheet" — same technique as the main wall path + * (source-texel column, vertical crop for close-up magnification, + * multiply-shaded), falling back to the flat placeholder tan if no + * sheet is loaded. */ - _drawDoorColumn(ctx, x, y, w, h, hit, doors) { - const door = doors.find((d) => d.x === hit.mapX && d.y === hit.mapY); - const visibleFrac = 1 - (door?.slide ?? 0); - ctx.fillStyle = hit.textureX < visibleFrac - ? shadeColor(WALL_COLORS[DOOR_WALL_TYPE] ?? 0xb08040, hit) - : DOOR_SOCKET_COLOR; + _drawDoorColumn(ctx, x, y, w, h, hit) { + const frame = this.doorTexture?.frames[0]; + if (!frame) { + ctx.fillStyle = shadeColor(WALL_COLORS[DOOR_WALL_TYPE] ?? 0xb08040, hit); + ctx.fillRect(x, y, w, h); + return; + } + + const lineHeight = VIEW_H / hit.perpDist; + const rawStart = -lineHeight / 2 + VIEW_H / 2; + const rawEnd = lineHeight / 2 + VIEW_H / 2; + const drawStart = Math.max(0, rawStart); + const drawEnd = Math.min(VIEW_H, rawEnd); + const srcX = frame.cutX + Math.min(frame.width - 1, Math.floor(hit.textureX * frame.width)); + const srcY = frame.cutY + ((drawStart - rawStart) / lineHeight) * frame.height; + const srcH = ((drawEnd - drawStart) / lineHeight) * frame.height; + ctx.drawImage(frame.source.image, srcX, srcY, 1, srcH, x, y, w, h); + ctx.globalCompositeOperation = 'multiply'; + ctx.fillStyle = shadeGrey(hit); ctx.fillRect(x, y, w, h); + ctx.globalCompositeOperation = 'source-over'; } _drawSprites(state, camera) { diff --git a/src/games/wolfenstein/sprites.md b/src/games/wolfenstein/sprites.md index 4c1102e..9def9e9 100644 --- a/src/games/wolfenstein/sprites.md +++ b/src/games/wolfenstein/sprites.md @@ -13,28 +13,41 @@ loaded as a plain Phaser spritesheet (frames read left-to-right, top-to-bottom one 64×64 tile per wall type**, in `WolfensteinArt.WALL_COLORS` insertion order: frame 0 = type 1 (stone), frame 1 = type 2 (wood), frame 2 = type 3 (blue-tile), frame 3 = type 4 (green-tile). Doors (`DOOR_WALL_TYPE`, type 9) -are **not** part of this sheet at all — `WolfensteinView._drawDoorColumn` -(as of 2026-08-21) handles type 9 as a special case before the texture path -even runs, rendering the flat placeholder tan color (`WALL_COLORS[9]`) for -whatever fraction of the door hasn't yet slid open, and a flat dark "socket" -fill for the rest (see the sliding-door doc below) — so a 5th sheet frame -wouldn't currently be read even if added. Painting a real door texture means -teaching `_drawDoorColumn` to sample a frame the same way the wall path -does, not just adding a 320×64 sheet. +are **not** part of this sheet at all — see `sheets.doors` below. -### Sliding, recessed doors +## Door textures — `sheets.doors` + +`frameWidth: 64, frameHeight: 64`, same spritesheet convention as walls. +**Wired up as of 2026-08-21: frame 0 is the one and only door type ("Normal" +— the editor's Door category has no other option yet) and is fully live.** +The current painted sheet is 256×256 (a 4×4 grid, 16 frames), of which only +frame 0 is read; the other 15 sit unused until a second door type exists +(matching how the editor's Door dropdown would need a second `option` in +`WolfensteinEditor.js`'s `CATEGORIES`, with the new tool id mapped to a +frame index the same way `WALL_FRAME` maps wall types today — no such +mapping exists yet since there's only the one type). `WolfensteinView.js` +grabs the texture once in the constructor (`this.doorTexture`) and +`_drawDoorColumn` samples it exactly like the wall path samples +`sheets.walls` (1px source-texel column, drawImage-stretched, same vertical +source-crop for close-up magnification, same `multiply`-composited +side/fog shading) — falls back to the flat placeholder tan if the sheet +isn't loaded. See the sliding/recessed-door notes below for how that +texture actually gets positioned in the scene (mid-cell plane, slide-driven +visible fraction). + +### Sliding, recessed, see-through doors Doors animate open/closed (`WolfensteinLogic.js`'s `stepDoors`, `slide` field 0→1 over `DOOR_SLIDE_MS`) rather than popping instantly, and the cell stays solid for *gameplay* (collision/LOS/bullets) through the entire animation — it only becomes passable once `slide` reaches exactly 1 (see stepDoors' -comment for why). That's unrelated to the visual, though: doors also render -recessed to the middle of the wall's depth — real geometry, not a flat-color -trick — via `WolfensteinRaycaster.js`'s `intersectDoorMidplane`, which -`WolfensteinView`'s render-only raycast opts into with `castColumns(..., -true)` (the `doorAware` flag; every gameplay raycast in `WolfensteinLogic.js` -omits it and still sees a door as an ordinary flush full-cell solid, so this -never touches what's actually walkable/shootable). +comment for why: no squeezing through a half-open door). That's unrelated to +the visual, though, which is real mid-cell geometry, not a projection trick: +`WolfensteinRaycaster.js`'s `intersectDoorMidplane`, opted into by passing +`state.doors` to `castColumns`/`castRay` (every gameplay raycast in +`WolfensteinLogic.js` omits it and still sees a door as an ordinary flush +full-cell solid — this never touches what's actually walkable/shootable/ +visible to AI, only what the player's screen shows). A door's blocking axis is inferred from geometry (which pair of neighbor cells is walled off) rather than the `doors[]` `orientation` field, which @@ -42,25 +55,34 @@ nothing in the codebase actually sets to anything but `'vertical'`. Given that axis, the door plane sits at the cell's exact midpoint (e.g. `x = doorCellX + 0.5` for a door blocking east-west travel) instead of at the near face like a normal wall — a straight-on ray is `perpDist`-recessed by -0.5 relative to where a flush wall would sit. Critically, a ray whose -trajectory would clear the door cell sideways (cross into a neighboring -cell) *before* reaching that mid-plane isn't given a fake hit at all — -`castRay` just keeps stepping the DDA, so the ray goes on to hit the actual -flanking wall cell at *its own* true, unrecessed distance. That's what -produces the "H" shape: the door is the crossbar, visibly set back between -two verticals that are ordinary wall hits, not anything faked in -`_drawDoorColumn` — confirmed with a sweep of rays across a doorway (see -git history around 2026-08-21 for the script) showing a symmetric run of -door hits at the recessed distance, flanked on both sides by wall hits at -their own, nearer distances, with no discontinuity at the transition. +0.5 relative to where a flush wall would sit. -`_drawDoorColumn` itself is now simple: `hit.perpDist`/`(x,y,w,h)` already -reflect the recessed mid-plane (computed by the caller exactly like any -other wall), so it only has to pick door-texture-tan vs. dark socket color -per column, using `hit.textureX` (now measured at the mid-plane, 0..1 along -the door's face) against `1 - slide` — the same left-to-right slide-open -wipe as before, just riding on correct geometry instead of a projection -trick. +Two things make a ray pass straight through a door cell instead of hitting +it — both handled by `intersectDoorMidplane` returning `null`, which +`castRay` treats as "keep stepping the DDA," not a miss: +- the ray's own trajectory clears the cell sideways (crosses into a + neighboring cell) before ever reaching the mid-plane — the DDA carries on + to the real flanking wall cell, hit at *its own* true, unrecessed + distance, which is what produces the "H" shape (door as the crossbar, + visibly set back between two verticals that are ordinary wall hits, not + anything faked in `_drawDoorColumn`) — confirmed with a sweep of rays + across a doorway showing a symmetric run of door hits at the recessed + distance flanked by wall hits at their own nearer distances, no + discontinuity at the transition; +- the crossing point has already slid open (`along < slide`, where `along` + is the 0..1 position along the door's face and the panel is modeled as + having physically translated `slide` cell-widths into its pocket, opening + from `along = 0` outward) — the DDA carries on *past the door entirely*, + so a column here renders whatever's genuinely beyond the doorway (through + the ordinary wall path, at that room's own true distance), not a stand-in + color — you can see into the next room as the door opens, exactly as far + as the geometry actually allows. + +For a still-covered point, the returned `textureX` is `along - slide`, not +raw `along` — sampling that directly (`_drawDoorColumn`, same source-texel +technique the wall path uses) is what makes the door *texture* visibly +translate sideways into the wall as it opens, rather than the closed +portion just being a static crop that shrinks in place. **Now wired up** (as of 2026-08-21): `WolfensteinView._drawWalls` reads `hit.textureX` (the raycaster's fractional wall-face position) and