From 8fca14e42d901f138b2a0748a17b612abc8ee05e Mon Sep 17 00:00:00 2001 From: claude Date: Sat, 24 Oct 2026 11:54:07 +0200 Subject: [PATCH] =?UTF-8?q?M9a:=20stationer=20i=20flera=20v=C3=A5ningar=20?= =?UTF-8?q?med=20trappor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - level: Point har Z; banor kan ha floors (lika stora rutnät med namn), gamla grid-banor läses som en våning; trappor U/D på samma x,y en våning upp/ner valideras parvis; Neighbors ger grannar över våningar så att Path, lösbarhet, spärregel och Unreachable fungerar; skyltar har z - sim: att kliva på en trappa flyttar spelaren till andra änden (FloorChanges); dimman är per våning och avslöjar trappans andra ände; tåg hörs/syns bara på egen våning - game: bara spelarens våning ritas (följare på andra våningar döljs), HUD och minikarta visar våningens namn, trappgrafik i genart och färg på minikartan - bana 004 Tokyo (svårighet 3, tre i sällskapet): start under mark på B1, trappa upp till 1F med perronger; generatorn gör våningar med trappor som flaskhalsar; ordboken utökad - testbotar och tools/shots förstår trappor; två våningsbilder - docs/README/wiki uppdaterade Co-Authored-By: Claude Fable 5.1 --- README.md | 17 ++- doc/img/floors-1f.png | Bin 0 -> 18034 bytes doc/img/floors-b1.png | Bin 0 -> 16522 bytes doc/img/map.png | Bin 15174 -> 15224 bytes doc/img/words.png | Bin 16169 -> 15982 bytes doc/mal-och-tester.md | 6 +- doc/plan.md | 2 +- doc/roadmap.md | 38 +++-- internal/game/assets.go | 1 + internal/game/minimap.go | 21 ++- internal/game/scene_level.go | 20 ++- internal/game/trains.go | 14 +- internal/level/level.go | 204 +++++++++++++++++++-------- internal/level/level_test.go | 113 +++++++++++++-- internal/sim/fog.go | 63 +++++---- internal/sim/mover.go | 2 +- internal/sim/run.go | 25 +++- internal/sim/run_test.go | 87 ++++++++++-- resources/data/levels/004-tokyo.json | 159 +++++++++++++++++++++ resources/sprites/tiles.png | Bin 950 -> 1106 bytes resources/text/jp-dictionary.json | 49 +++++++ tools/genart/main.go | 23 ++- tools/shots/main.go | 12 ++ 23 files changed, 704 insertions(+), 152 deletions(-) create mode 100644 doc/img/floors-1f.png create mode 100644 doc/img/floors-b1.png create mode 100644 resources/data/levels/004-tokyo.json diff --git a/README.md b/README.md index a354208..6702b76 100644 --- a/README.md +++ b/README.md @@ -10,14 +10,14 @@ Storyn följer några polare på semester i Japan, från landningen på flygplatsen till avresan. Efter storyn låses ett oändligt läge och New Game+ upp. Hela speldesignen finns i [doc/spelbeskrivning.md](doc/spelbeskrivning.md). -**Status:** M8 klar (2026-10-24) – fyra banor på en bankarta i +**Status:** M9a klar (2026-10-24) – fem banor på en bankarta i spelbrädesstil, med sidospår. Japanska skyltar och ordinlärning, lämmeltåget, minikarta, tågkort, biljetter och minispel, svårighet med -tidsgräns, poäng och debuffar, tåg som rullar in till avgångsmelodin. Och nu -progression: XP och nivåer, ett skillträd, items med buffar och genvägar som -köps i kioskernas butiker eller vinns på sidospår, och en sparfil som minns -allt inklusive seeden på den bana du höll på med. Nästa är M9a: stationer i -flera våningar. Se [doc/roadmap.md](doc/roadmap.md). +tidsgräns, poäng och debuffar, tåg som rullar in till avgångsmelodin, +progression med XP, skillträd, items och sparfil. Och nu stationer i flera +våningar: Tokyo station börjar under mark och trappan tar dig upp till +perrongerna. Nästa är M9b: folk, reklam och ljudröra. Se +[doc/roadmap.md](doc/roadmap.md). ![Bankartan](doc/img/map.png) @@ -51,6 +51,11 @@ visar vad du bär. ![Skillträdet](doc/img/skills.png) +Stationer kan ha flera våningar. Du ser bara den våning du står på; trappor +tar dig upp och ner, och minikartan visar vilket plan du är på. + +![Tokyo station, 1F](doc/img/floors-1f.png) + På de svåraste banorna har alla i sällskapet egna kort som måste ha saldo, alla behöver biljett, och när biljetten är köpt ska sällskapet placeras i vagnen efter sina önskemål: fönster eller gång, vänster eller höger sida, diff --git a/doc/img/floors-1f.png b/doc/img/floors-1f.png new file mode 100644 index 0000000000000000000000000000000000000000..1f33a1ae75c98682a8f7e6194597dbd16e3a77e0 GIT binary patch literal 18034 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 literal 0 HcmV?d00001 diff --git a/doc/img/floors-b1.png b/doc/img/floors-b1.png new file mode 100644 index 0000000000000000000000000000000000000000..e3e8fc7a28d8ce1dadfd1adae9e741bf87433c47 GIT binary patch literal 16522 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 literal 0 HcmV?d00001 diff --git a/doc/img/map.png b/doc/img/map.png index dbb80bfcd04d0ea5b04f5a90a0d9a08488a79139..8d0a6a1a83a7f025cc94eb1ff9e2b57c0e65eb00 100644 GIT binary patch delta 6089 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 delta 6756 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 diff --git a/doc/img/words.png b/doc/img/words.png index 3889a13d2b38a0d1a938d0c91c31737ca3aece7d..5f021395dfd1d3f2fcd355b4a4da24f8e640facd 100644 GIT binary patch literal 15982 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 Pqg;_yktvkAasU4Se{ge4 literal 16169 zcmeHu2T)W?(>6J?qU5Lq$vG}cl&F#=iR7RpWfvqYQL=z6A|gtTB7$U)jDWBt36dqb zWF<+?@L{-Kg@gCj_tyVceN``Ws?O;_W_o&hdb*!(gVabd(h z3zvGm3r(=auGQcDh$=A6&BV!RWoxoT%jF(UYLDn08}CE92*xW#Xk1}Sd#v%f!u2%% zJuJ)rgU^f!sw)Y88kS+^7Gm5+FUzaG)czHD!u!0b%mki-Js`){F zExHa#q;vU?GklwWCIGjPjpjMzX{`xf;E|8cSrWKiCQqJHTHwd|_zLHthQ9g@u#P8( z&RTTzhlK^xfx4+c=tO(20X{X5-d~XVw5pK1<+)Zh; zU|gkt(ps-3^2AHoAUkT4{AKV1$v|8*df-Bc<+qNMFf#Zq2Wk1i26SO!&cEedPGi_7 z$qFx>&(uwEOt3&$dnjG=!TyTK6LH)9=eO>N^)0#b!!6i{)q4hWrZ%pew>;Q@|MIEy4topH;vE>=vAJf#4-l=J*`S4Ce(^XgvmEGgp8EVo$h6vN|wr;kTBS`;@W z>U$zL$N40HLs}h%3ua4kt6@4i8P@U+8%6wf&YP_2992A{<$6BqE4`ns>~bWOycQO( zx~Mh@u;l1*AeVyzzmoV#UlHwk($UP z7P7lS8^*Y4oeopRBzS^=?XI#n77_)Z^@D1YLhNg>_Dy}Gj5=p24TWT$>oL6=x@wDY zPMTR}G|DHkLhr7GHwU<&x9qv*G(8Pvc}3Mgom}@AQ;^QhPii9Ai1O0W_fN9#iuarO zeDqJbl6_uJd6TzQwd$@~+Ee43j*efHs75CFX7!kR>M%Lvb}}D z$E*FDDt$uYDq~Dd=d$qOJka5rSmbddJz`*r>}>xL>v*j>NYhT!ExjR<@r4z0c|sdc zs?0glNVz=`!ZY}${5#9Yl`?+5&zHRA0h-~pljTYO@LWmP*@?31iDuDr=egSsZ1SbW z&!2w0OH|8upX1go7BVu8C2Rg?*>Bp~;m;)(jcDu2o{e6`+K9owMK|i}HmHqwFR5OcMR&=DVq*FlGP)r(lexw^Gd%zB}a>T?A%4oR-k{8<0pbB2f_Se2* zw);#y5vT>KC2rlDLXSRWDRJ~;rWSkE=pbI=+5R0f!vdMUF`Y#TmD}Z&L39hDhc6oX3J(k3f3{5FWzc#qnqaoq71?_9%AWTrR+4F*m z70oYrpkvfM892MXKqq2y?*6!C-F)KYc<}vbboKZ4XfNZ?uA05V&gH7M^<_Bswx_Gz z>wJtG$;OQk5s1!SIr-vs)tw{*9#skJ3f1M<39YsXV)CK-EF&dMJ#XzqQp;aoUWSRv zU?qTdGY$f7n-M*`arWSI+AP6Ax4gT7O5i$Vw`qwl`?cGS;U_{`#O1ax+|}jn4#OTt z$Ng)TnFCTwV$cZzY9g4;-+m8CF6!i@2HDfFD zi%`@2>Me3Re?<9>tRz4e;N;ipr=0^iVVC<3 z*DP!*I4LmLcALj`AN#{2Ihgw%p$r|CK5r zw*0}>&(Xdz34Ku;?sEEaa;e?URHXYh(`ZL1*~*Zmy0$$+t~ApY9@_{n5;B)?86${DSQaboQ|j#tUl4nX_kL5CU*&g~)cg7G z`}$pi5o|Q$`M#y?R&5?e7o~rbt6d}gX5P;A{X8|sA@~7TdYi{18C&1 z%u#LJ!Je^OU!Y*IrwFdw%r4v$zL7OkN+q5=hM{>s9{s%oI+I$wF&%&2cq_PR@gUlg z8JbKIpUyM-_M5_`7$EOsL+FJka_~N-$!LZW?-a&)Adw^K(AEtr^DTP(Gq;-<5iDCI z4&00qO&3FUpFX8niG3#7baC3F_bQT~EWVPL|H!HNDTcHDQZ|V~9aNTtg;;v%ue^J| zlQY3s+ZzX7eO>EY?`Qe0Z$Oukl`8ZM_I@PKuZc8=T9>F_iPJ<@(#%#mp#O4S{@N`$ zjOgQsCJ{oA17S^j{K0~cxJrn4tXQ^GGGXx)FlUvw(3Fp!y5IC$8d>63>wV(h3Zm_u zyt4K}Wc$>IR=}H#wA~CfCh@4Wu z`IKt)$cl^CMGxWaMux%AQT%H1!M#cfCZ2Y$^mkqQ!xbtq0LM8@HjyjUl%5nnLszl> z5<8hj-os#jP0sIK`giFgT_k$6{nPbe6NX`LOeaLqwdZ5QUS8>8YdlpRezw?!lHn{{ z>qGyXHW+1H{1`!>QvW&Ixy$`eBi2&?vQ|g<1|WOY!Q0jTDf0u@#L8Y$iJsL0yUpqM z-%=@}t@@IBg{|n-XQ4@kUySZo^}%ND_-PLThCH;E@H9Mxs6!iz?-F=9KZ-s^5&4c{ zNvk{71Mea^T(87oBt6#w`J%SX8bL~IX0ptwlnXNq^}u5!hesE*$L5ShRH&x%s+c5P zsz+)mz_tRS>}b{}sbR4TIpe?+0xme56uzKZWjd`-7Yl=|kNXznlIIt8toKkl!9DPb zMjGw?-cDQ>s%dD`GMdzLaWQ#Q+;TrT9z+WyuZ8ggeK$v4D zNV5s0zkC}otlHg5IECe4rVDsf$bbg}WkV`BXz09Q7*53s##@!d`XFxXC^LAA`qLNV zndsU>p|yMJ)}y4e@tpeVX*=6&0IKzx2>51D_aA3yA6~wU4g1(Ld&kA|bvQ(ESkkB<-L z!{i|j@KVXON8_=(3#17a3A9lOp4%-%mT1a}xPsoy9T?G4#Q*PzFp`-Sd&nC}ZHZ;o zh{a8+h>8L}$*KJ%*vjHSSDk^V!k?H}T}9(UK7=TOnP|>{a0C5hYU#g`Q;J~DS=ruu zRhLSWhIFr#=Iy(11bXH4tSO7qY)I^Ece>cOpqC3%n>oey%v*>KWrt+WAGnMfNwkO_ zTlFfd_mkr@FwwBYLu}80M!UXOE*Gl$o9&DgT<)&rACu~20t>ZVT|JVexd_P89;j(1-lXq+Js2rYx zFqj!g+~AWO26Y4H9fm;NjtF4AnmLcZD&pq+;Jp%2091@kVv`dige1dXH{`Z0r!6K& z*g2PTHH^Br#bm-ie;KASzA zH8Imu>ef)I)=-f9sYAn+!tTIW)tJn+R?ER(!BEG+AFvcm@SB_b-89ZJ0A2_wl6VSX zkNiWeBhOrTn=nhX)I+wVpxpNMZQWLLba`){~)gF^NqiDOZ%$91jG{$YSga8oGqESN9Kp8jV->*JF!;Y`AnH+uw1=&udR%5KjU7y4Wz6q{7O|>|Mft zUJ8}>f&j3)+rvSl-rq=lc)j?x)J%S~hlrNYe7kMN6{5x`r-W`-Pp_^s-98c;eHB%9 z5KtU$pq7Z}YT}%~L6`bowt@AG@G>&FXQGLrx^c3ne%WCaa_}@eXwEfwKl6t8^@O?M zO69jwL<-PObX2}GqFic+4{8w+?9pk59gESe-9az={JzWiddq@t z-^8RH`SnNK&dC<`OC^ohA4%3mj811GSVjR<|3_TB$`9;MEWCu=R}AkE2sV#m9OZwI z8Tyd#QhVd;<;W*Z09yA$ffp44l@9?UfmgH$7neRco1p)&v}UNkX{Zq-zb=SplA^0H z!OHs(7jbX}H?sdK|TKc#tjAILokJHb6Uv@c2tuLj?Y#Z9#+Zypa>i8bYtJ3H1AziPd z?jc5K$2CFdP>S@#^yGW!5J6YjdfXR8;tJy^YHCn)CO-q-_l>aj%QUmoUU{82=M2<| zW>;8F{Q@oElYmYi{)DFl0$JwYz=t130A5210_%IYPu-Q0CBqewEY zNOEI;N_D@}U|*8qJ;zx)oynzxlF9>apvewHA|Z`VML9;&P))(#KO)tsGh&?}+PBt% z%`PIG;PYi?2cf*oPQ0MFP0S_SFNbYrAGcqw-?GV7<2zRK(`NpGoMoj^uuip~D!t)M zdgzT}oig#wmF@>W%moNAS7U(=Jbz-~Z`%#*l(UILu^-%3sL9>Mw zFByAn)#kQCwz8`gTgzlCbp<)dULXGYR@)D#C9h_Z8XC9I2L;+}z{uvi^=QxpzjP$> z_mkDLgp%rMtB@((EK0SQ-27ZPrpMToYE2OInJ6UX-08l{!Df%fHeaTJpBWm_{|x3< zFJNqcMdGZ^Vb!v7E=bj2mK>0t#`F?+6xk1Ncsr`(TbnNx;o-XTI1RN`E@3Q2E-Vt_~~xK`UJwH)jZH z^Xsf4#GQDz{7u=&`F2&4L`qAnXz!c9yj&Zkt{!!A4~K*I`k6t-Ib-a^?IGd?d6}*S_jhJCU*&d%=*;$-TYkHS8G!nhDRC7DORG`l&&xOwBprx69 zggG#GTzq1FSgS#6syI`rg+L-|+%ciF=D2iL9|3!*w77wM&z;ZMnnzMMhrZ?%(YRuM z<57^zKEV-z{1lJGFrZ|_MoaQYLiiPV5~hSID;~G3b4=>MLsy004=PHw$-A08 z;A0jz@XDEVm9$%ovX=uBf&9>sQh$EIJ8=~P3^7(3D^P{+5gMcSqx^h@1{yQ2l+b?u zhY@~E0vGm^2@hr6UwG{b^5A5c0;RC~3o2}>HCo!nkkp3ECx zbz>iV!hg2jd_7)2)$v_{8{h_xKX9^b&lNW-oTpSM@M?G(p}ZQOWve+*At?tm7SH9x(3{+74(4b9`nB%Jpc>QxoV$V7MCo!b^GeON^=HKkV{Y;#KCcc2xba2)KbQFl$|8TeWZPD+RbrPJ*DXveGnAm?X=5c5 z$Wq?tC$c!#EcMu%6M6q6uom&GM9{jy4QjSN1P4l*Yr5835d{BN!G$8tI`Ljn8vb1_FeQJZP_um-KwC@qtU< z0JHamOtIfD>irPO^UW=wFzjcYo8H+l(f|_d9xz&^8(6EZ#@tV08ys#MY?=9V9g;Mj z{N>%!jU}ah@q;Z^X0PXUb<{&=ofcTqyeZ)0tlSs|INW7l&|Gob7lSJ9;Sc777&q=( zMW`5hQbXS-#awc6zrQ2cJDRR8(d^YMem!#IaaTTU9ACsU7#Q@32S!yC`utMtb>y2v zLpQ7_1Zq>YSc(1l%`69E6}V#hUdb6T7Tj@Ir&t+fa#RF$K2mz@w)`Ttd1_g`WvUsm zavyjq<_<4l(5Z}>rr~Kb0k!aBz5oka<0f`<()?gpKHhOeDG_)8S0);_;auc%kccDA zr{|zFKMm7I8m9gBbq16R%@Cc2R%eUjs9Ycspo`JTh%BkOGECnat(sS+%=w zY$O@qUOkkv*us5$9j`DxpaM158q{s%{Ho`cTUHgrYsj1<55jOUFAIJZ#HR%^p<)q| z+YWwKjRlM?iXT?x0l7MKxPcbe%X2WODJKK$WlF;}7`o0sqnNNcE|PEPQVM?IPdMwJE>{1zEjy@gQS3*&zvK0du;C0da^9W^>hbUa?13CAf6)wE4DWhAA}=vSZ^lZq>Ir!w3fst{w;WTP&H^&-n| zAS3v{=e&j#J|q?_5CClsmkV*fnGOv<8>TwvUSLv42jgaqZHmGp6+txe=xc4w4vs`* zeyP13brR&_DglgFxU$leCU0nVg}%>wpBJ5NMtTPGm5(-Gh<1{zRx4DfPRx=n)`CtJ zC|#U=0uC8in>3kv*aSUWF0(mm)@SLYoFn6=Js()OFJY*AQw!qaZktQ*Fd87Wi8n^3Cm@IZp_9wsaYgxjE)W zRxCSL`dlpn`Iwn|L)<6xNtq-f6Wj;V$5hi&R^GUuIw@GP>pBu}eD%fgQ9Qu3?en|M z#22+;UhH-^2kP{~b52mykG0}Eih{lXa#z`s5o zc=cf&jf4KTA%X(h%#PET`1<5VKVYzUv{M`%Ftn33UmW*#wO^5M=_r?=By6ZZXs|io;klEr0V^Cl731t&7>i)~J z9u1j)&C2N2F4c|DPMG7oMInJZLy(vi2c!lAPX(=>*P)<$&NuAdeM725ms3vyx&~H4 z_IuzVmUVPsTa7%Y>*){cS;AV}r~71I1gQ}xF<(;li^dT;ZOzdAd&k0{tRhfmeO(Q# zT}sT@a>}q)LBpMCGCwT!ZBk=+^kn@j84VTlb&gMgMr#J|TZ@=9x=H8vR~vazB@g+=f!lFtQOg#m)ZKh4=i^Ga}Jv>`lnR zs86=CEVzI*KP56qFwdpRMxMSI%GC!2YY)P49KHkQVjm{Iur9HdE~zsd+;rcV_(-nx z#7hzr;G&@fHg<=i&xfj4(8fgYCA-`aJ;dRml`W?Omw6cLOP_1eR8N@F_sS1FS)p}H zEpgPbBb|Pr1&vQ;K6s=<%XzG3=|>J@M(4C8HBg&r!1{(!Y%I>2F3@UcS(Cozrk?~+ zddeN5Xk=Hn>fvp&bPw~j0)WR!mnzq$Y%A{DEG8<@3;&OZdZn$z*0dKTC!Gey7h5Y{MW zPKRwl_ov(Ex0NY^O>_AM#BATIJhn61_xU;R2|+;|TL+mXej(-uwA@d z-vRzq&-1UP1IJ&*be$VgeMx$i0I{_3<~$gJ&aFmMil#931alILo-Nkr>{e9u$8jb2*}ApMKi~}rmmIK}4rO5vBsaMGh%xxLZ?teP1%rT$Q4Z|N#I@kGl0 zW_G18f*-e42PK*RHtLT__6$iZfdrUGilkW1`VzS5B|X^~q?WGL z>E0G{Ax=%V?ODMJ`(lw;j;W7mX#uB<=;_ae?9oP_;*zFLJLwT#=LSF4Up3S;l$g@z zwPz6QemU-ddGS?Sfy!byags_VeDXq^k4&ZWBL|gm_uL-*tqu$a`&*}zwOC+{+FI}n z^(>_!Ds7m;f)9Mn;8UeN`W8Uk0q!4LQ# z=rBW6FI2YsgY?pKk~bI%#YzuH7<8pG;R~T*$D_+C7}9a1atwGLapQKLShp#eOq0yv zIQ0KoB}x4l?=D`Vu70z^i#Nz$%fYhgQr-l*;OLc5rW5;7d9EUj`a{ji6ltBVMAsxH z&zH&=dy&~WV+lpk?mPxj_EV%2>8>LSD)zUq1K9!ehGd2klL|sYmU9Z{@dnbu$tX&@*M@xND=uX#g62XK4B2wZRJ}sO~R~=?*x=iN{t!2CHb52c+en&jw4y<)k{TM2MKkQ@TAM47){tp5_YpF@zPV zR-5D}gBy4TbYe$|HZDR_^YRGA+SKCZwlF2}g~n19TE~m;E{lkO?X#)n97Kdgo>uo= zGk@8X(G&xU-WG9=l)pz7sC zaWq%_HNYn)EaavsgUr+>;Rhc1fF^L$j9my<)e&{CTPB?Cv(p_xv-5uaG2hO68nzB| z^4i|1JI 1 { + floorLabel = l.FloorName(z) + } w := max(mapW, 44) + 6 h := mapH + 6 + len(deps)*LineH + if floorLabel != "" { + h += LineH + } px, py := r.Player.WorldPos(TileSize) ox, oy := s.cam.Offset() playerRect := image.Rect(int(px+ox), int(py+oy), int(px+ox)+TileSize, int(py+oy)+TileSize) @@ -89,13 +99,17 @@ func (s *LevelScene) drawMinimap(g *Game, screen *ebiten.Image, box image.Rectan vector.DrawFilledRect(screen, float32(rc.Min.X), float32(rc.Min.Y), float32(w), float32(h), colPanel, false) x0, y0 := rc.Min.X+3, rc.Min.Y+3 + if floorLabel != "" { + a.Draw(screen, floorLabel, x0, y0, colKnown) + y0 += LineH + } for y := 0; y < l.Height(); y++ { for x := 0; x < l.Width(); x++ { - p := level.Point{X: x, Y: y} + p := level.Point{X: x, Y: y, Z: z} if !r.Fog.Visible(l, p) { continue } - t := l.At(x, y) + t := l.At(p) c := minimapColors[t] if (t == level.Platform || t == level.WrongPlatform) && r.TrainVisible(t) { c = color.RGBA{255, 255, 255, 255} // tåget är inne @@ -106,6 +120,9 @@ func (s *LevelScene) drawMinimap(g *Game, screen *ebiten.Image, box image.Rectan // Sällskapet: följarna i sina toner, spelaren blinkande vit. for i := len(r.Party.Followers()); i >= 1; i-- { from, _ := r.Party.FollowerTiles(i) + if from.Z != z { + continue + } tint := memberTints[i%len(memberTints)] vector.DrawFilledRect(screen, float32(x0+from.X*sc), float32(y0+from.Y*sc), float32(sc), float32(sc), color.RGBA{uint8(255 * tint[0]), uint8(255 * tint[1]), uint8(255 * tint[2]), 255}, false) diff --git a/internal/game/scene_level.go b/internal/game/scene_level.go index 9dda08d..04a9c5e 100644 --- a/internal/game/scene_level.go +++ b/internal/game/scene_level.go @@ -125,7 +125,7 @@ func (s *LevelScene) Update(g *Game) error { g.SetScene(NewMapScene(g)) return nil } - if shopJustPressed() && !s.run.Player.Moving() && s.run.Level.At(s.run.Player.Tile.X, s.run.Player.Tile.Y) == level.Kiosk { + if shopJustPressed() && !s.run.Player.Moving() && s.run.Level.At(s.run.Player.Tile) == level.Kiosk { s.openShop(g) return nil } @@ -191,16 +191,18 @@ func (s *LevelScene) Draw(g *Game, screen *ebiten.Image) { y0 := max(0, int(s.cam.Y)/TileSize) x1 := min(lvl.Width(), int(s.cam.X+ScreenW)/TileSize+2) y1 := min(lvl.Height(), int(s.cam.Y+ScreenH)/TileSize+2) + z := s.run.Player.Tile.Z // bara spelarens våning ritas var op ebiten.DrawImageOptions for y := y0; y < y1; y++ { for x := x0; x < x1; x++ { - if s.rails[level.Point{X: x, Y: y}] { - s.drawRails(g, screen, level.Point{X: x, Y: y}, ox, oy) + p := level.Point{X: x, Y: y, Z: z} + if s.rails[p] { + s.drawRails(g, screen, p, ox, oy) continue } op.GeoM.Reset() op.GeoM.Translate(ox+float64(x*TileSize), oy+float64(y*TileSize)) - screen.DrawImage(g.Assets.Tiles[lvl.At(x, y)], &op) + screen.DrawImage(g.Assets.Tiles[lvl.At(p)], &op) } } s.drawTrains(g, screen, ox, oy) @@ -211,6 +213,9 @@ func (s *LevelScene) Draw(g *Game, screen *ebiten.Image) { followers := s.run.Party.Followers() for i := len(followers); i >= 1; i-- { from, to := s.run.Party.FollowerTiles(i) + if from.Z != z && to.Z != z { + continue // på en annan våning + } fx := float64(from.X*TileSize) + float64((to.X-from.X)*TileSize)*leader.Progress fy := float64(from.Y*TileSize) + float64((to.Y-from.Y)*TileSize)*leader.Progress facing := sim.Down @@ -296,6 +301,9 @@ func (s *LevelScene) drawHUD(g *Game, screen *ebiten.Image) { dest += " " + r.Level.Destination.Get("ja") } name := r.Level.Name.Get(UILang) + if r.Level.NumFloors() > 1 { + name += " " + r.Level.FloorName(r.Player.Tile.Z) + } a.Draw(screen, name, 4, 0, colText) destX := 4 + a.Width(name) + 12 a.Draw(screen, a.T("hud.destination")+": "+dest, destX, 0, colKnown) @@ -411,7 +419,7 @@ func (s *LevelScene) infoLines(a *Assets) (lines []line, accent color.Color, bar lines = append(lines, a.wrapLine(plainLine(fmt.Sprintf("%s: Track %s %s for %s", a.T("hud.english"), tr.Track, tr.Line.Get("en"), tr.Destination.Get("en")), colEnglish), maxW)...) } - phase, _ := r.PlatformPhase(lvl.At(tile.X, tile.Y)) + phase, _ := r.PlatformPhase(lvl.At(tile)) switch { case phase == level.Closing: accent = colWarn @@ -461,7 +469,7 @@ func (s *LevelScene) infoLines(a *Assets) (lines []line, accent color.Color, bar return lines, accent, bar } - switch lvl.At(tile.X, tile.Y) { + switch lvl.At(tile) { case level.Toilet: if r.Facility.Active { return []line{plainLine(a.T("hud.toilet_busy"), colDim)}, colGood, true diff --git a/internal/game/trains.go b/internal/game/trains.go index 8856037..aad487d 100644 --- a/internal/game/trains.go +++ b/internal/game/trains.go @@ -47,7 +47,7 @@ func (s *LevelScene) buildTracks() { } else { c.Y += k } - if s.run.Level.At(c.X, c.Y) == level.Wall && (c.X == 0 || c.Y == 0 || c.X == s.run.Level.Width()-1 || c.Y == s.run.Level.Height()-1) { + if s.run.Level.At(c) == level.Wall && (c.X == 0 || c.Y == 0 || c.X == s.run.Level.Width()-1 || c.Y == s.run.Level.Height()-1) { s.rails[c] = true } } @@ -74,7 +74,7 @@ func (s *LevelScene) drawRails(g *Game, screen *ebiten.Image, c level.Point, ox, var op ebiten.DrawImageOptions vertical := false for _, tr := range s.tracks { - if !tr.horizontal && (c.X == tr.track.X) { + if !tr.horizontal && c.X == tr.track.X && c.Z == tr.track.Z { vertical = true } } @@ -89,7 +89,11 @@ func (s *LevelScene) drawRails(g *Game, screen *ebiten.Image, c level.Point, ox, // drawTrains ritar tågen i rätt läge för sin fas. func (s *LevelScene) drawTrains(g *Game, screen *ebiten.Image, ox, oy float64) { + z := s.run.Player.Tile.Z for _, tr := range s.tracks { + if tr.platform.Z != z { + continue // tågen på andra våningar syns inte härifrån + } phase, prog := s.run.PlatformPhase(tr.tile) // Strålkastare när tåget är på väg in: ett sken från det håll det kommer. if phase == level.Approaching { @@ -148,6 +152,10 @@ func (s *LevelScene) updateEffects(g *Game) { for _, tr := range s.tracks { phase, prog := r.PlatformPhase(tr.tile) if last, seen := s.lastPhase[tr.tile]; !seen || last != phase { + if tr.platform.Z != r.Player.Tile.Z { + s.lastPhase[tr.tile] = phase + continue // hörs inte från en annan våning + } s.lastPhase[tr.tile] = phase dist := math.Hypot(float64(tr.platform.X*TileSize)-px, float64(tr.platform.Y*TileSize)-py) / TileSize switch phase { @@ -159,7 +167,7 @@ func (s *LevelScene) updateEffects(g *Game) { } // Vind längs perrongen när tåget rullar in eller ut (och strax före). windy := phase == level.Arriving || phase == level.Leaving || (phase == level.Approaching && prog > 0.7) - if windy && len(s.particles) < maxParticles { + if windy && tr.platform.Z == r.Player.Tile.Z && len(s.particles) < maxParticles { dir := 1.0 if phase == level.Arriving { dir = 1.0 // luften trycks framför tåget: samma håll som tåget rör sig (in från minus-sidan) diff --git a/internal/level/level.go b/internal/level/level.go index 3420778..0fa2aea 100644 --- a/internal/level/level.go +++ b/internal/level/level.go @@ -31,13 +31,19 @@ const ( Sign Tile = 'i' // skylt Toilet Tile = 'W' // toalett (sällskapets toamätare) Kiosk Tile = 'K' // kiosk/automat för mat och dryck + StairsUp Tile = 'U' // trappa upp: leder till 'D' på samma x,y en våning upp + StairsDown Tile = 'D' // trappa ner: leder till 'U' på samma x,y en våning ner ) var knownTiles = map[Tile]bool{ Wall: true, Floor: true, Spawn: true, Platform: true, WrongPlatform: true, Gate: true, TicketMachine: true, Sign: true, Toilet: true, Kiosk: true, + StairsUp: true, StairsDown: true, } +// IsStairs rapporterar om rutan är en trappa. +func (t Tile) IsStairs() bool { return t == StairsUp || t == StairsDown } + // Passable rapporterar om en ruta går att gå på (utan hänsyn till spärrar). func (t Tile) Passable() bool { return t != Wall } @@ -169,9 +175,17 @@ type Train struct { type SignText struct { X int `json:"x"` Y int `json:"y"` + Z int `json:"z"` // våning (0 = första i floors) Text Text `json:"text"` } +// Storey är en våning: ett rutnät med namn ("B1", "1F", "2F"). +type Storey struct { + Name string `json:"name"` + Grid []string `json:"grid"` + Floors []Storey `json:"floors,omitempty"` // flervåningsbana; alla våningar lika stora +} + // Rewards är vad en klarad bana ger utöver poäng. type Rewards struct { Items []string `json:"items"` // item-id:n (t.ex. skins) som delas ut första gången @@ -187,24 +201,32 @@ type Level struct { Rewards Rewards `json:"rewards"` Shop []string `json:"shop"` // items som kiosken säljer på banan (tom = seedat urval) Name Text `json:"name"` - Difficulty int `json:"difficulty"` // 1 (lätt) … 5 (svår) - Destination Text `json:"destination"` // dit spelaren ska - TimeLimitSec int `json:"timeLimitSec"` // 0 = ingen tidsgräns - ParSec int `json:"parSec"` // "bra tid" för tre stjärnor (0 = räknas ut från kortaste vägen) - Party int `json:"party"` // antal i sällskapet inkl. spelaren (0 = 1) - TicketRequired bool `json:"ticketRequired"` // spärren kräver pappersbiljett (köps i automaten) i stället för kort - RideStops int `json:"rideStops"` // antal stationer på tågresan (0 = DefaultRideStops) - MaxMissedTrains int `json:"maxMissedTrains"` // >0: fler missade tåg än så förlorar banan - IndividualCards bool `json:"individualCards"` // alla i sällskapet har egna kort (och biljetter) som spärren kräver - SeatSelection bool `json:"seatSelection"` // biljettköpet fortsätter med sittplatsval efter sällskapets önskemål - FareYen int `json:"fareYen"` // vad spärren drar från kortet (0 = standard) - Grid []string `json:"grid"` + Difficulty int `json:"difficulty"` // 1 (lätt) … 5 (svår) + Destination Text `json:"destination"` // dit spelaren ska + TimeLimitSec int `json:"timeLimitSec"` // 0 = ingen tidsgräns + ParSec int `json:"parSec"` // "bra tid" för tre stjärnor (0 = räknas ut från kortaste vägen) + Party int `json:"party"` // antal i sällskapet inkl. spelaren (0 = 1) + TicketRequired bool `json:"ticketRequired"` // spärren kräver pappersbiljett (köps i automaten) i stället för kort + RideStops int `json:"rideStops"` // antal stationer på tågresan (0 = DefaultRideStops) + MaxMissedTrains int `json:"maxMissedTrains"` // >0: fler missade tåg än så förlorar banan + IndividualCards bool `json:"individualCards"` // alla i sällskapet har egna kort (och biljetter) som spärren kräver + SeatSelection bool `json:"seatSelection"` // biljettköpet fortsätter med sittplatsval efter sällskapets önskemål + FareYen int `json:"fareYen"` // vad spärren drar från kortet (0 = standard) + Grid []string `json:"grid,omitempty"` // envåningsbana (kompatibilitet) – blir Floors[0] + Floors []Storey `json:"floors,omitempty"` // flervåningsbana; alla våningar lika stora Trains []Train `json:"trains"` Signs []SignText `json:"signs"` } -// Point är en rutkoordinat. -type Point struct{ X, Y int } +// Point är en rutkoordinat: x, y och våning z. +type Point struct{ X, Y, Z int } + +// Up/Down ger rutan rakt ovanför/under (via trappa). +func (p Point) Up() Point { return Point{X: p.X, Y: p.Y, Z: p.Z + 1} } +func (p Point) Down() Point { return Point{X: p.X, Y: p.Y, Z: p.Z - 1} } + +// dirs är de fyra grannriktningarna på samma våning. +var dirs = [...]Point{{X: 1}, {X: -1}, {Y: 1}, {Y: -1}} // Parse avkodar och validerar en bana. func Parse(data []byte) (*Level, error) { @@ -218,36 +240,77 @@ func Parse(data []byte) (*Level, error) { return &l, nil } -// Width och Height i rutor. +// Width och Height i rutor (alla våningar är lika stora). func (l *Level) Width() int { - if len(l.Grid) == 0 { + if len(l.Floors) == 0 || len(l.Floors[0].Grid) == 0 { return 0 } - return len(l.Grid[0]) + return len(l.Floors[0].Grid[0]) +} +func (l *Level) Height() int { + if len(l.Floors) == 0 { + return 0 + } + return len(l.Floors[0].Grid) } -func (l *Level) Height() int { return len(l.Grid) } -// At returnerar rutan på (x, y); utanför kartan räknas som vägg. -func (l *Level) At(x, y int) Tile { - if y < 0 || y >= len(l.Grid) || x < 0 || x >= len(l.Grid[y]) { +// NumFloors är antalet våningar. +func (l *Level) NumFloors() int { return len(l.Floors) } + +// FloorName är våningens namn ("1F" om det saknas). +func (l *Level) FloorName(z int) string { + if z < 0 || z >= len(l.Floors) || l.Floors[z].Name == "" { + return "1F" + } + return l.Floors[z].Name +} + +// At returnerar rutan på p; utanför kartan (eller våningarna) räknas som vägg. +func (l *Level) At(p Point) Tile { return l.FloorAt(p.Z, p.X, p.Y) } + +// FloorAt returnerar rutan (x, y) på våning z; utanför kartan räknas som vägg. +func (l *Level) FloorAt(z, x, y int) Tile { + if z < 0 || z >= len(l.Floors) { return Wall } - return Tile(l.Grid[y][x]) + g := l.Floors[z].Grid + if y < 0 || y >= len(g) || x < 0 || x >= len(g[y]) { + return Wall + } + return Tile(g[y][x]) } -// Find returnerar alla rutor av en viss typ. +// Find returnerar alla rutor av en viss typ på alla våningar. func (l *Level) Find(t Tile) []Point { var pts []Point - for y, row := range l.Grid { - for x := 0; x < len(row); x++ { - if Tile(row[x]) == t { - pts = append(pts, Point{x, y}) + for z, f := range l.Floors { + for y, row := range f.Grid { + for x := 0; x < len(row); x++ { + if Tile(row[x]) == t { + pts = append(pts, Point{X: x, Y: y, Z: z}) + } } } } return pts } +// Neighbors är rutorna man kan gå till från p: de fyra grannarna på samma +// våning samt trappans andra ände. Passerbarhet avgörs av anroparen. +func (l *Level) Neighbors(p Point) []Point { + out := make([]Point, 0, 5) + for _, d := range dirs { + out = append(out, Point{X: p.X + d.X, Y: p.Y + d.Y, Z: p.Z}) + } + switch l.At(p) { + case StairsUp: + out = append(out, p.Up()) + case StairsDown: + out = append(out, p.Down()) + } + return out +} + // SpawnPoint returnerar startrutan (valideringen garanterar att den finns). func (l *Level) SpawnPoint() Point { return l.Find(Spawn)[0] } @@ -274,25 +337,47 @@ func (l *Level) Validate() error { if l.Order < 0 { return fmt.Errorf("level %s: negativ order", l.ID) } - if len(l.Grid) == 0 { - return fmt.Errorf("level %s: tomt rutnät", l.ID) - } - w := len(l.Grid[0]) - spawns, platforms := 0, 0 - for y, row := range l.Grid { - if len(row) != w { - return fmt.Errorf("level %s: rad %d har längd %d, förväntade %d", l.ID, y, len(row), w) + if len(l.Floors) == 0 { + if len(l.Grid) == 0 { + return fmt.Errorf("level %s: tomt rutnät", l.ID) } - for x := 0; x < len(row); x++ { - t := Tile(row[x]) - if !knownTiles[t] { - return fmt.Errorf("level %s: okänt tecken %q på (%d,%d)", l.ID, row[x], x, y) + l.Floors = []Storey{{Name: "1F", Grid: l.Grid}} + } else if len(l.Grid) > 0 { + return fmt.Errorf("level %s: ange antingen grid eller floors, inte båda", l.ID) + } + l.Grid = l.Floors[0].Grid // kompatibilitet: Grid = första våningen + if len(l.Floors[0].Grid) == 0 { + return fmt.Errorf("level %s: tom första våning", l.ID) + } + w, h := len(l.Floors[0].Grid[0]), len(l.Floors[0].Grid) + spawns, platforms := 0, 0 + for z, f := range l.Floors { + if len(f.Grid) != h { + return fmt.Errorf("level %s: våning %d har %d rader, förväntade %d", l.ID, z, len(f.Grid), h) + } + for y, row := range f.Grid { + if len(row) != w { + return fmt.Errorf("level %s: våning %d rad %d har längd %d, förväntade %d", l.ID, z, y, len(row), w) } - switch t { - case Spawn: - spawns++ - case Platform, WrongPlatform: - platforms++ + for x := 0; x < len(row); x++ { + t := Tile(row[x]) + if !knownTiles[t] { + return fmt.Errorf("level %s: okänt tecken %q på (%d,%d,%d)", l.ID, row[x], x, y, z) + } + switch t { + case Spawn: + spawns++ + case Platform, WrongPlatform: + platforms++ + case StairsUp: + if l.FloorAt(z+1, x, y) != StairsDown { + return fmt.Errorf("level %s: trappa upp på (%d,%d,%d) saknar trappa ner ovanför", l.ID, x, y, z) + } + case StairsDown: + if l.FloorAt(z-1, x, y) != StairsUp { + return fmt.Errorf("level %s: trappa ner på (%d,%d,%d) saknar trappa upp under", l.ID, x, y, z) + } + } } } } @@ -311,7 +396,7 @@ func (l *Level) Validate() error { if len(tr.Platform) != 1 || (Tile(tr.Platform[0]) != Platform && Tile(tr.Platform[0]) != WrongPlatform) { return fmt.Errorf("level %s: tåg %d har ogiltig perrong %q", l.ID, i, tr.Platform) } - if !strings.Contains(strings.Join(l.Grid, ""), tr.Platform) { + if len(l.Find(Tile(tr.Platform[0]))) == 0 { return fmt.Errorf("level %s: tåg %d står vid perrong %q som inte finns i rutnätet", l.ID, i, tr.Platform) } if tr.Correct { @@ -364,9 +449,9 @@ func (l *Level) Validate() error { // Skyltar: varje post måste stå på en 'i'-ruta och varje 'i'-ruta ha text. seen := map[Point]bool{} for i, sg := range l.Signs { - p := Point{sg.X, sg.Y} - if l.At(p.X, p.Y) != Sign { - return fmt.Errorf("level %s: skylt %d på (%d,%d) står inte på en skyltruta", l.ID, i, p.X, p.Y) + p := Point{X: sg.X, Y: sg.Y, Z: sg.Z} + if l.At(p) != Sign { + return fmt.Errorf("level %s: skylt %d på (%d,%d,%d) står inte på en skyltruta", l.ID, i, p.X, p.Y, p.Z) } if seen[p] { return fmt.Errorf("level %s: två skyltar på (%d,%d)", l.ID, p.X, p.Y) @@ -378,7 +463,7 @@ func (l *Level) Validate() error { } for _, p := range l.Find(Sign) { if !seen[p] { - return fmt.Errorf("level %s: skyltrutan (%d,%d) saknar text i \"signs\"", l.ID, p.X, p.Y) + return fmt.Errorf("level %s: skyltrutan (%d,%d,%d) saknar text i \"signs\"", l.ID, p.X, p.Y, p.Z) } } return nil @@ -387,7 +472,7 @@ func (l *Level) Validate() error { // SignAt returnerar skylten på en ruta, eller nil. func (l *Level) SignAt(p Point) *SignText { for i := range l.Signs { - if l.Signs[i].X == p.X && l.Signs[i].Y == p.Y { + if l.Signs[i].X == p.X && l.Signs[i].Y == p.Y && l.Signs[i].Z == p.Z { return &l.Signs[i] } } @@ -413,8 +498,7 @@ func (l *Level) Path(from, to Point, canEnter func(Point) bool) []Point { } return path } - for _, d := range [...]Point{{1, 0}, {-1, 0}, {0, 1}, {0, -1}} { - n := Point{p.X + d.X, p.Y + d.Y} + for _, n := range l.Neighbors(p) { if _, ok := prev[n]; ok || !canEnter(n) { continue } @@ -434,9 +518,8 @@ func (l *Level) Reachable() map[Point]bool { for len(queue) > 0 { p := queue[0] queue = queue[1:] - for _, d := range [...]Point{{1, 0}, {-1, 0}, {0, 1}, {0, -1}} { - n := Point{p.X + d.X, p.Y + d.Y} - if seen[n] || !l.At(n.X, n.Y).Passable() { + for _, n := range l.Neighbors(p) { + if seen[n] || !l.At(n).Passable() { continue } seen[n] = true @@ -472,9 +555,8 @@ func (l *Level) UngatedPlatforms() []Point { for len(queue) > 0 { p := queue[0] queue = queue[1:] - for _, d := range [...]Point{{1, 0}, {-1, 0}, {0, 1}, {0, -1}} { - n := Point{p.X + d.X, p.Y + d.Y} - t := l.At(n.X, n.Y) + for _, n := range l.Neighbors(p) { + t := l.At(n) if seen[n] || !t.Passable() || t == Gate { continue } @@ -518,8 +600,8 @@ func (tr *Train) BoardText() string { // anger om rälsen (och tågets rörelse) går i sidled. func (l *Level) TrackTile(p Point) (tile Point, horizontal bool) { for _, d := range [...]Point{{X: 0, Y: 1}, {X: 1, Y: 0}, {X: -1, Y: 0}, {X: 0, Y: -1}} { - n := Point{X: p.X + d.X, Y: p.Y + d.Y} - if l.At(n.X, n.Y) == Wall { + n := Point{X: p.X + d.X, Y: p.Y + d.Y, Z: p.Z} + if l.At(n) == Wall { return n, d.Y != 0 } } @@ -540,7 +622,7 @@ func (l *Level) CorrectPlatform() Point { } // CanEnter rapporterar om rutan går att gå in på (för sim.Mover). -func (l *Level) CanEnter(p Point) bool { return l.At(p.X, p.Y).Passable() } +func (l *Level) CanEnter(p Point) bool { return l.At(p).Passable() } // TrainAt returnerar tåget vid en perrongruta, eller nil. func (l *Level) TrainAt(t Tile) *Train { diff --git a/internal/level/level_test.go b/internal/level/level_test.go index cc1261e..a80a7df 100644 --- a/internal/level/level_test.go +++ b/internal/level/level_test.go @@ -40,7 +40,7 @@ func TestParseAndSolve(t *testing.T) { if l.Width() != 7 || l.Height() != 5 { t.Fatalf("storlek %dx%d", l.Width(), l.Height()) } - if l.SpawnPoint() != (Point{1, 1}) { + if l.SpawnPoint() != (Point{X: 1, Y: 1}) { t.Fatalf("spawn %v", l.SpawnPoint()) } if !l.Solvable() { @@ -56,14 +56,14 @@ func TestUnsolvableWhenWalledOff(t *testing.T) { if l.Solvable() { t.Fatal("banan ska INTE vara lösbar när perrongen är inmurad") } - if u := l.Unreachable(); len(u) != 1 || u[0] != (Point{5, 1}) { + if u := l.Unreachable(); len(u) != 1 || u[0] != (Point{X: 5, Y: 1}) { t.Fatalf("Unreachable = %v, vill ha [(5,1)]", u) } } func TestOutsideGridIsWall(t *testing.T) { l := mustParse(t, sample) - if l.At(-1, 0).Passable() || l.At(0, 99).Passable() { + if l.FloorAt(0, -1, 0).Passable() || l.FloorAt(0, 0, 99).Passable() || l.At(Point{Z: 5}).Passable() { t.Fatal("utanför kartan ska räknas som vägg") } } @@ -227,19 +227,19 @@ func TestSignAt(t *testing.T) { src := strings.Replace(strings.Replace(sample, `"#S..#P#"`, `"#Si.#P#"`, 1), `"trains"`, `"signs": [{"x": 2, "y": 1, "text": {"sv": "Hej"}}], "trains"`, 1) l := mustParse(t, src) - if sg := l.SignAt(Point{2, 1}); sg == nil || sg.Text.Get("sv") != "Hej" { + if sg := l.SignAt(Point{X: 2, Y: 1}); sg == nil || sg.Text.Get("sv") != "Hej" { t.Fatalf("SignAt = %v", sg) } - if l.SignAt(Point{1, 1}) != nil { + if l.SignAt(Point{X: 1, Y: 1}) != nil { t.Fatal("ingen skylt på startrutan") } } func TestPath(t *testing.T) { l := mustParse(t, sample) - path := l.Path(l.SpawnPoint(), Point{5, 1}, l.CanEnter) + path := l.Path(l.SpawnPoint(), Point{X: 5, Y: 1}, l.CanEnter) // S(1,1) -> (1,2) -> (1,3) -> (2,3) -> (3,3) -> (4,3) -> (5,3) -> (5,2) -> (5,1) - if len(path) != 9 || path[0] != (Point{1, 1}) || path[len(path)-1] != (Point{5, 1}) { + if len(path) != 9 || path[0] != (Point{X: 1, Y: 1}) || path[len(path)-1] != (Point{X: 5, Y: 1}) { t.Fatalf("path = %v", path) } for i := 1; i < len(path); i++ { @@ -248,7 +248,7 @@ func TestPath(t *testing.T) { t.Fatalf("steg %d är inte en grannruta: %v -> %v", i, path[i-1], path[i]) } } - if l.Path(l.SpawnPoint(), Point{0, 0}, l.CanEnter) != nil { + if l.Path(l.SpawnPoint(), Point{X: 0, Y: 0}, l.CanEnter) != nil { t.Fatal("väg in i en vägg ska vara nil") } if p := l.Path(l.SpawnPoint(), l.SpawnPoint(), l.CanEnter); len(p) != 1 { @@ -259,7 +259,7 @@ func TestPath(t *testing.T) { func TestUngatedPlatforms(t *testing.T) { // Perrong utan spärr framför -> rapporteras. l := mustParse(t, sample) - if u := l.UngatedPlatforms(); len(u) != 1 || u[0] != (Point{5, 1}) { + if u := l.UngatedPlatforms(); len(u) != 1 || u[0] != (Point{X: 5, Y: 1}) { t.Fatalf("UngatedPlatforms = %v, vill ha [(5,1)]", u) } // Spärr i enda passagen -> inget rapporteras, men banan är fortfarande lösbar. @@ -280,14 +280,14 @@ func TestPartyAndParDefaults(t *testing.T) { if _, err := Parse([]byte(strings.Replace(sample, `"difficulty": 1`, `"difficulty": 1, "party": 9`, 1))); err == nil { t.Fatal("för stort sällskap ska ge fel") } - if l.CorrectPlatform() != (Point{5, 1}) { + if l.CorrectPlatform() != (Point{X: 5, Y: 1}) { t.Fatalf("CorrectPlatform = %v", l.CorrectPlatform()) } if got := l.Trains[0].BoardText(); got != "番線 A B方面" { t.Fatalf("BoardText = %q", got) } l2 := mustParse(t, strings.Replace(sample, `"#S..#P#"`, `"#SW.KP#"`, 1)) - if l2.At(2, 1) != Toilet || l2.At(4, 1) != Kiosk || !l2.Solvable() { + if l2.FloorAt(0, 2, 1) != Toilet || l2.FloorAt(0, 4, 1) != Kiosk || !l2.Solvable() { t.Fatal("W och K ska vara passerbara rutor") } } @@ -378,12 +378,12 @@ func TestScheduleValidationForPhases(t *testing.T) { func TestTrackTile(t *testing.T) { l := mustParse(t, sample) - tile, horizontal := l.TrackTile(Point{5, 1}) // P uppe till höger: vägg ovanför? nej, (5,0) är vägg -> men ordningen är ner, höger, vänster, upp - if tile != (Point{5, 2}) && tile != (Point{6, 1}) && tile != (Point{5, 0}) { + tile, horizontal := l.TrackTile(Point{X: 5, Y: 1}) // ordningen är ner, höger, vänster, upp + if tile != (Point{X: 5, Y: 2}) && tile != (Point{X: 6, Y: 1}) && tile != (Point{X: 5, Y: 0}) { t.Fatalf("TrackTile = %v", tile) } // (5,2) är golv, (6,1) är vägg -> höger: vertikal räls - if tile == (Point{6, 1}) && horizontal { + if tile == (Point{X: 6, Y: 1}) && horizontal { t.Fatal("räls till höger om perrongen är vertikal") } } @@ -430,3 +430,88 @@ func TestKindOrderParentAndLoadAll(t *testing.T) { t.Fatal("det ska finnas minst en sidobana") } } + +const floorsSample = `{ + "id": "two-floors", "name": {"sv": "Två våningar"}, "difficulty": 2, "destination": {"sv": "x"}, + "floors": [ + {"name": "B1", "grid": [ + "#######", + "#S..U.#", + "#.###.#", + "#.....#", + "#######" + ]}, + {"name": "1F", "grid": [ + "#######", + "#...D.#", + "#.#####", + "#G..i.#", + "#P#####" + ]} + ], + "signs": [{"x": 4, "y": 3, "z": 1, "text": {"sv": "Upp", "en": "Up"}}], + "trains": [{"platform": "P", "track": "1", "line": {"en": "A"}, "destination": {"en": "B"}, "correct": true}] +}` + +func TestFloorsStairsAndPaths(t *testing.T) { + l := mustParse(t, floorsSample) + if l.NumFloors() != 2 || l.FloorName(0) != "B1" || l.FloorName(1) != "1F" || l.FloorName(9) != "1F" { + t.Fatalf("våningar: %d %s %s", l.NumFloors(), l.FloorName(0), l.FloorName(1)) + } + if l.Width() != 7 || l.Height() != 5 || len(l.Grid) != 5 { + t.Fatal("storlek/kompatibilitet") + } + if l.SpawnPoint() != (Point{X: 1, Y: 1, Z: 0}) || l.CorrectPlatform() != (Point{X: 1, Y: 4, Z: 1}) { + t.Fatalf("spawn=%v platform=%v", l.SpawnPoint(), l.CorrectPlatform()) + } + if l.At(Point{X: 4, Y: 1}) != StairsUp || l.At(Point{X: 4, Y: 1, Z: 1}) != StairsDown || !StairsUp.IsStairs() || !StairsUp.Passable() { + t.Fatal("trappor") + } + up := Point{X: 4, Y: 1} + n := l.Neighbors(up) + if len(n) != 5 || n[4] != up.Up() { + t.Fatalf("grannar vid trappa: %v", n) + } + if !l.Solvable() { + t.Fatal("banan ska vara lösbar via trappan") + } + path := l.Path(l.SpawnPoint(), l.CorrectPlatform(), l.CanEnter) + if path == nil || path[0].Z != 0 || path[len(path)-1].Z != 1 { + t.Fatalf("vägen ska byta våning: %v", path) + } + climbed := false + for i := 1; i < len(path); i++ { + if path[i].Z != path[i-1].Z { + climbed = true + if path[i].X != path[i-1].X || path[i].Y != path[i-1].Y || !l.At(path[i-1]).IsStairs() { + t.Fatalf("våningsbyte utan trappa: %v -> %v", path[i-1], path[i]) + } + } + } + if !climbed { + t.Fatal("vägen använde ingen trappa") + } + if u := l.UngatedPlatforms(); len(u) != 0 { + t.Fatalf("perrongen ligger innanför spärren: %v", u) + } + if sg := l.SignAt(Point{X: 4, Y: 3, Z: 1}); sg == nil || l.SignAt(Point{X: 4, Y: 3}) != nil { + t.Fatal("skylten hör till våning 1") + } + if u := l.Unreachable(); len(u) != 0 { + t.Fatalf("onåbart: %v", u) + } +} + +func TestFloorsValidation(t *testing.T) { + bad := map[string]string{ + "trappa utan motpart": strings.Replace(floorsSample, `"#...D.#"`, `"#.....#"`, 1), + "olika storlek": strings.Replace(floorsSample, `"#P#####"`, `"#P######"`, 1), + "både grid och floors": strings.Replace(floorsSample, `"floors"`, `"grid": ["###","#S#","###"], "floors"`, 1), + "skylt på fel våning": strings.Replace(floorsSample, `"z": 1`, `"z": 0`, 1), + } + for name, src := range bad { + if _, err := Parse([]byte(src)); err == nil { + t.Errorf("%s: förväntade fel", name) + } + } +} diff --git a/internal/sim/fog.go b/internal/sim/fog.go index a0a7588..0d96861 100644 --- a/internal/sim/fog.go +++ b/internal/sim/fog.go @@ -9,23 +9,24 @@ const FogRadius = 3 // Fog håller reda på vilka rutor spelaren har sett i den här omgången. // Minikartan ritar bara sedda rutor plus riktmärken som alltid syns. type Fog struct { - w, h int + w, h, floors int seen []bool Radius int count int ExtraLandmarks bool // toaletter, kiosker och automater syns alltid (skillen Lokalkännedom) } -// NewFog skapar en tom dimma för banan. +// NewFog skapar en tom dimma för banan (alla våningar). func NewFog(l *level.Level, radius int) *Fog { - return &Fog{w: l.Width(), h: l.Height(), seen: make([]bool, l.Width()*l.Height()), Radius: radius} + n := max(1, l.NumFloors()) + return &Fog{w: l.Width(), h: l.Height(), floors: n, seen: make([]bool, l.Width()*l.Height()*n), Radius: radius} } func (f *Fog) idx(p level.Point) (int, bool) { - if p.X < 0 || p.Y < 0 || p.X >= f.w || p.Y >= f.h { + if p.X < 0 || p.Y < 0 || p.Z < 0 || p.X >= f.w || p.Y >= f.h || p.Z >= f.floors { return 0, false } - return p.Y*f.w + p.X, true + return (p.Z*f.h+p.Y)*f.w + p.X, true } // Seen rapporterar om rutan har setts. @@ -41,8 +42,9 @@ func (f *Fog) mark(p level.Point) { } } -// Reveal avslöjar allt inom Radius steg från from via passerbara rutor, -// samt väggarna som gränsar till dem (så att gångarnas form syns). +// Reveal avslöjar allt inom Radius steg från from via passerbara rutor på +// samma våning, samt väggarna som gränsar till dem (så att gångarnas form +// syns). Trappans andra ände avslöjas också. func (f *Fog) Reveal(l *level.Level, from level.Point) { dist := map[level.Point]int{from: 0} queue := []level.Point{from} @@ -50,10 +52,16 @@ func (f *Fog) Reveal(l *level.Level, from level.Point) { p := queue[0] queue = queue[1:] f.mark(p) + switch l.At(p) { + case level.StairsUp: + f.mark(p.Up()) + case level.StairsDown: + f.mark(p.Down()) + } for dx := -1; dx <= 1; dx++ { for dy := -1; dy <= 1; dy++ { - n := level.Point{X: p.X + dx, Y: p.Y + dy} - if !l.At(n.X, n.Y).Passable() { + n := level.Point{X: p.X + dx, Y: p.Y + dy, Z: p.Z} + if !l.At(n).Passable() { f.mark(n) // gränsande vägg } } @@ -62,8 +70,8 @@ func (f *Fog) Reveal(l *level.Level, from level.Point) { continue } for _, d := range [...]level.Point{{X: 1}, {X: -1}, {Y: 1}, {Y: -1}} { - n := level.Point{X: p.X + d.X, Y: p.Y + d.Y} - if _, ok := dist[n]; ok || !l.At(n.X, n.Y).Passable() { + n := level.Point{X: p.X + d.X, Y: p.Y + d.Y, Z: p.Z} + if _, ok := dist[n]; ok || !l.At(n).Passable() { continue } dist[n] = dist[p] + 1 @@ -75,8 +83,8 @@ func (f *Fog) Reveal(l *level.Level, from level.Point) { // Landmark är rutor som alltid syns på minikartan: perrongerna (avgångarna), // starten och spärrarna – det man orienterar sig efter. func Landmark(l *level.Level, p level.Point) bool { - switch l.At(p.X, p.Y) { - case level.Platform, level.WrongPlatform, level.Spawn, level.Gate: + switch l.At(p) { + case level.Platform, level.WrongPlatform, level.Spawn, level.Gate, level.StairsUp, level.StairsDown: return true } return false @@ -88,7 +96,7 @@ func (f *Fog) Visible(l *level.Level, p level.Point) bool { return true } if f.ExtraLandmarks { - switch l.At(p.X, p.Y) { + switch l.At(p) { case level.Toilet, level.Kiosk, level.TicketMachine: return true } @@ -98,9 +106,11 @@ func (f *Fog) Visible(l *level.Level, p level.Point) bool { // RevealAll avslöjar hela banan (kartan köpt som item). func (f *Fog) RevealAll(l *level.Level) { - for y := 0; y < l.Height(); y++ { - for x := 0; x < l.Width(); x++ { - f.mark(level.Point{X: x, Y: y}) + for z := 0; z < l.NumFloors(); z++ { + for y := 0; y < l.Height(); y++ { + for x := 0; x < l.Width(); x++ { + f.mark(level.Point{X: x, Y: y, Z: z}) + } } } } @@ -111,14 +121,17 @@ func (f *Fog) Count() int { return f.count } // Explored är andelen passerbara rutor som setts, 0..1. func (f *Fog) Explored(l *level.Level) float64 { total, seen := 0, 0 - for y := 0; y < l.Height(); y++ { - for x := 0; x < l.Width(); x++ { - if !l.At(x, y).Passable() { - continue - } - total++ - if f.Seen(level.Point{X: x, Y: y}) { - seen++ + for z := 0; z < l.NumFloors(); z++ { + for y := 0; y < l.Height(); y++ { + for x := 0; x < l.Width(); x++ { + p := level.Point{X: x, Y: y, Z: z} + if !l.At(p).Passable() { + continue + } + total++ + if f.Seen(p) { + seen++ + } } } } diff --git a/internal/sim/mover.go b/internal/sim/mover.go index 48bb355..2105969 100644 --- a/internal/sim/mover.go +++ b/internal/sim/mover.go @@ -45,7 +45,7 @@ func (m *Mover) Step(dt float64, want Dir, canEnter func(level.Point) bool) int } m.Facing = want dx, dy := want.Delta() - next := level.Point{X: m.Tile.X + dx, Y: m.Tile.Y + dy} + next := level.Point{X: m.Tile.X + dx, Y: m.Tile.Y + dy, Z: m.Tile.Z} if !canEnter(next) { return arrived } diff --git a/internal/sim/run.go b/internal/sim/run.go index 5966e67..4470086 100644 --- a/internal/sim/run.go +++ b/internal/sim/run.go @@ -47,6 +47,8 @@ type Run struct { Progressed []string // ord som fick framsteg vid senaste läsningen, "ord n/3" ProgressedAt float64 // < 0 = aldrig + FloorChanges int // antal trappbyten (statistik) + // Resultat när banan vunnits. Stars int // 1–3 efter tid mot par PointsEarned int // språkpoäng @@ -266,7 +268,7 @@ func (r *Run) StarsFor(t float64) int { // kräver biljett (på biljettbanor) eller ett användbart kort med saldo (att // gå mellan två spärrutor är gratis). func (r *Run) CanEnter(p level.Point) bool { - t := r.Level.At(p.X, p.Y) + t := r.Level.At(p) if !t.Passable() { return false } @@ -315,7 +317,7 @@ func (r *Run) MembersWithoutFare() []*Member { } func (r *Run) inGate() bool { - return r.Level.At(r.Player.Tile.X, r.Player.Tile.Y) == level.Gate + return r.Level.At(r.Player.Tile) == level.Gate } // Step för banan framåt dt sekunder. want är hållen riktning, board=true @@ -342,6 +344,18 @@ func (r *Run) Step(dt float64, want Dir, board bool) { } r.Steps += r.Player.Step(dt, want, r.CanEnter) if r.Player.Tile != before { + // Trappa: hamnar man på en trappruta flyttas man till andra änden + // (samma x, y, en våning upp eller ner) och fortsätter därifrån. + switch r.Level.At(r.Player.Tile) { + case level.StairsUp: + r.Player.Tile = r.Player.Tile.Up() + r.Player.Target = r.Player.Tile + r.FloorChanges++ + case level.StairsDown: + r.Player.Tile = r.Player.Tile.Down() + r.Player.Target = r.Player.Tile + r.FloorChanges++ + } r.Party.OnLeaderArrived(r.Player.Tile) r.Fog.Reveal(r.Level, r.Player.Tile) } @@ -372,7 +386,7 @@ func (r *Run) Step(dt float64, want Dir, board bool) { } } if board && !r.Player.Moving() { - switch r.Level.At(r.Player.Tile.X, r.Player.Tile.Y) { + switch r.Level.At(r.Player.Tile) { case level.Toilet: r.useToilet() case level.Kiosk: @@ -652,7 +666,7 @@ func (r *Run) FacilityProgress() float64 { // spelaren inte står på en perrong), om det är inne, och sekunder kvar // till avgång respektive ankomst. func (r *Run) PlatformHere() (tr *level.Train, atPlatform bool, seconds float64) { - t := r.Level.At(r.Player.Tile.X, r.Player.Tile.Y) + t := r.Level.At(r.Player.Tile) if t != level.Platform && t != level.WrongPlatform { return nil, false, 0 } @@ -679,6 +693,9 @@ func (r *Run) NearTrack(maxDist int) (tr *level.Train, platform level.Point, pha best := maxDist + 1 for _, t := range []level.Tile{level.Platform, level.WrongPlatform} { for _, p := range r.Level.Find(t) { + if p.Z != r.Player.Tile.Z { + continue // man ser och hör bara tågen på sin egen våning + } d := max(abs(p.X-r.Player.Tile.X), abs(p.Y-r.Player.Tile.Y)) if d < best { if cand := r.Level.TrainAt(t); cand != nil { diff --git a/internal/sim/run_test.go b/internal/sim/run_test.go index b0572c1..db5f838 100644 --- a/internal/sim/run_test.go +++ b/internal/sim/run_test.go @@ -85,21 +85,29 @@ func dirTo(from, to level.Point) Dir { // walk låter en bot följa en väg ruta för ruta, en bildruta i taget. Tangenten // släpps så fort steget mot nästa ruta påbörjats, så att boten stannar exakt // där även när farten inte är bildruteexakt (telefon- eller sällskapsfaktor). +// Trappor: att kliva på en trappruta flyttar en till andra änden i samma +// steg, så vägens nästa punkt (motparten) är redan nådd. func walk(t *testing.T, r *Run, path []level.Point) { t.Helper() - for _, next := range path[1:] { - d := dirTo(r.Player.Tile, next) - for i := 0; i < 600; i++ { - if r.Player.Tile == next && !r.Player.Moving() { - break - } - if r.Player.Target == next || r.Player.Tile == next { + for idx := 1; idx < len(path); idx++ { + next := path[idx] + cur := r.Player.Tile + if next.Z != cur.Z && next.X == cur.X && next.Y == cur.Y { + continue // våningsbytet skedde när vi klev på trappan + } + d := dirTo(cur, next) + arrived := func() bool { + p := r.Player.Tile + return !r.Player.Moving() && (p == next || (r.Level.At(next).IsStairs() && p.X == next.X && p.Y == next.Y)) + } + for i := 0; i < 600 && !arrived(); i++ { + if r.Player.Target == next || r.Player.Tile == next || (r.Player.Tile.X == next.X && r.Player.Tile.Y == next.Y) { r.Step(frame, None, false) } else { r.Step(frame, d, false) } } - if r.Player.Tile != next || r.Player.Moving() { + if !arrived() { t.Fatalf("boten fastnade på %v på väg mot %v", r.Player.Tile, next) } } @@ -203,7 +211,7 @@ func TestGateNeedsCardBalance(t *testing.T) { r.Player.Tile, r.Player.Target = gate, gate r.Wallet.Card = 0 other := level.Point{X: gate.X + 1, Y: gate.Y} - if l.At(other.X, other.Y) == level.Gate && !r.CanEnter(other) { + if l.At(other) == level.Gate && !r.CanEnter(other) { t.Fatal("att gå mellan två spärrutor ska inte kosta") } } @@ -635,7 +643,7 @@ func TestPhoneSpeedRescanAndBattery(t *testing.T) { gate := l.Find(level.Gate)[0] path := l.Path(r.Player.Tile, gate, r.CanEnter) for i, p := range path { // stanna på den första spärrutan – där sker skanningen - if l.At(p.X, p.Y) == level.Gate { + if l.At(p) == level.Gate { path = path[:i+1] break } @@ -810,7 +818,7 @@ func TestIndividualCardsAtGateAndMachine(t *testing.T) { before := r.Wallet.Spent path := l1.Path(r.Player.Tile, gate, r.CanEnter) for i, p := range path { - if l1.At(p.X, p.Y) == level.Gate { + if l1.At(p) == level.Gate { path = path[:i+1] break } @@ -981,7 +989,7 @@ func TestRunOptionsFromProgression(t *testing.T) { } path := l.Path(r.Player.Tile, gate, r.CanEnter) for i, p := range path { - if l.At(p.X, p.Y) == level.Gate { + if l.At(p) == level.Gate { path = path[:i+1] break } @@ -1001,3 +1009,58 @@ func TestRunOptionsFromProgression(t *testing.T) { t.Fatal("landmärken från skillen") } } + +const stairsLevel = `{ + "id": "stairs", "name": {"sv": "Trappor"}, "difficulty": 1, "destination": {"sv": "x", "ja": "新宿"}, + "floors": [ + {"name": "B1", "grid": ["#######", "#S..U.#", "#######"]}, + {"name": "1F", "grid": ["#######", "#.P.D.#", "#######"]} + ], + "trains": [{"platform": "P", "track": "1", "line": {"en": "A"}, "destination": {"en": "B", "ja": "新宿"}, "correct": true}] +}` + +func TestStairsMoveBetweenFloors(t *testing.T) { + l, err := level.Parse([]byte(stairsLevel)) + if err != nil { + t.Fatal(err) + } + r := NewRun(l, RunOptions{Seed: rng.FromInt(1)}) + if r.Player.Tile.Z != 0 || r.Fog.Seen(level.Point{X: 2, Y: 1, Z: 1}) { + t.Fatal("startar på B1; 1F är inte sedd än (utom trappans ände)") + } + walk(t, r, []level.Point{pt(1, 1), pt(2, 1), pt(3, 1)}) + if !r.Fog.Seen(level.Point{X: 4, Y: 1, Z: 1}) { + t.Fatal("nära trappan avslöjas dess andra ände") + } + // Gå in på trappan: hamnar på 1F i trappan ner. + for i := 0; i < 120 && r.Player.Tile.Z == 0; i++ { + r.Step(frame, Right, false) + } + if r.Player.Tile != (level.Point{X: 4, Y: 1, Z: 1}) || r.Player.Moving() || r.FloorChanges != 1 { + t.Fatalf("efter trappan: %v moving=%v changes=%d", r.Player.Tile, r.Player.Moving(), r.FloorChanges) + } + if _, _, _, _, _, ok := r.NearTrack(2); !ok { + t.Fatal("på 1F är perrongen nära") + } + // Dimman på 1F är nu avslöjad runt trappan. + if !r.Fog.Seen(level.Point{X: 2, Y: 1, Z: 1}) { + t.Fatal("dimman på 1F ska lätta runt trappan") + } + // Tillbaka ner: kliv av trappan och gå på den igen. + walk(t, r, []level.Point{r.Player.Tile, {X: 3, Y: 1, Z: 1}}) + walk(t, r, []level.Point{{X: 3, Y: 1, Z: 1}, {X: 4, Y: 1, Z: 1}}) + if r.Player.Tile != (level.Point{X: 4, Y: 1, Z: 0}) || r.FloorChanges != 2 { + t.Fatalf("tillbaka till B1: %v changes=%d", r.Player.Tile, r.FloorChanges) + } + // Hela vägen till perrongen med BFS över våningar. Man står i trappans + // nedre ände, så kliv av först – trappan tar en när man kliver PÅ den. + walk(t, r, []level.Point{r.Player.Tile, {X: 3, Y: 1, Z: 0}}) + path := l.Path(r.Player.Tile, l.CorrectPlatform(), r.CanEnter) + if path == nil { + t.Fatal("ingen väg") + } + walk(t, r, path) + if r.Player.Tile != l.CorrectPlatform() { + t.Fatalf("boten nådde inte perrongen: %v", r.Player.Tile) + } +} diff --git a/resources/data/levels/004-tokyo.json b/resources/data/levels/004-tokyo.json new file mode 100644 index 0000000..2d928b7 --- /dev/null +++ b/resources/data/levels/004-tokyo.json @@ -0,0 +1,159 @@ +{ + "destination": { + "en": "Ueno", + "ja": "上野", + "romaji": "Ueno", + "sv": "Ueno" + }, + "difficulty": 3, + "fareYen": 200, + "floors": [ + { + "grid": [ + "#################################", + "#S..#.......#.............#.....#", + "#.#.#.#####.###.#########.#####.#", + "#.#.#.#...#...#.#.....#...#...#.#", + "#.#.#.#.#.###.#.#.###.#.###.#.#.#", + "#.#...#.#...#.#.....#.#.....#...#", + "#.#####.#####.#######.#.#########", + "#.......#...#.#.....#.#.#.......#", + "#.#.#####.#.#.#.###.#.#.#.#####.#", + "#.#.#...#.#.#.#.#...#.#...#...#.#", + "#.#.#.#.#.#.#.#.#.###.###.#.#.#.#", + "#...#.#...#.#...#...#...#.#.#.#.#", + "###.#.#####.#######.###.#.#.#.#.#", + "#...#.....#.......#...#...#.#...#", + "#.#.#####.#.#########.#####.###.#", + "#.#.#...#.#.........#...#...#.#.#", + "#.###.#.#.#######.#.###.#.###.#.#", + "#.#...#...#.....#.#.......#...#.#", + "#.#.#########.#.#.#########.###.#", + "#.....U.......#.#...............#", + "#################################" + ], + "name": "B1" + }, + { + "grid": [ + "#################################", + "#.......#.............#...#.....#", + "#.#####.#.#########.#.#.#.#.#.#.#", + "#..i..#.#.....#...#.#.#.#.#.#.#.#", + "###.#.#.#######.#.#.#.#.#.###.###", + "#...#.#.......#.#...#.#.#...#...#", + "#.###.#######.#.#####.#.#.#.###.#", + "#.........#T#.#.#K..#...#.#...#.#", + "#.#######.#.#.#.###.#####.###.#.#", + "#...#...#...#.#...#.........#..i#", + "#.#.#.#.###.#.###.#.###########.#", + "#W#...#...#.#.#...#.......#...#.#", + "#########.#.#.#.###.#####.#.#.#.#", + "#........i#.#...#.#.#...#...#...#", + "#.#######.#######.#.#.#.#########", + "#...#.....#.........#.#...#.....#", + "###.#.#.###.###########.#.#.###.#", + "#...#.#...#.....#.......#.#...#.#", + "#G###.###.#####.#.#.#####.###.#G#", + "#pG...D.#...........#.........#P#", + "#################################" + ], + "name": "1F" + } + ], + "id": "004-tokyo", + "kind": "main", + "maxMissedTrains": 2, + "name": { + "en": "Tokyo Station", + "ja": "東京駅", + "sv": "Tokyo station" + }, + "order": 4, + "party": 3, + "rideStops": 4, + "shop": [ + "beanie", + "mask_fox", + "hiking_boots", + "omamori", + "conductor_cap" + ], + "signs": [ + { + "text": { + "en": "Tokyo Station, floor 1F. You arrived on the Marunouchi Line underground (B1). Track 4 Yamanote Line for Ueno: south-east. Track 7 Tokaido Line for Shinagawa: south-west.", + "ja": "東京駅 1階 地下 丸ノ内線 ↓ 4番線 山手線 上野方面 → 南東 7番線 東海道線 品川方面 → 南西", + "sv": "Tokyo station, våning 1F. Du kom med Marunouchi-linjen under mark (B1). Spår 4 Yamanote-linjen mot Ueno: sydost. Spår 7 Tokaido-linjen mot Shinagawa: sydväst." + }, + "x": 9, + "y": 13, + "z": 1 + }, + { + "text": { + "en": "\u003c- Track 7 Tokaido Line (Shinagawa) Track 4 Yamanote Line (Ueno) -\u003e", + "ja": "← 7番線 東海道線 品川方面 4番線 山手線 上野方面 →", + "sv": "\u003c- Spår 7 Tokaido-linjen (Shinagawa) Spår 4 Yamanote-linjen (Ueno) -\u003e" + }, + "x": 3, + "y": 3, + "z": 1 + }, + { + "text": { + "en": "Track 4 Yamanote Line for Ueno: the gate in the south-east corner. The stairs down lead back to B1.", + "ja": "4番線 山手線 上野方面 → 南東口 改札 ↓ 地下", + "sv": "Spår 4 Yamanote-linjen mot Ueno: spärren i sydöstra hörnet. Trappan ner leder tillbaka till B1." + }, + "x": 31, + "y": 9, + "z": 1 + } + ], + "ticketRequired": false, + "timeLimitSec": 420, + "trains": [ + { + "correct": true, + "crowded": true, + "destination": { + "en": "Ueno", + "ja": "上野", + "romaji": "Ueno" + }, + "line": { + "en": "Yamanote Line", + "ja": "山手線", + "romaji": "Yamanote-sen" + }, + "platform": "P", + "schedule": { + "dwellSec": 8, + "intervalSec": 24, + "offsetSec": 5 + }, + "track": "4" + }, + { + "correct": false, + "destination": { + "en": "Shinagawa", + "ja": "品川", + "romaji": "Shinagawa" + }, + "line": { + "en": "Tokaido Line", + "ja": "東海道線", + "romaji": "Tokaido-sen" + }, + "platform": "p", + "schedule": { + "dwellSec": 8, + "intervalSec": 28, + "offsetSec": 11 + }, + "track": "7" + } + ] +} \ No newline at end of file diff --git a/resources/sprites/tiles.png b/resources/sprites/tiles.png index 9f95497e0986e3c1927eb8b9c788cbd33627075c..ddeb2f3518d023433ae2f033399a5c63e54c50c5 100644 GIT binary patch literal 1106 zcmV-Y1g-mtP)9yAFP66}vX7&b`di_pCI zFqU9xh7cwUsUep32aKI%*h6C3OHiTVg?f|;_M*|U@^UOCIEcq&GaC^mRTD z0B?7-V}%;yqXQ^2AI)Yw&2>~*V$K1}hZzn4LatE&=(+y|Rwx{mwAlJMN)9w42l{%k zPKwaa8z^;q=7TMi2A95RMrly|L@@vy-a3@j+-S>H%;ibIIbbrA9I>{ONAwSXm4YL) z{&sqXM5MVAPfCIW85?{l6N|-8KVf!J7u|2Q0Kn|x2FEqoUxq+%Szn*H^xFVhz4~9C zCaaV@2{`-NE6ur0G{ebKFk2 z`Ka*W4417LE~~7p{*F?L(#kqcZSHW3vK~?WUR2;8kE3+jzwy~>_H>iKCTgEe4%qI@ z{`Q!9R4{J)?8B5c{?ptqGV3;~+dA}Z5!X=#wj_jfLB^x%Z@HLFlZju1&TYisO|wx^ zW@@R=)&7m7KPig*MdN27-O={@I+M;?`tb(P&6$a$$n^$JsRhqfdG9ETXu#5*Q zWRd*7qzm?XX59axKT-n|pP@7u8n)IbapLu%WO%)uf?_I1Zs-QDr4*psy&>v2ZvH03MGA zE6&3VC(oa|`s(t<`f^E~4s>~&eyBNvYSPf{_n+OU*hr{lFzlQQxXXX+qvW8Kph^xS z8EwAl*&8}}N^qgb0je({M3~cp7f2z zoTh0)r|iveqTMD@#;)o=8?^f;4i>V{B;S6f#&2_qlBtd-Q|+^m9=ZP9P_yx;M_GF? Y0F@O+a0XYTs{jB107*qoM6N<$f>s_Cv;Y7A literal 950 zcmV;n14;aeP)NV?qbgXG|>*)JMG#Hn)wkg@O~_y&@){-Q6R z-=h__KmY#MxMx8PK3Vk1&uraALQ%MVs zcR_M6Gp4z*0X`}G*C!yuw(jsGq``~xn;{Ld{>dVuxsP*_dMzeI3XoYObgNqUBm5uH zX!HqMD(XW&b2|bLdd5` z0_nTxg_L6I`8(VGnCY&6bi{C0^c0qzohPCvM>Bq}5Xo90lFI1V9Y`s1^8bUB!Xc_! zMgD*$h!2GzQypKvzL34#5?^AAhvXEG>~Plj=vYQnFmC_sW?^=IZQzDX-C*h}r<47% z6+KITF>`Iuhj5su3DL>I=1a@*=Eg^05fkPI_@2d|H(6qI_Wq7+!LK?KT**!`uxZIEoyQ?_?ZhKZ)DX(-* zjSr!f3R0I4WAojRL^<^I6ZE3(&F+LuDRJKZWE-0eszv49$(P)`XH73ZQ