From 08f1b2d154c28a3ad4bc34b3fcf67ef4947a0204 Mon Sep 17 00:00:00 2001 From: Adrien Jaguenet Date: Sat, 19 Sep 2026 17:16:41 +0200 Subject: [PATCH] Added textured terrain, procedural water and a blurred fog of war --- client/assets/textures/forest.png | Bin 0 -> 37455 bytes client/assets/textures/grass.png | Bin 0 -> 13772 bytes client/assets/textures/hills.png | Bin 0 -> 10206 bytes client/assets/textures/hills_normal_map.png | Bin 0 -> 35048 bytes client/assets/textures/mountain.png | Bin 0 -> 36436 bytes .../assets/textures/mountains_normal_map.png | Bin 0 -> 54816 bytes client/assets/textures/sand.png | Bin 0 -> 11266 bytes client/css/style.css | 49 +- client/index.html | 2 +- client/js/bench.js | 4 +- client/js/game_screen.js | 5 +- client/js/game_screen/panels.js | 30 + client/js/map_view.js | 33 +- client/js/map_view/constants.js | 19 - client/js/map_view/distortion.js | 218 ++++ client/js/map_view/entities.js | 14 +- client/js/map_view/fog.js | 79 ++ client/js/map_view/input.js | 92 +- client/js/map_view/terrain.js | 152 +-- client/js/map_view/textures.js | 192 +++ client/js/map_view/utils.js | 19 +- client/js/map_view/webgl.js | 1096 ++++++++++++++--- tests/distortion_test.js | 153 +++ tests/fog_test.js | 89 ++ tests/framework/helpers.js | 2 +- tests/map_view_test.js | 110 +- tests/textures_test.js | 215 ++++ tests/webgl_geometry_test.js | 162 +++ 28 files changed, 2358 insertions(+), 377 deletions(-) create mode 100644 client/assets/textures/forest.png create mode 100644 client/assets/textures/grass.png create mode 100644 client/assets/textures/hills.png create mode 100644 client/assets/textures/hills_normal_map.png create mode 100644 client/assets/textures/mountain.png create mode 100644 client/assets/textures/mountains_normal_map.png create mode 100644 client/assets/textures/sand.png create mode 100644 client/js/map_view/distortion.js create mode 100644 client/js/map_view/fog.js create mode 100644 client/js/map_view/textures.js create mode 100644 tests/distortion_test.js create mode 100644 tests/fog_test.js create mode 100644 tests/textures_test.js create mode 100644 tests/webgl_geometry_test.js diff --git a/client/assets/textures/forest.png b/client/assets/textures/forest.png new file mode 100644 index 0000000000000000000000000000000000000000..9c59c3729ffc581ec1def29fd311ce106d66a444 GIT binary patch literal 37455 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 Q1_lNOPgg&ebxsLQ0BQBw3IG5A literal 0 HcmV?d00001 diff --git a/client/assets/textures/grass.png b/client/assets/textures/grass.png new file mode 100644 index 0000000000000000000000000000000000000000..314df9f2638ef51acc9e4d310a6d9dbfe293401c GIT binary patch literal 13772 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 literal 0 HcmV?d00001 diff --git a/client/assets/textures/hills.png b/client/assets/textures/hills.png new file mode 100644 index 0000000000000000000000000000000000000000..c2b689080be8860305b692f00ee778ac8113883b GIT binary patch literal 10206 zcmeAS@N?(olHy`uVBq!ia0y~yU}ykg4mJh`hQoG=rx_R+7>k44ofvPP)Tw7+U|>mi z^mSxl*x1kgCy|wbfk7eJBgmJ5p-PQ`p`nF=;TK5r3kHT#0|tgy2@DKYGZ+}e3+C(! zv|(Ui;4JWnEM{QPQwCwiilz2t3=9eko-U3d6>&@FzV3T{rqK0m4DS)PPs;rwKmNum z)iX=DO_)8`#r2HRwKXf3eX(Sh=vZ{(-L5x#SMNGH^YiQYzpujg*Z=)<{`K?vub;!$ zKaSsD_w4HNIk)4U%h=yBe*awlx#d5r+PTl~pSho$^QmIq^Se9bia)0x`}_R*`Tcw9 z{{8;?dHH(zzW-~Ai_cW|{k1ZDQrq9TKIV^@`0p>7d+a)I$2{Ki&*I#WIhcx_bw z%L?#x>3Xs?GaEjit-*7&(Z9j;+sE_A`7|2--M)VE{+_zJ?@xc*?~D8Ynww?vAG!0j z%zVt(>=NqVKKpa${rOoo_s`2ro*aCJOXbm(>$gf-7{skENDKV?WqLTCt3|20IH@=x zXERey)`1(Jy*{nw+g9mdZ(skPwJGuK57Xzr)_ZCPYs4Gw*=kI0pkFksi+ZfxL7Co;%e~n*)$99|Zx@gvCd-6Fy>>YYw~|H2puymu0QR0)AE6Hs>hb!ED`qKbDtjKF*m6GBc_q8QRK2g zT4TxPzl>Z5zJ_rL?NL3=vsOyvWS##f-H)Xg4t%olU2xOPH@-Rlssbl7-!rwBb3XqF zj{DJGwbRDr=NaFi*)@;6R%ZFg94=SA*0QTFnn`E7bn~n)lZ8w^M*ClQvio21imMNn zX!UH4jXwHePOQ-E z*te%v+n6NQvfSo;{zF(GCu>9FfsU_#)ENxc&5R2#;H_SM4e($%Tfd=YBpc7V`KMsP7PX zoA=S$lr1cGw#Gg9b-(&8|j#KVcVQ3CexXs7}w@9-@9*8d+#oy`vzBp#-z znaPwcthi@=C62x3WzS*fGCtXWH>{!<~g}p^a;w-O#vRb8hmVX-v}l8`m8yj@*!A z#OIY@cGl9X>*kGnO)PUJg{PVEZ9D5}akMbFMY5S~$6of;+?)3&+z2_aamVAEY6WU@ zId!J5b(Q_YDZz3%!GU+i?N2XLXGiYbxW93mOO@sEpnipg1`P)u6cs;fsa(glsccH6 zG>3=P`qaOB-k&;PSa7B@KA!u*H}UHgHuBehS;!al?Mh@`BgWgucuDH%qyz?mkaha6 zwD-^3{B`=qXErC>pN4VV_S9@(@Vl_`3&VuT|NWoeUwLqv&4EmZ&_eHHeaRo<>?X0? zaCj8$|28N4&NGeYT_HT%xCC-0g?8SRIw-lnBR}EKjB@6uyb3w0KiLEvx7&Z4zGFu0 z^6dFjm?G5^?}@#uRh{wpL9+V%8wt;S^EM0I)RACl&3-)Xi`SXSMhjRz%l@{o=L??i z*%zOlvs#4H^!M8j*W9=G8L5>?eVHAzD@8yV`wW1s(;4n>XIz+dwQ^fN z>udIp+jQSEJlJ>Y=dPqg)`Eb7hNT@7(^L&!pYBqVQtLffqipv0+_LjEX8U^&KMre1 zIF@4D?;G0C^x=rpcRk-%nf3>|o?hZ`+1>dDNXZ{xRFeIoxwzyz2!PU&x>FR+?ycHX-|J(7A zsiNoUYag!PyeoE;y(#J6s>$H6sHehz5vO^=hSQ~W%+keTEafv@8O?qcDx9uTmoGga zIpNJLx28nF_l6fz8PrS*Rqilo{Pt|RxsUPo^IN|k@P9UTKi&JDdBwW-x68`k8Zj_T zdv1Cot9^ZW-5REQCXedk>NO5*`;u`vZTVb<=GG&>71{m$G7C;xn)aP(pVL?*!>n`0 z)mQBxUq$Zyd(3BU8GqiEe}kcM?(J*kq6Z9{8Cd837M5Quq04-GdJgMc@2m6IGNc8k zUg5kEaOd?~fv;0LIhQeSQa$CV^6>^Y1B383>y^>WPYrp+ytR_^v<}&({b5sKKA3uK zQqJto>Gsd(%=G685m4J;pl@smH>T zR<8UUy?1lXoQ-#kYnXhR_oyz@O5Je4E-A3}#xbe?ToL`57Yw&g?Fp4g{mjy_^LoQ( zuU#?`u@wiN3PY;P?8sv7Fjsq`JU~Oxhn$mmsm|q(`1xo_;dJmdKmBjpT9Dh zy%Kn{to?+Cw;i7~sM&xqKVq|~@U%-(9b=dJfqJWQ)U^!!v8 zIK}>#OLXz+Yrdy=n&fu#M8wWG-*)zfUludFz#jou=1q)M-W4-rR8tn8=uNNH-mymI zvt)mL?y5wmGsOZLQXl75zqD0&a`L*_+_=_?)shMESX)|PvF&~sV~6c}4T10d_Q~hJuGL_8pu%^KRc8CXh;0*`jeoR6tSg+m*xaD| z)`m0H>t)h}ISncgukbW{FegKOMUT0|8Iy?S{Zm?cd$&zpKW}&WMokR{htfOzJ7x0C zwwo{J{~&+JfZ@l~AHQcFFuJoM-QarW>K5KbkxUB>Zd?y|ZE&mWfy+-Tp@xTd9Uc{{ z-|f2a*>_85`tLI-29Ft79$Wu>d|&p(^^I4jKCV=~CUbeg%oyWq!8@5cW^?(RH+h1{!4hRLDR@AznVxxKJ*d6sq zb;h^8?;PBg$|(On{qtB`_PS$lv>jFMbVW3MkbCsei%)&O$=Tw*bluLIp-kHxcdxnW z5b)r^l(~v8-PTK*U5_%|(7I9g`rQ??vKc1M+`jSdW0Q-17uej?Pp@V?p26Jo{}YGZ z_L=i`erz_3QJOq&_L@7#_o}zfIKTYQpKo88fA9Ypzz~u)>&aS4o!7HxSS22P5Y?Q( z&GD4sz>P$8`Ja2P?@U@Inv}fztGiD42^7en5^Lvvd^9Moee=k^93p`tTq~BwmK2@KzVLI7Tk_=a(`&q0d=(EawVY*HutF&INb`ruLOVp}NVzdz_WZv0EvE;= z$AX=Y?A|dR=v&@s_F0%)URe9wSySf(hvyMz{s?y`{{LgF$aqjx+f0h5$yacWhj)pi z@!e-WuM)P}nRU9UJ1RyrFkCPUTe{V@LE5=*HHW`ZzGc~ijpxhG++lO67EL-{$Nni+ z^hO8c?%Vliwif@_`K;D1Tzovs?z!M@%Qgpl2PTGX52kQNAJ92ld}e;6sqo&g`v;!7 zwI%FQpRsX*jh*~$o+C4xmrVI9%@Di&uJjiF3N3H8pVLa3r~T!7QAu@> zu7DQt8kVVT$IH%G_%jp~n5jPa$F{wzwO7wSP36rpnSj^QHSf%Kn{V10bLQ)xGZKq! z>b70lH*?E`4M%qV+00bIP~Ka1$+OrgHeLN^$-))gGef>T)(EN!k~3Z%bZ1`klDb26 z)0^1}gBi4^CMO@-(Yf`|*}|na9^AjO%|U5m!$B5Pi@PCAej*KbVz~}nOJ)va)p$Ky zW3zxjgPnAwWJ$xVjHWMUzT1}i&YN6kwN9vl$Gn{FR#^_i4Sw_1X;oJa7`?9uX_dIF z&Box!!?oa8@jc`B{dNab7xYP2=HAF->FufcyM=G39OE?M-*+`6S)%s`s?V}{Q;>OM zNj>wyFurGZ!akWPSRY(|tDh@}H&^sP;qiR$%1(z*=9`S>zx}t2!Eeday_Bex@X)-fHZxqU%3ym_1Ix5+^X-z>!pPUJklrNyvKaDNj^zygNU|H2FzKhFHo zT^Gtd`Ks>&Q*o{fow_&O7B&VR$T_Wl&i15TPeZ1hczVIS?6n%KXTy%)zBykcF?jN= zcUIL}jME08rZgg5Dd=r+qzQ#*4 z;l=?D36Ba@*=lo!9EV4DI#;TvR$KM&4{Q9@^eX4Gc*(re+O2C@1j9d7Xdaj*!}FkR zx!+U;;VZead5d+O-+ArT*%V@THRxEW!R!?8-M^cCHP^R04z>KC^S zO@-Q;XEIyU)@?kyG=cd^X}mo5{)r#0ZcI4!oX^OyxinhyzzyA>x{T4&cb}ZsoU=RD zNoVowvnrL7>-smdPO$Ctm9;o7;bHvQQtrPzLxmj2lf6tynHv~-va73p?`=@p?`du| ze^>UD1D#q79(HGik409VS+{oG&3R|9H9gRLd8M40$<*Y;nhaKk>oa@97|o0Q|J@Mp zn_HlMRqJS-S2Cx;v#5FA7EBL9(xq)qzwWGzDGp-vRyscQufJ$RO0H1Vrdn;uuiXdA zUq1U@%&(*J(1!O+`s`h~uQsi`eXSZw0GS@-xST~$<9 zTc6C(v3|dRk^j_+N*CdWsprxbG^eu~&R}5btMXaxkoy0~rg`<^D}pm`gt0F#Vu+UD zjt_J>t((KH5_c=?;XTHh;O&;_NmV>=66Bgs_b>hw*^8Q*wcP`n{%znYGs=KU;R_ zI_Y-v_8i=EZ=)MFjfUM@ro>eBF^*eu=g5Xk<|LB zZ)7d3u6r8F?*Di;{&@6F&9CA7AM)4B&wKFjqSC(;N=pxB|5B^I{o`fryJcs){F}aZ zGFdeE{afy!abfNowSQmgyf5i%=API+liy*xU6S&o$a5A?mY*_B{`Z>U!TdP;&p9`3 zj%-*u_8{N;-)CPx|Ne(r!IOpiRj;T>%`8n>y;tI`dXncH(S;q)jhP-YJ6sQYd_4Z=S_LA@^gLowQUhqdzNox{J_RnllZ;7F8zb~KdT#@vsFHvmdpFF z?MVN@zlRRTZn)_M8bAw{R1*km>is##Sm4ZywVqq#StFLtxodOdvuEXxrCJUA+m6XH z`0U=T-7VW$e#9NS=XEUY^H!cY$Ikmoed-m~`OhXJ;kd}+`9b%%GiE|3ZFSuLtPPz$ z!_Thlha$fwS-NhA+#}h^Oc@8N;jUJ-DY-HZhlP8#v36A%I;-# zGf7x3Z%kanc(BfR!AoYfscF~0s>uo*$}3xcrcB;?2JaHpLT1KQj~0aI>F#dW;AyYv z7`f{!%hsD;<#+EAveH^b@QyPyY7KE;v z+%nJMLCfx!k9PRnIn^{dI8{kZ-Avj*`Um$k>thTIKZ^^xR_*Mx%JJNylh1Z*fm+qN z_rgYed^)BQI?1X3*_fCghD>^6Z+N4sQF8v$D=$BPWAWX`m=wltP`o$FKM zB8eAI9_f{{GHkt*@kDK#NZ|>s2M5HymhmwBI2h>g;QXlr>YgSXTzy{u*&1|z7N6@h zdNBJ(?)(LwdKz=i+=?+0ny#xhJ1f8``q7ESo<;VeIX92~;2V`kFZ3uV6R^>=Hn{yfi6x@7b6ldF%|n>K|ViMw8r+p{g{SJgVx#c%sL zoAtl_pO-Cip!BwNiNez6%A#4COA5KCO>KHF#USAMx%~Q+Szh6ck#1XL61M-%m=H5h zXYrlgSBsnLwV9(C5|49KUnT z6iJ3@Zm*UdOmW|qtbFa2^P)3)r)TwZOEpzoKX9G%der$!EoQ$vD`jqcpT@slNyfhK zu7Pvg%;TP0qNdzb^Lre2pq{xQkYCC+kooD7+%+F854bYz3AT8g7cU{mVVzR#$F+fR zf#W89e})^+Zg@2~KGc1rCwbp=!ndh*Cv7LyZcefc%Q<)P{LU>*N*?w9^Adz(Ep1sl zf6ude)WSMR^IY&(0Mocnatjx z(6eQ?)K6_*Umwyixkqa16+Yvx^Rg|Q?ssl}bG9PKZR?&IrL)mV9(HA-KHHA|+o3oo zyW;;6lc^tcSNd7SA1bkDOnqQeFVVgK%T~c9Tn;)N(caaK1xFk2+MKS>&SreDAd*o= z<2#d{=#{H#*LjXlJM|^z@y1(;;h8)PeRnkuibVw4pPkmUAmM()i|iW)#fxT_teffn z<;U!Oy$9WFgxi#^_FH>78;;waYl^?7Y9yY1=r1Pu-ugX=yF&n-8IE`2;}dC-Q&qMR(x7vDd| z3Qma#H8s9rSIF~W&z9{>0YM)=R_V-n*2I1KSg@|@gxO3cKf?q!-#lw3kS%2Mm+)~W{{0@(pkN>~h)7Gji zV4IfrZsn`3f23I6-efp8NB!r7J6wf7U&=9jsb+9USW;Q!oiVYJi;vOFdt#~G=UZF3 z9{&y$zV6d!E~+%aZ(TE=M3nzpiM0R6j@{ZlRV&N>wDYO6+f45MnmBWD*Yk>V={C2{ zcOAOU+>_6+;qtWd>j&m-Z(){^-s8FPV9M5odAZTESQBRU?U~B^Bjj1q_NNz5tJQ4t zWxF=*$Fa2C#}Xv&|Ky*`_Tfr;&Zq3(6VJvo@3ZygESlb)D;iLln11r)7L|oU(f$Qo zOneeLyk(+KjyNg*7MZc?+Me%|PM&mUogtsPW@;{;dKH~wQ_X?9+)b@97 z|5c&4+N;*`=%pb4MHa`+m)0J&o0W7*HR1k&yE_)P{HU>SHe{GF*`$%>M5EFTg*%L! zj^sHmczyH+6N6e<(Q28r|IcI|SIOUF)T)@nF-z5c#-TqtuFvFkpJS%tbh2;hUTPi1{&APRz5c5F_D`8 z^Um+P(U%x}B0Za=WS(tFnqc7lbY*ycW*Je$fM-ta&3{Ga8sN^cSIsiqe* z6jtXPxp*pN|K0jKPtP+f_$K*QZVm5LX~XHvOjl($FdfiQj_{K>x$e{lIfYvrEw^!1 zTwd`ijUngh@qN4h6~B3KOYPB%d2K9<7wtJ7Kb?^wE5Pa+TZ3qO+?s?fbHlD>|KVlO zW51J7_UHjyhwjg~_fc&!2}}vc3i;IUu703%F0wSHjH`jIr^;Khq=9D!(}B7iv+6xs zk_U=UT6%6jc=ra=vYqAC9}X{2^s_6M{aszDcj$mxHzy}s>CfW});H$pFcb)!Ugyim z#Sqe#u+uPgAKS@Ix&L`A)VaTfy4n6?dldKnd~yt9SpQbj z3v!=N%@;k;QC^U^=VsWEuyxOF8SY}c&CrzACUG*ZZf9PydPBlGqxR2G=eApHB?vRzyb3eT>V zW;?HJW>cs*+E|&oIoUa7spz%?0u%~F#J5NHOubwmj2r%Ricm9rZx25nsHEP=Z4b_Ws^4KGdnysejm%W zM(<2|0+U}uz@r6gR&+l%KD^qE?P%`zWelm!(+w8n7H@iK({u4=^_{0ePu9hnvoSVYGih?Tu*!%p!~5Xhc7Zr^ zNrq{45>{qRD)P}wV{YB~KI5RkvaY9-7#d#ku302k9_{t%{)dF}**?ujJ@sF%v&&}) z=*)CtWH`v0X*%U|++(u~EEASIWHCCk>5T7+^|7gMPP1#bOJ7(yGdS+qou2pqz4{?J7>sHq5d*?f$;lX znKz3+oh>q6l5A-)g;)8ygE7MyDTd?+7wRvVoUNX>_4BX&y|S;|g1Gv=G5PNB7Ufa9 zn45H4xUHZbR8b>L-TRE#uW zQD9o7b)?45=y&wS``@HpdTxeKpL&a5(Cy$%1_pL(&{}{K8<`nq)z+jfxMBZJbH~Td zjR|{`8=H2$$?Lp#>QL!9t^{VkE`~+2F6wfbwO&E%G7~aq z{rjN0@{73i4f6}3vfI)&li3*wMrEj_qzSzeQ zAM@D%h}MHOa%>D`#e08qdDl(O7kF^KU+y5+fp)Vsj){k^6$&{d=!CEPU);mYHlfk% z^KQ*Y73&l^4JNv;x+9W!TBYiS{r@=vzj&@ND1?Y^_{zfM;5jpKsqynnlg_8hR@6K* zN-J_)@>9maxX&?F;qhfpPM$T|_oIb51bU{i9jt9sJ!!hv!VnfIBMK(Dn4Ml zqVxG!yQc}G@&=nE)9bJDj}3k7)(n!5oo8*j!No9BqWsPCg?``E0*?GT^doiu=_C9Oix}4OnqKo%7F4-1 zTP^oWMgE2@EDt=Sm8LMY?T|5iXX9=D`(ch!spk{Z`s=qC{;{waU+FBrwDNqzj89A) zc8t@jyJv2`yU+2(4aRq?W<7uNM^C(*bH$U*W>1s6{Zl0t%$OV9r293LiJd=Pt2y z-SB7InX_{m3PduWZT&O*WZL@BBI)9H#h>n8m^4|PVTVX4<2!qSkBiOrsG07r<+&ZZ zJWA=#ku519U4@H!j0@Ii#g$dAxs~xDC|deW@6S2an~FEje6Z=vm(R6&(y8_z7fzWs z<+x#j&56hF~L#rYCZ}Iy?bx*70nP+|5 z-Zqw=+5MGKw{qIAradP&Uz*X{!0>frSBPbH{%_uWo_9}4Bp7a<`{eS+T3d#K-Ym-n z_d@5Hxz9;Z-`D7TF8ckeJ>HBbvp-4SS$ls*cEW_*o2S^G#~jLOe|o!c^ObG#A8mVE zLr?zO{d227YvIXm*(popH`$+gzMNr$^Rz2QiQ1fxCC;cPuy%U8u3!2229tAI4*Q~4 z2V+9JS24NaqMKU{>)&nV+A;I?jhB`NTRxq!VZAWt`#eiu zks_~zO1;J-5(h%}soh;r^7(pl$U?8nDYHCoxc{`A%FDc6rEt>r$N7H`?yhZ6yE9wR zL}lGng_$jz+e|S{4TiG_jQNgWQ*(d z-xeoll6;JBp~~{OMdx;^RLV_a z`0cr3^796PKO1}RZ`&S>E%6D^4inuoE$eE8G1_m zoAanxYpS|wO*q#+&4bnqTgqlzN^hP$$4lQWSZv;e9QLzMjEa+2E!|UGV0%)E^}+Wy z$Clfj+sC@a==QS<4j*0};N1`zFJSR9dZ+H^+tQ(Lw-pu4y*-cP(2uo+&b+fSX04iM zzpy&_s<4I4yUS8HKle7*eYkw4?BophQw%+-mpxS*mLIV?xjfnHuyDitM_VU84PoNC euvqJl{0Xn5_S+qe%NQ6K7(8A5T-G@yGywq0g-GN8 literal 0 HcmV?d00001 diff --git a/client/assets/textures/hills_normal_map.png b/client/assets/textures/hills_normal_map.png new file mode 100644 index 0000000000000000000000000000000000000000..6b5659534ed6df39b8d36d42b2af0b03b28b2097 GIT binary patch literal 35048 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 literal 0 HcmV?d00001 diff --git a/client/assets/textures/mountain.png b/client/assets/textures/mountain.png new file mode 100644 index 0000000000000000000000000000000000000000..7f2ff498bbfcd2c8373f0007784dc49fb991b10f GIT binary patch literal 36436 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 R2?hoR22WQ%mvv4FO#oS4C$j(m literal 0 HcmV?d00001 diff --git a/client/assets/textures/mountains_normal_map.png b/client/assets/textures/mountains_normal_map.png new file mode 100644 index 0000000000000000000000000000000000000000..acb054dafca9e8ae21e0880560843562e4b73c62 GIT binary patch literal 54816 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

%99bT^e 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 K&t;ucLK6T2Rh|F< literal 0 HcmV?d00001 diff --git a/client/assets/textures/sand.png b/client/assets/textures/sand.png new file mode 100644 index 0000000000000000000000000000000000000000..dfbd40c63f6a2949817283f77da07b418278678d GIT binary patch literal 11266 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 Zy)rV?P9I9^U|?Wi@O1TaS?83{1OUm?OHBX( literal 0 HcmV?d00001 diff --git a/client/css/style.css b/client/css/style.css index 0f27343..7b807c0 100644 --- a/client/css/style.css +++ b/client/css/style.css @@ -163,8 +163,7 @@ input:focus, select:focus { border-color: var(--accent); } } /* When WebGL owns the map, the DOM geometry layers are left empty and hidden; the icon layers (paths, targets, entities, labels) still render. */ -#map-world.gl-render .detail-layer, -#map-world.gl-render #layer-overview { display: none; } +#map-world.gl-render .detail-layer { display: none; } /* Promote the map to its own layer only while panning, so a drag is a GPU translation instead of a repaint of every hex. Keeping it off when idle avoids holding a huge composited layer in memory. */ @@ -174,13 +173,6 @@ input:focus, select:focus { border-color: var(--accent); } moved with `transform`; its tiles are positioned relative to it. */ .chunk { position: absolute; left: 0; top: 0; } -/* Zoomed-out level of detail: the detailed hex/fog/border layers are hidden and - a single simplified canvas is shown instead. */ -#layer-overview { display: none; } -#map-world.lod #layer-overview { display: block; } -#map-world.lod .detail-layer { display: none; } -.overview-block { position: absolute; } - .hex { position: absolute; width: var(--hex-w); @@ -191,13 +183,27 @@ input:focus, select:focus { border-color: var(--accent); } background-repeat: no-repeat; } .hex.terrain { - background-image: url("../assets/terrain.png"); - background-size: 441px 56px; + /* Tiling land textures are painted per tile by terrain.js; the atlas only + supplies the hexagonal alpha mask here. */ + background-repeat: repeat; + -webkit-mask-image: url("../assets/terrain.png"); + -webkit-mask-size: 441px 56px; + -webkit-mask-repeat: no-repeat; + mask-image: url("../assets/terrain.png"); + mask-size: 441px 56px; + mask-repeat: no-repeat; } .hex.fog { pointer-events: none; background-size: var(--hex-w) var(--hex-h); } +/* The DOM fallback cannot blend fog per vertex, so blur the whole fog layer: + its edge against the visible map fades instead of stepping at the hex line. + The WebGL path hides the layer entirely and does the blur in the shader. */ +#layer-fog { + filter: blur(3.5px); + -webkit-filter: blur(3.5px); +} .fog-unexplored { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='63' height='56'%3E%3Cpolygon points='63,28 47.25,0 15.75,0 0,28 15.75,56 47.25,56' fill='%2304040a'/%3E%3C/svg%3E"); } @@ -255,15 +261,24 @@ input:focus, select:focus { border-color: var(--accent); } box-shadow: 0 0 6px rgba(255, 43, 43, 0.85); } +/* Outlines the selected or inspected tile. The polygon is built in JS from the + same warped hexagon as the terrain, so it follows the geography. */ .tile-selection { position: absolute; width: var(--hex-w); height: var(--hex-h); - clip-path: polygon(100% 50%, 75% 0, 25% 0, 0 50%, 25% 100%, 75% 100%); - border: none; - box-shadow: inset 0 0 0 3px rgba(255, 255, 255, 0.85); + display: none; pointer-events: none; } +.tile-selection.active { display: block; } +.tile-selection svg { display: block; width: 100%; height: 100%; overflow: visible; } +.tile-selection polygon { + fill: rgba(255, 255, 255, 0.12); + stroke: rgba(255, 255, 255, 0.9); + stroke-width: 3; + stroke-linejoin: round; + vector-effect: non-scaling-stroke; +} .entity { position: absolute; } .unit { @@ -391,8 +406,10 @@ input:focus, select:focus { border-color: var(--accent); } box-shadow: 0 2px 6px rgba(0, 0, 0, 0.7); } /* City names live in their own layer above every unit, marker and status icon, - so they stay legible instead of being covered by whatever is nearby. */ -.city-label-layer { pointer-events: none; } + so they stay legible instead of being covered by whatever is nearby. The + layer needs an explicit z-index because those icons give their own children + z-index 2-4, which would otherwise paint over this layer. */ +.city-label-layer { pointer-events: none; z-index: 5; } .city-label { position: absolute; transform: translate(-50%, -100%); diff --git a/client/index.html b/client/index.html index 4b95d6d..22bf422 100644 --- a/client/index.html +++ b/client/index.html @@ -100,11 +100,11 @@

-
+
diff --git a/client/js/bench.js b/client/js/bench.js index 6779b90..b5fea7f 100644 --- a/client/js/bench.js +++ b/client/js/bench.js @@ -20,11 +20,11 @@ import { MapView } from "./map_view.js"; const MARKUP = `
-
+
@@ -34,11 +34,11 @@ const MARKUP = ` function mapLayers($) { return { - overview: $("#layer-overview"), terrain: $("#layer-terrain"), roads: $("#layer-roads"), borders: $("#layer-borders"), fog: $("#layer-fog"), + highlight: $("#layer-highlight"), paths: $("#layer-paths"), targets: $("#layer-targets"), entities: $("#layer-entities"), diff --git a/client/js/game_screen.js b/client/js/game_screen.js index 1424fde..2188fe6 100644 --- a/client/js/game_screen.js +++ b/client/js/game_screen.js @@ -24,6 +24,8 @@ export class GameScreen { this.protoUnits = PROTO_UNITS; this.selectedUnitIds = []; this.selectedCityId = 0; + // The tile whose info panel is open, if any (right-click inspection). + this._inspectedTile = null; this._trainingCityId = 0; this._cityBuildings = new Map(); this._population = new Map(); @@ -48,11 +50,11 @@ export class GameScreen { $("#map-viewport"), $("#map-world"), { - overview: $("#layer-overview"), terrain: $("#layer-terrain"), roads: $("#layer-roads"), borders: $("#layer-borders"), fog: $("#layer-fog"), + highlight: $("#layer-highlight"), paths: $("#layer-paths"), targets: $("#layer-targets"), entities: $("#layer-entities"), @@ -153,6 +155,7 @@ export class GameScreen { this.map.applySnapshot(state); this.selectedUnitIds = this.selectedUnitIds.filter((id) => this._unit(id)); this.map.setSelectedUnits(this.selectedUnitIds); + this._updateTileHighlight(); this._updateTopPanel(state); this._updateBottomPanel(); this._updateCityModal(); diff --git a/client/js/game_screen/panels.js b/client/js/game_screen/panels.js index 116f4cf..2b1eb6d 100644 --- a/client/js/game_screen/panels.js +++ b/client/js/game_screen/panels.js @@ -118,15 +118,41 @@ export const panelMethods = { } this.selectedUnitIds = units.map((unit) => unit.id); this.selectedCityId = 0; + if (units.length > 0) this._inspectedTile = null; this.map.setSelectedUnits(this.selectedUnitIds); this._hideStackMenu(); this._showUnits(units); + this._updateTileHighlight(); }, _selectedUnits() { return this.selectedUnitIds.map((id) => this._unit(id)).filter(Boolean); }, + // Outlines the tile the current selection or inspection sits on: the first + // selected unit's tile, else the selected city's, else the inspected tile. + _updateTileHighlight() { + if (this.selectedUnitIds.length > 0) { + const unit = this._unit(this.selectedUnitIds[0]); + if (unit) { + this.map.setSelectedTile({ x: unit.coords[0], y: unit.coords[1] }); + return; + } + } + if (this.selectedCityId) { + const city = this._city(this.selectedCityId); + if (city) { + this.map.setSelectedTile({ x: city.coords[0], y: city.coords[1] }); + return; + } + } + if (this._inspectedTile) { + this.map.setSelectedTile(this._inspectedTile); + return; + } + this.map.clearSelectedTile(); + }, + _openStackMenu(coords, ids, clientX, clientY) { const units = []; for (const id of ids) { @@ -550,11 +576,13 @@ export const panelMethods = { this._refreshBottomVisibility(); return; } + this._inspectedTile = null; this._renderTileInfo({ x: city.coords[0], y: city.coords[1] }); $("#city-name").text(city.name); $("#unit-info").addClass("hidden"); $("#tile-info, #city-info").removeClass("hidden"); this._refreshBottomVisibility(); + this._updateTileHighlight(); }, _showTile(coords) { @@ -562,12 +590,14 @@ export const panelMethods = { // snapshot would immediately switch the panel back to the old selection. this.selectedCityId = 0; this.selectedUnitIds = []; + this._inspectedTile = { x: coords.x, y: coords.y }; this.map.setSelectedUnits([]); this._hideStackMenu(); this._renderTileInfo(coords); $("#city-info, #unit-info").addClass("hidden"); $("#tile-info").removeClass("hidden"); this._refreshBottomVisibility(); + this._updateTileHighlight(); }, // Fills the shared tile fields without touching which section is visible. diff --git a/client/js/map_view.js b/client/js/map_view.js index 401d578..7a4667e 100644 --- a/client/js/map_view.js +++ b/client/js/map_view.js @@ -27,6 +27,16 @@ export class MapView { : $("#layer-roads"); this.$borders = layers.borders; this.$fog = layers.fog; + // The outline drawn over the selected or inspected tile. It sits above the + // terrain and fog but below the unit, target and city icons. + this.$highlight = layers.highlight && layers.highlight.length + ? layers.highlight + : $("#layer-highlight"); + if (!this.$highlight.length) { + this.$highlight = $('
'); + if (this.$fog && this.$fog.length) this.$fog.after(this.$highlight); + else this.$world.append(this.$highlight); + } this.$paths = layers.paths; // Bombardment target lines and rings, drawn over the terrain and paths but // under the unit and city icons. @@ -34,15 +44,6 @@ export class MapView { ? layers.targets : $("#layer-targets"); this.$entities = layers.entities; - // The simplified map used when zoomed out. It is created on demand so - // callers (and tests) that predate it keep working unchanged. - this.$overview = layers.overview && layers.overview.length - ? layers.overview - : $("#layer-overview"); - if (!this.$overview.length) { - this.$overview = $('
'); - this.$world.prepend(this.$overview); - } // City names live on their own layer above the entities so they are never // hidden behind a unit or city marker. this.$labels = layers.labels && layers.labels.length @@ -64,6 +65,8 @@ export class MapView { this.explored = new Set(); this.visible = new Set(); this.territory = new Map(); + // Each land tile's region, keyed "x,y" and mapped to the region's city id. + this.regions = new Map(); // Pre-generated road tiles, keyed "x,y". this.roads = new Set(); this.civilisations = []; @@ -76,6 +79,11 @@ export class MapView { this._centered = false; this._selectedUnitIds = new Set(); this._selectedTile = null; + // Region (city id) of the selected/inspected tile, highlighted as a whole. + this._selectedRegion = null; + // Lazily built SVG outline for the selected/inspected tile. + this._highlightEl = null; + this._highlightPoly = null; this._unitViews = new Map(); this._unitMotion = new Map(); @@ -88,6 +96,7 @@ export class MapView { this._maxStepLength = 1.0; this._territorySignature = ""; + this._regionsSignature = null; this._roadsSignature = null; this._exploredCount = -1; this._visibleSignature = null; @@ -95,6 +104,7 @@ export class MapView { // Cylindrical wrap state. `_period` is the horizontal pixel period (0 on a // flat map); every entity is shifted to the copy nearest the camera. this._period = 0; + this._textureRepeat = 0; this._worldWidth = 0; this._worldHeight = 0; this._worldMinX = 0; @@ -115,7 +125,6 @@ export class MapView { this._borderPool = []; this._roadPool = []; this._wrapperPool = []; - this.lod = false; this.onUnitSelected = () => {}; this.onCitySelected = () => {}; @@ -125,8 +134,8 @@ export class MapView { this.onScheduleOrdered = () => {}; this.onStackMenu = () => {}; - // The map (terrain, roads, borders, fog, overview) is drawn on a WebGL - // canvas when the browser provides a context; the icon layers stay DOM. + // The map (terrain, roads, borders, fog) is drawn on a WebGL canvas when + // the browser provides a context; the icon layers stay DOM. // "dom" forces the classic renderer (tests, benchmark, troubleshooting). this.glRenderer = null; this.glCanvas = null; diff --git a/client/js/map_view/constants.js b/client/js/map_view/constants.js index 00bd002..aa207f2 100644 --- a/client/js/map_view/constants.js +++ b/client/js/map_view/constants.js @@ -12,11 +12,6 @@ export const ZOOM_MAX = 2.0; export const BATTLE_SIDE_OFFSET = 15; export const BATTLE_STACK_STEP = 9; -// The zoomed-out overview groups this many tiles per side into one block, so a -// full world is a few hundred flat elements instead of tens of thousands of -// textured ones. -export const OVERVIEW_BLOCK = 4; - // The detailed view is built from square blocks of tiles. Only the blocks that // intersect the viewport (plus CHUNK_MARGIN blocks of slack on every side) are // materialised, so panning costs the same no matter how big the map is. Tiles @@ -27,17 +22,3 @@ export const CHUNK_MARGIN = 1; // already visited reattaches them instead of rebuilding hundreds of tiles. The // cap bounds how many detached tile nodes can be held at once. export const CHUNK_CACHE_LIMIT = 160; - -// Flat colours used by the simplified overview, keyed by terrain type. The -// full-resolution view keeps the textured hexes; this is only the LOD stand-in. -export const OVERVIEW_COLOURS = { - Forest: "#2f6b3a", - Hills: "#7a6a44", - Mountain: "#7d7469", - Desert: "#c2b280", - Ice: "#dcecf5", - Land: "#4f8a4f", - Sea: "#1b3f6b", -}; -export const OVERVIEW_SEA = "#1b3f6b"; -export const OVERVIEW_FOG = "#04040a"; diff --git a/client/js/map_view/distortion.js b/client/js/map_view/distortion.js new file mode 100644 index 0000000..4786ffa --- /dev/null +++ b/client/js/map_view/distortion.js @@ -0,0 +1,218 @@ +// Seed-driven, purely cosmetic distortion of the hex grid. +// +// The server's grid must stay perfectly regular: movement, range and +// pathfinding all use the ideal hex centres. This module warps only the +// *drawing* of the terrain, so the world looks a little less mechanical while +// the simulation is untouched. The warp is a pure function of the world seed, +// so every client that joins the same game sees exactly the same shape. +// +// A hexagon vertex is shared by three tiles, so its offset is derived from the +// vertex's position on the lattice rather than from the tile: shared corners +// move by exactly the same amount and the mesh stays watertight (no cracks +// between neighbouring hexes). The lattice is integral in units of HEX_W/4 +// horizontally and HEX_H/2 vertically, which are exactly representable, so the +// rounded coordinates are identical for every tile that touches a vertex. +// +// Each edge is then split into EDGE_SEGMENTS pieces and bent by a perpendicular +// wobble that vanishes at both corners, so the outline reads as wavy while the +// corners still meet exactly. Edges are canonicalised by their two lattice +// endpoints, so the two tiles that share an edge bend it identically. +// +// On a cylindrical map the world repeats every `3 * width` lattice columns, so +// folding the horizontal coordinate by that period keeps the wrap seamless. + +import { HEX_W, HEX_H } from "../../../shared/hex.js"; + +// Lattice unit lengths: a vertex is always a whole number of these from the +// tile centre (63/4 and 56/2 are exact binary fractions). +const UNIT_X = HEX_W / 4; +const UNIT_Y = HEX_H / 2; + +// How far a vertex may wander, how far the midpoint of an edge may bow, and how +// far a road may drift inside a tile, all in world pixels. The vertex wander +// stays well under half the shortest hexagon edge (~31 px) so edges never fold. +export const DISTORTION = { x: 7, y: 6, wobble: 3.5, road: 3 }; + +// Every hexagon edge is split into this many wobbled pieces. More pieces make +// the bowed edge read as a smooth curve rather than a jagged run of facets. +export const EDGE_SEGMENTS = 4; + +// The six corners of a flat-top hexagon, relative to its centre, in the order +// the rest of the view uses (right, then counter-clockwise). +export const CORNERS = [ + { x: HEX_W / 2, y: 0 }, + { x: HEX_W / 4, y: -HEX_H / 2 }, + { x: -HEX_W / 4, y: -HEX_H / 2 }, + { x: -HEX_W / 2, y: 0 }, + { x: -HEX_W / 4, y: HEX_H / 2 }, + { x: HEX_W / 4, y: HEX_H / 2 }, +]; + +// How many horizontal lattice units one wrap period spans, or 0 on a flat map. +export function periodUnits(topology) { + if (!topology || !topology.cylindrical || !topology.width) return 0; + return topology.width * 3; +} + +// A small integer avalanche hash; the lattice coordinates may be negative. +function hash32(value) { + let h = value | 0; + h = Math.imul(h ^ (h >>> 16), 0x7feb352d); + h = Math.imul(h ^ (h >>> 15), 0x846ca68b); + return (h ^ (h >>> 16)) >>> 0; +} + +function vertexHash(seed, salt, xi, yi) { + let h = (seed | 0) ^ Math.imul(salt | 0, 0x9e3779b1); + h = Math.imul(h ^ (xi | 0), 0x85ebca6b); + h = Math.imul(h ^ (yi | 0), 0xc2b2ae35); + return hash32(h); +} + +function edgeHash(seed, a, b, index) { + let h = (seed | 0) ^ Math.imul(index | 0, 0x165667b1); + h = Math.imul(h ^ (a.xi | 0), 0x9e3779b1); + h = Math.imul(h ^ (a.yi | 0), 0x85ebca6b); + h = Math.imul(h ^ (b.xi | 0), 0xc2b2ae35); + h = Math.imul(h ^ (b.yi | 0), 0x27d4eb2d); + return hash32(h); +} + +// Signed [-1, 1) value of an edge/bend hash. +function signedEdgeHash(seed, a, b, index) { + return edgeHash(seed, a, b, index) / 4294967296 * 2 - 1; +} + +// A sine that is exactly zero at t=0 and t=1 (sin(PI) is only ~1e-16, which +// an unnormalised hash would turn into a visible crack at the corners). +function envelope(t) { + return t <= 0 || t >= 1 ? 0 : Math.sin(Math.PI * t); +} + +function latticeOf(x, y) { + return { xi: Math.round(x / UNIT_X), yi: Math.round(y / UNIT_Y) }; +} + +// Stable integer key of the lattice vertex nearest a world point. Two tiles that +// share a corner produce exactly the same key, which lets callers stitch their +// borders into one continuous run without floating-point comparison. +export function latticeKey(x, y) { + return `${Math.round(x / UNIT_X)},${Math.round(y / UNIT_Y)}`; +} + +function latticeBefore(a, b) { + return a.xi !== b.xi ? a.xi < b.xi : a.yi < b.yi; +} + +// Deterministic offset of one lattice vertex, in world pixels. +export function vertexOffset(seed, xi, yi, period) { + let vx = xi | 0; + if (period > 0) vx = ((vx % period) + period) % period; + const nx = vertexHash(seed, 0x1b873593, vx, yi) / 4294967296; + const ny = vertexHash(seed, 0xcc9e2d51, vx, yi) / 4294967296; + return { + x: (nx * 2 - 1) * DISTORTION.x, + y: (ny * 2 - 1) * DISTORTION.y, + }; +} + +// Displaces an ideal lattice vertex at world (x, y). +export function distortPoint(seed, x, y, period) { + const offset = vertexOffset(seed, Math.round(x / UNIT_X), Math.round(y / UNIT_Y), period); + return { x: x + offset.x, y: y + offset.y }; +} + +// The six displaced corners of the tile centred at `centre`, in CORNERS order. +export function distortedCorners(seed, centre, period) { + const points = []; + for (const corner of CORNERS) { + points.push(distortPoint(seed, centre.x + corner.x, centre.y + corner.y, period)); + } + return points; +} + +// A point at parameter t (0..1) along the jagged edge between ideal lattice +// vertices a and b. The edge is canonicalised so both tiles that share it draw +// the same zig-zag, and the perpendicular offset vanishes at t=0 and t=1 so the +// edge still meets its neighbours' corners exactly. +export function edgePoint(seed, a, b, t, period) { + let ia = latticeOf(a.x, a.y); + let ib = latticeOf(b.x, b.y); + // Fold the wrapped axis before ordering and hashing, so the two copies of a + // seam edge are canonicalised and jagged identically. + if (period > 0) { + ia.xi = ((ia.xi % period) + period) % period; + ib.xi = ((ib.xi % period) + period) % period; + } + let p0 = a; + let p1 = b; + let param = t; + if (latticeBefore(ib, ia)) { + const swap = ia; + ia = ib; + ib = swap; + p0 = b; + p1 = a; + param = 1 - t; + } + const o0 = vertexOffset(seed, ia.xi, ia.yi, period); + const o1 = vertexOffset(seed, ib.xi, ib.yi, period); + const ax = p0.x + o0.x; + const ay = p0.y + o0.y; + const bx = p1.x + o1.x; + const by = p1.y + o1.y; + const x = ax + (bx - ax) * param; + const y = ay + (by - ay) * param; + const dx = bx - ax; + const dy = by - ay; + const length = Math.hypot(dx, dy) || 1; + // An independent offset at every sampled point, so each segment kicks a + // different way: a jagged saw-tooth rather than a flowing curve. The offset + // still vanishes exactly at both corners. + const wobble = + signedEdgeHash(seed, ia, ib, Math.round(param * 4096)) * + DISTORTION.wobble * + envelope(param); + return { x: x - (dy / length) * wobble, y: y + (dx / length) * wobble }; +} + +// The closed wavy boundary of the tile centred at `centre`. Edge i occupies +// entries [i*segments, (i+1)*segments); entries also carry the ideal position +// and the parameter t so callers can derive texture UVs and blend weights. +export function distortedBoundary(seed, centre, period, segments = EDGE_SEGMENTS) { + const points = []; + for (let i = 0; i < CORNERS.length; i++) { + const c0 = CORNERS[i]; + const c1 = CORNERS[(i + 1) % CORNERS.length]; + const a = { x: centre.x + c0.x, y: centre.y + c0.y }; + const b = { x: centre.x + c1.x, y: centre.y + c1.y }; + for (let j = 0; j < segments; j++) { + const t = j / segments; + const p = edgePoint(seed, a, b, t, period); + points.push({ + x: p.x, + y: p.y, + idealX: a.x + (b.x - a.x) * t, + idealY: a.y + (b.y - a.y) * t, + t, + }); + } + } + return points; +} + +// A gently bent interior path from `from` to `to` at parameter t. The wobble is +// zero at both ends, so a road leaves a tile centre and reaches the border +// exactly where its neighbouring tile's half does. +export function bentPath(seed, from, to, salt, t) { + const x = from.x + (to.x - from.x) * t; + const y = from.y + (to.y - from.y) * t; + const dx = to.x - from.x; + const dy = to.y - from.y; + const length = Math.hypot(dx, dy) || 1; + const amount = + signedEdgeHash(seed, latticeOf(from.x, from.y), latticeOf(to.x, to.y), salt) * + DISTORTION.road * + envelope(t); + return { x: x - (dy / length) * amount, y: y + (dx / length) * amount }; +} diff --git a/client/js/map_view/entities.js b/client/js/map_view/entities.js index c9d3a2c..780c590 100644 --- a/client/js/map_view/entities.js +++ b/client/js/map_view/entities.js @@ -17,6 +17,7 @@ export const entityMethods = { this.setRoads(state.roads || []); this.localCiv = state.viewer; this._applyTerritory(state.territory); + this._applyRegions(state.regions || []); this._applyKnowledge(state.explored, state.visible); this._applyBattles(state.battles); this._syncCities(state.cities); @@ -68,7 +69,17 @@ export const entityMethods = { this._renderBorders(); if (this.glRenderer) this.glRenderer.buildBorders(this); - this._drawOverview(); + }, + + // Mirrors each land tile's region (the city that claims it). The region of a + // selected or inspected tile is drawn as a whole, so this only has to refresh + // the overlay; nothing is drawn for an unselected map. + _applyRegions(entries) { + const signature = hashEntries(entries); + if (signature === this._regionsSignature) return; + this._regionsSignature = signature; + this.regions = new Map(entries.map((entry) => [key(entry[0], entry[1]), entry[2]])); + if (this.glRenderer) this.glRenderer.buildRegionOverlay(this); }, // Draws a thick tinted segment along every edge shared with a different owner @@ -155,7 +166,6 @@ export const entityMethods = { this.visible = new Set(visibleEntries.map((e) => key(e[0], e[1]))); this._refreshFog(); - this._drawOverview(); }, _syncCities(cities) { diff --git a/client/js/map_view/fog.js b/client/js/map_view/fog.js new file mode 100644 index 0000000..6859743 --- /dev/null +++ b/client/js/map_view/fog.js @@ -0,0 +1,79 @@ +// Fog-of-war levels and their blur across the visibility frontier. +// +// Every tile maps to a fog colour and opacity: nothing over currently visible +// ground, a soft veil over explored ground and an opaque shroud over unexplored +// ground. Drawing those values per tile would leave a hard hex edge wherever +// visibility changes, so each *vertex* takes the distance-weighted average of +// the tiles around it. The frontier then ramps over about one tile and the +// explored/unexplored colours mix into each other instead of stepping. +// +// The kernel mirrors the land-texture blend, so a shared hex vertex produces +// the same value whichever of the two tiles evaluates it. + +import { HEX_H } from "../../../shared/hex.js"; + +export const FOG_EXPLORED_RGB = [4 / 255, 6 / 255, 18 / 255]; +export const FOG_UNEXPLORED_RGB = [4 / 255, 4 / 255, 10 / 255]; +export const FOG_EXPLORED_OPACITY = 0.62; +export const FOG_UNEXPLORED_OPACITY = 1.0; + +// Kernel radius. It reaches just past the shared edge (a neighbour's centre is +// about HEX_H away), so the frontier feathers over roughly a third of a tile +// while the fog stays at full strength either side of it. A wider kernel washed +// the fog out across the whole frontier. +const BLEND_RADIUS = HEX_H * 0.72; + +function falloff(distance) { + if (distance >= BLEND_RADIUS) return 0; + const t = 1 - distance / BLEND_RADIUS; + return t * t; +} + +// The fog colour and opacity of a single tile. +export function fogLevel(visible, explored) { + if (visible) return { rgb: FOG_EXPLORED_RGB, opacity: 0 }; + if (explored) return { rgb: FOG_EXPLORED_RGB, opacity: FOG_EXPLORED_OPACITY }; + return { rgb: FOG_UNEXPLORED_RGB, opacity: FOG_UNEXPLORED_OPACITY }; +} + +// The tile has fog of its own; a fully visible tile only needs geometry when a +// neighbour is fogged, which the ring check covers. +export function isFogged(level) { + return !!level && level.opacity > 0; +} + +// Blurs the fog of the tiles around a world point. +// +// `samples` is a list of { level, dx, dy } with the offset from the point to +// the tile centre; levels that are missing (off-map neighbours) are ignored. +// Returns { rgb, opacity } where opacity is the faded alpha and rgb the +// opacity-weighted colour of the fog around the point. +export function blurFog(samples) { + let total = 0; + let opacity = 0; + let r = 0; + let g = 0; + let b = 0; + let colourWeight = 0; + for (const sample of samples) { + if (!sample.level) continue; + const weight = falloff(Math.hypot(sample.dx, sample.dy)); + if (weight <= 0) continue; + total += weight; + opacity += weight * sample.level.opacity; + if (sample.level.opacity > 0) { + const w = weight * sample.level.opacity; + r += sample.level.rgb[0] * w; + g += sample.level.rgb[1] * w; + b += sample.level.rgb[2] * w; + colourWeight += w; + } + } + if (total <= 0 || colourWeight <= 0) { + return { rgb: FOG_UNEXPLORED_RGB, opacity: 0 }; + } + return { + rgb: [r / colourWeight, g / colourWeight, b / colourWeight], + opacity: opacity / total, + }; +} diff --git a/client/js/map_view/input.js b/client/js/map_view/input.js index 154f817..de1372b 100644 --- a/client/js/map_view/input.js +++ b/client/js/map_view/input.js @@ -3,7 +3,7 @@ import { mapToLocal, key, parity, COL_STEP, HEX_W, HEX_H } from "../../../shared/hex.js"; import { DRAG_THRESHOLD, ZOOM_MIN, ZOOM_MAX } from "./constants.js"; -import { clamp } from "./utils.js"; +import { CORNERS, distortedBoundary, periodUnits } from "./distortion.js"; export const inputMethods = { // ------------------------------------------------------------- camera -- @@ -32,43 +32,15 @@ export const inputMethods = { _applyCamera(applyWrap = true) { const width = this.$viewport.width(); const height = this.$viewport.height(); - this._updateLod(width, height); - if (this.lod) { - // With the whole world on screen there is nothing to scroll to; keep the - // camera over the map so the overview never drifts into the void. - this.camera.x = clamp(this.camera.x, this._worldMinX, this._worldMinX + this._worldWidth); - this.camera.y = clamp(this.camera.y, this._worldMinY, this._worldMinY + this._worldHeight); - } const { x, y, zoom } = this.camera; const tx = width / 2 - x * zoom; const ty = height / 2 - y * zoom; this.$world.css("transform", `translate(${tx}px, ${ty}px) scale(${zoom})`); if (applyWrap) this._syncChunks(); + if (this._selectedTile) this._positionHighlight(); this._glRender(); }, - // Switches between the textured hex view and the simplified overview. The - // detailed view only makes sense while the visible world stays within one - // horizontal period (so wrapping is seamless); past that -- when the player - // is zoomed out to see the whole map -- the coarse overview takes over. - _updateLod(width, height) { - let detailedWidth = this._worldWidth; - if (this._period > 0) detailedWidth = Math.min(detailedWidth, this._period) - HEX_W; - const detailed = detailedWidth > 0 && - width / this.camera.zoom <= detailedWidth && - height / this.camera.zoom <= this._worldHeight + HEX_H; - const lod = !detailed; - if (lod === this.lod) return; - this.lod = lod; - this.$world.toggleClass("lod", lod); - if (lod) { - this._clearChunks(); - this._drawOverview(true); - } else { - this._chunkDirty = true; - } - }, - _setupCameraInput() { let dragging = false; let dragged = false; @@ -220,6 +192,66 @@ export const inputMethods = { this.onUnitSelected(0); }, + // ---------------------------------------------------- tile highlight -- + + // Outlines the tile under the current selection or inspection. The outline + // follows the same warped hexagon the terrain draws, so it sits exactly on + // the distorted tile; on the DOM renderer (no warp) it is a plain hexagon. + setSelectedTile(coords) { + this._selectedTile = coords ? { x: coords.x, y: coords.y } : null; + this._selectedRegion = this._selectedTile && this.regions + ? (this.regions.get(key(this._selectedTile.x, this._selectedTile.y)) ?? null) + : null; + if (!this._selectedTile || !this.topology) { + if (this._highlightEl) this._highlightEl.classList.remove("active"); + if (this.glRenderer) this.glRenderer.buildRegionOverlay(this); + return; + } + this._ensureHighlight(); + const local = mapToLocal(this._selectedTile.x, this._selectedTile.y); + const outline = this.glRenderer + ? distortedBoundary(this.seed | 0, local, periodUnits(this.topology)) + : CORNERS.map((corner) => ({ x: local.x + corner.x, y: local.y + corner.y })); + const points = outline + .map((corner) => + `${(corner.x - local.x + HEX_W / 2).toFixed(2)},` + + `${(corner.y - local.y + HEX_H / 2).toFixed(2)}`) + .join(" "); + this._highlightPoly.setAttribute("points", points); + this._highlightEl.classList.add("active"); + this._positionHighlight(); + if (this.glRenderer) this.glRenderer.buildRegionOverlay(this); + }, + + clearSelectedTile() { + this.setSelectedTile(null); + }, + + _ensureHighlight() { + if (this._highlightEl) return; + const ns = "http://www.w3.org/2000/svg"; + const el = document.createElement("div"); + el.className = "tile-selection"; + const svg = document.createElementNS(ns, "svg"); + svg.setAttribute("viewBox", `0 0 ${HEX_W} ${HEX_H}`); + svg.setAttribute("preserveAspectRatio", "none"); + const poly = document.createElementNS(ns, "polygon"); + svg.appendChild(poly); + el.appendChild(svg); + this.$highlight[0].appendChild(el); + this._highlightEl = el; + this._highlightPoly = poly; + }, + + // Keeps the (already measured) outline on the wrapped copy nearest the camera. + _positionHighlight() { + if (!this._highlightEl || !this._selectedTile) return; + const local = mapToLocal(this._selectedTile.x, this._selectedTile.y); + const wrapped = this._wrappedPosition(local); + this._highlightEl.style.left = `${wrapped.x - HEX_W / 2}px`; + this._highlightEl.style.top = `${wrapped.y - HEX_H / 2}px`; + }, + // ------------------------------------------------------- hit-testing -- unitIdsAt(coords) { diff --git a/client/js/map_view/terrain.js b/client/js/map_view/terrain.js index 8f147fd..586f83c 100644 --- a/client/js/map_view/terrain.js +++ b/client/js/map_view/terrain.js @@ -1,5 +1,4 @@ -// Terrain construction, world-space bounds, the coarse overview used at low -// zoom, and the chunked world-space layers. +// Terrain construction, world-space bounds and the chunked world-space layers. // // The detailed map is not one element per tile of the whole world. It is built // from square CHUNK_SIZE x CHUNK_SIZE tile blocks that are materialised only @@ -14,8 +13,8 @@ import { MapTopology, mapToLocal, key, parseKey, HEX_W, HEX_H, COL_STEP } from " import { MapGenerator } from "../../../shared/map_generator.js"; import { TerrainStats } from "../../../shared/terrain_stats.js"; import { MAP_CONFIG } from "../../../shared/data.js"; -import { OVERVIEW_BLOCK, OVERVIEW_FOG, CHUNK_SIZE, CHUNK_MARGIN, CHUNK_CACHE_LIMIT } from "./constants.js"; -import { overviewColour, blendColours } from "./utils.js"; +import { CHUNK_SIZE, CHUNK_MARGIN, CHUNK_CACHE_LIMIT } from "./constants.js"; +import { terrainLayers, textureRepeat } from "./textures.js"; export const terrainMethods = { ensureTerrain(seed, mapConfig = null) { @@ -40,6 +39,8 @@ export const terrainMethods = { this.terrainStats = new TerrainStats(this.tiles); this._maxStepLength = this._computeMaxStepLength(); this._period = this.topology.periodPixels(); + // World pixels per tiling-texture repeat, snapped to the wrap period. + this._textureRepeat = textureRepeat(this._period); this._computeWorldBounds(); this._chunkSize = CHUNK_SIZE; @@ -59,6 +60,7 @@ export const terrainMethods = { this.$fog.addClass("detail-layer"); this._territorySignature = ""; + this._regionsSignature = null; this._exploredCount = -1; this._visibleSignature = null; this._chunkDirty = true; @@ -74,19 +76,19 @@ export const terrainMethods = { for (const view of this._battleViews.values()) view.remove(); this._battleViews.clear(); this._battles.clear(); + // A new world invalidates the old tile outline; the game screen re-applies + // the current selection once the new snapshot has been read. + this.clearSelectedTile(); this._centered = false; this._chunkDirty = true; // A WebGL build reads the whole world out of the view, so hand it the new // terrain before any of the per-layer update hooks run. if (this.glRenderer) this.glRenderer.setWorld(this); - // The overview is only built when the player is (or becomes) zoomed out; - // keeping it out of startup leaves the initial view to the visible chunks. - if (this.lod) this._drawOverview(true); this._syncChunks(); }, - // Pixel bounds of the whole world, used to lay out the overview and clamp the - // camera when the simplified map is on screen. + // Pixel bounds of the whole world, used to draw the wrapped copies of the + // terrain in the renderer. _computeWorldBounds() { let minX = Infinity; let maxX = -Infinity; @@ -251,13 +253,6 @@ export const terrainMethods = { this._releaseWrapper(rec.borderEl); }, - // Parks every materialised chunk (used when the overview takes over) so - // zooming back in is cheap. - _clearChunks() { - if (!this._chunks || this._chunks.size === 0) return; - for (const [chunkKey, rec] of Array.from(this._chunks)) this._detachChunk(chunkKey, rec); - }, - _destroyAllChunks() { if (this._chunks) { for (const rec of this._chunks.values()) this._destroyChunk(rec); @@ -273,7 +268,7 @@ export const terrainMethods = { // CHUNK_MARGIN blocks) and releases the rest. Cheap enough to run each frame: // it walks the chunk grid, not the tiles. _syncChunks() { - if (!this.topology || !this._chunks || this.lod) return; + if (!this.topology || !this._chunks) return; // With WebGL the map lives in one canvas, not in DOM chunks. if (this.glRenderer) return; const width = this.$viewport.width(); @@ -345,13 +340,38 @@ export const terrainMethods = { div.className = "hex terrain detail-layer"; div.style.left = `${local.x - rec.x - HEX_W / 2}px`; div.style.top = `${local.y - rec.y - HEX_H / 2}px`; - div.style.backgroundPosition = `${-tile.col * HEX_W}px 0`; + this._applyTerrainTexture(div, tile, local); fragment.appendChild(div); } } rec.terrainEl.appendChild(fragment); }, + // Paints one terrain tile in the DOM fallback. The tiling textures are + // positioned from the tile's world origin, so two neighbours continue the + // same pattern; the legacy atlas provides the hexagonal alpha mask. Terrains + // without a tiling texture keep using the atlas art directly. + _applyTerrainTexture(div, tile, local) { + const left = local.x - HEX_W / 2; + const top = local.y - HEX_H / 2; + const repeat = this._textureRepeat || 0; + const layers = terrainLayers(tile.terrainType); + const maskPosition = `${-tile.col * HEX_W}px 0`; + if (layers.length && repeat > 0) { + const position = `${-left}px ${-top}px`; + div.style.backgroundImage = layers.map((src) => `url("${src}")`).join(", "); + div.style.backgroundSize = `${repeat}px ${repeat}px`; + // The overlay (if any) is the top layer and shares the base's position. + div.style.backgroundPosition = layers.map(() => position).join(", "); + } else { + div.style.backgroundImage = 'url("assets/terrain.png")'; + div.style.backgroundSize = "441px 56px"; + div.style.backgroundPosition = maskPosition; + } + div.style.maskPosition = maskPosition; + div.style.webkitMaskPosition = maskPosition; + }, + // Fog is rebuilt per materialised chunk, so a change in visibility only costs // the few hundred tiles actually on screen instead of the whole map. _buildFog(rec) { @@ -477,7 +497,7 @@ export const terrainMethods = { // [camera - P/2, camera + P/2) keeps neighbouring tiles contiguous, so the // world tiles seamlessly in both directions. _wrapShift(centerX) { - if (!this._period || this.lod) return 0; + if (!this._period) return 0; const period = this._period; return Math.ceil((this.camera.x - period / 2 - centerX) / period) * period; }, @@ -486,100 +506,6 @@ export const terrainMethods = { return { x: point.x + this._wrapShift(point.x), y: point.y }; }, - // ---------------------------------------------------------------- LOD -- - - // Builds the simplified map as a coarse grid: one flat block per - // OVERVIEW_BLOCK x OVERVIEW_BLOCK tiles. At low zoom these few hundred - // elements replace the detailed chunks, so seeing the entire map stays cheap. - _drawOverview(force = false) { - if (this.glRenderer) { - if (force || this.lod) this.glRenderer.buildOverview(this); - return; - } - if (!this.$overview || !this.$overview.length || !this.topology) return; - // While the detailed view is on screen the overview is hidden; keep the - // redraw for the moment the player zooms out rather than paying for it on - // every hourly snapshot. - if (!force && !this.lod) return; - - const step = OVERVIEW_BLOCK; - const startX = this.topology.originX; - const startY = this.topology.originY; - const endX = startX + this.topology.width; - const endY = startY + this.topology.height; - const fragment = document.createDocumentFragment(); - for (let by = startY; by < endY; by += step) { - for (let bx = startX; bx < endX; bx += step) { - const block = this._overviewBlock(bx, by, step); - if (block) fragment.appendChild(block); - } - } - this.$overview.empty()[0].appendChild(fragment); - }, - - // The block's geometry and flat colour, independent of how it is drawn. The - // DOM overview turns it into a positioned div; the WebGL renderer turns it - // into a coloured quad. Returns null for a block with no tiles. - _overviewBlockData(bx, by, step) { - const endX = Math.min(bx + step, this.topology.originX + this.topology.width); - const endY = Math.min(by + step, this.topology.originY + this.topology.height); - let minX = Infinity; - let maxX = -Infinity; - let minY = Infinity; - let maxY = -Infinity; - let representative = null; - let land = null; - let owner = -1; - let visible = false; - let explored = false; - for (let y = by; y < endY; y++) { - for (let x = bx; x < endX; x++) { - const k = key(x, y); - const tile = this.tiles[k]; - if (!tile) continue; - if (!representative) representative = tile; - if (tile.terrainClass !== "Sea" && !land) land = tile; - const local = mapToLocal(x, y); - minX = Math.min(minX, local.x - HEX_W / 2); - maxX = Math.max(maxX, local.x + HEX_W / 2); - minY = Math.min(minY, local.y - HEX_H / 2); - maxY = Math.max(maxY, local.y + HEX_H / 2); - const tileOwner = this.territory.get(k); - if (tileOwner !== undefined && tileOwner >= 0 && this.civilisations[tileOwner]) { - owner = tileOwner; - } - if (this.visible.has(k)) visible = true; - if (this.explored.has(k)) explored = true; - } - } - if (!representative) return null; - - let colour = overviewColour(land || representative); - if (owner >= 0) colour = blendColours(colour, this.civilisations[owner].primaryColour, 0.45); - if (!visible) colour = blendColours(colour, OVERVIEW_FOG, explored ? 0.62 : 1); - - return { - minX, - minY, - width: maxX - minX, - height: maxY - minY, - colour, - }; - }, - - _overviewBlock(bx, by, step) { - const data = this._overviewBlockData(bx, by, step); - if (!data) return null; - const div = document.createElement("div"); - div.className = "overview-block"; - div.style.left = `${data.minX}px`; - div.style.top = `${data.minY}px`; - div.style.width = `${data.width}px`; - div.style.height = `${data.height}px`; - div.style.background = data.colour; - return div; - }, - _computeMaxStepLength() { let longest = 0; for (const neighbour of this.topology.neighbours(0, 0)) { diff --git a/client/js/map_view/textures.js b/client/js/map_view/textures.js new file mode 100644 index 0000000..b845720 --- /dev/null +++ b/client/js/map_view/textures.js @@ -0,0 +1,192 @@ +// Texture assignment and blending for the world map. +// +// The land is no longer drawn from the per-tile terrain.png atlas. Every land +// terrain sits on a seamless tiling base (grass, or sand for the desert) and +// forest, hills and mountains composite their own translucent overlay on top. +// Sea is flagged for the procedural water shader and ice keeps the legacy atlas +// art, so neither is textured or blended here. +// +// The tiling textures are addressed in *world* space, not per tile: every +// vertex carries its undistorted world position and the shader divides it by a +// fixed repeat. Two tiles that share an edge therefore sample the same part of +// the same texture, so the grain runs continuously across the wavy, distorted +// hex boundary instead of restarting in every tile. On a cylindrical map the +// repeat is snapped so the world width is a whole number of repeats, which keeps +// the pattern coherent across the wrap seam as well. +// +// Neighbouring land tiles are blended by a distance kernel over the tile +// centres. The weight only depends on the world position, so both tiles on a +// shared edge compute the same mix and the transition has no seam. A wider +// kernel fades the sand texture into non-mountain coasts as a beach. + +import { HEX_H } from "../../../shared/hex.js"; + +export const TEXTURE_DIR = "assets/textures/"; +export const TEXTURE_GRASS = "grass"; +export const TEXTURE_SAND = "sand"; +export const TEXTURE_FOREST = "forest"; +export const TEXTURE_HILLS = "hills"; +export const TEXTURE_MOUNTAIN = "mountain"; +// Tangent-space normal maps that give the hills and mountains their relief. +export const TEXTURE_HILLS_NORMAL = "hills_normal_map"; +export const TEXTURE_MOUNTAIN_NORMAL = "mountains_normal_map"; + +// The overlay layers, in the order the shader composites them. +export const OVERLAY_TEXTURES = [TEXTURE_FOREST, TEXTURE_HILLS, TEXTURE_MOUNTAIN]; + +// The normal map that lights an overlay, or null when it has none. +export function overlayNormal(overlay) { + if (overlay === TEXTURE_HILLS) return TEXTURE_HILLS_NORMAL; + if (overlay === TEXTURE_MOUNTAIN) return TEXTURE_MOUNTAIN_NORMAL; + return null; +} + +// The tiling textures are 128x128, so one repeat every 128 world pixels keeps +// them at roughly native density at zoom 1. +export const TEXTURE_DESIRED_REPEAT = 128; + +// Which terrains use a tiling base texture and which overlay, if any, sits on +// top of it. A terrain missing from this table is drawn from the terrain.png +// atlas. +const LAND_TEXTURES = { + Forest: { base: TEXTURE_GRASS, overlay: TEXTURE_FOREST }, + Hills: { base: TEXTURE_GRASS, overlay: TEXTURE_HILLS }, + Land: { base: TEXTURE_GRASS }, + Desert: { base: TEXTURE_SAND }, + Mountain: { base: TEXTURE_GRASS, overlay: TEXTURE_MOUNTAIN }, +}; + +// Blend radius of the land kernel. A neighbour's centre is one edge-width away +// (about HEX_H), so a shared edge lands at an even 50/50 while a tile centre +// stays almost entirely its own texture. +const BLEND_RADIUS = HEX_H; + +// Beaches reach a little further than the ordinary land blend, so the sand +// spreads over most of the coastal tile instead of clinging to the shoreline, +// and are capped below 1 so a hint of the ground shows through. +const BEACH_RADIUS = HEX_H * 1.35; +const BEACH_STRENGTH = 1.25; + +// The base/overlay description for a terrain type, or null for the atlas +// terrains (Sea and Ice). +export function landTexture(terrainType) { + return LAND_TEXTURES[terrainType] || null; +} + +export function isTexturedLand(tile) { + return !!(tile && LAND_TEXTURES[tile.terrainType]); +} + +// Sea is drawn by the procedural water shader rather than a tiling texture, but +// it still rides in the same vertex weights as a flag, so the renderer knows +// where the water should replace the atlas art. +export function isWaterTile(tile) { + return !!tile && tile.terrainType === "Sea"; +} + +// World pixels per texture repeat. On a cylindrical world the repeat is snapped +// so that the wrap period is a whole number of repeats; otherwise two copies of +// the same tile would show different texture phases at the seam. +export function textureRepeat(periodPixels) { + if (!periodPixels || periodPixels <= 0) return TEXTURE_DESIRED_REPEAT; + const whole = Math.max(1, Math.round(periodPixels / TEXTURE_DESIRED_REPEAT)); + return periodPixels / whole; +} + +// The layer list for a terrain type, topmost first: the terrain's overlay (when +// it has one) then its base. Sea and ice return an empty list and keep the +// atlas art. +export function terrainLayers(terrainType) { + const texture = LAND_TEXTURES[terrainType]; + if (!texture) return []; + const layers = [`${TEXTURE_DIR}${texture.base}.png`]; + if (texture.overlay) layers.unshift(`${TEXTURE_DIR}${texture.overlay}.png`); + return layers; +} + +// A smooth (squared) falloff that reaches zero at the blend radius. +function falloff(distance) { + if (distance >= BLEND_RADIUS) return 0; + const t = 1 - distance / BLEND_RADIUS; + return t * t; +} + +// The wider falloff used for the coastal sand. +function beachFalloff(distance) { + if (distance >= BEACH_RADIUS) return 0; + const t = 1 - distance / BEACH_RADIUS; + return t * t; +} + +// Blends the tiling textures of the land tiles near a world point. +// +// `samples` is a list of { tile, dx, dy }: `tile` is the tile data and (dx, dy) +// is the offset from the point to that tile's centre. The result carries the +// grass/sand base split (summing to 1) and the opacity of each overlay, or null +// when no textured-land tile is close enough. The value depends only on the +// point and its surroundings, never on which tile asked, so shared vertices +// agree. +export function blendTextures(samples) { + let grass = 0; + let sand = 0; + let total = 0; + const overlays = {}; + for (const name of OVERLAY_TEXTURES) overlays[name] = 0; + for (const sample of samples) { + const texture = sample.tile && LAND_TEXTURES[sample.tile.terrainType]; + if (!texture) continue; + const weight = falloff(Math.hypot(sample.dx, sample.dy)); + if (weight <= 0) continue; + total += weight; + if (texture.base === TEXTURE_SAND) sand += weight; + else grass += weight; + if (texture.overlay) overlays[texture.overlay] += weight; + } + if (total <= 0) return null; + const blend = { grass: grass / total, sand: sand / total }; + for (const name of OVERLAY_TEXTURES) blend[name] = overlays[name] / total; + return blend; +} + +// How much of the sand texture the coast blends into a land tile at a point, +// from 0 inland to about BEACH_STRENGTH/2 on the shoreline. The value depends +// only on the point and its surroundings, so both the land side and its +// neighbouring sea compute the same shoreline. +export function blendBeach(samples) { + let sea = 0; + let total = 0; + for (const sample of samples) { + if (!sample.tile) continue; + const weight = beachFalloff(Math.hypot(sample.dx, sample.dy)); + if (weight <= 0) continue; + total += weight; + if (isWaterTile(sample.tile)) sea += weight; + } + if (total <= 0) return 0; + return sea / total; +} + +// Per-vertex texture weights, packed for the shader as +// [grass, sand, atlas, forest, hills, mountain, water, beach]. Atlas terrains +// are fully atlas; textured land is fully texture, with the base split, the +// overlay opacities and the coastal sand coming from the neighbouring-tile +// blend. Mountain coasts stay rocky, so they get no beach. +export function vertexTextureWeights(tile, samples) { + const water = isWaterTile(tile) ? 1 : 0; + if (!isTexturedLand(tile)) return [0, 0, 1, 0, 0, 0, water, 0]; + const blend = blendTextures(samples); + if (!blend) return [0, 0, 1, 0, 0, 0, water, 0]; + const beach = tile.terrainType === "Mountain" + ? 0 + : Math.min(1, blendBeach(samples) * BEACH_STRENGTH); + return [ + blend.grass, + blend.sand, + 0, + blend[TEXTURE_FOREST], + blend[TEXTURE_HILLS], + blend[TEXTURE_MOUNTAIN], + water, + beach, + ]; +} diff --git a/client/js/map_view/utils.js b/client/js/map_view/utils.js index b0cb0a6..55664c7 100644 --- a/client/js/map_view/utils.js +++ b/client/js/map_view/utils.js @@ -3,7 +3,6 @@ // testable. import { HEX_W, HEX_H } from "../../../shared/hex.js"; -import { OVERVIEW_COLOURS, OVERVIEW_SEA } from "./constants.js"; // Order-independent FNV-style hash of an [x, y, ...] entry array, used to skip // rebuilding a layer when the snapshot did not actually change it. @@ -117,21 +116,6 @@ export function makeSegment(a, b, colour, width, offset = 0) { return div; } -export function overviewColour(tile) { - if (tile.terrainType && OVERVIEW_COLOURS[tile.terrainType]) { - return OVERVIEW_COLOURS[tile.terrainType]; - } - return tile.terrainClass === "Sea" ? OVERVIEW_SEA : OVERVIEW_COLOURS.Land; -} - -// Mixes two #rrggbb colours, `t` being how much of `b` to use. -export function blendColours(a, b, t) { - const ca = parseColour(a); - const cb = parseColour(b); - const mix = (i) => Math.round(ca[i] + (cb[i] - ca[i]) * t); - return `rgb(${mix(0)}, ${mix(1)}, ${mix(2)})`; -} - export function parseColour(value) { if (value.charAt(0) === "#") { const hex = value.slice(1); @@ -141,8 +125,7 @@ export function parseColour(value) { parseInt(hex.slice(4, 6), 16), ]; } - // Tolerates the "rgb(r, g, b)" form blendColours produces, so a blended - // overview colour can be fed straight back in. + // Tolerates an "rgb(r, g, b)" string as well as a #rrggbb one. const match = value.match(/(\d+)\D+(\d+)\D+(\d+)/); if (match) return [Number(match[1]), Number(match[2]), Number(match[3])]; return [0, 0, 0]; diff --git a/client/js/map_view/webgl.js b/client/js/map_view/webgl.js index b94cd1d..e5c7019 100644 --- a/client/js/map_view/webgl.js +++ b/client/js/map_view/webgl.js @@ -1,5 +1,5 @@ // WebGL renderer for the world map. It draws the terrain, the pre-generated -// roads, the territory borders, the fog of war and the zoomed-out overview as +// roads, the territory borders, the fog of war and the selected region as // batched triangles on a single canvas behind the DOM icon layers. The icons // (units, cities, labels, paths, targets) stay ordinary DOM elements, so the // game's look, hit-testing and accessibility are unchanged. @@ -15,34 +15,80 @@ // and a handful of draw calls, so panning does not touch the DOM at all. import { HEX_W, HEX_H, mapToLocal, parseKey, key } from "../../../shared/hex.js"; -import { OVERVIEW_BLOCK } from "./constants.js"; -import { nearestCorners, inwardOffset, parseColour } from "./utils.js"; +import { nearestCorners, parseColour } from "./utils.js"; +import { + CORNERS, + EDGE_SEGMENTS, + bentPath, + distortedBoundary, + edgePoint, + latticeKey, + periodUnits, +} from "./distortion.js"; +import { + TEXTURE_DIR, + TEXTURE_FOREST, + TEXTURE_GRASS, + TEXTURE_HILLS, + TEXTURE_HILLS_NORMAL, + TEXTURE_MOUNTAIN, + TEXTURE_MOUNTAIN_NORMAL, + TEXTURE_SAND, + isTexturedLand, + textureRepeat, + vertexTextureWeights, +} from "./textures.js"; +import { blurFog, fogLevel, isFogged } from "./fog.js"; -// Bytes per vertex: x, y, u, v, r, g, b, a (all 32-bit floats). -const STRIDE = 8 * 4; +// Full terrain vertex layout, in floats: x, y, u, v, wx, wy, plus the base +// colour (grass, sand, atlas) and one weight per overlay. Fog uses a compact +// x, y, r, g, b, a layout instead. The flat-colour passes (roads, borders, +// region) use x, y, u, v, r, g, b, a. const ATTR_POS = 0; const ATTR_UV = 1; const ATTR_COLOUR = 2; +const ATTR_WORLD = 3; +const ATTR_OVERLAY = 4; + +// Floats per vertex in each layout. +const TERRAIN_FLOATS = 14; +const COLOUR_FLOATS = 8; +const FOG_FLOATS = 6; -const WHITE = [1, 1, 1, 1]; const ROAD = [138 / 255, 90 / 255, 43 / 255, 1]; -const FOG_EXPLORED = [4 / 255, 6 / 255, 18 / 255, 0.62]; -const FOG_UNEXPLORED = [4 / 255, 4 / 255, 10 / 255, 1]; // The terrain atlas is a single row of equal cells, one per terrain type. const ATLAS_COLS = 7; -const ATLAS_W = HEX_W * ATLAS_COLS; -const ATLAS_H = HEX_H; +// Texture unit per sampler. +const UNIT_ATLAS = 0; +const UNIT_GRASS = 1; +const UNIT_SAND = 2; +const UNIT_FOREST = 3; +const UNIT_HILLS = 4; +const UNIT_MOUNTAIN = 5; +const UNIT_HILLS_NORMAL = 6; +const UNIT_MOUNTAIN_NORMAL = 7; +// National border stroke, and the region border that is half as thick and +// drawn as dotted dashes instead of a continuous line. +const NATIONAL_BORDER = 3.5; +const REGION_BORDER = NATIONAL_BORDER / 2; +const REGION_FILL = [1, 1, 1, 0.06]; +const REGION_BORDER_COLOUR = [1, 1, 1, 0.55]; +const REGION_DASHES = 6; const VERTEX_SHADER = ` attribute vec2 aPos; attribute vec2 aUV; attribute vec4 aColour; +attribute vec2 aWorld; +attribute vec4 aOverlay; uniform vec2 uCamera; uniform vec2 uViewport; uniform float uZoom; uniform float uShiftX; varying vec2 vUV; varying vec4 vColour; +varying vec2 vWorld; +varying vec4 vOverlay; void main() { vec2 world = vec2(aPos.x + uShiftX, aPos.y); vec2 screen = (world - uCamera) * uZoom + uViewport * 0.5; @@ -50,24 +96,141 @@ void main() { gl_Position = vec4(clip.x, -clip.y, 0.0, 1.0); vUV = aUV; vColour = aColour; + vWorld = aWorld; + vOverlay = aOverlay; } `; +// Terrain (uMode 0) blends the two tiling land textures and the atlas by the +// per-vertex weights, composites the forest/hills/mountain overlays on top and +// replaces sea with the animated water. The flat passes (uMode 1) just output +// their vertex colour. const FRAGMENT_SHADER = ` precision mediump float; -uniform sampler2D uTexture; +uniform sampler2D uAtlas; +uniform sampler2D uGrass; +uniform sampler2D uSand; +uniform sampler2D uForest; +uniform sampler2D uHills; +uniform sampler2D uMountain; +uniform sampler2D uHillsNormal; +uniform sampler2D uMountainNormal; +uniform float uTextureRepeat; +uniform float uTime; uniform float uMode; varying vec2 vUV; varying vec4 vColour; +varying vec2 vWorld; +varying vec4 vOverlay; +// The relief normal is rebuilt from the red/green gradient and a fixed up axis, +// so the maps' blue channel (which is not a clean 255 at rest) cannot bias the +// lighting. A flat texel is (0, 0, 1) and lights to exactly 1. +vec3 reliefNormal(vec4 texel) { + vec2 slope = texel.xy * 2.0 - 1.0; + return normalize(vec3(slope, 1.0)); +} +// Procedural sea: a few crossing swells drive the deep/shallow gradient and a +// soft specular sparkle. It is driven by the same period-snapped texture +// coordinate as the land, whose whole cycles repeat with the world, so the waves +// stay seamless across both tile edges and the cylindrical wrap. +float waterWave(vec2 uv, vec2 freq, float speed, float t) { + return sin(6.2831853 * dot(freq, uv) + t * speed); +} +vec3 waterColour(vec2 uv, float t) { + float swell = waterWave(uv, vec2(1.0, 0.6), 0.55, t) * 0.4 + + waterWave(uv, vec2(-2.0, 1.3), 0.8, t) * 0.3 + + waterWave(uv, vec2(3.0, 0.4), 1.05, t) * 0.25; + float ripple = 0.5 + 0.5 * waterWave(uv, vec2(5.0, -3.0), 1.6, t); + // A narrow, low-contrast blue so the sea reads as calm water rather than a + // busy pattern. Depth drifts only slightly and the highlights are faint. + vec3 deep = vec3(0.078, 0.235, 0.353); + vec3 shallow = vec3(0.098, 0.275, 0.396); + vec3 colour = mix(deep, shallow, clamp(swell * 0.35 + 0.5, 0.0, 1.0)); + colour += vec3(0.04, 0.06, 0.07) * pow(clamp(swell, 0.0, 1.0), 3.0); + colour += vec3(0.06, 0.08, 0.09) * pow(ripple, 8.0); + return colour; +} void main() { if (uMode < 0.5) { - gl_FragColor = texture2D(uTexture, vUV) * vColour; + vec2 uv = vWorld / uTextureRepeat; + vec3 grass = texture2D(uGrass, uv).rgb; + vec3 sand = texture2D(uSand, uv).rgb; + vec4 atlas = texture2D(uAtlas, vUV); + vec3 base = grass * vColour.r + sand * vColour.g + atlas.rgb * vColour.b; + vec4 forest = texture2D(uForest, uv); + vec4 hills = texture2D(uHills, uv); + vec4 mountain = texture2D(uMountain, uv); + base = mix(base, forest.rgb, forest.a * vOverlay.x); + base = mix(base, hills.rgb, hills.a * vOverlay.y); + base = mix(base, mountain.rgb, mountain.a * vOverlay.z); + // Coastal sand fades the sand texture into non-mountain shores. + base = mix(base, sand, vOverlay.w); + // Sea tiles are replaced by the animated water; the flag is 0 elsewhere. + base = mix(base, waterColour(uv, uTime), vColour.a); + // A light from the upper left rakes over the hills and mountains. The term + // is one over flat ground, so it only touches the relief overlays. + vec3 light = normalize(vec3(-0.5, -0.6, 0.8)); + float hillsLight = max(dot(reliefNormal(texture2D(uHillsNormal, uv)), light), 0.0) / light.z; + float mountainLight = max(dot(reliefNormal(texture2D(uMountainNormal, uv)), light), 0.0) / light.z; + float relief = 1.0 + (hillsLight - 1.0) * vOverlay.y + + (mountainLight - 1.0) * vOverlay.z; + base *= clamp(relief, 0.55, 1.6); + // Tiling land is opaque; the legacy atlas keeps its hexagonal alpha so its + // transparent corners are not painted black. + float alpha = mix(1.0, atlas.a, vColour.b); + gl_FragColor = vec4(base, alpha); } else { gl_FragColor = vColour; } } `; +// A clip-space quad with a matching 0..1 UV, used by the fog post-process. It +// carries no camera transform, so it fills whatever framebuffer is bound. +const FS_VERTEX_SHADER = ` +attribute vec2 aPos; +attribute vec2 aUV; +varying vec2 vUV; +void main() { + gl_Position = vec4(aPos, 0.0, 1.0); + vUV = aUV; +} +`; + +// Nine-tap separable Gaussian (uMode 0) and a plain copy (uMode 1) for the +// final composite. `uTexel` is one off-screen texel and `uDirection` picks the +// axis, so the same shader runs both blur passes. +const FS_FRAGMENT_SHADER = ` +precision mediump float; +uniform sampler2D uTexture; +uniform vec2 uTexel; +uniform vec2 uDirection; +uniform float uMode; +varying vec2 vUV; +void main() { + if (uMode > 0.5) { + gl_FragColor = texture2D(uTexture, vUV); + return; + } + vec2 step = uTexel * uDirection; + vec4 sum = texture2D(uTexture, vUV) * 0.2270270270; + sum += texture2D(uTexture, vUV + step) * 0.1945945946; + sum += texture2D(uTexture, vUV - step) * 0.1945945946; + sum += texture2D(uTexture, vUV + step * 2.0) * 0.1216216216; + sum += texture2D(uTexture, vUV - step * 2.0) * 0.1216216216; + sum += texture2D(uTexture, vUV + step * 3.0) * 0.0540540541; + sum += texture2D(uTexture, vUV - step * 3.0) * 0.0540540541; + sum += texture2D(uTexture, vUV + step * 4.0) * 0.0162162162; + sum += texture2D(uTexture, vUV - step * 4.0) * 0.0162162162; + gl_FragColor = sum; +} +`; + +// How far the fog blur reaches, in off-screen texels. The buffer is half +// resolution, so a step of 3 is six screen pixels per tap and the kernel +// spreads over about twenty-four. +const FOG_BLUR_STEP = 3.0; + // Builds a renderer on the given canvas, or null when no WebGL context can be // created. The availability check is done without touching canvas.getContext in // environments (jsdom) that would throw and log. @@ -90,17 +253,32 @@ export class GLMapRenderer { this.version = 0; this._sig = null; this.view = null; - this.texture = null; + // One GL texture per source: the legacy atlas plus the three land textures. + this.textures = {}; this.texReady = false; // One pass per layer: { buffer, count } and, for terrain, its atlas cell. this.terrain = { buffer: null, count: 0 }; this.roads = { buffer: null, count: 0 }; this.borders = { buffer: null, count: 0 }; this.fog = { buffer: null, count: 0 }; - this.overview = { buffer: null, count: 0 }; + // The selected/inspected tile's region: a soft fill plus dotted borders. + this.regionOverlay = { buffer: null, count: 0 }; + // The fog is rendered off-screen and blurred before it is composited, so + // its frontier is a soft gradient rather than the hex-tiling edges. + this.fullscreenProgram = null; + this.fullscreenQuad = null; + this.fogBlur = null; this._initContext(); this._initProgram(); + try { + this._initFullscreenProgram(); + } catch { + // Without the blur program the fog is still drawn, just without the + // post-process, so a picky driver does not cost us the whole map. + this.fullscreenProgram = null; + this.fullscreenQuad = null; + } this._initTexture(); this._listenForContextLoss(); } @@ -151,13 +329,17 @@ export class GLMapRenderer { dispose() { const gl = this.gl; if (!gl) return; - for (const pass of [this.terrain, this.roads, this.borders, this.fog, this.overview]) { + for (const pass of [this.terrain, this.roads, this.borders, this.fog, this.regionOverlay]) { if (pass.buffer) gl.deleteBuffer(pass.buffer); pass.buffer = null; pass.count = 0; } - if (this.texture) gl.deleteTexture(this.texture); + for (const texture of Object.values(this.textures)) gl.deleteTexture(texture); + this.textures = {}; if (this.program) gl.deleteProgram(this.program); + if (this.fullscreenProgram) gl.deleteProgram(this.fullscreenProgram); + if (this.fullscreenQuad) gl.deleteBuffer(this.fullscreenQuad); + this._disposeFogBlur(); const lose = gl.getExtension("WEBGL_lose_context"); if (lose) lose.loseContext(); this.gl = null; @@ -174,6 +356,8 @@ export class GLMapRenderer { gl.bindAttribLocation(program, ATTR_POS, "aPos"); gl.bindAttribLocation(program, ATTR_UV, "aUV"); gl.bindAttribLocation(program, ATTR_COLOUR, "aColour"); + gl.bindAttribLocation(program, ATTR_WORLD, "aWorld"); + gl.bindAttribLocation(program, ATTR_OVERLAY, "aOverlay"); gl.linkProgram(program); if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { throw new Error(`link failed: ${gl.getProgramInfoLog(program)}`); @@ -184,11 +368,22 @@ export class GLMapRenderer { this.aPos = gl.getAttribLocation(program, "aPos"); this.aUV = gl.getAttribLocation(program, "aUV"); this.aColour = gl.getAttribLocation(program, "aColour"); + this.aWorld = gl.getAttribLocation(program, "aWorld"); + this.aOverlay = gl.getAttribLocation(program, "aOverlay"); this.uCamera = gl.getUniformLocation(program, "uCamera"); this.uViewport = gl.getUniformLocation(program, "uViewport"); this.uZoom = gl.getUniformLocation(program, "uZoom"); this.uShiftX = gl.getUniformLocation(program, "uShiftX"); - this.uTexture = gl.getUniformLocation(program, "uTexture"); + this.uAtlas = gl.getUniformLocation(program, "uAtlas"); + this.uGrass = gl.getUniformLocation(program, "uGrass"); + this.uSand = gl.getUniformLocation(program, "uSand"); + this.uForest = gl.getUniformLocation(program, "uForest"); + this.uHills = gl.getUniformLocation(program, "uHills"); + this.uMountain = gl.getUniformLocation(program, "uMountain"); + this.uHillsNormal = gl.getUniformLocation(program, "uHillsNormal"); + this.uMountainNormal = gl.getUniformLocation(program, "uMountainNormal"); + this.uTextureRepeat = gl.getUniformLocation(program, "uTextureRepeat"); + this.uTime = gl.getUniformLocation(program, "uTime"); this.uMode = gl.getUniformLocation(program, "uMode"); } @@ -205,29 +400,185 @@ export class GLMapRenderer { return shader; } - _initTexture() { + // The post-process program and the clip-space quad it draws. Called again + // after a context loss, since all of it lives in GL. + _initFullscreenProgram() { const gl = this.gl; - this.texture = gl.createTexture(); - gl.bindTexture(gl.TEXTURE_2D, this.texture); - // A 1x1 transparent texel until the real atlas arrives. - gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, - new Uint8Array([0, 0, 0, 0])); + const vertex = this._compile(gl.VERTEX_SHADER, FS_VERTEX_SHADER); + const fragment = this._compile(gl.FRAGMENT_SHADER, FS_FRAGMENT_SHADER); + const program = gl.createProgram(); + gl.attachShader(program, vertex); + gl.attachShader(program, fragment); + gl.bindAttribLocation(program, 0, "aPos"); + gl.bindAttribLocation(program, 1, "aUV"); + gl.linkProgram(program); + if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { + throw new Error(`fog link failed: ${gl.getProgramInfoLog(program)}`); + } + gl.deleteShader(vertex); + gl.deleteShader(fragment); + this.fullscreenProgram = program; + this.fsPos = gl.getAttribLocation(program, "aPos"); + this.fsUV = gl.getAttribLocation(program, "aUV"); + this.fsTexture = gl.getUniformLocation(program, "uTexture"); + this.fsTexel = gl.getUniformLocation(program, "uTexel"); + this.fsDirection = gl.getUniformLocation(program, "uDirection"); + this.fsMode = gl.getUniformLocation(program, "uMode"); + this.fullscreenQuad = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, this.fullscreenQuad); + // x, y, u, v for the four corners of a triangle strip. + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([ + -1, -1, 0, 0, + 1, -1, 1, 0, + -1, 1, 0, 1, + 1, 1, 1, 1, + ]), gl.STATIC_DRAW); + } + + _drawFullscreen(texture, texelX, texelY, dirX, dirY, mode) { + const gl = this.gl; + gl.useProgram(this.fullscreenProgram); + // Attribute locations are shared across programs, so turn off the map + // program's extra arrays before pointing 0 and 1 at the quad. + gl.disableVertexAttribArray(this.aColour); + gl.disableVertexAttribArray(this.aWorld); + gl.disableVertexAttribArray(this.aOverlay); + gl.bindBuffer(gl.ARRAY_BUFFER, this.fullscreenQuad); + gl.enableVertexAttribArray(this.fsPos); + gl.vertexAttribPointer(this.fsPos, 2, gl.FLOAT, false, 16, 0); + gl.enableVertexAttribArray(this.fsUV); + gl.vertexAttribPointer(this.fsUV, 2, gl.FLOAT, false, 16, 8); + gl.activeTexture(gl.TEXTURE0); + gl.bindTexture(gl.TEXTURE_2D, texture); + gl.uniform1i(this.fsTexture, 0); + gl.uniform2f(this.fsTexel, texelX, texelY); + gl.uniform2f(this.fsDirection, dirX, dirY); + gl.uniform1f(this.fsMode, mode); + gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); + } + + // One off-screen colour target for the fog blur. Returns null if the + // framebuffer is not complete, so the renderer can fall back to drawing the + // fog straight to the canvas. + _createFogTarget(width, height) { + const gl = this.gl; + const texture = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, texture); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); + const fbo = gl.createFramebuffer(); + gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); + gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0); + const complete = gl.checkFramebufferStatus(gl.FRAMEBUFFER) === gl.FRAMEBUFFER_COMPLETE; + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + if (!complete) { + gl.deleteFramebuffer(fbo); + gl.deleteTexture(texture); + return null; + } + return { fbo, texture, width, height }; + } + + _disposeFogBlur() { + const gl = this.gl; + if (!gl || !this.fogBlur) return; + for (const target of [this.fogBlur.a, this.fogBlur.b]) { + if (target.fbo) gl.deleteFramebuffer(target.fbo); + if (target.texture) gl.deleteTexture(target.texture); + } + this.fogBlur = null; + } + + // Two half-resolution targets, recreated when the viewport resizes. + _ensureFogBlur(width, height) { + const w = Math.max(1, Math.floor(width / 2)); + const h = Math.max(1, Math.floor(height / 2)); + if (this.fogBlur && this.fogBlur.width === w && this.fogBlur.height === h) { + return this.fogBlur; + } + this._disposeFogBlur(); + if (!this.fullscreenProgram) return null; + try { + const a = this._createFogTarget(w, h); + const b = this._createFogTarget(w, h); + if (!a || !b) { + if (a) { + this.gl.deleteFramebuffer(a.fbo); + this.gl.deleteTexture(a.texture); + } + if (b) { + this.gl.deleteFramebuffer(b.fbo); + this.gl.deleteTexture(b.texture); + } + return null; + } + this.fogBlur = { a, b, width: w, height: h }; + } catch { + this._disposeFogBlur(); + return null; + } + return this.fogBlur; + } + + // Loads the four sources the terrain shader samples. The atlas is clamped + // (its cells must not bleed into one another); the three land textures repeat, + // so the seam between two tiles continues the same pattern. + _initTexture() { + this.textures = {}; this.texReady = false; - const image = new Image(); - image.onload = () => { - if (this.lost) return; - gl.bindTexture(gl.TEXTURE_2D, this.texture); - gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false); - gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image); + let pending = 0; + const loaded = () => { + pending -= 1; + if (pending > 0 || this.lost) return; this.texReady = true; this.version += 1; this._sig = null; }; - image.src = "assets/terrain.png"; + const load = (name, src, repeat) => { + const texture = this._createTexture(repeat); + this.textures[name] = texture; + pending += 1; + const image = new Image(); + image.onload = () => { + if (this.lost) return; + this._uploadImage(texture, image); + loaded(); + }; + image.src = src; + }; + load("atlas", "assets/terrain.png", false); + load(TEXTURE_GRASS, `${TEXTURE_DIR}${TEXTURE_GRASS}.png`, true); + load(TEXTURE_SAND, `${TEXTURE_DIR}${TEXTURE_SAND}.png`, true); + load(TEXTURE_FOREST, `${TEXTURE_DIR}${TEXTURE_FOREST}.png`, true); + load(TEXTURE_HILLS, `${TEXTURE_DIR}${TEXTURE_HILLS}.png`, true); + load(TEXTURE_MOUNTAIN, `${TEXTURE_DIR}${TEXTURE_MOUNTAIN}.png`, true); + load(TEXTURE_HILLS_NORMAL, `${TEXTURE_DIR}${TEXTURE_HILLS_NORMAL}.png`, true); + load(TEXTURE_MOUNTAIN_NORMAL, `${TEXTURE_DIR}${TEXTURE_MOUNTAIN_NORMAL}.png`, true); + } + + _createTexture(repeat) { + const gl = this.gl; + const texture = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, texture); + // A 1x1 transparent texel until the image arrives. + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, + new Uint8Array([0, 0, 0, 0])); + const wrap = repeat ? gl.REPEAT : gl.CLAMP_TO_EDGE; + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, wrap); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, wrap); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); + return texture; + } + + _uploadImage(texture, image) { + const gl = this.gl; + gl.bindTexture(gl.TEXTURE_2D, texture); + gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image); } _listenForContextLoss() { @@ -237,7 +588,14 @@ export class GLMapRenderer { }); this.canvas.addEventListener("webglcontextrestored", () => { try { + this.fogBlur = null; this._initProgram(); + try { + this._initFullscreenProgram(); + } catch { + this.fullscreenProgram = null; + this.fullscreenQuad = null; + } this._initTexture(); this.lost = false; if (this.view) this.setWorld(this.view); @@ -253,6 +611,24 @@ export class GLMapRenderer { arr.push(x, y, u, v, colour[0], colour[1], colour[2], colour[3]); } + // Terrain vertices also carry the undistorted world position, which the + // shader turns into a continuous tiling-texture coordinate, plus the base + // (grass, sand, atlas), the per-overlay weights, the water flag and the + // coastal beach weight. + _pushTerrain(arr, x, y, u, v, wx, wy, weights) { + arr.push( + x, y, u, v, wx, wy, + weights[0], weights[1], weights[2], weights[6], + weights[3], weights[4], weights[5], weights[7] + ); + } + + // Fog has no texture, so it uses a compact position/colour-only vertex. That + // keeps the buffer (re-uploaded whenever visibility changes) small. + _pushSimple(arr, x, y, colour) { + arr.push(x, y, colour[0], colour[1], colour[2], colour[3]); + } + // Adds a convex quad given its four vertices (with per-vertex UVs) as two // triangles. Wind order does not matter: we never cull faces. _quad(arr, corners, colour) { @@ -286,12 +662,13 @@ export class GLMapRenderer { ], colour); } - _upload(pass, arr, usage) { + _upload(pass, arr, usage, floats = 8) { const gl = this.gl; if (!pass.buffer) pass.buffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, pass.buffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(arr), usage || gl.STATIC_DRAW); - pass.count = arr.length / 8; + pass.count = arr.length / floats; + pass.floats = floats; // Only terrain and fog are row-banded; everything else draws whole. pass.rowStart = null; pass.originY = 0; @@ -311,15 +688,21 @@ export class GLMapRenderer { this.buildRoads(view); this.buildBorders(view); this.buildKnowledge(view); - this.buildOverview(view); + this.buildRegionOverlay(view); } buildTerrain(view) { const arr = []; const tiles = view.tiles || {}; const topo = view.topology; - const uPad = 0.5 / ATLAS_W; - const vPad = 0.5 / ATLAS_H; + const seed = view.seed | 0; + const period = periodUnits(topo); + const segments = EDGE_SEGMENTS; + const boundaryLength = CORNERS.length * segments; + // Half-texel inset so linear filtering cannot sample the neighbouring atlas + // cell along the hexagon's border. + const uPad = 0.5 / HEX_W; + const vPad = 0.5 / HEX_H; // Built row by row so the renderer can draw only the tile rows on screen. // `rowStart[i]` is the first vertex of tile row i; the last entry is the // total, which makes a visible range one draw call. @@ -327,35 +710,108 @@ export class GLMapRenderer { const originX = topo.originX; const originY = topo.originY; for (let y = originY; y < originY + topo.height; y++) { - rowStart.push(arr.length / 8); + rowStart.push(arr.length / TERRAIN_FLOATS); for (let x = originX; x < originX + topo.width; x++) { const tile = tiles[key(x, y)]; if (!tile) continue; const centre = mapToLocal(x, y); const column = tile.col | 0; - const u0 = (column * HEX_W) / ATLAS_W + uPad; - const u1 = ((column + 1) * HEX_W) / ATLAS_W - uPad; - const v0 = vPad; - const v1 = 1 - vPad; - const x0 = centre.x - HEX_W / 2; - const x1 = centre.x + HEX_W / 2; - const y0 = centre.y - HEX_H / 2; - const y1 = centre.y + HEX_H / 2; - this._quad(arr, [ - { x: x0, y: y0, u: u0, v: v0 }, - { x: x0, y: y1, u: u0, v: v1 }, - { x: x1, y: y1, u: u1, v: v1 }, - { x: x1, y: y0, u: u1, v: v0 }, - ], WHITE); + const cu = (column + 0.5) / ATLAS_COLS; + const cv = 0.5; + // Land textures are blended from the tile and its neighbours; the ring + // is only needed when this tile is one of the textured lands. + const ring = isTexturedLand(tile) ? this._textureRing(view, x, y, centre) : null; + // A fan over the wavy boundary: the six points (and the extra border + // points) can all be moved independently, which a quad could not do. + const boundary = distortedBoundary(seed, centre, period, segments); + // The weights are a pure function of the undistorted world position, so + // the two tiles that share a boundary point compute the same value. + const colours = new Array(boundaryLength); + for (let i = 0; i < boundaryLength; i++) { + colours[i] = this._textureWeights(tile, ring, boundary[i].idealX, boundary[i].idealY); + } + const centreColour = this._textureWeights(tile, ring, centre.x, centre.y); + for (let i = 0; i < CORNERS.length; i++) { + for (let j = 0; j < segments; j++) { + const a = boundary[i * segments + j]; + const b = boundary[(i * segments + j + 1) % boundaryLength]; + const ua = this._cellUV(column, a.idealX - centre.x, a.idealY - centre.y, uPad, vPad); + const ub = this._cellUV(column, b.idealX - centre.x, b.idealY - centre.y, uPad, vPad); + this._pushTerrain(arr, centre.x, centre.y, cu, cv, centre.x, centre.y, centreColour); + this._pushTerrain(arr, a.x, a.y, ua.u, ua.v, a.idealX, a.idealY, colours[i * segments + j]); + this._pushTerrain(arr, b.x, b.y, ub.u, ub.v, b.idealX, b.idealY, colours[(i * segments + j + 1) % boundaryLength]); + } + } } } - rowStart.push(arr.length / 8); - this._upload(this.terrain, arr); + rowStart.push(arr.length / TERRAIN_FLOATS); + this._upload(this.terrain, arr, undefined, TERRAIN_FLOATS); this.terrain.rowStart = rowStart; this.terrain.originY = originY; this.terrain.rows = topo.height; } + // The tile itself plus its six neighbours, each with its wrapped pixel + // centre, used to blend the tiling textures across tile boundaries. + _textureRing(view, x, y, centre) { + const topo = view.topology; + const tiles = view.tiles; + const coords = { x, y }; + const ring = [{ tile: tiles[key(x, y)], x: centre.x, y: centre.y }]; + for (const neighbour of topo.neighbours(x, y)) { + const delta = topo.pixelDelta(coords, neighbour); + ring.push({ + tile: tiles[key(neighbour.x, neighbour.y)], + x: centre.x + delta.x, + y: centre.y + delta.y, + }); + } + return ring; + } + + // The shader weights at one undistorted world point, from the nearby tiles. + _textureWeights(tile, ring, worldX, worldY) { + if (!ring) return vertexTextureWeights(tile, []); + const samples = ring.map((sample) => ({ + tile: sample.tile, + dx: sample.x - worldX, + dy: sample.y - worldY, + })); + return vertexTextureWeights(tile, samples); + } + + // The fog level of the tile itself plus its six neighbours, each with its + // wrapped pixel centre, used to blur the visibility frontier. + _fogRing(topo, x, y, centre, levelAt) { + const coords = { x, y }; + const ring = [{ level: levelAt(x, y), x: centre.x, y: centre.y }]; + for (const neighbour of topo.neighbours(x, y)) { + const delta = topo.pixelDelta(coords, neighbour); + ring.push({ + level: levelAt(neighbour.x, neighbour.y), + x: centre.x + delta.x, + y: centre.y + delta.y, + }); + } + return ring; + } + + // UV of a point at local offset (dx, dy) from the tile centre, inside the + // tile's atlas cell and inset by half a texel. + _cellUV(column, dx, dy, uPad, vPad) { + let uc = (dx + HEX_W / 2) / HEX_W; + let vc = (dy + HEX_H / 2) / HEX_H; + uc = Math.min(Math.max(uc, uPad), 1 - uPad); + vc = Math.min(Math.max(vc, vPad), 1 - vPad); + return { u: (column + uc) / ATLAS_COLS, v: vc }; + } + + // Draws the road network as mitered strokes. Each pair of adjacent road tiles + // is drawn once as a single polyline from centre to centre, crossing the + // shared wavy border at its midpoint; the bend there and the gentle in-tile + // drifts are joined with the same miter as the borders, so no piece overlaps + // and no notch is left at a bend. A small cap at every tile centre covers the + // junction where several roads meet. buildRoads(view) { const arr = []; const roads = view.roads; @@ -363,129 +819,406 @@ export class GLMapRenderer { this._upload(this.roads, arr); return; } + const seed = view.seed | 0; + const topo = view.topology; + const period = periodUnits(topo); + const steps = 3; + const drawn = new Set(); for (const k of roads) { const coords = parseKey(k); const centre = mapToLocal(coords.x, coords.y); - for (const neighbour of view.topology.neighbours(coords.x, coords.y)) { - if (!roads.has(key(neighbour.x, neighbour.y))) continue; - const delta = view.topology.pixelDelta(coords, neighbour); - this._segment(arr, centre, { x: centre.x + delta.x, y: centre.y + delta.y }, 5, ROAD); + for (const neighbour of topo.neighbours(coords.x, coords.y)) { + const nk = key(neighbour.x, neighbour.y); + if (!roads.has(nk)) continue; + const pair = k < nk ? `${k}|${nk}` : `${nk}|${k}`; + if (drawn.has(pair)) continue; + drawn.add(pair); + const delta = topo.pixelDelta(coords, neighbour); + const target = { x: centre.x + delta.x, y: centre.y + delta.y }; + // Cross the border exactly where it is now: the midpoint of the shared + // wavy edge, not the ideal straight boundary. + const [idealA, idealB] = nearestCorners(centre, target); + const mid = edgePoint(seed, idealA, idealB, 0.5, period); + // Half the road drifts in from each tile centre towards that midpoint. + const points = [centre]; + for (let j = 1; j <= steps; j++) { + points.push(bentPath(seed, centre, mid, 0x5a17, j / steps)); + } + for (let j = steps - 1; j >= 1; j--) { + points.push(bentPath(seed, target, mid, 0x5a17, j / steps)); + } + points.push(target); + this._roadBand(arr, points, 5, ROAD); } } + for (const k of roads) { + const coords = parseKey(k); + this._roadJoint(arr, mapToLocal(coords.x, coords.y)); + } this._upload(this.roads, arr); } + // A centered band along a polyline. Both edges are mitered (and consecutive + // quads share them), so a bend is filled rather than left as a wedge. + _roadBand(arr, points, width, colour) { + const n = points.length; + if (n < 2) return; + const half = width / 2; + const normals = []; + for (let i = 0; i < n - 1; i++) { + const a = points[i]; + const b = points[i + 1]; + const dx = b.x - a.x; + const dy = b.y - a.y; + const length = Math.hypot(dx, dy) || 1; + normals.push({ x: -dy / length, y: dx / length }); + } + const offsets = []; + for (let i = 0; i < n; i++) { + const before = normals[i === 0 ? 0 : i - 1]; + const after = normals[i === n - 1 ? n - 2 : i]; + let denom = 1 + before.x * after.x + before.y * after.y; + if (denom < 0.2) denom = 0.2; + const scale = half / denom; + offsets.push({ x: (before.x + after.x) * scale, y: (before.y + after.y) * scale }); + } + for (let i = 0; i < n - 1; i++) { + const a = points[i]; + const b = points[i + 1]; + const oa = offsets[i]; + const ob = offsets[i + 1]; + this._quad(arr, [ + { x: a.x + oa.x, y: a.y + oa.y }, + { x: b.x + ob.x, y: b.y + ob.y }, + { x: b.x - ob.x, y: b.y - ob.y }, + { x: a.x - oa.x, y: a.y - oa.y }, + ], colour); + } + } + + // A small square that hides the notch where several roads meet at a centre. + _roadJoint(arr, point) { + const r = 2.5; + this._quad(arr, [ + { x: point.x - r, y: point.y - r }, + { x: point.x - r, y: point.y + r }, + { x: point.x + r, y: point.y + r }, + { x: point.x + r, y: point.y - r }, + ], ROAD); + } + + // Draws each nation's coastline/border as one continuous stroked outline. + // + // Every border edge is inherited from its owning tile and oriented so the + // owner lies on the left of the directed edge. Edges are then stitched across + // tile boundaries by their shared lattice corners, so a corner where two + // *different* tiles of the same nation meet is joined with a single miter + // instead of two independent (and divergent) butt caps. buildBorders(view) { const arr = []; const territory = view.territory; - if (!territory || territory.size === 0 || !view.topology) { + if (!territory || territory.size === 0 || !view.topology || !view.civilisations) { this._upload(this.borders, arr); return; } + const seed = view.seed | 0; + const period = periodUnits(view.topology); + const glow = 6; + const edgesByOwner = new Map(); for (const [k, owner] of territory) { - const civ = view.civilisations && view.civilisations[owner]; - if (!civ) continue; - const rgb = parseColour(civ.primaryColour); - const colour = [rgb[0] / 255, rgb[1] / 255, rgb[2] / 255, 1]; + if (!view.civilisations[owner]) continue; const coords = parseKey(k); const centre = mapToLocal(coords.x, coords.y); - for (const neighbour of view.topology.neighbours(coords.x, coords.y)) { + const neighbours = this._edgeNeighbours(view, coords.x, coords.y); + for (let e = 0; e < CORNERS.length; e++) { + const neighbour = neighbours[e]; + if (!neighbour) continue; const nk = key(neighbour.x, neighbour.y); if (territory.has(nk) && territory.get(nk) === owner) continue; - const delta = view.topology.pixelDelta(coords, neighbour); - const target = { x: centre.x + delta.x, y: centre.y + delta.y }; - const [a, b] = nearestCorners(centre, target); - const width = 6; - const inward = inwardOffset(a, b, centre, width / 2); - this._segment(arr, a, b, width, colour, inward); - // A dot at each corner fills the notch left where two rounded edges meet. + const a = { x: centre.x + CORNERS[e].x, y: centre.y + CORNERS[e].y }; + const next = CORNERS[(e + 1) % CORNERS.length]; + const b = { x: centre.x + next.x, y: centre.y + next.y }; + const points = this._edgePolyline(seed, centre, e, period); + // Orient the edge so the owner sits on its left; the stroke then always + // offsets towards the owner and neighbouring edges agree on a miter. const dx = b.x - a.x; const dy = b.y - a.y; - const length = Math.hypot(dx, dy) || 1; - const nx = -dy / length; - const ny = dx / length; - const corner = width * 1.5; - for (const point of [a, b]) { - const cx = point.x + nx * inward; - const cy = point.y + ny * inward; - this._quad(arr, [ - { x: cx - corner / 2, y: cy - corner / 2 }, - { x: cx - corner / 2, y: cy + corner / 2 }, - { x: cx + corner / 2, y: cy + corner / 2 }, - { x: cx + corner / 2, y: cy - corner / 2 }, - ], colour); + const mx = (a.x + b.x) / 2; + const my = (a.y + b.y) / 2; + const ownerIsLeft = (centre.x - mx) * -dy + (centre.y - my) * dx >= 0; + let run = points; + let start = a; + let end = b; + if (!ownerIsLeft) { + run = points.slice().reverse(); + start = b; + end = a; } + if (!edgesByOwner.has(owner)) edgesByOwner.set(owner, []); + edgesByOwner.get(owner).push({ + points: run, + startKey: latticeKey(start.x, start.y), + endKey: latticeKey(end.x, end.y), + }); + } + } + for (const [owner, edges] of edgesByOwner) { + const rgb = parseColour(view.civilisations[owner].primaryColour); + const colour = [rgb[0] / 255, rgb[1] / 255, rgb[2] / 255, 1]; + const soft = [colour[0], colour[1], colour[2], 0.18]; + for (const chain of this._chainEdges(edges)) { + this._stroke(arr, chain.points, glow, soft, chain.closed); + this._stroke(arr, chain.points, NATIONAL_BORDER, colour, chain.closed); } } this._upload(this.borders, arr); } + // Stitches directed edges into runs by matching each edge's end corner to the + // next edge's start corner. Because a nation's boundary is a set of closed + // loops (plus open runs cut off at the map edge), every vertex pairs exactly + // one incoming and one outgoing edge, so the follow is unambiguous. + _chainEdges(edges) { + const byStart = new Map(); + for (const edge of edges) byStart.set(edge.startKey, edge); + const hasIncoming = new Set(edges.map((edge) => edge.endKey)); + const used = new Set(); + const chains = []; + const follow = (first) => { + const points = []; + let closed = false; + let edge = first; + while (edge && !used.has(edge)) { + used.add(edge); + for (let i = edge === first ? 0 : 1; i < edge.points.length; i++) { + points.push(edge.points[i]); + } + const next = byStart.get(edge.endKey); + if (next === first) { + closed = true; + if (points.length > 1) points.pop(); + break; + } + edge = next; + } + return { points, closed }; + }; + // Open runs first (their start has no incoming edge), then any closed loop. + for (const edge of edges) { + if (used.has(edge) || hasIncoming.has(edge.startKey)) continue; + chains.push(follow(edge)); + } + for (const edge of edges) { + if (used.has(edge)) continue; + chains.push(follow(edge)); + } + return chains; + } + + // The neighbour tile across each of the hexagon's six edges, in CORNERS order + // (edge i runs from corner i to corner i+1). Matching by direction keeps this + // correct for both column parities and for the wrapped seam. + _edgeNeighbours(view, x, y) { + const topo = view.topology; + const coords = { x, y }; + const neighbours = topo.neighbours(x, y); + const result = []; + for (let i = 0; i < CORNERS.length; i++) { + const a = CORNERS[i]; + const b = CORNERS[(i + 1) % CORNERS.length]; + const mx = (a.x + b.x) / 2; + const my = (a.y + b.y) / 2; + const length = Math.hypot(mx, my) || 1; + let best = null; + let bestDot = -Infinity; + for (const neighbour of neighbours) { + const delta = topo.pixelDelta(coords, neighbour); + const distance = Math.hypot(delta.x, delta.y) || 1; + const dot = (mx * delta.x + my * delta.y) / (length * distance); + if (dot > bestDot) { + bestDot = dot; + best = neighbour; + } + } + result.push(best); + } + return result; + } + + // The jagged polyline (EDGE_SEGMENTS + 1 points) of one hexagon edge. + _edgePolyline(seed, centre, edge, period) { + const a = { x: centre.x + CORNERS[edge].x, y: centre.y + CORNERS[edge].y }; + const next = CORNERS[(edge + 1) % CORNERS.length]; + const b = { x: centre.x + next.x, y: centre.y + next.y }; + const points = []; + for (let j = 0; j <= EDGE_SEGMENTS; j++) { + points.push(edgePoint(seed, a, b, j / EDGE_SEGMENTS, period)); + } + return points; + } + + // Draws a stroke of the given width along a polyline, flush with it on the + // boundary side and offset to the left of the direction of travel. Callers + // orient their points so the owner lies on the left, which keeps the offset + // towards the owner. The offset at each vertex is the miter (the intersection + // of the two adjacent offset edges), and consecutive quads share their + // offset edge, so corners are filled without the pieces overlapping. + _stroke(arr, points, width, colour, closed) { + const n = points.length; + if (n < 2) return; + const count = closed ? n : n - 1; + const segments = []; + for (let i = 0; i < count; i++) { + const a = points[i]; + const b = points[(i + 1) % n]; + const dx = b.x - a.x; + const dy = b.y - a.y; + const length = Math.hypot(dx, dy) || 1; + segments.push({ x: -dy / length, y: dx / length }); + } + const offsets = []; + for (let i = 0; i < n; i++) { + const before = closed ? segments[(i - 1 + count) % count] : segments[i === 0 ? 0 : i - 1]; + const after = closed ? segments[i % count] : segments[i === n - 1 ? n - 2 : i]; + // Miter offset: (n1 + n2) * width / (1 + n1 . n2). The clamp keeps a + // near-doubling-back corner from shooting out a huge spike. + let denom = 1 + before.x * after.x + before.y * after.y; + if (denom < 0.2) denom = 0.2; + const scale = width / denom; + offsets.push({ x: (before.x + after.x) * scale, y: (before.y + after.y) * scale }); + } + for (let i = 0; i < count; i++) { + const a = points[i]; + const b = points[(i + 1) % n]; + const oa = offsets[i]; + const ob = offsets[(i + 1) % n]; + this._quad(arr, [ + a, + b, + { x: b.x + ob.x, y: b.y + ob.y }, + { x: a.x + oa.x, y: a.y + oa.y }, + ], colour); + } + } + + // Fog is no longer a flat colour per tile: every vertex takes the blurred + // average of the fog levels around it, so the frontier ramps over about a + // tile instead of stepping at the hex edge. Visible tiles that border fog are + // drawn too, which is what lets the ramp fade into the clear side. buildKnowledge(view) { const arr = []; const tiles = view.tiles || {}; const topo = view.topology; + const seed = view.seed | 0; + const period = periodUnits(topo); const explored = view.explored || new Set(); const visible = view.visible || new Set(); - // A flat-top hexagon fan, matching the corners the territory code uses. - const half = { x: HEX_W / 2, y: HEX_H / 2 }; - const corners = [ - { x: half.x, y: 0 }, - { x: half.x * 0.5, y: -half.y }, - { x: -half.x * 0.5, y: -half.y }, - { x: -half.x, y: 0 }, - { x: -half.x * 0.5, y: half.y }, - { x: half.x * 0.5, y: half.y }, - ]; + const segments = EDGE_SEGMENTS; const rowStart = []; const originX = topo.originX; const originY = topo.originY; + const levels = new Map(); + const levelAt = (x, y) => { + const k = key(x, y); + if (!tiles[k]) return null; + let level = levels.get(k); + if (!level) { + level = fogLevel(visible.has(k), explored.has(k)); + levels.set(k, level); + } + return level; + }; for (let y = originY; y < originY + topo.height; y++) { - rowStart.push(arr.length / 8); + rowStart.push(arr.length / 6); for (let x = originX; x < originX + topo.width; x++) { - const k = key(x, y); - if (!tiles[k] || visible.has(k)) continue; - const colour = explored.has(k) ? FOG_EXPLORED : FOG_UNEXPLORED; + if (!tiles[key(x, y)]) continue; const centre = mapToLocal(x, y); - for (let i = 0; i < corners.length; i++) { - const a = corners[i]; - const b = corners[(i + 1) % corners.length]; - this._push(arr, centre.x, centre.y, 0, 0, colour); - this._push(arr, centre.x + a.x, centre.y + a.y, 0, 0, colour); - this._push(arr, centre.x + b.x, centre.y + b.y, 0, 0, colour); + // The tile and its neighbours; a fully clear tile with clear + // surroundings needs no geometry at all. + const ring = this._fogRing(topo, x, y, centre, levelAt); + if (!ring.some((sample) => isFogged(sample.level))) continue; + const fogAt = (px, py) => { + const samples = []; + for (const sample of ring) { + samples.push({ level: sample.level, dx: sample.x - px, dy: sample.y - py }); + } + return blurFog(samples); + }; + // Fog must cover exactly the wavy terrain hexagon underneath it. + const boundary = distortedBoundary(seed, centre, period, segments); + const vertices = new Array(boundary.length); + for (let i = 0; i < boundary.length; i++) { + vertices[i] = fogAt(boundary[i].x, boundary[i].y); + } + const centreFog = fogAt(centre.x, centre.y); + const push = (point, fog) => { + this._pushSimple(arr, point.x, point.y, [fog.rgb[0], fog.rgb[1], fog.rgb[2], fog.opacity]); + }; + for (let i = 0; i < boundary.length; i++) { + const a = boundary[i]; + const b = boundary[(i + 1) % boundary.length]; + push(centre, centreFog); + push(a, vertices[i]); + push(b, vertices[(i + 1) % boundary.length]); } } } - rowStart.push(arr.length / 8); - this._upload(this.fog, arr, this.gl.DYNAMIC_DRAW); + rowStart.push(arr.length / 6); + this._upload(this.fog, arr, this.gl.DYNAMIC_DRAW, 6); this.fog.rowStart = rowStart; this.fog.originY = originY; this.fog.rows = topo.height; } - buildOverview(view) { + // The region of the selected/inspected tile: a soft translucent fill over + // every tile in it, plus a thin dotted border along its edge. Rebuilt on + // selection and when the region membership changes; empty when nothing is + // selected. + buildRegionOverlay(view) { const arr = []; - if (view.topology && typeof view._overviewBlockData === "function") { - const step = OVERVIEW_BLOCK; - const startX = view.topology.originX; - const startY = view.topology.originY; - const endX = startX + view.topology.width; - const endY = startY + view.topology.height; - for (let by = startY; by < endY; by += step) { - for (let bx = startX; bx < endX; bx += step) { - const data = view._overviewBlockData(bx, by, step); - if (!data) continue; - const rgb = parseColour(data.colour); - const colour = [rgb[0] / 255, rgb[1] / 255, rgb[2] / 255, 1]; - this._quad(arr, [ - { x: data.minX, y: data.minY }, - { x: data.minX, y: data.minY + data.height }, - { x: data.minX + data.width, y: data.minY + data.height }, - { x: data.minX + data.width, y: data.minY }, - ], colour); + const region = view._selectedRegion; + const regions = view.regions; + if (region === null || region === undefined || !regions || regions.size === 0 || + !view.topology) { + this._upload(this.regionOverlay, arr); + return; + } + const seed = view.seed | 0; + const topo = view.topology; + const period = periodUnits(topo); + const segments = EDGE_SEGMENTS; + const boundaryLength = CORNERS.length * segments; + const territory = view.territory; + for (const [k, cityId] of regions) { + if (cityId !== region) continue; + const coords = parseKey(k); + const centre = mapToLocal(coords.x, coords.y); + const boundary = distortedBoundary(seed, centre, period, segments); + for (let i = 0; i < boundaryLength; i++) { + const a = boundary[i]; + const b = boundary[(i + 1) % boundaryLength]; + this._push(arr, centre.x, centre.y, 0, 0, REGION_FILL); + this._push(arr, a.x, a.y, 0, 0, REGION_FILL); + this._push(arr, b.x, b.y, 0, 0, REGION_FILL); + } + const owner = territory ? territory.get(k) : undefined; + for (const neighbour of topo.neighbours(coords.x, coords.y)) { + const nk = key(neighbour.x, neighbour.y); + if (regions.get(nk) === region) continue; + // A national border already traces this edge; the dotted region line + // would only muddy it, so leave that edge to the border pass. + if (!territory || owner === undefined || territory.get(nk) !== owner) continue; + const delta = topo.pixelDelta(coords, neighbour); + const target = { x: centre.x + delta.x, y: centre.y + delta.y }; + const [idealA, idealB] = nearestCorners(centre, target); + for (let d = 0; d < REGION_DASHES; d++) { + const p0 = edgePoint(seed, idealA, idealB, (d + 0.28) / REGION_DASHES, period); + const p1 = edgePoint(seed, idealA, idealB, (d + 0.72) / REGION_DASHES, period); + this._segment(arr, p0, p1, REGION_BORDER, REGION_BORDER_COLOUR); } } } - this._upload(this.overview, arr); + this._upload(this.regionOverlay, arr, this.gl.DYNAMIC_DRAW); } // -------------------------------------------------------------- render -- @@ -531,18 +1264,61 @@ export class GLMapRenderer { const gl = this.gl; if (!pass || !pass.buffer || pass.count === 0) return; gl.uniform1f(this.uMode, mode); - if (mode === 0) { - gl.activeTexture(gl.TEXTURE0); - gl.bindTexture(gl.TEXTURE_2D, this.texture); - gl.uniform1i(this.uTexture, 0); - } gl.bindBuffer(gl.ARRAY_BUFFER, pass.buffer); + const floats = pass.floats || COLOUR_FLOATS; + const stride = floats * 4; gl.enableVertexAttribArray(this.aPos); - gl.vertexAttribPointer(this.aPos, 2, gl.FLOAT, false, STRIDE, 0); - gl.enableVertexAttribArray(this.aUV); - gl.vertexAttribPointer(this.aUV, 2, gl.FLOAT, false, STRIDE, 8); - gl.enableVertexAttribArray(this.aColour); - gl.vertexAttribPointer(this.aColour, 4, gl.FLOAT, false, STRIDE, 16); + gl.vertexAttribPointer(this.aPos, 2, gl.FLOAT, false, stride, 0); + if (floats === TERRAIN_FLOATS) { + // Terrain carries the atlas UV, the world position and the blend weights. + gl.activeTexture(gl.TEXTURE0); + gl.bindTexture(gl.TEXTURE_2D, this.textures.atlas); + gl.uniform1i(this.uAtlas, UNIT_ATLAS); + gl.activeTexture(gl.TEXTURE1); + gl.bindTexture(gl.TEXTURE_2D, this.textures.grass); + gl.uniform1i(this.uGrass, UNIT_GRASS); + gl.activeTexture(gl.TEXTURE2); + gl.bindTexture(gl.TEXTURE_2D, this.textures.sand); + gl.uniform1i(this.uSand, UNIT_SAND); + gl.activeTexture(gl.TEXTURE3); + gl.bindTexture(gl.TEXTURE_2D, this.textures.forest); + gl.uniform1i(this.uForest, UNIT_FOREST); + gl.activeTexture(gl.TEXTURE4); + gl.bindTexture(gl.TEXTURE_2D, this.textures.hills); + gl.uniform1i(this.uHills, UNIT_HILLS); + gl.activeTexture(gl.TEXTURE5); + gl.bindTexture(gl.TEXTURE_2D, this.textures.mountain); + gl.uniform1i(this.uMountain, UNIT_MOUNTAIN); + gl.activeTexture(gl.TEXTURE6); + gl.bindTexture(gl.TEXTURE_2D, this.textures.hills_normal_map); + gl.uniform1i(this.uHillsNormal, UNIT_HILLS_NORMAL); + gl.activeTexture(gl.TEXTURE7); + gl.bindTexture(gl.TEXTURE_2D, this.textures.mountains_normal_map); + gl.uniform1i(this.uMountainNormal, UNIT_MOUNTAIN_NORMAL); + gl.enableVertexAttribArray(this.aUV); + gl.vertexAttribPointer(this.aUV, 2, gl.FLOAT, false, stride, 8); + gl.enableVertexAttribArray(this.aWorld); + gl.vertexAttribPointer(this.aWorld, 2, gl.FLOAT, false, stride, 16); + gl.enableVertexAttribArray(this.aColour); + gl.vertexAttribPointer(this.aColour, 4, gl.FLOAT, false, stride, 24); + gl.enableVertexAttribArray(this.aOverlay); + gl.vertexAttribPointer(this.aOverlay, 4, gl.FLOAT, false, stride, 40); + } else if (floats === FOG_FLOATS) { + // Fog passes only carry position and colour; the disabled attributes keep + // their generic defaults, unused in the untextured shader branch. + gl.disableVertexAttribArray(this.aUV); + gl.disableVertexAttribArray(this.aWorld); + gl.disableVertexAttribArray(this.aOverlay); + gl.enableVertexAttribArray(this.aColour); + gl.vertexAttribPointer(this.aColour, 4, gl.FLOAT, false, stride, 8); + } else { + gl.enableVertexAttribArray(this.aUV); + gl.vertexAttribPointer(this.aUV, 2, gl.FLOAT, false, stride, 8); + gl.disableVertexAttribArray(this.aWorld); + gl.disableVertexAttribArray(this.aOverlay); + gl.enableVertexAttribArray(this.aColour); + gl.vertexAttribPointer(this.aColour, 4, gl.FLOAT, false, stride, 16); + } let start = 0; let count = pass.count; if (rows && pass.rowStart) { @@ -566,8 +1342,12 @@ export class GLMapRenderer { if (this.canvas.height !== bufferHeight) this.canvas.height = bufferHeight; const camera = view.camera; - const lod = !!view.lod; - const signature = `${camera.x},${camera.y},${camera.zoom},${width},${height},${lod ? 1 : 0},${this.version}`; + // The water animates, so the frame is repainted while the clock advances. + // The timestamp is quantised to keep the redraw rate near 24 fps instead of + // matching the display refresh exactly. + const now = (typeof performance !== "undefined" ? performance.now() : Date.now()) / 1000; + const tick = Math.floor(now * 24); + const signature = `${camera.x},${camera.y},${camera.zoom},${width},${height},${this.version},${tick}`; if (signature === this._sig) return; this._sig = signature; @@ -578,20 +1358,56 @@ export class GLMapRenderer { gl.uniform2f(this.uCamera, camera.x, camera.y); gl.uniform2f(this.uViewport, width, height); gl.uniform1f(this.uZoom, camera.zoom); + gl.uniform1f(this.uTextureRepeat, textureRepeat(view._period || 0)); + gl.uniform1f(this.uTime, now); gl.enable(gl.BLEND); gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); gl.disable(gl.DEPTH_TEST); const period = view._period || 0; - if (lod && this.overview.count > 0) { - this._drawPass(this.overview, 1, [0], period); - return; - } const shifts = this._neededShifts(view, width); const rows = this._visibleRows(view, width, height); if (this.texReady) this._drawPass(this.terrain, 0, shifts, period, rows); this._drawPass(this.roads, 1, shifts, period); this._drawPass(this.borders, 1, shifts, period); + this._drawFog(bufferWidth, bufferHeight, shifts, period, rows); + this._drawPass(this.regionOverlay, 1, shifts, period); + } + + // Draws the fog into an off-screen, half-resolution target, blurs it in two + // separable passes and composites it back. Doing it as a post-process smooths + // the frontier into a soft gradient regardless of the tile-shaped geometry. + // If the framebuffer cannot be built, it falls back to the direct pass. + _drawFog(bufferWidth, bufferHeight, shifts, period, rows) { + const gl = this.gl; + const blur = this._ensureFogBlur(bufferWidth, bufferHeight); + if (!blur) { + this._drawPass(this.fog, 1, shifts, period, rows); + return; + } + // Draw with straight alpha and no blending, so the stored alpha is exact. + gl.disable(gl.BLEND); + gl.bindFramebuffer(gl.FRAMEBUFFER, blur.a.fbo); + gl.viewport(0, 0, blur.width, blur.height); + gl.clearColor(0, 0, 0, 0); + gl.clear(gl.COLOR_BUFFER_BIT); + gl.useProgram(this.program); this._drawPass(this.fog, 1, shifts, period, rows); + + const texelX = FOG_BLUR_STEP / blur.width; + const texelY = FOG_BLUR_STEP / blur.height; + gl.bindFramebuffer(gl.FRAMEBUFFER, blur.b.fbo); + gl.clear(gl.COLOR_BUFFER_BIT); + this._drawFullscreen(blur.a.texture, texelX, texelY, 1, 0, 0); + gl.bindFramebuffer(gl.FRAMEBUFFER, blur.a.fbo); + gl.clear(gl.COLOR_BUFFER_BIT); + this._drawFullscreen(blur.b.texture, texelX, texelY, 0, 1, 0); + + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + gl.viewport(0, 0, bufferWidth, bufferHeight); + gl.enable(gl.BLEND); + gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); + this._drawFullscreen(blur.a.texture, 1 / blur.width, 1 / blur.height, 0, 0, 1); + gl.useProgram(this.program); } } diff --git a/tests/distortion_test.js b/tests/distortion_test.js new file mode 100644 index 0000000..2a01592 --- /dev/null +++ b/tests/distortion_test.js @@ -0,0 +1,153 @@ +import { TestCase } from "./framework/test_case.js"; +import { mapToLocal, MapTopology, COL_STEP } from "../shared/hex.js"; +import { + DISTORTION, + EDGE_SEGMENTS, + CORNERS, + periodUnits, + vertexOffset, + distortPoint, + distortedCorners, + distortedBoundary, + edgePoint, + bentPath, + latticeKey, +} from "../client/js/map_view/distortion.js"; + +export class DistortionTest extends TestCase { + async test_is_deterministic_for_a_seed() { + const first = distortedCorners(1234, { x: 40, y: 40 }, 0); + const second = distortedCorners(1234, { x: 40, y: 40 }, 0); + this.assertEqual(first, second, "the same seed warps the same way"); + const other = distortedCorners(5678, { x: 40, y: 40 }, 0); + this.assertNotEqual(first, other, "a different seed gives a different shape"); + } + + async test_offsets_stay_within_the_amplitude() { + for (let xi = -50; xi <= 50; xi += 7) { + for (let yi = -50; yi <= 50; yi += 5) { + const offset = vertexOffset(99, xi, yi, 0); + this.assertTrue(Math.abs(offset.x) <= DISTORTION.x, `x offset ${offset.x} too large`); + this.assertTrue(Math.abs(offset.y) <= DISTORTION.y, `y offset ${offset.y} too large`); + } + } + } + + async test_shared_corners_move_together_so_the_mesh_stays_watertight() { + // Tile (0,0)'s right corner is the same lattice vertex as tile (1,0)'s + // top-left corner: (31.5, 0). If the two tiles displaced it differently the + // map would tear along their shared edge. + const a = distortedCorners(2024, mapToLocal(0, 0), 0); + const b = distortedCorners(2024, mapToLocal(1, 0), 0); + this.assertEqual(a[0], b[2], "the shared corner is displaced identically"); + + // Same for the lower shared vertex (15.75, 28): (0,0)'s bottom-right and + // (1,0)'s left corner. + this.assertEqual(a[5], b[3], "the second shared corner is displaced identically"); + } + + async test_cylindrical_wrap_is_seamless() { + const topology = new MapTopology({ x: 40, y: 30 }, true); + const period = periodUnits(topology); + this.assertEqual(period, topology.width * 3, "one period is 3*width lattice columns"); + const x = 31.5; + const wrap = COL_STEP * topology.width; + const near = distortPoint(555, x, -28, period); + const wrapped = distortPoint(555, x + wrap, -28, period); + this.assertApprox(wrapped.x - near.x, wrap, 1e-9); + this.assertApprox(wrapped.y, near.y, 1e-9); + this.assertEqual(periodUnits(new MapTopology({ x: 4, y: 4 }, false)), 0, "flat maps do not fold"); + } + + async test_negative_lattice_coordinates_are_folded_consistently() { + const period = 60; + const wrap = period * (63 / 4); + const x = -10; + const before = distortPoint(12, x, -10, period); + const after = distortPoint(12, x + wrap, -10, period); + this.assertApprox(after.x - before.x, wrap, 1e-9); + this.assertApprox(after.y, before.y, 1e-9); + } + + async test_wrapped_edges_are_identical() { + // An edge and its copy one wrap period over are the same border. Both must + // bend the same way, or the map tears along the cylindrical seam. + const topology = new MapTopology({ x: 40, y: 30 }, true); + const period = periodUnits(topology); + const wrap = COL_STEP * topology.width; + const a = { x: 31.5, y: 0 }; + const b = { x: 15.75, y: -28 }; + for (let j = 0; j <= 8; j++) { + const t = j / 8; + const point = edgePoint(555, a, b, t, period); + const copy = edgePoint(555, { x: a.x + wrap, y: a.y }, { x: b.x + wrap, y: b.y }, t, period); + this.assertApprox(copy.x - point.x, wrap, 1e-9, "seam edge x"); + this.assertApprox(copy.y, point.y, 1e-9, "seam edge y"); + } + } + + async test_edges_are_subdivided_and_meet_their_corners() { + const centre = mapToLocal(3, -2); + const boundary = distortedBoundary(77, centre, 0); + this.assertSize(boundary, CORNERS.length * EDGE_SEGMENTS, "every edge is split"); + // Each edge's first point is its starting corner, displaced exactly like + // the corner function, so adjacent edges still join. + for (let i = 0; i < CORNERS.length; i++) { + const point = boundary[i * EDGE_SEGMENTS]; + const corner = distortPoint(77, centre.x + CORNERS[i].x, centre.y + CORNERS[i].y, 0); + this.assertApprox(point.x, corner.x, 1e-9, "corner moved"); + this.assertApprox(point.y, corner.y, 1e-9, "corner moved"); + this.assertApprox(point.idealX, centre.x + CORNERS[i].x, 1e-9, "ideal corner"); + this.assertApprox(point.idealY, centre.y + CORNERS[i].y, 1e-9, "ideal corner"); + this.assertEqual(point.t, 0); + } + } + + async test_wobbled_edge_is_shared_by_both_neighbours() { + // Edge (31.5, 0) -> (15.75, -28) of tile (0,0) is the same border as edge + // (15.75, -28) -> (31.5, 0) of its neighbour (1,-1). Traversed in opposite + // directions, both must produce the identical set of points. + const a0 = { x: 31.5, y: 0 }; + const a1 = { x: 15.75, y: -28 }; + const fromA = []; + const fromB = []; + for (let j = 0; j <= EDGE_SEGMENTS; j++) { + fromA.push(edgePoint(4242, a0, a1, j / EDGE_SEGMENTS, 0)); + fromB.push(edgePoint(4242, a1, a0, j / EDGE_SEGMENTS, 0)); + } + const sort = (list) => + list.slice().sort((p, q) => (p.x !== q.x ? p.x - q.x : p.y - q.y)); + const left = sort(fromA); + const right = sort(fromB); + this.assertSize(left, right.length); + for (let i = 0; i < left.length; i++) { + this.assertApprox(left[i].x, right[i].x, 1e-9, "shared border point"); + this.assertApprox(left[i].y, right[i].y, 1e-9, "shared border point"); + } + } + + async test_edge_wobble_vanishes_at_the_corners() { + const a = { x: 31.5, y: 0 }; + const b = { x: 15.75, y: -28 }; + this.assertEqual(edgePoint(9, a, b, 0, 0), distortPoint(9, a.x, a.y, 0)); + this.assertEqual(edgePoint(9, a, b, 1, 0), distortPoint(9, b.x, b.y, 0)); + } + + async test_shared_corners_share_a_lattice_key() { + // Tile (0,0)'s right corner and tile (1,0)'s top-left corner are the same + // lattice vertex, so border chaining can stitch them with one integer key. + const a = distortedCorners(1, mapToLocal(0, 0), 0); + const b = distortedCorners(1, mapToLocal(1, 0), 0); + this.assertEqual(latticeKey(a[0].x, a[0].y), latticeKey(b[2].x, b[2].y)); + this.assertNotEqual(latticeKey(a[0].x, a[0].y), latticeKey(a[1].x, a[1].y)); + } + + async test_bent_path_still_starts_and_ends_where_asked() { + const from = { x: 0, y: 0 }; + const to = { x: 30, y: 12 }; + this.assertEqual(bentPath(5, from, to, 1, 0), from); + this.assertEqual(bentPath(5, from, to, 1, 1), to); + const middle = bentPath(5, from, to, 1, 0.5); + this.assertApprox(middle.x, 15, DISTORTION.road, "the bend stays small"); + } +} diff --git a/tests/fog_test.js b/tests/fog_test.js new file mode 100644 index 0000000..44385c0 --- /dev/null +++ b/tests/fog_test.js @@ -0,0 +1,89 @@ +import { TestCase } from "./framework/test_case.js"; +import { MapTopology, mapToLocal } from "../shared/hex.js"; +import { GLMapRenderer } from "../client/js/map_view/webgl.js"; +import { + FOG_EXPLORED_OPACITY, + FOG_EXPLORED_RGB, + FOG_UNEXPLORED_OPACITY, + FOG_UNEXPLORED_RGB, + blurFog, + fogLevel, + isFogged, +} from "../client/js/map_view/fog.js"; + +// Two samples on opposite sides of a shared hex edge, halfway between the two +// tile centres. +const edgeSamples = (a, b) => [ + { level: a, dx: -23.625, dy: -14 }, + { level: b, dx: 23.625, dy: 14 }, +]; + +export class FogTest extends TestCase { + test_levels_describe_visible_explored_and_unexplored_ground() { + this.assertFalse(isFogged(fogLevel(true, true)), "visible ground has no fog"); + this.assertEqual(fogLevel(false, true).opacity, FOG_EXPLORED_OPACITY); + this.assertEqual(fogLevel(false, false).opacity, FOG_UNEXPLORED_OPACITY); + this.assertEqual(fogLevel(false, true).rgb, FOG_EXPLORED_RGB); + this.assertEqual(fogLevel(false, false).rgb, FOG_UNEXPLORED_RGB); + } + + test_the_frontier_fades_instead_of_stepping() { + const explored = fogLevel(false, true); + const visible = fogLevel(true, true); + const blend = blurFog(edgeSamples(explored, visible)); + // Half the kernel is over fogged ground, so the edge is about half as opaque + // as the explored tile rather than jumping straight to clear. + this.assertApprox(blend.opacity, FOG_EXPLORED_OPACITY / 2, 1e-6, "faded opacity"); + this.assertEqual(blend.rgb, FOG_EXPLORED_RGB, "the fog keeps its colour"); + } + + test_a_tile_centre_keeps_its_own_fog() { + const unexplored = fogLevel(false, false); + const clear = fogLevel(true, true); + const blur = blurFog([ + { level: unexplored, dx: 0, dy: 0 }, + { level: clear, dx: 47.25, dy: 28 }, + ]); + this.assertGreater(blur.opacity, 0.9, "the middle of the fog stays opaque"); + } + + test_explored_and_unexplored_colours_mix() { + const blend = blurFog(edgeSamples(fogLevel(false, true), fogLevel(false, false))); + this.assertGreater(blend.opacity, FOG_EXPLORED_OPACITY, "between the two fog levels"); + this.assertLess(blend.opacity, FOG_UNEXPLORED_OPACITY); + for (let i = 0; i < 3; i++) { + const low = Math.min(FOG_EXPLORED_RGB[i], FOG_UNEXPLORED_RGB[i]); + const high = Math.max(FOG_EXPLORED_RGB[i], FOG_UNEXPLORED_RGB[i]); + this.assertTrue(blend.rgb[i] >= low - 1e-9 && blend.rgb[i] <= high + 1e-9, `channel ${i} mixed`); + } + } + + test_the_blur_agrees_from_both_sides_of_an_edge() { + // The renderer builds one ring per tile; both tiles on a shared edge must + // produce the same blurred fog there, or the frontier would seam. + const topology = new MapTopology({ x: 5, y: 5 }, false); + const renderer = Object.create(GLMapRenderer.prototype); + const levelAt = (x, y) => fogLevel(false, !(x === 0 && y === 0)); + const centreA = mapToLocal(0, 0); + const centreB = mapToLocal(1, 0); + const ringA = renderer._fogRing(topology, 0, 0, centreA, levelAt); + const ringB = renderer._fogRing(topology, 1, 0, centreB, levelAt); + const px = (centreA.x + centreB.x) / 2; + const py = (centreA.y + centreB.y) / 2; + const samples = (ring) => ring.map((s) => ({ level: s.level, dx: s.x - px, dy: s.y - py })); + const a = blurFog(samples(ringA)); + const b = blurFog(samples(ringB)); + this.assertApprox(a.opacity, b.opacity, 1e-12, "the shared edge fades identically"); + for (let i = 0; i < 3; i++) { + this.assertApprox(a.rgb[i], b.rgb[i], 1e-12, `channel ${i} agrees`); + } + } + + test_off_map_neighbours_are_ignored() { + const blend = blurFog([ + { level: fogLevel(false, false), dx: 0, dy: 0 }, + { level: null, dx: 47.25, dy: 28 }, + ]); + this.assertApprox(blend.opacity, FOG_UNEXPLORED_OPACITY, 1e-6, "a missing tile does not dilute the fog"); + } +} diff --git a/tests/framework/helpers.js b/tests/framework/helpers.js index 39a76a2..4a3ef90 100644 --- a/tests/framework/helpers.js +++ b/tests/framework/helpers.js @@ -53,11 +53,11 @@ export function defaultState(civIds = ["france", "britain"], seed = SEED) { // The six render layers of the world map, as jQuery objects. export function mapLayers($) { return { - overview: $("#layer-overview"), terrain: $("#layer-terrain"), roads: $("#layer-roads"), borders: $("#layer-borders"), fog: $("#layer-fog"), + highlight: $("#layer-highlight"), paths: $("#layer-paths"), targets: $("#layer-targets"), entities: $("#layer-entities"), diff --git a/tests/map_view_test.js b/tests/map_view_test.js index 132812c..11ed0dc 100644 --- a/tests/map_view_test.js +++ b/tests/map_view_test.js @@ -1,7 +1,7 @@ import { TestCase } from "./framework/test_case.js"; import { setupDom, teardownDom } from "./framework/dom.js"; import { MapView, inwardOffset } from "../client/js/map_view.js"; -import { mapToLocal, COL_STEP } from "../shared/hex.js"; +import { mapToLocal, COL_STEP, HEX_W, parseKey } from "../shared/hex.js"; import { CIVILISATIONS, PROTO_UNITS } from "../shared/data.js"; import { defaultState, smallState, mapLayers, adjacentLand, seaTile } from "./framework/helpers.js"; @@ -40,6 +40,54 @@ export class MapViewTest extends TestCase { } } + async test_dom_terrain_uses_the_tiling_land_textures() { + const state = smallState(); + const { env, map } = await this.build(state); + try { + const local = mapToLocal(2, 1); + const land = env.window.document.createElement("div"); + map._applyTerrainTexture(land, { terrainType: "Land", col: 3 }, local); + this.assertTrue(land.style.backgroundImage.includes("textures/grass.png"), "plains use grass"); + // Positioned from the tile's world origin, so neighbours continue the pattern. + this.assertTrue( + land.style.backgroundPosition.includes(`${-(local.x - HEX_W / 2)}px`), + `world-positioned tiling: ${land.style.backgroundPosition}` + ); + + const forest = env.window.document.createElement("div"); + map._applyTerrainTexture(forest, { terrainType: "Forest", col: 0 }, local); + this.assertTrue(forest.style.backgroundImage.includes("textures/forest.png"), "forest overlays"); + this.assertTrue( + forest.style.backgroundImage.indexOf("forest.png") < + forest.style.backgroundImage.indexOf("grass.png"), + "the forest overlay is the top layer" + ); + + const desert = env.window.document.createElement("div"); + map._applyTerrainTexture(desert, { terrainType: "Desert", col: 4 }, local); + this.assertTrue(desert.style.backgroundImage.includes("textures/sand.png"), "desert uses sand"); + this.assertFalse(desert.style.backgroundImage.includes("grass.png"), "desert has no grass"); + + for (const [terrain, overlay] of [ + ["Hills", "hills.png"], + ["Mountain", "mountain.png"], + ]) { + const tile = env.window.document.createElement("div"); + map._applyTerrainTexture(tile, { terrainType: terrain, col: 1 }, local); + this.assertTrue( + tile.style.backgroundImage.includes(`textures/${overlay}`), + `${terrain} overlays ${overlay}` + ); + this.assertTrue( + tile.style.backgroundImage.includes("textures/grass.png"), + `${terrain} sits on grass` + ); + } + } finally { + teardownDom(env); + } + } + async test_renders_the_pre_generated_road_network() { const state = smallState(); const { env, map, snapshot } = await this.build(state); @@ -202,33 +250,15 @@ export class MapViewTest extends TestCase { } } - async test_zooming_out_switches_to_the_overview() { + async test_far_zoom_keeps_the_detailed_terrain() { const state = smallState(); const { env, map } = await this.build(state); try { - this.assertFalse(map.lod, "detailed at close zoom"); - this.assertSize(env.$("#layer-overview .overview-block"), 0, "no overview until it is needed"); - map.camera.zoom = 0.01; map._applyCamera(); - this.assertTrue(map.lod, "overview at far zoom"); - this.assertGreater( - env.$("#layer-overview .overview-block").length, - 0, - "overview blocks built" - ); - this.assertLess( - env.$("#layer-overview .overview-block").length, - Object.keys(map.tiles).length, - "overview is simpler than the full map" - ); - this.assertTrue(env.$("#map-world").hasClass("lod")); - this.assertTrue(env.$("#layer-terrain").hasClass("detail-layer")); - - map.camera.zoom = 1; - map._applyCamera(); - this.assertFalse(map.lod, "detailed again when zoomed in"); this.assertFalse(env.$("#map-world").hasClass("lod")); + this.assertTrue(env.$("#layer-terrain").hasClass("detail-layer")); + this.assertSize(env.$("#layer-overview"), 0, "there is no simplified overview"); } finally { teardownDom(env); } @@ -543,6 +573,42 @@ export class MapViewTest extends TestCase { } } + async test_selected_tile_is_outlined() { + const state = smallState(); + const { env, map } = await this.build(state); + try { + map.setSelectedTile({ x: 0, y: 0 }); + const hex = env.$("#layer-highlight .tile-selection"); + this.assertSize(hex, 1, "the outline is created"); + this.assertTrue(hex.hasClass("active"), "the outline is shown"); + const points = (hex.find("polygon").attr("points") || "").split(" "); + this.assertSize(points, 6, "a hexagon outline"); + const wrapped = map._wrappedPosition(mapToLocal(0, 0)); + this.assertApprox(parseFloat(hex[0].style.left), wrapped.x - HEX_W / 2, 1e-6); + + map.clearSelectedTile(); + this.assertFalse(hex.hasClass("active"), "the outline is hidden again"); + } finally { + teardownDom(env); + } + } + + async test_selected_tile_reports_its_region() { + const state = smallState(); + const { env, map } = await this.build(state); + try { + this.assertGreater(map.regions.size, 0, "regions are mirrored from the snapshot"); + const [tileKey, cityId] = map.regions.entries().next().value; + map.setSelectedTile(parseKey(tileKey)); + this.assertEqual(map._selectedRegion, cityId, "the tile's region is selected as a whole"); + + map.clearSelectedTile(); + this.assertNull(map._selectedRegion, "clearing drops the region"); + } finally { + teardownDom(env); + } + } + async test_stacked_units_show_one_icon_with_a_count_badge() { const state = smallState(); const unit = state.units.find((u) => u.civ === 0); diff --git a/tests/textures_test.js b/tests/textures_test.js new file mode 100644 index 0000000..1b19b97 --- /dev/null +++ b/tests/textures_test.js @@ -0,0 +1,215 @@ +import { TestCase } from "./framework/test_case.js"; +import { MapTopology, key, mapToLocal } from "../shared/hex.js"; +import { GLMapRenderer } from "../client/js/map_view/webgl.js"; +import { + TEXTURE_DESIRED_REPEAT, + blendBeach, + blendTextures, + isTexturedLand, + landTexture, + overlayNormal, + terrainLayers, + textureRepeat, + vertexTextureWeights, +} from "../client/js/map_view/textures.js"; + +// A flat fixture with the given terrain per coordinate, plus the renderer +// helpers `_textureRing`/`_textureWeights` under test. +function fixture(terrainAt, size = 5) { + const topology = new MapTopology({ x: size, y: size }, false); + const tiles = {}; + for (let y = topology.originY; y < topology.originY + size; y++) { + for (let x = topology.originX; x < topology.originX + size; x++) { + tiles[key(x, y)] = { terrainType: terrainAt(x, y), col: 0 }; + } + } + const renderer = Object.create(GLMapRenderer.prototype); + return { renderer, topology, tiles, view: { seed: 7, topology, tiles, _period: 0 } }; +} + +export class TexturesTest extends TestCase { + test_grassy_land_uses_grass_and_desert_uses_sand() { + for (const type of ["Forest", "Hills", "Land", "Mountain"]) { + this.assertEqual(landTexture(type).base, "grass", `${type} base`); + } + this.assertEqual(landTexture("Desert").base, "sand", "desert base"); + this.assertNull(landTexture("Sea")); + this.assertNull(landTexture("Ice")); + } + + test_only_terrain_with_a_tiling_texture_is_blended() { + this.assertTrue(isTexturedLand({ terrainType: "Land" })); + this.assertTrue(isTexturedLand({ terrainType: "Desert" })); + this.assertTrue(isTexturedLand({ terrainType: "Mountain" })); + this.assertFalse(isTexturedLand({ terrainType: "Sea" })); + this.assertFalse(isTexturedLand(null)); + } + + test_forest_hills_and_mountains_overlay_the_base() { + this.assertEqual(terrainLayers("Forest"), [ + "assets/textures/forest.png", + "assets/textures/grass.png", + ]); + this.assertEqual(terrainLayers("Hills"), [ + "assets/textures/hills.png", + "assets/textures/grass.png", + ]); + this.assertEqual(terrainLayers("Mountain"), [ + "assets/textures/mountain.png", + "assets/textures/grass.png", + ]); + this.assertEqual(terrainLayers("Desert"), ["assets/textures/sand.png"]); + this.assertEqual(terrainLayers("Land"), ["assets/textures/grass.png"]); + this.assertEmpty(terrainLayers("Ice"), "the atlas terrains keep their art"); + } + + test_only_hills_and_mountains_have_normal_maps() { + this.assertEqual(overlayNormal("hills"), "hills_normal_map"); + this.assertEqual(overlayNormal("mountain"), "mountains_normal_map"); + this.assertNull(overlayNormal("forest")); + this.assertNull(overlayNormal("sand")); + } + + test_the_repeat_snaps_to_a_whole_number_across_the_wrap() { + const repeat = textureRepeat(1890); + this.assertApprox(Math.round(1890 / repeat), 1890 / repeat, 1e-9, "a whole number of repeats"); + this.assertApprox(repeat, TEXTURE_DESIRED_REPEAT, 4, "close to the desired density"); + this.assertEqual(textureRepeat(0), TEXTURE_DESIRED_REPEAT, "flat maps use the desired repeat"); + } + + test_a_shared_edge_blends_to_an_even_mix() { + // (0,0) is land and (1,0) is desert; the point sits on their shared edge, + // halfway between the two centres. + const blend = blendTextures([ + { tile: { terrainType: "Land" }, dx: -23.625, dy: -14 }, + { tile: { terrainType: "Desert" }, dx: 23.625, dy: 14 }, + ]); + this.assertApprox(blend.grass, 0.5, 1e-9); + this.assertApprox(blend.sand, 0.5, 1e-9); + } + + test_a_tile_centre_stays_almost_purely_its_own_texture() { + const blend = blendTextures([ + { tile: { terrainType: "Desert" }, dx: 0, dy: 0 }, + { tile: { terrainType: "Land" }, dx: 47.25, dy: 28 }, + { tile: { terrainType: "Land" }, dx: 47.25, dy: -28 }, + { tile: { terrainType: "Land" }, dx: 0, dy: 56 }, + ]); + this.assertGreater(blend.sand, 0.9, "the tile centre keeps its sand"); + this.assertLess(blend.grass, 0.1, "the neighbours barely bleed in"); + } + + test_overlays_fade_towards_grass() { + const centre = blendTextures([ + { tile: { terrainType: "Forest" }, dx: 0, dy: 0 }, + { tile: { terrainType: "Land" }, dx: 47.25, dy: 28 }, + { tile: { terrainType: "Land" }, dx: 0, dy: 56 }, + ]); + this.assertGreater(centre.forest, 0.9, "a forest tile keeps its overlay"); + const edge = blendTextures([ + { tile: { terrainType: "Forest" }, dx: -23.625, dy: -14 }, + { tile: { terrainType: "Land" }, dx: 23.625, dy: 14 }, + ]); + this.assertApprox(edge.forest, 0.5, 1e-9, "the overlay is half strength on the shared edge"); + + const hills = blendTextures([{ tile: { terrainType: "Hills" }, dx: 0, dy: 0 }]); + this.assertApprox(hills.hills, 1, 1e-9, "a hill keeps its relief overlay"); + this.assertApprox(hills.grass, 1, 1e-9, "hills sit on grass"); + const mountain = blendTextures([{ tile: { terrainType: "Mountain" }, dx: 0, dy: 0 }]); + this.assertApprox(mountain.mountain, 1, 1e-9, "a mountain keeps its overlay"); + this.assertApprox(mountain.grass, 1, 1e-9, "mountains sit on grass"); + } + + test_the_blend_is_a_shared_field_from_either_tile() { + // Both tiles on a shared edge must compute the same weights, otherwise the + // transition would show a seam where the two fans meet. + const { renderer, tiles, view } = fixture((x) => (x >= 1 ? "Desert" : "Land")); + const centreLand = mapToLocal(0, 0); + const centreDesert = mapToLocal(1, 0); + const ringLand = renderer._textureRing(view, 0, 0, centreLand); + const ringDesert = renderer._textureRing(view, 1, 0, centreDesert); + const px = (centreLand.x + centreDesert.x) / 2; + const py = (centreLand.y + centreDesert.y) / 2; + const fromLand = renderer._textureWeights(tiles[key(0, 0)], ringLand, px, py); + const fromDesert = renderer._textureWeights(tiles[key(1, 0)], ringDesert, px, py); + this.assertEqual(fromLand, fromDesert, "the shared vertex agrees"); + this.assertApprox(fromLand[0], 0.5, 1e-9, "grass"); + this.assertApprox(fromLand[1], 0.5, 1e-9, "sand"); + } + + test_terrain_vertices_carry_world_space_texture_coordinates() { + const { renderer, view } = fixture(() => "Land", 3); + renderer.terrain = { name: "terrain" }; + renderer.gl = { STATIC_DRAW: 0, DYNAMIC_DRAW: 0 }; + let captured = null; + renderer._upload = (pass, arr, usage, floats = 8) => { + captured = arr; + pass.floats = floats; + }; + renderer.buildTerrain(view); + this.assertEqual(captured.length % 14, 0, "fourteen floats per terrain vertex"); + // The undistorted world position rides along as UVs; tile (0,0)'s right + // corner is the ideal lattice point (31.5, 0), independent of the warp. + let found = false; + for (let i = 0; i < captured.length; i += 14) { + this.assertTrue(Number.isFinite(captured[i + 4]), "world x is finite"); + this.assertTrue(Number.isFinite(captured[i + 5]), "world y is finite"); + if (Math.abs(captured[i + 4] - 31.5) < 1e-9 && Math.abs(captured[i + 5]) < 1e-9) { + found = true; + } + } + this.assertTrue(found, "the ideal shared corner appears as a world coordinate"); + } + + test_atlas_terrain_ignores_the_land_blend() { + const atlas = vertexTextureWeights( + { terrainType: "Ice" }, + [{ tile: { terrainType: "Land" }, dx: 0, dy: 0 }] + ); + this.assertEqual(atlas, [0, 0, 1, 0, 0, 0, 0, 0], "ice stays on the atlas"); + const land = vertexTextureWeights( + { terrainType: "Land" }, + [{ tile: { terrainType: "Land" }, dx: 0, dy: 0 }] + ); + this.assertEqual(land, [1, 0, 0, 0, 0, 0, 0, 0], "plains use the grass texture"); + const mountain = vertexTextureWeights( + { terrainType: "Mountain" }, + [{ tile: { terrainType: "Mountain" }, dx: 0, dy: 0 }] + ); + this.assertEqual(mountain, [1, 0, 0, 0, 0, 1, 0, 0], "mountains sit on grass with their overlay"); + } + + test_sea_is_flagged_for_the_water_shader() { + const sea = vertexTextureWeights( + { terrainType: "Sea" }, + [{ tile: { terrainType: "Sea" }, dx: 0, dy: 0 }] + ); + this.assertEqual(sea, [0, 0, 1, 0, 0, 0, 1, 0], "sea rides on the atlas but is flagged as water"); + this.assertFalse(isTexturedLand({ terrainType: "Sea" }), "sea is not a tiling land texture"); + } + + test_a_coast_blends_sand_into_non_mountain_land() { + const samples = [ + { tile: { terrainType: "Land" }, dx: -23.625, dy: -14 }, + { tile: { terrainType: "Sea" }, dx: 23.625, dy: 14 }, + ]; + const plains = vertexTextureWeights({ terrainType: "Land" }, samples); + this.assertGreater(plains[7], 0, "the shoreline gets some sand"); + this.assertLess(plains[7], 1, "the ground still shows through"); + const mountain = vertexTextureWeights({ terrainType: "Mountain" }, samples); + this.assertEqual(mountain[7], 0, "mountain coasts stay rocky"); + } + + test_beach_is_a_shared_field_between_land_and_sea() { + const inland = blendBeach([ + { tile: { terrainType: "Land" }, dx: 0, dy: 0 }, + { tile: { terrainType: "Land" }, dx: 47.25, dy: 28 }, + ]); + this.assertEqual(inland, 0, "inland ground has no beach"); + const shore = blendBeach([ + { tile: { terrainType: "Land" }, dx: -23.625, dy: -14 }, + { tile: { terrainType: "Sea" }, dx: 23.625, dy: 14 }, + ]); + this.assertApprox(shore, 0.5, 1e-9, "the shoreline is an even mix"); + } +} diff --git a/tests/webgl_geometry_test.js b/tests/webgl_geometry_test.js new file mode 100644 index 0000000..7c18c07 --- /dev/null +++ b/tests/webgl_geometry_test.js @@ -0,0 +1,162 @@ +import { TestCase } from "./framework/test_case.js"; +import { GLMapRenderer } from "../client/js/map_view/webgl.js"; +import { CORNERS, latticeKey } from "../client/js/map_view/distortion.js"; +import { MapTopology, key, mapToLocal } from "../shared/hex.js"; + +// Minimal stand-in for a GLMapRenderer: the prototype supplies the pure +// geometry helpers, and only the two GL touch-points (`_upload`, `this.gl`) are +// stubbed so the builders can run without a context. +function makeRenderer() { + const renderer = Object.create(GLMapRenderer.prototype); + renderer.gl = { DYNAMIC_DRAW: 0 }; + renderer.captured = {}; + renderer.borders = { name: "borders" }; + renderer.roads = { name: "roads" }; + renderer.regionOverlay = { name: "region" }; + renderer._upload = function (pass, arr) { + this.captured[pass.name] = arr; + pass.count = arr.length / 8; + }; + return renderer; +} + +function view(renderer, { size = 3, territory, regions, selectedRegion = null, roads = new Set() } = {}) { + const topology = new MapTopology({ x: size, y: size }, false); + const tiles = {}; + for (let y = topology.originY; y < topology.originY + size; y++) { + for (let x = topology.originX; x < topology.originX + size; x++) { + tiles[key(x, y)] = { col: 0 }; + } + } + return { + seed: 4242, + topology, + tiles, + roads, + territory: new Map(territory), + regions: new Map(regions), + civilisations: [{ primaryColour: "#ff0000" }, { primaryColour: "#00ff00" }], + _selectedRegion: selectedRegion, + }; +} + +export class WebglGeometryTest extends TestCase { + async test_chains_stitch_edges_across_tiles() { + const renderer = makeRenderer(); + const edges = [ + { points: [{ x: 0, y: 0 }, { x: 1, y: 0 }], startKey: "0", endKey: "1" }, + { points: [{ x: 1, y: 0 }, { x: 2, y: 0 }], startKey: "1", endKey: "2" }, + { points: [{ x: 9, y: 9 }, { x: 8, y: 9 }], startKey: "8", endKey: "7" }, + ]; + const chains = renderer._chainEdges(edges); + this.assertSize(chains, 2, "two disjoint runs"); + const run = chains.find((chain) => chain.points.length === 3); + this.assertNotNull(run, "the joined run has three points"); + this.assertFalse(run.closed, "a run with two open ends is not closed"); + this.assertEqual(run.points[0], { x: 0, y: 0 }, "the shared corner is only listed once"); + } + + async test_closed_boundary_is_one_chain() { + const renderer = makeRenderer(); + const edges = [ + { points: [{ x: 0, y: 0 }, { x: 1, y: 0 }], startKey: "0", endKey: "1" }, + { points: [{ x: 1, y: 0 }, { x: 1, y: 1 }], startKey: "1", endKey: "2" }, + { points: [{ x: 1, y: 1 }, { x: 0, y: 0 }], startKey: "2", endKey: "0" }, + ]; + const chains = renderer._chainEdges(edges); + this.assertSize(chains, 1, "the loop is a single chain"); + this.assertTrue(chains[0].closed, "the loop closes"); + this.assertSize(chains[0].points, 3, "the repeated closing point is dropped"); + } + + async test_border_strokes_are_finite() { + const renderer = makeRenderer(); + const territory = [ + ["-1,-1", 0], ["0,-1", 0], ["-1,0", 0], ["0,0", 0], + ["1,0", 1], ["1,1", 1], + ]; + const state = view(renderer, { territory }); + renderer.buildBorders(state); + const arr = renderer.captured.borders; + this.assertGreater(arr.length, 0, "borders are built"); + for (const value of arr) { + this.assertTrue(Number.isFinite(value), "every vertex coordinate is finite"); + } + } + + async test_a_nations_whole_outline_is_one_closed_chain() { + // A 3x3 block owned by a single nation has no open ends, so its border must + // chain into one loop rather than a handful of per-tile strokes. + const renderer = makeRenderer(); + const territory = []; + for (let y = -1; y <= 1; y++) { + for (let x = -1; x <= 1; x++) territory.push([key(x, y), 0]); + } + const state = view(renderer, { territory }); + const edges = []; + for (const [k, owner] of state.territory) { + const coords = k.split(",").map(Number); + const centre = mapToLocal(coords[0], coords[1]); + const neighbours = renderer._edgeNeighbours(state, coords[0], coords[1]); + for (let e = 0; e < CORNERS.length; e++) { + const neighbour = neighbours[e]; + if (!neighbour) continue; + const nk = key(neighbour.x, neighbour.y); + if (state.territory.get(nk) === owner) continue; + const a = { x: centre.x + CORNERS[e].x, y: centre.y + CORNERS[e].y }; + const next = CORNERS[(e + 1) % CORNERS.length]; + const b = { x: centre.x + next.x, y: centre.y + next.y }; + const dx = b.x - a.x; + const dy = b.y - a.y; + const mx = (a.x + b.x) / 2; + const my = (a.y + b.y) / 2; + const ownerIsLeft = (centre.x - mx) * -dy + (centre.y - my) * dx >= 0; + edges.push(ownerIsLeft + ? { points: [a, b], startKey: latticeKey(a.x, a.y), endKey: latticeKey(b.x, b.y) } + : { points: [b, a], startKey: latticeKey(b.x, b.y), endKey: latticeKey(a.x, a.y) }); + } + } + const chains = renderer._chainEdges(edges); + this.assertSize(chains, 1, "the whole coastline is one run"); + this.assertTrue(chains[0].closed, "the block's border wraps all the way round"); + } + + async test_region_border_is_suppressed_on_a_national_border() { + const selected = 0; + const sharedDifferentOwner = [ + ["0,0", 0], ["1,0", 1], + ]; + const sharedSameOwner = [ + ["0,0", 0], ["1,0", 0], + ]; + const regions = [["0,0", 0], ["1,0", 1]]; + + const splitRenderer = makeRenderer(); + splitRenderer.buildRegionOverlay( + view(splitRenderer, { territory: sharedDifferentOwner, regions, selectedRegion: selected }) + ); + const split = splitRenderer.captured.region.length; + + const joinedRenderer = makeRenderer(); + joinedRenderer.buildRegionOverlay( + view(joinedRenderer, { territory: sharedSameOwner, regions, selectedRegion: selected }) + ); + const joined = joinedRenderer.captured.region.length; + + // Only the selected tile (0,0) contributes; its six-edge fill is 24 tris. + const fill = 24 * 3 * 8; + this.assertEqual(split, fill, "no dotted line is drawn over the national border"); + this.assertGreater(joined, fill, "a same-owner region border is still dotted"); + } + + async test_roads_join_at_a_bend_without_a_notch() { + const renderer = makeRenderer(); + const state = view(renderer, { roads: new Set(["-1,0", "0,0", "1,0"]) }); + renderer.buildRoads(state); + const arr = renderer.captured.roads; + this.assertGreater(arr.length, 0, "roads are built"); + for (const value of arr) { + this.assertTrue(Number.isFinite(value), "every road vertex is finite"); + } + } +}