From c1dc5dc7707e30074653d6ea02c007f6869356a7 Mon Sep 17 00:00:00 2001 From: Denis Ranneft Date: Sat, 4 Apr 2026 02:07:46 +0300 Subject: [PATCH] graphics3 --- .gitignore | 1 + docs/pixellab-mcp-schema.md | 10 + .../enemies/enemy.boar_l2_2_forest.south.png | Bin 0 -> 5071 bytes .../enemies/enemy.boar_l2_2_meadow.south.png | Bin 0 -> 3781 bytes .../enemies/enemy.boar_l3_3_forest.south.png | Bin 0 -> 3138 bytes .../enemies/enemy.boar_l3_3_ruins.south.png | Bin 0 -> 5123 bytes .../enemies/enemy.boar_l4_4_canyon.south.png | Bin 0 -> 4097 bytes .../enemies/enemy.boar_l4_4_ruins.south.png | Bin 0 -> 3552 bytes .../enemies/enemy.wolf_l1_1_forest.south.png | Bin 0 -> 4875 bytes .../enemies/enemy.wolf_l1_1_meadow.south.png | Bin 0 -> 3306 bytes .../enemies/enemy.wolf_l2_2_forest.south.png | Bin 0 -> 4292 bytes .../enemies/enemy.wolf_l2_2_ruins.south.png | Bin 0 -> 3024 bytes .../enemies/enemy.wolf_l3_3_canyon.south.png | Bin 0 -> 4831 bytes .../enemies/enemy.wolf_l3_3_ruins.south.png | Bin 0 -> 2787 bytes .../enemies/enemy.wolf_l4_4_canyon.south.png | Bin 0 -> 2387 bytes .../enemies/enemy.wolf_l4_4_swamp.south.png | Bin 0 -> 1901 bytes .../enemies/enemy.wolf_l5_5_astral.south.png | Bin 0 -> 3985 bytes .../enemy.wolf_l5_5_volcanic.south.png | Bin 0 -> 4290 bytes frontend/assets/prop/prop.bones.v0.png | Bin 0 -> 1940 bytes frontend/assets/prop/prop.bones.v1.png | Bin 0 -> 1557 bytes frontend/assets/prop/prop.bush.v0.png | Bin 0 -> 4058 bytes frontend/assets/prop/prop.bush.v1.png | Bin 0 -> 5687 bytes frontend/assets/prop/prop.camp_bag.v0.png | Bin 0 -> 1672 bytes frontend/assets/prop/prop.camp_fire.v0.png | Bin 0 -> 1018 bytes frontend/assets/prop/prop.camp_tent.v0.png | Bin 0 -> 2047 bytes frontend/assets/prop/prop.leaves.v0.png | Bin 0 -> 1614 bytes frontend/assets/prop/prop.leaves.v1.png | Bin 0 -> 2527 bytes frontend/assets/prop/prop.mushroom.v0.png | Bin 0 -> 3208 bytes frontend/assets/prop/prop.mushroom.v1.png | Bin 0 -> 3789 bytes frontend/assets/prop/prop.rest_camp.wild.png | Bin 0 -> 20111 bytes frontend/assets/prop/prop.ruin.v0.png | Bin 0 -> 6930 bytes frontend/assets/prop/prop.ruin.v1.png | Bin 0 -> 7068 bytes frontend/assets/prop/prop.stump.v0.png | Bin 0 -> 3287 bytes frontend/assets/prop/prop.stump.v1.png | Bin 0 -> 3983 bytes frontend/package-lock.json | 11 + frontend/package.json | 4 +- frontend/public/assets/game/manifest.json | 175 +++- .../scripts/pixellab-enemy-south-batch.mjs | 347 +++++++ .../src/game/assets/gameSpriteRegistry.ts | 74 ++ .../src/game/assets/resolveGameAssetUrl.ts | 25 +- frontend/src/game/assets/spriteMapping.ts | 116 +++ frontend/src/game/camera.ts | 4 +- frontend/src/game/enemyVisuals.ts | 48 + frontend/src/game/engine.ts | 16 +- frontend/src/game/renderer.ts | 930 +++++++++++++----- 45 files changed, 1515 insertions(+), 246 deletions(-) create mode 100644 frontend/assets/enemies/enemy.boar_l2_2_forest.south.png create mode 100644 frontend/assets/enemies/enemy.boar_l2_2_meadow.south.png create mode 100644 frontend/assets/enemies/enemy.boar_l3_3_forest.south.png create mode 100644 frontend/assets/enemies/enemy.boar_l3_3_ruins.south.png create mode 100644 frontend/assets/enemies/enemy.boar_l4_4_canyon.south.png create mode 100644 frontend/assets/enemies/enemy.boar_l4_4_ruins.south.png create mode 100644 frontend/assets/enemies/enemy.wolf_l1_1_forest.south.png create mode 100644 frontend/assets/enemies/enemy.wolf_l1_1_meadow.south.png create mode 100644 frontend/assets/enemies/enemy.wolf_l2_2_forest.south.png create mode 100644 frontend/assets/enemies/enemy.wolf_l2_2_ruins.south.png create mode 100644 frontend/assets/enemies/enemy.wolf_l3_3_canyon.south.png create mode 100644 frontend/assets/enemies/enemy.wolf_l3_3_ruins.south.png create mode 100644 frontend/assets/enemies/enemy.wolf_l4_4_canyon.south.png create mode 100644 frontend/assets/enemies/enemy.wolf_l4_4_swamp.south.png create mode 100644 frontend/assets/enemies/enemy.wolf_l5_5_astral.south.png create mode 100644 frontend/assets/enemies/enemy.wolf_l5_5_volcanic.south.png create mode 100644 frontend/assets/prop/prop.bones.v0.png create mode 100644 frontend/assets/prop/prop.bones.v1.png create mode 100644 frontend/assets/prop/prop.bush.v0.png create mode 100644 frontend/assets/prop/prop.bush.v1.png create mode 100644 frontend/assets/prop/prop.camp_bag.v0.png create mode 100644 frontend/assets/prop/prop.camp_fire.v0.png create mode 100644 frontend/assets/prop/prop.camp_tent.v0.png create mode 100644 frontend/assets/prop/prop.leaves.v0.png create mode 100644 frontend/assets/prop/prop.leaves.v1.png create mode 100644 frontend/assets/prop/prop.mushroom.v0.png create mode 100644 frontend/assets/prop/prop.mushroom.v1.png create mode 100644 frontend/assets/prop/prop.rest_camp.wild.png create mode 100644 frontend/assets/prop/prop.ruin.v0.png create mode 100644 frontend/assets/prop/prop.ruin.v1.png create mode 100644 frontend/assets/prop/prop.stump.v0.png create mode 100644 frontend/assets/prop/prop.stump.v1.png create mode 100644 frontend/scripts/pixellab-enemy-south-batch.mjs create mode 100644 frontend/src/game/assets/gameSpriteRegistry.ts create mode 100644 frontend/src/game/assets/spriteMapping.ts diff --git a/.gitignore b/.gitignore index f4fc41e..93bc933 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ backend/vendor/ frontend/node_modules/ frontend/dist/ frontend/.vite/ +frontend/scripts/.enemy-south-pixellab-state.json # IDE .idea/ diff --git a/docs/pixellab-mcp-schema.md b/docs/pixellab-mcp-schema.md index 119f13f..f47baf7 100644 --- a/docs/pixellab-mcp-schema.md +++ b/docs/pixellab-mcp-schema.md @@ -57,3 +57,13 @@ Less central to AutoHero’s **diamond isometric** ground plane but valid for ex ## MCP configuration (local) Cursor `mcp.json` should use HTTP transport and Bearer token (see PixelLab setup). **Never commit API tokens** to the game repository. + +## Batch: 220 enemy templates (south, transparent) + +For all `enemies.type` slugs, use the repo script (API v2, same backend as MCP `create_map_object`): + +1. Ensure archetype reference PNGs exist under `frontend/assets/enemies/` (`enemy.wolf.png`, … per `manifest.json`). +2. Set `PIXELLAB_API_TOKEN` (same token as MCP Bearer). +3. From `frontend/`: `npm run gen:enemy-south:pixellab -- --limit 3` (smoke), then without `--limit` for the full run. State file: `frontend/scripts/.enemy-south-pixellab-state.json` (resume-safe). Updates `enemy..south` in `manifest.json` and writes `enemies/enemy..south.png`. + +Implementation: `frontend/scripts/pixellab-enemy-south-batch.mjs` (uses `POST /v2/map-objects`, polls `https://api.pixellab.ai/mcp/map-objects/{id}/download`). diff --git a/frontend/assets/enemies/enemy.boar_l2_2_forest.south.png b/frontend/assets/enemies/enemy.boar_l2_2_forest.south.png new file mode 100644 index 0000000000000000000000000000000000000000..9ca30567e8e6c04f74005896bb26e06e34bbb4f4 GIT binary patch literal 5071 zcmV;=6EN(FP)HX|Eqg+i9o6 z^@HI$Tsf}Sq1-S7(>W-V>G6Fuz)>KC7;-=YvIIHbZog{$tpl<*-A0M)9v8^AQp+&aaPsGl2AalC40I(oK&z{LXAK$3V^32DK#zk z0KFs>007Rm9#IJ_n-=MZW*FHu44Y9rw_MS11xRyzK?ww*kzJEW@`5%}T1bO(crJx- zIJvCX2@O|(Kp_@$Znh&>xIxcbyj1wJ^Ay$6qFvTBL^Sf@*_Nv#)o?p{xmcV*q4ZNw zjFI9MGv%Mf4bF9Wz2IHnDOFV_Tmv-J115YCkKqI9^ERm8Olg0>Xke>3- z!rs9P*`Ot0=xvwXmk`EmBa!y;sq9ixMBOP!SU?GuE@U`T#Nx{7Wp>&bi!-9|!{Owj z&(=6wpmDZ{zYB_JBpMO}7&ZeVrA3P7xjY}&YY8}fsxXJAT%Hd4dST< zkFl7joF6GP&K4xoEC688>~5$M>aKw7%4R-m7lf{KKKqz|IwpS37Pj=b^qkRU5xt?2 zN~pU6f}sVfP|E$4jsd*aNIgYJK>jEW@(omNvYxJnU=GtiOh952Z()Ct~Dt!+znYVm8TiJB`w zD#UmyCGxtIMx?@H*o@4w^C1>zU@_}t9wO`+lnYBcpPtq;7Svn;g2j5eJ)B0Z%(2;4 z4annw5~Syae|K;0FKKS^1T6+*aa@PFuCbt|3dpW@O8GyVf><^!6S&l}rJu9!OC%Yv zY`QG#rwpVl7y}DlP%o6NfI+jHTg+9ZbRs}n|51tqESt`|7GT+QP6m{sGYABuC0lr`pxH@!s%Ec`?D# z-h$9r@oS1oP>})#)~&$6x)ph?B_D?iR-B^w^Gd?a9z&@42=PKts=cEH`wtH*KF8AD zg76aWnkZWVNhX7#gC}7!=&@(l2K?~0yen$sf zv<5C(Q&fdiN2+0@vX()fIGcuM>{U|3mm8mcJVKXT(vJo@Gb z=pS%n&FXI4a7!DsLoM9Lr@mGJeBLiudo0ce%PZ6d0DxnwTQ-)(L;-dSrUGH~uq>FF zC<2n?NGWX%hm+`Xw1Q>RnD&Km^UYg<`BT8wFN{JNG2Q5GYj?G;K$L+FP0090F4RZ$Ys%hDI z!4sk}8rwVCke~?MvUiJk{>;cEzWU^}_`9>+jC02((08S)sP7d@ra}5bV0F2vaV=00 z1p&a^TpZuOeiKH|T*8rKqaaBFt9v^^5jJjoG6VqVrYQ7ncB5~zyXf&9>$~7s--T|9 zLT6kaAEGfD8;nNmylo5i?B0l(a1=A)C_XrN0w<4n@#r7zk&Pu7ODP~y^=Sw$TLFnQ zPmd`z2VF*m+qMrNcs79JC&odiSTQZ^rYN|3>}5~T-D8KwO2L}oz_MxRNHw;1v|;DI z&4|X6h{lsR@Zm8W`0yC6>9FFFeb)m3NQ*%n!|1ZexIG_)tSQE(5t~
Zx13OGk! zR6X`TA7EX#Lp-)>O(#Z9i~#`bRx@_mY_K^jmCg~Ai5I3d!NHo~(0^4g7&eW`i6DlC zhS6nd#UnTF#Ku(;nxPkhw#7t=9-9wJ*;0$upm1e%QSnOHv+p`=+qnTPq!!z|oKUBL zkrQK}Q!I2VEimfIYRnTh53lHr*Ia>~o=%(}3*gAmS!`^z;DM_*$gX|vvu|?l*R<&s zg&dB=8=9(CtboAh$6;C52dl-5cFKU82G>BH0?r*7D{69 zV}T$>{6V-nER_tY2$oG_+UpmC_?fel0Dx;|Aw zsJQMLp?#`H-W}q$tnLO$@WUNGJ&D_QuCH`xOa;QS@&lzx04ljSqL&kj*Ji^%9fQkh5-YQ%pep6)I6JL6>h)t{ zb`F7f0_hAZT5csE2#xFug=Tr-{l0r8QfdzV`Dbj~dkp|!?D%L|&xu}6fYP)e-+o0Y zAP5agi3wHPd5F+H)r1e69R#+o>jA*ihrQiY)_B?0@9ekZtIyA4^*|S{+bR#dv%{k} zergt%vbwvZ=LlBr%Ov4c>t)9SU||OVJGp{8M_*KIW3(Dw+^Q}MA~e5ZFVzF8 zy7d#GeJVS-f+GkGJUx6IxG)teIU5ShL_jBK5cB!J^h>d98b98DKz996uOI0Qi!Hs* zqMsq9mjGDBR+IJ!XwA?s9A*#|!%1e7c)RG9Un>}-IbaGasi*q}A zx%FLqE4*MN!4R*^I6GZ*+j~_NFlcsjE7tNn{ObeXV0wDtOSiRB1--yxqF}SN0sypx z2It2D2uI==WfmxdAT${0b;vloTnIroAN2@=)TjyB^XwpCvr=d^Q3!Ks(b}U?1`*n) ziqO82-Z`iV1+7}yj(@jo#Eb78Df%9%<)0kBi2a9$@&1t!tkN1}^wME6Vr|dj9#ssA zS@{YAm0nBx5{O=yL-fKN0>LPPp(K>zLouj{0(2@By4V){vh!A)_4)t+dQyW}T)FLx z)Dh6nzbkgu>%-8A3wWjFngwo_tb$oK4TDy0m3d1HdEm<=Me8mE6Y1mtUI~IHWdFq3 z>5{EpimGKn1BR8HP_gVV8Syy0xHr^^{fCG13LfC|{VV$T`zB6LV(5#Ln4CF-FPsnZ zV`aT^B&-cqt!zj1!d%XH03T0;)K@6D36vuUf3fdlhg`{(2)Phy+L-l1+a(P2I&kR3 z1+Z*7Cl!^hmd~BPgx8Or!*izV@P+e1{C)R1Z1Olj5(FrHZl91&Rk}Q~g|E|UN-n@v z8&&fFVTVirWilY1Od&Hdm$xotZ#Q9V>N1}D&F294EaT4jX8?druz}L+@-A#r94D?+ zi$%8OUyeh}-kCCySaGrPo1dOtE)bWak2x!TPCN{#{7{`hYd6>*Nfs1g=o0_;|^ zqVUD@l5^Sdp^)$k)mx<2ztp?pfHH%yUZq^`&}-G0_VPPY(_TNeZ0gH9hL+GkOK8OW zf3fnlpb(Z#BNkrhxXTwrD-pggldPL(l%)WoMfo+OGszS}$uuqw`@|hAZ5|G1*ff|j zc93QhN%$``?EjEp`i%vQ~5GUj4}yay=xtR z&muyeoG5M)C0M>t(^0C`2q;AfCeq0@DgXmDDo3xQjIz28kY5oU-24^i;|u+HX6j0vkuG*KaIVQ{sW#5ghem7@8uU&06+PwuU~vW zI2?*%U#|<>{%TNm{J;~ZkYLmJr`HZD-j}^|wHPM6LFC)b$_G0LqzM~WN>#I5lte*V zJ%M+gIV=8t=QBUYozMIn8YY9MZn}-52pf0T?hQ!Nxz;WOT`i$O;8GZ&**G+e2ETjO z3v=)NFuT78v-^9v^{Jm=_h0nm*Z;Z?aie^C*}|{1DrzE~Tq(+|1xnWN&u;!M_lG~^ zE!?r~A#r|^(CcCD zy&vFcIJP~62VHOAos(yA)ayq8EYI2C%L*pa$qBzJp&a_DH_Dpa^LO3D83+xw?6V>@ z!*7^8_MgY`z>Yz5?Yc_5&im#^Irj=pRDu!&jGp$xpi$xdkM4)wWaYg-OdqaS>Zp>IYoH^JA1JbQPDKLW14`vw5O z9iHBteO_NCiJv_F1=wU#6n;KPh41QV%i;fGP-s0-4^*W!v7@b*Pe)k>xBcP;06@Q^ z16`gu==ew6(!Nl1R)qGfmF&D_dVCi64AtGcd$(a-UEPN-F2fl=IFZ&NWB)rU;A;?dlCQk<-r_E z5Ee{ImOtSQ=B?)lTa7|mo2{sN(GO6?mGIlg&%o7T!PWQOg4f$x;h*#)4PbgU3R`On zE=ynK*O(6plP0p0`JD4k;G-AV0bpg=2< z$n)fif|*pUQ8fj8ddaIg{#uNCf~H|07v73V&@?Vg&j0|PxOoSHkvP5@oq%Wlys%mF z$*~bwsN7eA{_)^@C0pa<@KnJr1*vBUil7J^SI^)ppsEiOg5tebt&<&J)n$dPb>U^| z`25S$8JfW#zx+rkQDL~+U=@y|B)o3Hn#(-fZw{a_KWmsFs}vRAeqfzHK6%oODGgJzx3DstXWmH~`;OO|vX4CTdx7 zzksUvt90Qb^jg+xNiV#C6HoHno6A}FKn)gRwOWOmNGI2$Dg0*$#?wsQYdMvo;s1|V zu}EiFIQ#A5AMUW2C>TOCg6C&s*DNOB~B4X&z+~Jii-Oa>J7W1cFg$)C6dj z6=`U>@&B?!AGv}PJc1%@+@RUb<$KMji5fsEY!m5h3craanrNblCYorXi6)w8qKPJ& lXrhTGnrNblCYo6C_FQ~x`w{>E002ovPDHLkV1iN)qWk~= literal 0 HcmV?d00001 diff --git a/frontend/assets/enemies/enemy.boar_l2_2_meadow.south.png b/frontend/assets/enemies/enemy.boar_l2_2_meadow.south.png new file mode 100644 index 0000000000000000000000000000000000000000..bfc4dc742ee358a39330d6bd6b7fc036e9b0aab2 GIT binary patch literal 3781 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 literal 0 HcmV?d00001 diff --git a/frontend/assets/enemies/enemy.boar_l4_4_canyon.south.png b/frontend/assets/enemies/enemy.boar_l4_4_canyon.south.png new file mode 100644 index 0000000000000000000000000000000000000000..047606196ef42691c6e0151b995e218e022c28de GIT binary patch literal 4097 zcmV+c5dQCpP)f-B8GhEBLsg5!36j7`oD?=-pg@s7&_gdh7(tH(0`xx=z4TNR zKJ=1bP$0<3Mcg6Z_1Z5sPtq7X;xzEnyjJU0qp!e(GX^rWmy+Bin15<}d~Dp29#B;^0~I=XnaQq#dWGX^vv4 zP@n1kwLNDmwx3dY08vP!Ra(?yFN3k5*Bcztu7lb%;3?i~hjevx&jbVaMCH6QQ8}OJ zT~RPiv(kZq!!tYBbGG;~N1{|75cb81LY&64DOgDUGMc72q+Rd1#ysUn+4cW6)dQG< zg%gE1T^MNFEdWSu{S2JvIBZO&X-4)}Di6>!4RvL#$HIA~Fe-_nACdP(>R}*Vz4vdA zreuXr6yk_K;{W3)vZt+7t093}6r@`RQBOFSCj?oGTNTU|1Gb_Pz(p$e_3-4xWmrDVIeRrT~rtG6y_{ zDC>_BzWCZh;y9>9vHA1>06?lFkThiZE`=$8+O*=$Rzq{{G?p$4F(cv;mjVMWWt}I} z0DvvJor~cBC7>WiH%O8C#bp@d`pMeBzR{I`e*^^`DZ75DV~cJB$j56LDn)P1Vu6+@ z1k|P#apoGv<6+&;BWL3gY6(Fus6~Xs|=)0f(hpGT-QC1^&fb0w9Y3XPGFnfk2i=~NIfDG}V z$QTgFcm_!|!%)IpZ8r6L3Qt4RLbU!c_irC}VA0^zcK^`Gg^qY-hV++eelT1L;!41I zAWE3^k=np`U-YE?L&LvTB}!^q9jF2{P3u`LD#N~Mc1k zB{1qp;}K$8))zz9i#ut&AU6uSI=ci-)10^+Y5gi8F8yR*{{GIx#D1EsR*渍! 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

N8K<#jLrpcRiC+;#k#0Ju*~diD?p1`nT@t^YlYr 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 o6B82?6B82?6B82?6EhPx# literal 0 HcmV?d00001 diff --git a/frontend/assets/enemies/enemy.wolf_l2_2_forest.south.png b/frontend/assets/enemies/enemy.wolf_l2_2_forest.south.png new file mode 100644 index 0000000000000000000000000000000000000000..5dab946f53b04fdd89d7d330f013e8cc2f2a475d GIT binary patch literal 4292 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 SDZ{7$0000RH_lAmg0iM@xX*zxasUlju+`{;l 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 literal 0 HcmV?d00001 diff --git a/frontend/assets/enemies/enemy.wolf_l4_4_canyon.south.png b/frontend/assets/enemies/enemy.wolf_l4_4_canyon.south.png new file mode 100644 index 0000000000000000000000000000000000000000..1e0b9c5e1ee16806facdc929f5271bdf9f0281d0 GIT binary patch literal 2387 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 A)&Kwi literal 0 HcmV?d00001 diff --git a/frontend/assets/enemies/enemy.wolf_l4_4_swamp.south.png b/frontend/assets/enemies/enemy.wolf_l4_4_swamp.south.png new file mode 100644 index 0000000000000000000000000000000000000000..474e950af737f5f3d074f9909a705cce97d806b7 GIT binary patch literal 1901 zcmV-z2a@=SP)ogE41eWyBikFyP3*hkX5_OF zqYNQly#Q&32R6pHOcRcq6at8fF*fYNMR?T$Smw}I86JqXe8zeJ0DJbV6rxNKUabIm zjW5Gk51h}!-*JQxMT+q11jq_GfV$-2E%U^L54dx!r`0{bn=&)L7crh?LztkTfv$Gxqoa(UV4*KdBtVAoZ8K~`x&g`C?D zkT2DA;J|_vd{<+f&>Dh7CGz`nh7JoFzhw@6<~6~*W@KOS`DXYQ-Ci5tCbrV}jP=m% zwRgT2S>QZ~4p}$a`+zuCUKiHSAA!g=uHSTF&z|w+_8wPWHd}FN`9tto zWK%0{Mw0^fwiY++*s#Ov;l}c4g}TZV;{=0#3ynn&f3NSi?#oyY|MBF=S2lb+ofL*1 z{WHbjV?XowMa%~j)(HbZr_(~G6OH$(^l{aiMZ7^|B5_aRd=}ZZ;$~ucF%1jW(MHUN zpj80oHG?d~G+~)0=JN&W^^MaBvbTAh`-xfp=kZeW2byK&G;(zDbgJitsZcGY2oqdbHs@Cx`l49jrH2W z{oNBr(?yuY2xG{d;1woQ;ARpS_O#T;MW!v|daS>`~#dXTsJz%|d(C&1C+<&pT&n!db88{{4|IZdH zgxJ*+J>q)#5>_CAe62KX1%(PxITecl#OGa$Y0=7RF#;6Q3Cq)r z+YFP(Xz|vc=%kOZn(ZVe<$+`oY+u8uyhex_A9L(`4iR?3->0%Xy@(q*&g4j7j`K?k zd~)z5<)YB}fT|KMB!+_qb0f)u_H!3@rUOY^J$(r3v4e}3XTPwfS*i!bYdl3h94A&~ zUwPfFlQcRhRR9ZC=4!Pfke8f{4IA%1w$X3wzVvZlN)JK14*46*g=Lx;9br|1-15hU zjq5jE+}}On$9h$mH@?bFSRT~vi)>)w1@i~s$9i@6!FQk(4=8%#I}IU%We)wa-W^e> z09h5wGGCZc=D7L@?Nr~7fCGAPUgRMkqLys5yd zP>2BWVwp%i%Y08yRVb_zR%IM{4S$NEM^M?8Co-nsIo-`ukzy6ysd!E4BViTMr{~@1 z@)hHBj1N?BBy5>O|I_;mj13!|%V68CPr$q;DklICtep7t{vv!(d(dx{zJyR0l?*{+ z!$xD?!f3h(A7A~Y-oTHw-Zz(&K_zcS_2Q;e8vV*t`i5j7PSN-TK@bE%5ClOG1VIo4 nK@bE%5ClOG1VIo4p^5(i3;gs-xPLS300000NkvXXu0mjfW>}9q literal 0 HcmV?d00001 diff --git a/frontend/assets/enemies/enemy.wolf_l5_5_astral.south.png b/frontend/assets/enemies/enemy.wolf_l5_5_astral.south.png new file mode 100644 index 0000000000000000000000000000000000000000..65a2a7e82ad93b2df0345f6ebbde7e2a5cbdd800 GIT binary patch literal 3985 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 M07*qoM6N<$g4%Td0ssI2 literal 0 HcmV?d00001 diff --git a/frontend/assets/prop/prop.bush.v1.png b/frontend/assets/prop/prop.bush.v1.png new file mode 100644 index 0000000000000000000000000000000000000000..a992afb25077f79a2e6816f6d16ecf3cc9186fc2 GIT binary patch literal 5687 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 RwSfa+kQP)QQm!NmY5vrb*Mbs-3oV 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 ot>fl;X1ONa407*qoM6N<$g3SF1X8-^I literal 0 HcmV?d00001 diff --git a/frontend/assets/prop/prop.camp_tent.v0.png b/frontend/assets/prop/prop.camp_tent.v0.png new file mode 100644 index 0000000000000000000000000000000000000000..d4f57c21fea957282910fc9b31fe8a6ce7d38855 GIT binary patch literal 2047 zcmVO9ClSCjq+n0yZW#OMC+sTdKW7e1ViJ>>#KVTN(w(BLbf<{MTuL-1nJD({*(HK{h(|RaiY!x+ znkAZBYNbf#{J#(?E8Qt%J-{5%&1$nvR6#6C3!fc!)&WhJvH zE188W!%20&%+}R2SQ=}g+BXEiGb?7}O3j+EN)ZzZ!zPW6p}F3VBFiYU%uU-&02XE? zn*E9_vu%YFMV84dm`Uz*JMPW^nyyonGiSnaL`v6NaYiXu8fvwK|ebqA_yX9mjL*v{3eUyr*P(!7m)bfnPvcF zMP^Nwk#4>~h=$AEEL}EZ%=ylH6s~(a$c{JGrHef(`H5DXNsjS`28>goSKrwodi#SM zJlILztSJ=aCs2@|Ku}1?ij1rnJA!c8E{=!!^W#mQe9>-{Xie8SRMEkqiVouY+D$Td z;!Il~dmc~0m6|2C?cegiN)n> z%fH=oYjjVA$`5ZBzj$Y>P*n|!)%cU#M|+r`GMCB$nWI&{Q6AAX-TVOLC;Pe9)`6z$ zD6-7T#ex*gZ;r3-6S(~viUQg3Rw}zx&fYQ#g3W5>TwMpcrc?8Nvw6?vy&J^(=bju> zu`vn#`s6mTA+?At3s=$7q~U2Cq{|;*Xjo(Q(}}dUbaLTB1LrHerZU+ZU1V=`nb(Re z^Tz%{4pnrR^Iq&3*;N-h6$+gSng(va#;}lBV3qjzldGI^w~@2SMgG=DN5!S7I{o#7 zjFpCC%J-99Pl)P1!J1?{n_qYmfFJ(mebN@&a60XTREbw#xVBEf&>+LY2;*GM4RlRMR%CQdzl+SECq#Rj zu}_?t7te=p9ltC7F$twD&JePg!t&gC6fZLVjP>7L55Oz$9w8XiS(*_``wgA8_5peV zGV4lWP*u$c?k%552;h4^ucEkU0Z%+EW;_D?N3-ZUG3;^K2 zXnePO{bpZqNd2#85MvgF#)kUvU~#eD&f*2}Tzm07=I=`22U`rqe)^ZA02JiKp`)5) z0B&^|m2GVXoH*|@+YkSSfBZwEx;31>fx~W;^!pJRH=!_cvsMUz%q$o40=HSdI+>ME z8N|vzIElll67*Y1c3DtW4WY@@HVaOaUp9-y3qM;5fR0fhgeKF~E~sl7BzR>A$!ev= z;~f`~2@?tpBMMkq=)CsQ6`uQ1CIfvk@zV`o zYFfxtgqtB+TSnfMCh|HS@tMM%#4?7vSeTv2svX6E;jv&)Cq2X1@5vbH)8muq@&~xu zY$&99G@4qkBa8cct1Z~v9xV2Ip22SSti^V>JE z25m5;)9mdweG-jhtf-%y{%0!6ady7-{1aGZiC5k|LjB1g01b`as1gYy>gRHI1Sc+F zR7FSK-*MQjoc_aYe7*fR>{c9hYgCDZ@ui<_yHc}+D>;F*^f|aIjZJJM9(iaG_ShsQ dF^Pv8{{qR$Q(rBJ(p>-m002ovPDHLkV1i+v>+1jj literal 0 HcmV?d00001 diff --git a/frontend/assets/prop/prop.leaves.v0.png b/frontend/assets/prop/prop.leaves.v0.png new file mode 100644 index 0000000000000000000000000000000000000000..64c11895be49afb507553e7e7158dad1e86c8fd0 GIT binary patch literal 1614 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 literal 0 HcmV?d00001 diff --git a/frontend/assets/prop/prop.mushroom.v0.png b/frontend/assets/prop/prop.mushroom.v0.png new file mode 100644 index 0000000000000000000000000000000000000000..037c96c5e46475f1a74e63eaf865750360012aab GIT binary patch literal 3208 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 literal 0 HcmV?d00001 diff --git a/frontend/assets/prop/prop.mushroom.v1.png b/frontend/assets/prop/prop.mushroom.v1.png new file mode 100644 index 0000000000000000000000000000000000000000..bc6919cdad5682e0e8761981ef0f57dce0ee4dcf GIT binary patch literal 3789 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 D41Y}& literal 0 HcmV?d00001 diff --git a/frontend/assets/prop/prop.rest_camp.wild.png b/frontend/assets/prop/prop.rest_camp.wild.png new file mode 100644 index 0000000000000000000000000000000000000000..5c1c1b3ebddb483d372f73ba6ff33cfcd3926250 GIT binary patch literal 20111 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 CuGriF literal 0 HcmV?d00001 diff --git a/frontend/assets/prop/prop.ruin.v1.png b/frontend/assets/prop/prop.ruin.v1.png new file mode 100644 index 0000000000000000000000000000000000000000..12b4a1a4016315a41a0f1e66b2699dbe0f432337 GIT binary patch literal 7068 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 W1KsbPu_kcq29lRi`A{ik9P~e^iQqQ? literal 0 HcmV?d00001 diff --git a/frontend/assets/prop/prop.stump.v0.png b/frontend/assets/prop/prop.stump.v0.png new file mode 100644 index 0000000000000000000000000000000000000000..a53f560de0d9f6d79fa1e56a958a941f73e45a11 GIT binary patch literal 3287 zcmV;|3@G!7P)JqQfUy`GQ<6^XxG^1|wx=|$?cz*ZyCG?( z9;fjPb*4_IvD0{(*i$AoQ`3$+9mk0WY+fQ>%!@$+3B7SfFTn;ObdpbZtJ~e%?H{Y% zyVISpfECl3`a5%@eV%>x+2{9rzQ32}c~dv;syY!rt;oN1<5b*NMk;b zs;rq($c$-u`<89y)(2(-pvF`H_B4l5+WhTv>(ZXKez(Gsw&N?Khou!5hcus z0-oCRqzNF3UYeG8DJd>;lmPI<*G~fA_4$cwF#t5(U{-z(&;L=4RgN3{_t8-R9=aFQ zxR=OSl%w4uufP3ou8rGu1#I84&CE2jNB14#)n9f1(62-QD9_6y7!Fe^=g_Z2s4OgU z+P}1YrE~q>;d9h1yu&FUI2(734T2k;*vKTj;1#)at}9J=uoy5E{{CN2uwc~+va=-~ z`~8(1I2-4{**MvLiLWh}d9ZG6nnS%AxatITJ)Tx)#C1v*SJAKhD&sSig++7^52e)g zj*d`yMY_8!180kD|x(`OqA z_+2vOCr9Q|}Fmj*4$kDDI8^TMwQ zhQm}B-<4)IHw)KIzz%o2_6_m`31Ms|lq{}dprsw%&=Z3YMCR66jLDX>DOp^F5!abl zdk+h|A`Rbsnex26l=nzpcZTk|2>_hy>BT4JI`#IfYH@@7T&sRTeJudd{)=Rl%moq- z6D%LH`K9+A!M#2|OY|8Z3rfJHm9)kd3N&b5qJ^I)cu%t{aZP&28ric3q?|*q5;%L)X&klB0$jm=Oi+J}|`M ziuu!Sec~!=dT*aHyq~H8qRqS0Xf2S7dDCR;<4sTvve4E8r8qxZAaL zRunLFjcbD;n%mB!XQ2FK*sDjka9bH*Z+;}hRs_JY&Ymx1PfuvrH3tYTD&VP2Pnw&X ziZU`_MU6#qI}!kdh7zG{_MBo2U2{w!+4*^W{uH}++bm|p2~Gt&1cru&2ox2iC}C|~ zE#ic5QwcdbI^z16TvI^u#%3~3wDqLb1&XHjdF{R#4r-RpJ!erR#uTK{}8+OTPd%89U*Ln zOoe_WLaChNni;s}QBJh=aBd(EG0Ax)hdo>9H8qP|d7oQma7zqGp8XXy6+FJaIOV>b z_#`XhdPvR$C9jyW3F7t|z{V|`Kv&-YU3~-BZDT7U)9Nl6Vp9B9<2qweC8OYOn!!z* zz_v#pH?4Jk!v6pBx5LhL+1({+&)5mhr(dA3WZ1hvS+bn6h3*pt=F0`B<@F}q{QkgA zG2l-(KW08q3xn4Z9@<_59B63+;L_zmd|tr`gSKzz3-&P(Rnj}n0Keo#~4MpiBg5-NyvWz;#`acshYymq5 zKcl+%F2M2+$xd+c_SxLJwdTH-L(^i!v>MsR+~2yd5LHu&##GeUxO4x_clw-dEJN3L zI`@JWdJ^lDUK;jjtGZZTGQ46$&N4>4pVbyF`th{(wJc@p#9o;~J)(zDa#FQ90dATX?>@e_N=%6%XC;`HtO)1g=O=xpB=}H`M6CYVcUK^cc^MiErP!w7 zQJojqWzRB`uX$JUNMk;iE)NpZHFoalafF4`&CgOfhqg>D+&zb!yj(<4;KSC_c$S$&6@_eB#?0~{m_7`Tmhv|;GK$aZ=XdXPcwiz7 z{%CK3dAKt*KR?`Q@Mk+)`Sz=)S^fPBsI`jo)0f1F)Qn)PQBi9ZJj+Zp)gU*=L)}B` zs4OdG&u=cL9cLw@Ux^^eR!GYa%mQH1;%Z>(tk2Lj$HCdKnX{_u9?Vl7_XJ}n+%VZ#o>?20eEKf@A$=A zxvU$1fG0Q1an1%`cjIm{CeT4u2Oz>t$m7#VOP2eMH@QF7V}fJEZtI6)_5ygZReOeo^m zg(&OmWcIdLTEBmZ2cPB^~>dycKgrAShvW}kypOXrdhMtSYN;=ot?b-?g#@{t*^l}-AI{vwN^o#HL-Pe zIMLUxeS>UXm!SRnWlMPfqmR+UI&ojZC&*d36N)eq5a~SuSG{NQ@>N zWK>ZQvb-4MapXWY`sZ<^R@pIw`uS1H=HAKw92-N^4T4=4=^YQj>eR8slo)H?K%KPLGH24P4bG<$^dUl0XFVivEE!#F_(w#g|Vm=HV?H6DT^U< zd5m#WW!?G4y}z9GJ-b|jt+MktgcQh zL}O8jf84f!H=bXGk37n%tJ%7J7yU}a@f+WNs+XOw&$rrNjv@Et-ICxoZt3CLnnp9G z>W;nD)-;-7WthYO=(QWQ^-&_$7=MMVc?$%@a$ajeFX$DP(qlh%=wNt;RC)YCtl zWZFb=>WPyknaG(ab?iD(Y>#CtzGx{rC?29D3Y2;3AVrZhZ;$}7yI5@hSYkm)lqj2! z)fx9UgCVf*?c3k)`+oOZngqebrJ(g&5Wr1QDHo)09XWz_MLklVqnz6QTHI@ z-U*)GDgjVlQv^Wkk#+!@XbGYxOYuCmq&ELEoLz) zd`DFS{BjV0*7hDAf24%1N2>;o2T0v5rdJ##3F$2WDOXex>f+yAn!`Apt;{^cTtUnL+@J4+H-hhV;$0B2n91MrZh0uZkP~-rw97yKeX$3_N zaMV4>dRIoooR)4M-+!)=eaGDF+`NKko^R*%<6Yc$djVTk=Tdv}BDE(kUbXh?$GaGt z9=+=NuuqRJgZ|#AJ{oiZR9)7Bvv-|aHqRD*C`Rx~`0kFha+53J20R6)< z04?VRb%ti@0!xgZGvuY@a(?7$fF%42VcnnwM*%q?n_hgn8Gv!`MC4ddqSx1D{4U6{ z&$&iOc+iU@fG(e#+`mdec)(BL-)1nP_8Yq&O^QkwlmoI!4#*~p7*;5sKghQ7rN(cY z*>c0{b@|-<>&wgd`^QukX9{>HC6<3_8RPUtgWU*Vlcdsc#RstxR$|KdAxOiFO z!D<(Zre62HVc!@zwnE~y6l!<7>AEnR8az-twBSZ*Q~}T%mcfW?mKZDJ-U&uL-U#QYXsQtv zQteKLePf2E1CMzY51cwljt%b5c zv#4PfHQsHni&$gWH)fbYh(@NS=6qhX>wyIU5E8z??m{$~X=v4p8oMpL?E9znV#qd= z^8WPT0Y3lzVr)BydAGgJu=5+G&|D!97s12FrH>3iYE`n9T9vF>1dCRYRz*cx6@nnp z)G>mhsfL%|R=$+eo#OyJSnV?I-}3d9Y~47*vXTHxc4X1!Ic1E6ik(0pV0^CJ@zC$M znMNw;AUIv|T547DM+W1D0SF2H#J98YoS5R_@@@RX1Eo|HrLcMVVk(MKINdpJ*tKXf zv$8PBxL;LF{Pf@fc0M#h?eRoj-9L9-aAmrCWx|wdcN$tSyERsIZy~XkSUxHUH{=AJ zu6S)xzTWBi?v4-m$8C4xS2Rv^46wFLKf~L9tcR|lOJpP$A)3r6nrdWz7cWajFJRA0 zN%VH>yV!eR{_=LxG;-bXB(fN%kJ{aC${#DmJ1GHZ0K@g8^E$|RH6XPrS<73S3&6Qo zFY?~^c2Sm>LD`Zt_8o5H2QQt(FRMsOkoCny^bYAwkt;pCqNzMqy`5vvxltsU-UEGP zn)4`0SY^cb0=vt|_-0dq-F4Nq*_u#bcTr$>(Y)s@qRmWB*yO^7NrT!R)aq$s6H73+yi3dpoX@ z;d(O8YXa4keq=Sk3&5MT9oR$@zdzh*9D)GYZ5AT~9QRHb@%rGI%Op7x=^pGOJu%E^ zj`n_dJ+S?V+%sA6yv4YKdWe=@Kqpmbf%J4iMq3$LRwfV1faU&2VCJ zAkhG3c6TV`D5oUJ7@staFyOHn!Vmk#h}Tk>HjNNV%GE;5fRN-!q;H~^<>@PF+FMWg zW2LCRAgNW!+O_<_HBBHW1&Of)*s{ro*%Dyq?lRtRAEoue2#sz19PDahz%xXf=M-H& zH{5^^vMS_p(kYt~|&=^l9}B|^^oT6A3#sIK&LsLo=1U-hTOeCxA%2YAaryJ$T+ zh*LORqCrg%p_ilT7t75y!dB(m`MeE>*`LhZ?m^bgCloEtQlpUd`p{GBkfcTSRx zTw#1BI_B64k!2m&uimqYruI=LXPzV4=I=sX^K3A&(4l?q%cZQneVnFdJ?m0t>|B)v zjvgFfab6m-5@2~rEEg}Ud~mu4fQ{QSXmbZq6a~ev$NNyoD@j-dfKS#BH)Tav-PzRU zIYp*9k1@3$2|r68$T7z5+MdYoUeM18#tw%|>l|Al>G8=-NfPHhZRG9LPmu((NKfnK zYlg(@9*(9~C2OC3D4R{|WMoxWwYr9IjxP#f3ulTwQ6!mx z+9663R*}ER$%C7gaMXPn(PZY}tj^B3cftsTAyWcy#D}ZH^BtFr?~m`&lcByrm0$1D zBfxj|l+xB`#_jfTspS%5?#XKkdEGD=Yl%gb1;Y&L8tklF>oY!}YejXXpAuJiDQ9Yu z@!cZCke%;DQX~@A#Ob@6VjSexf*Eq@YkEFE^~# z%U0%atijBy`&DE~WasWOT#puU;25;FPhk=xdlPffyvO5CSG;!rqa~;@y5|lF-qak& zD%UiEMWwF6PE);?%q-m>oI5*+q(}x3zKLlhMYq$j!yXYj3b(3uGl32MycX+2;6fB}ZalS}>;~0uxW%T^;b$1_b zAP*RGPnzD?*R82u1wA9QBgp|ae?~7?Lng6nmx&`sOdM;zOlkQd?A91MJH051Lh16w zoNPW%R$eA!hdtPBF{~-dAUQ#Q{HYFI&vqRos&6lmUamd7{WWt^bcxJ_G}=6;4BIz} zK~^l+J4-ER{3JUaJaDg%J-^jhU6#O^wrN(b5PAJY-6<-gx|U6JdT&^$kM)K~D7{Qf zPh{=7WF*N%TBaVCDpsiMdp(xaRF!pWeH=NWn?(RWx24;tYg8#-Y~#hJo4KdB21(Kw z_fFF_bcu2L$lQ`fX?X@0x;>PZCUK$HPid){u1*u)Ns01Rahz=RAk4fY)OUUW)u-F~ zjTApo0U$()g$@m1-TkHb)o^*6w?sn;1W{#yjWyvG`SHW)43389)$VpjtXI4vAJJwe zJ>A0Pey<@Uz{(XOr&|=m9C}ZLBgc)X`n4!3?+9I4aG32eY}uB}#+qSXdo2YNGpUPo zC%S2!{`XLw#c-%cjwIlA`!IJ+z`qcl+%%>iJ{{_`;qU8~Tb1wh@002ovPDHLkV1fi@r|bX# literal 0 HcmV?d00001 diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 0dc4167..16a2a94 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -18,6 +18,7 @@ "@types/react-dom": "^19.0.0", "@vitejs/plugin-react": "^4.3.4", "eslint": "^9.17.0", + "pngjs": "^7.0.0", "typescript": "~5.7.2", "vite": "^6.2.0" } @@ -2561,6 +2562,16 @@ "url": "https://opencollective.com/pixijs" } }, + "node_modules/pngjs": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz", + "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.19.0" + } + }, "node_modules/postcss": { "version": "8.5.8", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", diff --git a/frontend/package.json b/frontend/package.json index fd9b3c6..ca901e3 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -7,7 +7,8 @@ "dev": "vite", "build": "tsc -b && vite build", "preview": "vite preview", - "lint": "eslint src --ext .ts,.tsx --max-warnings 0" + "lint": "eslint src --ext .ts,.tsx --max-warnings 0", + "gen:enemy-south:pixellab": "node scripts/pixellab-enemy-south-batch.mjs" }, "dependencies": { "pixi.js": "^8.6.6", @@ -20,6 +21,7 @@ "@types/react-dom": "^19.0.0", "@vitejs/plugin-react": "^4.3.4", "eslint": "^9.17.0", + "pngjs": "^7.0.0", "typescript": "~5.7.2", "vite": "^6.2.0" } diff --git a/frontend/public/assets/game/manifest.json b/frontend/public/assets/game/manifest.json index beb9f56..dcf6125 100644 --- a/frontend/public/assets/game/manifest.json +++ b/frontend/public/assets/game/manifest.json @@ -1,7 +1,7 @@ { - "version": 7, + "version": 11, "assetsRoot": "frontend/assets", - "note": "file paths relative to frontend/assets. hero.player.v0 Sellsword, v1 Duelist, v2 Spellblade — 8 dirs each (PixelLab create_character). Legacy hero.player.png kept. NPC npc.* south-only.", + "note": "file paths relative to frontend/assets. Rest camp: prop.camp_tent/fire/bag.v0 (wild rest). Other props + heroes + NPC.", "textures": { "terrain.grass.v0": { "file": "tiles/terrain.grass.v0.png", @@ -208,6 +208,81 @@ "kind": "map_object", "pixellabObjectId": "3aa0dcd6-0868-432f-b36e-5574e226b58c" }, + "prop.bush.v0": { + "file": "prop/prop.bush.v0.png", + "kind": "map_object", + "pixellabObjectId": "f6d16006-9600-484b-b3d2-218bb66b9e66" + }, + "prop.bush.v1": { + "file": "prop/prop.bush.v1.png", + "kind": "map_object", + "pixellabObjectId": "14defee7-aa08-4ab9-bf8b-b7b2c1221b1d" + }, + "prop.mushroom.v0": { + "file": "prop/prop.mushroom.v0.png", + "kind": "map_object", + "pixellabObjectId": "fa1ab040-56b9-48f2-b0cb-67adb67a0b33" + }, + "prop.mushroom.v1": { + "file": "prop/prop.mushroom.v1.png", + "kind": "map_object", + "pixellabObjectId": "7a2bbb95-d084-4529-b39d-a53a9ba197e2" + }, + "prop.leaves.v0": { + "file": "prop/prop.leaves.v0.png", + "kind": "map_object", + "pixellabObjectId": "50220bb9-0fdc-4220-b600-9ad4ddc12272" + }, + "prop.leaves.v1": { + "file": "prop/prop.leaves.v1.png", + "kind": "map_object", + "pixellabObjectId": "f1e44471-7bc0-43a2-bedc-195a4fd7caeb" + }, + "prop.stump.v0": { + "file": "prop/prop.stump.v0.png", + "kind": "map_object", + "pixellabObjectId": "7a52d31f-716a-4c77-bb28-78e08322be3f" + }, + "prop.stump.v1": { + "file": "prop/prop.stump.v1.png", + "kind": "map_object", + "pixellabObjectId": "bffe7173-b0ba-4801-bc76-e78a40f8378d" + }, + "prop.bones.v0": { + "file": "prop/prop.bones.v0.png", + "kind": "map_object", + "pixellabObjectId": "0ad79d67-863a-4260-99e7-84a243cfd751" + }, + "prop.bones.v1": { + "file": "prop/prop.bones.v1.png", + "kind": "map_object", + "pixellabObjectId": "8ceb4caf-4c1f-4268-b260-9dc45c1079f7" + }, + "prop.ruin.v0": { + "file": "prop/prop.ruin.v0.png", + "kind": "map_object", + "pixellabObjectId": "b2363bd6-2a6f-4010-9f83-3ac1a65c9220" + }, + "prop.ruin.v1": { + "file": "prop/prop.ruin.v1.png", + "kind": "map_object", + "pixellabObjectId": "e789e08a-cdde-4a51-8ecd-c5090614a6c5" + }, + "prop.camp_tent.v0": { + "file": "prop/prop.camp_tent.v0.png", + "kind": "map_object", + "pixellabObjectId": "2427b47e-da33-4463-9ea3-49ade72a9a60" + }, + "prop.camp_fire.v0": { + "file": "prop/prop.camp_fire.v0.png", + "kind": "map_object", + "pixellabObjectId": "0362257f-c2fe-4d67-8d58-2951f1c9622f" + }, + "prop.camp_bag.v0": { + "file": "prop/prop.camp_bag.v0.png", + "kind": "map_object", + "pixellabObjectId": "4c507361-376f-4462-bb5a-49237b82d760" + }, "building.house.v0": { "file": "building/building.house.v0.png", "kind": "map_object", @@ -283,6 +358,102 @@ "kind": "map_object", "pixellabObjectId": "72db0b9e-c3ec-4e17-baa6-a08e30065ab2" }, + "enemy.wolf_l1_1_meadow.south": { + "file": "enemies/enemy.wolf_l1_1_meadow.south.png", + "kind": "map_object", + "pixellabObjectId": "4bf33db2-712c-424a-b4ad-7e425e2e7e9b", + "rotation": "south" + }, + "enemy.wolf_l1_1_forest.south": { + "file": "enemies/enemy.wolf_l1_1_forest.south.png", + "kind": "map_object", + "pixellabObjectId": "d1f2dc86-520e-4360-9b07-0b61faf87e6d", + "rotation": "south" + }, + "enemy.wolf_l2_2_forest.south": { + "file": "enemies/enemy.wolf_l2_2_forest.south.png", + "kind": "map_object", + "pixellabObjectId": "7d140a0e-ff83-4e99-af73-5f20439b5f56", + "rotation": "south" + }, + "enemy.wolf_l2_2_ruins.south": { + "file": "enemies/enemy.wolf_l2_2_ruins.south.png", + "kind": "map_object", + "pixellabObjectId": "5ce86bef-f686-4a11-bd72-f49e18067a94", + "rotation": "south" + }, + "enemy.wolf_l3_3_ruins.south": { + "file": "enemies/enemy.wolf_l3_3_ruins.south.png", + "kind": "map_object", + "pixellabObjectId": "13159934-d604-4de7-a2ba-a648f7aed9f2", + "rotation": "south" + }, + "enemy.wolf_l3_3_canyon.south": { + "file": "enemies/enemy.wolf_l3_3_canyon.south.png", + "kind": "map_object", + "pixellabObjectId": "18a2b9e4-0321-4051-af42-a780179ead28", + "rotation": "south" + }, + "enemy.wolf_l4_4_canyon.south": { + "file": "enemies/enemy.wolf_l4_4_canyon.south.png", + "kind": "map_object", + "pixellabObjectId": "540c7767-775a-41e6-80fc-24399c4c8037", + "rotation": "south" + }, + "enemy.wolf_l4_4_swamp.south": { + "file": "enemies/enemy.wolf_l4_4_swamp.south.png", + "kind": "map_object", + "pixellabObjectId": "64022324-5ce7-435c-bd9b-13f1f37cac5f", + "rotation": "south" + }, + "enemy.wolf_l5_5_volcanic.south": { + "file": "enemies/enemy.wolf_l5_5_volcanic.south.png", + "kind": "map_object", + "pixellabObjectId": "2ffa9c9d-4459-4ec5-8ed0-8db90d1f91ff", + "rotation": "south" + }, + "enemy.wolf_l5_5_astral.south": { + "file": "enemies/enemy.wolf_l5_5_astral.south.png", + "kind": "map_object", + "pixellabObjectId": "e96d9819-fc57-4568-abc6-f9722cd9f163", + "rotation": "south" + }, + "enemy.boar_l2_2_meadow.south": { + "file": "enemies/enemy.boar_l2_2_meadow.south.png", + "kind": "map_object", + "pixellabObjectId": "99e1a473-34fe-4c58-bc1e-9192902e4709", + "rotation": "south" + }, + "enemy.boar_l2_2_forest.south": { + "file": "enemies/enemy.boar_l2_2_forest.south.png", + "kind": "map_object", + "pixellabObjectId": "771d2d9c-5d35-4a77-869d-70c86abc9d0b", + "rotation": "south" + }, + "enemy.boar_l3_3_forest.south": { + "file": "enemies/enemy.boar_l3_3_forest.south.png", + "kind": "map_object", + "pixellabObjectId": "11b8358c-d4ed-49df-8da4-603845cb23c8", + "rotation": "south" + }, + "enemy.boar_l3_3_ruins.south": { + "file": "enemies/enemy.boar_l3_3_ruins.south.png", + "kind": "map_object", + "pixellabObjectId": "3767a671-8c08-4ae3-b18a-fdcfe4d7c4ea", + "rotation": "south" + }, + "enemy.boar_l4_4_ruins.south": { + "file": "enemies/enemy.boar_l4_4_ruins.south.png", + "kind": "map_object", + "pixellabObjectId": "b401afd4-f349-4827-ab59-57fa1c2d1fdb", + "rotation": "south" + }, + "enemy.boar_l4_4_canyon.south": { + "file": "enemies/enemy.boar_l4_4_canyon.south.png", + "kind": "map_object", + "pixellabObjectId": "abfd83f3-1ede-4fe2-a70e-4bd5746dce78", + "rotation": "south" + }, "hero.player": { "file": "characters/hero.player.png", "kind": "map_object", diff --git a/frontend/scripts/pixellab-enemy-south-batch.mjs b/frontend/scripts/pixellab-enemy-south-batch.mjs new file mode 100644 index 0000000..a338d9d --- /dev/null +++ b/frontend/scripts/pixellab-enemy-south-batch.mjs @@ -0,0 +1,347 @@ +/** + * Batch-generate enemy..south map objects via PixelLab API v2, using archetype PNGs as init_image. + * + * Prerequisites: + * - PIXELLAB_API_TOKEN in env (https://pixellab.ai/account or MCP setup). + * - Reference sprites at frontend/assets/enemies/enemy..png (from manifest archetypes). + * + * Usage: + * set PIXELLAB_API_TOKEN=... # PowerShell: $env:PIXELLAB_API_TOKEN="..." + * npm run gen:enemy-south:pixellab -- --limit 3 # smoke test + * npm run gen:enemy-south:pixellab -- --slugs "wolf_l1_1_meadow,boar_l2_2_meadow" # explicit list + * npm run gen:enemy-south:pixellab # all ~220 (long run; resumable) + * + * Resume: state in scripts/.enemy-south-pixellab-state.json + * Output: assets/enemies/enemy..south.png + manifest entries (pixellabObjectId, rotation south). + */ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const ROOT = path.join(__dirname, '..'); +const ASSETS = path.join(ROOT, 'assets'); +const ENEMIES_DIR = path.join(ASSETS, 'enemies'); +const MANIFEST_PATH = path.join(ROOT, 'public/assets/game/manifest.json'); +const SLUGS_FILE = path.join(ROOT, 'src/game/enemyTemplateSlugs.ts'); +const STATE_PATH = path.join(__dirname, '.enemy-south-pixellab-state.json'); + +const API_BASE = process.env.PIXELLAB_API_BASE ?? 'https://api.pixellab.ai/v2'; +const TOKEN = process.env.PIXELLAB_API_TOKEN ?? ''; + +const PREFIX = `Dark fantasy, brutal mood, stylized painterly look inspired by Arcane series, +cinematic lighting, strong readable silhouette, muted desaturated colors with rare accent highlights, +single game asset, transparent background, no text, no watermark, no border. +Isometric three-quarter view toward camera, consistent with tile-based RPG ground plane.`; + +const SLUG_RE = /^(.+)_l(\d+)_(\d+)_(meadow|forest|ruins|canyon|swamp|volcanic|astral)$/; + +const ICE_ELEMENT_SLUGS = new Set([ + 'element_l12_14_forest', + 'element_l15_16_ruins', + 'element_l17_18_canyon', + 'element_l19_20_swamp', + 'element_l21_22_astral', +]); + +/** Manifest texture key → filename stem under enemies/ (enemy.wolf.png) */ +const ARCH_TO_REF = { + wolf: 'enemy.wolf', + boar: 'enemy.boar', + zombie: 'enemy.zombie', + spider: 'enemy.spider', + orc: 'enemy.orc', + skeleton: 'enemy.skeleton_archer', + battle_lizard: 'enemy.battle_lizard', + demon: 'enemy.fire_demon', + skeleton_king: 'enemy.skeleton_king', + forest_warden: 'enemy.forest_warden', + titan: 'enemy.lightning_titan', + element_ice: 'enemy.ice_guardian', + element_water: 'enemy.water_element', + golem: 'enemy.fire_demon', + wraith: 'enemy.water_element', + bandit: 'enemy.orc', + cultist: 'enemy.skeleton_archer', + treant: 'enemy.forest_warden', + basilisk: 'enemy.battle_lizard', + wyvern: 'enemy.battle_lizard', + harpy: 'enemy.spider', + manticore: 'enemy.boar', + shade: 'enemy.water_element', +}; + +const BIOME_HINT = { + meadow: 'open meadow, soft daylight, grassy hints', + forest: 'dense forest floor, cool greens and bark', + ruins: 'crumbling stone ruins, dust and cold gray', + canyon: 'dry canyon, warm rock and dust', + swamp: 'murky swamp, violet-brown rot and mist', + volcanic: 'volcanic ash, ember accents, scorched rock', + astral: 'ethereal astral void, faint cyan-violet glow', +}; + +function parseSlugs() { + const text = fs.readFileSync(SLUGS_FILE, 'utf8'); + const lb = text.indexOf('[', text.indexOf('ENEMY_TEMPLATE_SLUGS')); + const rb = text.indexOf('] as const', lb); + const body = text.slice(lb + 1, rb); + const slugs = []; + for (const m of body.matchAll(/'([^']+)'/g)) { + slugs.push(m[1]); + } + return slugs; +} + +function refKeyForSlug(slug, arch) { + if (arch === 'element') { + return ICE_ELEMENT_SLUGS.has(slug) ? 'element_ice' : 'element_water'; + } + return arch in ARCH_TO_REF ? arch : 'wolf'; +} + +function buildDescription(slug, arch, low, high, biome) { + const refK = refKeyForSlug(slug, arch); + const archetypeHint = + arch === 'element' + ? ICE_ELEMENT_SLUGS.has(slug) + ? 'elemental ice creature' + : 'elemental water creature' + : `${arch.replace(/_/g, ' ')} monster`; + const biomeHint = BIOME_HINT[biome] ?? biome; + return `${PREFIX} +Game unit: ${archetypeHint}, template "${slug}", levels ~${low}–${high}. +Biome flavor: ${biomeHint}. +Match the silhouette and style family of the reference init image, but make this a clearly distinct individual (markings, gear, mutations, wear). +Facing south (toward the camera), full body, feet near bottom center, pixel art, RPG enemy sprite.`; +} + +function loadState() { + try { + return JSON.parse(fs.readFileSync(STATE_PATH, 'utf8')); + } catch { + return { jobs: {} }; + } +} + +function saveState(state) { + fs.writeFileSync(STATE_PATH, JSON.stringify(state, null, 2)); +} + +function extractObjectId(json) { + if (!json || typeof json !== 'object') return null; + if (json.object_id) return json.object_id; + if (json.data?.object_id) return json.data.object_id; + if (json.id) return json.id; + if (json.data?.id) return json.data.id; + return null; +} + +async function postMapObject(body) { + const res = await fetch(`${API_BASE}/map-objects`, { + method: 'POST', + headers: { + Authorization: `Bearer ${TOKEN}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + }); + const text = await res.text(); + let json; + try { + json = JSON.parse(text); + } catch { + json = { raw: text }; + } + if (!res.ok) { + throw new Error(`POST map-objects ${res.status}: ${text.slice(0, 500)}`); + } + return json; +} + +async function waitForDownload(objectId, timeoutMs = 600_000, pollMs = 4000) { + const url = `https://api.pixellab.ai/mcp/map-objects/${objectId}/download`; + const t0 = Date.now(); + while (Date.now() - t0 < timeoutMs) { + const res = await fetch(url); + const ct = res.headers.get('content-type') ?? ''; + if (res.ok && (ct.includes('png') || ct.includes('octet-stream'))) { + return Buffer.from(await res.arrayBuffer()); + } + if (res.status === 404 || res.status === 425) { + await sleep(pollMs); + continue; + } + if (!res.ok) { + const errText = await res.text(); + if (res.status >= 500) { + await sleep(pollMs); + continue; + } + throw new Error(`download ${res.status}: ${errText.slice(0, 200)}`); + } + await sleep(pollMs); + } + throw new Error(`timeout waiting for object ${objectId}`); +} + +function sleep(ms) { + return new Promise((r) => setTimeout(r, ms)); +} + +function readRefInitImage(refStem) { + const p = path.join(ENEMIES_DIR, `${refStem}.png`); + if (!fs.existsSync(p)) return null; + const buf = fs.readFileSync(p); + return { + type: 'base64', + base64: buf.toString('base64'), + format: 'png', + }; +} + +function parseArgs() { + const a = process.argv.slice(2); + let limit = Infinity; + let dry = false; + /** If set, only these slugs (comma-separated on CLI). */ + let slugList = null; + for (let i = 0; i < a.length; i++) { + if (a[i] === '--limit' && a[i + 1]) { + limit = parseInt(a[i + 1], 10); + i++; + } + if (a[i] === '--slugs' && a[i + 1]) { + slugList = a[i + 1] + .split(',') + .map((s) => s.trim()) + .filter(Boolean); + i++; + } + if (a[i] === '--dry-run') dry = true; + } + return { limit, dry, slugList }; +} + +function setManifestEntry(slug, objectId) { + const manifest = JSON.parse(fs.readFileSync(MANIFEST_PATH, 'utf8')); + const key = `enemy.${slug}.south`; + const file = `enemies/enemy.${slug}.south.png`; + manifest.textures[key] = { + file, + kind: 'map_object', + pixellabObjectId: objectId, + rotation: 'south', + }; + fs.writeFileSync(MANIFEST_PATH, `${JSON.stringify(manifest, null, 2)}\n`); +} + +function bumpManifestVersion() { + const manifest = JSON.parse(fs.readFileSync(MANIFEST_PATH, 'utf8')); + manifest.version = (manifest.version | 0) + 1; + fs.writeFileSync(MANIFEST_PATH, `${JSON.stringify(manifest, null, 2)}\n`); +} + +async function main() { + const { limit, dry, slugList } = parseArgs(); + if (!TOKEN && !dry) { + console.error('Set PIXELLAB_API_TOKEN (Bearer token from PixelLab account / MCP).'); + process.exit(1); + } + + let slugs = slugList?.length ? slugList : parseSlugs(); + if (!slugList?.length && Number.isFinite(limit)) slugs = slugs.slice(0, limit); + + fs.mkdirSync(ENEMIES_DIR, { recursive: true }); + const state = loadState(); + let updatedManifest = false; + + for (const slug of slugs) { + const m = slug.match(SLUG_RE); + if (!m) { + console.warn('skip bad slug', slug); + continue; + } + const arch = m[1]; + const low = m[2]; + const high = m[3]; + const biome = m[4]; + const outPath = path.join(ENEMIES_DIR, `enemy.${slug}.south.png`); + + if (fs.existsSync(outPath) && state.jobs[slug]?.done) { + console.log('skip existing', slug); + continue; + } + + const description = buildDescription(slug, arch, low, high, biome); + const refStem = ARCH_TO_REF[refKeyForSlug(slug, arch)]; + const initImage = readRefInitImage(refStem); + + if (dry) { + console.log('---', slug, refStem, initImage ? 'has ref' : 'no ref'); + console.log(description.slice(0, 200) + '…'); + continue; + } + + let objectId = state.jobs[slug]?.objectId; + if (!objectId) { + const body = { + description, + image_size: { width: 96, height: 112 }, + view: 'low top-down', + outline: 'single color outline', + shading: 'medium shading', + detail: 'medium detail', + text_guidance_scale: 8, + }; + if (initImage) { + body.init_image = initImage; + body.init_image_strength = 420; + } + + console.log('create', slug, initImage ? `+ref ${refStem}` : 'text only'); + let created; + try { + created = await postMapObject(body); + } catch (e) { + if (initImage && String(e.message).includes('422')) { + console.warn('retry without init_image', slug); + delete body.init_image; + delete body.init_image_strength; + created = await postMapObject(body); + } else { + throw e; + } + } + objectId = extractObjectId(created); + if (!objectId) { + console.error('No object_id in response:', JSON.stringify(created).slice(0, 400)); + process.exit(1); + } + state.jobs[slug] = { objectId, startedAt: Date.now() }; + saveState(state); + await sleep(6000); + } + + if (!fs.existsSync(outPath)) { + console.log('poll', slug, objectId); + const png = await waitForDownload(objectId); + fs.writeFileSync(outPath, png); + } + + state.jobs[slug] = { objectId, done: true, finishedAt: Date.now() }; + saveState(state); + setManifestEntry(slug, objectId); + updatedManifest = true; + console.log('ok', slug); + await sleep(1500); + } + + if (!dry && updatedManifest) bumpManifestVersion(); + console.log('Done.'); +} + +main().catch((e) => { + console.error(e); + process.exit(1); +}); diff --git a/frontend/src/game/assets/gameSpriteRegistry.ts b/frontend/src/game/assets/gameSpriteRegistry.ts new file mode 100644 index 0000000..1fb2546 --- /dev/null +++ b/frontend/src/game/assets/gameSpriteRegistry.ts @@ -0,0 +1,74 @@ +import { Assets, Texture } from 'pixi.js'; +import { + fetchGameTextureManifest, + tryResolveGameAssetFile, + type GameTextureManifest, +} from './resolveGameAssetUrl'; +import { getRequiredSpriteKeys } from './spriteMapping'; + +export class GameSpriteRegistry { + private _manifest: GameTextureManifest | null = null; + private _textures = new Map(); + private _ready = false; + + private _buildFallbackManifest(keys: string[]): GameTextureManifest { + const textures: Record = {}; + for (const key of keys) { + let file: string | null = null; + if (key.startsWith('terrain.')) file = `tiles/${key}.png`; + else if (key.startsWith('prop.')) file = `prop/${key}.png`; + else if (key.startsWith('building.')) file = `building/${key}.png`; + else if (key.startsWith('enemy.')) file = `enemies/${key}.png`; + else if (key.startsWith('npc.') || key.startsWith('hero.')) file = `characters/${key}.png`; + if (!file) continue; + textures[key] = { file, kind: 'fallback' }; + } + return { + version: 0, + note: 'Fallback manifest (generated at runtime).', + textures, + }; + } + + get ready(): boolean { + return this._ready; + } + + get manifest(): GameTextureManifest | null { + return this._manifest; + } + + async loadAll(): Promise { + const requiredKeys = getRequiredSpriteKeys(); + try { + this._manifest = await fetchGameTextureManifest(); + } catch (error) { + console.warn('[Assets] Manifest load failed, using fallback manifest.', error); + this._manifest = this._buildFallbackManifest(requiredKeys); + } + for (const key of requiredKeys) { + if (!this._manifest.textures[key]) { + console.warn(`[Assets] Missing manifest entry for sprite key: ${key}`); + } + } + const entries = Object.entries(this._manifest.textures); + await Promise.all( + entries.map(async ([key, entry]) => { + const url = tryResolveGameAssetFile(entry.file); + if (!url) { + console.warn(`[Assets] Missing sprite file in bundle: ${entry.file}`); + return; + } + const texture = await Assets.load(url); + // Pixel-art tiles/props: nearest filtering avoids subpixel seam artifacts on tile borders. + texture.source.scaleMode = 'nearest'; + this._textures.set(key, texture); + }), + ); + this._ready = true; + } + + getTexture(key: string): Texture | null { + return this._textures.get(key) ?? null; + } +} diff --git a/frontend/src/game/assets/resolveGameAssetUrl.ts b/frontend/src/game/assets/resolveGameAssetUrl.ts index 177f4f3..58d420e 100644 --- a/frontend/src/game/assets/resolveGameAssetUrl.ts +++ b/frontend/src/game/assets/resolveGameAssetUrl.ts @@ -3,7 +3,7 @@ * Keep in sync with [public/assets/game/manifest.json](../../../../public/assets/game/manifest.json). */ -const raw = import.meta.glob('../../assets/**/*.png', { eager: true, as: 'url' }); +const raw = import.meta.glob('../../../assets/**/*.png', { eager: true, as: 'url' }); function toManifestRelativePath(globModulePath: string): string { const n = globModulePath.replace(/\\/g, '/'); @@ -52,8 +52,25 @@ export interface GameTextureManifest { textures: Record; } -export async function fetchGameTextureManifest(url = '/assets/game/manifest.json'): Promise { - const res = await fetch(url); +async function fetchManifestJson(resolvedUrl: string): Promise { + const res = await fetch(resolvedUrl); if (!res.ok) throw new Error(`Game manifest fetch failed: ${res.status}`); - return res.json() as Promise; + const manifest = (await res.json()) as GameTextureManifest; + if (!manifest || !manifest.textures) { + throw new Error('Game manifest missing textures'); + } + return manifest; +} + +export async function fetchGameTextureManifest(url?: string): Promise { + const baseUrl = import.meta.env.BASE_URL ?? '/'; + const resolvedUrl = url ?? `${baseUrl}assets/game/manifest.json`; + try { + return await fetchManifestJson(resolvedUrl); + } catch (error) { + if (resolvedUrl !== '/assets/game/manifest.json') { + return fetchManifestJson('/assets/game/manifest.json'); + } + throw error; + } } diff --git a/frontend/src/game/assets/spriteMapping.ts b/frontend/src/game/assets/spriteMapping.ts new file mode 100644 index 0000000..335b5cc --- /dev/null +++ b/frontend/src/game/assets/spriteMapping.ts @@ -0,0 +1,116 @@ +export const ROAD_SPRITE_KEY = 'terrain.road.v1'; +const DEFAULT_TERRAIN_KEY = 'terrain.grass.v1'; +const HERO_SPRITE_KEY = 'hero.player.v1.south'; +const MEET_PARTNER_SPRITE_KEY = 'hero.meet_partner'; + +/** Wilderness rest: separate props (transparent PNG), tent ~30px art height. */ +export const CAMP_TENT_TEXTURE_KEY = 'prop.camp_tent.v0'; +export const CAMP_FIRE_TEXTURE_KEY = 'prop.camp_fire.v0'; +export const CAMP_BAG_TEXTURE_KEY = 'prop.camp_bag.v0'; + +const TERRAIN_TEXTURE_BY_KEY: Record = { + plaza: 'terrain.plaza.v1', + road: ROAD_SPRITE_KEY, + dirt: 'terrain.dirt.v1', + stone: 'terrain.stone.v1', + forest_floor: 'terrain.forest_floor.v1', + ruins_floor: 'terrain.ruins_floor.v1', + canyon_floor: 'terrain.canyon_floor.v1', + swamp_floor: 'terrain.swamp_floor.v1', + volcanic_floor: 'terrain.volcanic_floor.v1', + astral_floor: 'terrain.astral_floor.v1', + grass: DEFAULT_TERRAIN_KEY, +}; + +const OBJECT_TEXTURE_BY_KEY: Record = { + tree: { v0: 'prop.tree.v0', v1: 'prop.tree.v1' }, + rock: { v0: 'prop.rock.v0', v1: 'prop.rock.v1' }, + cart: { v0: 'prop.cart.v0', v1: 'prop.cart.v1' }, + barrel: { v0: 'prop.barrel.v0', v1: 'prop.barrel.v1' }, + bush: { v0: 'prop.bush.v0', v1: 'prop.bush.v1' }, + mushroom: { v0: 'prop.mushroom.v0', v1: 'prop.mushroom.v1' }, + leaves: { v0: 'prop.leaves.v0', v1: 'prop.leaves.v1' }, + stump: { v0: 'prop.stump.v0', v1: 'prop.stump.v1' }, + bones: { v0: 'prop.bones.v0', v1: 'prop.bones.v1' }, + ruin: { v0: 'prop.ruin.v0', v1: 'prop.ruin.v1' }, +}; + +const NPC_TEXTURE_BY_TYPE: Record = { + merchant: 'npc.merchant', + armorer: 'npc.armorer', + weapon: 'npc.weapon', + jeweler: 'npc.jeweler', + healer: 'npc.healer', + bounty_hunter: 'npc.bounty_hunter', + elder: 'npc.elder', + quest_giver: 'npc.quest_giver', +}; + +const BUILDING_TEXTURE_BY_TYPE: Record = { + 'house.quest_giver': 'building.house.v1', + 'house.merchant': 'building.house.v1', + 'house.armorer': 'building.house.v1', + 'house.weapon_smith': 'building.house.v1', + 'house.jeweler': 'building.house.v1', + 'house.bounty_hunter': 'building.house.v1', + 'house.elder': 'building.house.v1', + 'house.healer': 'building.house.v1', +}; + +export function terrainToTextureKey(terrain: string): string { + return TERRAIN_TEXTURE_BY_KEY[terrain] ?? DEFAULT_TERRAIN_KEY; +} + +export function objectToTextureKey(obj: string, variant: number): string | null { + const entry = OBJECT_TEXTURE_BY_KEY[obj]; + if (!entry) return null; + return variant > 0.5 ? entry.v1 : entry.v0; +} + +export function npcTypeToTextureKey(npcType: string): string | null { + return NPC_TEXTURE_BY_TYPE[npcType] ?? null; +} + +export function buildingTypeToTextureKey(buildingType: string): string | null { + return BUILDING_TEXTURE_BY_TYPE[buildingType] ?? null; +} + +export function heroTextureKey(): string { + return HERO_SPRITE_KEY; +} + +export function meetPartnerTextureKey(): string { + return MEET_PARTNER_SPRITE_KEY; +} + +export function restCampTextureKeys(): [string, string, string] { + return [CAMP_TENT_TEXTURE_KEY, CAMP_FIRE_TEXTURE_KEY, CAMP_BAG_TEXTURE_KEY]; +} + +/** South-facing sprite per DB template (`enemies.type`); optional until listed in manifest + assets. */ +export function enemySouthTextureKey(slug: string): string { + return `enemy.${slug}.south`; +} + +export function getRequiredSpriteKeys(): string[] { + const terrainKeys = Object.values(TERRAIN_TEXTURE_BY_KEY); + const objectKeys = Object.values(OBJECT_TEXTURE_BY_KEY).flatMap((entry) => [ + entry.v0, + entry.v1, + ]); + const npcKeys = Object.values(NPC_TEXTURE_BY_TYPE); + const buildingKeys = Object.values(BUILDING_TEXTURE_BY_TYPE); + return [ + ...new Set([ + ...terrainKeys, + ...objectKeys, + ...npcKeys, + ...buildingKeys, + HERO_SPRITE_KEY, + MEET_PARTNER_SPRITE_KEY, + CAMP_TENT_TEXTURE_KEY, + CAMP_FIRE_TEXTURE_KEY, + CAMP_BAG_TEXTURE_KEY, + ]), + ]; +} diff --git a/frontend/src/game/camera.ts b/frontend/src/game/camera.ts index 199e765..e5d97ab 100644 --- a/frontend/src/game/camera.ts +++ b/frontend/src/game/camera.ts @@ -89,7 +89,7 @@ export class Camera { * The container is shifted so the camera target appears at screen center. */ applyTo(container: { x: number; y: number }, screenWidth: number, screenHeight: number): void { - container.x = screenWidth / 2 - this.finalX; - container.y = screenHeight / 2 - this.finalY; + container.x = Math.round(screenWidth / 2 - this.finalX); + container.y = Math.round(screenHeight / 2 - this.finalY); } } diff --git a/frontend/src/game/enemyVisuals.ts b/frontend/src/game/enemyVisuals.ts index e295c4e..945d7e2 100644 --- a/frontend/src/game/enemyVisuals.ts +++ b/frontend/src/game/enemyVisuals.ts @@ -664,6 +664,54 @@ export function resolveEnemyVisual(slug: string, archetype?: string): EnemyVisua return tweakVisualForSlug(base, slug); } +// --------------------------------------------------------------------------- +// HP bar only (body drawn as texture) +// --------------------------------------------------------------------------- + +export function drawEnemyHpBarOnly( + gfx: Graphics, + enemySlug: string, + enemyArchetype: string | undefined, + cx: number, + cy: number, + hp: number, + maxHp: number, +): void { + gfx.clear(); + const config = resolveEnemyVisual(enemySlug, enemyArchetype); + const { size } = config; + const bodyTop = getBodyTopY(cy, size, config.bodyShape); + const barWidth = 30; + const barHeight = 4; + const headHeight = + config.headShape === 'none' + ? config.isElite + ? 12 + : 6 + : config.headShape === 'crown' + ? 22 + : config.headShape === 'horns' + ? 18 + : config.headShape === 'helmet' + ? 16 + : 14; + const hpBarY = bodyTop - headHeight - 4; + + gfx.rect(cx - barWidth / 2, hpBarY, barWidth, barHeight); + gfx.fill({ color: 0x000000, alpha: 0.6 }); + const hpRatio = maxHp > 0 ? Math.max(0, hp / maxHp) : 0; + if (hpRatio > 0) { + gfx.rect(cx - barWidth / 2, hpBarY, barWidth * hpRatio, barHeight); + const hpColor = hpRatio > 0.5 ? 0xcc3333 : hpRatio > 0.25 ? 0xccaa22 : 0xff2222; + gfx.fill({ color: hpColor }); + } + if (config.isElite) { + gfx.rect(cx - barWidth / 2, hpBarY, barWidth, barHeight); + gfx.stroke({ color: config.glowColor ?? 0xffaa00, width: 1, alpha: 0.8 }); + } + gfx.zIndex = cy + 101; +} + // --------------------------------------------------------------------------- // Main draw function — replaces the generic red-diamond drawEnemy // --------------------------------------------------------------------------- diff --git a/frontend/src/game/engine.ts b/frontend/src/game/engine.ts index dc45cdc..16f6e73 100644 --- a/frontend/src/game/engine.ts +++ b/frontend/src/game/engine.ts @@ -972,13 +972,6 @@ export class GameEngine { ? 'fight' : 'idle'; - this.renderer.drawHero( - this._heroDisplayX, - this._heroDisplayY, - animPhase, - now, - ); - const rk = state.hero.restKind?.toLowerCase() ?? ''; const excPhase = state.hero.excursionPhase?.toLowerCase() ?? ''; // Camp only during the stationary wild phase; hide as soon as rest ends and return leg starts. @@ -992,6 +985,13 @@ export class GameEngine { this.renderer.clearRestCamp(); } + this.renderer.drawHero( + this._heroDisplayX, + this._heroDisplayY, + animPhase, + now, + ); + // Thought bubble during rest/town pauses if (this._thoughtText) { this.renderer.drawThoughtBubble( @@ -1083,6 +1083,8 @@ export class GameEngine { state.enemy.enemyArchetype, now, ); + } else { + this.renderer.clearEnemyCombat(); } // Sort entities for isometric depth diff --git a/frontend/src/game/renderer.ts b/frontend/src/game/renderer.ts index d7414d2..7de473c 100644 --- a/frontend/src/game/renderer.ts +++ b/frontend/src/game/renderer.ts @@ -1,9 +1,20 @@ -import { Application, Container, Graphics, Text, TextStyle } from 'pixi.js'; +import { Application, Container, Graphics, Sprite, Text, TextStyle, Texture } from 'pixi.js'; import { TILE_WIDTH, TILE_HEIGHT, MAP_ZOOM } from '../shared/constants'; import { getViewport } from '../shared/telegram'; import type { Camera } from './camera'; import type { TownData, NPCData, BuildingData } from './types'; -import { drawEnemyBySlug } from './enemyVisuals'; +import { drawEnemyBySlug, drawEnemyHpBarOnly } from './enemyVisuals'; +import { GameSpriteRegistry } from './assets/gameSpriteRegistry'; +import { + buildingTypeToTextureKey, + heroTextureKey, + meetPartnerTextureKey, + npcTypeToTextureKey, + objectToTextureKey, + restCampTextureKeys, + enemySouthTextureKey, + terrainToTextureKey, +} from './assets/spriteMapping'; /** * Isometric coordinate conversion utilities. @@ -23,6 +34,13 @@ export interface WorldPoint { y: number; } +type SpritePoolEntry = { + sprite: Sprite; + textureKey: string; +}; + +const TERRAIN_SEAM_BLEED_SCALE = 1.002; + /** Convert world (tile) coordinates to screen (pixel) coordinates */ export function worldToScreen(wx: number, wy: number): ScreenPoint { return { @@ -73,6 +91,30 @@ export class GameRenderer { /** Town ring + active route for procedural ground (null until towns loaded). */ private _worldTerrainContext: WorldTerrainContext | null = null; + // Sprite rendering + private _spriteRegistry = new GameSpriteRegistry(); + private _spritesReady = false; + private _groundSpriteLayer: Container; + private _objectSpriteLayer: Container; + private _buildingSpriteLayer: Container; + private _tileSpritePool = new Map(); + private _objectSpritePool = new Map(); + private _tileSpriteFreeList: Sprite[] = []; + private _objectSpriteFreeList: Sprite[] = []; + private _buildingSpritePool = new Map(); + private _characterSpritePool = new Map(); + private _npcSpritePool = new Map(); + private _groundDirty = true; + private _lastGroundBounds: + | { + startX: number; + endX: number; + startY: number; + endY: number; + } + | null = null; + private _lastSpritesReady = false; + // Reusable Graphics objects (avoid GC in hot path) private _groundGfx: Graphics | null = null; private _heroGfx: Graphics | null = null; @@ -93,6 +135,7 @@ export class GameRenderer { // Town rendering private _townGfx: Graphics | null = null; + private _townIconGfx: Graphics | null = null; private _townLabels: Text[] = []; private _townLabelPool: Text[] = []; @@ -106,6 +149,9 @@ export class GameRenderer { private _nearbyHeroLabels: Text[] = []; private _nearbyHeroLabelPool: Text[] = []; + private _lastEntitySortMs = 0; + private _entitySortIntervalMs = 120; + private _drawBush(gfx: Graphics, x: number, y: number, variant: number): void { const s = (0.9 + variant * 0.25) * 3.5; gfx.ellipse(x - 4 * s, y - 7 * s, 7 * s, 4.5 * s); @@ -162,9 +208,9 @@ export class GameRenderer { private _drawBones(gfx: Graphics, x: number, y: number, variant: number): void { const s = (0.8 + variant * 0.3) * 2.8; - gfx.roundRect(x - 8 * s, y - 2 * s, 16 * s, 3 * s, 1 * s); + gfx.roundRect(x - 8 * s, y - 2 * s, 16 * s, 3 * s, s); gfx.fill({ color: 0xddd5c8, alpha: 0.9 }); - gfx.circle(x - 5 * s, y - 1 * s, 2 * s); + gfx.circle(x - 5 * s, y - s, 2 * s); gfx.fill({ color: 0xc9c2b6, alpha: 0.9 }); gfx.circle(x + 6 * s, y, 1.5 * s); gfx.fill({ color: 0xb0a898, alpha: 0.85 }); @@ -226,9 +272,9 @@ export class GameRenderer { gfx.fill({ color: 0x6b4a30, alpha: 0.92 }); gfx.ellipse(x, y - 7 * s, 5 * s, 3 * s); gfx.fill({ color: 0x7a5a3a, alpha: 0.9 }); - gfx.rect(x - 5 * s, y - 5 * s, 10 * s, 1 * s); + gfx.rect(x - 5 * s, y - 5 * s, 10 * s, s); gfx.fill({ color: 0x4a3218, alpha: 0.6 }); - gfx.rect(x - 5 * s, y - 2 * s, 10 * s, 1 * s); + gfx.rect(x - 5 * s, y - 2 * s, 10 * s, s); gfx.fill({ color: 0x4a3218, alpha: 0.6 }); } @@ -269,9 +315,71 @@ export class GameRenderer { return 0x1a4a12; } + private _ensureSprite( + pool: Map, + poolKey: string, + textureKey: string, + texture: SpritePoolEntry['sprite']['texture'], + layer: Container, + freeList?: Sprite[], + ): SpritePoolEntry { + let entry = pool.get(poolKey); + if (!entry) { + const sprite = freeList && freeList.length > 0 + ? (freeList.pop() as Sprite) + : new Sprite(texture); + sprite.texture = texture; + sprite.anchor.set(0.5, 1); + sprite.roundPixels = true; + if (!sprite.parent) layer.addChild(sprite); + entry = { sprite, textureKey }; + pool.set(poolKey, entry); + return entry; + } + if (entry.textureKey !== textureKey) { + entry.sprite.texture = texture; + entry.textureKey = textureKey; + } + return entry; + } + + private _hideUnusedSprites(pool: Map, used: Set): void { + for (const [key, entry] of pool) { + entry.sprite.visible = used.has(key); + } + } + + private _evictSpritesOutsideTileBounds( + pool: Map, + minX: number, + maxX: number, + minY: number, + maxY: number, + freeList: Sprite[], + maxFreeListSize: number, + ): void { + for (const [key, entry] of pool) { + const sep = key.indexOf(','); + if (sep < 0) continue; + const wx = Number(key.slice(0, sep)); + const wy = Number(key.slice(sep + 1)); + if (!Number.isFinite(wx) || !Number.isFinite(wy)) continue; + if (wx >= minX && wx <= maxX && wy >= minY && wy <= maxY) continue; + pool.delete(key); + const sprite = entry.sprite; + sprite.visible = false; + if (freeList.length < maxFreeListSize) { + freeList.push(sprite); + } else { + sprite.destroy(); + } + } + } + /** Called from GameEngine when towns or route waypoints change. */ setWorldTerrainContext(ctx: WorldTerrainContext | null): void { this._worldTerrainContext = ctx; + this._groundDirty = true; } constructor() { @@ -281,6 +389,9 @@ export class GameRenderer { this.entityLayer = new Container(); this.effectLayer = new Container(); this.uiContainer = new Container(); + this._groundSpriteLayer = new Container(); + this._objectSpriteLayer = new Container(); + this._buildingSpriteLayer = new Container(); } get initialized(): boolean { @@ -305,6 +416,14 @@ export class GameRenderer { canvasContainer.appendChild(this.app.canvas as HTMLCanvasElement); + try { + await this._spriteRegistry.loadAll(); + this._spritesReady = this._spriteRegistry.ready; + } catch (error) { + this._spritesReady = false; + console.warn('[Renderer] Sprite preload failed, using fallback graphics.', error); + } + // Build scene graph this.worldContainer.addChild(this.groundLayer); this.worldContainer.addChild(this.entityLayer); @@ -318,9 +437,20 @@ export class GameRenderer { this.worldContainer.scale.set(MAP_ZOOM); // Create reusable graphics objects + this.groundLayer.sortableChildren = true; + this.groundLayer.addChild(this._groundSpriteLayer); + this._groundSpriteLayer.zIndex = 0; + this._groundGfx = new Graphics(); + this._groundGfx.zIndex = 1; this.groundLayer.addChild(this._groundGfx); + this.groundLayer.addChild(this._objectSpriteLayer); + this._objectSpriteLayer.zIndex = 2; + + this.groundLayer.addChild(this._buildingSpriteLayer); + this._buildingSpriteLayer.zIndex = 3; + this._heroGfx = new Graphics(); this.entityLayer.addChild(this._heroGfx); @@ -401,8 +531,13 @@ export class GameRenderer { // Town graphics (drawn between ground and entity layers) this._townGfx = new Graphics(); + this._townGfx.zIndex = 4; this.groundLayer.addChild(this._townGfx); + this._townIconGfx = new Graphics(); + this._townIconGfx.zIndex = 5; + this.groundLayer.addChild(this._townIconGfx); + // NPC graphics (drawn in entity layer for depth sorting) this._npcGfx = new Graphics(); this.entityLayer.addChild(this._npcGfx); @@ -432,7 +567,6 @@ export class GameRenderer { drawGround(camera: Camera, screenWidth: number, screenHeight: number): void { const gfx = this._groundGfx; if (!gfx) return; - gfx.clear(); const cx = camera.finalX; const cy = camera.finalY; @@ -469,37 +603,85 @@ export class GameRenderer { const startY = Math.floor(minWY); const endY = Math.ceil(maxWY); + const spritesReady = this._spritesReady; + const last = this._lastGroundBounds; + const sameBounds = + last && + last.startX === startX && + last.endX === endX && + last.startY === startY && + last.endY === endY; + if (!this._groundDirty && sameBounds && this._lastSpritesReady === spritesReady) { + return; + } + + this._groundDirty = false; + this._lastGroundBounds = { startX, endX, startY, endY }; + this._lastSpritesReady = spritesReady; + + gfx.clear(); + const hw = TILE_WIDTH / 2; const hh = TILE_HEIGHT / 2; const terrainCtx = this._worldTerrainContext; + const usedTileSprites = new Set(); + const usedObjectSprites = new Set(); // Pass 1: tiles for (let wx = startX; wx <= endX; wx++) { for (let wy = startY; wy <= endY; wy++) { - const terrain = proceduralTerrain(wx, wy, terrainCtx); const iso = worldToScreen(wx, wy); + if ( + Math.abs(iso.x - cx) > halfW + hw || + Math.abs(iso.y - cy) > halfH + hh + ) { + continue; + } + const terrain = proceduralTerrain(wx, wy, terrainCtx); const dark = (wx + wy) % 2 === 0; - const color = this._terrainColors(terrain, dark); - - gfx.poly([ - iso.x, iso.y - hh, - iso.x + hw, iso.y, - iso.x, iso.y + hh, - iso.x - hw, iso.y, - ]); - gfx.fill({ color, alpha: 1 }); - - gfx.poly([ - iso.x, iso.y - hh, - iso.x + hw, iso.y, - iso.x, iso.y + hh, - iso.x - hw, iso.y, - ]); - gfx.stroke({ - color: this._terrainStrokeColor(terrain), - width: 1, - alpha: 0.25, - }); + const textureKey = spritesReady ? terrainToTextureKey(terrain) : null; + const texture = textureKey ? this._spriteRegistry.getTexture(textureKey) : null; + + if (textureKey && texture) { + const poolKey = `${wx},${wy}`; + usedTileSprites.add(poolKey); + const entry = this._ensureSprite( + this._tileSpritePool, + poolKey, + textureKey, + texture, + this._groundSpriteLayer, + this._tileSpriteFreeList, + ); + entry.sprite.x = iso.x; + entry.sprite.y = iso.y + hh; + const texW = entry.sprite.texture.orig.width || entry.sprite.texture.width || TILE_WIDTH; + const scale = (TILE_WIDTH / texW) * TERRAIN_SEAM_BLEED_SCALE; + entry.sprite.scale.set(scale); + entry.sprite.visible = true; + } else { + const color = this._terrainColors(terrain, dark); + + gfx.poly([ + iso.x, iso.y - hh, + iso.x + hw, iso.y, + iso.x, iso.y + hh, + iso.x - hw, iso.y, + ]); + gfx.fill({ color, alpha: 1 }); + + gfx.poly([ + iso.x, iso.y - hh, + iso.x + hw, iso.y, + iso.x, iso.y + hh, + iso.x - hw, iso.y, + ]); + gfx.stroke({ + color: this._terrainStrokeColor(terrain), + width: 1, + alpha: 0.25, + }); + } } } @@ -512,26 +694,77 @@ export class GameRenderer { const objectEndY = endY + objectPaddingTiles; for (let wx = objectStartX; wx <= objectEndX; wx++) { for (let wy = objectStartY; wy <= objectEndY; wy++) { + const iso = worldToScreen(wx, wy); + if ( + Math.abs(iso.x - cx) > halfW + TILE_WIDTH * 1.5 || + Math.abs(iso.y - cy) > halfH + TILE_HEIGHT * 2 + ) { + continue; + } const terrainHere = proceduralTerrain(wx, wy, terrainCtx); const obj = proceduralObject(wx, wy, terrainHere, terrainCtx); if (!obj) continue; - const iso = worldToScreen(wx, wy); const variant = tileHash(wx, wy, 999); - if (obj === 'tree') this._drawTree(gfx, iso.x, iso.y, variant); - else if (obj === 'bush') this._drawBush(gfx, iso.x, iso.y, variant); - else if (obj === 'rock') this._drawRock(gfx, iso.x, iso.y, variant); - else if (obj === 'stump') this._drawStump(gfx, iso.x, iso.y, variant); - else if (obj === 'cart') this._drawBrokenCart(gfx, iso.x, iso.y, variant); - else if (obj === 'bones') this._drawBones(gfx, iso.x, iso.y, variant); - else if (obj === 'mushroom') this._drawMushroom(gfx, iso.x, iso.y, variant); - else if (obj === 'ruin') this._drawRuin(gfx, iso.x, iso.y, variant); - else if (obj === 'stall') this._drawMarketStall(gfx, iso.x, iso.y, variant); - else if (obj === 'well') this._drawWell(gfx, iso.x, iso.y, variant); - else if (obj === 'banner') this._drawBanner(gfx, iso.x, iso.y, variant); - else if (obj === 'barrel') this._drawBarrel(gfx, iso.x, iso.y, variant); - else if (obj === 'leaves') this._drawLeafPile(gfx, iso.x, iso.y, variant); + const objTextureKey = spritesReady ? objectToTextureKey(obj, variant) : null; + const objTexture = objTextureKey ? this._spriteRegistry.getTexture(objTextureKey) : null; + if (objTextureKey && objTexture) { + const poolKey = `${wx},${wy}`; + usedObjectSprites.add(poolKey); + const entry = this._ensureSprite( + this._objectSpritePool, + poolKey, + objTextureKey, + objTexture, + this._objectSpriteLayer, + this._objectSpriteFreeList, + ); + entry.sprite.x = iso.x; + entry.sprite.y = iso.y; + entry.sprite.visible = true; + } else { + if (obj === 'tree') this._drawTree(gfx, iso.x, iso.y, variant); + else if (obj === 'bush') this._drawBush(gfx, iso.x, iso.y, variant); + else if (obj === 'rock') this._drawRock(gfx, iso.x, iso.y, variant); + else if (obj === 'stump') this._drawStump(gfx, iso.x, iso.y, variant); + else if (obj === 'cart') this._drawBrokenCart(gfx, iso.x, iso.y, variant); + else if (obj === 'bones') this._drawBones(gfx, iso.x, iso.y, variant); + else if (obj === 'mushroom') this._drawMushroom(gfx, iso.x, iso.y, variant); + else if (obj === 'ruin') this._drawRuin(gfx, iso.x, iso.y, variant); + else if (obj === 'stall') this._drawMarketStall(gfx, iso.x, iso.y, variant); + else if (obj === 'well') this._drawWell(gfx, iso.x, iso.y, variant); + else if (obj === 'banner') this._drawBanner(gfx, iso.x, iso.y, variant); + else if (obj === 'barrel') this._drawBarrel(gfx, iso.x, iso.y, variant); + else if (obj === 'leaves') this._drawLeafPile(gfx, iso.x, iso.y, variant); + } } } + + if (spritesReady) { + this._hideUnusedSprites(this._tileSpritePool, usedTileSprites); + this._hideUnusedSprites(this._objectSpritePool, usedObjectSprites); + const evictPaddingTiles = 8; + this._evictSpritesOutsideTileBounds( + this._tileSpritePool, + startX - evictPaddingTiles, + endX + evictPaddingTiles, + startY - evictPaddingTiles, + endY + evictPaddingTiles, + this._tileSpriteFreeList, + 2800, + ); + this._evictSpritesOutsideTileBounds( + this._objectSpritePool, + objectStartX - evictPaddingTiles, + objectEndX + evictPaddingTiles, + objectStartY - evictPaddingTiles, + objectEndY + evictPaddingTiles, + this._objectSpriteFreeList, + 1800, + ); + } else { + this._hideUnusedSprites(this._tileSpritePool, new Set()); + this._hideUnusedSprites(this._objectSpritePool, new Set()); + } } /** @@ -602,13 +835,38 @@ export class GameRenderer { drawHero(wx: number, wy: number, phase: 'walk' | 'fight' | 'idle', now: number): void { const gfx = this._heroGfx; if (!gfx) return; - const { cy, iso } = this.paintHeroSilhouette(gfx, wx, wy, phase, now, 'self'); + const iso = worldToScreen(wx, wy); + const textureKey = this._spritesReady ? heroTextureKey() : null; + const texture = textureKey ? this._spriteRegistry.getTexture(textureKey) : null; + let cy = iso.y; + + if (textureKey && texture) { + gfx.clear(); + const entry = this._ensureSprite( + this._characterSpritePool, + 'hero', + textureKey, + texture, + this.entityLayer, + ); + entry.sprite.x = iso.x; + entry.sprite.y = iso.y; + entry.sprite.scale.set(0.80); + entry.sprite.zIndex = iso.y + 100; + entry.sprite.visible = true; + cy = iso.y; + } else { + const painted = this.paintHeroSilhouette(gfx, wx, wy, phase, now, 'self'); + cy = painted.cy; + const entry = this._characterSpritePool.get('hero'); + if (entry) entry.sprite.visible = false; + } const nameTxt = this._heroNameText; if (nameTxt && this._heroName) { nameTxt.text = this._heroName; nameTxt.x = iso.x; - nameTxt.y = iso.y - 42; + nameTxt.y = iso.y - 92; nameTxt.visible = true; nameTxt.zIndex = cy + 199; } @@ -621,7 +879,32 @@ export class GameRenderer { const gfx = this._meetPartnerGfx; const lbl = this._meetPartnerLabel; if (!gfx || !lbl) return; - const { cy, iso } = this.paintHeroSilhouette(gfx, wx, wy, 'idle', now, 'meet_partner'); + const iso = worldToScreen(wx, wy); + const textureKey = this._spritesReady ? meetPartnerTextureKey() : null; + const texture = textureKey ? this._spriteRegistry.getTexture(textureKey) : null; + let cy = iso.y; + + if (textureKey && texture) { + gfx.clear(); + const entry = this._ensureSprite( + this._characterSpritePool, + 'meet_partner', + textureKey, + texture, + this.entityLayer, + ); + entry.sprite.x = iso.x; + entry.sprite.y = iso.y; + entry.sprite.scale.set(0.80); + entry.sprite.zIndex = iso.y + 100; + entry.sprite.visible = true; + cy = iso.y; + } else { + const painted = this.paintHeroSilhouette(gfx, wx, wy, 'idle', now, 'meet_partner'); + cy = painted.cy; + const entry = this._characterSpritePool.get('meet_partner'); + if (entry) entry.sprite.visible = false; + } lbl.text = `${name} Lv.${level}`; lbl.x = iso.x; lbl.y = iso.y - 42; @@ -631,52 +914,100 @@ export class GameRenderer { clearMeetPartner(): void { this._meetPartnerGfx?.clear(); + const entry = this._characterSpritePool.get('meet_partner'); + if (entry) entry.sprite.visible = false; if (this._meetPartnerLabel) { this._meetPartnerLabel.visible = false; } } /** - * Draw a small camp (A-frame tent + campfire) near the hero during wilderness rest (wild phase). - * Placed slightly behind the hero in screen space for a “bivouac” read. + * Wilderness rest: tent + fire + bag as separate transparent sprites around the hero (wild phase). */ drawRestCamp(wx: number, wy: number, now: number): void { const gfx = this._restCampGfx; if (!gfx) return; - gfx.clear(); const iso = worldToScreen(wx, wy); const bob = Math.sin(now * 0.004) * 1.0; + const z = iso.y + 92; + + const hideRestCampSprites = (): void => { + for (const key of ['rest_camp_tent', 'rest_camp_fire', 'rest_camp_bag'] as const) { + const e = this._characterSpritePool.get(key); + if (e) e.sprite.visible = false; + } + }; + + if (this._spritesReady) { + const [tentKey, fireKey, bagKey] = restCampTextureKeys(); + const tentTex = this._spriteRegistry.getTexture(tentKey); + const fireTex = this._spriteRegistry.getTexture(fireKey); + const bagTex = this._spriteRegistry.getTexture(bagKey); + if (tentTex && fireTex && bagTex) { + gfx.clear(); + const b = bob * 0.35; + const place = ( + poolKey: string, + textureKey: string, + texture: Texture, + x: number, + y: number, + scale: number, + ): void => { + const entry = this._ensureSprite( + this._characterSpritePool, + poolKey, + textureKey, + texture, + this.entityLayer, + ); + entry.sprite.anchor.set(0.5, 1); + entry.sprite.x = x; + entry.sprite.y = y + b; + entry.sprite.scale.set(scale); + entry.sprite.zIndex = z; + entry.sprite.visible = true; + }; + place('rest_camp_tent', tentKey, tentTex, iso.x - 56, iso.y - 6, 2.1); + place('rest_camp_fire', fireKey, fireTex, iso.x + 48, iso.y + 6, 1.5); + place('rest_camp_bag', bagKey, bagTex, iso.x - 42, iso.y + 26, 1.1); + return; + } + } + + hideRestCampSprites(); + gfx.clear(); - // --- Tent (screen-left of hero, reads “behind” in iso) --- - const tx = iso.x - 26; - const ty = iso.y - 4 + bob * 0.4; + // --- Tent (screen-left of hero, reads “behind” in iso); ~2× size vs old fallback --- + const tx = iso.x - 54; + const ty = iso.y - 6 + bob * 0.4; // Ground shadow under tent - gfx.ellipse(tx, ty + 14, 22, 7); + gfx.ellipse(tx, ty + 28, 44, 14); gfx.fill({ color: 0x000000, alpha: 0.18 }); // Tent body (trapezoid wall + triangle roof) - gfx.poly([tx - 18, ty + 12, tx + 18, ty + 12, tx + 14, ty - 8, tx - 14, ty - 8]); + gfx.poly([tx - 36, ty + 24, tx + 36, ty + 24, tx + 28, ty - 16, tx - 28, ty - 16]); gfx.fill({ color: 0x8b6914, alpha: 0.92 }); - gfx.poly([tx - 14, ty - 8, tx, ty - 22, tx + 14, ty - 8]); + gfx.poly([tx - 28, ty - 16, tx, ty - 44, tx + 28, ty - 16]); gfx.fill({ color: 0xc4a574, alpha: 0.96 }); - gfx.poly([tx - 14, ty - 8, tx, ty - 22, tx + 14, ty - 8]); + gfx.poly([tx - 28, ty - 16, tx, ty - 44, tx + 28, ty - 16]); gfx.stroke({ color: 0x5c4030, width: 1.2, alpha: 0.85 }); - gfx.rect(tx - 5, ty + 2, 10, 10); + gfx.rect(tx - 10, ty + 4, 20, 20); gfx.fill({ color: 0x1a1510, alpha: 0.55 }); // Guy lines / pegs (tiny) - gfx.moveTo(tx - 18, ty + 12); - gfx.lineTo(tx - 26, ty + 16); + gfx.moveTo(tx - 36, ty + 24); + gfx.lineTo(tx - 52, ty + 32); gfx.stroke({ color: 0x4a3a2a, width: 1, alpha: 0.5 }); - gfx.moveTo(tx + 18, ty + 12); - gfx.lineTo(tx + 26, ty + 16); + gfx.moveTo(tx + 36, ty + 24); + gfx.lineTo(tx + 52, ty + 32); gfx.stroke({ color: 0x4a3a2a, width: 1, alpha: 0.5 }); - // --- Campfire (near tent / hero) --- - const cx = iso.x + 16; - const cy = iso.y + 10 + bob; + // --- Campfire (pushed right; center clear for hero) --- + const cx = iso.x + 42; + const cy = iso.y + 8 + bob; gfx.ellipse(cx, cy + 6, 12, 4); gfx.fill({ color: 0x000000, alpha: 0.22 }); @@ -701,6 +1032,10 @@ export class GameRenderer { clearRestCamp(): void { if (this._restCampGfx) this._restCampGfx.clear(); + for (const key of ['rest_camp_tent', 'rest_camp_fire', 'rest_camp_bag'] as const) { + const e = this._characterSpritePool.get(key); + if (e) e.sprite.visible = false; + } } /** @@ -717,9 +1052,45 @@ export class GameRenderer { ): void { const gfx = this._enemyGfx; if (!gfx) return; + + const iso = worldToScreen(wx, wy); + const sway = Math.sin(now * 0.004) * 2; + const cx = iso.x; + const cy = iso.y + sway; + + const southKey = enemySouthTextureKey(enemySlug); + const tex = this._spritesReady ? this._spriteRegistry.getTexture(southKey) : null; + if (tex) { + const entry = this._ensureSprite( + this._characterSpritePool, + 'enemy_combat', + southKey, + tex, + this.entityLayer, + ); + entry.sprite.anchor.set(0.5, 1); + entry.sprite.x = cx; + entry.sprite.y = cy; + const th = tex.height || 48; + const targetH = 52; + entry.sprite.scale.set(targetH / th); + entry.sprite.zIndex = cy + 100; + entry.sprite.visible = true; + drawEnemyHpBarOnly(gfx, enemySlug, enemyArchetype, cx, cy, hp, maxHp); + return; + } + + const pooled = this._characterSpritePool.get('enemy_combat'); + if (pooled) pooled.sprite.visible = false; drawEnemyBySlug(gfx, wx, wy, hp, maxHp, enemySlug, enemyArchetype, now, worldToScreen); } + clearEnemyCombat(): void { + if (this._enemyGfx) this._enemyGfx.clear(); + const pooled = this._characterSpritePool.get('enemy_combat'); + if (pooled) pooled.sprite.visible = false; + } + /** * Draw a white rounded-rect thought bubble above the hero with a small * downward-pointing triangle. Fades in over 300ms and fades out when the @@ -733,8 +1104,7 @@ export class GameRenderer { const elapsed = now - startMs; // Fade in over 300ms - const fadeIn = Math.min(1, elapsed / 300); - const alpha = fadeIn; + const alpha = Math.min(1, elapsed / 300); if (alpha <= 0) { txt.visible = false; return; @@ -920,6 +1290,8 @@ export class GameRenderer { */ private _drawServerBuildings( gfx: Graphics, + iconGfx: Graphics, + usedBuildingSprites: Set, buildings: BuildingData[], _townScreenX: number, _townScreenY: number, @@ -936,40 +1308,73 @@ export class GameRenderer { const rh = 32 * scale; const bt = b.buildingType; + const spriteKey = this._spritesReady ? buildingTypeToTextureKey(bt) : null; + const spriteTexture = spriteKey ? this._spriteRegistry.getTexture(spriteKey) : null; + if (spriteKey && spriteTexture) { + const poolKey = `building:${b.id}`; + usedBuildingSprites.add(poolKey); + const entry = this._ensureSprite( + this._buildingSpritePool, + poolKey, + spriteKey, + spriteTexture, + this._buildingSpriteLayer, + ); + entry.sprite.x = bx; + entry.sprite.y = by; + const texW = entry.sprite.texture.width || w; + const scaleFactor = texW > 0 ? w / texW : 1; + entry.sprite.scale.set(scaleFactor); + entry.sprite.zIndex = by; + entry.sprite.visible = true; + } + + const hasSprite = Boolean(spriteKey && spriteTexture); + if (!hasSprite) { + if (bt === 'house.quest_giver') { + this._drawHouse(gfx, bx, by, w, h, rh, 0xb89040, 0x6a3a22, 0); + this._drawFence(gfx, bx, by, w, 'left'); + } else if (bt === 'house.merchant') { + this._drawHouse(gfx, bx, by, w * 1.1, h, rh * 0.8, 0x44aa55, 0x2a5a30, 1); + this._drawTownStall(gfx, bx + w * 0.7, by + 4, scale * 0.6); + } else if (bt === 'house.armorer') { + this._drawHouse(gfx, bx, by, w * 1.05, h, rh * 0.85, 0x5a6e8a, 0x2a3548, 1); + } else if (bt === 'house.weapon_smith') { + this._drawHouse(gfx, bx, by, w * 1.05, h, rh * 0.85, 0x8a5a3a, 0x4a3020, 1); + } else if (bt === 'house.jeweler') { + this._drawHouse(gfx, bx, by, w, h * 0.95, rh * 0.9, 0x7a4a9a, 0x3a2050, 2); + } else if (bt === 'house.bounty_hunter') { + this._drawHouse(gfx, bx, by, w, h, rh, 0x906040, 0x4a2818, 0); + this._drawFence(gfx, bx, by, w, 'right'); + } else if (bt === 'house.elder') { + this._drawHouse(gfx, bx, by, w * 0.98, h, rh * 1.05, 0x9a8860, 0x5a4830, 0); + } else if (bt === 'house.healer') { + this._drawHouse(gfx, bx, by, w, h, rh, 0xccccdd, 0x5555aa, 2); + } else if (bt === 'decoration.well') { + this._drawTownWell(gfx, bx, by, scale); + } else if (bt === 'decoration.stall') { + this._drawTownStall(gfx, bx, by, scale * 0.9); + } else if (bt === 'decoration.signpost') { + this._drawSignpost(gfx, bx, by, scale); + } + } if (bt === 'house.quest_giver') { - this._drawHouse(gfx, bx, by, w, h, rh, 0xb89040, 0x6a3a22, 0); - this._drawFence(gfx, bx, by, w, 'left'); - this._drawBuildingIcon(gfx, bx, by - h - rh * 0.5, '!', 0xffd700, scale); + this._drawBuildingIcon(iconGfx, bx, by - h - rh * 0.5, '!', 0xffd700, scale); } else if (bt === 'house.merchant') { - this._drawHouse(gfx, bx, by, w * 1.1, h, rh * 0.8, 0x44aa55, 0x2a5a30, 1); - this._drawTownStall(gfx, bx + w * 0.7, by + 4, scale * 0.6); - this._drawBuildingIcon(gfx, bx, by - h - rh * 0.3, '$', 0x88dd88, scale); + this._drawBuildingIcon(iconGfx, bx, by - h - rh * 0.3, '$', 0x88dd88, scale); } else if (bt === 'house.armorer') { - this._drawHouse(gfx, bx, by, w * 1.05, h, rh * 0.85, 0x5a6e8a, 0x2a3548, 1); - this._drawBuildingIcon(gfx, bx, by - h - rh * 0.4, 'A', 0xaaccff, scale); + this._drawBuildingIcon(iconGfx, bx, by - h - rh * 0.4, 'A', 0xaaccff, scale); } else if (bt === 'house.weapon_smith') { - this._drawHouse(gfx, bx, by, w * 1.05, h, rh * 0.85, 0x8a5a3a, 0x4a3020, 1); - this._drawBuildingIcon(gfx, bx, by - h - rh * 0.35, 'W', 0xffaa66, scale); + this._drawBuildingIcon(iconGfx, bx, by - h - rh * 0.35, 'W', 0xffaa66, scale); } else if (bt === 'house.jeweler') { - this._drawHouse(gfx, bx, by, w, h * 0.95, rh * 0.9, 0x7a4a9a, 0x3a2050, 2); - this._drawBuildingIcon(gfx, bx, by - h - rh * 0.45, 'J', 0xdd88ff, scale); + this._drawBuildingIcon(iconGfx, bx, by - h - rh * 0.45, 'J', 0xdd88ff, scale); } else if (bt === 'house.bounty_hunter') { - this._drawHouse(gfx, bx, by, w, h, rh, 0x906040, 0x4a2818, 0); - this._drawFence(gfx, bx, by, w, 'right'); - this._drawBuildingIcon(gfx, bx, by - h - rh * 0.5, 'B', 0xffcc44, scale); + this._drawBuildingIcon(iconGfx, bx, by - h - rh * 0.5, 'B', 0xffcc44, scale); } else if (bt === 'house.elder') { - this._drawHouse(gfx, bx, by, w * 0.98, h, rh * 1.05, 0x9a8860, 0x5a4830, 0); - this._drawBuildingIcon(gfx, bx, by - h - rh * 0.5, 'E', 0xeeddaa, scale); + this._drawBuildingIcon(iconGfx, bx, by - h - rh * 0.5, 'E', 0xeeddaa, scale); } else if (bt === 'house.healer') { - this._drawHouse(gfx, bx, by, w, h, rh, 0xccccdd, 0x5555aa, 2); - this._drawBuildingIcon(gfx, bx, by - h - rh * 0.5, '+', 0xff6666, scale); - } else if (bt === 'decoration.well') { - this._drawTownWell(gfx, bx, by, scale); - } else if (bt === 'decoration.stall') { - this._drawTownStall(gfx, bx, by, scale * 0.9); - } else if (bt === 'decoration.signpost') { - this._drawSignpost(gfx, bx, by, scale); + this._drawBuildingIcon(iconGfx, bx, by - h - rh * 0.5, '+', 0xff6666, scale); } } } @@ -989,7 +1394,7 @@ export class GameRenderer { gfx.ellipse(cx, cy, 10 * s, 5 * s); gfx.fill({ color: 0x6a6a7a, alpha: 0.8 }); gfx.stroke({ color: 0x4a4a5a, width: 1.5, alpha: 0.6 }); - gfx.rect(cx - 1 * s, cy - 12 * s, 2 * s, 12 * s); + gfx.rect(cx - s, cy - 12 * s, 2 * s, 12 * s); gfx.fill({ color: 0x5a4a3a, alpha: 0.9 }); gfx.rect(cx - 6 * s, cy - 13 * s, 12 * s, 2 * s); gfx.fill({ color: 0x5a4a3a, alpha: 0.9 }); @@ -1064,7 +1469,7 @@ export class GameRenderer { /** Draw a signpost decoration. */ private _drawSignpost(gfx: Graphics, cx: number, cy: number, s: number): void { - gfx.rect(cx - 1 * s, cy - 16 * s, 2 * s, 16 * s); + gfx.rect(cx - s, cy - 16 * s, 2 * s, 16 * s); gfx.fill({ color: 0x6a5a3a, alpha: 0.9 }); gfx.poly([ cx + 2 * s, cy - 14 * s, @@ -1140,8 +1545,10 @@ export class GameRenderer { */ drawTowns(towns: TownData[], camera: Camera, screenWidth: number, screenHeight: number): void { const gfx = this._townGfx; - if (!gfx) return; + const iconGfx = this._townIconGfx; + if (!gfx || !iconGfx) return; gfx.clear(); + iconGfx.clear(); // Hide all existing labels first; we'll show visible ones below for (const lbl of this._townLabels) { @@ -1155,6 +1562,7 @@ export class GameRenderer { let labelIdx = 0; + const usedBuildingSprites = new Set(); for (const town of towns) { // Convert town world position to screen space const townScreen = worldToScreen(town.centerX, town.centerY); @@ -1208,7 +1616,7 @@ export class GameRenderer { // --- Buildings: server-driven if available, fallback procedural --- if (town.buildings && town.buildings.length > 0) { - this._drawServerBuildings(gfx, town.buildings, tx, ty, s); + this._drawServerBuildings(gfx, iconGfx, usedBuildingSprites, town.buildings, tx, ty, s); this._drawCivicBuilding(gfx, civicScreen.x, civicScreen.y, s); } else { this._drawProceduralBuildings(gfx, tx, ty, s, spread, town.size, townSeed); @@ -1252,6 +1660,8 @@ export class GameRenderer { label.zIndex = ty - 500; labelIdx++; } + + this._hideUnusedSprites(this._buildingSpritePool, usedBuildingSprites); } /** @@ -1279,6 +1689,7 @@ export class GameRenderer { const halfH = screenHeight / (2 * MAP_ZOOM) + TILE_HEIGHT * 3; let labelIdx = 0; + const usedNpcSprites = new Set(); for (const npc of npcs) { const iso = worldToScreen(npc.worldX, npc.worldY); @@ -1298,154 +1709,210 @@ export class GameRenderer { gfx.ellipse(cx, cy + 8, 10, 3.5); gfx.fill({ color: 0x000000, alpha: 0.22 }); - // NPC body diamond (type-specific color) - let bodyColor: number; - let bodyStroke: number; - let iconText: string; - let iconColor: number; - - switch (npc.type) { - case 'quest_giver': - case 'bounty_hunter': - bodyColor = 0xdaa520; - bodyStroke = 0x8a6510; - iconText = '!'; - iconColor = 0xffd700; - break; - case 'elder': - bodyColor = 0xc4a574; - bodyStroke = 0x7a6040; - iconText = '\u2020'; - iconColor = 0xeeddaa; - break; - case 'merchant': - bodyColor = 0x44aa55; - bodyStroke = 0x2a7a3a; - iconText = '$'; - iconColor = 0x88dd88; - break; - case 'armorer': - bodyColor = 0x5a7a9a; - bodyStroke = 0x304560; - iconText = '\u25C9'; - iconColor = 0xaaccff; - break; - case 'weapon': - bodyColor = 0xaa6633; - bodyStroke = 0x6a3818; - iconText = '\u2694'; - iconColor = 0xffaa66; - break; - case 'jeweler': - bodyColor = 0x8844aa; - bodyStroke = 0x502060; - iconText = '\u2666'; - iconColor = 0xdd88ff; - break; - case 'healer': - bodyColor = 0xdddddd; - bodyStroke = 0x8888aa; - iconText = '+'; - iconColor = 0xff6666; - break; - default: - bodyColor = 0x8888aa; - bodyStroke = 0x555577; - iconText = '?'; - iconColor = 0xaaaacc; - } + const npcTextureKey = this._spritesReady ? npcTypeToTextureKey(npc.type) : null; + const npcTexture = npcTextureKey ? this._spriteRegistry.getTexture(npcTextureKey) : null; + const hasSprite = Boolean(npcTextureKey && npcTexture); + + if (npcTextureKey && npcTexture) { + const poolKey = `npc:${npc.id}`; + usedNpcSprites.add(poolKey); + const entry = this._ensureSprite( + this._npcSpritePool, + poolKey, + npcTextureKey, + npcTexture, + this.entityLayer, + ); + entry.sprite.x = cx; + entry.sprite.y = cy; + entry.sprite.scale.set(1); + entry.sprite.zIndex = cy + 90; + entry.sprite.visible = true; + } else { + // NPC body diamond (type-specific color) + let bodyColor: number; + let bodyStroke: number; + let iconText: string; + let iconColor: number; + + switch (npc.type) { + case 'quest_giver': + case 'bounty_hunter': + bodyColor = 0xdaa520; + bodyStroke = 0x8a6510; + iconText = '!'; + iconColor = 0xffd700; + break; + case 'elder': + bodyColor = 0xc4a574; + bodyStroke = 0x7a6040; + iconText = '\u2020'; + iconColor = 0xeeddaa; + break; + case 'merchant': + bodyColor = 0x44aa55; + bodyStroke = 0x2a7a3a; + iconText = '$'; + iconColor = 0x88dd88; + break; + case 'armorer': + bodyColor = 0x5a7a9a; + bodyStroke = 0x304560; + iconText = '\u25C9'; + iconColor = 0xaaccff; + break; + case 'weapon': + bodyColor = 0xaa6633; + bodyStroke = 0x6a3818; + iconText = '\u2694'; + iconColor = 0xffaa66; + break; + case 'jeweler': + bodyColor = 0x8844aa; + bodyStroke = 0x502060; + iconText = '\u2666'; + iconColor = 0xdd88ff; + break; + case 'healer': + bodyColor = 0xdddddd; + bodyStroke = 0x8888aa; + iconText = '+'; + iconColor = 0xff6666; + break; + default: + bodyColor = 0x8888aa; + bodyStroke = 0x555577; + iconText = '?'; + iconColor = 0xaaaacc; + } - // Diamond body - const ds = 0.85; - gfx.poly([ - cx, cy - 18 * ds, - cx + 10 * ds, cy - 2 * ds, - cx, cy + 8 * ds, - cx - 10 * ds, cy - 2 * ds, - ]); - gfx.fill({ color: bodyColor, alpha: 0.75 }); - gfx.stroke({ color: bodyStroke, width: 1.5, alpha: 0.7 }); + // Diamond body + const ds = 0.85; + gfx.poly([ + cx, cy - 18 * ds, + cx + 10 * ds, cy - 2 * ds, + cx, cy + 8 * ds, + cx - 10 * ds, cy - 2 * ds, + ]); + gfx.fill({ color: bodyColor, alpha: 0.75 }); + gfx.stroke({ color: bodyStroke, width: 1.5, alpha: 0.7 }); - // Head circle - gfx.circle(cx, cy - 16 * ds, 5 * ds); - gfx.fill({ color: bodyColor, alpha: 0.6 }); + // Head circle + gfx.circle(cx, cy - 16 * ds, 5 * ds); + gfx.fill({ color: bodyColor, alpha: 0.6 }); - // Floating icon above head - const iconBob = Math.sin(now * 0.004 + npc.id * 2.3) * 2; - const iconY = cy - 28 * ds + iconBob; + // Floating icon above head + const iconBob = Math.sin(now * 0.004 + npc.id * 2.3) * 2; + const iconY = cy - 28 * ds + iconBob; - // Icon background circle - gfx.circle(cx, iconY, 7); - gfx.fill({ color: 0x000000, alpha: 0.35 }); + // Icon background circle + gfx.circle(cx, iconY, 7); + gfx.fill({ color: 0x000000, alpha: 0.35 }); - // We use Text for the icon character - // (drawn as part of label pool) + // We use Text for the icon character + // (drawn as part of label pool) - // NPC name label below - let nameLabel: Text; - if (labelIdx < this._npcLabels.length) { - nameLabel = this._npcLabels[labelIdx]!; - } else { - if (this._npcLabelPool.length > 0) { - nameLabel = this._npcLabelPool.pop()!; + // NPC name label below + let nameLabel: Text; + if (labelIdx < this._npcLabels.length) { + nameLabel = this._npcLabels[labelIdx]!; } else { - nameLabel = new Text({ - text: '', - style: new TextStyle({ - fontSize: 10, - fontFamily: 'system-ui, sans-serif', - fill: 0xcccccc, - stroke: { color: 0x000000, width: 2 }, - align: 'center', - }), - }); - nameLabel.anchor.set(0.5, 0.5); + if (this._npcLabelPool.length > 0) { + nameLabel = this._npcLabelPool.pop()!; + } else { + nameLabel = new Text({ + text: '', + style: new TextStyle({ + fontSize: 10, + fontFamily: 'system-ui, sans-serif', + fill: 0xcccccc, + stroke: { color: 0x000000, width: 2 }, + align: 'center', + }), + }); + nameLabel.anchor.set(0.5, 0.5); + } + this.entityLayer.addChild(nameLabel); + this._npcLabels.push(nameLabel); } - this.entityLayer.addChild(nameLabel); - this._npcLabels.push(nameLabel); - } - - nameLabel.text = npc.name; - nameLabel.x = cx; - nameLabel.y = cy + 14; - nameLabel.visible = true; - nameLabel.zIndex = cy + 101; - labelIdx++; - // Icon label (reusing label pool pattern: +1 entry for the icon) - let iconLabel: Text; - if (labelIdx < this._npcLabels.length) { - iconLabel = this._npcLabels[labelIdx]!; - } else { - if (this._npcLabelPool.length > 0) { - iconLabel = this._npcLabelPool.pop()!; + nameLabel.text = npc.name; + nameLabel.x = cx; + nameLabel.y = cy + 14; + nameLabel.visible = true; + nameLabel.zIndex = cy + 101; + labelIdx++; + + // Icon label (reusing label pool pattern: +1 entry for the icon) + let iconLabel: Text; + if (labelIdx < this._npcLabels.length) { + iconLabel = this._npcLabels[labelIdx]!; } else { - iconLabel = new Text({ - text: '', - style: new TextStyle({ - fontSize: 12, - fontFamily: 'system-ui, sans-serif', - fontWeight: 'bold', - fill: 0xffffff, - align: 'center', - }), - }); - iconLabel.anchor.set(0.5, 0.5); + if (this._npcLabelPool.length > 0) { + iconLabel = this._npcLabelPool.pop()!; + } else { + iconLabel = new Text({ + text: '', + style: new TextStyle({ + fontSize: 12, + fontFamily: 'system-ui, sans-serif', + fontWeight: 'bold', + fill: 0xffffff, + align: 'center', + }), + }); + iconLabel.anchor.set(0.5, 0.5); + } + this.entityLayer.addChild(iconLabel); + this._npcLabels.push(iconLabel); } - this.entityLayer.addChild(iconLabel); - this._npcLabels.push(iconLabel); + + iconLabel.text = iconText; + iconLabel.style.fill = iconColor; + iconLabel.x = cx; + iconLabel.y = iconY; + iconLabel.visible = true; + iconLabel.zIndex = cy + 201; + labelIdx++; + + gfx.zIndex = cy + 100; } - iconLabel.text = iconText; - iconLabel.style.fill = iconColor; - iconLabel.x = cx; - iconLabel.y = iconY; - iconLabel.visible = true; - iconLabel.zIndex = cy + 201; - labelIdx++; + // NPC name label below for sprite case + if (hasSprite) { + let nameLabel: Text; + if (labelIdx < this._npcLabels.length) { + nameLabel = this._npcLabels[labelIdx]!; + } else { + if (this._npcLabelPool.length > 0) { + nameLabel = this._npcLabelPool.pop()!; + } else { + nameLabel = new Text({ + text: '', + style: new TextStyle({ + fontSize: 10, + fontFamily: 'system-ui, sans-serif', + fill: 0xcccccc, + stroke: { color: 0x000000, width: 2 }, + align: 'center', + }), + }); + nameLabel.anchor.set(0.5, 0.5); + } + this.entityLayer.addChild(nameLabel); + this._npcLabels.push(nameLabel); + } - gfx.zIndex = cy + 100; + nameLabel.text = npc.name; + nameLabel.x = cx; + nameLabel.y = cy + 6; + nameLabel.visible = true; + nameLabel.zIndex = cy + 101; + labelIdx++; + } } + + this._hideUnusedSprites(this._npcSpritePool, usedNpcSprites); } /** Clear NPC visuals when there are none to render */ @@ -1454,6 +1921,7 @@ export class GameRenderer { for (const lbl of this._npcLabels) { lbl.visible = false; } + this._hideUnusedSprites(this._npcSpritePool, new Set()); } /** @@ -1563,8 +2031,7 @@ export class GameRenderer { continue; } const elapsed = now - item.startMs; - const fadeIn = Math.min(1, elapsed / 200); - const alpha = fadeIn; + const alpha = Math.min(1, elapsed / 200);; if (alpha <= 0) { txt.visible = false; gfx.clear(); @@ -1615,6 +2082,9 @@ export class GameRenderer { /** Sort entity layer by y-position for correct isometric depth */ sortEntities(): void { + const now = performance.now(); + if (now - this._lastEntitySortMs < this._entitySortIntervalMs) return; + this._lastEntitySortMs = now; this.entityLayer.sortableChildren = true; this.entityLayer.children.sort((a, b) => (a.zIndex ?? a.y) - (b.zIndex ?? b.y)); }