From 3ea8cbc8c34364bc4b66807965382428ae9f7a5b Mon Sep 17 00:00:00 2001 From: John Kenyon Date: Wed, 31 Dec 2025 19:42:46 -0800 Subject: [PATCH] Added more features and art assets --- assets/archer.png | Bin 0 -> 1373 bytes assets/axe.png | Bin 0 -> 1115 bytes assets/buckler.png | Bin 0 -> 1345 bytes assets/cobra.png | Bin 0 -> 1450 bytes assets/dagger.png | Bin 0 -> 848 bytes assets/dragon.png | Bin 0 -> 1539 bytes assets/dungeon lord.png | Bin 0 -> 1555 bytes assets/fountain.png | Bin 0 -> 1439 bytes assets/hero.png | Bin 0 -> 1567 bytes assets/house questgiver.png | Bin 0 -> 1130 bytes assets/kite shield.png | Bin 0 -> 1403 bytes assets/kobold.png | Bin 0 -> 1027 bytes assets/mysterious figure.png | Bin 0 -> 1233 bytes assets/ogre.png | Bin 0 -> 1980 bytes assets/orc.png | Bin 0 -> 1334 bytes assets/peasant1.png | Bin 0 -> 1065 bytes assets/peasant2.png | Bin 0 -> 1157 bytes assets/questgiver outside.png | Bin 0 -> 1331 bytes assets/rat.png | Bin 0 -> 1135 bytes assets/skeleton archer.png | Bin 0 -> 856 bytes assets/spellbook.png | Bin 0 -> 1592 bytes assets/sword.png | Bin 0 -> 1222 bytes assets/wooden shield.png | Bin 0 -> 1079 bytes assets/zappy laser.png | Bin 0 -> 1200 bytes index.html | 5 +- src/Entity.js | 62 +++- src/Game.js | 588 +++++++++++++++++++++++++++++++--- src/Item.js | 28 +- src/Map.js | 127 +++++++- src/UI.js | 413 +++++++++++++++++++++--- style.css | 28 +- 31 files changed, 1128 insertions(+), 123 deletions(-) create mode 100644 assets/archer.png create mode 100644 assets/axe.png create mode 100644 assets/buckler.png create mode 100644 assets/cobra.png create mode 100644 assets/dagger.png create mode 100644 assets/dragon.png create mode 100644 assets/dungeon lord.png create mode 100644 assets/fountain.png create mode 100644 assets/hero.png create mode 100644 assets/house questgiver.png create mode 100644 assets/kite shield.png create mode 100644 assets/kobold.png create mode 100644 assets/mysterious figure.png create mode 100644 assets/ogre.png create mode 100644 assets/orc.png create mode 100644 assets/peasant1.png create mode 100644 assets/peasant2.png create mode 100644 assets/questgiver outside.png create mode 100644 assets/rat.png create mode 100644 assets/skeleton archer.png create mode 100644 assets/spellbook.png create mode 100644 assets/sword.png create mode 100644 assets/wooden shield.png create mode 100644 assets/zappy laser.png diff --git a/assets/archer.png b/assets/archer.png new file mode 100644 index 0000000000000000000000000000000000000000..a9f3a64fdefb00d7b77a6bfd5b40024586a36d48 GIT binary patch literal 1373 zcmV-j1)}z@;j|==^1poj5AY({UO#lFTCIA3{ga82g0001h=l}q9FaQARU;qF* zm;eA5aGbhPJOBUy32;bRa{vG?BLDy{BLR4&KXw2B1jb23K~zY`wU*m&6m=BG&u{KC zJ3G7E>0a6$y4%w3T3QT5@IpWuF=7Y_(Lh3s2@gK{V&cEyn=vFNJ{VL|V-y348YCrJ z3b@Lp6nfijx24_f?6o_4o1OXXj1QHH5$MJB_cqBl=aX~3b1ulXZFuIht2N{hV7rgl zFnZ(TPa2<}`@aClA!HA~vXq#bJM-@F#o>H0-(mmdbDD&u(OzVzuwXV0IVPE5BL zP;b-?Yz#+)h-y~HzBzyCoSIWx2~hk>*cH$?W-`d7$mYd+mu~%W%{0x{0Gg%|cDc^% zKl0k)lktOx<~c!c8rN=L)hgO10n|nSlWNrbk|!W}c$(p8HumsAJe$lH^Rh>70RR9j zj1dCchAv-+$0_aZ>gCB^|a_BInk`wHk(0i zB|UoKYrU@J7$ZwJ0e}P`0GY^Eq0Zr)pK~qFOi>i=Y4>)9!e*rew$;#9XnpT2wH*eO?=Op7kFyt*k5s^#)R zd?tS9cWmeyZIua&0)&7-V2L(?!L9@7wyBn~jcN(kE0bd*U42oeQUcp%ANzfu>;X2- zOnQ0Z=BQpN_r7?jip&aOt%*oP$QCVFhyY}ORj8H*hK^ILqm)ig+!(FrRYa2iuFn%p z){4bqdimA2Pj^Lk^#}JTm_~pQASdGbnJ%xzQUIiFvyyb+&Exw|yu{a&miOSHC6T!%?QaUM1Z5{0ar9%OLikeDa{}}*6M^9~M*kRKw z7YoZ1vF_*h^$zR~gn9^qihch@{J~#yt=Z6>;mFRn-V^r^ZD02 za5yDbXV|D|er3ld0qAMhbs?LXpPtO8Q-M%;XaAtX>BOd)&!{6 literal 0 HcmV?d00001 diff --git a/assets/axe.png b/assets/axe.png new file mode 100644 index 0000000000000000000000000000000000000000..36be0b516e901f84f537049bda6b00af2ba847ee GIT binary patch literal 1115 zcmV-h1f=_kP)z@;j|==^1poj5AY({UO#lFTCIA3{ga82g0001h=l}q9FaQARU;qF* zm;eA5aGbhPJOBUy32;bRa{vG?BLDy{BLR4&KXw2B1H?&0K~zY`wU*Cs8&wd;XZDxv zwbz?8cAeNx?8c5_r4UjH8l(aj&fx(32felY3H={>%&8JLDj}g>NvcTDhSC5+wg?n3 zBu<>hv6I+wZ13*-$ssiDA$D+Z=d`=?-hTAvJ2P)kN(nvf__(#S{OIAsA2{cPa!peN zm!q*Ed&r7LN9M23rc!a!G!+HU+^)cRrmE`InW?$Ctm{%nd8b2~O=l!xN29}!ALk!D zSo!g%bG{*|pG~BqH*bC{rF^>fd!24fr%HBCJ_YWjRee}JP#gSxH+g-K&$tG2xS==pB7P$-tm2bSe0grO!?6#>B` zbhonaIBqBu?B@ja`bmL!XcpGD)_&gPTy#2~1gO{B@v&jYB>(~eITy8R-SfzqfbJR) zB830|M08vxg>+pCfC{2%8qH%T5HJAni~Gx^Qq3@QUDx^x5hVlw5QK)@v`s+84EmbQ zju6r#%=8Sgy3AfH9Hjw24>%!9v&X*wxwq`ebHPx z?e@vT-rjyb{}&^^z@}e6%jzb7H3{_E- z#l^2aT6iy;OdlxI&yvzc5@6zBC4(n&jSF?`N>Di3N0*r2mql_ zs7KuY6ToMj(H(n-yFZZ`W3tN~GBCia_x|7kgR-swr_(uiWct4+c)c1N)M}@5&S{zs z65zS6;2Z$(H33fN98}e?ZC@DBwa~`XY16i;=UttjkHzB`24J^T*{@djc6TYImm-m| hL_*i~0RskQ{{SCA)aMw?adQ9w002ovPDHLkV1h|h1~32s literal 0 HcmV?d00001 diff --git a/assets/buckler.png b/assets/buckler.png new file mode 100644 index 0000000000000000000000000000000000000000..364f8d814a717469a2bafc763c1e53aff7e3120a GIT binary patch literal 1345 zcmV-H1-|-;P)z@;j|==^1poj5AY({UO#lFTCIA3{ga82g0001h=l}q9FaQARU;qF* zm;eA5aGbhPJOBUy32;bRa{vG?BLDy{BLR4&KXw2B1gc3yK~zY`)s{_IUx>w#NSB*|!ZH-7oDJsM@ZyS+5i zS(-C|lIXZfN_1TZQOwOXOC`TvuU0GJQ6%wp;=8-?#zuR2rM0yc=ee<#E?qji(~hI4 zHa|c6{P~7Jfr#tt+sy2`P8hn2i{~z0n4O-kj|cR6gJ-|5J$(3w)@EV;%#|y1{eJRi zYkO+4+S%&`ftMs%6x9WQv9z?bjEF)A&sBHsd^$gWZu}LsTJ6V=R)<5qc=_y9W8$}` zYbz_S003YEKp+YtF8(y%cI6!-6uk~_UAYQ+Ip&3p7 z@Z$s5Rpqjmrg;%sDM>_HTPZP)hxhOQYK)zlsxZUa>SnV!H3nb}F>xFZR#)xg$7_fv zC2DQ5Y=kJoFkl8L5hW3k=Q(Mrm6CBh3<7sF(pjp<%)!%VtNZbQnd3MiB1GtRd+qkl z^70F{% z7HV)Ej{%HH27~0AZ-0KZ(e8BOn>VhO%YJc$h>fw#T!cPKYTp1n&#hKNW+vi7q37qX zG#azL{;=2Ue|`7fy`Pp4IY|bC!LZfZ$g+$Xk|ZnC0#S@tQz<2Yu$H8fWQmBI&8F{r zt=2}ZR&yNp%dhVGzPq`(Bc&8VHX2^J><@-1TR_Ah@Q(v1B|T5Mu0+J#SVv-RED_i0 zlTxbA-hQ{+W2W5LC~8De#4LmWW+p=*L^LxIhTic2H9I@AzTTdis;sSTGh($GYHhMC zZ%j|t>ou)4GY~PNcpV+O-J}@cD4IOf67ZiH%uLsBe73l=(>;4;(iod-GcgfXD!w%Y z0FLXruIu}*QmUxxLFZh%_Wtbb%y`2w#_sKR_d31rzW=3K4F`i%>o>p#pfTrZ2%_%6pX=Ol#EgoNX)Jy z!!YoD4}guaN=c>Eq56DMK=JSrLNLQ=Uv=XE|H=9bqFB3^8s&=I00000NkvXXu0mjf D`Tl>> literal 0 HcmV?d00001 diff --git a/assets/cobra.png b/assets/cobra.png new file mode 100644 index 0000000000000000000000000000000000000000..ee7f45f6d16e5d5116e17d1dca25b44e7e3379c0 GIT binary patch literal 1450 zcmV;b1y%ZqP)z@;j|==^1poj5AY({UO#lFTCIA3{ga82g0001h=l}q9FaQARU;qF* zm;eA5aGbhPJOBUy32;bRa{vG?BLDy{BLR4&KXw2B1rtd`K~zY`wN^`W6jc=d&bhaH zo|#S{nTb3#6|jo(fr>$vRE1*UMmJinS-8`!F8BrN|DcpoTnLmdRFp+jYBGQV@(PZG z3?z`G=Fu~K?>R0S0>}_Ri?itJuI|3y{mwbxcP^+YL`80CtF7&}wugB*M>&2V!|7_?du#3EGH6KSaB}z${o{ttXOM}}v9Rw1@9wda z3MJenCn`kruMPJB&{mrY`IX06nf*o%SL@RWdc469D1d!UcZ~YVIFIKkzk;En#Dx11 z?xe_h5NS82T+U6`czBrCuakwF=UcyhR{8P={qg!YHvkO0IY1x+6_5`Rei(c(X(l)F z#=9g$K!AdX(#qWi&`yVfKt(7BKqQwgCx?&n^Ut>~PQCx$w`YS}G2$M2>E3e4CU6WI z!q5awydAu?%k0WgZlwkyLPX4bPl~)C!zrdVTiWUHhV|%m-C%x*M%RJ~)Ipq}o4_aN zDb&D@V)TTbsP$@}hM!E>iL~ETHND=SBJaTrB8a$ZO)gw8ufD?T*EtveoU7f?8wnCa zhQPne>8ecsrB?`Sz!4$>@8pU7nf)n=l!u5IW1gT*V~qvEWV+g2)}>Q&>TCD)DLGxC z%7V_Xp|u|eAMLa|9|W)~4UARQl%?#Qo6GNo05Ca3C%pJM$xy3~&(N2)SChq;C}7O~hwOQ8&^pfZfLO|%+l0PyfxJu9hc*?dTp_}m}t`Ft`;=yC}ojNi~F-b zz90(^Wl)!T&DUz3lQwHhTC$OeH+Su?#vU`|DS6##K|uhB9k|DN&Cgb@HD;#s=kwB# z+(`C|O`pH=()QiFyAMEJ>fgEVj&zQ?hU>L^$!2HPF+%kABxZqmp2MS|hN^i510L{* z!?Jjaq1U-o`j{J19}VMSVj zdnPh% z+8!}Di)?>@3}pc25kpnzfbnDAt@rj%piTCs;Wir>17kL@yXBmE5*W`AQWm$(g@us8 z0m4BV><=)`TQ-esMns*U)4d$`;>2I^vPQ`%jS!7cDq}b2{j<_TLV)6SKiVKDpa5WB zxNppi^%q(y-I|;{t7isj=<4*}{+i5Qy6OkL-y*t06&fHYpb$|sKm$c8GK@m9$QDst z)}_5>@1&g^q`?&(^ilG8e<7k06K9caqS^NC88-ufEdwZ0F^_yiQAlBk(8DK42xMVX zei23Yo@E|A(FAwu2&8|e2aDUY96o)!;)DO|WcMH9AEyqsJbS#z7XSbN07*qoM6N<$ Eg6jX9tpET3 literal 0 HcmV?d00001 diff --git a/assets/dagger.png b/assets/dagger.png new file mode 100644 index 0000000000000000000000000000000000000000..300f2c6575bfa6a54dd149c35234f7888e863eec GIT binary patch literal 848 zcmV-W1F!svP)z@;j|==^1poj5AY({UO#lFTCIA3{ga82g0001h=l}q9FaQARU;qF* zm;eA5aGbhPJOBUy32;bRa{vG?BLDy{BLR4&KXw2B0=Y>Cap%$T`KnpDwJH5=b z3@xQFl+sR%iyA^=0Hy85@LzprbAI`MmvcVo=;#R4Q!uErE&=CCbsVj({oI#x0066% z)#~3=v9E5Q&tKyd;W*}STS6Jlb*5h*^*M$XjBLSzkKoBYGok^s@4Gj06`Fgp|Oo+)F>PbMiix7RLX*| z$#E8)j;sNoA_B+FW*Wy-@wiktEN-Q;nM|5zd!U3|G^pB z-IXUM+(x6`$+Z{^`e-!yV>UlJ>N(68)M~V&!&0q-gM)mbP>jVShH2{S>m3+)_~3pQ z3`1k%b0eR>&3PB91&|~`(*}-XF-%kGT?c2qd*?a=Lo+igLOhvBNL8gsuh;eW-va=U zt!0mst-0000z@;j|==^1poj5AY({UO#lFTCIA3{ga82g0001h=l}q9FaQARU;qF* zm;eA5aGbhPJOBUy32;bRa{vG?BLDy{BLR4&KXw2B1#C$~K~zY`wU$Y56h{=t->d5G z>6z}?dptHU9@`ibupwZ;EOr7hJ7Q5#Zb*^BA)JDgFTj-}pMgvA1)?M<0s&4$5DE^M z31p1zvBw_93-)-H?&<2Tt{iL$g9tnCP*NSLe)YD0JrqK~$20;T*`$>lFFz?P3!_nz zFg`rM%TK!Z?(=Impk`uFhh#!Nb%J)R`(OZ)S|b6Fc+9=|yBNvcygRs;bKbsI`Rerh z0B|YEyjCj>3=qFRw0%3(H0R21HOtBlXFsKKK07Rb^L2V>&wF_=bNRAcEDFb|XEJJM zXQ*$F`{(Vsp$zoy0l(`0^t0&dLM`xK9z3~psr=~C=+md7Uf;BSeW16mzEILeM(3wz z(TJyyACr$9dUwE*vDw+;rAvPm3LW$FencykBC(h`J3o2%-r>`yq|f_Ed;8lK7{0X8 zc&#>d@nUIkP}FLAPmg^45A8s|l3D|`8Xh?$ClbD7@*M$gxy)u}oTpDc+ZK$u)6>>3 zzse)y5DwNOL8ZwNuYg@KRq0n0-xjr>zPfXZ_f>*8D4<5J=A3~)fY#U=SG4#Mq zBt(UyOq_V!u`G7wij+*gDPZB+H8wfv7K` z?&|XO_9`tcKU}@~(@mrOG*ME=pECPFOR_6GuWfIuWd41)xNQe)#_E~gsCs*M|K;c$0%_X~Cn8_H? zXriHk5z^Gsva7fEWsPQ+71D3Y>55@6Y9O zQ}w#9xjCIq@7uSJUcbI$$Bvnq8Pha7J3C)46p?4H?eA*uVeA*w}`?KFjk~#`^yOu^1Qz8pcx910n!`5J1TCy0rDY<;)9# z!60pGyRWLNdwRO|?tQhgN?;fu%g=6K>SRfRdL0x67!!cIJ$>b*s91^-3xB97izbIS!F!R1_l1Zn^Bt&kNV3x-Q4#uLC3!z!+_5 ziJm*BHa9EDB$4IkWjo^tRnyb002ovPDHLkV1oP#zoP&E literal 0 HcmV?d00001 diff --git a/assets/dungeon lord.png b/assets/dungeon lord.png new file mode 100644 index 0000000000000000000000000000000000000000..04f7cd0c16901044004b82b1ef2043db466bebc8 GIT binary patch literal 1555 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 RqBQ^j002ovPDHLkV1i!d@A?1$ literal 0 HcmV?d00001 diff --git a/assets/house questgiver.png b/assets/house questgiver.png new file mode 100644 index 0000000000000000000000000000000000000000..55a9bcfbecfeb7ab5fca1b96fa85b3ef978d3d9a GIT binary patch literal 1130 zcmV-w1eN=VP)z@;j|==^1poj5AY({UO#lFTCIA3{ga82g0001h=l}q9FaQARU;qF* zm;eA5aGbhPJOBUy32;bRa{vG?BLDy{BLR4&KXw2B1Jg-FK~zY`wUx_HV|5hA&+m0_ z?`?0PEz(kCrW8;!Iw6vIBqm?5`!&Mf7|m*KKVXQ4naitKOet&tfYwp84o4n)bWJtiGNPDiWqla z2+(OodmG)$ok&ZZS;>{^x%pQ;ZavD9eJyu|y;f~}{z3qw%)t|1hK6$R53=UFo0XRz zkysOAS-G|H+$S$;5filePVwy5kTGEPY2hVbrZ$?-7FX|cK}X4e+#Ir2#&3tZ-8tMG z96g2Z3Ai;@UClSvre0aEOdw>&>2$@C{dl<<3uMn?KJ36B-v;G{+=wk2K_;abMNS^> zF6ot3%@|NYCRWqGg)orPadBZ$=Sz$=s#Ejchye26i)M_vyws0s#8t1OSNR5ZfNzqMH;bX|yy-8kuSp#*n0fL$41cXETe{n%*c)y-7j89q(>* zJ4bQS*I8hMORWMWOr`_>&iX>F=}o|*gYW$CSn3RzMV3F@@h{3M7W1n|PO&+5O_ekU zzjQahZk-(|GI4&i3Q0D3Aa! zJ?pHjm&;4GNzpC)ox?j`t<^s)7=4ul+Gy0uj0bS1gj406y=3c~%YHisfMG9-t`#8c zKD?I&wy-1-#4bd={WKkF%Ek$q3p%${FdyOf56+*qyR*%FIFJ|n!@;3M1fbwn2FcI? zgw%J2{cEje#B4$p*pVks!hE@74AChf0&>&6h-dHSZ&TA!&3Wy;($8O9 zHQz5b-Yk}CP8`Uqo#cGGS)89TF&d50wJKJu+i!BG6>5n9P?>kRpzY)Er-$c9kB25f zG(c_QE%CQMtuNj$-*VAr=AYA^CAxQ%QURy{G!OxHkqL`U<{l_%TF?BXtEnK=B7_37 zN$P83a58{6kgcsy#|RXF01*TX@Ek=miHoMBIol8XRt5-E5Yzwwgo4gcNizXJ&`l42 z9LRwI0O3*Q5&!@KGBmk7p8y~U)zxn?BJcu5g@giO005;WYchaAJJAXOfpSQD#;tL} z01&*IRjIoZ0c0vfKmY=j*KM636@&;VR;{t`!M`S)#o9Px#07*qoM6N<$f>}@XMF0Q* literal 0 HcmV?d00001 diff --git a/assets/kite shield.png b/assets/kite shield.png new file mode 100644 index 0000000000000000000000000000000000000000..005f9291a8a4b64b14d406a7aec4201d3702e08a GIT binary patch literal 1403 zcmV->1%&#EP)z@;j|==^1poj5AY({UO#lFTCIA3{ga82g0001h=l}q9FaQARU;qF* zm;eA5aGbhPJOBUy32;bRa{vG?BLDy{BLR4&KXw2B1msCXK~zY`wU*m&)J7P^=bIVZ z##c{)FhJ>U5bD5cPs6h>cIUmQR`o}?_tEyj$~0hVQ8j2}JPmGW7q zQ{Udc!nr64Nhzfi2aTHVI~W7TfDk%<9GnXP*x&E>4o939hle8}7z9I6ltM_1VKSMxuG8<2mXVal@K_J_0fn65qchvVh5=* zPC|v$(tKiBDFCu;V`e5e3!s$Jv|d@#D{D!w_mCc|Y(5R&^n=lJ%F$>Gp!hGR`H zy0laZGZwkda590l)vo6`X8~%SU6zIG>PkTv#J2s^^L$;WX`0Wpo3G!@Ze8D%vONCK z00;myjTVIzLNca*``ryqqi01$&37p!Ceu96isJ-;3X!L2{=p6@r9Qf2j4=iwib^TY zlbbu2UB^Bri)@Rv+l}!gwJbx|X;oEmoK{u!-c}*oW`;qJ-EG@I2mt_6$oK6B557M? z>a}ElpCN=Si%D5hij`7?l$Te#hM`{+V44gAa$OSuT-Q1}8Uo;|Dh(r_#LQr+$@Ix2 z1%N!4rfEdcWOlYW-NM=2T#MrvA=PfTMIlVn;5`#q%Nv+$7MOjgl_wIci1huJ)*QwHVeP*Wd>+W7vRRD1N_Lb%3ZW#C{LjThN0I1n) zG(&GVimz`jZ)_}|`)p1+oZGI2DEj@Olx3~v)@swaUkLD-`VaPrG6;lrkz@;j|==^1poj5AY({UO#lFTCIA3{ga82g0001h=l}q9FaQARU;qF* zm;eA5aGbhPJOBUy32;bRa{vG?BLDy{BLR4&KXw2B18hk|K~zY`?UvbY+eQ?I&rsx$ zlw_LPEjxjdD3%nfs1Qp=8lXtg4PDX)={qFPlZ$RZ+soXvNsGpC5w}QfYm1aRmMO`S zwUZ+*GreHo254NnGJI3yzd9g6emMX6d4`BF2Inab&RMSkCyGDI!~Oo)EmYNiE`Y{< z=ip%A^GVBBmyTnc$)YJS2{HS<@o1!C*vbnL|*7k>k{&+a-_YMtx^88u- zZ;MQuI*dAOYTC1OpzC_C+tmrNOmkjQc6WDUv1l+H0f0(n&$R4bZ_I!JfMuNyKn%kz zBv}Rk0b>CRgTZJt9;vE&VQC3*$k25F7*7mM>rEz;y}dsbMMY@w;ln4AjKd-2^5yuk zfQ|J>Ps>G~=aZ@Q&9`r*GuP_Xie;LwT!@TON*QB}F=#e57F!sO^l(Ty!J=NL!+>?# z?VUfKNV3c!P7Owzt-ftr%(4AGE*J^{fHCe)^#K;-5&8*e49 zBrz6Wm;kJOXp*U8+mte3l#!{^(1!Q!-S4#99uF^y9xMtX=0!<3Zpi4L{uZ~lp4Dp2 z#vbC({U2(UO&I`V3;_T_0Kg%*kzdSaQ_=V(Nmc+L6jCA)^|Ud~tYkAQS+}ll+Bl$20TBQ!%Pehg^?SV;0RdI5 z*6PJ_+3hdBbn&CxpCv9OQkhg_A&w9NfXQh1-Pd3KzWI0-Kyo>i%Uur!gIcQ>)uUVU^J=cJR(e{C xgd=J&JmZjEwOcK>c=?3+uR^bX9smD;zW`Fdq!n@4xitU)002ovPDHLkV1f=t+2;TN literal 0 HcmV?d00001 diff --git a/assets/mysterious figure.png b/assets/mysterious figure.png new file mode 100644 index 0000000000000000000000000000000000000000..1db619a25cc7a4634e70dfb82e36bf8ba6ea319f GIT binary patch literal 1233 zcmV;?1TOoDP)z@;j|==^1poj5AY({UO#lFTCIA3{ga82g0001h=l}q9FaQARU;qF* zm;eA5aGbhPJOBUy32;bRa{vG?BLDy{BLR4&KXw2B1UgAXK~zY`t=36zR7Vg1;HrN8 zwpl!jv6(T3D8vK|he#lVL~_ps@lz8eHRd*o~!H1XDrc?$1T+UpV!`as-&P0(o_-8nqiA!6JZnxR#G+o#El7QCc z_b11PuVZ7W-SWHL%C&13u3lZgb#wa*0E0pF=Zl^t$TFie2_Z+L?C<^Phx=)}Rqb|L z=LP0-@zc+*T#iA&0;^LAr6MBwV{fu`X^nGdV`FV~AW8D+G_6*65U_w!OouUsBhhdW zGpQdv`uE~y{c`~Jp$#GOmj~l^+dnzUT^+T<;bvvptuC}CV{De0(Kz{>EBYX?T*@>p zm&@`}$3rcEC~!r{3*Rxu5CNn~aV`J=l0>Dc0>I2xHTeuQXIc|_E*fp~LMvrf2Pzdm z3^@S4I*LF4%F+x?v3ulhu^r|-{1djd}3Z4LR19Ybxx*fzkiUVN=kdKKG@o7UH>-w z{{Hn!g&!a9)oR-a8@^Ay{(h_3>2|IF;(4Enot=xjyR*aB%e98Md&ht9;MU{E`<&lZ znyOV^3Egv55dh@58Vur6*{$xC+qb`~RBEku*I4pg^gPDcxg(fNmM;%x2%xn6<=0;S z-4g(4HacPGOlL(==p@PO^b$cMvamKy?aIZF~9F9&3ev7`;PTK@Cr10nL&ujsfp zefqSA2#8=TSps9#V%Y}(rR;K9N~we>RtKij^v&CZAtHd$B;~XcwyZS(U=2y3vrMm^ zWTi|dmE#~|Xe=pheXm}Y!gE8d3j#2f;#92;q-mb#n!C(eGMbd4vP_CR=G-fb6cLS~ zA~#x_6@l3-8IO|->j4p2g45BU(cDrhlUa1yVKOsuoUd4>^SSK5OS_$_F;EuPVGu>V zvPe2xHvzzL&{`@BGoQ;4%5pf$3T1@Q$N&Jsu;B-FhDa8O00E4J$s}0?m`s*? z&)!|$t{xr5MQITc5D0(>{GguaF%bcxF*Kftet&jmAd2K{mJLU8J}&`)2&@GlK!hL& zRG9$*0wBV{!D292o*59bkkSBw*2En|1SzLsxW>7Mh};deDF6Wg2;k_=>?2>>`+?yw zi56v=YKCAeA%IfL!SF9+UY?n?MzbgqAP9U`78dC2If&*`YtrWb0!0RC61Q4kZCtnp z2n?(9Sx?BRTYkh zJ~FWK_tvN%IE2W700000NkvXXu0mjfjTtlN literal 0 HcmV?d00001 diff --git a/assets/ogre.png b/assets/ogre.png new file mode 100644 index 0000000000000000000000000000000000000000..ccdb0ab1ecc1000e6a47b1a3abf409e49104f49c GIT binary patch literal 1980 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 literal 0 HcmV?d00001 diff --git a/assets/peasant1.png b/assets/peasant1.png new file mode 100644 index 0000000000000000000000000000000000000000..959b8bea9da5db13fed9b4613a8c65976784efbd GIT binary patch literal 1065 zcmV+^1lIeBP)z@;j|==^1poj5AY({UO#lFTCIA3{ga82g0001h=l}q9FaQARU;qF* zm;eA5aGbhPJOBUy32;bRa{vG?BLDy{BLR4&KXw2B1CmKZK~zY`wU$e78#fSuhvYuA zTCF5Yl_lG?lm@9A`SS%q@BTCZ!O!LvfLBaF9a(O9$Qff9H zN8@p8cej+L7Jx#Wgkf(u>>MB0@7-G#004}!Vy(7TbiXmyth@^V!E9D77Toj@pc|$9sD}A0Ga2aF8fj`tJL3t+o{4V|;l13NHWVT&#KCRS;~| zYEq_!$KP_#1J0M#6adUx&50H{!vuh>S`7e%X;G4Z=dGsDHoy9sZ*Cv}01#0WN3kiG z<1Cwle?{t#bQ}YKG%dsP03h$WE2w0x)WrA0>%_^Kwv!Khf9n7B`>Vl^Kkcso5Jyou zpSNGW{N=ZqV^i6k&tsiRo^x}HI9@>|_bx7u58D?PbDLrv)1RTo8EWJYD}!XQSQaqx z{a&XtdVl7kc5-@>ZdL5iGX{v5#^y8#Mpsu~G#YmV#8K2eKkuBJFwSk;)=I5A0TH1# zNJ`Uj{&?|%h!*pCp;#|+w(AmNCMg3jY`flSImP1n@$s+E zo?Xvox2}WtXJ=>a_GB=~mrA?)`xV>HBZ^FF&KW?Z(RlRa32N<@N_Tmp*?2tcb{Xf} zJ3Gw>5B$?pAVNf742S^0w5*M0(|h_907`3Xw+0B#Eyt<1TFspu$8}99xAXaTaZEz6 z>^*dVh<^B7OW)lFa^?Ep&q=vzW7-NLUI0wf1oldZ~x4yY~M?ke+|G1ZF zDi;fzF(HIb5^h;QG#w73FkH=4A=5Pbh*w#h?JCCN|hvth)Ss>@%#PRbh;cM48tH$TF=r{ONm-5 zDKn{1>&alyJ3n8lAB;yM##ksNW6Us3V2lunh!C?i8;_R)IIgRe;zDpC3;-o%p;F2E jsN;EFx!h>AZUp=TfTs&#hQ7tF00000NkvXXu0mjf=J(Z_ literal 0 HcmV?d00001 diff --git a/assets/peasant2.png b/assets/peasant2.png new file mode 100644 index 0000000000000000000000000000000000000000..838d913e099458449d5fca0c2403f02583ade323 GIT binary patch literal 1157 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 literal 0 HcmV?d00001 diff --git a/assets/rat.png b/assets/rat.png new file mode 100644 index 0000000000000000000000000000000000000000..617bdacfb0a44d85d6f0a047380434064c299e0f GIT binary patch literal 1135 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 literal 0 HcmV?d00001 diff --git a/assets/wooden shield.png b/assets/wooden shield.png new file mode 100644 index 0000000000000000000000000000000000000000..a1ca9e45a95483d06d9f01a6cc5bd9650787e061 GIT binary patch literal 1079 zcmV-71jze|P)z@;j|==^1poj5AY({UO#lFTCIA3{ga82g0001h=l}q9FaQARU;qF* zm;eA5aGbhPJOBUy32;bRa{vG?BLDy{BLR4&KXw2B1E5JnK~zY`<(5lt97Pbvt6%fl znc3OhS--U;jx1y_5tj%gPJ9N=d;-22C$11KT!2W302Xqb7}+@MN4(zGc&4Xc#lhGL z1O?4Xxj-FTE&Zyy{`IdShzM^oD7@kLPXZ7TUMdVCYOV1mngaN%M_MaHv_@;K0l+z& zPp9oHW1P3*1OTit2tWv-loCQ-4g60!pao`9}wPkH7sgY_*T}59XIwX}9(8i_do+d~kaF zV*CF6AHV-PYUP7_cRRiQgO5IL1^_~|Rzmnj>59(1F!dvpQeI7qAHI3mS-M-xQkL>+ zb`r(DbJiNuSVgLqhfjZw(luG2R?b?hWktDgmJOfn$+|2iML8|3wS*9Bn*)rsUJ!dh z9L7t<xl$r7lj7gGd;y!zdYFoH&P#0gN)rXq;w*&@f5R*(A#eC9SlJ zsEx5+5LCrn)fE7sbBzInQUHk4R4Y}NWt?@3*@$t04hbPjmP>1Q0I`-OI$Wre5nD;; ze0YRt5fKPDYn|2BA)(9~-N}~`LDh0WtDuw?v&*abm;kZHStHR|bk1rEj&R}0vUFB+ zE*1qa7KCvt=^y|Xei$clmK!6Tbs`jud)Aqo&m@Zg7~}obL0L?=5CljRCyaZHF=y?| z|9I^y*9B_^5T3Ych?=m!-}zHE2vs@Dy32sgRYVDG#VG(7Ra;}czy|z@;j|==^1poj5AY({UO#lFTCIA3{ga82g0001h=l}q9FaQARU;qF* zm;eA5aGbhPJOBUy32;bRa{vG?BLDy{BLR4&KXw2B1Q|(0K~zY`t(M(yQ*{`}KhJ4T z*Y>O{UCUV8BM5T_SOPW6GL3*SAt51N8hlCokNgMp&KNE*;nqk@FkGlejV#F6D|=yQ z>niQax}Co4^qj{FB$7BPD?GQq{POvIf8Xa#RFwln06qXAFka?Dm;;5*z%;br6mk+0 zuMx10R^eClCbXacEo2M^Uk5;!VF~>jk`RXs%%i{{@)`go_zk@dX~>lsfO1Lq3bF!= zXdF4-4yrB(DNXy4G|4dMt}H#Cf12M2au1>nIf>^NA^m#1w~F-e@2{2+~@ zhR~a^`w9SEM(3ag{+MKD^~n;47Z{so?6ky88sMXw;PnRZU=i**L?Q8)E)7Vqtj1Tt zy(z^fSC9;>!TPHJ9Pn(08mg<92IeX{Es;BHZZf<b%fFvgmE7_NL$w`sV^?T^VlBjuQys7^Y473A-l<4zp9&meVznx&U@S z9lQ)3bj~BH_z*IX5TQMShzx}AY^o16*;^Kw=xjyPDG?|~X|@ROsw&)NsX-)6L{R9A zh&bAd0vysAnYC2G_mCz=g+348M=qKbdv|Vl|34;kEkm{wIx@?1M5d{?_Oy5a4^fA_`Y+HKG;f;(;LmzShIo3;> zT9^fCKmtw(9h>MCN!YR$=XXdzeOeJJbCyhYpUhnVZRo&&t7}lQ7>A(- z$q7k;^E8iP9I^);nI3phgU3(;_Xn*_%4H-sr86JM7L*~epvkI?zbi)%xK-#flsy(< z$J9A!6*W33r){aDi%nuK8x|>egj`HX0fr6)Scf@Bzxz?;vV=UUYZ|MQo)8QD8T>uo zwCF$+axsp=|34p)XPcde-gERz$ip$%D(Tv35sQADdR)px?j>@)`O7c3=gXFam;n;gALX%h
-
- +
+
diff --git a/src/Entity.js b/src/Entity.js index e034f5b..f65be10 100644 --- a/src/Entity.js +++ b/src/Entity.js @@ -32,6 +32,7 @@ export class Player extends Entity { this.xp = 0; this.maxXp = 10; this.gold = 0; + this.poisoned = false; } gainXp(amount) { @@ -55,7 +56,7 @@ export class Player extends Entity { let dmg = Math.floor(this.stats.str / 5); // Base damage if (dmg < 1) dmg = 1; if (this.equipment.weapon) { - dmg += this.equipment.weapon.stats.damage || 0; + dmg += (this.equipment.weapon.stats.damage || 0) + (this.equipment.weapon.modifier || 0); } return dmg; } @@ -63,7 +64,7 @@ export class Player extends Entity { getDefense() { let def = 0; if (this.equipment.shield) { - def += this.equipment.shield.stats.defense || 0; + def += (this.equipment.shield.stats.defense || 0) + (this.equipment.shield.modifier || 0); } // Could add armor here later return def; @@ -91,6 +92,13 @@ export class Archer extends Monster { } } +export class Cobra extends Monster { + constructor(x, y, name, symbol, color, level) { + super(x, y, name, symbol, color, level); + this.poisonChance = 0.3; // 30% chance to poison on hit + } +} + export class Shopkeeper extends Entity { constructor(x, y, name, shopName, inventory) { super(x, y, name, '$', '#ffd700'); @@ -112,3 +120,53 @@ export class WanderingQuestGiver extends Entity { this.quest = null; // { target: string, required: number, current: number, completed: false } } } + +export class Farmer extends Entity { + constructor(x, y, name) { + super(x, y, name, 'f', '#ffa500'); // Orange 'f' + this.quotes = [ + "You're growing like a weed!", + "Potatoes! Boil 'em, mash 'em, stick 'em in a stew!", + "The Dungeon Lord steals our crops to feed his monsters!", + "That Dungeon Lord is nothing but trouble!", + "You look undernourished, have a carrot!" + ]; + } + + getRandomQuote() { + return this.quotes[Math.floor(Math.random() * this.quotes.length)]; + } +} + +export class Priest extends Entity { + constructor(x, y, name) { + super(x, y, name, 'P', '#ffffff'); // White 'P' + this.quotes = [ + "The sun's light blesses our crops.", + "I remember when you were just a baby, now you're a hero!", + "Come back if you get injured.", + "Blessings upon you.", + "Don't forget to say your prayers!" + ]; + } + + getRandomQuote() { + return this.quotes[Math.floor(Math.random() * this.quotes.length)]; + } +} + +export class Gravekeeper extends Entity { + constructor(x, y, name) { + super(x, y, name, 'G', '#808080'); // Gray 'G' + this.quotes = [ + "Guarding this place is a grave responsibility.", + "Hope you don't end up here!", + "Got this shovel from a lass named Rosella.", + "I won't let the Dungeon Lord's monsters disturb the dead!" + ]; + } + + getRandomQuote() { + return this.quotes[Math.floor(Math.random() * this.quotes.length)]; + } +} diff --git a/src/Game.js b/src/Game.js index 0680a5b..ac59b42 100644 --- a/src/Game.js +++ b/src/Game.js @@ -2,14 +2,24 @@ import { Map } from './Map.js'; import { UI } from './UI.js'; import { Player } from './Entity.js'; -import { Monster, Shopkeeper, QuestGiver, WanderingQuestGiver, Archer } from './Entity.js'; +import { Monster, Shopkeeper, QuestGiver, WanderingQuestGiver, Archer, Cobra, Farmer, Priest, Gravekeeper } from './Entity.js'; import { Item } from './Item.js'; export class Game { constructor() { - console.log("Game Version 1.3 Loaded - Persistent Levels"); + console.log("Game Version 1.4 Loaded - Asset Support"); this.canvas = document.getElementById('game-canvas'); this.ctx = this.canvas.getContext('2d'); + + // Handle resizing + this.resizeCanvas(); + window.addEventListener('resize', () => { + this.resizeCanvas(); + this.render(); + }); + + this.assets = {}; + this.loadAssets(); this.ui = new UI(this.ctx, this); // Pass game instance to UI for callbacks this.player = new Player(10, 10); this.monsters = []; @@ -21,8 +31,33 @@ export class Game { this.dungeonLevels = {}; // Persistent storage for each depth this.tileSize = 32; + this.mapWidth = 80; + this.mapHeight = 80; - this.gameState = 'PLAY'; // 'PLAY', 'SHOP', 'TARGETING', 'INVENTORY' + this.gameState = 'INTRO'; // 'INTRO', 'PLAY', 'SHOP', 'TARGETING', 'INVENTORY', 'OVERVIEW' + this.storyText = [ + "Once upon a time the people of Oakhaven found a baby left", + "on the temple steps--you! The villagers raised you as", + "their own, always wondering what mysterious traveler had", + "blessed them with a child. You grew up strong and brave,", + "working hard in the fields and protecting the town from", + "wolf packs and bandits. After one particularly tough", + "fight, your friends even started calling you Hero!", + "", + "Recently a solar eclipse darkened the sky. As the", + "villagers stood watching this cosmic event, a forgotten", + "dry well in the corner of town burst open and monsters", + "swarmed out! They declared that Oakhaven was now ruled", + "by the Dungeon Lord, who would be taking whatever he", + "wanted from the fields and the shops! Under this", + "oppression food is becoming scarce and the situation", + "will soon be desperate. Of all villagers, you have the", + "best chance to defeat the Dungeon Lord and free your home!", + "", + "Take up arms, and become the Hero you were destined to be!", + "", + "[ Click anywhere to start your adventure ]" + ]; this.currentShopkeeper = null; this.shopSelection = 0; this.shopMode = 'BUY'; // 'BUY' or 'SELL' @@ -45,6 +80,90 @@ export class Game { // Bind input window.addEventListener('keydown', (e) => this.handleInput(e)); + this.canvas.addEventListener('mousedown', (e) => this.handleCanvasClick(e)); + + // Start real-time timers + this.startPoisonTimer(); + } + + loadAssets() { + const assetNames = [ + 'archer', 'axe', 'buckler', 'cobra', 'dagger', 'dragon', + 'dungeon lord', 'fountain', 'hero', 'house questgiver', + 'kite shield', 'kobold', 'mysterious figure', 'ogre', + 'orc', 'peasant1', 'peasant2', 'questgiver outside', + 'rat', 'skeleton archer', 'spellbook', 'sword', + 'wooden shield', 'zappy laser' + ]; + + assetNames.forEach(name => { + const img = new Image(); + img.src = `assets/${name}.png`; + img.onload = () => { + this.assets[name] = img; + this.render(); // Re-render when an asset loads + }; + }); + } + + resizeCanvas() { + const container = document.getElementById('viewport-container'); + if (container) { + this.canvas.width = container.clientWidth; + this.canvas.height = container.clientHeight; + } + } + + startPoisonTimer() { + setInterval(() => { + if (this.player && this.player.poisoned && !this.gameOver) { + const damage = Math.max(1, Math.floor(this.player.maxHp * 0.05)); // 5% of max HP + this.player.hp -= damage; + this.ui.log(`The poison burns... You lose ${damage} HP.`); + this.ui.updateStats(this.player, this.depth); + + if (this.player.hp <= 0) { + this.ui.log("You have succumbed to the poison!"); + this.ui.log("GAME OVER"); + this.gameOver = true; + setTimeout(() => alert("Game Over! Refresh to restart."), 100); + } + this.render(); + } + }, 30000); // Every 30 seconds + } + + updateExplored() { + if (!this.map || !this.map.explored) return; + + const radius = 3.5; + const startX = Math.max(0, Math.floor(this.player.x - radius)); + const endX = Math.min(this.map.width - 1, Math.ceil(this.player.x + radius)); + const startY = Math.max(0, Math.floor(this.player.y - radius)); + const endY = Math.min(this.map.height - 1, Math.ceil(this.player.y + radius)); + + for (let y = startY; y <= endY; y++) { + for (let x = startX; x <= endX; x++) { + const dist = Math.sqrt(Math.pow(this.player.x - x, 2) + Math.pow(this.player.y - y, 2)); + if (dist <= radius) { + // Check LOS for exploration too, so we don't explore through walls + if (this.map.hasLineOfSight(this.player.x, this.player.y, x, y)) { + this.map.explored[y][x] = true; + } + } + } + } + + // Also explore anything permanently lit + if (this.map.permanentlyLit) { + for (let y = 0; y < this.map.height; y++) { + for (let x = 0; x < this.map.width; x++) { + if (this.map.permanentlyLit[y][x]) { + this.map.explored[y][x] = true; + } + } + } + } } start() { @@ -65,8 +184,8 @@ export class Game { // Reposition player if (this.depth === 0) { if (direction === 'up') { - this.player.x = 43; - this.player.y = 8; + this.player.x = 63; + this.player.y = 13; } else { this.player.x = Math.floor(this.map.width / 2); this.player.y = Math.floor(this.map.height / 2); @@ -87,7 +206,7 @@ export class Game { } else { // Generate NEW level this.items = []; - this.map = new Map(50, 50); + this.map = new Map(this.mapWidth, this.mapHeight); if (this.depth === 0) { this.map.generateTown(); @@ -119,6 +238,7 @@ export class Game { } this.spawnMonsters(); // Always respawn monsters + this.updateExplored(); this.render(); this.ui.updateStats(this.player, this.depth); @@ -169,8 +289,8 @@ export class Game { spawnStairs() { if (this.depth === 0) { // Town: Stairs Down in the Dungeon Entrance building - const sx = 43; // Center X - const sy = 8; // Center Y + const sx = 63; // Center X + const sy = 13; // Center Y this.map.tiles[sy][sx] = '>'; return; } @@ -214,24 +334,29 @@ export class Game { { item: new Item("Buckler", "shield", 1, { defense: 1 }), price: 40 }, { item: new Item("Wooden Shield", "shield", 2, { defense: 2 }), price: 80 } ]; - const weaponSmith = new Shopkeeper(41, 18, "Smith", "Weapon Shop", weaponShopInventory); + weaponShopInventory.forEach(entry => entry.item.identified = true); + const weaponSmith = new Shopkeeper(59, 33, "Smith", "Weapon Shop", weaponShopInventory); this.monsters.push(weaponSmith); // General Store (West) const generalStoreInventory = [ { item: new Item("Potion", "potion", 1, { heal: 5 }), price: 20 }, + { item: new Item("Antidote", "antidote", 1, {}), price: 30 }, { item: new Item("Spellbook: Fireball", "spellbook", 1, { spell: "Fireball" }), price: 300 }, { item: new Item("Spellbook: Light", "spellbook", 1, { spell: "Light" }), price: 200 }, { item: new Item("Spellbook: Heal", "spellbook", 1, { spell: "Heal" }), price: 400 }, - { item: new Item("Spellbook: Return", "spellbook", 1, { spell: "Return" }), price: 500 } + { item: new Item("Spellbook: Return", "spellbook", 1, { spell: "Return" }), price: 500 }, + { item: new Item("Spellbook: Detect Traps", "spellbook", 1, { spell: "Detect Traps" }), price: 150 }, + { item: new Item("Spellbook: Cure Poison", "spellbook", 1, { spell: "Cure Poison" }), price: 250 } ]; - const merchant = new Shopkeeper(9, 18, "Merchant", "General Store", generalStoreInventory); + generalStoreInventory.forEach(entry => entry.item.identified = true); + const merchant = new Shopkeeper(19, 33, "Merchant", "General Store", generalStoreInventory); this.monsters.push(merchant); // Spawn Quest Givers in Houses - const q1 = new QuestGiver(13, 32, "Villager", "Please bring back my Golden Locket! It was stolen by an orc!"); - const q2 = new QuestGiver(25, 32, "Old Man", "Please bring back my Silver Chalice! It was stolen by an orc!"); - const q3 = new QuestGiver(37, 32, "Woman", "Please bring back my Ruby Ring! It was stolen by an orc!"); + const q1 = new QuestGiver(23, 57, "Villager", "Please bring back my Golden Locket! It was stolen by an orc!"); + const q2 = new QuestGiver(40, 57, "Old Man", "Please bring back my Silver Chalice! It was stolen by an orc!"); + const q3 = new QuestGiver(57, 57, "Woman", "Please bring back my Ruby Ring! It was stolen by an orc!"); this.monsters.push(q1, q2, q3); // Spawn Wandering Quest Givers @@ -247,6 +372,16 @@ export class Game { this.monsters.push(wqg); } + // Spawn Farmers near fields + this.monsters.push(new Farmer(21, 19, "Farmer Giles")); + this.monsters.push(new Farmer(54, 48, "Farmer Maggot")); + + // Spawn Priest in Temple + this.monsters.push(new Priest(40, 11, "Father Sun")); + + // Spawn Gravekeeper in Graveyard + this.monsters.push(new Gravekeeper(11, 9, "Mort")); + return; } @@ -273,7 +408,8 @@ export class Game { else monster = new Archer(mx, my, "Skeleton Archer", "s", "#eeeeee", 3); } else if (this.depth <= 7) { if (type < 0.2) monster = new Monster(mx, my, "Kobold", "k", "#00ff00", 2); - else if (type < 0.5) monster = new Monster(mx, my, "Orc", "o", "#008000", 3); + else if (type < 0.4) monster = new Monster(mx, my, "Orc", "o", "#008000", 3); + else if (type < 0.6) monster = new Cobra(mx, my, "Cobra", "c", "#ffff00", 4); else if (type < 0.8) monster = new Monster(mx, my, "Ogre", "O", "#ff0000", 5); else monster = new Archer(mx, my, "Elite Archer", "S", "#ffffff", 5); } else { @@ -287,6 +423,7 @@ export class Game { // Set Monster Speeds if (monster.name === "Rat") monster.speed = 1.0; else if (monster.name === "Kobold") monster.speed = 1.0; + else if (monster.name === "Cobra") monster.speed = 0.9; else if (monster.name === "Orc") monster.speed = 0.8; else if (monster instanceof Archer) monster.speed = 0.7; else if (monster.name === "Ogre") monster.speed = 0.5; @@ -330,6 +467,7 @@ export class Game { if (Math.random() < 0.2) tier++; // Chance for higher tier if (type < 0.3) item = new Item("Potion", "potion", 1, { heal: 5 + this.depth * 2 }); + else if (type < 0.35) item = new Item("Antidote", "antidote", 1, {}); else if (type < 0.45) { if (tier <= 1) item = new Item("Dagger", "weapon", 1, { damage: 4 }); else if (tier === 2) item = new Item("Short Sword", "weapon", 2, { damage: 6 }); @@ -348,14 +486,18 @@ export class Game { } else if (type < 0.8) { // Spellbooks const spellRoll = Math.random(); - if (spellRoll < 0.25) { + if (spellRoll < 0.16) { item = new Item("Spellbook: Fireball", "spellbook", 1, { spell: "Fireball" }); - } else if (spellRoll < 0.5) { + } else if (spellRoll < 0.32) { item = new Item("Spellbook: Light", "spellbook", 1, { spell: "Light" }); - } else if (spellRoll < 0.75) { + } else if (spellRoll < 0.48) { item = new Item("Spellbook: Heal", "spellbook", 1, { spell: "Heal" }); - } else { + } else if (spellRoll < 0.64) { item = new Item("Spellbook: Return", "spellbook", 1, { spell: "Return" }); + } else if (spellRoll < 0.8) { + item = new Item("Spellbook: Detect Traps", "spellbook", 1, { spell: "Detect Traps" }); + } else { + item = new Item("Spellbook: Cure Poison", "spellbook", 1, { spell: "Cure Poison" }); } } else { item = new Item("Potion", "potion", 1, { heal: 5 + this.depth * 2 }); @@ -363,6 +505,20 @@ export class Game { item.x = ix; item.y = iy; + + // Roll for Enchantment / Curse (20% enchanted, 20% cursed) + if (item.type === 'weapon' || item.type === 'shield' || item.type === 'armor') { + const roll = Math.random(); + if (roll < 0.2) { + // Enchanted + item.modifier = Math.floor(Math.random() * 3) + 1; + } else if (roll < 0.4) { + // Cursed + item.modifier = -(Math.floor(Math.random() * 2) + 1); + item.isCursed = true; + } + } + this.items.push(item); } } @@ -371,6 +527,12 @@ export class Game { handleInput(e) { if (this.gameOver) return; + if (this.gameState === 'INTRO') { + this.gameState = 'PLAY'; + this.render(); + return; + } + if (this.gameState === 'SHOP') { this.handleShopInput(e); return; @@ -381,6 +543,14 @@ export class Game { return; } + if (this.gameState === 'OVERVIEW') { + if (e.key === 'm' || e.key === 'M' || e.key === 'Escape') { + this.gameState = 'PLAY'; + this.render(); + } + return; + } + if (this.gameState === 'TARGETING') { this.handleTargetingInput(e); return; @@ -405,6 +575,11 @@ export class Game { case 'NumPad3': dx = 1; dy = 1; handled = true; break; case '.': case 'NumPad5': handled = true; break; // Wait case 'i': case 'I': this.toggleInventory(); handled = true; break; + case 'm': case 'M': + this.gameState = 'OVERVIEW'; + this.render(); + handled = true; + break; case 'Enter': case '>': if (this.map.tiles[this.player.y][this.player.x] === '>') { @@ -451,6 +626,16 @@ export class Game { else this.ui.log("You don't know that spell!"); handled = true; break; + case '6': + if (this.player.spells.includes('Detect Traps')) this.castDetectTrapsSpell(); + else this.ui.log("You don't know that spell!"); + handled = true; + break; + case '7': + if (this.player.spells.includes('Cure Poison')) this.castCurePoisonSpell(); + else this.ui.log("You don't know that spell!"); + handled = true; + break; } if (handled) { @@ -465,18 +650,18 @@ export class Game { handleShopInput(e) { e.preventDefault(); - const inventory = this.shopMode === 'BUY' ? this.currentShopkeeper.inventory : this.player.inventory; + const list = this.shopMode === 'BUY' ? this.currentShopkeeper.inventory : this.getGroupedInventory(); switch (e.key) { case 'ArrowUp': case 'NumPad8': this.shopSelection--; - if (this.shopSelection < 0) this.shopSelection = inventory.length - 1; + if (this.shopSelection < 0) this.shopSelection = list.length - 1; break; case 'ArrowDown': case 'NumPad2': this.shopSelection++; - if (this.shopSelection >= inventory.length) this.shopSelection = 0; + if (this.shopSelection >= list.length) this.shopSelection = 0; break; case 'Tab': this.shopMode = this.shopMode === 'BUY' ? 'SELL' : 'BUY'; @@ -485,9 +670,12 @@ export class Game { case 'Enter': case ' ': if (this.shopMode === 'BUY') { - this.buyItem(inventory[this.shopSelection]); + this.buyItem(list[this.shopSelection]); } else { - this.sellItem(this.shopSelection); + if (list[this.shopSelection]) { + const actualIndex = list[this.shopSelection].indices[0]; + this.sellItem(actualIndex); + } } break; case 'Escape': @@ -522,25 +710,141 @@ export class Game { this.render(); } + getGroupedInventory() { + const groups = []; + this.player.inventory.forEach((item, index) => { + const group = groups.find(g => g.name === item.name && g.type === item.type && JSON.stringify(g.stats) === JSON.stringify(item.stats)); + if (group) { + group.count++; + group.indices.push(index); + } else { + groups.push({ + name: item.name, + type: item.type, + stats: item.stats, + item: item, // Representative item + count: 1, + indices: [index] + }); + } + }); + return groups; + } + + handleCanvasClick(e) { + if (this.gameOver) return; + + if (this.gameState === 'INTRO') { + this.gameState = 'PLAY'; + this.render(); + return; + } + + // Get relative mouse coordinates on canvas + const rect = this.canvas.getBoundingClientRect(); + const mouseX = e.clientX - rect.left; + const mouseY = e.clientY - rect.top; + + if (this.gameState === 'TARGETING') { + // Calculate which tile was clicked + const viewWidth = Math.ceil(this.canvas.width / this.tileSize); + const viewHeight = Math.ceil(this.canvas.height / this.tileSize); + const startX = this.player.x - Math.floor(viewWidth / 2); + const startY = this.player.y - Math.floor(viewHeight / 2); + + const clickedMapX = Math.floor(mouseX / this.tileSize) + startX; + const clickedMapY = Math.floor(mouseY / this.tileSize) + startY; + + // Calculate direction from player to clicked tile + const dx = Math.sign(clickedMapX - this.player.x); + const dy = Math.sign(clickedMapY - this.player.y); + + if (dx !== 0 || dy !== 0) { + this.castSpell(this.targetingSpell, dx, dy); + this.gameState = 'PLAY'; + this.targetingSpell = null; + this.render(); + } + } else if (this.gameState === 'PLAY') { + // Click player to toggle inventory + const viewWidth = Math.ceil(this.canvas.width / this.tileSize); + const viewHeight = Math.ceil(this.canvas.height / this.tileSize); + const startX = this.player.x - Math.floor(viewWidth / 2); + const startY = this.player.y - Math.floor(viewHeight / 2); + + const clickedMapX = Math.floor(mouseX / this.tileSize) + startX; + const clickedMapY = Math.floor(mouseY / this.tileSize) + startY; + + if (clickedMapX === this.player.x && clickedMapY === this.player.y) { + this.toggleInventory(); + } + + // Click signs + if (this.map.signs) { + const sign = this.map.signs.find(s => s.x === clickedMapX && s.y === clickedMapY); + if (sign) { + this.ui.showPopup(sign.text, 1000); + } + } + } else if (this.gameState === 'INVENTORY' || this.gameState === 'SHOP') { + // Check if clicking inside the box items + const width = 500; + const height = this.gameState === 'INVENTORY' ? 400 : 400; // Match UI.js dimensions + const bx = (this.canvas.width - width) / 2; + const by = (this.canvas.height - height) / 2; + + if (mouseX >= bx + 20 && mouseX <= bx + width - 20) { + const isSelling = this.gameState === 'SHOP' && this.shopMode === 'SELL'; + const isInventory = this.gameState === 'INVENTORY'; + + let list; + if (isInventory || isSelling) { + list = this.getGroupedInventory(); + } else { + list = this.currentShopkeeper.inventory; + } + + const startY = by + 70; + const clickedIndex = Math.floor((mouseY - startY + 5) / 30); + + if (clickedIndex >= 0 && clickedIndex < list.length) { + if (isInventory) { + const actualIndex = list[clickedIndex].indices[0]; + this.useItem(actualIndex); + } else { + if (this.shopMode === 'BUY') { + this.buyItem(list[clickedIndex]); + } else { + const actualIndex = list[clickedIndex].indices[0]; + this.sellItem(actualIndex); + } + } + this.render(); + } + } + } + } + handleInventoryInput(e) { e.preventDefault(); - const inventory = this.player.inventory; + const groupedItems = this.getGroupedInventory(); switch (e.key) { case 'ArrowUp': case 'NumPad8': this.inventorySelection--; - if (this.inventorySelection < 0) this.inventorySelection = inventory.length - 1; + if (this.inventorySelection < 0) this.inventorySelection = groupedItems.length - 1; break; case 'ArrowDown': case 'NumPad2': this.inventorySelection++; - if (this.inventorySelection >= inventory.length) this.inventorySelection = 0; + if (this.inventorySelection >= groupedItems.length) this.inventorySelection = 0; break; case 'Enter': case ' ': - if (inventory[this.inventorySelection]) { - this.useItem(this.inventorySelection); + if (groupedItems[this.inventorySelection]) { + const actualIndex = groupedItems[this.inventorySelection].indices[0]; + this.useItem(actualIndex); } break; case 'Escape': @@ -561,9 +865,10 @@ export class Game { // Copy visual props newItem.symbol = entry.item.symbol; newItem.color = entry.item.color; + newItem.identified = true; // Shop items are identified this.player.inventory.push(newItem); - this.ui.log(`You bought ${entry.item.name} for ${entry.price} gold.`); + this.ui.log(`You bought ${newItem.getDisplayName()} for ${entry.price} gold.`); this.ui.updateInventory(this.player); this.ui.updateStats(this.player, this.depth); } else { @@ -581,8 +886,15 @@ export class Game { return; } + // Cannot sell cursed items while equipped + if (item.isCursed && item.identified) { + if (this.player.equipment.weapon === item || this.player.equipment.shield === item || this.player.equipment.armor === item) { + this.ui.log("You cannot sell a cursed item you are currently wearing!"); + return; + } + } + // Base price calculation (1/4 of buy price) - // We'll estimate price based on item level/type if it doesn't have a value let baseValue = 40; // Default if (item.type === 'weapon') baseValue = 100 * item.level; if (item.type === 'shield') baseValue = 40 * item.level; @@ -598,7 +910,7 @@ export class Game { this.player.gold += sellPrice; this.player.inventory.splice(index, 1); - this.ui.log(`You sold ${item.name} for ${sellPrice} gold.`); + this.ui.log(`You sold ${item.getDisplayName()} for ${sellPrice} gold.`); // Reset selection if last item was sold if (this.shopSelection >= this.player.inventory.length) { @@ -655,6 +967,49 @@ export class Game { this.ui.showPopup(msg, 2000); this.ui.log(`${targetMonster.name} says: "${msg}"`); } + } else if (targetMonster instanceof Farmer) { + const quote = targetMonster.getRandomQuote(); + this.ui.showPopup(quote, 2000); + this.ui.log(`${targetMonster.name} says: "${quote}"`); + } else if (targetMonster instanceof Priest) { + // Father Sun's Identify & Uncurse services + const unidentifiedItems = this.player.inventory.filter(i => !i.identified); + const cursedEquipped = Object.values(this.player.equipment).filter(i => i && i.isCursed && i.identified); + + if (unidentifiedItems.length > 0) { + if (this.player.gold >= 50) { + this.player.gold -= 50; + unidentifiedItems.forEach(i => i.identified = true); + this.ui.showPopup("Father Sun identifies your mysterious items!", 2000); + this.ui.log(`Father Sun says: "The sun reveals the truth of your gear." (50g paid)`); + this.ui.updateStats(this.player, this.depth); + this.ui.updateInventory(this.player); + } else { + this.ui.log(`Father Sun says: "I would identify your gear, but you lack the 50 gold donation."`); + } + } else if (cursedEquipped.length > 0) { + if (this.player.gold >= 100) { + this.player.gold -= 100; + cursedEquipped.forEach(i => { + i.isCursed = false; + this.ui.log(`The curse is lifted from your ${i.name}!`); + }); + this.ui.showPopup("The curses are lifted!", 2000); + this.ui.log(`Father Sun says: "May you be free from these dark shackles." (100g paid)`); + this.ui.updateStats(this.player, this.depth); + this.ui.updateInventory(this.player); + } else { + this.ui.log(`Father Sun says: "I would lift your curses, but you lack the 100 gold donation."`); + } + } else { + const quote = targetMonster.getRandomQuote(); + this.ui.showPopup(quote, 2000); + this.ui.log(`${targetMonster.name} says: "${quote}"`); + } + } else if (targetMonster instanceof Gravekeeper) { + const quote = targetMonster.getRandomQuote(); + this.ui.showPopup(quote, 2000); + this.ui.log(`${targetMonster.name} says: "${quote}"`); } else { this.attack(this.player, targetMonster); } @@ -662,21 +1017,56 @@ export class Game { this.player.x = newX; this.player.y = newY; + // Check for trap + const trap = this.map.traps.find(t => t.x === newX && t.y === newY); + if (trap) { + trap.revealed = true; + const damage = Math.floor(this.depth * 1.5) + Math.floor(Math.random() * 3) + 2; + this.player.hp -= damage; + this.ui.log(`*SNAP* You triggered a trap! You take ${damage} damage.`); + this.ui.showPopup("TRAP!", 1000); + this.ui.updateStats(this.player, this.depth); + + if (this.player.hp <= 0) { + this.ui.log("The trap was lethal!"); + this.ui.log("GAME OVER"); + this.gameOver = true; + setTimeout(() => alert("Game Over! Refresh to restart."), 100); + } + } + + // Check for fountain + const fountain = this.map.fountains.find(f => f.x === newX && f.y === newY); + if (fountain && !fountain.used) { + if (this.player.hp < this.player.maxHp || this.player.poisoned) { + this.player.hp = this.player.maxHp; + this.player.poisoned = false; + fountain.used = true; + this.ui.log("You drink from the glowing fountain. You feel completely restored!"); + this.ui.showPopup("RESTORED!", 1000); + this.ui.updateStats(this.player, this.depth); + } else { + this.ui.log("You drink from the fountain. It's refreshing."); + } + } + // Check for item const item = this.items.find(i => i.x === newX && i.y === newY); if (item) { this.ui.log(`You see a ${item.name}. (Press 'g' to get)`); } } else { - console.log(`Blocked at ${newX},${newY}. Tile: '${this.map.tiles[newY][newX]}'`); - if (dx !== 0 || dy !== 0) this.ui.log("Blocked!"); + // Quietly blocked } + this.updateExplored(); + // Temple Healing (Town only) - if (this.depth === 0 && this.player.x === 25 && this.player.y === 12) { - if (this.player.hp < this.player.maxHp) { + if (this.depth === 0 && this.player.x === 40 && this.player.y === 14) { + if (this.player.hp < this.player.maxHp || this.player.poisoned) { this.player.hp = this.player.maxHp; - this.ui.log("You enter the temple and feel refreshed. HP fully restored!"); + this.player.poisoned = false; + this.ui.log("You enter the temple and feel refreshed. HP fully restored and poison cured!"); this.ui.updateStats(this.player, this.depth); } else { this.ui.log("You enter the temple. It is peaceful here."); @@ -790,6 +1180,7 @@ export class Game { } } } + this.updateExplored(); this.render(); } @@ -841,6 +1232,55 @@ export class Game { this.render(); } + castDetectTrapsSpell() { + const manaCost = 2; + if (this.player.mana < manaCost) { + this.ui.log("Not enough mana!"); + return; + } + + this.player.mana -= manaCost; + this.ui.updateStats(this.player, this.depth); + this.ui.log("You cast Detect Traps!"); + + let foundSomething = false; + this.map.traps.forEach(trap => { + if (!trap.revealed) { + // 50% chance to detect each unrevealed trap on the current level + if (Math.random() < 0.5) { + trap.revealed = true; + foundSomething = true; + } + } + }); + + if (foundSomething) { + this.ui.log("You sense danger nearby..."); + } else { + this.ui.log("You sense nothing unusual."); + } + this.render(); + } + + castCurePoisonSpell() { + const manaCost = 3; + if (this.player.mana < manaCost) { + this.ui.log("Not enough mana!"); + return; + } + + if (!this.player.poisoned) { + this.ui.log("You are not poisoned."); + return; + } + + this.player.mana -= manaCost; + this.player.poisoned = false; + this.ui.updateStats(this.player, this.depth); + this.ui.log("You cast Cure Poison! The toxins leave your body."); + this.render(); + } + animateProjectile(projectile) { const interval = setInterval(() => { const newX = projectile.x + projectile.dx; @@ -930,6 +1370,7 @@ export class Game { itemData.dropped = true; const questItem = new Item(itemData.name, "quest", 1, {}); + questItem.identified = true; questItem.x = monster.x; questItem.y = monster.y; this.items.push(questItem); @@ -943,6 +1384,7 @@ export class Game { this.ui.log(`You kill the ${monster.name}! It drops ${gold} gold.`); if (monster.name === "The Dungeon Lord") { const map = new Item("Ancient Map", "map", 1, {}); + map.identified = true; map.x = monster.x; map.y = monster.y; this.items.push(map); @@ -998,18 +1440,55 @@ export class Game { if (!item) return; if (item.type === 'weapon') { - this.player.equipment.weapon = item; - this.ui.log(`You equipped ${item.name}.`); + if (this.player.equipment.weapon === item) { + if (item.isCursed && item.identified) { + this.ui.log(`You cannot unequip the cursed ${item.name}!`); + return; + } + this.player.equipment.weapon = null; + this.ui.log(`You unequipped ${item.getDisplayName()}.`); + } else { + this.player.equipment.weapon = item; + this.ui.log(`You equipped ${item.getDisplayName()}.`); + if (!item.identified) { + item.identified = true; + if (item.modifier !== 0 || item.isCursed) { + this.ui.log(`It's a ${item.getDisplayName()}!`); + } + } + } } else if (item.type === 'shield') { - this.player.equipment.shield = item; - this.ui.log(`You equipped ${item.name}.`); + if (this.player.equipment.shield === item) { + if (item.isCursed && item.identified) { + this.ui.log(`You cannot unequip the cursed ${item.name}!`); + return; + } + this.player.equipment.shield = null; + this.ui.log(`You unequipped ${item.getDisplayName()}.`); + } else { + this.player.equipment.shield = item; + this.ui.log(`You equipped ${item.getDisplayName()}.`); + if (!item.identified) { + item.identified = true; + if (item.modifier !== 0 || item.isCursed) { + this.ui.log(`It's a ${item.getDisplayName()}!`); + } + } + } } else if (item.type === 'potion') { if (item.stats.heal) { this.player.hp = Math.min(this.player.hp + item.stats.heal, this.player.maxHp); this.ui.log(`You drank ${item.name} and recovered ${item.stats.heal} HP.`); - // Remove potion this.player.inventory.splice(index, 1); } + } else if (item.type === 'antidote') { + if (this.player.poisoned) { + this.player.poisoned = false; + this.ui.log("You drink the Antidote. The poison is cured!"); + this.player.inventory.splice(index, 1); + } else { + this.ui.log("You aren't poisoned."); + } } else if (item.type === 'spellbook') { const spell = item.stats.spell; if (this.player.spells.includes(spell)) { @@ -1029,7 +1508,7 @@ export class Game { attack(attacker, defender, presetDamage = null) { // Shopkeepers and QuestGivers are invincible/pacifist - if (defender instanceof Shopkeeper || defender instanceof QuestGiver || defender instanceof WanderingQuestGiver) return; + if (defender instanceof Shopkeeper || defender instanceof QuestGiver || defender instanceof WanderingQuestGiver || defender instanceof Farmer || defender instanceof Priest || defender instanceof Gravekeeper) return; // Peasants are invincible if (defender.name === "Peasant") { @@ -1062,6 +1541,15 @@ export class Game { defender.hp -= damage; this.ui.log(`${attacker.name} hits ${defender.name} for ${damage} damage.`); + // Poison logic + if (attacker instanceof Cobra && defender === this.player && !this.player.poisoned) { + if (Math.random() < attacker.poisonChance) { + this.player.poisoned = true; + this.ui.log("You have been poisoned by the Cobra!"); + this.ui.showPopup("POISONED!", 2000); + } + } + if (defender.hp <= 0) { if (defender === this.player) { this.ui.log("GAME OVER"); @@ -1075,7 +1563,7 @@ export class Game { updateMonsters() { for (const monster of this.monsters) { - if (monster instanceof Shopkeeper || monster instanceof QuestGiver) continue; // Shopkeepers and QuestGivers don't move + if (monster instanceof Shopkeeper || monster instanceof QuestGiver || monster instanceof Farmer || monster instanceof Priest || monster instanceof Gravekeeper) continue; // Stationary NPCs don't move // Speed check: Does the monster move this turn? if (monster.speed !== undefined && Math.random() > monster.speed) continue; @@ -1100,12 +1588,12 @@ export class Game { const dy = this.player.y - monster.y; const dist = Math.sqrt(dx * dx + dy * dy); - if (dist < 8) { // Aggro range + if (dist < 8 && this.map.hasLineOfSight(monster.x, monster.y, this.player.x, this.player.y)) { // Aggro range + LOS // Ranged behavior for Archers if (monster instanceof Archer && dist > 1.5 && dist <= monster.range) { const isOrthogonal = this.player.x === monster.x || this.player.y === monster.y; const isDiagonal = Math.abs(dx) === Math.abs(dy); - if (isOrthogonal || isDiagonal) { + if ((isOrthogonal || isDiagonal) && this.map.hasLineOfSight(monster.x, monster.y, this.player.x, this.player.y)) { this.monsterShoot(monster); continue; } @@ -1148,6 +1636,14 @@ export class Game { this.ui.drawInventory(this.player, this.inventorySelection); } + if (this.gameState === 'OVERVIEW') { + this.ui.drawOverviewMap(this.map, this.player); + } + + if (this.gameState === 'INTRO') { + this.ui.drawIntro(this.storyText); + } + this.ui.updateStats(this.player, this.depth); } } diff --git a/src/Item.js b/src/Item.js index 92201f6..507c44c 100644 --- a/src/Item.js +++ b/src/Item.js @@ -1,7 +1,7 @@ export class Item { constructor(name, type, level, stats) { this.name = name; - this.type = type; // 'weapon', 'armor', 'potion' + this.type = type; // 'weapon', 'armor', 'shield', 'potion', 'spellbook', 'antidote', 'map', 'quest' this.level = level; this.stats = stats || {}; // { damage: 5 } or { defense: 2 } this.x = 0; @@ -9,12 +9,38 @@ export class Item { this.symbol = '?'; this.color = '#ffff00'; + // Enchantment / Curse properties + this.modifier = 0; + this.isCursed = false; + this.identified = false; + + // Auto-identify non-equippables + if (['potion', 'antidote', 'spellbook', 'map', 'quest'].includes(type)) { + this.identified = true; + } + if (type === 'weapon') this.symbol = ')'; if (type === 'armor') this.symbol = '['; if (type === 'shield') this.symbol = ']'; if (type === 'potion') this.symbol = '!'; + if (type === 'antidote') { this.symbol = '!'; this.color = '#00ff00'; } if (type === 'map') { this.symbol = '?'; this.color = '#ffd700'; } if (type === 'quest') { this.symbol = '*'; this.color = '#ff00ff'; } if (type === 'spellbook') { this.symbol = 'B'; this.color = '#00ffff'; } } + + getDisplayName() { + if (!this.identified) { + if (this.modifier !== 0 || this.isCursed) { + return `A Mysterious ${this.name}`; + } + return this.name; + } + + let name = this.name; + if (this.isCursed) name = "Cursed " + name; + if (this.modifier > 0) name += ` (+${this.modifier})`; + if (this.modifier < 0) name += ` (${this.modifier})`; + return name; + } } diff --git a/src/Map.js b/src/Map.js index d164fae..b1f7eeb 100644 --- a/src/Map.js +++ b/src/Map.js @@ -5,23 +5,33 @@ export class Map { this.tiles = []; this.rooms = []; this.permanentlyLit = []; // New grid for spell effects + this.explored = []; // Track which tiles the player has seen + this.traps = []; // New array for level traps + this.fountains = []; // New array for healing fountains + this.signs = []; // New array for interactive signs } generate() { this.isTown = false; this.rooms = []; this.permanentlyLit = []; + this.explored = []; + this.traps = []; + this.fountains = []; + this.signs = []; // Initialize with walls and dark for (let y = 0; y < this.height; y++) { this.tiles[y] = []; this.permanentlyLit[y] = []; + this.explored[y] = []; for (let x = 0; x < this.width; x++) { this.tiles[y][x] = '#'; this.permanentlyLit[y][x] = false; + this.explored[y][x] = false; } } - const MAX_ROOMS = 10; + const MAX_ROOMS = 30; const MIN_SIZE = 6; const MAX_SIZE = 12; @@ -63,19 +73,51 @@ export class Map { this.rooms.push(newRoom); } } + + this.generateTraps(); + this.generateFountain(); + } + + generateTraps() { + const numTraps = Math.floor(Math.random() * 6); // 0-5 + for (let i = 0; i < numTraps; i++) { + if (this.rooms.length === 0) break; + const room = this.rooms[Math.floor(Math.random() * this.rooms.length)]; + const tx = Math.floor(Math.random() * room.w) + room.x; + const ty = Math.floor(Math.random() * room.h) + room.y; + + // Avoid placing trap exactly on player starting stairs if possible + this.traps.push({ x: tx, y: ty, revealed: false }); + } + } + + generateFountain() { + if (Math.random() < 0.25) { // 25% chance + if (this.rooms.length === 0) return; + const room = this.rooms[Math.floor(Math.random() * this.rooms.length)]; + const fx = Math.floor(Math.random() * room.w) + room.x; + const fy = Math.floor(Math.random() * room.h) + room.y; + this.fountains.push({ x: fx, y: fy, used: false }); + } } generateTown() { this.isTown = true; this.rooms = []; this.permanentlyLit = []; + this.explored = []; + this.traps = []; + this.fountains = []; + this.signs = []; // Fill with grass/floor for (let y = 0; y < this.height; y++) { this.tiles[y] = []; this.permanentlyLit[y] = []; + this.explored[y] = []; for (let x = 0; x < this.width; x++) { this.tiles[y][x] = '.'; this.permanentlyLit[y][x] = true; // Town is all lit + this.explored[y][x] = true; // Town is fully explored } } @@ -108,21 +150,55 @@ export class Map { }; // 1. Temple (North Center) - drawBuilding(20, 5, 10, 8, 'bottom'); + drawBuilding(35, 10, 10, 8, 'bottom'); + this.signs.push({ x: 41, y: 18, text: "Temple of the Sun" }); // 2. General Store (West) - drawBuilding(5, 15, 8, 6, 'right'); + drawBuilding(15, 30, 8, 6, 'right'); + this.signs.push({ x: 23, y: 31, text: "Oakhaven General Goods" }); // 3. Weapon Shop (East) - drawBuilding(37, 15, 8, 6, 'left'); + drawBuilding(55, 30, 8, 6, 'left'); + this.signs.push({ x: 54, y: 31, text: "Oakhaven Blacksmith" }); // 4. Dungeon Entrance (North East) - drawBuilding(40, 5, 6, 6, 'bottom'); + drawBuilding(60, 10, 6, 6, 'bottom'); // 5. Houses (South) - drawBuilding(10, 30, 6, 5, 'top'); - drawBuilding(22, 30, 6, 5, 'top'); - drawBuilding(34, 30, 6, 5, 'top'); + drawBuilding(20, 55, 6, 5, 'top'); + drawBuilding(37, 55, 6, 5, 'top'); + drawBuilding(54, 55, 6, 5, 'top'); + + // 6. Cosmetic Fields (Farmed land) + const drawField = (x, y, w, h) => { + for (let fy = y; fy < y + h; fy++) { + for (let fx = x; fx < x + w; fx++) { + // Alternating brown and green for a "plowed" look + this.tiles[fy][fx] = (fx + fy) % 2 === 0 ? '"' : ','; + } + } + }; + + drawField(10, 15, 10, 8); // West field + drawField(55, 45, 12, 6); // East field + + // 7. Graveyard (North West) + const drawGraveyard = (x, y, w, h) => { + for (let gy = y; gy < y + h; gy++) { + for (let gx = x; gx < x + w; gx++) { + if (gy === y || gy === y + h - 1 || gx === x || gx === x + w - 1) { + this.tiles[gy][gx] = '#'; // Fence/Wall + } else { + // Randomly place tombstones + this.tiles[gy][gx] = Math.random() < 0.2 ? 't' : '.'; + } + } + } + // Entrance + this.tiles[y + h - 1][Math.floor(x + w / 2)] = '.'; + }; + + drawGraveyard(5, 5, 12, 8); } @@ -151,6 +227,41 @@ export class Map { return this.tiles[y][x] === '.' || this.tiles[y][x] === '>' || this.tiles[y][x] === '<'; } + hasLineOfSight(x1, y1, x2, y2) { + let dx = Math.abs(x2 - x1); + let dy = Math.abs(y2 - y1); + let x = x1; + let y = y1; + let n = 1 + dx + dy; + let x_inc = (x2 > x1) ? 1 : -1; + let y_inc = (y2 > y1) ? 1 : -1; + let error = dx - dy; + dx *= 2; + dy *= 2; + + for (; n > 0; --n) { + // Check if current tile blocks light (only wall '#' blocks light) + if (this.tiles[y][x] === '#' && (x !== x1 || y !== y1) && (x !== x2 || y !== y2)) { + return false; + } + + if (error > 0) { + x += x_inc; + error -= dy; + } else if (error < 0) { + y += y_inc; + error += dx; + } else { + // Diagonal move + x += x_inc; + y += y_inc; + error += dx - dy; + n--; + } + } + return true; + } + isLit(x, y) { // Town is always fully lit if (this.isTown) return true; diff --git a/src/UI.js b/src/UI.js index dacf267..4393110 100644 --- a/src/UI.js +++ b/src/UI.js @@ -1,3 +1,6 @@ +import { Farmer, Gravekeeper } from './Entity.js'; + + export class UI { constructor(ctx, game) { console.log("UI Version 1.1 Loaded"); @@ -5,7 +8,8 @@ export class UI { this.game = game; this.messageLog = document.getElementById('message-log'); this.spellsList = document.getElementById('spells-list'); - this.inventoryLink = document.getElementById('inventory-link'); + this.inventoryLink = docugment.getElementById('inventory-link'); + this.inventoryLabel = document.querySelector('#inventory-panel .group-box-label'); if (this.inventoryLink) { this.inventoryLink.onclick = (e) => { @@ -14,6 +18,11 @@ export class UI { }; } + if (this.inventoryLabel) { + this.inventoryLabel.style.cursor = 'pointer'; + this.inventoryLabel.onclick = () => this.game.toggleInventory(); + } + this.popup = null; this.popupTimeout = null; } @@ -50,12 +59,18 @@ export class UI { const tile = map.tiles[mapY][mapX]; const isLit = map.isLit(mapX, mapY); const dist = Math.sqrt(Math.pow(player.x - mapX, 2) + Math.pow(player.y - mapY, 2)); - const inRadius = dist <= 3.5; // Radius of 3 (using 3.5 for better circle approximation) + const inRadius = dist <= 3.5; + const isExplored = map.explored[mapY][mapX]; - if (isLit || inRadius) { - this.drawTile(x * tileSize, y * tileSize, tile, tileSize); + // Tiles are visible if lit, in radius, or remembered + if (isLit || inRadius || isExplored) { + // Check if it's currently in Line of Sight to draw it "bright" + const hasLOS = map.hasLineOfSight(player.x, player.y, mapX, mapY); + const isCurrentlyVisible = (isLit || inRadius) && hasLOS; + + this.drawTile(x * tileSize, y * tileSize, tile, tileSize, !isCurrentlyVisible); } else { - // Draw nothing or a very dark tile for "unseen" areas + // Draw nothing for unseen areas this.ctx.fillStyle = '#000000'; this.ctx.fillRect(x * tileSize, y * tileSize, tileSize, tileSize); } @@ -63,12 +78,91 @@ export class UI { } } + // Draw Traps + if (map.traps) { + for (const trap of map.traps) { + if (!trap.revealed) continue; + + const isLit = map.isLit(trap.x, trap.y); + const dist = Math.sqrt(Math.pow(player.x - trap.x, 2) + Math.pow(player.y - trap.y, 2)); + const inRadius = dist <= 3.5; + + if (!(isLit || inRadius)) continue; + if (!map.hasLineOfSight(player.x, player.y, trap.x, trap.y)) continue; + + const screenX = (trap.x - startX) * tileSize; + const screenY = (trap.y - startY) * tileSize; + if (screenX >= -tileSize && screenX < this.ctx.canvas.width && + screenY >= -tileSize && screenY < this.ctx.canvas.height) { + this.ctx.fillStyle = '#ff0000'; + this.ctx.fillText('^', screenX + tileSize / 4, screenY); + } + } + } + + // Draw Fountains + if (map.fountains) { + for (const fountain of map.fountains) { + const isLit = map.isLit(fountain.x, fountain.y); + const dist = Math.sqrt(Math.pow(player.x - fountain.x, 2) + Math.pow(player.y - fountain.y, 2)); + const inRadius = dist <= 3.5; + + if (!(isLit || inRadius)) continue; + if (!map.hasLineOfSight(player.x, player.y, fountain.x, fountain.y)) continue; + + const screenX = (fountain.x - startX) * tileSize; + const screenY = (fountain.y - startY) * tileSize; + if (screenX >= -tileSize && screenX < this.ctx.canvas.width && + screenY >= -tileSize && screenY < this.ctx.canvas.height) { + + const asset = this.game.assets['fountain']; + if (asset && !fountain.used) { + this.ctx.drawImage(asset, screenX, screenY, tileSize, tileSize); + } else { + this.ctx.fillStyle = fountain.used ? '#808080' : '#00ffff'; + this.ctx.fillText('&', screenX + tileSize / 4, screenY); + } + } + } + } + + // Draw Signs + if (map.signs) { + for (const sign of map.signs) { + const isLit = map.isLit(sign.x, sign.y); + const dist = Math.sqrt(Math.pow(player.x - sign.x, 2) + Math.pow(player.y - sign.y, 2)); + const inRadius = dist <= 3.5; + const isExplored = map.explored[sign.y][sign.x]; + + if (!(isLit || inRadius || isExplored)) continue; + + // Only show bright if in LOS + const hasLOS = map.hasLineOfSight(player.x, player.y, sign.x, sign.y); + const isCurrentlyVisible = (isLit || inRadius) && hasLOS; + + const screenX = (sign.x - startX) * tileSize; + const screenY = (sign.y - startY) * tileSize; + if (screenX >= -tileSize && screenX < this.ctx.canvas.width && + screenY >= -tileSize && screenY < this.ctx.canvas.height) { + this.ctx.fillStyle = isCurrentlyVisible ? '#8b4513' : '#4a250a'; // Darker brown if not visible + this.ctx.fillRect(screenX + 4, screenY + 4, tileSize - 8, tileSize - 8); + this.ctx.fillStyle = isCurrentlyVisible ? '#ffffff' : '#888888'; + this.ctx.font = `bold ${tileSize/2}px monospace`; + this.ctx.textAlign = 'center'; + this.ctx.fillText('?', screenX + tileSize / 2, screenY + tileSize / 1.5); + } + } + } + // Draw Items if (items) { for (const item of items) { const isLit = map.isLit(item.x, item.y); const dist = Math.sqrt(Math.pow(player.x - item.x, 2) + Math.pow(player.y - item.y, 2)); - if (!(isLit || dist <= 3.5)) continue; + const inRadius = dist <= 3.5; + + if (!(isLit || inRadius)) continue; + if (!map.hasLineOfSight(player.x, player.y, item.x, item.y)) continue; const screenX = (item.x - startX) * tileSize; const screenY = (item.y - startY) * tileSize; @@ -83,7 +177,10 @@ export class UI { for (const monster of monsters) { const isLit = map.isLit(monster.x, monster.y); const dist = Math.sqrt(Math.pow(player.x - monster.x, 2) + Math.pow(player.y - monster.y, 2)); - if (!(isLit || dist <= 3.5)) continue; + const inRadius = dist <= 3.5; + + if (!(isLit || inRadius)) continue; + if (!map.hasLineOfSight(player.x, player.y, monster.x, monster.y)) continue; const screenX = (monster.x - startX) * tileSize; const screenY = (monster.y - startY) * tileSize; @@ -120,7 +217,10 @@ export class UI { } updateStats(player, depth) { - document.getElementById('stat-hp').textContent = player.hp; + const hpElem = document.getElementById('stat-hp'); + hpElem.textContent = player.hp; + hpElem.style.color = player.poisoned ? '#00ff00' : '#ffffff'; + document.getElementById('stat-max-hp').textContent = player.maxHp; document.getElementById('stat-str').textContent = player.stats.str; document.getElementById('stat-level').textContent = player.level; @@ -131,6 +231,11 @@ export class UI { document.getElementById('stat-gold').textContent = player.gold; document.getElementById('stat-mana').textContent = `${player.mana}/${player.maxMana}`; document.getElementById('stat-int').textContent = player.stats.int; + + const statusElem = document.getElementById('stat-status'); + if (statusElem) { + statusElem.textContent = player.poisoned ? "POISONED" : ""; + } } updateInventory(player) { @@ -140,6 +245,106 @@ export class UI { } } + drawOverviewMap(map, player) { + const width = 500; + const height = 500; + const x = (this.ctx.canvas.width - width) / 2; + const y = (this.ctx.canvas.height - height) / 2; + + // Background + this.ctx.fillStyle = 'rgba(0, 0, 0, 0.85)'; + this.ctx.fillRect(x, y, width, height); + this.ctx.strokeStyle = '#ffffff'; + this.ctx.lineWidth = 2; + this.ctx.strokeRect(x, y, width, height); + + // Title + this.ctx.fillStyle = '#ffffff'; + this.ctx.font = 'bold 20px monospace'; + this.ctx.textAlign = 'center'; + this.ctx.fillText('MAP OVERVIEW', x + width / 2, y + 30); + + // Map Scale + const padding = 50; + const mapAreaWidth = width - padding * 2; + const mapAreaHeight = height - padding * 2; + const scaleX = mapAreaWidth / map.width; + const scaleY = mapAreaHeight / map.height; + const scale = Math.min(scaleX, scaleY); + + const offsetX = x + (width - (map.width * scale)) / 2; + const offsetY = y + (height - (map.height * scale)) / 2; + + for (let my = 0; my < map.height; my++) { + for (let mx = 0; mx < map.width; mx++) { + if (map.explored[my][mx]) { + const tile = map.tiles[my][mx]; + if (tile === '#') { + this.ctx.fillStyle = '#555555'; + } else if (tile === '>') { + this.ctx.fillStyle = '#ffffff'; // Highlight stairs down + } else if (tile === '<') { + this.ctx.fillStyle = '#ffffff'; // Highlight stairs up + } else if (tile === 't') { + // Tombstone + this.ctx.fillStyle = '#222222'; + } else { + this.ctx.fillStyle = '#222222'; + } + this.ctx.fillRect(offsetX + mx * scale, offsetY + my * scale, scale, scale); + } + } + } + + // Draw Player Position + this.ctx.fillStyle = '#ffff00'; + this.ctx.beginPath(); + this.ctx.arc(offsetX + player.x * scale + scale / 2, offsetY + player.y * scale + scale / 2, scale * 1.5, 0, Math.PI * 2); + this.ctx.fill(); + + // Legend/Instructions + this.ctx.fillStyle = '#888888'; + this.ctx.font = '12px monospace'; + this.ctx.fillText('Yellow: You | White: Stairs | [M] to Close', x + width / 2, y + height - 15); + + this.ctx.textAlign = 'start'; + } + + drawIntro(textLines) { + const width = 750; + const height = 550; + const x = (this.ctx.canvas.width - width) / 2; + const y = (this.ctx.canvas.height - height) / 2; + + // Shadow + this.ctx.fillStyle = 'rgba(0, 0, 0, 0.7)'; + this.ctx.fillRect(x + 10, y + 10, width, height); + + // Background + this.ctx.fillStyle = '#000000'; + this.ctx.fillRect(x, y, width, height); + this.ctx.strokeStyle = '#ffffff'; + this.ctx.lineWidth = 3; + this.ctx.strokeRect(x, y, width, height); + + // Title + this.ctx.fillStyle = '#ffff00'; + this.ctx.font = 'bold 24px "Courier New", monospace'; + this.ctx.textAlign = 'center'; + this.ctx.fillText('THE LEGEND OF OAKHAVEN', x + width / 2, y + 40); + + // Text + this.ctx.fillStyle = '#ffffff'; + this.ctx.font = '16px "Courier New", monospace'; + let lineY = y + 80; + textLines.forEach(line => { + this.ctx.fillText(line, x + width / 2, lineY); + lineY += 20; + }); + + this.ctx.textAlign = 'start'; + } + drawInventory(player, selectedIndex) { const ctx = this.ctx; const width = 500; @@ -165,14 +370,19 @@ export class UI { ctx.textAlign = 'left'; let itemY = y + 70; - if (player.inventory.length === 0) { + const groupedItems = this.game.getGroupedInventory(); + + if (groupedItems.length === 0) { ctx.fillStyle = '#888888'; ctx.textAlign = 'center'; ctx.fillText('Empty', x + width / 2, itemY + 20); } else { - player.inventory.forEach((item, index) => { + groupedItems.forEach((group, index) => { const isSelected = index === selectedIndex; - const isEquipped = player.equipment.weapon === item || player.equipment.armor === item || player.equipment.shield === item; + const isEquipped = group.indices.some(idx => { + const item = player.inventory[idx]; + return player.equipment.weapon === item || player.equipment.armor === item || player.equipment.shield === item; + }); if (isSelected) { ctx.fillStyle = '#333333'; @@ -180,13 +390,32 @@ export class UI { ctx.fillStyle = '#ffff00'; ctx.fillText('>', x + 25, itemY + 10); } else { - ctx.fillStyle = '#aaaaaa'; + // Color based on quality if identified + if (!group.item.identified) { + ctx.fillStyle = '#aaaaaa'; // Gray for unidentified + } else if (group.item.isCursed) { + ctx.fillStyle = '#ff4444'; // Red for cursed + } else if (group.item.modifier > 0) { + ctx.fillStyle = '#00ffff'; // Cyan for enchanted + } else { + ctx.fillStyle = '#aaaaaa'; + } } - let displayName = item.name; + let displayName = group.item.getDisplayName(); + if (group.count > 1) displayName += ` (${group.count})`; if (isEquipped) displayName += " (E)"; - ctx.fillText(displayName, x + 50, itemY + 10); + // Draw Icon if available + const assetName = this.getAssetName(group.item); + const asset = this.game.assets[assetName]; + if (asset) { + this.ctx.drawImage(asset, x + 50, itemY, 20, 20); + this.ctx.fillText(displayName, x + 80, itemY + 10); + } else { + this.ctx.fillText(displayName, x + 50, itemY + 10); + } + itemY += 30; }); } @@ -195,7 +424,7 @@ export class UI { ctx.fillStyle = '#888888'; ctx.font = '12px monospace'; ctx.textAlign = 'center'; - ctx.fillText('1: Laser | 2: Fireball | 3: Light | 4: Heal | 5: Return | Up/Down: Select | Enter: Use/Equip | Esc/i: Close', x + width / 2, y + height - 20); + ctx.fillText('1:Laser|2:Fire|3:Light|4:Heal|5:Ret|6:Det|7:Cure|Up/Down:Sel|Enter:Use|Esc/i:Close', x + width / 2, y + height - 20); // Reset ctx.textAlign = 'start'; @@ -207,48 +436,63 @@ export class UI { player.spells.forEach((spell, index) => { const li = document.createElement('li'); li.textContent = `${index + 1}. ${spell}`; + li.style.cursor = 'pointer'; + li.style.padding = '2px'; + li.onmouseover = () => li.style.backgroundColor = 'rgba(255,255,255,0.1)'; + li.onmouseout = () => li.style.backgroundColor = 'transparent'; + li.onclick = () => { + if (spell === 'zappy laser') this.game.startTargeting('zappy laser'); + else if (spell === 'Fireball') this.game.startTargeting('Fireball'); + else if (spell === 'Light') this.game.castLightSpell(); + else if (spell === 'Heal') this.game.castHealSpell(); + else if (spell === 'Return') this.game.castReturnSpell(); + else if (spell === 'Detect Traps') this.game.castDetectTrapsSpell(); + else if (spell === 'Cure Poison') this.game.castCurePoisonSpell(); + }; this.spellsList.appendChild(li); }); } - drawTile(screenX, screenY, tile, size) { + drawTile(screenX, screenY, tile, size, isMemorized = false) { + const ctx = this.ctx; if (tile === '#') { - this.ctx.fillStyle = '#808080'; // Gray wall - this.ctx.fillRect(screenX, screenY, size, size); - // Add a bevel effect for walls - this.ctx.strokeStyle = '#ffffff'; - this.ctx.lineWidth = 2; - this.ctx.beginPath(); - this.ctx.moveTo(screenX, screenY + size); - this.ctx.lineTo(screenX, screenY); - this.ctx.lineTo(screenX + size, screenY); - this.ctx.stroke(); - - this.ctx.strokeStyle = '#404040'; - this.ctx.beginPath(); - this.ctx.moveTo(screenX + size, screenY); - this.ctx.lineTo(screenX + size, screenY + size); - this.ctx.lineTo(screenX, screenY + size); - this.ctx.stroke(); + ctx.fillStyle = isMemorized ? '#404040' : '#808080'; // Darker gray for memorized walls + ctx.fillRect(screenX, screenY, size, size); + + // Wall highlights + ctx.strokeStyle = isMemorized ? '#606060' : '#ffffff'; + ctx.lineWidth = 1; + ctx.strokeRect(screenX + 1, screenY + 1, size - 2, size - 2); } else if (tile === '>') { // Stairs Down - this.ctx.fillStyle = '#202020'; - this.ctx.fillRect(screenX, screenY, size, size); - this.ctx.fillStyle = '#ffffff'; - this.ctx.fillText('>', screenX + size / 4, screenY); + ctx.fillStyle = '#202020'; + ctx.fillRect(screenX, screenY, size, size); + ctx.fillStyle = isMemorized ? '#888888' : '#ffffff'; + ctx.fillText('>', screenX + size / 4, screenY); } else if (tile === '<') { // Stairs Up - this.ctx.fillStyle = '#202020'; - this.ctx.fillRect(screenX, screenY, size, size); - this.ctx.fillStyle = '#ffffff'; - this.ctx.fillText('<', screenX + size / 4, screenY); + ctx.fillStyle = '#202020'; + ctx.fillRect(screenX, screenY, size, size); + ctx.fillStyle = isMemorized ? '#888888' : '#ffffff'; + ctx.fillText('<', screenX + size / 4, screenY); + } else if (tile === '"' || tile === ',') { + // Field + if (tile === '"') ctx.fillStyle = isMemorized ? '#3d2b25' : '#5d4037'; + else ctx.fillStyle = isMemorized ? '#1b4a1e' : '#2e7d32'; + ctx.fillRect(screenX, screenY, size, size); + } else if (tile === 't') { + // Tombstone + ctx.fillStyle = '#202020'; + ctx.fillRect(screenX, screenY, size, size); + ctx.fillStyle = isMemorized ? '#404040' : '#888888'; + ctx.fillText('†', screenX + size / 4, screenY); } else { // Floor - this.ctx.fillStyle = '#202020'; - this.ctx.fillRect(screenX, screenY, size, size); - this.ctx.fillStyle = '#404040'; - this.ctx.fillText('.', screenX + size / 4, screenY); + ctx.fillStyle = '#202020'; + ctx.fillRect(screenX, screenY, size, size); + ctx.fillStyle = isMemorized ? '#222222' : '#404040'; // Very dark for memorized floor + ctx.fillText('.', screenX + size / 4, screenY); } } @@ -297,8 +541,53 @@ export class UI { } drawEntity(screenX, screenY, entity, size) { - this.ctx.fillStyle = entity.color; - this.ctx.fillText(entity.symbol, screenX + size / 4, screenY); + const assetName = this.getAssetName(entity); + const asset = this.game.assets[assetName]; + + if (asset) { + this.ctx.drawImage(asset, screenX, screenY, size, size); + } else { + this.ctx.fillStyle = entity.color; + this.ctx.fillText(entity.symbol, screenX + size / 4, screenY); + } + } + + getAssetName(entity) { + if (!entity) return null; + const name = entity.name || ""; + + if (name === 'Player') return 'hero'; + if (name === 'Rat') return 'rat'; + if (name === 'Kobold') return 'kobold'; + if (name === 'Orc') return 'orc'; + if (name === 'Skeleton Archer') return 'skeleton archer'; + if (name === 'Elite Archer' || name === 'Sniper' || name === 'Archer') return 'archer'; + if (name === 'Ogre' || name === 'Troll') return 'ogre'; + if (name === 'Cobra') return 'cobra'; + if (name === 'Dragon') return 'dragon'; + if (name === 'The Dungeon Lord') return 'dungeon lord'; + + // Town NPCs + if (name === 'Peasant' || name.startsWith('Traveler') || name.startsWith('Farmer') || entity instanceof Farmer) { + // Consistent but "random" assignment based on coordinates + return (entity.x + entity.y) % 2 === 0 ? 'peasant1' : 'peasant2'; + } + if (name === 'Villager' || name === 'Old Man' || name === 'Woman') return 'house questgiver'; + if (name === 'Priest' || name === 'Father Sun' || name === 'Smith' || name === 'Merchant' || name === 'Mort' || entity instanceof Gravekeeper) return 'mysterious figure'; + + // Items + if (name === 'Dagger') return 'dagger'; + if (name.includes('Sword') || name.includes('Blade')) return 'sword'; + if (name.includes('Axe') || name.includes('Hammer')) return 'axe'; + if (name === 'Buckler') return 'buckler'; + if (name === 'Wooden Shield') return 'wooden shield'; + if (name.includes('Shield')) return 'kite shield'; + if (entity.type === 'spellbook' || name.includes('Spellbook')) return 'spellbook'; + + // Projectiles / Special + if (name === 'zappy laser') return 'zappy laser'; + + return null; } drawShop(shopkeeper, player, selectedIndex, mode = 'BUY') { @@ -326,7 +615,7 @@ export class UI { ctx.textAlign = 'left'; let itemY = y + 70; - const list = mode === 'BUY' ? shopkeeper.inventory : player.inventory; + const list = mode === 'BUY' ? shopkeeper.inventory : this.game.getGroupedInventory(); if (list.length === 0) { ctx.fillStyle = '#888888'; @@ -335,7 +624,8 @@ export class UI { } else { list.forEach((entry, index) => { const isSelected = index === selectedIndex; - const item = mode === 'BUY' ? entry.item : entry; + const item = mode === 'BUY' ? entry.item : entry.item; + const count = mode === 'SELL' ? entry.count : 1; let price = 0; if (mode === 'BUY') { @@ -355,10 +645,29 @@ export class UI { ctx.fillStyle = '#ffff00'; ctx.fillText('>', x + 25, itemY + 10); } else { - ctx.fillStyle = '#aaaaaa'; + if (mode === 'SELL') { + if (!item.identified) ctx.fillStyle = '#aaaaaa'; + else if (item.isCursed) ctx.fillStyle = '#ff4444'; + else if (item.modifier > 0) ctx.fillStyle = '#00ffff'; + else ctx.fillStyle = '#aaaaaa'; + } else { + ctx.fillStyle = '#aaaaaa'; + } + } + + // Draw Icon if available + const assetName = this.getAssetName(item); + const asset = this.game.assets[assetName]; + let itemName = item.getDisplayName ? item.getDisplayName() : item.name; + if (count > 1) itemName += ` (${count})`; + + if (asset) { + this.ctx.drawImage(asset, x + 50, itemY, 20, 20); + this.ctx.fillText(`${itemName}`, x + 80, itemY + 10); + } else { + this.ctx.fillText(`${itemName}`, x + 50, itemY + 10); } - ctx.fillText(`${item.name}`, x + 50, itemY + 10); ctx.textAlign = 'right'; ctx.fillText(`${price}g`, x + width - 50, itemY + 10); ctx.textAlign = 'left'; @@ -375,7 +684,7 @@ export class UI { // Instructions ctx.fillStyle = '#888888'; ctx.font = '12px monospace'; - ctx.fillText('Tab: Toggle Buy/Sell | Up/Down: Select | Enter: Action | Esc: Exit', x + width / 2, y + height - 20); + ctx.fillText('Tab: Mode | Up/Down: Sel | Enter: Action | Esc: Exit', x + width / 2, y + height - 20); // Reset ctx.textAlign = 'start'; diff --git a/style.css b/style.css index 44cdf08..585582a 100644 --- a/style.css +++ b/style.css @@ -8,8 +8,8 @@ } body { - background-color: #008080; /* Classic teal desktop */ - font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; /* Fallback to modern sans */ + background-color: var(--win-bg); /* Match window background */ + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; display: flex; justify-content: center; align-items: center; @@ -21,15 +21,13 @@ body { /* Windows 3.1 / 95 Style Window */ .window { background-color: var(--win-bg); - border: 2px solid var(--win-white); - border-right-color: var(--win-gray-dark); - border-bottom-color: var(--win-gray-dark); - box-shadow: 2px 2px 0 #000; - padding: 2px; + border: none; /* Remove outer borders for full screen */ + box-shadow: none; + padding: 0; display: flex; flex-direction: column; - width: 900px; - height: 700px; + width: 100vw; + height: 100vh; } .title-bar { @@ -79,10 +77,13 @@ body { display: flex; justify-content: center; align-items: center; + border: none; /* Remove border for cleaner look */ } #game-canvas { image-rendering: pixelated; + width: 100%; + height: 100%; } #sidebar { @@ -113,11 +114,14 @@ body { } #message-log { - height: 100px; - padding: 5px; + height: 120px; + padding: 10px; overflow-y: auto; font-family: 'Courier New', Courier, monospace; - font-size: 14px; + font-size: 15px; + background: #000; + color: #00ff00; /* Matrix/Classic terminal style */ + border: 2px solid var(--win-gray-dark); } .status-bar {