From ce4fcad1108848592e33be92390d45292f435839 Mon Sep 17 00:00:00 2001 From: MatusGuy <85036874+MatusGuy@users.noreply.github.com> Date: Mon, 10 Jul 2023 00:20:55 +0100 Subject: [PATCH 01/50] Remove screenshake on death (#2522) This should close #2494 --- src/object/player.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/object/player.cpp b/src/object/player.cpp index 750c00021f9..5c5122f1d17 100644 --- a/src/object/player.cpp +++ b/src/object/player.cpp @@ -2321,7 +2321,7 @@ Player::kill(bool completely) } } - Sector::get().get_camera().shake(0.1f, m_dying ? 32.f : 0.f, m_dying ? 20.f : 10.f); + //Sector::get().get_camera().shake(0.1f, m_dying ? 32.f : 0.f, m_dying ? 20.f : 10.f); } void From 74393507936a42192a0534d4fe19c9c292b4a5bb Mon Sep 17 00:00:00 2001 From: MatusGuy <85036874+MatusGuy@users.noreply.github.com> Date: Mon, 10 Jul 2023 15:25:32 +0100 Subject: [PATCH 02/50] Add physics to mole and set collision groups. (#2519) @Rusty-Box said that placing moles in the editor requires precision to place correctly. With this fix, the mole can be 1 tile above and it will drop down to the ground just like if it was placed precisely. Only minor issue is that the mole might be noticeable when falling. Maybe increase gravity on the mole? --- src/badguy/mole.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/badguy/mole.cpp b/src/badguy/mole.cpp index 610d5d6b7c3..7e18bd9cf3d 100644 --- a/src/badguy/mole.cpp +++ b/src/badguy/mole.cpp @@ -37,7 +37,7 @@ Mole::Mole(const ReaderMapping& reader) : timer(), throw_timer() { - m_physic.enable_gravity(false); + m_physic.enable_gravity(true); SoundManager::current()->preload("sounds/fall.wav"); SoundManager::current()->preload("sounds/squish.wav"); SoundManager::current()->preload("sounds/dartfire.wav"); @@ -132,31 +132,31 @@ Mole::set_state(MoleState new_state) switch (new_state) { case PRE_THROWING: set_action("idle"); - set_colgroup_active(COLGROUP_DISABLED); + set_colgroup_active(COLGROUP_MOVING_ONLY_STATIC); timer.start(MOLE_WAIT_TIME); break; case THROWING: set_action("idle"); - set_colgroup_active(COLGROUP_DISABLED); + set_colgroup_active(COLGROUP_MOVING_ONLY_STATIC); timer.start(THROW_TIME); throw_timer.start(THROW_INTERVAL); break; case POST_THROWING: set_action("idle"); - set_colgroup_active(COLGROUP_DISABLED); + set_colgroup_active(COLGROUP_MOVING_ONLY_STATIC); timer.start(MOLE_WAIT_TIME); break; case PEEKING: set_action("peeking", 1); - set_colgroup_active(COLGROUP_STATIC); + set_colgroup_active(COLGROUP_MOVING_STATIC); break; case DEAD: set_action("squished"); - set_colgroup_active(COLGROUP_DISABLED); + set_colgroup_active(COLGROUP_MOVING_ONLY_STATIC); break; case BURNING: set_action("burning", 1); - set_colgroup_active(COLGROUP_DISABLED); + set_colgroup_active(COLGROUP_MOVING_ONLY_STATIC); break; } From 6caba1d1ad439ac33bfa7f4590654979c2cac458 Mon Sep 17 00:00:00 2001 From: Carsten Wirtz Date: Mon, 10 Jul 2023 19:31:52 +0200 Subject: [PATCH 03/50] Use 'splash.wav' instead of 'splash.ogg' for Jumping Fish 'splash.ogg' is more commonly associated with melting rather than falling into water (it also is rather annoying to constantly hear compared to the other sound). --- src/badguy/fish_jumping.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/badguy/fish_jumping.cpp b/src/badguy/fish_jumping.cpp index fa81a688324..58f08ca764b 100644 --- a/src/badguy/fish_jumping.cpp +++ b/src/badguy/fish_jumping.cpp @@ -81,7 +81,7 @@ FishJumping::collision_tile(uint32_t tile_attributes) if (!m_frozen) start_waiting(); m_col.set_movement(Vector(0, 0)); - SoundManager::current()->play("sounds/splash.ogg", get_pos()); + SoundManager::current()->play("sounds/splash.wav", get_pos()); } } if ((!(tile_attributes & Tile::WATER) || m_frozen) && (tile_attributes & Tile::HURTS)) { From efd65f4531d6a84ade6dd6b593ed3c2bf95959a8 Mon Sep 17 00:00:00 2001 From: MatusGuy <85036874+MatusGuy@users.noreply.github.com> Date: Mon, 10 Jul 2023 19:57:54 +0100 Subject: [PATCH 04/50] Change CODINGSTYLE.md to C++17 (#2525) [ci skip] --- CODINGSTYLE.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CODINGSTYLE.md b/CODINGSTYLE.md index e3c5bea51c6..868ae6000f3 100644 --- a/CODINGSTYLE.md +++ b/CODINGSTYLE.md @@ -2,10 +2,10 @@ ## Language -C++14 is the main langauge used for this project. GCC, Clang and MSVC are supported. +C++17 is the main langauge used for this project. GCC, Clang and MSVC are supported. For better backward compatibilty with older compiler, namely gcc5, -some C++14 features are restricted: +some C++17 features are restricted: * generic lambda functions are not allowed, e.g. `[](auto foo){}` * tuple constructors have to be explicit, e.g. `std::tuple{5, 6}`, not `{5, 6}` From bd6b0c93dd615dee02bbacd941846fc88ff45cef Mon Sep 17 00:00:00 2001 From: mrkubax10 Date: Wed, 12 Jul 2023 20:21:57 +0200 Subject: [PATCH 05/50] Revert "Add physics to mole and set collision groups. (#2519)" It was decided to revert this commit due to issues explained in #2526. --- src/badguy/mole.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/badguy/mole.cpp b/src/badguy/mole.cpp index 7e18bd9cf3d..610d5d6b7c3 100644 --- a/src/badguy/mole.cpp +++ b/src/badguy/mole.cpp @@ -37,7 +37,7 @@ Mole::Mole(const ReaderMapping& reader) : timer(), throw_timer() { - m_physic.enable_gravity(true); + m_physic.enable_gravity(false); SoundManager::current()->preload("sounds/fall.wav"); SoundManager::current()->preload("sounds/squish.wav"); SoundManager::current()->preload("sounds/dartfire.wav"); @@ -132,31 +132,31 @@ Mole::set_state(MoleState new_state) switch (new_state) { case PRE_THROWING: set_action("idle"); - set_colgroup_active(COLGROUP_MOVING_ONLY_STATIC); + set_colgroup_active(COLGROUP_DISABLED); timer.start(MOLE_WAIT_TIME); break; case THROWING: set_action("idle"); - set_colgroup_active(COLGROUP_MOVING_ONLY_STATIC); + set_colgroup_active(COLGROUP_DISABLED); timer.start(THROW_TIME); throw_timer.start(THROW_INTERVAL); break; case POST_THROWING: set_action("idle"); - set_colgroup_active(COLGROUP_MOVING_ONLY_STATIC); + set_colgroup_active(COLGROUP_DISABLED); timer.start(MOLE_WAIT_TIME); break; case PEEKING: set_action("peeking", 1); - set_colgroup_active(COLGROUP_MOVING_STATIC); + set_colgroup_active(COLGROUP_STATIC); break; case DEAD: set_action("squished"); - set_colgroup_active(COLGROUP_MOVING_ONLY_STATIC); + set_colgroup_active(COLGROUP_DISABLED); break; case BURNING: set_action("burning", 1); - set_colgroup_active(COLGROUP_MOVING_ONLY_STATIC); + set_colgroup_active(COLGROUP_DISABLED); break; } From 8fe630e56eac44aa53b239ead4e1c12fb956d7af Mon Sep 17 00:00:00 2001 From: Carsten Wirtz Date: Thu, 13 Jul 2023 00:17:50 +0200 Subject: [PATCH 06/50] New Tux run animation (#2479) Tux walk animation alone doesn't look all that convincing at faster speeds so a run animation has been added. The idea is that Tux starts moving with his walking animation until he reaches max speed. Once the walk animation has played its last frame when at max speed. Tux's run animation will start playing until he gets slower. Exception would be moments like the "level end sequence" or his main menu appearence where Tux should always play the walking animation due to the speed at which he walks in so instances. --- data/images/creatures/tux/big/run-0.png | Bin 0 -> 19207 bytes data/images/creatures/tux/big/run-1.png | Bin 0 -> 19198 bytes data/images/creatures/tux/big/run-2.png | Bin 0 -> 19446 bytes data/images/creatures/tux/big/run-3.png | Bin 0 -> 17125 bytes data/images/creatures/tux/big/run-4.png | Bin 0 -> 19210 bytes data/images/creatures/tux/big/run-5.png | Bin 0 -> 19953 bytes data/images/creatures/tux/big/run-6.png | Bin 0 -> 19430 bytes data/images/creatures/tux/big/run-7.png | Bin 0 -> 17049 bytes .../creatures/tux/big/run_transition-0.png | Bin 0 -> 15684 bytes .../creatures/tux/big/walk_transition-0.png | Bin 0 -> 17703 bytes data/images/creatures/tux/small/run-0.png | Bin 0 -> 14613 bytes data/images/creatures/tux/small/run-1.png | Bin 0 -> 15577 bytes data/images/creatures/tux/small/run-2.png | Bin 0 -> 15328 bytes data/images/creatures/tux/small/run-3.png | Bin 0 -> 14183 bytes data/images/creatures/tux/small/run-4.png | Bin 0 -> 15517 bytes data/images/creatures/tux/small/run-5.png | Bin 0 -> 17061 bytes data/images/creatures/tux/small/run-6.png | Bin 0 -> 17607 bytes data/images/creatures/tux/small/run-7.png | Bin 0 -> 14138 bytes .../creatures/tux/small/run_transition-0.png | Bin 0 -> 14402 bytes .../creatures/tux/small/walk_transition-0.png | Bin 0 -> 12978 bytes data/images/creatures/tux/tux.sprite | 194 ++++++++++++++++-- src/object/player.cpp | 10 +- 22 files changed, 181 insertions(+), 23 deletions(-) create mode 100644 data/images/creatures/tux/big/run-0.png create mode 100644 data/images/creatures/tux/big/run-1.png create mode 100644 data/images/creatures/tux/big/run-2.png create mode 100644 data/images/creatures/tux/big/run-3.png create mode 100644 data/images/creatures/tux/big/run-4.png create mode 100644 data/images/creatures/tux/big/run-5.png create mode 100644 data/images/creatures/tux/big/run-6.png create mode 100644 data/images/creatures/tux/big/run-7.png create mode 100644 data/images/creatures/tux/big/run_transition-0.png create mode 100644 data/images/creatures/tux/big/walk_transition-0.png create mode 100644 data/images/creatures/tux/small/run-0.png create mode 100644 data/images/creatures/tux/small/run-1.png create mode 100644 data/images/creatures/tux/small/run-2.png create mode 100644 data/images/creatures/tux/small/run-3.png create mode 100644 data/images/creatures/tux/small/run-4.png create mode 100644 data/images/creatures/tux/small/run-5.png create mode 100644 data/images/creatures/tux/small/run-6.png create mode 100644 data/images/creatures/tux/small/run-7.png create mode 100644 data/images/creatures/tux/small/run_transition-0.png create mode 100644 data/images/creatures/tux/small/walk_transition-0.png diff --git a/data/images/creatures/tux/big/run-0.png b/data/images/creatures/tux/big/run-0.png new file mode 100644 index 0000000000000000000000000000000000000000..5a3d9243c8c1a29827e2fe39edccdd154e66b88a GIT binary patch literal 19207 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 Ak^lez literal 0 HcmV?d00001 diff --git a/data/images/creatures/tux/big/run-2.png b/data/images/creatures/tux/big/run-2.png new file mode 100644 index 0000000000000000000000000000000000000000..b6c9bfe5dff5e187c48ae073c8fbfc2d4f13404e GIT binary patch literal 19446 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 literal 0 HcmV?d00001 diff --git a/data/images/creatures/tux/big/run-3.png b/data/images/creatures/tux/big/run-3.png new file mode 100644 index 0000000000000000000000000000000000000000..346cbf6e8ad4ad3ea787af55c180f5127774fd28 GIT binary patch literal 17125 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 literal 0 HcmV?d00001 diff --git a/data/images/creatures/tux/big/run-4.png b/data/images/creatures/tux/big/run-4.png new file mode 100644 index 0000000000000000000000000000000000000000..8bdc35c9012469e98076a0821e7024cf845ca967 GIT binary patch literal 19210 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

y`;O-JExDzC}gb+yZ;2zv91cv~@9$9Pc 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 E1^gvJQUCw| literal 0 HcmV?d00001 diff --git a/data/images/creatures/tux/small/run-0.png b/data/images/creatures/tux/small/run-0.png new file mode 100644 index 0000000000000000000000000000000000000000..7425c0ea8acb84ee46c4482e5f79226382db2aa9 GIT binary patch literal 14613 zcmeHtcT|(j)^8}%dj~0@Nr%uO^xk`sCJ;h^&;kj)H|Z!%REqSb^dg}2qJRhp0t!g) z9RWc=xzYDM=e*~B=YHQkYu)?b$y!OCnfdMC-m_=#J$W)uf}y?!F#!Vs001D?(o{7@ zf2*V4vUoV?&$-7V>;M3RU684Tk1@m_=!HZ$!QEj%pFl4d5EcM;0ssPL_j4@MnIzNI zZt4?lF`cBcqCZsmjlVlY9jCje6;<%QxOCms6}BTeW+RiI?;%HScHMY?2|qS+`zH9< zsd;AS`=H#p)Ww_YO=x><&*u4A=-TC3%hBrLJuAEL_-o$dY_a@?&3NuVYypjJp6&j~ z2OW)VC!Mv>Z0+~@8YgqzU;ECW5Wa49ldcnuAh3h(Fb<}FmoFWiN$sqT ziMZy~|4I4+so=bR<*xvPjoEQ=PK}X2u)C%kx%_eVxQldsrpI5XKfG0BovgFwy6^BU z{2#)%8;zRK?WgSgh>3P?nE3mp1>dM+$#yWTIb5= zJ0;V4M=ZSu>LuRnsHwAXc)%rL3$+pMShRGlfBUGy9a%i)1!cc0L;u~nMDlp+_ecrS+q{8IC1&;CLhCKurd$*0I|{E$X(@4YdYaRtv>&Zmd~6OhAUynJv9dX1Pm{o1S*GslatHYcLt>{K zRvfFY#rfFS6+WOXR9u$rR@wZL_vp4j^St(pYH_y?Vx6pNi)N>ds=2LtuI~ENS?iXC zD>@q=uZ^D9xVXu}tGc3NBD?OBPWezHdA{0)9oKjBJ8pZelQ>*{Up(dxa>ArCCx*&% zgJ$a|$67OI^&}UOyB;&1U8}*U0hR_Y}BNl z_?WocOkV0GaR}~uvo`}#r|VQ97s(a)^Yv9mo9A}SD6Grea4wJYULAC_!`?xc_I=BF-pN6>dnDE|l2cWc=`LYB&^B7Nb^qn!nNwA;%~ts3++3x{Lt_5YKx;h+ zjk$PAjwWT`j16(yQGWkw*V#_kcie*2_3|dlMS)_fVeY4z3@tL+mKvZ&=$>=RvdnwBH%C^0?JX0rM9XQuTNK}-X!wf{ zc-!l&a9wq@^15xd_kx3f*xO%O5G*ZCPq=vQr7miDVfc|eHt6eUV>MmT=bBS-es^IzOW3|clwyc&1ELc0sPx6jwbOk8a(lIN^T$<4eIL%k zX5X!hS1O;eDxbjZLwe{tYYDiL#r-`3>6ST7PCoK9>}Nt}J()kbUo@@m)^^J=S}E;| z@c2wqPl1%W+NSM7d&)ycheE%W?gd+&)f}kpj~P8K5)#k9rDnZ#mQOdT zA%)MVem}QK0-z)t&c>sF!?@8U#3_TozA|NZF*V z-_(bvO-+^gGUwQkCN>4(%Y63K#N}>V3%j5TWQqwaY=J(FSGlH6BjzwA82TXWI6!Ky z!C>2Gq#mN;js2 zhyf{i1l8N@g$GgM@pGSI@pe9BHz4$$r7Yc#E{Kn8Sy)H9<;}4(=1de6a%Q%e;{R9- z-Q&b^P--A;^RtwWGip{;VW)bmN*_q%c^8>aTI9-Vv~tT;OKxYAe^?KeRESq@=yySD zLkeH6PU}B@1<;hCZuPc_0V{nnT#)M;rgv6dFnyT#*&ry)VF#Y01m+zuG4eOI@oUCx z<$ktjzC5lZTwaIDk(n!^x-I?|qYabfH1zhqHaA82-6fugqbuLVG|va(uj!?{l^G^% zoEE+$P@tB-h*{)T*N@}K*Cs5qz>a7uX2LK%b@O65?7>72ompc z@VGL&Q?vXz#>9QyEDTU?pNoCY5rn0E&@u&ckg&;8+ow#wGRoY6LH!klCYFTFktk3j zu6d>(A7-uO3GlrLxrZG9(zATOTRtl1pKBh^*^IZ6-jw+Hx#GhyV+^P>sq|xP2jfcb z=bq_UZ+MefDX17KUa2rP^hwv=cf&azs0_va2wsy$-O=TIXj|Wzf!#w>;EJT}FJMSu z+_IlrRc!mAeS<7M>QMSPf3256o7iQToaoS+LPj3ZX9azs-=q5I-F&0&QIZ=fJeLKK zC}xXu>=cZ2cv~h>_`IN$om1@V;nQ#VL8b3^q7^b4y(Fx+z3RIZ?|`saib=0a$TY!@ zPw}~C!?Bi^PzlUj7`x-Q3oFQ;yPnv-V$o?hJg7w~bC~W%K1Wa}0WoQ!l5~x*e{kX!jNMR9XuIj$5(dno5@T zk56@AKvL=9IwW_2TPc#%5sU5o;tsIp!8k1m0*8*G_MTp-#V3| z&0;Bg_r3v$>zK*NTyvKEb~R(4dD(dTvQ(7DR?n$BuZW)~3eJpAeFrX-NdZpuyO%7o zH5VROZ(mNuw(BS5G7hicJScq_3&w8#xZUoBUHfyEdoZ0#vLWi@ z*pPaDcYijH{xE?)L!${qkjjD4lB9%k()?xx0#VGG7&U&hSfb3I!~ZfPn=Ek`aI0cs zm5U-{!r!9?;Nh3WteaScECvbZ!&ptsL&F+TtIY>CDc?3j2eFsf>ADIvjH4U0ny6Vg ztRWw~y-S>NK-hv9=LnZuaQ4z^0<+iB>)d8i_L`&>5{k5GWyF=5U5*Kq+Dk}sERS)l zBe|!JaGwTk&b`FsXC~*c^hL(`wFlCzuZT;^z;AhyC`aX%p0FvFC02IoMwNcES7*Cr zs9pI@SOF8-r#F^jgV!K$4P^4I?LSw)65Q!_my>AtXq1{D6WLnAIQGT|Al%OOO23*) z(!po+7}#zJe|je(bta~&p!-gAgZ_tkjGIo+?VJZVn%$EiTL>MuJP3a>&7z=Pk%@eZ z=xL&X(dG2J7i72XvgMk!k;KpFLR0mJEsObF!$w=l4?IUA&KO)c$NLnWJxMJ*w_c|h zDtBUEGMg@It1^)rUNE)GVWAv(vf<%7Ed?f5A1&NTP{d zWQq_0*^6q{uuE(|22iz@((~D4Gf#S%V1B(P+M>3ttijmwX0Zq6p--UNL9ClctfNB( z`HXCP>WAxHk=M9#mvar{bzd(qqRjaCJN}Xnj5hVln|jz9PggWwca}J@wIF?N=cJ6U z(UQ(E$9*;B;G`IsAFp#^k^AWy$KA@Kzslsm%6s5DmU9a$GcLgwPm z(27s-MJqTA(jTq|6Wd_!tU5uDYU(0w`_lD-Sk~|iJif>KRV;p_xn-bBO8gRefGtz5 zlwSxfFY(}x~gQL_f)#b#u9wEH$JA{N7;TYc? zYM*eoD3!!DiP=9lX7N7FO`HFCRCc#AbP?ta)V(`+JVIMkFpj@hzJyZ`W(vH&DJw>! z!D?6;5`^8C+;kox{^;TOnaXTW!XdbsxV%cMcflvHR>1Eo=XV#nN{qc;{?yRnc>v$N z&Y|H$`i9v;>~qdKsG`+`4It4k#av*Jd8+>Iyp)i#z*)N#?K~eXzPFkW29y3puQz+I zU|!4PZStw7)(3{-{h-A5cq<0cS3_i-G?8@R{_0qML$;W09+rYg%5{<@rpyEF72$|M z;FwYeR2M&4UEt198EemjWSe)sb;HcYG{(^Z>}`q%clKEiRlZ<=+BUsRo$^THI*83o z;$_%YEvtcQ&xN&=?yn@5@zUZZSl&S@ljwF*+yWdf6kd-vI=Ye+_1LcA5|zqn>?~ng zHVnuaX(@j8y3idasctp$nH%rFkj5Z5Wgvf7DG(N1(!|X{$6)(rtR;p9$kgxf&YYR{ zZAcJeqBTB>_04QtR!Yw7=9|bM%#`Do_*8WiD%mrs-fC*Mg|w-SC-6^qa<@eXq8`Yb z@PB95t3Q}K4ossT%v&{Ln_scYS+Gy25@U7LJz+oQTd3tABfk9TNz9D(q(h>9<5o zzPVYSin&q9AdWZ-@YB5+S@p5p@W|p#sa0l*867{QMlrjmkD0Mc#qhnv<rpD6!gzInaxio|qqojR*kFk4Ko@^cyeo3Br!`dr{b4w3$hszZm_ z1gRP`g5=E^{=BnB78Q>k^!Io+c_6KFKU(mIkk77_XX9}mt7fi{SS!yF!7-LfiquUh znFd~M)nW_LnJuw$p57j&6qvOKTg$9^k*m{5#5@a&HlO%dQl&(0v!$6akZtx@nwpVp z9Dl)KNAgZ62hm(b`XJ#kD4*+t1<4mUH|=zS0vm}wf%X<%c@>px-t)ye;R(t~I?vdi zLS>6*0Wr2~LrV>x(ww%u5(z!eI%Ga7H;i$+bLri7DWV2T)Mu1fh$D!}i3%9hs$E`x zmW4(77dI&mm$l_WuEnA#W^7s&@V@xnDUb7mdMmc~126_wEU5?|MC9CO7#da9y(jio zRYf0~V=MUn%V`rUZqEKC(M)%yX{S?2~Whnytne&RL$%oLE4`2Z|H1M@fyxadl3#$%gA!*oJ}6=~=7kjry8XtRNRW zZX)qDLoyy5%^SU%(+S-R(PGMzK%o}(&*{Y&Vci^0_SL~naSg>r8qmJRXz z^2elAlctqFzkAD`T)Bs*PV14PM@(iwtPxbCrF)>qfiy^@p^#BXGS;z`{71P|lp|Iwe3nL44W)Iifs4-L>B zcojkNkuy*u#7Ms{DeAH2Fk8FGIv>wg?g>+cOocv>X?~r|-Ve8xU{$TVA}EHLIz(h8 z4Xlxn$e$X5=MQqh;%0^yy7W;OaM&zctSfF#X4-vZJx*$!+E^y<;!N?t^xjfiT~fT? zTagNX*hEe6!!f3*ZM>F4jJXiu`P5Zy1Tw8J&&{107gR!an2z(YhA)#YU5Zaz{JgYK z{L6u>A`g3E^T5Q=n;fC7wv4WT`%XsrW|L2fh*j|_R2puDX7cDq$0n9pV)TVIb$JMN z&e{PG*aQ>$4EHj^D<%1JUvduOuQyuxc!@rdG-rlIdC~Qj8Y##o;ui7Kj3k04Dmo`H z&hSumZyj&*&Xn9CrS2*g6)F2F&EbI;3cMAHF%=Z`wKdC{l`sM@PiJANLGR*E{UtHj zegA#qqZ}M;N*!!Xj+)jG7WN|iaNGi&g0u^PS}eihmSz?*z91@2DRG6*12QixlTC>O zCJM-^iHEYJ9F`eA^%h$*ikZ#G8*adyt>%waIoa{!$9(HNY@cbe1C}-% z;x%c`Vcdh4_+a+GFw;ma*ohs^n zsEat3yI1Jjn}XD50rj#8gaXFur)pQYt;UlNupdC+(b49w=>26xE{)WPcU7Z8UzVBM zw8*`7d&1mqkOZic1ZVKMEWya)xm#K&2yJnvqjTI>vbP#ud^szmpFEXPNKyEj5mtZu z=iQv|_M;}KH2+ildaikS!~4z|>$vPqS_(wN^0WRCl-SA?qO-FML7*S zr!FyKhfr%!j5F?}O3?&ir&wd(Zt5T625{@G&`n8C4)asS_OG%=@di1f3ISgh){JF% z^<(0l7HONRL4wa+FrH#luV3!*Mesj)u$5)i?0i3h9sjFOf?%THN(0ZCm>x` z6uVuj2AzreB#0He6v%vkOh;^xr0v$Kc~QyIRQ(d%&oHkUCI%{ZL6%C3Y2xb5sF+9z z{qR1V{o&Vg`X z*T(~lpm3e$VOqMVXt4-GI-nJcEkB*YR%^>%i(dr7o&IwQ!L3ww1#@Gfif5QseR{JF z=R06oDL$Xn9XPP$*P(ca=| z1BHt8{s+KqylUIhVfJkyH4&U+m^S>wH^pvxa&ASs4;do(J70=%a7i3F^|xIn^&L*} z$QrH-<@YD*2{1&1O6Hc-^>RC2dJqqEn4M_`8Um7gL22_3WDn9$R9+YA$ULPMgLcqY zi4r$m_BFCoVCGSHSdq9tD^$9UdWp|<$@dzI0!jTfv1Ms6nZV-ixn4&W-I(eiVHVq7 zUgHK1%O@c*r(4M`7X+x+?8_6k7S)f{tNbS?$tqmwt=8TtVesqSpO}kQuZAZt>1ExZ z?o)J2EA1aUZYh3Z$hn)#8?@t5y0qDh&rjRF@}X*a74YCgJHMJLneTJKlYy<&YeYUj2N2{~s^FDEoA1slT?E%Q!E;<>y=118CLdv#cOkTA zB3r0PHev$%lme!s1M)Nm5C0Mz?A-2tk&5Di%GDgZyq4(CW>?haa@3ZCky{eEW+OqJ z+QWEM6eMQxUPGUPJ=RS)R$LAImM@8Ro_M6>rA2E@M_F~N zVtKc65>PyOW`m{ZIjbD@9J^S&Ug<8WqvnGCew~ZSvfNT+_AbLE<*Tx{{DtPgfl3;3 zIw5A3$q~{Yhb;jgY88kgzOZn0cd-B_!9F5>E%3?kG2#H19Q6%BrzbS<`Z_XtgIiu( z+i%TSD&SEyT&%pxc))In5q?w?e^GY~hwuxoje__0QgoizT^;ou(GlZ15q*ANZ#G1s z2V9GJxu6T~TW2oY!2`Z$VM%{5iwZQ%$J);15m%uLa3`}3G9Sb|dG4ImtB9xf zK3PXwIXBt5{KpYxBy0nu?td4xYqS`ae{cEZqo^Ere(Z|D6C}~X_NPSvgJRabptiz% zsYcSVT{reGkr9f5v+Ap55#Na~GMi;V593dO$0>C;(=UtPQ0kSt({{N3C^WZGmkik9 z-nM+9wWF~(F1Gm`NB(UYuCtWZxRirShW-KBFhHiZ@VPz@qQB@z;||W$hjBa|Qh8ck zSX+~D_VLgfS$4l6u-n(hkbuvyawh6ZM9|1z$1vQ1y|fAgMeW(o!liITtJ1zG|ZmPCPKNyjNImja4O>&4m8?A)P|5r{swwL;T8v_Q z*M28VxMZ~{!G27~|D#!8ic&pynxhFDYY^yoKXirZ#wUi^4EyTX&Bf?FXMJA&TfS!J zf~PBtb`S5G?>BYf_ISl_rCc)Kvc7B_G<^sTk`uk`{W*vAkN~`$Hsnv$q?<$bur{s- zQ|@gwYwRMHz0U&A<~R|hm(1OMstqj2#P-cMe7^G>toZqZgK&VN1XjvtR%|1oO0Xu6QHC_^$%n4Ra-v%J>3bz@5)cpx_m#Ln$2_ct> z)RI;yf|^^n3N$ZB1V>+iTM43;aN}PjWfUc4UUZd=v%QI44QqdW-WrS>kYiRxCr24k zlpO>nr$v4@V?82JV595B>tz#da&!_VoH_O-CAB+rkbU=KKEK#bkdEjGuOFp5skcJB z@>I`cwcUWx0cLbZo{M>AJF5So&11%}Rhx#-??hFCB;*Vj*=V%y@%sw%?GhY|d7GM> z4xeu@W=SKw&7b>DcXEY8>I!NcTlGbwu(MKC2{1W68YxlVacFv8LZ>+yHb21f@&^x6 zYwdYN8WcmEo6JMug5~;Pnp|oFEC2v$E*yRKXQ8JHh9W!!A&v+Km|%d1 z7y4`veMYDd;01xY!F+%YFlV@@9NXdRE;b>u0Fq*&v<^ur+c({9_zyWe>zj?vv>tEeMY{1_TA2&HR3q3=i3IYiOiV2Dd zf&|n8;C>=(@&rIxq@xqqSXKQ`3Uo`3&Be#Z3oIn$@9!_@FDi&YItvL)OG^uZM1({{ z1keZpRG_C1BtXCu#r})p4-Qor3W|h#`M?pLz+apY2ZXPW92*;Y9QY?WdK9V_fbRTL z!LRmjdX$f&kQTZjhMo@^AS40;NeO^N1Vp5T{wj|i)zkaCv?uCMFQWY^6aeuO5*7pr zd3gMj1j#WtA}k;#408~GNJ>fzh>J<0qe}z|m6nwJ3xxp^j?M&#`(Lg4MdgU55*3F@ zfFy(=0um0QjsjvJF{pqv#KB2GLfkIW`eN&_8+%-61|s z2&4x(z2KgX2!GT+hD_leFcTlhFSiLxNQsLIOA4b6l#q~;kopJ69EL=p!~7SkFi243 zkC{6{!RlyE2s+&09uQ}kke8?PZ;@XM14d5=?OMpM>_9XB?nloCtb&9=d=N-e1j1d8 z?U%K{UzER-3Ml($x`1^N(BFc;!7#^P8TV(-DMOrvem7->{yXr0VKQ++_)8FFe|A708{GT!UKZ*Yp_J_0z!YdG+#V$Su{+|D> z`hNlZgFzP#g?XY7|DEZ7h5X@`zidQkoB!xTZ*b`SQt0m+=ATjWE0O*`zW$89|Bo8b z)c;ZPZ~6B>?)s0r{w)vuTj2jh*MHpgZ+YO~0{mqD8ae1_##=}j74<8}KcR=d3~&NI!HA9g9Q4xm`Rf-i z+}h6hzk4HQ#JCfwWKXdtYE8jrykCOH0uRqS7Lg?G_E@(+b>m3XMH2tlOL@W6o9Gk# zBw;{V1VtToE`Z-Y@%ZMJurZmJ+|gUBT{l(JVqj13VeWb7VTZzAM>ysMc^zH}i@$?> zvF{NR`Df$m>8RzjSY~?WN7~gJYW-)y_hwuLzOS!mS=qSHHZ59@HtflCk47Gs8NPx|%w`YhgK;?+$M~MJ9JubFNfi@{g_n&V4_b zvEV}l#-(#cE=p@;UwfNH;*CUQWhEh8jG^BYFP+&v zo^=au+bSY#+ZpDon0elQHHwjaf+BjDY+U&f;OMlm_FeH-z-95CHZM^zZk2erwD*Ev ztcLm%I+o&04m|t30Xmi0LsZBmUXG+eL-4s^rm@BDlYGQinU?U_NBYBDWr7@{^g3tPAeTgDEFh-Z zbac|U!HvvDaO_W{nYSseb*$pz6$`|*@gW5Xy?{gWdxOM+HnyI^Vq*QiGxy#-V3G+~ zOVa?oF??ZGX~wSX(Rw_?)pi!^{U+8&^8Li9_RN>{+;G6fy#f+OjM-gw`CSg@%G_ZSBPSr@U*`~-`fD)&tJJEc(!x`@VKq&OPPRPNi%(&GFZg>Fh*pE|7 z&$eaE&hv2zZSPJZ&H&YD|Ajzd1k=H+vu(DULW_S*_@1M+^rYxQ>ESoypBq#D7sBJ< zyZyQV1+15GkSbYb&TL?E{0haRS-jEUbg3d+0m}QV+C$v}=8$BFxA(L4X$pAu#C8?P zH0k21_91G006*mwnFa%sIT`swg1ek_V(+B9SLpKA z>E+wrdrz*%f<^|Ag(-4ijl(UXFU5_m0}{OA+?!8Np0NN7FoKhcPE=;-Fa%ZB20*$| z#=cUOrg8DbsoX@|k0^#qDU7HhxiOi5iEOggdpmC}b5YUPJvWjWCu`DU4sK7seWegB zFFPJ(!?faI!QOe~DHM!zVX#EK#9c?!dPXwCa1mO1y73{SOEA+r%{4jbS&t zwNguN-FEqe4%ioFOfj`24?Xl(T2-N~`qk!~JXW~c+cp{;b|wBT}`D#Ru~O%Cf@%h@@13&g%b6V;VZFBY(^JS z-46NwPZxnPg`-ccqbJJ^kf(z(^P%To_U7AAz)#Nc$5NYc|5tmhm*ClV0@~|fNQHFx zq!h6I;@SYfd|9v-V(5Tb>Vw+-rsHZvCoF5qmB^Nh;ujnhMO{*L*wQ)^zX2C<_vl2B zWXR45%<~hdq#H&{UH)MHq8}GltCCeZAAWPUb+_hq>t2HnF8N6CL*bVd&*h%hhn&5z zTRrCbnRa;;AQ+@ADlZX5@#*ShQ_hS1Xg-LXlBaaB(%fxz6Y(_BN{0~nb*<^Hc9L;o zt0`w9A2-vAjnP#+k&U`)-5t(-0cZpBXm(^-Jf4+X?un|=V1GY0sr&`-{EPo$uyV_J zpvkU=f&9?s;m`Jm+A0(SrLy7V5cj5@`4Mmb%a5l#oAVoIRExWjU%a`?^1oZa^*q)> z@OxcS+ky^{u^hg(!1dl}zt)EVR@dfon`fV#u>@i$~r>A)Qn3SH^t z%F14TUCX%XUw>>TQCDEnNGN;u(d6fX&^0-mm25$DJV<+7DOo(Ut6eH5=rH|^VH1#y_Q`MNZTNa3@?INA;$&)7>c$mGt#A;M*OYLvz zRkBM@U-3O@8cSo3YyElY)^@pL2Yt68z$kS@bF~;Sxj%cx-{BdDOr|`Z*bdw-CE08b z=#Mk{8Zf&X;wg7Rw_o_28vUqeT6}8}SY9O+n6AN@;n#MZ_=1j^Sq&FsA-bMsZg(BK zCLPPsa=5-2*XT1m)|FgBbT2TFHf|v(@_mMH%a5q+ke%UIvPYj6vj|6D2-^^vGV3*0m7qTozx=%V`yNQK9QKomGiQs;Jq^R&-S32?>Z{4+%w+OE{wf>EHFNjso z#54<%$Ev62$tfsA>QnGRTk2w3zNUkFm9VAX#{WItx^^Ci78y0GHqRmB_hK5wEICVF z-hJZkk%N4b1PBNNC* z2i-nvR8FRkaVIa7mU*A`f%&dMAcSb6fTrD2d^l~w^(kp(4gXqB36 z0Q+deh}%;2*_|@tPmdShBx&}^75K0VH6(*#BIY`)G3xmj$a-u&)tKNfTzBImV&UUcIczEbfo zkM4^TK8k!993ogTO58c;pyKhq#&O|EwnDGVl&JLHko&WBaCn(OA$HblP$TxfJT?Ox zabc_!0OS0^Q)X_bRWhQMN+xsHRTv+Xo$1Cgg%f|^H%Dj`yxmkof?)93Hou}^V>Wtz zAsF!ZM%pDtM#CRQ?kwW@1!Foo0K{As?MJFFR%jj-#KZE=JM82U(Sm$J4x+K{22Ue- z9kP^wnj*S!AX6_bnI(%T5uwViiqm@SV|8M>1C0kyRu9o05*85{x7&9_7tQLw)^TSV zqxB>}xce4-jw&S3O;KUoFkh8r1QR24p${abr*x!AWC86XmQo5QkqP8!3Z$!v={EF= zcsuMF{O?)H7d;?)!-h}_4Z)WpWNS8$P;OtN-L(l;)LjYU_^|15vh0@6m)eSC5C z2xj!=f4?gKnB&OzYoFTYb(0D=8*Y>Nth6K#TT$~J5;bWd$ETd4=3j|lsuq>#lRY9c za---{8sUxklwNu-Xy3Bz&XT0iG0`IwW-yPgKr+N7^`W?0E?;oT!c}-xr_G-Q)d2CH%nH zDZWZ2M|QX6zEqi-$tBg1$C9Cvss)+Xlg5ycJsa}I9ow&+w%=xjd1($JY=tj-wIz;> cy2CMMdK0Yk1Rk8BZ;k+3YWk`*%JxzJ3)!_>9smFU literal 0 HcmV?d00001 diff --git a/data/images/creatures/tux/small/run-1.png b/data/images/creatures/tux/small/run-1.png new file mode 100644 index 0000000000000000000000000000000000000000..d7630a97d1171519ebe99302f18890f6d5c6f1dc GIT binary patch literal 15577 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 literal 0 HcmV?d00001 diff --git a/data/images/creatures/tux/small/run-2.png b/data/images/creatures/tux/small/run-2.png new file mode 100644 index 0000000000000000000000000000000000000000..e5f42a7d8e0e942933cefe01f279e3fef40eb8e9 GIT binary patch literal 15328 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 literal 0 HcmV?d00001 diff --git a/data/images/creatures/tux/small/run-3.png b/data/images/creatures/tux/small/run-3.png new file mode 100644 index 0000000000000000000000000000000000000000..c2dd8962c588eae135809b498d08aa8b68965005 GIT binary patch literal 14183 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 literal 0 HcmV?d00001 diff --git a/data/images/creatures/tux/small/run-4.png b/data/images/creatures/tux/small/run-4.png new file mode 100644 index 0000000000000000000000000000000000000000..0f71a8d808e83ac2cf5f5d67813824de18ff3444 GIT binary patch literal 15517 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 literal 0 HcmV?d00001 diff --git a/data/images/creatures/tux/small/run-5.png b/data/images/creatures/tux/small/run-5.png new file mode 100644 index 0000000000000000000000000000000000000000..9109524506747aedd942137bbe6bf607ff79a178 GIT binary patch literal 17061 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 R`pE#GAfqZ>BWV`=zX0%o8bSa7 literal 0 HcmV?d00001 diff --git a/data/images/creatures/tux/small/run-7.png b/data/images/creatures/tux/small/run-7.png new file mode 100644 index 0000000000000000000000000000000000000000..9c8393592d194f3b9f0904a56c0a5a12c0b130eb GIT binary patch literal 14138 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 Hw+Q_|NJxZ8 literal 0 HcmV?d00001 diff --git a/data/images/creatures/tux/small/run_transition-0.png b/data/images/creatures/tux/small/run_transition-0.png new file mode 100644 index 0000000000000000000000000000000000000000..45084c4b4d2802f1422aa5fbf7378ca3f308f40d GIT binary patch literal 14402 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 WXSO7LVoy&iFbXoN(&dt-LH`S-EL1@N literal 0 HcmV?d00001 diff --git a/data/images/creatures/tux/small/walk_transition-0.png b/data/images/creatures/tux/small/walk_transition-0.png new file mode 100644 index 0000000000000000000000000000000000000000..909c3af99a0ba29ed2282278686afefa705e898e GIT binary patch literal 12978 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 literal 0 HcmV?d00001 diff --git a/data/images/creatures/tux/tux.sprite b/data/images/creatures/tux/tux.sprite index c08c3e6a624..8bd0bc704ee 100644 --- a/data/images/creatures/tux/tux.sprite +++ b/data/images/creatures/tux/tux.sprite @@ -141,9 +141,11 @@ (action (name "small-walk-right") - (fps 15.0) + (fps 14.0) + (loop-frame 2) (hitbox 16 26 32 32) - (images "small/walk-0.png" + (images "small/walk_transition-0.png" + "small/walk-0.png" "small/walk-1.png" "small/walk-2.png" "small/walk-3.png" @@ -154,10 +156,33 @@ (action (name "small-walk-left") - (fps 15.0) + (fps 14.0) + (loop-frame 2) (hitbox 16 26 32 32) (mirror-action "small-walk-right")) + (action + (name "small-run-right") + (fps 15.0) + (loop-frame 2) + (hitbox 18 26 32 32) + (images "small/run_transition-0.png" + "small/run-0.png" + "small/run-1.png" + "small/run-2.png" + "small/run-3.png" + "small/run-4.png" + "small/run-5.png" + "small/run-6.png" + "small/run-7.png")) + + (action + (name "small-run-left") + (fps 15.0) + (loop-frame 2) + (hitbox 20 26 32 32) + (mirror-action "small-run-right")) + (action (name "small-jump-right") (fps 15.0) @@ -604,9 +629,11 @@ (action (name "big-walk-right") - (fps 16.0) + (fps 14.0) + (loop-frame 2) (hitbox 16 14 32 64) - (images "big/walk-0.png" + (images "big/walk_transition-0.png" + "big/walk-0.png" "big/walk-1.png" "big/walk-2.png" "big/walk-3.png" @@ -617,10 +644,33 @@ (action (name "big-walk-left") - (fps 16.0) + (fps 14.0) + (loop-frame 2) (hitbox 16 14 32 64) (mirror-action "big-walk-right")) + (action + (name "big-run-right") + (fps 15.0) + (loop-frame 2) + (hitbox 18 14 32 64) + (images "big/run_transition-0.png" + "big/run-0.png" + "big/run-1.png" + "big/run-2.png" + "big/run-3.png" + "big/run-4.png" + "big/run-5.png" + "big/run-6.png" + "big/run-7.png")) + + (action + (name "big-run-left") + (fps 15.0) + (loop-frame 2) + (hitbox 20 14 32 64) + (mirror-action "big-run-right")) + (action (name "big-jump-right") (fps 15.0) @@ -1075,9 +1125,11 @@ (action (name "fire-walk-right") - (fps 16.0) + (fps 14.0) + (loop-frame 2) (hitbox 16 14 32 64) - (images "big/walk-0.png" + (images "big/walk_transition-0.png" + "big/walk-0.png" "big/walk-1.png" "big/walk-2.png" "big/walk-3.png" @@ -1088,11 +1140,34 @@ (action (name "fire-walk-left") - (fps 16.0) + (fps 14.0) + (loop-frame 2) (hitbox 16 14 32 64) (mirror-action "fire-walk-right")) (action + (name "fire-run-right") + (fps 15.0) + (loop-frame 2) + (hitbox 18 14 32 64) + (images "big/run_transition-0.png" + "big/run-0.png" + "big/run-1.png" + "big/run-2.png" + "big/run-3.png" + "big/run-4.png" + "big/run-5.png" + "big/run-6.png" + "big/run-7.png")) + + (action + (name "fire-run-left") + (fps 15.0) + (loop-frame 2) + (hitbox 20 14 32 64) + (mirror-action "fire-run-right")) + + (action (name "fire-jump-right") (fps 15.0) (loops 1) @@ -1546,9 +1621,11 @@ (action (name "ice-walk-right") - (fps 16.0) + (fps 14.0) + (loop-frame 2) (hitbox 16 14 32 64) - (images "big/walk-0.png" + (images "big/walk_transition-0.png" + "big/walk-0.png" "big/walk-1.png" "big/walk-2.png" "big/walk-3.png" @@ -1559,11 +1636,34 @@ (action (name "ice-walk-left") - (fps 16.0) + (fps 14.0) + (loop-frame 2) (hitbox 16 14 32 64) (mirror-action "ice-walk-right")) (action + (name "ice-run-right") + (fps 15.0) + (loop-frame 2) + (hitbox 18 14 32 64) + (images "big/run_transition-0.png" + "big/run-0.png" + "big/run-1.png" + "big/run-2.png" + "big/run-3.png" + "big/run-4.png" + "big/run-5.png" + "big/run-6.png" + "big/run-7.png")) + + (action + (name "ice-run-left") + (fps 15.0) + (loop-frame 2) + (hitbox 20 14 32 64) + (mirror-action "ice-run-right")) + + (action (name "ice-jump-right") (fps 15.0) (loops 1) @@ -2017,9 +2117,11 @@ (action (name "earth-walk-right") - (fps 16.0) + (fps 14.0) + (loop-frame 2) (hitbox 16 14 32 64) - (images "big/walk-0.png" + (images "big/walk_transition-0.png" + "big/walk-0.png" "big/walk-1.png" "big/walk-2.png" "big/walk-3.png" @@ -2030,11 +2132,34 @@ (action (name "earth-walk-left") - (fps 16.0) + (fps 14.0) + (loop-frame 2) (hitbox 16 14 32 64) (mirror-action "earth-walk-right")) (action + (name "earth-run-right") + (fps 15.0) + (loop-frame 2) + (hitbox 18 14 32 64) + (images "big/run_transition-0.png" + "big/run-0.png" + "big/run-1.png" + "big/run-2.png" + "big/run-3.png" + "big/run-4.png" + "big/run-5.png" + "big/run-6.png" + "big/run-7.png")) + + (action + (name "earth-run-left") + (fps 15.0) + (loop-frame 2) + (hitbox 20 14 32 64) + (mirror-action "earth-run-right")) + + (action (name "earth-jump-right") (fps 15.0) (loops 1) @@ -2487,9 +2612,11 @@ (action (name "air-walk-right") - (fps 16.0) + (fps 14.0) + (loop-frame 2) (hitbox 16 14 32 64) - (images "big/walk-0.png" + (images "big/walk_transition-0.png" + "big/walk-0.png" "big/walk-1.png" "big/walk-2.png" "big/walk-3.png" @@ -2500,11 +2627,34 @@ (action (name "air-walk-left") - (fps 16.0) + (fps 14.0) + (loop-frame 2) (hitbox 16 14 32 64) (mirror-action "air-walk-right")) (action + (name "air-run-right") + (fps 15.0) + (loop-frame 2) + (hitbox 18 14 32 64) + (images "big/run_transition-0.png" + "big/run-0.png" + "big/run-1.png" + "big/run-2.png" + "big/run-3.png" + "big/run-4.png" + "big/run-5.png" + "big/run-6.png" + "big/run-7.png")) + + (action + (name "air-run-left") + (fps 15.0) + (loop-frame 2) + (hitbox 20 14 32 64) + (mirror-action "air-run-right")) + + (action (name "air-jump-right") (fps 15.0) (loops 1) @@ -2903,8 +3053,8 @@ "cutscene/credits-0.png")) (action - (name "credits-left") - (fps 10.0) - (hitbox 16 14 32 64) - (mirror-action "credits-right")) + (name "credits-left") + (fps 10.0) + (hitbox 16 14 32 64) + (mirror-action "credits-right")) ) diff --git a/src/object/player.cpp b/src/object/player.cpp index 5c5122f1d17..a3739d3dfe2 100644 --- a/src/object/player.cpp +++ b/src/object/player.cpp @@ -2044,8 +2044,16 @@ Player::draw(DrawingContext& context) m_sprite->set_action(sa_prefix+("-" + IDLE_STAGES[m_idle_stage])+sa_postfix, Sprite::LOOPS_CONTINUED); } } - else { + else + { + if (std::abs(m_physic.get_velocity_x()) >= MAX_RUN_XM-3) + { + m_sprite->set_action(sa_prefix+"-run"+sa_postfix); + } + else + { m_sprite->set_action(sa_prefix+"-walk"+sa_postfix); + } } } From 3234bacd1f54e11a52091ffb525ea53e8861bd89 Mon Sep 17 00:00:00 2001 From: Tobias Markus Date: Thu, 13 Jul 2023 07:43:56 +0200 Subject: [PATCH 07/50] Fixed a few linter issues --- src/badguy/crusher.cpp | 2 +- src/gui/dialog.cpp | 2 +- src/object/custom_particle_system.cpp | 2 +- src/scripting/wrapper.cpp | 44 ++++++++------------------- src/supertux/gameconfig.cpp | 2 +- 5 files changed, 16 insertions(+), 36 deletions(-) diff --git a/src/badguy/crusher.cpp b/src/badguy/crusher.cpp index 51d2ba15c16..c7af75e8544 100644 --- a/src/badguy/crusher.cpp +++ b/src/badguy/crusher.cpp @@ -115,7 +115,7 @@ Crusher::collision(GameObject& other, const CollisionHit& hit) badguy->kill_fall(); } - auto heavy_coin = dynamic_cast(&other); + const auto heavy_coin = dynamic_cast(&other); if (heavy_coin) { return ABORT_MOVE; } diff --git a/src/gui/dialog.cpp b/src/gui/dialog.cpp index 5e177a87be4..59d954206bd 100644 --- a/src/gui/dialog.cpp +++ b/src/gui/dialog.cpp @@ -229,12 +229,12 @@ Dialog::draw(DrawingContext& context) for (int i = 0; i < static_cast(m_buttons.size()); ++i) { float segment_width = bg_rect.get_width() / static_cast(m_buttons.size()); - float button_width = segment_width; Vector pos(bg_rect.get_left() + segment_width/2.0f + static_cast(i) * segment_width, bg_rect.get_bottom() - 12); if (i == m_selected_button) { + float button_width = segment_width; float button_height = 24.0f; float blink = (sinf(g_real_time * math::PI * 1.0f)/2.0f + 0.5f) * 0.5f + 0.25f; context.color().draw_filled_rect(Rectf(Vector(pos.x - button_width/2, pos.y - button_height/2), diff --git a/src/object/custom_particle_system.cpp b/src/object/custom_particle_system.cpp index fb7dace1bb3..feb6cb204bd 100644 --- a/src/object/custom_particle_system.cpp +++ b/src/object/custom_particle_system.cpp @@ -741,7 +741,6 @@ CustomParticleSystem::update(float dt_sec) auto c = get_collision(particle, Vector(particle->speedX, particle->speedY) * dt_sec); float speed_angle = atanf(-particle->speedY / particle->speedX); - float face_angle = atanf(c.slope_normal.y / c.slope_normal.x); if (c.slope_normal.x == 0.f && c.slope_normal.y == 0.f) { auto cX = get_collision(particle, Vector(particle->speedX, 0) * dt_sec); if (cX.left != cX.right) @@ -750,6 +749,7 @@ CustomParticleSystem::update(float dt_sec) if (cY.top != cY.bottom) particle->speedY *= -1; } else { + float face_angle = atanf(c.slope_normal.y / c.slope_normal.x); float dest_angle = face_angle * 2.f - speed_angle; // Reflect the angle around face_angle float dX = cosf(dest_angle), dY = sinf(dest_angle); diff --git a/src/scripting/wrapper.cpp b/src/scripting/wrapper.cpp index 82b0ea0333e..9a9fad35e44 100644 --- a/src/scripting/wrapper.cpp +++ b/src/scripting/wrapper.cpp @@ -1251,7 +1251,7 @@ static SQInteger Clouds_set_amount_wrapper(HSQUIRRELVM vm) static SQInteger ConveyorBelt_release_hook(SQUserPointer ptr, SQInteger ) { - auto _this = reinterpret_cast (ptr); + scripting::ConveyorBelt* _this = reinterpret_cast (ptr); delete _this; return 0; } @@ -1263,11 +1263,7 @@ static SQInteger ConveyorBelt_start_wrapper(HSQUIRRELVM vm) sq_throwerror(vm, _SC("'start' called without instance")); return SQ_ERROR; } - auto _this = reinterpret_cast (data); - - if (_this == nullptr) { - return SQ_ERROR; - } + scripting::ConveyorBelt* _this = reinterpret_cast (data); try { @@ -1292,11 +1288,7 @@ static SQInteger ConveyorBelt_stop_wrapper(HSQUIRRELVM vm) sq_throwerror(vm, _SC("'stop' called without instance")); return SQ_ERROR; } - auto _this = reinterpret_cast (data); - - if (_this == nullptr) { - return SQ_ERROR; - } + scripting::ConveyorBelt* _this = reinterpret_cast (data); try { @@ -1321,11 +1313,7 @@ static SQInteger ConveyorBelt_move_left_wrapper(HSQUIRRELVM vm) sq_throwerror(vm, _SC("'move_left' called without instance")); return SQ_ERROR; } - auto _this = reinterpret_cast (data); - - if (_this == nullptr) { - return SQ_ERROR; - } + scripting::ConveyorBelt* _this = reinterpret_cast (data); try { @@ -1350,11 +1338,7 @@ static SQInteger ConveyorBelt_move_right_wrapper(HSQUIRRELVM vm) sq_throwerror(vm, _SC("'move_right' called without instance")); return SQ_ERROR; } - auto _this = reinterpret_cast (data); - - if (_this == nullptr) { - return SQ_ERROR; - } + scripting::ConveyorBelt* _this = reinterpret_cast (data); try { @@ -1379,11 +1363,7 @@ static SQInteger ConveyorBelt_set_speed_wrapper(HSQUIRRELVM vm) sq_throwerror(vm, _SC("'set_speed' called without instance")); return SQ_ERROR; } - auto _this = reinterpret_cast (data); - - if (_this == nullptr) { - return SQ_ERROR; - } + scripting::ConveyorBelt* _this = reinterpret_cast (data); SQFloat arg0; if(SQ_FAILED(sq_getfloat(vm, 2, &arg0))) { @@ -1392,7 +1372,7 @@ static SQInteger ConveyorBelt_set_speed_wrapper(HSQUIRRELVM vm) } try { - _this->set_speed(static_cast (arg0)); + _this->set_speed(arg0); return 0; @@ -14051,35 +14031,35 @@ void register_supertux_wrapper(HSQUIRRELVM v) } sq_pushstring(v, "start", -1); sq_newclosure(v, &ConveyorBelt_start_wrapper, 0); - sq_setparamscheck(v, SQ_MATCHTYPEMASKSTRING, "x|t"); + sq_setparamscheck(v, SQ_MATCHTYPEMASKSTRING, "."); if(SQ_FAILED(sq_createslot(v, -3))) { throw SquirrelError(v, "Couldn't register function 'start'"); } sq_pushstring(v, "stop", -1); sq_newclosure(v, &ConveyorBelt_stop_wrapper, 0); - sq_setparamscheck(v, SQ_MATCHTYPEMASKSTRING, "x|t"); + sq_setparamscheck(v, SQ_MATCHTYPEMASKSTRING, "."); if(SQ_FAILED(sq_createslot(v, -3))) { throw SquirrelError(v, "Couldn't register function 'stop'"); } sq_pushstring(v, "move_left", -1); sq_newclosure(v, &ConveyorBelt_move_left_wrapper, 0); - sq_setparamscheck(v, SQ_MATCHTYPEMASKSTRING, "x|t"); + sq_setparamscheck(v, SQ_MATCHTYPEMASKSTRING, "."); if(SQ_FAILED(sq_createslot(v, -3))) { throw SquirrelError(v, "Couldn't register function 'move_left'"); } sq_pushstring(v, "move_right", -1); sq_newclosure(v, &ConveyorBelt_move_right_wrapper, 0); - sq_setparamscheck(v, SQ_MATCHTYPEMASKSTRING, "x|t"); + sq_setparamscheck(v, SQ_MATCHTYPEMASKSTRING, "."); if(SQ_FAILED(sq_createslot(v, -3))) { throw SquirrelError(v, "Couldn't register function 'move_right'"); } sq_pushstring(v, "set_speed", -1); sq_newclosure(v, &ConveyorBelt_set_speed_wrapper, 0); - sq_setparamscheck(v, SQ_MATCHTYPEMASKSTRING, "x|tn"); + sq_setparamscheck(v, SQ_MATCHTYPEMASKSTRING, ".b|n"); if(SQ_FAILED(sq_createslot(v, -3))) { throw SquirrelError(v, "Couldn't register function 'set_speed'"); } diff --git a/src/supertux/gameconfig.cpp b/src/supertux/gameconfig.cpp index a11921f4193..e6ead3275c8 100644 --- a/src/supertux/gameconfig.cpp +++ b/src/supertux/gameconfig.cpp @@ -525,7 +525,7 @@ bool Config::is_christmas() const { std::time_t time = std::time(nullptr); - std::tm* now = std::localtime(&time); + const std::tm* now = std::localtime(&time); /* Activate Christmas mode from Dec 6th until Dec 31st. */ return now->tm_mday >= 6 && now->tm_mon == 11; From 517c2939d77fcc5242ce047eadd0a07d609fbd00 Mon Sep 17 00:00:00 2001 From: MatusGuy <85036874+MatusGuy@users.noreply.github.com> Date: Thu, 13 Jul 2023 13:22:51 +0100 Subject: [PATCH 08/50] Rublight sparkle is played when someone steps on it. (#2534) In this case, "someone" means the player, a walking badguy, or an explosion. More of a feature than a fix. Also changed light colour to white to match current sprite. --- data/images/objects/rublight/rublight.sprite | 44 ++++++++++---------- src/object/rublight.cpp | 12 ++++-- 2 files changed, 31 insertions(+), 25 deletions(-) diff --git a/data/images/objects/rublight/rublight.sprite b/data/images/objects/rublight/rublight.sprite index f5568b4104f..323f1057f14 100644 --- a/data/images/objects/rublight/rublight.sprite +++ b/data/images/objects/rublight/rublight.sprite @@ -1,22 +1,22 @@ -(supertux-sprite - (action - (name "default") - (fps 8) - (images "rublight-0.png" - "rublight-1.png" - "rublight-2.png" - "rublight-3.png" - "rublight-4.png" - "rublight-5.png" - "rublight-6.png" - "rublight-7.png" - "rublight-8.png" - "rublight-9.png" - "rublight-10.png" - "rublight-0.png" - "rublight-0.png" - "rublight-0.png" - "rublight-0.png" - "rublight-0.png") - ) -) +(supertux-sprite + (action + (name "active") + (fps 8) + (loops 1) + (images "rublight-0.png" + "rublight-1.png" + "rublight-2.png" + "rublight-3.png" + "rublight-4.png" + "rublight-5.png" + "rublight-6.png" + "rublight-7.png" + "rublight-8.png" + "rublight-9.png" + "rublight-10.png") + ) + (action + (name "inactive") + (images "rublight-0.png") + ) +) diff --git a/src/object/rublight.cpp b/src/object/rublight.cpp index 586d7172465..90b134d22c6 100644 --- a/src/object/rublight.cpp +++ b/src/object/rublight.cpp @@ -33,11 +33,11 @@ RubLight::RubLight(const ReaderMapping& mapping) : stored_energy(0), light(SpriteManager::current()->create( "images/objects/lightmap_light/lightmap_light.sprite")), - color(1.0f, 0.5f, 0.3f), + color(1.f, 1.f, 1.f), fading_speed(5.0f), strength_multiplier(1.0f) { - m_sprite->set_action("normal"); + m_sprite->set_action("inactive"); std::vector vColor; if (mapping.get("color", vColor)) @@ -95,6 +95,7 @@ void RubLight::rub(float strength) { if (strength <= 0) return; + m_sprite->set_action("active"); strength *= strength_multiplier; stored_energy = std::max(stored_energy, strength); if (state == STATE_DARK) @@ -104,9 +105,14 @@ void RubLight::rub(float strength) void RubLight::update(float dt_sec) { + if (m_sprite->get_action() == "active" && m_sprite->animation_done()) + { + m_sprite->set_action("inactive"); + } + switch (state) { case STATE_DARK: - // Nothing to do + m_sprite->set_action("inactive"); break; case STATE_FADING: From 23131b0e288760b8f858b51921add20ef24e993a Mon Sep 17 00:00:00 2001 From: MatusGuy <85036874+MatusGuy@users.noreply.github.com> Date: Thu, 13 Jul 2023 20:00:36 +0100 Subject: [PATCH 09/50] Unstable tile now refuses to respawn when tux is occupying original position (#2530) Apparently #1998 might've tried to fix this? --- src/object/unstable_tile.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/object/unstable_tile.cpp b/src/object/unstable_tile.cpp index 1dbc157d100..08fcaae4963 100644 --- a/src/object/unstable_tile.cpp +++ b/src/object/unstable_tile.cpp @@ -198,7 +198,7 @@ UnstableTile::update(float dt_sec) { if (m_revive_timer.check()) { - if (Sector::current() && Sector::get().is_free_of_movingstatics(m_col.m_bbox.grown(-1.f))) + if (Sector::get().is_free_of_movingstatics(Rectf(m_original_pos, get_bbox().get_size()).grown(-1.f))) { revive(); } From ab1386570271106d3515ab0f8883c4a44499dd4b Mon Sep 17 00:00:00 2001 From: Vankata453 <78196474+Vankata453@users.noreply.github.com> Date: Fri, 14 Jul 2023 00:12:01 +0300 Subject: [PATCH 10/50] PathGameObject saveable check now checks for references in all sectors Fixes an issue, introduced in #2320, regarding the `PathGameObject::is_saveable()` function's implementation. The issue caused paths to not be saveable, if the current sector, opened in the editor, does not have any PathObjects that reference them. The check is now performed for all sectors in a level. --- src/object/path_gameobject.cpp | 12 +++++++----- src/supertux/level.hpp | 1 + 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/object/path_gameobject.cpp b/src/object/path_gameobject.cpp index 4ef36d849fd..fb99f82ddb0 100644 --- a/src/object/path_gameobject.cpp +++ b/src/object/path_gameobject.cpp @@ -24,6 +24,7 @@ #include "sprite/sprite.hpp" #include "sprite/sprite_manager.hpp" #include "supertux/debug.hpp" +#include "supertux/level.hpp" #include "supertux/sector.hpp" #include "util/log.hpp" #include "util/reader_mapping.hpp" @@ -215,11 +216,12 @@ PathGameObject::is_saveable() const if (!Sector::current()) return false; - const auto& path_objects = Sector::get().get_objects_by_type(); - - for (const auto& path_obj : path_objects) - if (path_obj.get_path_gameobject() == this) - return true; + for (const auto& sector : Level::current()->get_sectors()) + { + for (const auto& path_obj : sector->get_objects_by_type()) + if (path_obj.get_path_gameobject() == this) + return true; + } return false; } diff --git a/src/supertux/level.hpp b/src/supertux/level.hpp index 67ec6f6c4e8..6d77d8f0980 100644 --- a/src/supertux/level.hpp +++ b/src/supertux/level.hpp @@ -52,6 +52,7 @@ class Level final size_t get_sector_count() const; Sector* get_sector(size_t num) const; + const std::vector >& get_sectors() const { return m_sectors; } std::string get_tileset() const { return m_tileset; } From 9cfad1a02c2027231ef70468439d08517f43492a Mon Sep 17 00:00:00 2001 From: Tobias Markus Date: Sat, 15 Jul 2023 21:03:23 +0200 Subject: [PATCH 11/50] Move tilemap position checks into its own function --- src/editor/overlay_widget.cpp | 40 +++++++++++------------------------ src/editor/overlay_widget.hpp | 1 + 2 files changed, 13 insertions(+), 28 deletions(-) diff --git a/src/editor/overlay_widget.cpp b/src/editor/overlay_widget.cpp index d41ffc01cc8..9e26db89a2b 100644 --- a/src/editor/overlay_widget.cpp +++ b/src/editor/overlay_widget.cpp @@ -153,14 +153,7 @@ void EditorOverlayWidget::input_tile(const Vector& pos, uint32_t tile) { auto tilemap = m_editor.get_selected_tilemap(); - if (!tilemap) { - return; - } - - if ( pos.x < 0 || - pos.y < 0 || - pos.x >= static_cast(tilemap->get_width()) || - pos.y >= static_cast(tilemap->get_height())) { + if (!tilemap || !is_position_inside_tilemap(tilemap, pos)) { return; } @@ -172,14 +165,7 @@ void EditorOverlayWidget::autotile(const Vector& pos, uint32_t tile) { auto tilemap = m_editor.get_selected_tilemap(); - if (!tilemap) { - return; - } - - if ( pos.x < 0 || - pos.y < 0 || - pos.x >= static_cast(tilemap->get_width()) || - pos.y >= static_cast(tilemap->get_height())) { + if (!tilemap || !is_position_inside_tilemap(tilemap, pos)) { return; } @@ -211,14 +197,7 @@ EditorOverlayWidget::autotile_corner(const Vector& pos, uint32_t tile, TileMap::AutotileCornerOperation op) { auto tilemap = m_editor.get_selected_tilemap(); - if (!tilemap) { - return; - } - - if ( pos.x < 0 || - pos.y < 0 || - pos.x >= static_cast(tilemap->get_width()) || - pos.y >= static_cast(tilemap->get_height())) { + if (!tilemap || !is_position_inside_tilemap(tilemap, pos)) { return; } @@ -461,10 +440,7 @@ EditorOverlayWidget::fill() Vector tpos = pos - m_hovered_tile; // Tests for being inside tilemap: - if ( pos.x < 0 || - pos.y < 0 || - pos.x >= static_cast(tilemap->get_width()) || - pos.y >= static_cast(tilemap->get_height())) + if (!is_position_inside_tilemap(tilemap, pos)) { pos_stack.pop_back(); continue; @@ -1522,4 +1498,12 @@ EditorOverlayWidget::align_to_tilemap(const Vector& sp, int tile_size) const return glm::trunc(sp_) * static_cast(tile_size); } +bool +EditorOverlayWidget::is_position_inside_tilemap(const TileMap* tilemap, const Vector& pos) const +{ + return pos.x > 0 && pos.y > 0 && + pos.x <= static_cast(tilemap->get_width()) && + pos.y <= static_cast(tilemap->get_height()); +} + /* EOF */ diff --git a/src/editor/overlay_widget.hpp b/src/editor/overlay_widget.hpp index f657b8dda69..63e0b2801ca 100644 --- a/src/editor/overlay_widget.hpp +++ b/src/editor/overlay_widget.hpp @@ -111,6 +111,7 @@ class EditorOverlayWidget final : public Widget Vector sp_to_tp(const Vector& sp, int tile_size = 32) const; Vector tile_screen_pos(const Vector& tp, int tile_size = 32) const; Vector align_to_tilemap(const Vector& sp, int tile_size = 32) const; + bool is_position_inside_tilemap(const TileMap* tilemap, const Vector& pos) const; // in sector position Rectf drag_rect() const; From ec6035dfa1f4bb22f67007f029365d5b570fbbc3 Mon Sep 17 00:00:00 2001 From: Vankata453 <78196474+Vankata453@users.noreply.github.com> Date: Sun, 16 Jul 2023 22:27:33 +0300 Subject: [PATCH 12/50] Refactor WorldMap to support functional sectors (#2377) Refactored `WorldMap` class, which supports functional sectors. Teleporting to a different sector in the same worldmap is now possible. This removes the need for using the unintuitive in most cases alternative, which is having multiple worldmaps. `WorldMapSector` is the new class which handles sectors in a worldmap. It inherits `SectorBase`, which is a base for the `Sector` class that is used in levels, but only with its basic properties and functions included. This allows `WorldMapSector` to be parsed using `WorldMapSectorParser`, which inherits `SectorParser`. Teleporting from one sector to another is possible both with scripting and using teleporters. This pull request also features some general organizations/improvements to worldmap-related code: * Editor worldmap object classes and main functional worldmap object classes have been merged into one for easier management. * A fix for the scripting issue, previously attempted to be fixed in #2375, is incorporated in this PR, where the approach taken is to keep a reference to each `GameObject`'s parent `GameObjectManager`. Fixes #1511. --- data/images/engine/editor/objects.stoi | 5 +- data/levels/world1/worldmap.stwm | 2 +- src/editor/editor.cpp | 3 +- src/editor/overlay_widget.cpp | 4 +- src/editor/worldmap_objects.cpp | 280 ------- src/editor/worldmap_objects.hpp | 170 ----- src/object/tilemap.cpp | 2 +- src/scripting/badguy.hpp | 4 +- src/scripting/dispenser.hpp | 8 +- src/scripting/floating_image.cpp | 2 +- src/scripting/functions.cpp | 8 +- src/scripting/game_object.cpp | 15 +- src/scripting/game_object.hpp | 8 +- src/scripting/game_object_manager.hpp | 4 +- src/scripting/willowisp.hpp | 8 +- .../{worldmap.cpp => worldmap_sector.cpp} | 29 +- src/scripting/worldmap_sector.hpp | 92 +++ src/scripting/wrapper.cpp | 150 +++- src/scripting/wrapper.hpp | 4 +- src/scripting/wrapper.interface.hpp | 2 +- src/squirrel/exposed_object.hpp | 2 +- src/supertux/colorscheme.cpp | 7 +- src/supertux/d_scope.cpp | 2 +- src/supertux/d_scope.hpp | 10 +- src/supertux/game_object.hpp | 2 + src/supertux/game_object_factory.cpp | 22 +- src/supertux/game_session.cpp | 10 +- src/supertux/menu/worldmap_cheat_menu.cpp | 27 +- src/supertux/object_factory.hpp | 6 +- src/supertux/sector.cpp | 66 +- src/supertux/sector.hpp | 43 +- src/supertux/sector_base.cpp | 38 + src/supertux/sector_base.hpp | 66 ++ src/supertux/sector_parser.cpp | 110 +-- src/supertux/sector_parser.hpp | 25 +- src/worldmap/camera.cpp | 21 +- src/worldmap/direction.cpp | 8 +- src/worldmap/direction.hpp | 3 - src/worldmap/level_tile.cpp | 110 ++- src/worldmap/level_tile.hpp | 28 +- src/worldmap/spawn_point.cpp | 37 +- src/worldmap/spawn_point.hpp | 27 +- src/worldmap/special_tile.cpp | 82 +-- src/worldmap/special_tile.hpp | 28 +- src/worldmap/sprite_change.cpp | 64 +- src/worldmap/sprite_change.hpp | 50 +- src/worldmap/teleporter.cpp | 56 +- src/worldmap/teleporter.hpp | 36 +- src/worldmap/tux.cpp | 60 +- src/worldmap/tux.hpp | 4 +- src/worldmap/world_select.cpp | 1 - src/worldmap/worldmap.cpp | 691 ++++-------------- src/worldmap/worldmap.hpp | 163 +---- src/worldmap/worldmap_object.cpp | 135 ++++ src/worldmap/worldmap_object.hpp | 75 ++ src/worldmap/worldmap_parser.cpp | 206 ------ src/worldmap/worldmap_sector.cpp | 609 +++++++++++++++ src/worldmap/worldmap_sector.hpp | 147 ++++ src/worldmap/worldmap_sector_parser.cpp | 69 ++ ..._parser.hpp => worldmap_sector_parser.hpp} | 27 +- src/worldmap/worldmap_state.cpp | 446 ++++++----- src/worldmap/worldmap_state.hpp | 26 +- 62 files changed, 2432 insertions(+), 2013 deletions(-) delete mode 100644 src/editor/worldmap_objects.cpp delete mode 100644 src/editor/worldmap_objects.hpp rename src/scripting/{worldmap.cpp => worldmap_sector.cpp} (61%) create mode 100644 src/scripting/worldmap_sector.hpp create mode 100644 src/supertux/sector_base.cpp create mode 100644 src/supertux/sector_base.hpp create mode 100644 src/worldmap/worldmap_object.cpp create mode 100644 src/worldmap/worldmap_object.hpp delete mode 100644 src/worldmap/worldmap_parser.cpp create mode 100644 src/worldmap/worldmap_sector.cpp create mode 100644 src/worldmap/worldmap_sector.hpp create mode 100644 src/worldmap/worldmap_sector_parser.cpp rename src/worldmap/{worldmap_parser.hpp => worldmap_sector_parser.hpp} (53%) diff --git a/data/images/engine/editor/objects.stoi b/data/images/engine/editor/objects.stoi index 5aaf6e284e4..df295ab97ea 100644 --- a/data/images/engine/editor/objects.stoi +++ b/data/images/engine/editor/objects.stoi @@ -418,5 +418,8 @@ (object (class "tilemap") (icon "images/engine/editor/tilemap.png")) - ) + (object + (class "background") + (icon "images/engine/editor/background.png")) + ) ) diff --git a/data/levels/world1/worldmap.stwm b/data/levels/world1/worldmap.stwm index a9c94ee78c8..22b00573d5e 100644 --- a/data/levels/world1/worldmap.stwm +++ b/data/levels/world1/worldmap.stwm @@ -2629,7 +2629,7 @@ state.ambient_b <- 1;") ) ) (tilemap - (solid #f) + (solid #t) (z-pos 2) (name "iv_secret") (width 100) diff --git a/src/editor/editor.cpp b/src/editor/editor.cpp index c90d6cc3c05..e1167993656 100644 --- a/src/editor/editor.cpp +++ b/src/editor/editor.cpp @@ -333,6 +333,7 @@ Editor::test_level(const std::optional>& test_pos m_autosave_levelfile = FileSystem::join(directory, backup_filename); m_level->save(m_autosave_levelfile); m_time_since_last_save = 0.f; + m_leveltested = true; if (!m_level->is_worldmap()) { @@ -342,8 +343,6 @@ Editor::test_level(const std::optional>& test_pos { GameManager::current()->start_worldmap(*current_world, "", m_autosave_levelfile); } - - m_leveltested = true; } void diff --git a/src/editor/overlay_widget.cpp b/src/editor/overlay_widget.cpp index 9e26db89a2b..3be7f1b7f47 100644 --- a/src/editor/overlay_widget.cpp +++ b/src/editor/overlay_widget.cpp @@ -27,7 +27,6 @@ #include "editor/tile_selection.hpp" #include "editor/tip.hpp" #include "editor/util.hpp" -#include "editor/worldmap_objects.hpp" #include "gui/menu.hpp" #include "gui/menu_manager.hpp" #include "math/bezier.hpp" @@ -44,6 +43,7 @@ #include "video/renderer.hpp" #include "video/video_system.hpp" #include "video/viewport.hpp" +#include "worldmap/worldmap_object.hpp" namespace { @@ -839,7 +839,7 @@ EditorOverlayWidget::put_object() mo->move_to(mo->get_pos() - Vector(bbox.get_width() / 2, bbox.get_height() / 2)); } - auto* wo = dynamic_cast(object.get()); + auto* wo = dynamic_cast(object.get()); if (wo) { wo->move_to(wo->get_pos() / 32.0f); } diff --git a/src/editor/worldmap_objects.cpp b/src/editor/worldmap_objects.cpp deleted file mode 100644 index 62e898c40a3..00000000000 --- a/src/editor/worldmap_objects.cpp +++ /dev/null @@ -1,280 +0,0 @@ -// SuperTux -// Copyright (C) 2016 Hume2 -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -#include "editor/worldmap_objects.hpp" - -#include - -#include "editor/editor.hpp" -#include "sprite/sprite.hpp" -#include "sprite/sprite_manager.hpp" -#include "supertux/world.hpp" -#include "util/file_system.hpp" -#include "util/log.hpp" -#include "util/reader_mapping.hpp" -#include "util/writer.hpp" - -namespace worldmap_editor { - -WorldmapObject::WorldmapObject (const ReaderMapping& mapping, const std::string& default_sprite) : - MovingSprite(mapping, default_sprite), - m_tile_x(), - m_tile_y() -{ - m_col.m_bbox = Rectf(Vector(32 * m_col.m_bbox.get_left(), - 32 * m_col.m_bbox.get_top()), - Sizef(32.0f, 32.0f)); -} - -WorldmapObject::WorldmapObject (const ReaderMapping& mapping) : - MovingSprite(mapping), - m_tile_x(), - m_tile_y() -{ - m_col.m_bbox.set_left(32 * m_col.m_bbox.get_left()); - m_col.m_bbox.set_top(32 * m_col.m_bbox.get_top()); - m_col.m_bbox.set_size(32, 32); -} - -WorldmapObject::WorldmapObject (const Vector& pos, const std::string& default_sprite) : - MovingSprite(pos, default_sprite), - m_tile_x(), - m_tile_y() -{ - m_col.m_bbox.set_left(32 * m_col.m_bbox.get_left()); - m_col.m_bbox.set_top(32 * m_col.m_bbox.get_top()); - m_col.m_bbox.set_size(32, 32); -} - -ObjectSettings -WorldmapObject::get_settings() -{ - ObjectSettings result = MovingSprite::get_settings(); - - m_tile_x = static_cast(m_col.m_bbox.get_left()) / 32; - m_tile_y = static_cast(m_col.m_bbox.get_top()) / 32; - - result.remove("x"); - result.remove("y"); - - result.add_int(_("X"), &m_tile_x, "x", {}, OPTION_HIDDEN); - result.add_int(_("Y"), &m_tile_y, "y", {}, OPTION_HIDDEN); - - return result; -} - -void -WorldmapObject::move_to(const Vector& pos) -{ - set_pos(Vector(32.0f * static_cast(pos.x / 32), - 32.0f * static_cast(pos.y / 32))); -} - -LevelDot::LevelDot(const ReaderMapping& mapping) : - WorldmapObject(mapping, "images/worldmap/common/leveldot.sprite"), - m_level_filename(), - m_extro_script(), - m_auto_play(false), - m_title_color(1, 1, 1) -{ - mapping.get("extro-script", m_extro_script); - mapping.get("auto-play", m_auto_play); - if (!mapping.get("level", m_level_filename)) { - // Hack for backward compatibility with 0.5.x level - m_level_filename = std::move(m_name); - } - - std::vector vColor; - if (mapping.get("color", vColor)) { - m_title_color = Color(vColor); - } -} - -void -LevelDot::draw(DrawingContext& context) -{ - m_sprite->draw(context.color(), m_col.m_bbox.p1() + Vector(16, 16), m_layer); -} - -ObjectSettings -LevelDot::get_settings() -{ - ObjectSettings result = WorldmapObject::get_settings(); - - std::string basedir = (Editor::current() && Editor::current()->get_world()) ? - Editor::current()->get_world()->get_basedir() : std::string(); - - // FIXME: hack to make the basedir absolute, making - // World::get_basedir() itself absolute would be correct, but - // invalidate savefiles. - if (!basedir.empty() && basedir.front() != '/') { - basedir = "/" + basedir; - } - - result.add_level(_("Level"), &m_level_filename, "level", basedir); - result.add_script(_("Outro script"), &m_extro_script, "extro-script"); - result.add_bool(_("Auto play"), &m_auto_play, "auto-play", false); - //result.add_sprite(_("Sprite"), &m_sprite_name, "sprite"); - result.add_color(_("Title colour"), &m_title_color, "color", Color::WHITE); - - result.reorder({"name", "sprite", "x", "y"}); - - return result; -} - -Teleporter::Teleporter (const ReaderMapping& mapping) : - WorldmapObject(mapping, "images/worldmap/common/teleporterdot.sprite"), - m_worldmap(), - m_spawnpoint(), - m_message(), - m_automatic(), - m_change_worldmap() -{ - mapping.get("worldmap", m_worldmap); - mapping.get("spawnpoint", m_spawnpoint); - mapping.get("message", m_message); - - mapping.get("automatic", m_automatic); - - m_change_worldmap = m_worldmap.size() > 0; -} - -void -Teleporter::draw(DrawingContext& context) -{ - m_sprite->draw(context.color(), m_col.m_bbox.p1() + Vector(16, 16), m_layer); -} - -ObjectSettings -Teleporter::get_settings() -{ - ObjectSettings result = WorldmapObject::get_settings(); - - result.add_text(_("Spawnpoint"), &m_spawnpoint, "spawnpoint"); - result.add_translatable_text(_("Message"), &m_message, "message"); - result.add_bool(_("Automatic"), &m_automatic, "automatic", false); - // result.add_bool(_("Change worldmap"), &m_change_worldmap, "worldmap", true); - result.add_worldmap(_("Target worldmap"), &m_worldmap, "worldmap"); - //result.add_sprite(_("Sprite"), &m_sprite_name, "sprite"); - - result.reorder({"spawnpoint", "automatic", "message", "sprite", "x", "y"}); - - return result; -} - -WorldmapSpawnPoint::WorldmapSpawnPoint (const ReaderMapping& mapping) : - WorldmapObject(mapping, "images/worldmap/common/tux.png"), - m_dir(worldmap::Direction::NONE) -{ - mapping.get("name", m_name); - - std::string auto_dir_str; - if (mapping.get("auto-dir", auto_dir_str)) { - m_dir = worldmap::string_to_direction(auto_dir_str); - } -} - -WorldmapSpawnPoint::WorldmapSpawnPoint (const std::string& name_, const Vector& pos) : - WorldmapObject(pos, "images/worldmap/common/tux.png"), - m_dir(worldmap::Direction::NONE) -{ - m_name = name_; -} - -ObjectSettings -WorldmapSpawnPoint::get_settings() -{ - ObjectSettings result = WorldmapObject::get_settings(); - - result.add_worldmap_direction(_("Direction"), &m_dir, worldmap::Direction::NONE, "auto-dir"); - result.remove("sprite"); - - result.reorder({"auto-dir", "name", "x", "y"}); - - return result; -} - -SpriteChange::SpriteChange (const ReaderMapping& mapping) : - WorldmapObject(mapping, "images/engine/editor/spritechange.png"), - m_target_sprite(m_sprite_name), - m_stay_action(), - m_initial_stay_action(false), - m_stay_group(), - m_change_on_touch(true) -{ - // To make obvious where the sprite change is, let's use an universal 32×32 sprite - m_sprite = SpriteManager::current()->create("images/engine/editor/spritechange.png"); - - mapping.get("stay-action", m_stay_action); - mapping.get("initial-stay-action", m_initial_stay_action); - mapping.get("stay-group", m_stay_group); - - mapping.get("change-on-touch", m_change_on_touch); -} - -ObjectSettings -SpriteChange::get_settings() -{ - ObjectSettings result = WorldmapObject::get_settings(); - - //result.add_sprite(_("Sprite"), &m_target_sprite, "sprite"); - result.add_text(_("Stay action"), &m_stay_action, "stay-action"); - result.add_bool(_("Initial stay action"), &m_initial_stay_action, "initial-stay-action"); - result.add_text(_("Stay group"), &m_stay_group, "stay-group"); - result.add_bool(_("Change on touch"), &m_change_on_touch, "change-on-touch"); - - result.reorder({"change-on-touch", "initial-stay-action", "stay-group", "sprite", "x", "y"}); - - return result; -} - -SpecialTile::SpecialTile (const ReaderMapping& mapping) : - WorldmapObject(mapping, "images/worldmap/common/specialtile.png"), - m_map_message(), - m_script(), - m_passive_message(false), - m_invisible_tile(false), - m_apply_to_directions("north-east-south-west") -{ - mapping.get("map-message", m_map_message); - mapping.get("script", m_script); - - mapping.get("passive-message", m_passive_message); - mapping.get("invisible-tile", m_invisible_tile); - - mapping.get("apply-to-direction", m_apply_to_directions); -} - -ObjectSettings -SpecialTile::get_settings() -{ - ObjectSettings result = WorldmapObject::get_settings(); - - result.add_translatable_text(_("Message"), &m_map_message, "map-message"); - result.add_bool(_("Show message"), &m_passive_message, "passive-message", false); - result.add_script(_("Script"), &m_script, "script"); - result.add_bool(_("Invisible"), &m_invisible_tile, "invisible-tile", false); - result.add_text(_("Direction"), &m_apply_to_directions, "apply-to-direction", std::string("north-east-south-west")); - //result.add_sprite(_("Sprite"), &m_sprite_name, "sprite"); - - result.reorder({"map-message", "invisible-tile", "script", "passive-message", "apply-to-direction", "sprite", "x", "y"}); - - return result; -} - -} // namespace worldmap_editor - -/* EOF */ diff --git a/src/editor/worldmap_objects.hpp b/src/editor/worldmap_objects.hpp deleted file mode 100644 index 8d52daa3c72..00000000000 --- a/src/editor/worldmap_objects.hpp +++ /dev/null @@ -1,170 +0,0 @@ -// SuperTux -// Copyright (C) 2016 Hume2 -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -#ifndef HEADER_SUPERTUX_EDITOR_WORLDMAP_OBJECTS_HPP -#define HEADER_SUPERTUX_EDITOR_WORLDMAP_OBJECTS_HPP - -#include "object/moving_sprite.hpp" -#include "video/color.hpp" -#include "worldmap/direction.hpp" - -namespace worldmap_editor { - -class WorldmapObject : public MovingSprite -{ -public: - WorldmapObject(const ReaderMapping& mapping, const std::string& default_sprite); - WorldmapObject(const ReaderMapping& mapping); - WorldmapObject(const Vector& pos, const std::string& default_sprite); - - virtual ObjectSettings get_settings() override; - virtual HitResponse collision(GameObject& other, const CollisionHit& hit) override { return FORCE_MOVE; } - static std::string class_name() { return "worldmap-object"; } - virtual std::string get_class_name() const override { return class_name(); } - virtual void move_to(const Vector& pos) override; - -private: - // FIXME: purely used for saving, is not updated normally, don't use. - int m_tile_x; - int m_tile_y; - -private: - WorldmapObject(const WorldmapObject&) = delete; - WorldmapObject& operator=(const WorldmapObject&) = delete; -}; - -class LevelDot final : public WorldmapObject -{ -public: - LevelDot(const ReaderMapping& mapping); - - virtual void draw(DrawingContext& context) override; - - static std::string class_name() { return "level"; } - virtual std::string get_class_name() const override { return class_name(); } - static std::string display_name() { return _("Level"); } - virtual std::string get_display_name() const override { return display_name(); } - virtual ObjectSettings get_settings() override; - -private: - std::string m_level_filename; - std::string m_extro_script; - bool m_auto_play; - Color m_title_color; - -private: - LevelDot(const LevelDot&) = delete; - LevelDot& operator=(const LevelDot&) = delete; -}; - -class Teleporter final : public WorldmapObject -{ -public: - Teleporter(const ReaderMapping& mapping); - - virtual void draw(DrawingContext& context) override; - - static std::string class_name() { return "teleporter"; } - virtual std::string get_class_name() const override { return class_name(); } - static std::string display_name() { return _("Teleporter"); } - virtual std::string get_display_name() const override { return display_name(); } - virtual ObjectSettings get_settings() override; - -private: - std::string m_worldmap; - std::string m_spawnpoint; - std::string m_message; - bool m_automatic; - bool m_change_worldmap; - -private: - Teleporter(const Teleporter&) = delete; - Teleporter& operator=(const Teleporter&) = delete; -}; - -class WorldmapSpawnPoint final : public WorldmapObject -{ -public: - WorldmapSpawnPoint(const ReaderMapping& mapping); - WorldmapSpawnPoint(const std::string& name_, const Vector& pos); - - static std::string class_name() { return "worldmap-spawnpoint"; } - virtual std::string get_class_name() const override { return class_name(); } - static std::string display_name() { return _("Spawn point"); } - virtual std::string get_display_name() const override { return display_name(); } - - virtual ObjectSettings get_settings() override; - -private: - worldmap::Direction m_dir; - -private: - WorldmapSpawnPoint(const WorldmapSpawnPoint&) = delete; - WorldmapSpawnPoint& operator=(const WorldmapSpawnPoint&) = delete; -}; - -class SpriteChange final : public WorldmapObject -{ -public: - SpriteChange(const ReaderMapping& mapping); - - static std::string class_name() { return "sprite-change"; } - virtual std::string get_class_name() const override { return class_name(); } - static std::string display_name() { return _("Sprite Change"); } - virtual std::string get_display_name() const override { return display_name(); } - virtual ObjectSettings get_settings() override; - -private: - std::string m_target_sprite; - std::string m_stay_action; - bool m_initial_stay_action; - std::string m_stay_group; - bool m_change_on_touch; - -private: - SpriteChange(const SpriteChange&) = delete; - SpriteChange& operator=(const SpriteChange&) = delete; -}; - -class SpecialTile final : public WorldmapObject -{ -public: - SpecialTile(const ReaderMapping& mapping); - - static std::string class_name() { return "special-tile"; } - virtual std::string get_class_name() const override { return class_name(); } - static std::string display_name() { return _("Special tile"); } - virtual std::string get_display_name() const override { return display_name(); } - virtual ObjectSettings get_settings() override; - -private: - std::string m_map_message; - std::string m_script; - bool m_passive_message; - bool m_invisible_tile; - - std::string m_apply_to_directions; - -private: - SpecialTile(const SpecialTile&) = delete; - SpecialTile& operator=(const SpecialTile&) = delete; -}; - -} // namespace worldmap_editor - -#endif - -/* EOF */ diff --git a/src/object/tilemap.cpp b/src/object/tilemap.cpp index e4e7b5c8d3e..a9aadd9f5a4 100644 --- a/src/object/tilemap.cpp +++ b/src/object/tilemap.cpp @@ -947,7 +947,7 @@ TileMap::update_effective_solid() { Sector::get().update_solid(this); } else if(worldmap::WorldMap::current() != nullptr && old != m_effective_solid) { - worldmap::WorldMap::current()->update_solid(this); + worldmap::WorldMapSector::current()->update_solid(this); } } diff --git a/src/scripting/badguy.hpp b/src/scripting/badguy.hpp index cdb541b41ce..b0410515448 100644 --- a/src/scripting/badguy.hpp +++ b/src/scripting/badguy.hpp @@ -39,8 +39,8 @@ class BadGuy { #ifndef SCRIPTING_API public: - BadGuy(UID uid) : - GameObject<::BadGuy>(uid) + BadGuy(UID uid, ::GameObjectManager& parent) : + GameObject<::BadGuy>(uid, parent) {} private: diff --git a/src/scripting/dispenser.hpp b/src/scripting/dispenser.hpp index f32cdadc586..445d4e0b804 100644 --- a/src/scripting/dispenser.hpp +++ b/src/scripting/dispenser.hpp @@ -38,10 +38,10 @@ class Dispenser final : public scripting::BadGuy { #ifndef SCRIPTING_API public: - Dispenser(UID uid) : - GameObject<::BadGuy>(uid), - GameObject<::Dispenser>(uid), - BadGuy(uid) + Dispenser(UID uid, ::GameObjectManager& parent) : + GameObject<::BadGuy>(uid, parent), + GameObject<::Dispenser>(uid, parent), + BadGuy(uid, parent) {} private: diff --git a/src/scripting/floating_image.cpp b/src/scripting/floating_image.cpp index 717c5be587a..642af02db89 100644 --- a/src/scripting/floating_image.cpp +++ b/src/scripting/floating_image.cpp @@ -24,7 +24,7 @@ namespace scripting { FloatingImage::FloatingImage(const std::string& spritefile) : - GameObject(get_game_object_manager().add<::FloatingImage>(spritefile).get_uid()) + GameObject(get_game_object_manager().add<::FloatingImage>(spritefile).get_uid(), get_game_object_manager()) { } diff --git a/src/scripting/functions.cpp b/src/scripting/functions.cpp index 3a8d78835a0..5735882f15a 100644 --- a/src/scripting/functions.cpp +++ b/src/scripting/functions.cpp @@ -286,12 +286,12 @@ void debug_draw_editor_images(bool enable) void debug_worldmap_ghost(bool enable) { - auto worldmap = worldmap::WorldMap::current(); + auto worldmap_sector = worldmap::WorldMapSector::current(); - if (worldmap == nullptr) - throw std::runtime_error("Can't change ghost mode without active WorldMap"); + if (worldmap_sector == nullptr) + throw std::runtime_error("Can't change ghost mode without active WorldMapSector"); - auto& tux = worldmap->get_singleton_by_type(); + auto& tux = worldmap_sector->get_singleton_by_type(); tux.set_ghost_mode(enable); } diff --git a/src/scripting/game_object.cpp b/src/scripting/game_object.cpp index c9122fa8f16..bbc368601c4 100644 --- a/src/scripting/game_object.cpp +++ b/src/scripting/game_object.cpp @@ -1,5 +1,6 @@ // SuperTux // Copyright (C) 2018 Ingo Ruhnke +// 2023 Vankata453 // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -16,22 +17,16 @@ #include "scripting/game_object.hpp" -#include "supertux/sector.hpp" -#include "worldmap/worldmap.hpp" +#include "supertux/d_scope.hpp" namespace scripting { ::GameObjectManager& get_game_object_manager() { - using namespace worldmap; + if (d_gameobject_manager) + return *d_gameobject_manager.get(); - if (::Sector::current() != nullptr) { - return ::Sector::get(); - } else if (::worldmap::WorldMap::current() != nullptr) { - return *::worldmap::WorldMap::current(); - } else { - throw std::runtime_error("Neither sector nor worldmap active"); - } + throw std::runtime_error("Unable to perform scripting GameObject action: No active GameObjectManager."); } } // namespace scripting diff --git a/src/scripting/game_object.hpp b/src/scripting/game_object.hpp index fdf5f090f50..fd697406dbd 100644 --- a/src/scripting/game_object.hpp +++ b/src/scripting/game_object.hpp @@ -87,17 +87,19 @@ template class GameObject { public: - GameObject(UID uid) : - m_uid(uid) + GameObject(UID uid, ::GameObjectManager& parent) : + m_uid(uid), + m_parent(parent) {} T* get_object_ptr() const { - return get_game_object_manager().get_object_by_uid(m_uid); + return m_parent.get_object_by_uid(m_uid); } protected: UID m_uid; + ::GameObjectManager& m_parent; }; } // namespace scripting diff --git a/src/scripting/game_object_manager.hpp b/src/scripting/game_object_manager.hpp index 85b8fd3fc17..11ca2c5b5f0 100644 --- a/src/scripting/game_object_manager.hpp +++ b/src/scripting/game_object_manager.hpp @@ -25,9 +25,9 @@ class GameObjectManager; namespace scripting { /** - * @summary This class provides basic controlling functions for a sector. Applies for both worldmaps and in-level sectors. + * @summary This class provides basic controlling functions for a sector. Applies for both worldmap and in-level sectors. * @instances For in-level sectors, an instance under ""sector.settings"" is available from scripts and the console.${SRG_NEWPARAGRAPH} - For worldmaps, such instance is available under ""worldmap.settings"". + For worldmap sectors, such instance is available under ""worldmap.settings"". */ class GameObjectManager { diff --git a/src/scripting/willowisp.hpp b/src/scripting/willowisp.hpp index a4063c70258..033f2d32fdb 100644 --- a/src/scripting/willowisp.hpp +++ b/src/scripting/willowisp.hpp @@ -40,10 +40,10 @@ class WillOWisp final : public scripting::BadGuy { #ifndef SCRIPTING_API public: - WillOWisp(UID uid) : - GameObject<::BadGuy>(uid), - GameObject<::WillOWisp>(uid), - BadGuy(uid) + WillOWisp(UID uid, ::GameObjectManager& parent) : + GameObject<::BadGuy>(uid, parent), + GameObject<::WillOWisp>(uid, parent), + BadGuy(uid, parent) {} private: diff --git a/src/scripting/worldmap.cpp b/src/scripting/worldmap_sector.cpp similarity index 61% rename from src/scripting/worldmap.cpp rename to src/scripting/worldmap_sector.cpp index 68dcd61ceba..904a33b300e 100644 --- a/src/scripting/worldmap.cpp +++ b/src/scripting/worldmap_sector.cpp @@ -1,5 +1,6 @@ // SuperTux // Copyright (C) 2021 A. Semphris +// 2023 Vankata453 // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -14,30 +15,50 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -#include "scripting/worldmap.hpp" +#include "scripting/worldmap_sector.hpp" #include "worldmap/worldmap.hpp" namespace scripting { -WorldMap::WorldMap(::worldmap::WorldMap* parent) : +WorldMapSector::WorldMapSector(::worldmap::WorldMapSector* parent) : GameObjectManager(parent), m_parent(parent) { } float -WorldMap::get_tux_x() const +WorldMapSector::get_tux_x() const { return m_parent->get_tux_pos().x; } float -WorldMap::get_tux_y() const +WorldMapSector::get_tux_y() const { return m_parent->get_tux_pos().y; } +void +WorldMapSector::set_sector(const std::string& sector) +{ + SCRIPT_GUARD_WORLDMAP; + worldmap.set_sector(sector); +} + +void +WorldMapSector::spawn(const std::string& sector, const std::string& spawnpoint) +{ + SCRIPT_GUARD_WORLDMAP; + worldmap.set_sector(sector, spawnpoint); +} + +void +WorldMapSector::move_to_spawnpoint(const std::string& spawnpoint) +{ + m_parent->move_to_spawnpoint(spawnpoint); +} + } // namespace scripting /* EOF */ diff --git a/src/scripting/worldmap_sector.hpp b/src/scripting/worldmap_sector.hpp new file mode 100644 index 00000000000..ac54cc1dc40 --- /dev/null +++ b/src/scripting/worldmap_sector.hpp @@ -0,0 +1,92 @@ +// SuperTux +// Copyright (C) 2021 A. Semphris +// 2023 Vankata453 +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#ifndef HEADER_SUPERTUX_SCRIPTING_WORLDMAP_SECTOR_HPP +#define HEADER_SUPERTUX_SCRIPTING_WORLDMAP_SECTOR_HPP + +#ifndef SCRIPTING_API + +#include + +#include "scripting/game_object_manager.hpp" + +namespace worldmap { + class WorldMapSector; +} + +/** Macro to allow quick and easy access to the parent worldmap. **/ +#define SCRIPT_GUARD_WORLDMAP \ + auto& worldmap = m_parent->get_worldmap() + +#endif + +namespace scripting { + +/** + * @summary This class provides additional controlling functions for a worldmap sector, other than the ones listed at ${SRG_REF_GameObjectManager}. + * @instances An instance under ""worldmap.settings"" is available from scripts and the console. + */ +class WorldMapSector final : public GameObjectManager +{ +#ifndef SCRIPTING_API +private: + ::worldmap::WorldMapSector* m_parent; + +public: + WorldMapSector(::worldmap::WorldMapSector* parent); + +private: + WorldMapSector(const WorldMapSector&) = delete; + WorldMapSector& operator=(const WorldMapSector&) = delete; +#endif + +public: + /** + * Gets Tux's X position on the worldmap. + */ + float get_tux_x() const; + /** + * Gets Tux's Y position on the worldmap. + */ + float get_tux_y() const; + + /** + * Changes the current sector of the worldmap to a specified new sector. + * @param string $sector + */ + void set_sector(const std::string& sector); + + /** + * Changes the current sector of the worldmap to a specified new sector, + moving Tux to the specified spawnpoint. + * @param string $sector + * @param string $spawnpoint + */ + void spawn(const std::string& sector, const std::string& spawnpoint); + + /** + * Moves Tux to a specified spawnpoint. + * @param string $spawnpoint + */ + void move_to_spawnpoint(const std::string& spawnpoint); +}; + +} // namespace scripting + +#endif + +/* EOF */ diff --git a/src/scripting/wrapper.cpp b/src/scripting/wrapper.cpp index 9a9fad35e44..7d316bc8251 100644 --- a/src/scripting/wrapper.cpp +++ b/src/scripting/wrapper.cpp @@ -11058,21 +11058,21 @@ static SQInteger Wind_stop_wrapper(HSQUIRRELVM vm) } -static SQInteger WorldMap_release_hook(SQUserPointer ptr, SQInteger ) +static SQInteger WorldMapSector_release_hook(SQUserPointer ptr, SQInteger ) { - scripting::WorldMap* _this = reinterpret_cast (ptr); + scripting::WorldMapSector* _this = reinterpret_cast (ptr); delete _this; return 0; } -static SQInteger WorldMap_get_tux_x_wrapper(HSQUIRRELVM vm) +static SQInteger WorldMapSector_get_tux_x_wrapper(HSQUIRRELVM vm) { SQUserPointer data; if(SQ_FAILED(sq_getinstanceup(vm, 1, &data, nullptr, SQTrue)) || !data) { sq_throwerror(vm, _SC("'get_tux_x' called without instance")); return SQ_ERROR; } - scripting::WorldMap* _this = reinterpret_cast (data); + scripting::WorldMapSector* _this = reinterpret_cast (data); try { @@ -11091,14 +11091,14 @@ static SQInteger WorldMap_get_tux_x_wrapper(HSQUIRRELVM vm) } -static SQInteger WorldMap_get_tux_y_wrapper(HSQUIRRELVM vm) +static SQInteger WorldMapSector_get_tux_y_wrapper(HSQUIRRELVM vm) { SQUserPointer data; if(SQ_FAILED(sq_getinstanceup(vm, 1, &data, nullptr, SQTrue)) || !data) { sq_throwerror(vm, _SC("'get_tux_y' called without instance")); return SQ_ERROR; } - scripting::WorldMap* _this = reinterpret_cast (data); + scripting::WorldMapSector* _this = reinterpret_cast (data); try { @@ -11117,6 +11117,101 @@ static SQInteger WorldMap_get_tux_y_wrapper(HSQUIRRELVM vm) } +static SQInteger WorldMapSector_set_sector_wrapper(HSQUIRRELVM vm) +{ + SQUserPointer data; + if(SQ_FAILED(sq_getinstanceup(vm, 1, &data, nullptr, SQTrue)) || !data) { + sq_throwerror(vm, _SC("'set_sector' called without instance")); + return SQ_ERROR; + } + scripting::WorldMapSector* _this = reinterpret_cast (data); + + const SQChar* arg0; + if(SQ_FAILED(sq_getstring(vm, 2, &arg0))) { + sq_throwerror(vm, _SC("Argument 1 not a string")); + return SQ_ERROR; + } + + try { + _this->set_sector(arg0); + + return 0; + + } catch(std::exception& e) { + sq_throwerror(vm, e.what()); + return SQ_ERROR; + } catch(...) { + sq_throwerror(vm, _SC("Unexpected exception while executing function 'set_sector'")); + return SQ_ERROR; + } + +} + +static SQInteger WorldMapSector_spawn_wrapper(HSQUIRRELVM vm) +{ + SQUserPointer data; + if(SQ_FAILED(sq_getinstanceup(vm, 1, &data, nullptr, SQTrue)) || !data) { + sq_throwerror(vm, _SC("'spawn' called without instance")); + return SQ_ERROR; + } + scripting::WorldMapSector* _this = reinterpret_cast (data); + + const SQChar* arg0; + if(SQ_FAILED(sq_getstring(vm, 2, &arg0))) { + sq_throwerror(vm, _SC("Argument 1 not a string")); + return SQ_ERROR; + } + const SQChar* arg1; + if(SQ_FAILED(sq_getstring(vm, 3, &arg1))) { + sq_throwerror(vm, _SC("Argument 2 not a string")); + return SQ_ERROR; + } + + try { + _this->spawn(arg0, arg1); + + return 0; + + } catch(std::exception& e) { + sq_throwerror(vm, e.what()); + return SQ_ERROR; + } catch(...) { + sq_throwerror(vm, _SC("Unexpected exception while executing function 'spawn'")); + return SQ_ERROR; + } + +} + +static SQInteger WorldMapSector_move_to_spawnpoint_wrapper(HSQUIRRELVM vm) +{ + SQUserPointer data; + if(SQ_FAILED(sq_getinstanceup(vm, 1, &data, nullptr, SQTrue)) || !data) { + sq_throwerror(vm, _SC("'move_to_spawnpoint' called without instance")); + return SQ_ERROR; + } + scripting::WorldMapSector* _this = reinterpret_cast (data); + + const SQChar* arg0; + if(SQ_FAILED(sq_getstring(vm, 2, &arg0))) { + sq_throwerror(vm, _SC("Argument 1 not a string")); + return SQ_ERROR; + } + + try { + _this->move_to_spawnpoint(arg0); + + return 0; + + } catch(std::exception& e) { + sq_throwerror(vm, e.what()); + return SQ_ERROR; + } catch(...) { + sq_throwerror(vm, _SC("Unexpected exception while executing function 'move_to_spawnpoint'")); + return SQ_ERROR; + } + +} + static SQInteger display_wrapper(HSQUIRRELVM vm) { return scripting::display(vm); @@ -13153,27 +13248,27 @@ void create_squirrel_instance(HSQUIRRELVM v, scripting::Wind* object, bool setup sq_remove(v, -2); // remove root table } -void create_squirrel_instance(HSQUIRRELVM v, scripting::WorldMap* object, bool setup_releasehook) +void create_squirrel_instance(HSQUIRRELVM v, scripting::WorldMapSector* object, bool setup_releasehook) { using namespace wrapper; sq_pushroottable(v); - sq_pushstring(v, "WorldMap", -1); + sq_pushstring(v, "WorldMapSector", -1); if(SQ_FAILED(sq_get(v, -2))) { std::ostringstream msg; - msg << "Couldn't resolved squirrel type 'WorldMap'"; + msg << "Couldn't resolved squirrel type 'WorldMapSector'"; throw SquirrelError(v, msg.str()); } if(SQ_FAILED(sq_createinstance(v, -1)) || SQ_FAILED(sq_setinstanceup(v, -1, object))) { std::ostringstream msg; - msg << "Couldn't setup squirrel instance for object of type 'WorldMap'"; + msg << "Couldn't setup squirrel instance for object of type 'WorldMapSector'"; throw SquirrelError(v, msg.str()); } sq_remove(v, -2); // remove object name if(setup_releasehook) { - sq_setreleasehook(v, -1, WorldMap_release_hook); + sq_setreleasehook(v, -1, WorldMapSector_release_hook); } sq_remove(v, -2); // remove root table @@ -15095,31 +15190,52 @@ void register_supertux_wrapper(HSQUIRRELVM v) throw SquirrelError(v, "Couldn't register class 'Sector'"); } - // Register class WorldMap - sq_pushstring(v, "WorldMap", -1); + // Register class WorldMapSector + sq_pushstring(v, "WorldMapSector", -1); sq_pushstring(v, "GameObjectManager", -1); sq_get(v, -3); if(sq_newclass(v, SQTrue) < 0) { std::ostringstream msg; - msg << "Couldn't create new class 'WorldMap'"; + msg << "Couldn't create new class 'WorldMapSector'"; throw SquirrelError(v, msg.str()); } sq_pushstring(v, "get_tux_x", -1); - sq_newclosure(v, &WorldMap_get_tux_x_wrapper, 0); + sq_newclosure(v, &WorldMapSector_get_tux_x_wrapper, 0); sq_setparamscheck(v, SQ_MATCHTYPEMASKSTRING, "."); if(SQ_FAILED(sq_createslot(v, -3))) { throw SquirrelError(v, "Couldn't register function 'get_tux_x'"); } sq_pushstring(v, "get_tux_y", -1); - sq_newclosure(v, &WorldMap_get_tux_y_wrapper, 0); + sq_newclosure(v, &WorldMapSector_get_tux_y_wrapper, 0); sq_setparamscheck(v, SQ_MATCHTYPEMASKSTRING, "."); if(SQ_FAILED(sq_createslot(v, -3))) { throw SquirrelError(v, "Couldn't register function 'get_tux_y'"); } + sq_pushstring(v, "set_sector", -1); + sq_newclosure(v, &WorldMapSector_set_sector_wrapper, 0); + sq_setparamscheck(v, SQ_MATCHTYPEMASKSTRING, ".s"); + if(SQ_FAILED(sq_createslot(v, -3))) { + throw SquirrelError(v, "Couldn't register function 'set_sector'"); + } + + sq_pushstring(v, "spawn", -1); + sq_newclosure(v, &WorldMapSector_spawn_wrapper, 0); + sq_setparamscheck(v, SQ_MATCHTYPEMASKSTRING, ".ss"); + if(SQ_FAILED(sq_createslot(v, -3))) { + throw SquirrelError(v, "Couldn't register function 'spawn'"); + } + + sq_pushstring(v, "move_to_spawnpoint", -1); + sq_newclosure(v, &WorldMapSector_move_to_spawnpoint_wrapper, 0); + sq_setparamscheck(v, SQ_MATCHTYPEMASKSTRING, ".s"); + if(SQ_FAILED(sq_createslot(v, -3))) { + throw SquirrelError(v, "Couldn't register function 'move_to_spawnpoint'"); + } + if(SQ_FAILED(sq_createslot(v, -3))) { - throw SquirrelError(v, "Couldn't register class 'WorldMap'"); + throw SquirrelError(v, "Couldn't register class 'WorldMapSector'"); } // Register class Gradient diff --git a/src/scripting/wrapper.hpp b/src/scripting/wrapper.hpp index 3abba741bfa..d2625106a44 100644 --- a/src/scripting/wrapper.hpp +++ b/src/scripting/wrapper.hpp @@ -74,8 +74,8 @@ class WillOWisp; void create_squirrel_instance(HSQUIRRELVM v, scripting::WillOWisp* object, bool setup_releasehook = false); class Wind; void create_squirrel_instance(HSQUIRRELVM v, scripting::Wind* object, bool setup_releasehook = false); -class WorldMap; -void create_squirrel_instance(HSQUIRRELVM v, scripting::WorldMap* object, bool setup_releasehook = false); +class WorldMapSector; +void create_squirrel_instance(HSQUIRRELVM v, scripting::WorldMapSector* object, bool setup_releasehook = false); } diff --git a/src/scripting/wrapper.interface.hpp b/src/scripting/wrapper.interface.hpp index 440a93853b3..0827d26202d 100644 --- a/src/scripting/wrapper.interface.hpp +++ b/src/scripting/wrapper.interface.hpp @@ -33,6 +33,6 @@ #include "scripting/torch.hpp" #include "scripting/willowisp.hpp" #include "scripting/wind.hpp" -#include "scripting/worldmap.hpp" +#include "scripting/worldmap_sector.hpp" /* EOF */ diff --git a/src/squirrel/exposed_object.hpp b/src/squirrel/exposed_object.hpp index b50517ebb1c..08419c6437d 100644 --- a/src/squirrel/exposed_object.hpp +++ b/src/squirrel/exposed_object.hpp @@ -75,7 +75,7 @@ class ExposedObject : virtual public ScriptInterface log_debug << "Exposing " << m_parent->get_class_name() << " object " << name << std::endl; - auto object = std::make_unique(m_parent->get_uid()); + auto object = std::make_unique(m_parent->get_uid(), *m_parent->get_parent()); expose_object(vm, table_idx, std::move(object), name); } diff --git a/src/supertux/colorscheme.cpp b/src/supertux/colorscheme.cpp index e64848bf4f9..490d2834bc3 100644 --- a/src/supertux/colorscheme.cpp +++ b/src/supertux/colorscheme.cpp @@ -23,6 +23,7 @@ #include "object/text_object.hpp" #include "supertux/levelintro.hpp" #include "supertux/player_status_hud.hpp" +#include "supertux/statistics.hpp" #include "supertux/textscroller_screen.hpp" #include "trigger/climbable.hpp" #include "trigger/secretarea_trigger.hpp" @@ -61,9 +62,9 @@ Color SecretAreaTrigger::text_color(1.f,1.f,0.6f); Color Climbable::text_color(1.f,1.f,0.6f); -Color worldmap::WorldMap::level_title_color(1.f,1.f,1.f); -Color worldmap::WorldMap::message_color(1.f,1.f,0.6f); -Color worldmap::WorldMap::teleporter_message_color(1.f,1.f,1.f); +Color worldmap::WorldMap::s_level_title_color(1.f,1.f,1.f); +Color worldmap::WorldMap::s_message_color(1.f,1.f,0.6f); +Color worldmap::WorldMap::s_teleporter_message_color(1.f,1.f,1.f); Color ColorScheme::Text::small_color(1.f,1.f,1.f); Color ColorScheme::Text::heading_color(1.f,1.f,0.6f); diff --git a/src/supertux/d_scope.cpp b/src/supertux/d_scope.cpp index e6cf7ba61b6..488a2bb3fab 100644 --- a/src/supertux/d_scope.cpp +++ b/src/supertux/d_scope.cpp @@ -17,7 +17,7 @@ #include "supertux/d_scope.hpp" DynamicScopedRef d_sector; -DynamicScopedRef d_worldmap; +DynamicScopedRef d_worldmap_sector; DynamicScopedRef d_gameobject_manager; /* EOF */ diff --git a/src/supertux/d_scope.hpp b/src/supertux/d_scope.hpp index d122d07696d..9c790ad2bb3 100644 --- a/src/supertux/d_scope.hpp +++ b/src/supertux/d_scope.hpp @@ -23,20 +23,20 @@ class Sector; extern DynamicScopedRef d_sector; namespace worldmap { -class WorldMap; +class WorldMapSector; } // namespace worldmap -extern DynamicScopedRef d_worldmap; +extern DynamicScopedRef d_worldmap_sector; class GameObjectManager; extern DynamicScopedRef d_gameobject_manager; #define BIND_SECTOR(x) \ - auto sector_guard = d_sector.bind(x); \ + auto sector_guard = d_sector.bind(x); \ auto gameobject_manager_guard = d_gameobject_manager.bind(x) -#define BIND_WORLDMAP(x) \ - auto worldmap_guard = d_worldmap.bind(x); \ +#define BIND_WORLDMAP_SECTOR(x) \ + auto worldmap_guard = d_worldmap_sector.bind(x); \ auto gameobject_manager_guard = d_gameobject_manager.bind(x) #endif diff --git a/src/supertux/game_object.hpp b/src/supertux/game_object.hpp index 843e6202d53..06f339a858d 100644 --- a/src/supertux/game_object.hpp +++ b/src/supertux/game_object.hpp @@ -186,6 +186,8 @@ class GameObject together (e.g. platform on a path) */ virtual void editor_update() {} + GameObjectManager* get_parent() const { return m_parent; } + protected: /** Parse object type. **/ void parse_type(const ReaderMapping& reader); diff --git a/src/supertux/game_object_factory.cpp b/src/supertux/game_object_factory.cpp index f1c1a611fdd..cbcfd321052 100644 --- a/src/supertux/game_object_factory.cpp +++ b/src/supertux/game_object_factory.cpp @@ -73,7 +73,6 @@ #include "badguy/yeti.hpp" #include "badguy/yeti_stalactite.hpp" #include "badguy/zeekling.hpp" -#include "editor/worldmap_objects.hpp" #include "math/vector.hpp" #include "object/ambient_light.hpp" #include "object/ambient_sound.hpp" @@ -143,6 +142,11 @@ #include "trigger/text_area.hpp" #include "util/reader_document.hpp" #include "util/reader_mapping.hpp" +#include "worldmap/level_tile.hpp" +#include "worldmap/spawn_point.hpp" +#include "worldmap/special_tile.hpp" +#include "worldmap/sprite_change.hpp" +#include "worldmap/teleporter.hpp" GameObjectFactory& GameObjectFactory::instance() @@ -231,7 +235,7 @@ GameObjectFactory::init_factories() add_factory("ambient-light"); add_factory("ambient_sound"); // backward compatibilty add_factory("ambient-sound"); - add_factory("background"); + add_factory("background", OBJ_PARAM_WORLDMAP); add_factory("path"); add_factory("bicycle-platform"); add_factory("bonusblock", OBJ_PARAM_DISPENSABLE); @@ -245,7 +249,7 @@ GameObjectFactory::init_factories() add_factory("particles-custom"); add_factory("particles-custom-file"); add_factory("coin", OBJ_PARAM_DISPENSABLE); - add_factory("decal"); + add_factory("decal", OBJ_PARAM_WORLDMAP); add_factory("explosion", OBJ_PARAM_DISPENSABLE); add_factory("fallblock", OBJ_PARAM_DISPENSABLE); add_factory("firefly"); @@ -298,12 +302,12 @@ GameObjectFactory::init_factories() // editor stuff add_factory("spawnpoint"); - // worldmap editor objects - add_factory("level"); - add_factory("special-tile"); - add_factory("sprite-change"); - add_factory("teleporter"); - add_factory("worldmap-spawnpoint"); + // worldmap objects + add_factory("level", OBJ_PARAM_WORLDMAP); + add_factory("special-tile", OBJ_PARAM_WORLDMAP); + add_factory("sprite-change", OBJ_PARAM_WORLDMAP); + add_factory("teleporter", OBJ_PARAM_WORLDMAP); + add_factory("worldmap-spawnpoint", OBJ_PARAM_WORLDMAP); add_factory("tilemap", TileMap::display_name(), [](const ReaderMapping& reader) { auto tileset = TileManager::current()->get_tileset(Level::current()->get_tileset()); diff --git a/src/supertux/game_session.cpp b/src/supertux/game_session.cpp index ed76530b3e5..919a5dba5de 100644 --- a/src/supertux/game_session.cpp +++ b/src/supertux/game_session.cpp @@ -630,9 +630,9 @@ GameSession::finish(bool win) } if (win) { - if (WorldMap::current()) + if (WorldMapSector::current()) { - WorldMap::current()->finished_level(m_level.get()); + WorldMapSector::current()->finished_level(m_level.get()); } if (LevelsetScreen::current()) @@ -795,17 +795,17 @@ GameSession::start_sequence(Player* caller, Sequence seq, const SequenceData* da m_endsequence_timer.stop(); - if (const auto& worldmap = worldmap::WorldMap::current()) + if (const auto& worldmap_sector = worldmap::WorldMapSector::current()) { if (data != nullptr) { if (!data->fade_tilemap.empty()) { - worldmap->set_initial_fade_tilemap(data->fade_tilemap, data->fade_type); + worldmap_sector->set_initial_fade_tilemap(data->fade_tilemap, data->fade_type); } if (!data->spawnpoint.empty()) { - worldmap->set_initial_spawnpoint(data->spawnpoint); + worldmap_sector->set_initial_spawnpoint(data->spawnpoint); } } } diff --git a/src/supertux/menu/worldmap_cheat_menu.cpp b/src/supertux/menu/worldmap_cheat_menu.cpp index 31653fbc7a9..3bdc62d0646 100644 --- a/src/supertux/menu/worldmap_cheat_menu.cpp +++ b/src/supertux/menu/worldmap_cheat_menu.cpp @@ -29,8 +29,8 @@ WorldmapCheatMenu::WorldmapCheatMenu() { - auto worldmap = worldmap::WorldMap::current(); - auto& tux = worldmap->get_singleton_by_type(); + auto worldmap_sector = worldmap::WorldMapSector::current(); + auto& tux = worldmap_sector->get_singleton_by_type(); add_label(_("Cheats")); add_hl(); @@ -60,8 +60,9 @@ void WorldmapCheatMenu::menu_action(MenuItem& item) { auto worldmap = worldmap::WorldMap::current(); - auto& tux = worldmap->get_singleton_by_type(); - assert(worldmap); + auto worldmap_sector = &worldmap->get_sector(); + auto& tux = worldmap_sector->get_singleton_by_type(); + assert(worldmap_sector); PlayerStatus& status = worldmap->get_savegame().get_player_status(); @@ -114,7 +115,7 @@ WorldmapCheatMenu::menu_action(MenuItem& item) case MNID_FINISH_LEVEL: { - auto level_tile = worldmap->at_level(); + auto level_tile = worldmap_sector->at_object(); if (level_tile) { level_tile->set_solved(true); @@ -126,7 +127,7 @@ WorldmapCheatMenu::menu_action(MenuItem& item) case MNID_RESET_LEVEL: { - auto level_tile = worldmap->at_level(); + auto level_tile = worldmap_sector->at_object(); if (level_tile) { level_tile->set_solved(false); @@ -151,7 +152,7 @@ WorldmapCheatMenu::menu_action(MenuItem& item) return; case MNID_MOVE_TO_MAIN: - worldmap->move_to_spawnpoint("main"); + worldmap_sector->move_to_spawnpoint("main"); MenuManager::instance().clear_menu_stack(); break; } @@ -181,11 +182,11 @@ WorldmapCheatMenu::do_cheat(PlayerStatus& status, WorldmapLevelSelectMenu::WorldmapLevelSelectMenu() { - auto worldmap = worldmap::WorldMap::current(); + auto worldmap_sector = worldmap::WorldMapSector::current(); int id = 0; add_label(_("Select level")); add_hl(); - for (auto& level : worldmap->get_objects_by_type()) + for (auto& level : worldmap_sector->get_objects_by_type()) { add_entry(id, level.get_title()); id++; @@ -197,14 +198,14 @@ WorldmapLevelSelectMenu::WorldmapLevelSelectMenu() void WorldmapLevelSelectMenu::menu_action(MenuItem& item) { - auto worldmap = worldmap::WorldMap::current(); - auto& tux = worldmap->get_singleton_by_type(); + auto worldmap_sector = worldmap::WorldMapSector::current(); + auto& tux = worldmap_sector->get_singleton_by_type(); int id = 0; - for(const auto& tile : worldmap->get_objects_by_type()) + for(const auto& tile : worldmap_sector->get_objects_by_type()) { if(id == item.get_id()) { - tux.set_tile_pos(tile.get_pos()); + tux.set_tile_pos(tile.get_tile_pos()); break; } id++; diff --git a/src/supertux/object_factory.hpp b/src/supertux/object_factory.hpp index f26848b9619..f123847dc86 100644 --- a/src/supertux/object_factory.hpp +++ b/src/supertux/object_factory.hpp @@ -35,6 +35,7 @@ class ObjectFactory private: typedef std::function (const ReaderMapping&)> FactoryFunction; typedef std::map Factories; + Factories factories; std::vector m_badguys_names; std::vector m_badguys_params; @@ -51,7 +52,8 @@ class ObjectFactory { OBJ_PARAM_NONE = 0, OBJ_PARAM_PORTABLE = 0b10000000, - OBJ_PARAM_DISPENSABLE = 0b00100000, + OBJ_PARAM_WORLDMAP = 0b01000000, + OBJ_PARAM_DISPENSABLE = 0b00100000 }; public: @@ -92,7 +94,7 @@ class ObjectFactory template void add_factory(const char* class_name, uint8_t obj_params = 0, const std::string& display_name = "") { - add_factory(class_name, (display_name == "") ? C::display_name() : display_name, + add_factory(class_name, display_name.empty() ? C::display_name() : display_name, [](const ReaderMapping& reader) { return std::make_unique(reader); }, obj_params); diff --git a/src/supertux/sector.cpp b/src/supertux/sector.cpp index c54eae92b65..06e120b4e19 100644 --- a/src/supertux/sector.cpp +++ b/src/supertux/sector.cpp @@ -55,6 +55,7 @@ #include "supertux/resources.hpp" #include "supertux/savegame.hpp" #include "supertux/tile.hpp" +#include "supertux/tile_manager.hpp" #include "util/file_system.hpp" #include "util/writer.hpp" #include "video/video_system.hpp" @@ -68,15 +69,14 @@ PlayerStatus dummy_player_status(1); } // namespace + Sector::Sector(Level& parent) : + Base::Sector("sector"), m_level(parent), - m_name(), m_fully_constructed(false), - m_init_script(), m_foremost_layer(), - m_squirrel_environment(new SquirrelEnvironment(SquirrelVirtualMachine::current()->get_vm(), "sector")), - m_collision_system(new CollisionSystem(*this)), - m_gravity(10.0) + m_gravity(10.0f), + m_collision_system(new CollisionSystem(*this)) { Savegame* savegame = (Editor::current() && Editor::is_active()) ? Editor::current()->m_savegame.get() : @@ -136,14 +136,18 @@ Sector::finish_construction(bool editable) if (!editable) { convert_tiles2gameobject(); - bool has_background = std::any_of(get_objects().begin(), get_objects().end(), - [](const auto& obj) { - return (dynamic_cast(obj.get()) || - dynamic_cast(obj.get())); - }); - if (!has_background) { - auto& gradient = add(); - gradient.set_gradient(Color(0.3f, 0.4f, 0.75f), Color(1.f, 1.f, 1.f)); + if (!m_level.is_worldmap()) + { + bool has_background = std::any_of(get_objects().begin(), get_objects().end(), + [](const auto& obj) { + return (dynamic_cast(obj.get()) || + dynamic_cast(obj.get())); + }); + if (!has_background) + { + auto& gradient = add(); + gradient.set_gradient(Color(0.3f, 0.4f, 0.75f), Color(1.f, 1.f, 1.f)); + } } } @@ -152,7 +156,8 @@ Sector::finish_construction(bool editable) } if (!get_object_by_type()) { - log_warning << "sector '" << get_name() << "' does not contain a camera." << std::endl; + if (!m_level.is_worldmap()) + log_warning << "sector '" << get_name() << "' does not contain a camera." << std::endl; add("Camera"); } @@ -185,12 +190,6 @@ Sector::finish_construction(bool editable) m_fully_constructed = true; } -Level& -Sector::get_level() const -{ - return m_level; -} - void Sector::activate(const std::string& spawnpoint) { @@ -203,7 +202,8 @@ Sector::activate(const std::string& spawnpoint) } if (!sp) { - log_warning << "Spawnpoint '" << spawnpoint << "' not found." << std::endl; + if (!m_level.is_worldmap()) + log_warning << "Spawnpoint '" << spawnpoint << "' not found." << std::endl; if (spawnpoint != "main") { activate("main"); } else { @@ -347,6 +347,18 @@ Sector::get_foremost_layer() const return m_foremost_layer; } +TileSet* +Sector::get_tileset() const +{ + return TileManager::current()->get_tileset(m_level.get_tileset()); +} + +bool +Sector::in_worldmap() const +{ + return m_level.is_worldmap(); +} + void Sector::update(float dt_sec) { @@ -614,12 +626,6 @@ Sector::set_gravity(float gravity) m_gravity = gravity; } -float -Sector::get_gravity() const -{ - return m_gravity; -} - Player* Sector::get_nearest_player (const Vector& pos) const { @@ -776,12 +782,6 @@ Sector::convert_tiles2gameobject() } } -void -Sector::run_script(const std::string& script, const std::string& sourcename) -{ - m_squirrel_environment->run_script(script, sourcename); -} - Camera& Sector::get_camera() const { diff --git a/src/supertux/sector.hpp b/src/supertux/sector.hpp index 8ace57d7d87..aacbb49e6a7 100644 --- a/src/supertux/sector.hpp +++ b/src/supertux/sector.hpp @@ -17,15 +17,15 @@ #ifndef HEADER_SUPERTUX_SUPERTUX_SECTOR_HPP #define HEADER_SUPERTUX_SUPERTUX_SECTOR_HPP +#include "supertux/sector_base.hpp" + #include #include #include "math/anchor_point.hpp" #include "math/easing.hpp" #include "math/fwd.hpp" -#include "squirrel/squirrel_environment.hpp" #include "supertux/d_scope.hpp" -#include "supertux/game_object_manager.hpp" #include "supertux/tile.hpp" #include "video/color.hpp" @@ -49,7 +49,7 @@ class Writer; /** Represents one of (potentially) multiple, separate parts of a Level. Sectors contain GameObjects, e.g. Badguys and Players. */ -class Sector final : public GameObjectManager +class Sector final : public Base::Sector { public: friend class CollisionSystem; @@ -67,20 +67,19 @@ class Sector final : public GameObjectManager Sector(Level& parent); ~Sector() override; - /** Needs to be called after parsing to finish the construction of - the Sector before using it. */ - void finish_construction(bool editable); + void finish_construction(bool editable) override; - Level& get_level() const; + Level& get_level() const { return m_level; } + TileSet* get_tileset() const override; + bool in_worldmap() const override; /** activates this sector (change music, initialize player class, ...) */ void activate(const std::string& spawnpoint); void activate(const Vector& player_pos); void deactivate(); - void update(float dt_sec); - - void draw(DrawingContext& context); + void draw(DrawingContext& context) override; + void update(float dt_sec) override; void save(Writer &writer); @@ -90,9 +89,6 @@ class Sector final : public GameObjectManager /** continues the looping sounds in whole sector. */ void play_looping_sounds(); - void set_name(const std::string& name_) { m_name = name_; } - const std::string& get_name() const { return m_name; } - /** tests if a given rectangle is inside the sector (a rectangle that is on top of the sector is considered inside) */ bool inside(const Rectf& rectangle) const; @@ -138,13 +134,7 @@ class Sector final : public GameObjectManager /** set gravity throughout sector */ void set_gravity(float gravity); - float get_gravity() const; - - void set_init_script(const std::string& init_script) { - m_init_script = init_script; - } - - void run_script(const std::string& script, const std::string& sourcename); + float get_gravity() const { return m_gravity; } Camera& get_camera() const; std::vector get_players() const; @@ -163,22 +153,15 @@ class Sector final : public GameObjectManager void convert_tiles2gameobject(); private: - /** Parent level containing this sector */ - Level& m_level; - - std::string m_name; + Level& m_level; // Parent level bool m_fully_constructed; - - std::string m_init_script; - int m_foremost_layer; - std::unique_ptr m_squirrel_environment; - std::unique_ptr m_collision_system; - float m_gravity; + std::unique_ptr m_collision_system; + private: Sector(const Sector&) = delete; Sector& operator=(const Sector&) = delete; diff --git a/src/supertux/sector_base.cpp b/src/supertux/sector_base.cpp new file mode 100644 index 00000000000..306f7722ffd --- /dev/null +++ b/src/supertux/sector_base.cpp @@ -0,0 +1,38 @@ +// SuperTux +// Copyright (C) 2023 Vankata453 +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include "supertux/sector_base.hpp" + +#include "util/log.hpp" + +namespace Base { + +Sector::Sector(const std::string& type) : + m_name(), + m_init_script(), + m_squirrel_environment(new SquirrelEnvironment(SquirrelVirtualMachine::current()->get_vm(), type)) +{ +} + +void +Sector::run_script(const std::string& script, const std::string& sourcename) +{ + m_squirrel_environment->run_script(script, sourcename); +} + +} // namespace Base + +/* EOF */ diff --git a/src/supertux/sector_base.hpp b/src/supertux/sector_base.hpp new file mode 100644 index 00000000000..20b47db63c6 --- /dev/null +++ b/src/supertux/sector_base.hpp @@ -0,0 +1,66 @@ +// SuperTux +// Copyright (C) 2023 Vankata453 +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#ifndef HEADER_SUPERTUX_SUPERTUX_SECTOR_BASE_HPP +#define HEADER_SUPERTUX_SUPERTUX_SECTOR_BASE_HPP + +#include "supertux/game_object_manager.hpp" + +#include "squirrel/squirrel_environment.hpp" + +class Level; +class TileSet; + +namespace Base { + +/** A base for sector classes. Contains main properties and functions. */ +class Sector : public GameObjectManager +{ +public: + Sector(const std::string& type); + + /** Needs to be called after parsing to finish the construction of + the Sector before using it. */ + virtual void finish_construction(bool editable) {} + + virtual void draw(DrawingContext& context) = 0; + virtual void update(float dt_sec) = 0; + + virtual TileSet* get_tileset() const = 0; + virtual bool in_worldmap() const = 0; + + void set_name(const std::string& name) { m_name = name; } + const std::string& get_name() const { return m_name; } + + void set_init_script(const std::string& init_script) { m_init_script = init_script; } + void run_script(const std::string& script, const std::string& sourcename); + +protected: + std::string m_name; + std::string m_init_script; + + std::unique_ptr m_squirrel_environment; + +private: + Sector(const Sector&) = delete; + Sector& operator=(const Sector&) = delete; +}; + +} // namespace Base + +#endif + +/* EOF */ diff --git a/src/supertux/sector_parser.cpp b/src/supertux/sector_parser.cpp index 17c1aba89f9..45229011c39 100644 --- a/src/supertux/sector_parser.cpp +++ b/src/supertux/sector_parser.cpp @@ -1,5 +1,6 @@ // SuperTux // Copyright (C) 2015 Ingo Ruhnke +// 2023 Vankata453 // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -23,7 +24,6 @@ #include "badguy/fish_jumping.hpp" #include "badguy/jumpy.hpp" #include "editor/editor.hpp" -#include "editor/worldmap_objects.hpp" #include "object/ambient_light.hpp" #include "object/background.hpp" #include "object/camera.hpp" @@ -39,9 +39,9 @@ #include "supertux/level.hpp" #include "supertux/sector.hpp" #include "supertux/tile.hpp" -#include "supertux/tile_manager.hpp" #include "util/reader_collection.hpp" #include "util/reader_mapping.hpp" +#include "worldmap/spawn_point.hpp" static const std::string DEFAULT_BG = "images/background/antarctic/arctis2.png"; @@ -75,43 +75,57 @@ SectorParser::from_nothing(Level& level) return sector; } -SectorParser::SectorParser(Sector& sector, bool editable) : +SectorParser::SectorParser(Base::Sector& sector, bool editable) : m_sector(sector), m_editable(editable) { } std::unique_ptr -SectorParser::parse_object(const std::string& name_, const ReaderMapping& reader) +SectorParser::parse_object(const std::string& name, const ReaderMapping& reader) { - if (name_ == "money") { // for compatibility with old maps - return std::make_unique(reader); - } else if (name_ == "fish") { //because the "fish" was renamed to "fish-jumping" - return std::make_unique(reader); - } else { - try { - return GameObjectFactory::instance().create(name_, reader); - } catch(std::exception& e) { - log_warning << e.what() << "" << std::endl; - return {}; - } + if (parse_object_additional(name, reader)) + return {}; // Object was parsed by additional rules, so cancel regular object parsing. + + try + { + return GameObjectFactory::instance().create(name, reader); + } + catch (std::exception& err) + { + log_warning << err.what() << std::endl; + return {}; } } +bool +SectorParser::parse_object_additional(const std::string& name, const ReaderMapping& reader) +{ + return false; // No additional object parsing rules, continue with regular object parsing. +} + void -SectorParser::parse(const ReaderMapping& sector) +SectorParser::parse(const ReaderMapping& reader) { - auto iter = sector.get_iter(); + auto iter = reader.get_iter(); while (iter.next()) { - if (iter.get_key() == "name") { + if (iter.get_key() == "name") + { std::string value; iter.get(value); m_sector.set_name(value); - } else if (iter.get_key() == "gravity") { + } + else if (iter.get_key() == "gravity") + { + auto sector = dynamic_cast(&m_sector); + if (!sector) continue; + float value; iter.get(value); - m_sector.set_gravity(value); - } else if (iter.get_key() == "music") { + sector->set_gravity(value); + } + else if (iter.get_key() == "music") + { const auto& sx = iter.get_sexp(); if (sx.is_array() && sx.as_array().size() == 2 && sx.as_array()[1].is_string()) { std::string value; @@ -120,18 +134,22 @@ SectorParser::parse(const ReaderMapping& sector) } else { m_sector.add(iter.as_mapping()); } - } else if (iter.get_key() == "init-script") { + } + else if (iter.get_key() == "init-script") + { std::string value; iter.get(value); m_sector.set_init_script(value); - } else if (iter.get_key() == "ambient-light") { + } + else if (iter.get_key() == "ambient-light") + { const auto& sx = iter.get_sexp(); if (sx.is_array() && sx.as_array().size() >= 3 && sx.as_array()[1].is_real() && sx.as_array()[2].is_real() && sx.as_array()[3].is_real()) { // for backward compatibilty std::vector vColor; - bool hasColor = sector.get("ambient-light", vColor); + bool hasColor = reader.get("ambient-light", vColor); if (vColor.size() < 3 || !hasColor) { log_warning << "(ambient-light) requires a color as argument" << std::endl; } else { @@ -141,11 +159,12 @@ SectorParser::parse(const ReaderMapping& sector) // modern format m_sector.add(iter.as_mapping()); } - } else { + } + else + { auto object = parse_object(iter.get_key(), iter.as_mapping()); - if (object) { + if (object) m_sector.add_object(std::move(object)); - } } } @@ -157,9 +176,13 @@ SectorParser::parse_old_format(const ReaderMapping& reader) { m_sector.set_name("main"); - float gravity; - if (reader.get("gravity", gravity)) - m_sector.set_gravity(gravity); + auto sector = dynamic_cast(&m_sector); + if (sector) + { + float gravity; + if (reader.get("gravity", gravity)) + sector->set_gravity(gravity); + } std::string backgroundimage; if (reader.get("background", backgroundimage) && (!backgroundimage.empty())) { @@ -234,8 +257,7 @@ SectorParser::parse_old_format(const ReaderMapping& reader) std::vector tiles; if (reader.get("interactive-tm", tiles) || reader.get("tilemap", tiles)) { - auto* tileset = TileManager::current()->get_tileset(m_sector.get_level().get_tileset()); - auto& tilemap = m_sector.add(tileset); + auto& tilemap = m_sector.add(m_sector.get_tileset()); tilemap.set(width, height, tiles, LAYER_TILES, true); // replace tile id 112 (old invisible tile) with 1311 (new invisible tile) @@ -251,15 +273,13 @@ SectorParser::parse_old_format(const ReaderMapping& reader) } if (reader.get("background-tm", tiles)) { - auto* tileset = TileManager::current()->get_tileset(m_sector.get_level().get_tileset()); - auto& tilemap = m_sector.add(tileset); + auto& tilemap = m_sector.add(m_sector.get_tileset()); tilemap.set(width, height, tiles, LAYER_BACKGROUNDTILES, false); if (height < 19) tilemap.resize(width, 19); } if (reader.get("foreground-tm", tiles)) { - auto* tileset = TileManager::current()->get_tileset(m_sector.get_level().get_tileset()); - auto& tilemap = m_sector.add(tileset); + auto& tilemap = m_sector.add(m_sector.get_tileset()); tilemap.set(width, height, tiles, LAYER_FOREGROUNDTILES, false); // fill additional space in foreground with tiles of ID 2035 (lightmap/black) @@ -313,20 +333,18 @@ SectorParser::parse_old_format(const ReaderMapping& reader) void SectorParser::create_sector() { - auto tileset = TileManager::current()->get_tileset(m_sector.get_level().get_tileset()); - bool worldmap = m_sector.get_level().is_worldmap(); - if (!worldmap) + if (!m_sector.in_worldmap()) { auto& background = m_sector.add(); background.set_image(DEFAULT_BG); background.set_speed(0.5); - auto& bkgrd = m_sector.add(tileset); + auto& bkgrd = m_sector.add(m_sector.get_tileset()); bkgrd.resize(100, 35); bkgrd.set_layer(-100); bkgrd.set_solid(false); - auto& frgrd = m_sector.add(tileset); + auto& frgrd = m_sector.add(m_sector.get_tileset()); frgrd.resize(100, 35); frgrd.set_layer(100); frgrd.set_solid(false); @@ -338,14 +356,14 @@ SectorParser::create_sector() } else { - auto& water = m_sector.add(tileset); + auto& water = m_sector.add(m_sector.get_tileset()); water.resize(100, 35, 1); water.set_layer(-100); water.set_solid(false); } - auto& intact = m_sector.add(tileset); - if (worldmap) { + auto& intact = m_sector.add(m_sector.get_tileset()); + if (m_sector.in_worldmap()) { intact.resize(100, 100, 0); } else { intact.resize(100, 35, 0); @@ -353,8 +371,8 @@ SectorParser::create_sector() intact.set_layer(0); intact.set_solid(true); - if (worldmap) { - m_sector.add("main", Vector(4, 4)); + if (m_sector.in_worldmap()) { + m_sector.add("main", Vector(4, 4)); } else { m_sector.add("main", Vector(64, 480)); } diff --git a/src/supertux/sector_parser.hpp b/src/supertux/sector_parser.hpp index a204ed95fd8..adbb6821942 100644 --- a/src/supertux/sector_parser.hpp +++ b/src/supertux/sector_parser.hpp @@ -1,5 +1,6 @@ // SuperTux // Copyright (C) 2015 Ingo Ruhnke +// 2023 Vankata453 // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -25,23 +26,33 @@ class Level; class ReaderMapping; class Sector; -class SectorParser final +namespace Base { + class Sector; +} + +class SectorParser { public: static std::unique_ptr from_reader(Level& level, const ReaderMapping& sector, bool editable); static std::unique_ptr from_reader_old_format(Level& level, const ReaderMapping& sector, bool editable); static std::unique_ptr from_nothing(Level& level); -private: - SectorParser(Sector& sector, bool editable); +protected: + SectorParser(Base::Sector& sector, bool editable); + virtual ~SectorParser() {} void parse_old_format(const ReaderMapping& reader); - void parse(const ReaderMapping& sector); + void parse(const ReaderMapping& reader); void create_sector(); - std::unique_ptr parse_object(const std::string& name_, const ReaderMapping& reader); -private: - Sector& m_sector; + std::unique_ptr parse_object(const std::string& name, const ReaderMapping& reader); + + /** Allows setting additional rules for parsing objects. + Return value indicates whether the regular object parsing process should be skipped. **/ + virtual bool parse_object_additional(const std::string& name, const ReaderMapping& reader); + +protected: + Base::Sector& m_sector; bool m_editable; private: diff --git a/src/worldmap/camera.cpp b/src/worldmap/camera.cpp index c3a6f3b18f3..0998dacdedf 100644 --- a/src/worldmap/camera.cpp +++ b/src/worldmap/camera.cpp @@ -82,8 +82,7 @@ Camera::pan() Vector Camera::get_camera_pos_for_tux() const { - auto& worldmap = *WorldMap::current(); - auto& tux = worldmap.get_singleton_by_type(); + auto& tux = WorldMapSector::current()->get_singleton_by_type(); Vector camera_offset_(0.0f, 0.0f); Vector tux_pos = tux.get_pos(); @@ -95,7 +94,7 @@ Camera::get_camera_pos_for_tux() const void Camera::clamp_camera_position(Vector& c) const { - auto& worldmap = *WorldMap::current(); + auto& worldmap_sector = *WorldMapSector::current(); if (c.x < 0) { c.x = 0; @@ -105,20 +104,20 @@ Camera::clamp_camera_position(Vector& c) const c.y = 0; } - if (c.x > worldmap.get_width() - static_cast(SCREEN_WIDTH)) { - c.x = worldmap.get_width() - static_cast(SCREEN_WIDTH); + if (c.x > worldmap_sector.get_width() - static_cast(SCREEN_WIDTH)) { + c.x = worldmap_sector.get_width() - static_cast(SCREEN_WIDTH); } - if (c.y > worldmap.get_height() - static_cast(SCREEN_HEIGHT)) { - c.y = worldmap.get_height() - static_cast(SCREEN_HEIGHT); + if (c.y > worldmap_sector.get_height() - static_cast(SCREEN_HEIGHT)) { + c.y = worldmap_sector.get_height() - static_cast(SCREEN_HEIGHT); } - if (worldmap.get_width() < static_cast(SCREEN_WIDTH)) { - c.x = (worldmap.get_width() - static_cast(SCREEN_WIDTH)) / 2.0f; + if (worldmap_sector.get_width() < static_cast(SCREEN_WIDTH)) { + c.x = (worldmap_sector.get_width() - static_cast(SCREEN_WIDTH)) / 2.0f; } - if (worldmap.get_height() < static_cast(SCREEN_HEIGHT)) { - c.y = (worldmap.get_height() - static_cast(SCREEN_HEIGHT)) / 2.0f; + if (worldmap_sector.get_height() < static_cast(SCREEN_HEIGHT)) { + c.y = (worldmap_sector.get_height() - static_cast(SCREEN_HEIGHT)) / 2.0f; } } diff --git a/src/worldmap/direction.cpp b/src/worldmap/direction.cpp index 8b5db588861..6b6036cb256 100644 --- a/src/worldmap/direction.cpp +++ b/src/worldmap/direction.cpp @@ -16,8 +16,6 @@ #include "worldmap/direction.hpp" -#include "editor/object_option.hpp" -#include "util/gettext.hpp" #include "util/log.hpp" namespace worldmap { @@ -40,8 +38,7 @@ Direction reverse_dir(Direction direction) return Direction::NONE; } -std::string -direction_to_string(Direction direction) +std::string direction_to_string(Direction direction) { switch (direction) { @@ -58,8 +55,7 @@ direction_to_string(Direction direction) } } -Direction -string_to_direction(const std::string& directory) +Direction string_to_direction(const std::string& directory) { if (directory == "west") return Direction::WEST; diff --git a/src/worldmap/direction.hpp b/src/worldmap/direction.hpp index e3f34f21579..9d91389f49d 100644 --- a/src/worldmap/direction.hpp +++ b/src/worldmap/direction.hpp @@ -17,11 +17,8 @@ #ifndef HEADER_SUPERTUX_WORLDMAP_DIRECTION_HPP #define HEADER_SUPERTUX_WORLDMAP_DIRECTION_HPP -#include #include -class ObjectOption; - namespace worldmap { enum class Direction { NONE, WEST, EAST, NORTH, SOUTH }; diff --git a/src/worldmap/level_tile.cpp b/src/worldmap/level_tile.cpp index 2fa039c8fbe..9bccae009a2 100644 --- a/src/worldmap/level_tile.cpp +++ b/src/worldmap/level_tile.cpp @@ -1,6 +1,7 @@ // SuperTux // Copyright (C) 2004 Ingo Ruhnke // Copyright (C) 2006 Christoph Sommer +// 2023 Vankata453 // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -19,19 +20,20 @@ #include -#include "sprite/sprite.hpp" -#include "sprite/sprite_manager.hpp" +#include "editor/editor.hpp" +#include "supertux/level_parser.hpp" #include "util/file_system.hpp" +#include "util/gettext.hpp" #include "util/log.hpp" +#include "util/reader_document.hpp" #include "util/reader_mapping.hpp" #include "worldmap/worldmap.hpp" namespace worldmap { -LevelTile::LevelTile(const std::string& basedir, const ReaderMapping& mapping) : - GameObject(mapping), - m_pos(0.0f, 0.0f), - m_basedir(basedir), +LevelTile::LevelTile(const ReaderMapping& mapping) : + WorldMapObject(mapping, "images/worldmap/common/leveldot.sprite"), + m_basedir(WorldMap::current() ? WorldMap::current()->get_levels_path() : ""), m_level_filename(), m_title(), m_auto_play(false), @@ -40,32 +42,25 @@ LevelTile::LevelTile(const std::string& basedir, const ReaderMapping& mapping) : m_solved(false), m_perfect(false), m_statistics(), - m_sprite(), - m_title_color(WorldMap::level_title_color) + m_title_color(WorldMap::s_level_title_color) { + if (m_basedir.empty() && Editor::current() && Editor::current()->get_world()) + m_basedir = Editor::current()->get_world()->get_basedir(); + if (!mapping.get("level", m_level_filename)) { // Hack for backward compatibility with 0.5.x level m_level_filename = m_name; } - mapping.get("x", m_pos.x); - mapping.get("y", m_pos.y); mapping.get("auto-play", m_auto_play); - - std::string spritefile = "images/worldmap/common/leveldot.sprite"; - mapping.get("sprite", spritefile); - m_sprite = SpriteManager::current()->create(spritefile); - mapping.get("extro-script", m_extro_script); std::vector vColor; - if (mapping.get("color", vColor)) { + if (mapping.get("color", vColor)) m_title_color = Color(vColor); - } - if (m_basedir == "./") { + if (m_basedir == "./") m_basedir = ""; - } if (!PHYSFS_exists(FileSystem::join(m_basedir, m_level_filename).c_str())) { @@ -73,6 +68,9 @@ LevelTile::LevelTile(const std::string& basedir, const ReaderMapping& mapping) : << "' does not exist and will not be added to the worldmap" << std::endl; return; } + + if (in_worldmap()) + load_level_information(); } LevelTile::~LevelTile() @@ -80,24 +78,58 @@ LevelTile::~LevelTile() } void -LevelTile::draw(DrawingContext& context) +LevelTile::load_level_information() { - m_sprite->draw(context.color(), m_pos * 32.0f + Vector(16, 16), LAYER_OBJECTS - 1); -} + /** Set default properties. */ + m_title = _(""); + m_target_time = 0.0f; -void -LevelTile::update(float ) -{ + if (!WorldMap::current()) + return; + + try + { + // Determine the level filename. + const std::string& levels_path = WorldMap::current()->get_levels_path(); + std::string filename = levels_path + get_level_filename(); + + if (levels_path == "./") + filename = get_level_filename(); + + try + { + auto doc = ReaderDocument::from_file(filename); + auto root = doc.get_root(); + + if (root.get_name() != "supertux-level") + throw std::runtime_error("'" + filename + "': file is not a supertux-level file."); + + auto mapping = root.get_mapping(); + + mapping.get("name", m_title); + mapping.get("target-time", m_target_time); + } + catch (std::exception& err) + { + std::stringstream out; + out << "Cannot read level info: " << err.what() << std::endl; + throw std::runtime_error(out.str()); + } + } + catch (std::exception& err) + { + log_warning << "Problem when reading level information: " << err.what() << std::endl; + return; + } } void LevelTile::update_sprite_action() { - if (!m_solved) { + if (!m_solved) m_sprite->set_action("default"); - } else { + else m_sprite->set_action((m_sprite->has_action("perfect") && m_perfect) ? "perfect" : "solved"); - } } void @@ -114,6 +146,28 @@ LevelTile::set_perfect(bool v) update_sprite_action(); } +ObjectSettings +LevelTile::get_settings() +{ + // FIXME: hack to make the basedir absolute, making + // World::get_basedir() itself absolute would be correct, but + // invalidate savefiles. + std::string basedir = m_basedir; + if (!basedir.empty() && basedir.front() != '/') + basedir = "/" + basedir; + + ObjectSettings result = WorldMapObject::get_settings(); + + result.add_level(_("Level"), &m_level_filename, "level", basedir); + result.add_script(_("Outro script"), &m_extro_script, "extro-script"); + result.add_bool(_("Auto play"), &m_auto_play, "auto-play", false); + result.add_color(_("Title colour"), &m_title_color, "color", Color::WHITE); + + result.reorder({"name", "sprite", "x", "y"}); + + return result; +} + } // namespace worldmap /* EOF */ diff --git a/src/worldmap/level_tile.hpp b/src/worldmap/level_tile.hpp index a602aab71b1..ced8fc3da2c 100644 --- a/src/worldmap/level_tile.hpp +++ b/src/worldmap/level_tile.hpp @@ -1,6 +1,7 @@ // SuperTux // Copyright (C) 2004 Ingo Ruhnke // Copyright (C) 2006 Christoph Sommer +// 2023 Vankata453 // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -18,25 +19,24 @@ #ifndef HEADER_SUPERTUX_WORLDMAP_LEVEL_TILE_HPP #define HEADER_SUPERTUX_WORLDMAP_LEVEL_TILE_HPP -#include "math/vector.hpp" -#include "sprite/sprite_ptr.hpp" -#include "supertux/game_object.hpp" -#include "supertux/statistics.hpp" +#include "worldmap/worldmap_object.hpp" -class ReaderMapping; +#include "supertux/statistics.hpp" namespace worldmap { -class LevelTile final : public GameObject +class LevelTile final : public WorldMapObject { - friend class WorldMapParser; - public: - LevelTile(const std::string& basedir, const ReaderMapping& mapping); + LevelTile(const ReaderMapping& mapping); ~LevelTile() override; - virtual void draw(DrawingContext& context) override; - virtual void update(float dt_sec) override; + static std::string class_name() { return "level"; } + virtual std::string get_class_name() const override { return class_name(); } + static std::string display_name() { return _("Level"); } + virtual std::string get_display_name() const override { return display_name(); } + + virtual ObjectSettings get_settings() override; void set_solved(bool v); bool is_solved() const { return m_solved; } @@ -49,8 +49,6 @@ class LevelTile final : public GameObject void update_sprite_action(); - Vector get_pos() const { return m_pos; } - std::string get_title() const { return m_title; } std::string get_level_filename() const { return m_level_filename; } std::string get_basedir() const { return m_basedir; } @@ -60,8 +58,9 @@ class LevelTile final : public GameObject bool is_auto_play() const { return m_auto_play; } private: - Vector m_pos; + void load_level_information(); +private: std::string m_basedir; std::string m_level_filename; std::string m_title; @@ -80,7 +79,6 @@ class LevelTile final : public GameObject Statistics m_statistics; - SpritePtr m_sprite; Color m_title_color; private: diff --git a/src/worldmap/spawn_point.cpp b/src/worldmap/spawn_point.cpp index ef566934834..964b4dd6246 100644 --- a/src/worldmap/spawn_point.cpp +++ b/src/worldmap/spawn_point.cpp @@ -1,5 +1,6 @@ // SuperTux - Worldmap Spawnpoint // Copyright (C) 2006 Matthias Braun +// 2023 Vankata453 // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -33,9 +34,8 @@ SpawnPoint::SpawnPoint(const ReaderMapping& mapping) : mapping.get("y", m_pos.y); std::string auto_dir_str; - if (mapping.get("auto-dir", auto_dir_str)) { + if (mapping.get("auto-dir", auto_dir_str)) m_auto_dir = string_to_direction(auto_dir_str); - } if (m_name.empty()) { @@ -50,6 +50,39 @@ SpawnPoint::SpawnPoint(const ReaderMapping& mapping) : } } + +SpawnPointObject::SpawnPointObject(const ReaderMapping& mapping) : + WorldMapObject(mapping, "images/worldmap/common/tux.png"), + m_dir(Direction::NONE) +{ + mapping.get("name", m_name); + + std::string auto_dir_str; + if (mapping.get("auto-dir", auto_dir_str)) + m_dir = string_to_direction(auto_dir_str); +} + +SpawnPointObject::SpawnPointObject(const std::string& name, const Vector& pos) : + WorldMapObject(pos, "images/worldmap/common/tux.png"), + m_dir(Direction::NONE) +{ + m_name = name; +} + +ObjectSettings +SpawnPointObject::get_settings() +{ + ObjectSettings result = WorldMapObject::get_settings(); + + result.remove("sprite"); + + result.add_worldmap_direction(_("Direction"), &m_dir, Direction::NONE, "auto-dir"); + + result.reorder({"auto-dir", "name", "x", "y"}); + + return result; +} + } // namespace worldmap /* EOF */ diff --git a/src/worldmap/spawn_point.hpp b/src/worldmap/spawn_point.hpp index d3ef96de188..846b0798dc3 100644 --- a/src/worldmap/spawn_point.hpp +++ b/src/worldmap/spawn_point.hpp @@ -1,5 +1,6 @@ // SuperTux - Worldmap Spawnpoint // Copyright (C) 2006 Matthias Braun +// 2023 Vankata453 // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -17,13 +18,13 @@ #ifndef HEADER_SUPERTUX_WORLDMAP_SPAWN_POINT_HPP #define HEADER_SUPERTUX_WORLDMAP_SPAWN_POINT_HPP +#include "worldmap/worldmap_object.hpp" + #include #include "math/vector.hpp" #include "worldmap/direction.hpp" -class ReaderMapping; - namespace worldmap { class SpawnPoint final @@ -45,6 +46,28 @@ class SpawnPoint final SpawnPoint& operator=(const SpawnPoint&) = delete; }; + +class SpawnPointObject final : public WorldMapObject +{ +public: + SpawnPointObject(const ReaderMapping& mapping); + SpawnPointObject(const std::string& name, const Vector& pos); + + static std::string class_name() { return "worldmap-spawnpoint"; } + virtual std::string get_class_name() const override { return class_name(); } + static std::string display_name() { return _("Spawn point"); } + virtual std::string get_display_name() const override { return display_name(); } + + virtual ObjectSettings get_settings() override; + +private: + Direction m_dir; + +private: + SpawnPointObject(const SpawnPointObject&) = delete; + SpawnPointObject& operator=(const SpawnPointObject&) = delete; +}; + } // namespace worldmap #endif diff --git a/src/worldmap/special_tile.cpp b/src/worldmap/special_tile.cpp index fa15f39c0cd..34fbbdb7608 100644 --- a/src/worldmap/special_tile.cpp +++ b/src/worldmap/special_tile.cpp @@ -1,6 +1,7 @@ // SuperTux // Copyright (C) 2004 Ingo Ruhnke // Copyright (C) 2006 Christoph Sommer +// 2023 Vankata453 // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -17,70 +18,38 @@ #include "worldmap/special_tile.hpp" -#include "sprite/sprite.hpp" -#include "sprite/sprite_manager.hpp" -#include "util/log.hpp" #include "util/reader_mapping.hpp" namespace worldmap { SpecialTile::SpecialTile(const ReaderMapping& mapping) : - m_pos(0.0f, 0.0f), - m_sprite(), + WorldMapObject(mapping, "images/worldmap/common/specialtile.png"), m_map_message(), m_passive_message(false), m_script(), m_invisible(false), + m_apply_direction(), m_apply_action_north(true), m_apply_action_east(true), m_apply_action_south(true), m_apply_action_west(true) { - if (!mapping.get("x", m_pos.x)) { - log_warning << "X coordinate of special tile not set, defaulting to 0" << std::endl; - } - if (!mapping.get("y", m_pos.y)) { - log_warning << "Y coordinate of special tile not set, defaulting to 0" << std::endl; - } - if (!mapping.get("invisible-tile", m_invisible)) { - // Ignore attribute if it's not specified. Tile is visible. - } + mapping.get("invisible-tile", m_invisible); - if (!m_invisible) { - std::string spritefile = ""; - if (!mapping.get("sprite", spritefile)) { - log_warning << "No sprite specified for visible special tile." << std::endl; - } - m_sprite = SpriteManager::current()->create(spritefile); - } + if (in_worldmap() && !has_found_sprite()) // In worldmap and no valid sprite is specified, be invisible + m_invisible = true; - if (!mapping.get("map-message", m_map_message)) { - // Ignore attribute if it's not specified. No map message set. - } - if (!mapping.get("passive-message", m_passive_message)) { - // Ignore attribute if it's not specified. No passive message set. - } - if (!mapping.get("script", m_script)) { - // Ignore attribute if it's not specified. No script set. - } + mapping.get("map-message", m_map_message); + mapping.get("passive-message", m_passive_message); + mapping.get("script", m_script); - std::string apply_direction; - if (!mapping.get("apply-to-direction", apply_direction)) { - // Ignore attribute if it's not specified. Applies to all directions. - } - if (!apply_direction.empty()) { - m_apply_action_north = false; - m_apply_action_south = false; - m_apply_action_east = false; - m_apply_action_west = false; - if (apply_direction.find("north") != std::string::npos) - m_apply_action_north = true; - if (apply_direction.find("south") != std::string::npos) - m_apply_action_south = true; - if (apply_direction.find("east") != std::string::npos) - m_apply_action_east = true; - if (apply_direction.find("west") != std::string::npos) - m_apply_action_west = true; + mapping.get("apply-to-direction", m_apply_direction); + if (!m_apply_direction.empty()) + { + m_apply_action_north = m_apply_direction.find("north") != std::string::npos; + m_apply_action_south = m_apply_direction.find("south") != std::string::npos; + m_apply_action_east = m_apply_direction.find("east") != std::string::npos; + m_apply_action_west = m_apply_direction.find("west") != std::string::npos; } } @@ -89,17 +58,28 @@ SpecialTile::~SpecialTile() } void -SpecialTile::draw(DrawingContext& context) +SpecialTile::draw_worldmap(DrawingContext& context) { if (m_invisible) return; - m_sprite->draw(context.color(), m_pos*32.0f + Vector(16, 16), LAYER_OBJECTS - 1); + WorldMapObject::draw_worldmap(context); } -void -SpecialTile::update(float ) +ObjectSettings +SpecialTile::get_settings() { + ObjectSettings result = WorldMapObject::get_settings(); + + result.add_translatable_text(_("Message"), &m_map_message, "map-message"); + result.add_bool(_("Show message"), &m_passive_message, "passive-message", false); + result.add_script(_("Script"), &m_script, "script"); + result.add_bool(_("Invisible"), &m_invisible, "invisible-tile", false); + result.add_text(_("Direction"), &m_apply_direction, "apply-to-direction", std::string("north-east-south-west")); + + result.reorder({"map-message", "invisible-tile", "script", "passive-message", "apply-to-direction", "sprite", "x", "y"}); + + return result; } } diff --git a/src/worldmap/special_tile.hpp b/src/worldmap/special_tile.hpp index 9e85e00835a..84beca08a13 100644 --- a/src/worldmap/special_tile.hpp +++ b/src/worldmap/special_tile.hpp @@ -1,6 +1,7 @@ // SuperTux // Copyright (C) 2004 Ingo Ruhnke // Copyright (C) 2006 Christoph Sommer +// 2023 Vankata453 // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -18,26 +19,27 @@ #ifndef HEADER_SUPERTUX_WORLDMAP_SPECIAL_TILE_HPP #define HEADER_SUPERTUX_WORLDMAP_SPECIAL_TILE_HPP -#include - -#include "math/vector.hpp" -#include "sprite/sprite_ptr.hpp" -#include "supertux/game_object.hpp" +#include "worldmap/worldmap_object.hpp" -class ReaderMapping; +#include namespace worldmap { -class SpecialTile final : public GameObject +class SpecialTile final : public WorldMapObject { public: SpecialTile(const ReaderMapping& mapping); ~SpecialTile() override; - virtual void draw(DrawingContext& context) override; - virtual void update(float dt_sec) override; + static std::string class_name() { return "special-tile"; } + virtual std::string get_class_name() const override { return class_name(); } + static std::string display_name() { return _("Special Tile"); } + virtual std::string get_display_name() const override { return display_name(); } + + virtual void draw_worldmap(DrawingContext& context) override; + + virtual ObjectSettings get_settings() override; - Vector get_pos() const { return m_pos; } std::string get_map_message() const { return m_map_message; } bool is_passive_message() const { return m_passive_message; } std::string get_script() const { return m_script; } @@ -48,11 +50,6 @@ class SpecialTile final : public GameObject bool get_apply_action_west() const { return m_apply_action_west; } private: - Vector m_pos; - - /** Sprite to render instead of guessing what image to draw */ - SpritePtr m_sprite; - /** Message to show in the Map */ std::string m_map_message; bool m_passive_message; @@ -64,6 +61,7 @@ class SpecialTile final : public GameObject bool m_invisible; /** Only applies actions (ie. passive messages) when going to that direction */ + std::string m_apply_direction; bool m_apply_action_north; bool m_apply_action_east; bool m_apply_action_south; diff --git a/src/worldmap/sprite_change.cpp b/src/worldmap/sprite_change.cpp index b122b1172a0..8b96c355916 100644 --- a/src/worldmap/sprite_change.cpp +++ b/src/worldmap/sprite_change.cpp @@ -1,5 +1,6 @@ // SuperTux // Copyright (C) 2006 Matthias Braun +// 2023 Vankata453 // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -16,58 +17,38 @@ #include "worldmap/sprite_change.hpp" -#include "sprite/sprite.hpp" -#include "sprite/sprite_manager.hpp" #include "util/reader_mapping.hpp" -#include "video/drawing_context.hpp" +#include "worldmap/worldmap_sector.hpp" namespace worldmap { -std::list SpriteChange::s_all_sprite_changes; - SpriteChange::SpriteChange(const ReaderMapping& mapping) : - m_pos(0.0f, 0.0f), + WorldMapObject(mapping, "images/engine/editor/spritechange.png"), m_change_on_touch(false), - m_sprite(), - m_sprite_name(), m_stay_action(), m_stay_group(), m_in_stay_action(false) { - mapping.get("x", m_pos.x); - mapping.get("y", m_pos.y); mapping.get("change-on-touch", m_change_on_touch); - - if (!mapping.get("sprite", m_sprite_name)) m_sprite_name = ""; - m_sprite = SpriteManager::current()->create(m_sprite_name); - mapping.get("stay-action", m_stay_action); mapping.get("initial-stay-action", m_in_stay_action); - mapping.get("stay-group", m_stay_group); - - s_all_sprite_changes.push_back(this); } SpriteChange::~SpriteChange() { - s_all_sprite_changes.remove(this); } void -SpriteChange::draw(DrawingContext& context) +SpriteChange::draw_worldmap(DrawingContext& context) { - if (m_in_stay_action && !m_stay_action.empty()) { + if (m_in_stay_action && !m_stay_action.empty()) + { m_sprite->set_action(m_stay_action); - m_sprite->draw(context.color(), m_pos * 32.0f, LAYER_OBJECTS-1); + WorldMapObject::draw_worldmap(context); } } -void -SpriteChange::update(float ) -{ -} - bool SpriteChange::show_stay_action() const { @@ -86,14 +67,37 @@ SpriteChange::clear_stay_action(bool propagate) m_in_stay_action = false; // if we are in a stay_group, also clear all stay actions in this group - if (!m_stay_group.empty() && propagate) { - for (const auto& sc : s_all_sprite_changes) { - if (sc->m_stay_group != m_stay_group) continue; - sc->m_in_stay_action = false; + if (!m_stay_group.empty() && propagate) + { + for (SpriteChange& sc : WorldMapSector::current()->get_objects_by_type()) + { + if (sc.m_stay_group != m_stay_group) continue; + sc.m_in_stay_action = false; } } } +SpritePtr +SpriteChange::clone_sprite() const +{ + return m_sprite->clone(); +} + +ObjectSettings +SpriteChange::get_settings() +{ + ObjectSettings result = WorldMapObject::get_settings(); + + result.add_text(_("Stay action"), &m_stay_action, "stay-action"); + result.add_bool(_("Initial stay action"), &m_in_stay_action, "initial-stay-action"); + result.add_text(_("Stay group"), &m_stay_group, "stay-group"); + result.add_bool(_("Change on touch"), &m_change_on_touch, "change-on-touch"); + + result.reorder({"change-on-touch", "initial-stay-action", "stay-group", "sprite", "x", "y"}); + + return result; +} + } // namespace worldmap /* EOF */ diff --git a/src/worldmap/sprite_change.hpp b/src/worldmap/sprite_change.hpp index ff919ab6e05..e889b681066 100644 --- a/src/worldmap/sprite_change.hpp +++ b/src/worldmap/sprite_change.hpp @@ -1,5 +1,6 @@ // SuperTux // Copyright (C) 2006 Matthias Braun +// 2023 Vankata453 // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -17,61 +18,53 @@ #ifndef HEADER_SUPERTUX_WORLDMAP_SPRITE_CHANGE_HPP #define HEADER_SUPERTUX_WORLDMAP_SPRITE_CHANGE_HPP -#include -#include - -#include "math/vector.hpp" -#include "sprite/sprite_ptr.hpp" -#include "supertux/game_object.hpp" +#include "worldmap/worldmap_object.hpp" -class ReaderMapping; -class Sprite; +#include namespace worldmap { -class SpriteChange final : public GameObject +class SpriteChange final : public WorldMapObject { -private: - static std::list s_all_sprite_changes; - public: SpriteChange(const ReaderMapping& mapping); ~SpriteChange() override; - virtual void draw(DrawingContext& context) override; - virtual void update(float dt_sec) override; + static std::string class_name() { return "sprite-change"; } + virtual std::string get_class_name() const override { return class_name(); } + static std::string display_name() { return _("Sprite Change"); } + virtual std::string get_display_name() const override { return display_name(); } + + virtual void draw_worldmap(DrawingContext& context) override; + + virtual ObjectSettings get_settings() override; /** * Activates the SpriteChange's stay action, if applicable */ void set_stay_action(); - /** * Deactivates the SpriteChange's stay action, if applicable * @param propagate : Also change stay actions in the same stay group */ void clear_stay_action(bool propagate = true); - - /* + /** * Get the current value of in_stay_action */ - bool show_stay_action() const; + bool show_stay_action() const; - Vector get_pos() const { return m_pos; } + /** + * Clone the current sprite. + */ + SpritePtr clone_sprite() const; -private: - Vector m_pos; + bool change_on_touch() const { return m_change_on_touch; } -public: - /** should tuxs sprite change when the tile has been completely entered, +private: + /** should Tux's sprite change when the tile has been completely entered, or already when the tile was just touched */ bool m_change_on_touch; - /** sprite to change tux image to */ - SpritePtr m_sprite; - std::string m_sprite_name; - -private: /** stay action can be used for objects like boats or cars, if it is not empty then this sprite will be displayed when tux left the tile towards another SpriteChange object. */ @@ -81,7 +74,6 @@ class SpriteChange final : public GameObject its stay_action displayed. Leave empty if you don't care. */ std::string m_stay_group; -private: /** should the stayaction be displayed */ bool m_in_stay_action; diff --git a/src/worldmap/teleporter.cpp b/src/worldmap/teleporter.cpp index 92a1420210d..204181683b1 100644 --- a/src/worldmap/teleporter.cpp +++ b/src/worldmap/teleporter.cpp @@ -1,5 +1,6 @@ // SuperTux - Teleporter Worldmap Tile // Copyright (C) 2006 Christoph Sommer +// 2023 Vankata453 // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -16,53 +17,42 @@ #include "worldmap/teleporter.hpp" -#include "sprite/sprite.hpp" -#include "sprite/sprite_manager.hpp" #include "util/reader_mapping.hpp" namespace worldmap { Teleporter::Teleporter(const ReaderMapping& mapping) : - m_pos(0.0f, 0.0f), - m_sprite(), + WorldMapObject(mapping, "images/worldmap/common/teleporterdot.sprite"), m_worldmap(), + m_sector(), m_spawnpoint(), m_automatic(false), m_message() { - mapping.get("x", m_pos.x); - mapping.get("y", m_pos.y); - - std::string spritefile = ""; - if (mapping.get("sprite", spritefile)) { - m_sprite = SpriteManager::current()->create(spritefile); - } - - if (!mapping.get("worldmap", m_worldmap)) { - // worldmap parameter doesn't need to be set. Ignore. - } - if (!mapping.get("spawnpoint", m_spawnpoint)) { - // not set, use "main" spawnpoint. - } - if (!mapping.get("automatic", m_automatic)) { - // doesn't need to be set. Don't teleport automatically. - } - if (!mapping.get("message", m_message)) { - // Optional message not set. Ignore! - } + if (in_worldmap() && !has_found_sprite()) // In worldmap and no valid sprite is specified, remove it + m_sprite.reset(); + + mapping.get("worldmap", m_worldmap); + mapping.get("sector", m_sector); + mapping.get("spawnpoint", m_spawnpoint); + mapping.get("automatic", m_automatic); + mapping.get("message", m_message); } -void -Teleporter::draw(DrawingContext& context) +ObjectSettings +Teleporter::get_settings() { - if (m_sprite) { - m_sprite->draw(context.color(), m_pos * 32.0f + Vector(16, 16), LAYER_OBJECTS - 1); - } -} + ObjectSettings result = WorldMapObject::get_settings(); -void -Teleporter::update(float ) -{ + result.add_text(_("Sector"), &m_sector, "sector"); + result.add_text(_("Spawnpoint"), &m_spawnpoint, "spawnpoint"); + result.add_translatable_text(_("Message"), &m_message, "message"); + result.add_bool(_("Automatic"), &m_automatic, "automatic", false); + result.add_worldmap(_("Target worldmap"), &m_worldmap, "worldmap"); + + result.reorder({"sector", "spawnpoint", "automatic", "message", "sprite", "x", "y"}); + + return result; } } // namespace worldmap diff --git a/src/worldmap/teleporter.hpp b/src/worldmap/teleporter.hpp index 8a1f31914a8..b35b43e6aa4 100644 --- a/src/worldmap/teleporter.hpp +++ b/src/worldmap/teleporter.hpp @@ -1,5 +1,6 @@ // SuperTux - Teleporter Worldmap Tile // Copyright (C) 2006 Christoph Sommer +// 2023 Vankata453 // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -17,40 +18,37 @@ #ifndef HEADER_SUPERTUX_WORLDMAP_TELEPORTER_HPP #define HEADER_SUPERTUX_WORLDMAP_TELEPORTER_HPP -#include - -#include "math/vector.hpp" -#include "sprite/sprite_ptr.hpp" -#include "supertux/game_object.hpp" +#include "worldmap/worldmap_object.hpp" -class ReaderMapping; +#include namespace worldmap { -class Teleporter final : public GameObject +class Teleporter final : public WorldMapObject { public: Teleporter(const ReaderMapping& mapping); - virtual void draw(DrawingContext& context) override; - virtual void update(float dt_sec) override; + static std::string class_name() { return "teleporter"; } + virtual std::string get_class_name() const override { return class_name(); } + static std::string display_name() { return _("Teleporter"); } + virtual std::string get_display_name() const override { return display_name(); } + + virtual ObjectSettings get_settings() override; - Vector get_pos() const { return m_pos; } - std::string get_worldmap() const { return m_worldmap; } - std::string get_spawnpoint() const { return m_spawnpoint; } + const std::string& get_worldmap() const { return m_worldmap; } + const std::string& get_sector() const { return m_sector; } + const std::string& get_spawnpoint() const { return m_spawnpoint; } bool is_automatic() const { return m_automatic; } - std::string get_message() const { return m_message; } + const std::string& get_message() const { return m_message; } private: - /** Position (in tiles, not pixels) */ - Vector m_pos; - - /** Sprite to render, or 0 for no sprite */ - SpritePtr m_sprite; - /** Worldmap filename (relative to data root) to teleport to. Leave empty to use current word */ std::string m_worldmap; + /** Sector to teleport to. Leave empty to stay at the current sector **/ + std::string m_sector; + /** Spawnpoint to teleport to. Leave empty to use "main" or last one */ std::string m_spawnpoint; diff --git a/src/worldmap/tux.cpp b/src/worldmap/tux.cpp index bf6ec8b524d..5f2b82a67e9 100644 --- a/src/worldmap/tux.cpp +++ b/src/worldmap/tux.cpp @@ -27,9 +27,12 @@ #include "supertux/tile.hpp" #include "util/log.hpp" #include "worldmap/camera.hpp" +#include "worldmap/direction.hpp" #include "worldmap/level_tile.hpp" #include "worldmap/special_tile.hpp" #include "worldmap/sprite_change.hpp" +#include "worldmap/teleporter.hpp" +#include "worldmap/worldmap.hpp" namespace worldmap { @@ -53,7 +56,7 @@ Tux::Tux(WorldMap* worldmap) : void Tux::draw(DrawingContext& context) { - if (m_worldmap->get_camera().is_panning()) return; + if (m_worldmap->get_sector().get_camera().is_panning()) return; std::string action = get_action_prefix_for_bonus(m_worldmap->get_savegame().get_player_status().bonus[0]); if (!action.empty()) @@ -85,7 +88,7 @@ Tux::draw(DrawingContext& context) log_debug << "Bonus type not handled in worldmap." << std::endl; m_sprite->set_action("large-stop"); } - m_sprite->draw(context.color(), get_pos(), LAYER_OBJECTS); + m_sprite->draw(context.color(), get_pos(), LAYER_OBJECTS + 1); } std::string @@ -196,13 +199,13 @@ Tux::try_start_walking() if (m_input_direction == Direction::NONE) return; - auto level = m_worldmap->at_level(); + auto level = m_worldmap->get_sector().at_object(); // We got a new direction, so lets start walking when possible Vector next_tile(0.0f, 0.0f); if ((!level || level->is_solved() || level->is_perfect() || (Editor::current() && Editor::current()->is_testing_level())) - && m_worldmap->path_ok(m_input_direction, m_tile_pos, &next_tile)) { + && m_worldmap->get_sector().path_ok(m_input_direction, m_tile_pos, &next_tile)) { m_tile_pos = next_tile; m_moving = true; m_direction = m_input_direction; @@ -210,7 +213,7 @@ Tux::try_start_walking() } else if (m_ghost_mode || (m_input_direction == m_back_direction)) { m_moving = true; m_direction = m_input_direction; - m_tile_pos = m_worldmap->get_next_tile(m_tile_pos, m_direction); + m_tile_pos = m_worldmap->get_sector().get_next_tile(m_tile_pos, m_direction); m_back_direction = reverse_dir(m_direction); } } @@ -228,11 +231,11 @@ Tux::can_walk(int tile_data, Direction dir) const void Tux::change_sprite(SpriteChange* sprite_change) { - //SpriteChange* sprite_change = m_worldmap->at_sprite_change(tile_pos); + //SpriteChange* sprite_change = m_worldmap->at_object(); if (sprite_change != nullptr) { - m_sprite = sprite_change->m_sprite->clone(); + m_sprite = sprite_change->clone_sprite(); sprite_change->clear_stay_action(); - m_worldmap->get_savegame().get_player_status().worldmap_sprite = sprite_change->m_sprite_name; + m_worldmap->get_savegame().get_player_status().worldmap_sprite = sprite_change->get_sprite_name(); } } @@ -251,11 +254,13 @@ Tux::try_continue_walking(float dt_sec) m_offset -= 32; - auto sprite_change = m_worldmap->at_sprite_change(m_tile_pos); + auto worldmap_sector = &m_worldmap->get_sector(); + + auto sprite_change = worldmap_sector->at_object(m_tile_pos); change_sprite(sprite_change); // if this is a special_tile with passive_message, display it - auto special_tile = m_worldmap->at_special_tile(); + auto special_tile = worldmap_sector->at_object(); if (special_tile) { // direction and the apply_action_ are opposites, since they "see" @@ -270,11 +275,11 @@ Tux::try_continue_walking(float dt_sec) } // check if we are at a Teleporter - auto teleporter = m_worldmap->at_teleporter(m_tile_pos); + auto teleporter = worldmap_sector->at_object(m_tile_pos); // stop if we reached a level, a WORLDMAP_STOP tile, a teleporter or a special tile without a passive_message - if ((m_worldmap->at_level()) || - (m_worldmap->tile_data_at(m_tile_pos) & Tile::WORLDMAP_STOP) || + if ((worldmap_sector->at_object()) || + (worldmap_sector->tile_data_at(m_tile_pos) & Tile::WORLDMAP_STOP) || (special_tile && !special_tile->is_passive_message() && special_tile->get_script().empty()) || (teleporter) || m_ghost_mode) @@ -287,7 +292,7 @@ Tux::try_continue_walking(float dt_sec) } // if user wants to change direction, try changing, else guess the direction in which to walk next - const int tile_data = m_worldmap->tile_data_at(m_tile_pos); + const int tile_data = worldmap_sector->tile_data_at(m_tile_pos); if ((m_direction != m_input_direction) && can_walk(tile_data, m_input_direction)) { m_direction = m_input_direction; m_back_direction = reverse_dir(m_direction); @@ -319,17 +324,17 @@ Tux::try_continue_walking(float dt_sec) return; Vector next_tile(0.0f, 0.0f); - if (!m_ghost_mode && !m_worldmap->path_ok(m_direction, m_tile_pos, &next_tile)) { + if (!m_ghost_mode && !worldmap_sector->path_ok(m_direction, m_tile_pos, &next_tile)) { log_debug << "Tilemap data is buggy" << std::endl; stop(); return; } - auto next_sprite = m_worldmap->at_sprite_change(next_tile); - if (next_sprite != nullptr && next_sprite->m_change_on_touch) { + auto next_sprite = worldmap_sector->at_object(next_tile); + if (next_sprite != nullptr && next_sprite->change_on_touch()) { change_sprite(next_sprite); } - //SpriteChange* last_sprite = m_worldmap->at_sprite_change(tile_pos); + //SpriteChange* last_sprite = m_worldmap->at_object(next_tile); if (sprite_change != nullptr && next_sprite != nullptr) { log_debug << "Old: " << m_tile_pos << " New: " << next_tile << std::endl; sprite_change->set_stay_action(); @@ -354,7 +359,7 @@ Tux::update_input_direction() void Tux::update(float dt_sec) { - if (m_worldmap->get_camera().is_panning()) return; + if (m_worldmap->get_sector().get_camera().is_panning()) return; update_input_direction(); if (m_moving) @@ -367,7 +372,7 @@ void Tux::setup() { // check if we already touch a SpriteChange object - auto sprite_change = m_worldmap->at_sprite_change(m_tile_pos); + auto sprite_change = m_worldmap->get_sector().at_object(m_tile_pos); change_sprite(sprite_change); } @@ -378,12 +383,17 @@ Tux::process_special_tile(SpecialTile* special_tile) return; } - if (special_tile->is_passive_message()) { + if (special_tile->is_passive_message()) m_worldmap->set_passive_message(special_tile->get_map_message(), map_message_TIME); - } else if (!special_tile->get_script().empty()) { - try { - m_worldmap->run_script(special_tile->get_script(), "specialtile"); - } catch(std::exception& e) { + + if (!special_tile->get_script().empty()) + { + try + { + m_worldmap->get_sector().run_script(special_tile->get_script(), "specialtile"); + } + catch(std::exception& e) + { log_warning << "Couldn't execute special tile script: " << e.what() << std::endl; } diff --git a/src/worldmap/tux.hpp b/src/worldmap/tux.hpp index d40c292993a..7fd559ecb51 100644 --- a/src/worldmap/tux.hpp +++ b/src/worldmap/tux.hpp @@ -18,10 +18,10 @@ #ifndef HEADER_SUPERTUX_WORLDMAP_TUX_HPP #define HEADER_SUPERTUX_WORLDMAP_TUX_HPP -#include "sprite/sprite_ptr.hpp" #include "supertux/game_object.hpp" + +#include "sprite/sprite_ptr.hpp" #include "supertux/player_status.hpp" -#include "worldmap/worldmap.hpp" class Controller; diff --git a/src/worldmap/world_select.cpp b/src/worldmap/world_select.cpp index d489ff6df45..2d5dd30fd26 100644 --- a/src/worldmap/world_select.cpp +++ b/src/worldmap/world_select.cpp @@ -29,7 +29,6 @@ #include "video/compositor.hpp" #include "video/drawing_context.hpp" #include "video/surface.hpp" -#include "worldmap/worldmap_parser.hpp" #include "worldmap/worldmap.hpp" namespace worldmap { diff --git a/src/worldmap/worldmap.cpp b/src/worldmap/worldmap.cpp index 213ee71f2c1..3549d8cd057 100644 --- a/src/worldmap/worldmap.cpp +++ b/src/worldmap/worldmap.cpp @@ -1,6 +1,7 @@ // SuperTux - A Jump'n Run // Copyright (C) 2004 Ingo Ruhnke // Copyright (C) 2006 Christoph Sommer +// 2023 Vankata453 // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -17,291 +18,123 @@ #include "worldmap/worldmap.hpp" -#include - #include "audio/sound_manager.hpp" -#include "control/input_manager.hpp" #include "gui/menu_manager.hpp" -#include "object/ambient_light.hpp" -#include "object/decal.hpp" -#include "object/display_effect.hpp" -#include "object/music_object.hpp" -#include "object/tilemap.hpp" -#include "physfs/ifile_stream.hpp" -#include "physfs/physfs_file_system.hpp" -#include "scripting/worldmap.hpp" -#include "sprite/sprite.hpp" -#include "squirrel/squirrel_environment.hpp" -#include "supertux/d_scope.hpp" -#include "supertux/debug.hpp" +#include "physfs/util.hpp" #include "supertux/fadetoblack.hpp" #include "supertux/game_manager.hpp" -#include "supertux/game_session.hpp" #include "supertux/gameconfig.hpp" -#include "supertux/level.hpp" #include "supertux/menu/menu_storage.hpp" -#include "supertux/player_status_hud.hpp" -#include "supertux/resources.hpp" -#include "supertux/savegame.hpp" +#include "supertux/player_status.hpp" #include "supertux/screen_manager.hpp" -#include "supertux/shrinkfade.hpp" -#include "supertux/tile.hpp" #include "supertux/tile_manager.hpp" #include "util/file_system.hpp" +#include "util/log.hpp" #include "util/reader.hpp" #include "util/reader_document.hpp" #include "util/reader_mapping.hpp" -#include "video/compositor.hpp" -#include "video/video_system.hpp" -#include "video/video_system.hpp" -#include "video/viewport.hpp" -#include "worldmap/camera.hpp" +#include "video/drawing_context.hpp" +#include "worldmap/direction.hpp" #include "worldmap/level_tile.hpp" -#include "worldmap/spawn_point.hpp" -#include "worldmap/special_tile.hpp" -#include "worldmap/sprite_change.hpp" -#include "worldmap/teleporter.hpp" #include "worldmap/tux.hpp" #include "worldmap/world_select.hpp" -#include "worldmap/worldmap_parser.hpp" #include "worldmap/worldmap_screen.hpp" +#include "worldmap/worldmap_sector.hpp" +#include "worldmap/worldmap_sector_parser.hpp" #include "worldmap/worldmap_state.hpp" namespace worldmap { -WorldMap::WorldMap(const std::string& filename, Savegame& savegame, const std::string& force_spawnpoint_) : - m_squirrel_environment(new SquirrelEnvironment(SquirrelVirtualMachine::current()->get_vm(), "worldmap")), - m_camera(new Camera), - m_enter_level(false), - m_tux(), +WorldMap::WorldMap(const std::string& filename, Savegame& savegame, const std::string& force_spawnpoint) : + m_sector(), + m_sectors(), m_savegame(savegame), - m_tileset(nullptr), - m_name(""), - m_init_script(), - m_passive_message_timer(), + m_tileset(), + m_name(), + m_map_filename(physfsutil::realpath(filename)), + m_levels_path(FileSystem::dirname(m_map_filename)), + m_next_worldmap(), m_passive_message(), - m_map_filename(), - m_levels_path(), - m_spawn_points(), - m_force_spawnpoint(force_spawnpoint_), - m_main_is_default(true), - m_initial_fade_tilemap(), - m_fade_direction(), + m_passive_message_timer(), + m_enter_level(false), m_in_level(false), m_in_world_select(false) { - m_tux = &add(this); - add(m_savegame.get_player_status()); - SoundManager::current()->preload("sounds/warp.wav"); - BIND_WORLDMAP(*this); + /** Parse worldmap */ + register_translation_directory(m_map_filename); + auto doc = ReaderDocument::from_file(m_map_filename); + auto root = doc.get_root(); - // load worldmap objects - WorldMapParser parser(*this); - parser.load_worldmap(filename); -} + if (root.get_name() != "supertux-level") + throw std::runtime_error("file isn't a supertux-level file."); -WorldMap::~WorldMap() -{ - clear_objects(); - m_spawn_points.clear(); -} + auto mapping = root.get_mapping(); -void -WorldMap::finish_construction() -{ - if (!get_object_by_type()) { - add(Color::WHITE); - } + mapping.get("name", m_name); - if (!get_object_by_type()) { - add(); - } + std::string tileset_name; + if (mapping.get("tileset", tileset_name)) + m_tileset = TileManager::current()->get_tileset(tileset_name); + else + m_tileset = TileManager::current()->get_tileset("images/ice_world.strf"); - if (!get_object_by_type()) { - add("Effect"); + auto iter = mapping.get_iter(); + while (iter.next()) + { + if (iter.get_key() == "sector") + add_sector(WorldMapSectorParser::from_reader(*this, iter.as_mapping())); } - - flush_game_objects(); } -bool -WorldMap::before_object_add(GameObject& object) -{ - m_squirrel_environment->try_expose(object); - return true; -} void -WorldMap::before_object_remove(GameObject& object) +WorldMap::setup() { - m_squirrel_environment->try_unexpose(object); -} + MenuManager::instance().clear_menu_stack(); -void -WorldMap::move_to_spawnpoint(const std::string& spawnpoint, bool pan, bool main_as_default) -{ - auto sp = get_spawnpoint_by_name(spawnpoint); - if (sp != nullptr) { - Vector p = sp->get_pos(); - m_tux->set_tile_pos(p); - m_tux->set_direction(sp->get_auto_dir()); - if (pan) { - m_camera->pan(); - } - return; - } + load_state(); + m_sector->setup(); - log_warning << "Spawnpoint '" << spawnpoint << "' not found." << std::endl; - if (spawnpoint != "main" && main_as_default) { - move_to_spawnpoint("main"); - } + m_in_world_select = false; } void -WorldMap::change(const std::string& filename, const std::string& force_spawnpoint_) +WorldMap::leave() { - m_savegame.get_player_status().last_worldmap = filename; - ScreenManager::current()->pop_screen(); - ScreenManager::current()->push_screen(std::make_unique( - std::make_unique(filename, m_savegame, force_spawnpoint_))); -} + save_state(); + m_sector->leave(); -void -WorldMap::on_escape_press() -{ - // Show or hide the menu - if (!MenuManager::instance().is_active()) - { - MenuManager::instance().set_menu(MenuStorage::WORLDMAP_MENU); - m_tux->set_direction(Direction::NONE); // stop tux movement when menu is called - } + GameManager::current()->load_next_worldmap(); } -Vector -WorldMap::get_next_tile(const Vector& pos, const Direction& direction) const -{ - auto position = pos; - switch (direction) { - case Direction::WEST: - position.x -= 1; - break; - case Direction::EAST: - position.x += 1; - break; - case Direction::NORTH: - position.y -= 1; - break; - case Direction::SOUTH: - position.y += 1; - break; - case Direction::NONE: - break; - } - return position; -} -bool -WorldMap::path_ok(const Direction& direction, const Vector& old_pos, Vector* new_pos) const +void +WorldMap::draw(DrawingContext& context) { - *new_pos = get_next_tile(old_pos, direction); + m_sector->draw(context); - if (!(new_pos->x >= 0 && new_pos->x < get_tiles_width() - && new_pos->y >= 0 && new_pos->y < get_tiles_height())) - { // New position is outsite the tilemap - return false; - } - else - { // Check if the tile allows us to go to new_pos - int old_tile_data = tile_data_at(old_pos); - int new_tile_data = tile_data_at(*new_pos); - switch (direction) - { - case Direction::WEST: - return (old_tile_data & Tile::WORLDMAP_WEST - && new_tile_data & Tile::WORLDMAP_EAST); - - case Direction::EAST: - return (old_tile_data & Tile::WORLDMAP_EAST - && new_tile_data & Tile::WORLDMAP_WEST); - - case Direction::NORTH: - return (old_tile_data & Tile::WORLDMAP_NORTH - && new_tile_data & Tile::WORLDMAP_SOUTH); - - case Direction::SOUTH: - return (old_tile_data & Tile::WORLDMAP_SOUTH - && new_tile_data & Tile::WORLDMAP_NORTH); - - case Direction::NONE: - log_warning << "path_ok() can't walk if direction is NONE" << std::endl; - assert(false); - } - return false; - } + context.pop_transform(); } void -WorldMap::finished_level(Level* gamelevel) +WorldMap::update(float dt_sec) { - // TODO use Level* parameter here? - auto level = at_level(); - - if (level == nullptr) { - return; - } - - bool old_level_state = level->is_solved(); - level->set_solved(true); - - // deal with statistics - level->get_statistics().update(gamelevel->m_stats); - - if (level->get_statistics().completed(level->get_statistics(), level->get_target_time())) { - level->set_perfect(true); - } + if (m_in_world_select) return; - save_state(); + if (m_in_level) return; + if (MenuManager::instance().is_active()) return; - if (old_level_state != level->is_solved()) { - // Try to detect the next direction to which we should walk - // FIXME: Mostly a hack - Direction dir = Direction::NONE; - - int dirdata = available_directions_at(m_tux->get_tile_pos()); - // first, test for crossroads - if (dirdata == Tile::WORLDMAP_CNSE || - dirdata == Tile::WORLDMAP_CNSW || - dirdata == Tile::WORLDMAP_CNEW || - dirdata == Tile::WORLDMAP_CSEW || - dirdata == Tile::WORLDMAP_CNSEW) - dir = Direction::NONE; - else if (dirdata & Tile::WORLDMAP_NORTH - && m_tux->m_back_direction != Direction::NORTH) - dir = Direction::NORTH; - else if (dirdata & Tile::WORLDMAP_SOUTH - && m_tux->m_back_direction != Direction::SOUTH) - dir = Direction::SOUTH; - else if (dirdata & Tile::WORLDMAP_EAST - && m_tux->m_back_direction != Direction::EAST) - dir = Direction::EAST; - else if (dirdata & Tile::WORLDMAP_WEST - && m_tux->m_back_direction != Direction::WEST) - dir = Direction::WEST; - - if (dir != Direction::NONE) { - m_tux->set_direction(dir); - } + if (m_next_worldmap) // A worldmap is scheduled to be changed to. + { + m_savegame.get_player_status().last_worldmap = m_next_worldmap->m_map_filename; + ScreenManager::current()->pop_screen(); + ScreenManager::current()->push_screen(std::make_unique(std::move(m_next_worldmap))); + return; } - if (!level->get_extro_script().empty()) { - try { - run_script(level->get_extro_script(), "worldmap:extro_script"); - } catch(std::exception& e) { - log_warning << "Couldn't run level-extro-script: " << e.what() << std::endl; - } - } + m_sector->update(dt_sec); } void @@ -345,392 +178,136 @@ WorldMap::process_input(const Controller& controller) } } + void -WorldMap::update(float dt_sec) +WorldMap::on_escape_press() { - if (m_in_world_select) return; - - BIND_WORLDMAP(*this); - - if (m_in_level) return; - if (MenuManager::instance().is_active()) return; - - GameObjectManager::update(dt_sec); - - m_camera->update(dt_sec); - - { - // check for teleporters - auto teleporter = at_teleporter(m_tux->get_tile_pos()); - if (teleporter && (teleporter->is_automatic() || (m_enter_level && (!m_tux->is_moving())))) { - m_enter_level = false; - if (!teleporter->get_worldmap().empty()) { - change(teleporter->get_worldmap(), teleporter->get_spawnpoint()); - } else { - // TODO: an animation, camera scrolling or a fading would be a nice touch - SoundManager::current()->play("sounds/warp.wav"); - m_tux->m_back_direction = Direction::NONE; - move_to_spawnpoint(teleporter->get_spawnpoint(), true); - } - } - } - - { - // check for auto-play levels - auto level = at_level(); - if (level && level->is_auto_play() && !level->is_solved() && !m_tux->is_moving()) { - m_enter_level = true; - // automatically mark these levels as solved in case player aborts - level->set_solved(true); - } - } - + // Show or hide the menu + if (!MenuManager::instance().is_active()) { - if (m_enter_level && !m_tux->is_moving()) - { - /* Check level action */ - auto level_ = at_level(); - if (!level_) { - //Respawn if player on a tile with no level and nowhere to go. - int tile_data = tile_data_at(m_tux->get_tile_pos()); - if (!( tile_data & ( Tile::WORLDMAP_NORTH | Tile::WORLDMAP_SOUTH | Tile::WORLDMAP_WEST | Tile::WORLDMAP_EAST ))){ - log_warning << "Player at illegal position " << m_tux->get_tile_pos().x << ", " << m_tux->get_tile_pos().y << " respawning." << std::endl; - move_to_spawnpoint("main"); - return; - } - log_warning << "No level to enter at: " << m_tux->get_tile_pos().x << ", " << m_tux->get_tile_pos().y << std::endl; - return; - } - - if (level_->get_pos() == m_tux->get_tile_pos()) { - try { - Vector shrinkpos = Vector(level_->get_pos().x * 32 + 16 - m_camera->get_offset().x, - level_->get_pos().y * 32 + 8 - m_camera->get_offset().y); - std::string levelfile = m_levels_path + level_->get_level_filename(); - - // update state and savegame - save_state(); - ScreenManager::current()->push_screen(std::make_unique(levelfile, m_savegame, &level_->get_statistics()), - std::make_unique(shrinkpos, 1.0f)); - - m_in_level = true; - } catch(std::exception& e) { - log_fatal << "Couldn't load level: " << e.what() << std::endl; - } - } - } - else - { - // tux->set_direction(input_direction); - } - } -} - -int -WorldMap::tile_data_at(const Vector& p) const -{ - int dirs = 0; - - for (const auto& tilemap : get_solid_tilemaps()) { - const Tile& tile = tilemap->get_tile(static_cast(p.x), static_cast(p.y)); - int dirdata = tile.get_data(); - dirs |= dirdata; + MenuManager::instance().set_menu(MenuStorage::WORLDMAP_MENU); + m_sector->get_tux().set_direction(Direction::NONE); // stop tux movement when menu is called } - - return dirs; } -int -WorldMap::available_directions_at(const Vector& p) const -{ - return tile_data_at(p) & Tile::WORLDMAP_DIR_MASK; -} -LevelTile* -WorldMap::at_level() const -{ - for (auto& level : get_objects_by_type()) { - if (level.get_pos() == m_tux->get_tile_pos()) - return &level; - } - - return nullptr; -} - -SpecialTile* -WorldMap::at_special_tile() const +size_t +WorldMap::level_count() const { - for (auto& special_tile : get_objects_by_type()) { - if (special_tile.get_pos() == m_tux->get_tile_pos()) - return &special_tile; + size_t count = 0; + for (auto& sector : m_sectors) + { + count += sector->level_count(); } - - return nullptr; + return count; } -SpriteChange* -WorldMap::at_sprite_change(const Vector& pos) const +size_t +WorldMap::solved_level_count() const { - for (auto& sprite_change : get_objects_by_type()) { - if (sprite_change.get_pos() == pos) - return &sprite_change; + size_t count = 0; + for (auto& sector : m_sectors) + { + count += sector->solved_level_count(); } - - return nullptr; + return count; } -Teleporter* -WorldMap::at_teleporter(const Vector& pos) const -{ - for (auto& teleporter : get_objects_by_type()) { - if (teleporter.get_pos() == pos) - return &teleporter; - } - - return nullptr; -} void -WorldMap::draw(DrawingContext& context) +WorldMap::load_state() { - BIND_WORLDMAP(*this); - - if (get_width() < static_cast(context.get_width()) || - get_height() < static_cast(context.get_height())) - { - context.color().draw_filled_rect(context.get_rect(), - Color(0.0f, 0.0f, 0.0f, 1.0f), LAYER_BACKGROUND0); - } - - context.push_transform(); - context.set_translation(m_camera->get_offset()); - - GameObjectManager::draw(context); - - if (g_debug.show_worldmap_path) - { - for (int x = 0; x < static_cast(get_tiles_width()); x++) { - for (int y = 0; y < static_cast(get_tiles_height()); y++) { - const int data = tile_data_at(Vector(static_cast(x), static_cast(y))); - const int px = x * 32; - const int py = y * 32; - const int W = 4; - const int layer = LAYER_FOREGROUND1 + 1000; - const Color color(1.0f, 0.0f, 1.0f, 0.5f); - if (data & Tile::WORLDMAP_NORTH) context.color().draw_filled_rect(Rect(px + 16-W, py , px + 16+W, py + 16-W), color, layer); - if (data & Tile::WORLDMAP_SOUTH) context.color().draw_filled_rect(Rect(px + 16-W, py + 16+W, px + 16+W, py + 32 ), color, layer); - if (data & Tile::WORLDMAP_EAST) context.color().draw_filled_rect(Rect(px + 16+W, py + 16-W, px + 32 , py + 16+W), color, layer); - if (data & Tile::WORLDMAP_WEST) context.color().draw_filled_rect(Rect(px , py + 16-W, px + 16-W, py + 16+W), color, layer); - if (data & Tile::WORLDMAP_DIR_MASK) context.color().draw_filled_rect(Rect(px + 16-W, py + 16-W, px + 16+W, py + 16+W), color, layer); - if (data & Tile::WORLDMAP_STOP) context.color().draw_filled_rect(Rect(px + 4 , py + 4 , px + 28 , py + 28 ), color, layer); - } - } - } - - draw_status(context); - context.pop_transform(); + WorldMapState state(*this); + state.load_state(); } void -WorldMap::draw_status(DrawingContext& context) +WorldMap::save_state() { - context.push_transform(); - context.set_translation(Vector(0, 0)); - - if (!m_tux->is_moving()) { - for (auto& level : get_objects_by_type()) { - if (level.get_pos() == m_tux->get_tile_pos()) { - context.color().draw_text(Resources::normal_font, level.get_title(), - Vector(static_cast(context.get_width()) / 2.0f, - static_cast(context.get_height()) - Resources::normal_font->get_height() - 10), - ALIGN_CENTER, LAYER_HUD, level.get_title_color()); - - if (g_config->developer_mode) { - context.color().draw_text(Resources::small_font, FileSystem::join(level.get_basedir(), level.get_level_filename()), - Vector(static_cast(context.get_width()) / 2.0f, - static_cast(context.get_height()) - Resources::normal_font->get_height() - 25), - ALIGN_CENTER, LAYER_HUD, level.get_title_color()); - } - - // if level is solved, draw level picture behind stats - /* - if (level.solved) { - if (const Surface* picture = level.get_picture()) { - Vector pos = Vector(context.get_width() - picture->get_width(), context.get_height() - picture->get_height()); - context.push_transform(); - context.set_alpha(0.5); - context.color().draw_surface(picture, pos, LAYER_FOREGROUND1-1); - context.pop_transform(); - } - } - */ - level.get_statistics().draw_worldmap_info(context, level.get_target_time()); - break; - } - } - - for (auto& special_tile : get_objects_by_type()) { - if (special_tile.get_pos() == m_tux->get_tile_pos()) { - /* Display an in-map message in the map, if any as been selected */ - if (!special_tile.get_map_message().empty() && !special_tile.is_passive_message()) - context.color().draw_text(Resources::normal_font, special_tile.get_map_message(), - Vector(static_cast(context.get_width()) / 2.0f, - static_cast(context.get_height()) - static_cast(Resources::normal_font->get_height()) - 60.0f), - ALIGN_CENTER, LAYER_FOREGROUND1, WorldMap::message_color); - break; - } - } - - // display teleporter messages - auto teleporter = at_teleporter(m_tux->get_tile_pos()); - if (teleporter && (!teleporter->get_message().empty())) { - Vector pos = Vector(static_cast(context.get_width()) / 2.0f, - static_cast(context.get_height()) - Resources::normal_font->get_height() - 30.0f); - context.color().draw_text(Resources::normal_font, teleporter->get_message(), pos, ALIGN_CENTER, LAYER_FOREGROUND1, WorldMap::teleporter_message_color); - } - } - - /* Display a passive message in the map, if needed */ - if (m_passive_message_timer.started()) - context.color().draw_text(Resources::normal_font, m_passive_message, - Vector(static_cast(context.get_width()) / 2.0f, - static_cast(context.get_height()) - Resources::normal_font->get_height() - 60.0f), - ALIGN_CENTER, LAYER_FOREGROUND1, WorldMap::message_color); - - context.pop_transform(); + WorldMapState state(*this); + state.save_state(); } -void -WorldMap::setup() -{ - auto& music_object = get_singleton_by_type(); - music_object.play_music(MusicType::LEVEL_MUSIC); - - MenuManager::instance().clear_menu_stack(); - ScreenManager::current()->set_screen_fade(std::make_unique(FadeToBlack::FADEIN, 1.0f)); - - load_state(); - - // if force_spawnpoint was set, move Tux there, then clear force_spawnpoint - if (!m_force_spawnpoint.empty()) { - move_to_spawnpoint(m_force_spawnpoint, false, m_main_is_default); - m_force_spawnpoint = ""; - m_main_is_default = true; - } - - // If we specified a fade tilemap, let's fade it: - if (!m_initial_fade_tilemap.empty()) - { - auto tilemap = get_object_by_name(m_initial_fade_tilemap); - if (tilemap != nullptr) - { - if (m_fade_direction == 0) - { - tilemap->fade(1.0, 1); - } - else - { - tilemap->fade(0.0, 1); - } - } - m_initial_fade_tilemap = ""; - } - - m_tux->setup(); - - // register worldmap_table as worldmap in scripting - m_squirrel_environment->expose_self(); - - m_squirrel_environment->expose("settings", std::make_unique(this)); - - //Run default.nut just before init script - try { - IFileStream in(m_levels_path + "default.nut"); - m_squirrel_environment->run_script(in, "WorldMap::default.nut"); - } catch(std::exception& ) { - // doesn't exist or erroneous; do nothing - } - - if (!m_init_script.empty()) { - m_squirrel_environment->run_script(m_init_script, "WorldMap::init"); - } - m_tux->process_special_tile( at_special_tile() ); - - m_in_world_select = false; -} void -WorldMap::leave() +WorldMap::change(const std::string& filename, const std::string& force_spawnpoint_) { - // save state of world and player - save_state(); - - // remove worldmap_table from roottable - m_squirrel_environment->unexpose_self(); - - GameManager::current()->load_next_worldmap(); + // Schedule worldmap to be changed to next frame. + m_next_worldmap = std::make_unique(filename, m_savegame, force_spawnpoint_); } + void WorldMap::set_levels_solved(bool solved, bool perfect) { - for (auto& level : get_objects_by_type()) + for (auto& level : m_sector->get_objects_by_type()) { level.set_solved(solved); level.set_perfect(perfect); } } -size_t -WorldMap::level_count() const +void +WorldMap::set_passive_message(const std::string& message, float time) { - return get_object_count(); + m_passive_message = message; + m_passive_message_timer.start(time); } -size_t -WorldMap::solved_level_count() const + +WorldMapSector* +WorldMap::get_sector(const std::string& name) const { - size_t count = 0; - for (auto& level : get_objects_by_type()) { - if (level.is_solved()) { - count++; - } + for (auto& sector : m_sectors) + { + if (sector->get_name() == name) + return sector.get(); } - - return count; + return nullptr; } -void -WorldMap::load_state() +WorldMapSector* +WorldMap::get_sector(int index) const { - WorldMapState state(*this); - state.load_state(); -} + if (index < 0 || index > static_cast(m_sectors.size()) - 1) + return nullptr; -void -WorldMap::save_state() -{ - WorldMapState state(*this); - state.save_state(); + return m_sectors.at(index).get(); } + void -WorldMap::run_script(const std::string& script, const std::string& sourcename) +WorldMap::add_sector(std::unique_ptr sector) { - m_squirrel_environment->run_script(script, sourcename); + m_sectors.push_back(std::move(sector)); } void -WorldMap::set_passive_message(const std::string& message, float time) +WorldMap::set_sector(const std::string& name, const std::string& spawnpoint, + bool perform_full_setup) { - m_passive_message = message; - m_passive_message_timer.start(time); -} + if (m_sector) // There is a current sector. + { + save_state(); + m_sector->leave(); + } -Vector -WorldMap::get_tux_pos() -{ - return m_tux->get_pos(); + m_sector = get_sector(name); + + if (!m_sector) // The sector was not found, so no sector is assigned. + { + log_warning << "Sector '" << name << "' not found. Setting first sector." << std::endl; + m_sector = get_sector(0); // In that case, assign the first sector. + } + + // Set up the new sector. + if (perform_full_setup) + load_state(); + m_sector->setup(); + + // If a spawnpoint has been provided, move to it. + if (!spawnpoint.empty()) + m_sector->move_to_spawnpoint(spawnpoint); } } // namespace worldmap diff --git a/src/worldmap/worldmap.hpp b/src/worldmap/worldmap.hpp index 508bffde955..e7e791eb693 100644 --- a/src/worldmap/worldmap.hpp +++ b/src/worldmap/worldmap.hpp @@ -1,6 +1,7 @@ // SuperTux // Copyright (C) 2004 Ingo Ruhnke // Copyright (C) 2006 Christoph Sommer +// 2023 Vankata453 // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -18,189 +19,91 @@ #ifndef HEADER_SUPERTUX_WORLDMAP_WORLDMAP_HPP #define HEADER_SUPERTUX_WORLDMAP_WORLDMAP_HPP -#include +#include "util/currenton.hpp" -#include "math/vector.hpp" -#include "supertux/game_object_manager.hpp" -#include "squirrel/squirrel_environment.hpp" -#include "supertux/statistics.hpp" +#include "control/controller.hpp" +#include "supertux/savegame.hpp" #include "supertux/timer.hpp" -#include "util/currenton.hpp" -#include "worldmap/direction.hpp" -#include "worldmap/spawn_point.hpp" - -class Controller; -class Level; -class PlayerStatus; -class Savegame; -class Sprite; -class SquirrelEnvironment; -class TileMap; +#include "worldmap/worldmap_sector.hpp" + class TileSet; namespace worldmap { -class Camera; -class LevelTile; -class SpecialTile; -class SpriteChange; -class Teleporter; -class Tux; - -class WorldMap final : public GameObjectManager, - public Currenton +class WorldMap final : public Currenton { -public: - friend class WorldMapParser; + friend class WorldMapSector; friend class WorldMapState; - static Color level_title_color; - static Color message_color; - static Color teleporter_message_color; +public: + static Color s_level_title_color; + static Color s_message_color; + static Color s_teleporter_message_color; public: WorldMap(const std::string& filename, Savegame& savegame, const std::string& force_spawnpoint = ""); - ~WorldMap() override; - - void finish_construction(); void setup(); void leave(); void draw(DrawingContext& context); void update(float dt_sec); - void process_input(const Controller& controller); - Vector get_next_tile(const Vector& pos, const Direction& direction) const; - - /** gets a bitfield of Tile::WORLDMAP_NORTH | Tile::WORLDMAP_WEST | - ... values, which indicates the directions Tux can move to when - at the given position. */ - int available_directions_at(const Vector& pos) const; - - /** returns a bitfield representing the union of all - Tile::WORLDMAP_XXX values of all solid tiles at the given - position */ - int tile_data_at(const Vector& pos) const; - size_t level_count() const; size_t solved_level_count() const; - /** gets called from the GameSession when a level has been successfully - finished */ - void finished_level(Level* level); - - Savegame& get_savegame() const { return m_savegame; } - - /** Get a spawnpoint by its name @param name The name of the - spawnpoint @return spawnpoint corresponding to that name */ - SpawnPoint* get_spawnpoint_by_name(const std::string& spawnpoint_name) const - { - for (const auto& sp : m_spawn_points) { - if (sp->get_name() == spawnpoint_name) { - return sp.get(); - } - } - return nullptr; - } - - LevelTile* at_level() const; - SpecialTile* at_special_tile() const; - SpriteChange* at_sprite_change(const Vector& pos) const; - Teleporter* at_teleporter(const Vector& pos) const; - - /** Check if it is possible to walk from \a pos into \a direction, - if possible, write the new position to \a new_pos */ - bool path_ok(const Direction& direction, const Vector& pos, Vector* new_pos) const; - - /** Save worldmap state to squirrel state table */ - void save_state(); - /** Load worldmap state from squirrel state table */ void load_state(); - const std::string& get_title() const { return m_name; } + /** Save worldmap state to squirrel state table */ + void save_state(); /** switch to another worldmap. filename is relative to data root path */ - void change(const std::string& filename, const std::string& force_spawnpoint=""); - - /** Moves Tux to the given spawnpoint - @param spawnpoint Name of the spawnpoint to move to - @param pan Pan the camera during to new spawnpoint - @param main_as_default Move Tux to main spawnpoint if specified spawnpoint can't be found */ - void move_to_spawnpoint(const std::string& spawnpoint, bool pan = false, bool main_as_default = true); + void change(const std::string& filename, const std::string& force_spawnpoint = ""); /** Mark all levels as solved or unsolved */ void set_levels_solved(bool solved, bool perfect); - /** Sets the name of the tilemap that should fade when worldmap is set up. */ - void set_initial_fade_tilemap(const std::string& tilemap_name, int direction) - { - m_initial_fade_tilemap = tilemap_name; - m_fade_direction = direction; - } - - /** Sets the initial spawnpoint on worldmap setup */ - void set_initial_spawnpoint(const std::string& spawnpoint_name) - { - m_force_spawnpoint = spawnpoint_name; - - // If spawnpoint we specified can not be found, - // don't bother moving to the main spawnpoint. - m_main_is_default = false; - } - - void run_script(const std::string& script, const std::string& sourcename); - + /** Sets the passive message with specific time **/ void set_passive_message(const std::string& message, float time); - Camera& get_camera() const { return *m_camera; } + const std::string& get_title() const { return m_name; } + Savegame& get_savegame() const { return m_savegame; } + const std::string& get_levels_path() const { return m_levels_path; } - Vector get_tux_pos(); + WorldMapSector* get_sector(const std::string& name) const; + WorldMapSector* get_sector(int index) const; -protected: - virtual bool before_object_add(GameObject& object) override; - virtual void before_object_remove(GameObject& object) override; + void add_sector(std::unique_ptr sector); + WorldMapSector& get_sector() const { return *m_sector; } + void set_sector(const std::string& name, const std::string& spawnpoint = "", + bool perform_full_setup = true); private: - void draw_status(DrawingContext& context); - - void load(const std::string& filename); void on_escape_press(); private: - std::unique_ptr m_squirrel_environment; - std::unique_ptr m_camera; - - bool m_enter_level; - - Tux* m_tux; + WorldMapSector* m_sector; /* The currently active sector. */ + std::vector > m_sectors; Savegame& m_savegame; - TileSet* m_tileset; std::string m_name; - std::string m_init_script; - - /** Variables to deal with the passive map messages */ - Timer m_passive_message_timer; - std::string m_passive_message; - std::string m_map_filename; std::string m_levels_path; - std::vector > m_spawn_points; + /* A worldmap, scheduled to change to next frame. */ + std::unique_ptr m_next_worldmap; - std::string m_force_spawnpoint; /**< if set, spawnpoint will be forced to this value */ - bool m_main_is_default; - std::string m_initial_fade_tilemap; - int m_fade_direction; + /** Passive map message variables */ + std::string m_passive_message; + Timer m_passive_message_timer; + bool m_enter_level; bool m_in_level; - bool m_in_world_select; private: diff --git a/src/worldmap/worldmap_object.cpp b/src/worldmap/worldmap_object.cpp new file mode 100644 index 00000000000..87543d622cf --- /dev/null +++ b/src/worldmap/worldmap_object.cpp @@ -0,0 +1,135 @@ +// SuperTux +// Copyright (C) 2016 Hume2 +// 2023 Vankata453 +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include "worldmap/worldmap_object.hpp" + +#include "editor/editor.hpp" +#include "worldmap/worldmap_sector.hpp" + +namespace worldmap { + +bool +WorldMapObject::in_worldmap() +{ + return !Editor::is_active(); +} + + +WorldMapObject::WorldMapObject(const ReaderMapping& mapping, const std::string& default_sprite) : + MovingSprite(mapping, default_sprite), + m_tile_x(), + m_tile_y() +{ + initialize(); +} + +WorldMapObject::WorldMapObject(const ReaderMapping& mapping) : + MovingSprite(mapping), + m_tile_x(), + m_tile_y() +{ + initialize(); +} + +WorldMapObject::WorldMapObject(const Vector& pos, const std::string& default_sprite) : + MovingSprite(pos, default_sprite), + m_tile_x(), + m_tile_y() +{ + initialize(); +} + +void +WorldMapObject::initialize() +{ + // Set sector position from provided tile position + set_pos(Vector(32.0f * m_col.m_bbox.get_left() + + (m_col.m_bbox.get_width() < 32.f ? (32.f - m_col.m_bbox.get_width()) / 2 : 0), + 32.0f * m_col.m_bbox.get_top() + + (m_col.m_bbox.get_width() < 32.f ? (32.f - m_col.m_bbox.get_height()) / 2 : 0))); + + update_pos(); + update_hitbox(); +} + +ObjectSettings +WorldMapObject::get_settings() +{ + ObjectSettings result = MovingSprite::get_settings(); + + result.remove("x"); + result.remove("y"); + + result.add_int(_("X"), &m_tile_x, "x", {}, OPTION_HIDDEN); + result.add_int(_("Y"), &m_tile_y, "y", {}, OPTION_HIDDEN); + + return result; +} + +void +WorldMapObject::draw(DrawingContext& context) +{ + if (in_worldmap()) + draw_worldmap(context); + else + draw_normal(context); +} + +void +WorldMapObject::draw_worldmap(DrawingContext& context) +{ + draw_normal(context); +} + +void +WorldMapObject::draw_normal(DrawingContext& context) +{ + if (!m_sprite) return; + + m_sprite->draw(context.color(), + m_col.m_bbox.p1() + Vector((m_col.m_bbox.get_width() < 32.f ? (32.f - m_col.m_bbox.get_width()) / 2 : 0), + (m_col.m_bbox.get_height() < 32.f ? (32.f - m_col.m_bbox.get_height()) / 2 : 0)), + m_layer); +} + +void +WorldMapObject::update(float) +{ + update_pos(); +} + +void +WorldMapObject::update_pos() +{ + m_tile_x = static_cast(m_col.m_bbox.get_left()) / 32; + m_tile_y = static_cast(m_col.m_bbox.get_top()) / 32; +} + +void +WorldMapObject::move_to(const Vector& pos) +{ + // Set sector position to the provided position, rounding it to be divisible by 32 + set_pos(Vector(32.0f * static_cast(pos.x / 32) + + (m_col.m_bbox.get_width() < 32.f ? (32.f - m_col.m_bbox.get_width()) / 2 : 0), + 32.0f * static_cast(pos.y / 32) + + (m_col.m_bbox.get_width() < 32.f ? (32.f - m_col.m_bbox.get_height()) / 2 : 0))); + update_pos(); +} + +} // namespace worldmap + +/* EOF */ diff --git a/src/worldmap/worldmap_object.hpp b/src/worldmap/worldmap_object.hpp new file mode 100644 index 00000000000..7d60963f188 --- /dev/null +++ b/src/worldmap/worldmap_object.hpp @@ -0,0 +1,75 @@ +// SuperTux +// Copyright (C) 2016 Hume2 +// 2023 Vankata453 +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#ifndef HEADER_SUPERTUX_WORLDMAP_WORLDMAP_OBJECT_HPP +#define HEADER_SUPERTUX_WORLDMAP_WORLDMAP_OBJECT_HPP + +#include "object/moving_sprite.hpp" + +#include "math/vector.hpp" + +class DrawingContext; +class ReaderMapping; + +namespace worldmap { + +class WorldMapObject : public MovingSprite +{ +protected: + static bool in_worldmap(); + +public: + WorldMapObject(const ReaderMapping& mapping, const std::string& default_sprite); + WorldMapObject(const ReaderMapping& mapping); + WorldMapObject(const Vector& pos, const std::string& default_sprite); + + static std::string class_name() { return "worldmap-object"; } + virtual std::string get_class_name() const override { return class_name(); } + + void draw(DrawingContext& context) override; + + /** Draws the object, when on a worldmap. */ + virtual void draw_worldmap(DrawingContext& context); + + void update(float) override; + + virtual HitResponse collision(GameObject& other, const CollisionHit& hit) override { return FORCE_MOVE; } + virtual ObjectSettings get_settings() override; + virtual void move_to(const Vector& pos) override; + + Vector get_tile_pos() const { return { m_tile_x, m_tile_y }; } + +private: + void initialize(); + + void draw_normal(DrawingContext& context); + void update_pos(); + +private: + int m_tile_x; + int m_tile_y; + +private: + WorldMapObject(const WorldMapObject&) = delete; + WorldMapObject& operator=(const WorldMapObject&) = delete; +}; + +} // namespace worldmap + +#endif + +/* EOF */ diff --git a/src/worldmap/worldmap_parser.cpp b/src/worldmap/worldmap_parser.cpp deleted file mode 100644 index 90bc074db69..00000000000 --- a/src/worldmap/worldmap_parser.cpp +++ /dev/null @@ -1,206 +0,0 @@ -// SuperTux -// Copyright (C) 2018 Ingo Ruhnke -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -#include "worldmap/worldmap_parser.hpp" - -#include -#include - -#include "object/ambient_light.hpp" -#include "object/background.hpp" -#include "object/decal.hpp" -#include "object/music_object.hpp" -#include "object/path_gameobject.hpp" -#include "object/tilemap.hpp" -#include "physfs/physfs_file_system.hpp" -#include "physfs/util.hpp" -#include "supertux/tile_manager.hpp" -#include "util/file_system.hpp" -#include "util/log.hpp" -#include "util/reader.hpp" -#include "util/reader_document.hpp" -#include "util/reader_mapping.hpp" -#include "util/reader_object.hpp" -#include "worldmap/level_tile.hpp" -#include "worldmap/spawn_point.hpp" -#include "worldmap/special_tile.hpp" -#include "worldmap/sprite_change.hpp" -#include "worldmap/teleporter.hpp" -#include "worldmap/tux.hpp" -#include "worldmap/worldmap.hpp" -#include "worldmap/worldmap_parser.hpp" -#include "worldmap/worldmap_screen.hpp" - -namespace worldmap { - -WorldMapParser::WorldMapParser(WorldMap& worldmap) : - m_worldmap(worldmap) -{ -} - -void -WorldMapParser::load_worldmap(const std::string& filename) -{ - m_worldmap.m_map_filename = physfsutil::realpath(filename); - m_worldmap.m_levels_path = FileSystem::dirname(m_worldmap.m_map_filename); - - try { - register_translation_directory(m_worldmap.m_map_filename); - auto doc = ReaderDocument::from_file(m_worldmap.m_map_filename); - auto root = doc.get_root(); - - if (root.get_name() != "supertux-level") - throw std::runtime_error("file isn't a supertux-level file."); - - auto level_ = root.get_mapping(); - - level_.get("name", m_worldmap.m_name); - - std::string tileset_name; - if (level_.get("tileset", tileset_name)) { - if (m_worldmap.m_tileset != nullptr) { - log_warning << "multiple tilesets specified in level_" << std::endl; - } else { - m_worldmap.m_tileset = TileManager::current()->get_tileset(tileset_name); - } - } - /* load default tileset */ - if (m_worldmap.m_tileset == nullptr) { - m_worldmap.m_tileset = TileManager::current()->get_tileset("images/ice_world.strf"); - } - - std::optional sector; - if (!level_.get("sector", sector)) { - throw std::runtime_error("No sector specified in worldmap file."); - } else { - auto iter = sector->get_iter(); - while (iter.next()) { - if (iter.get_key() == "tilemap") { - m_worldmap.add(m_worldmap.m_tileset, iter.as_mapping()); - } else if (iter.get_key() == "background") { - m_worldmap.add(iter.as_mapping()); - } else if (iter.get_key() == "music") { - const auto& sx = iter.get_sexp(); - if (sx.is_array() && sx.as_array().size() == 2 && sx.as_array()[1].is_string()) { - std::string value; - iter.get(value); - m_worldmap.add().set_music(value); - } else { - m_worldmap.add(iter.as_mapping()); - } - } else if (iter.get_key() == "init-script") { - iter.get(m_worldmap.m_init_script); - } else if (iter.get_key() == "worldmap-spawnpoint") { - auto sp = std::make_unique(iter.as_mapping()); - m_worldmap.m_spawn_points.push_back(std::move(sp)); - } else if (iter.get_key() == "level") { - auto& level = m_worldmap.add(m_worldmap.m_levels_path, iter.as_mapping()); - load_level_information(level); - } else if (iter.get_key() == "special-tile") { - m_worldmap.add(iter.as_mapping()); - } else if (iter.get_key() == "sprite-change") { - m_worldmap.add(iter.as_mapping()); - } else if (iter.get_key() == "teleporter") { - m_worldmap.add(iter.as_mapping()); - } else if (iter.get_key() == "decal") { - m_worldmap.add(iter.as_mapping()); - } else if (iter.get_key() == "path") { - m_worldmap.add(iter.as_mapping()); - } else if (iter.get_key() == "ambient-light") { - const auto& sx = iter.get_sexp(); - if (sx.is_array() && sx.as_array().size() >= 3 && - sx.as_array()[1].is_real() && sx.as_array()[2].is_real() && sx.as_array()[3].is_real()) - { - // for backward compatibilty - std::vector vColor; - bool hasColor = sector->get("ambient-light", vColor); - if (vColor.size() < 3 || !hasColor) { - log_warning << "(ambient-light) requires a color as argument" << std::endl; - } else { - m_worldmap.add(Color(vColor)); - } - } else { - // modern format - m_worldmap.add(iter.as_mapping()); - } - } else if (iter.get_key() == "name") { - // skip - } else { - log_warning << "Unknown token '" << iter.get_key() << "' in worldmap" << std::endl; - } - } - } - - m_worldmap.flush_game_objects(); - - if (m_worldmap.get_solid_tilemaps().empty()) - log_warning << "No solid tilemap specified" << std::endl; - - m_worldmap.move_to_spawnpoint("main"); - - } catch(std::exception& e) { - std::stringstream msg; - msg << "Problem when parsing worldmap '" << m_worldmap.m_map_filename << "': " << - e.what(); - throw std::runtime_error(msg.str()); - } - - m_worldmap.finish_construction(); -} - -void -WorldMapParser::load_level_information(LevelTile& level) -{ - /** get special_tile's title */ - level.m_title = _(""); - level.m_target_time = 0.0f; - - try { - std::string filename = m_worldmap.m_levels_path + level.get_level_filename(); - - if (m_worldmap.m_levels_path == "./") - filename = level.get_level_filename(); - - if (!PHYSFS_exists(filename.c_str())) - { - log_warning << "Level file '" << filename << "' does not exist. Skipping." << std::endl; - return; - } - if (physfsutil::is_directory(filename)) - { - log_warning << "Level file '" << filename << "' is a directory. Skipping." << std::endl; - return; - } - - register_translation_directory(filename); - auto doc = ReaderDocument::from_file(filename); - auto root = doc.get_root(); - if (root.get_name() != "supertux-level") { - return; - } else { - auto level_mapping = root.get_mapping(); - level_mapping.get("name", level.m_title); - level_mapping.get("target-time", level.m_target_time); - } - } catch(std::exception& e) { - log_warning << "Problem when reading level information: " << e.what() << std::endl; - return; - } -} - -} // namespace worldmap - -/* EOF */ diff --git a/src/worldmap/worldmap_sector.cpp b/src/worldmap/worldmap_sector.cpp new file mode 100644 index 00000000000..e680bb94c36 --- /dev/null +++ b/src/worldmap/worldmap_sector.cpp @@ -0,0 +1,609 @@ +// SuperTux - A Jump'n Run +// Copyright (C) 2004 Ingo Ruhnke +// Copyright (C) 2006 Christoph Sommer +// 2023 Vankata453 +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include "worldmap/worldmap_sector.hpp" + +#include "audio/sound_manager.hpp" +#include "object/ambient_light.hpp" +#include "object/display_effect.hpp" +#include "object/music_object.hpp" +#include "object/tilemap.hpp" +#include "physfs/ifile_stream.hpp" +#include "scripting/worldmap_sector.hpp" +#include "squirrel/squirrel_environment.hpp" +#include "supertux/d_scope.hpp" +#include "supertux/debug.hpp" +#include "supertux/fadetoblack.hpp" +#include "supertux/game_manager.hpp" +#include "supertux/game_session.hpp" +#include "supertux/gameconfig.hpp" +#include "supertux/level.hpp" +#include "supertux/player_status_hud.hpp" +#include "supertux/resources.hpp" +#include "supertux/screen_manager.hpp" +#include "supertux/shrinkfade.hpp" +#include "supertux/tile.hpp" +#include "util/file_system.hpp" +#include "worldmap/camera.hpp" +#include "worldmap/level_tile.hpp" +#include "worldmap/spawn_point.hpp" +#include "worldmap/special_tile.hpp" +#include "worldmap/teleporter.hpp" +#include "worldmap/worldmap.hpp" + +namespace worldmap { + +WorldMapSector* +WorldMapSector::current() +{ + if (!WorldMap::current()) + return nullptr; + + return &WorldMap::current()->get_sector(); +} + + +WorldMapSector::WorldMapSector(WorldMap& parent) : + Base::Sector("worldmap"), + m_parent(parent), + m_camera(new Camera), + m_tux(&add(&parent)), + m_spawnpoints(), + m_force_spawnpoint(), + m_main_is_default(true), + m_initial_fade_tilemap(), + m_fade_direction() +{ + BIND_WORLDMAP_SECTOR(*this); + + add(m_parent.get_savegame().get_player_status()); +} + +WorldMapSector::~WorldMapSector() +{ + m_spawnpoints.clear(); + + clear_objects(); +} + +void +WorldMapSector::finish_construction(bool) +{ + if (!get_object_by_type()) + add(Color::WHITE); + + if (!get_object_by_type()) + add(); + + if (!get_object_by_type()) + add("Effect"); + + flush_game_objects(); +} + + +void +WorldMapSector::setup() +{ + BIND_WORLDMAP_SECTOR(*this); + + auto& music_object = get_singleton_by_type(); + music_object.play_music(MusicType::LEVEL_MUSIC); + + ScreenManager::current()->set_screen_fade(std::make_unique(FadeToBlack::FADEIN, 1.0f)); + + // If we specified a fade tilemap, let's fade it: + if (!m_initial_fade_tilemap.empty()) + { + auto tilemap = get_object_by_name(m_initial_fade_tilemap); + if (tilemap != nullptr) + { + if (m_fade_direction == 0) + { + tilemap->fade(1.0, 1); + } + else + { + tilemap->fade(0.0, 1); + } + } + m_initial_fade_tilemap = ""; + } + + m_tux->setup(); + + // register worldmap_table as "worldmap" in scripting + m_squirrel_environment->expose_self(); + m_squirrel_environment->expose("settings", std::make_unique(this)); + + /** Force spawnpoint, if the property is set. **/ + if (!m_force_spawnpoint.empty()) + { + move_to_spawnpoint(m_force_spawnpoint, false); + m_force_spawnpoint.clear(); + m_main_is_default = true; + } + + /** Perform scripting related actions. **/ + // Run default.nut just before init script + try + { + IFileStream in(m_parent.get_levels_path() + "default.nut"); + m_squirrel_environment->run_script(in, "WorldMapSector::default.nut"); + } + catch (...) + { + // doesn't exist or erroneous; do nothing + } + + if (!m_init_script.empty()) + m_squirrel_environment->run_script(m_init_script, "WorldMapSector::init"); +} + +void +WorldMapSector::leave() +{ + BIND_WORLDMAP_SECTOR(*this); + + // remove worldmap_table from roottable + m_squirrel_environment->unexpose_self(); +} + + +void +WorldMapSector::draw(DrawingContext& context) +{ + BIND_WORLDMAP_SECTOR(*this); + + if (get_width() < static_cast(context.get_width()) || + get_height() < static_cast(context.get_height())) + { + context.color().draw_filled_rect(context.get_rect(), + Color(0.0f, 0.0f, 0.0f, 1.0f), LAYER_BACKGROUND0); + } + + context.push_transform(); + context.set_translation(m_camera->get_offset()); + + GameObjectManager::draw(context); + + if (g_debug.show_worldmap_path) + { + for (int x = 0; x < static_cast(get_tiles_width()); x++) { + for (int y = 0; y < static_cast(get_tiles_height()); y++) { + const int data = tile_data_at(Vector(static_cast(x), static_cast(y))); + const int px = x * 32; + const int py = y * 32; + const int W = 4; + const int layer = LAYER_FOREGROUND1 + 1000; + const Color color(1.0f, 0.0f, 1.0f, 0.5f); + if (data & Tile::WORLDMAP_NORTH) context.color().draw_filled_rect(Rect(px + 16-W, py , px + 16+W, py + 16-W), color, layer); + if (data & Tile::WORLDMAP_SOUTH) context.color().draw_filled_rect(Rect(px + 16-W, py + 16+W, px + 16+W, py + 32 ), color, layer); + if (data & Tile::WORLDMAP_EAST) context.color().draw_filled_rect(Rect(px + 16+W, py + 16-W, px + 32 , py + 16+W), color, layer); + if (data & Tile::WORLDMAP_WEST) context.color().draw_filled_rect(Rect(px , py + 16-W, px + 16-W, py + 16+W), color, layer); + if (data & Tile::WORLDMAP_DIR_MASK) context.color().draw_filled_rect(Rect(px + 16-W, py + 16-W, px + 16+W, py + 16+W), color, layer); + if (data & Tile::WORLDMAP_STOP) context.color().draw_filled_rect(Rect(px + 4 , py + 4 , px + 28 , py + 28 ), color, layer); + } + } + } + + draw_status(context); +} + +void +WorldMapSector::draw_status(DrawingContext& context) +{ + context.push_transform(); + context.set_translation(Vector(0, 0)); + + if (!m_tux->is_moving()) { + LevelTile* level = at_object(); + if (level) + { + context.color().draw_text(Resources::normal_font, level->get_title(), + Vector(static_cast(context.get_width()) / 2.0f, + static_cast(context.get_height()) - Resources::normal_font->get_height() - 10), + ALIGN_CENTER, LAYER_HUD, level->get_title_color()); + + if (g_config->developer_mode) { + context.color().draw_text(Resources::small_font, FileSystem::join(level->get_basedir(), level->get_level_filename()), + Vector(static_cast(context.get_width()) / 2.0f, + static_cast(context.get_height()) - Resources::normal_font->get_height() - 25), + ALIGN_CENTER, LAYER_HUD, level->get_title_color()); + } + + // if level is solved, draw level picture behind stats + /* + if (level.solved) { + if (const Surface* picture = level->get_picture()) { + Vector pos = Vector(context.get_width() - picture->get_width(), context.get_height() - picture->get_height()); + context.push_transform(); + context.set_alpha(0.5); + context.color().draw_surface(picture, pos, LAYER_FOREGROUND1-1); + context.pop_transform(); + } + } + */ + level->get_statistics().draw_worldmap_info(context, level->get_target_time()); + } + + SpecialTile* special_tile = at_object(); + if (special_tile) + { + /* Display an in-map message in the map, if any as been selected */ + if (!special_tile->get_map_message().empty() && !special_tile->is_passive_message()) + context.color().draw_text(Resources::normal_font, special_tile->get_map_message(), + Vector(static_cast(context.get_width()) / 2.0f, + static_cast(context.get_height()) - static_cast(Resources::normal_font->get_height()) - 60.0f), + ALIGN_CENTER, LAYER_FOREGROUND1, WorldMap::s_message_color); + } + + // display teleporter messages + Teleporter* teleporter = at_object(); + if (teleporter && (!teleporter->get_message().empty())) + { + Vector pos = Vector(static_cast(context.get_width()) / 2.0f, + static_cast(context.get_height()) - Resources::normal_font->get_height() - 30.0f); + context.color().draw_text(Resources::normal_font, teleporter->get_message(), pos, ALIGN_CENTER, LAYER_FOREGROUND1, WorldMap::s_teleporter_message_color); + } + } + + /* Display a passive message on the map, if set */ + if (m_parent.m_passive_message_timer.started()) + context.color().draw_text(Resources::normal_font, m_parent.m_passive_message, + Vector(static_cast(context.get_width()) / 2.0f, + static_cast(context.get_height()) - Resources::normal_font->get_height() - 60.0f), + ALIGN_CENTER, LAYER_FOREGROUND1, WorldMap::s_message_color); + + context.pop_transform(); +} + +void +WorldMapSector::update(float dt_sec) +{ + BIND_WORLDMAP_SECTOR(*this); + + GameObjectManager::update(dt_sec); + + m_camera->update(dt_sec); + + { + // check for teleporters + auto teleporter = at_object(); + if (teleporter && (teleporter->is_automatic() || (m_parent.m_enter_level && (!m_tux->is_moving())))) { + m_parent.m_enter_level = false; + if (!teleporter->get_worldmap().empty()) + { + // Change worldmap. + m_parent.change(teleporter->get_worldmap(), teleporter->get_spawnpoint()); + } + else + { + // TODO: an animation, camera scrolling or a fading would be a nice touch + SoundManager::current()->play("sounds/warp.wav"); + m_tux->m_back_direction = Direction::NONE; + if (!teleporter->get_sector().empty()) + { + // A target sector is set, so teleport to it at the specified spawnpoint. + m_parent.set_sector(teleporter->get_sector(), teleporter->get_spawnpoint()); + } + else + { + // No target sector is set, so teleport at the specified spawnpoint in the current one. + move_to_spawnpoint(teleporter->get_spawnpoint(), true); + } + } + } + } + + { + // check for auto-play levels + auto level = at_object(); + if (level && level->is_auto_play() && !level->is_solved() && !m_tux->is_moving()) { + m_parent.m_enter_level = true; + // automatically mark these levels as solved in case player aborts + level->set_solved(true); + } + } + + { + if (m_parent.m_enter_level && !m_tux->is_moving()) + { + /* Check level action */ + auto level_ = at_object(); + if (!level_) { + //Respawn if player on a tile with no level and nowhere to go. + int tile_data = tile_data_at(m_tux->get_tile_pos()); + if (!( tile_data & ( Tile::WORLDMAP_NORTH | Tile::WORLDMAP_SOUTH | Tile::WORLDMAP_WEST | Tile::WORLDMAP_EAST ))){ + log_warning << "Player at illegal position " << m_tux->get_tile_pos().x << ", " << m_tux->get_tile_pos().y << " respawning." << std::endl; + move_to_spawnpoint("main"); + return; + } + log_warning << "No level to enter at: " << m_tux->get_tile_pos().x << ", " << m_tux->get_tile_pos().y << std::endl; + return; + } + + if (level_->get_tile_pos() == m_tux->get_tile_pos()) { + try { + Vector shrinkpos = Vector(level_->get_pos().x + 16 - m_camera->get_offset().x, + level_->get_pos().y + 8 - m_camera->get_offset().y); + std::string levelfile = m_parent.m_levels_path + level_->get_level_filename(); + + // update state and savegame + m_parent.save_state(); + ScreenManager::current()->push_screen(std::make_unique(levelfile, m_parent.m_savegame, &level_->get_statistics()), + std::make_unique(shrinkpos, 1.0f)); + + m_parent.m_in_level = true; + } catch(std::exception& e) { + log_fatal << "Couldn't load level: " << e.what() << std::endl; + } + } + } + else + { + // tux->set_direction(input_direction); + } + } +} + + +Vector +WorldMapSector::get_next_tile(const Vector& pos, const Direction& direction) const +{ + auto position = pos; + switch (direction) { + case Direction::WEST: + position.x -= 1; + break; + case Direction::EAST: + position.x += 1; + break; + case Direction::NORTH: + position.y -= 1; + break; + case Direction::SOUTH: + position.y += 1; + break; + case Direction::NONE: + break; + } + return position; +} + +int +WorldMapSector::available_directions_at(const Vector& p) const +{ + return tile_data_at(p) & Tile::WORLDMAP_DIR_MASK; +} + +int +WorldMapSector::tile_data_at(const Vector& p) const +{ + int dirs = 0; + + for (const auto& tilemap : get_solid_tilemaps()) { + const Tile& tile = tilemap->get_tile(static_cast(p.x), static_cast(p.y)); + int dirdata = tile.get_data(); + dirs |= dirdata; + } + + return dirs; +} + + +size_t +WorldMapSector::level_count() const +{ + return get_object_count(); +} + +size_t +WorldMapSector::solved_level_count() const +{ + size_t count = 0; + for (auto& level : get_objects_by_type()) { + if (level.is_solved()) { + count++; + } + } + + return count; +} + + +void +WorldMapSector::finished_level(Level* gamelevel) +{ + // TODO use Level* parameter here? + auto level = at_object(); + + if (level == nullptr) { + return; + } + + bool old_level_state = level->is_solved(); + level->set_solved(true); + + // deal with statistics + level->get_statistics().update(gamelevel->m_stats); + + if (level->get_statistics().completed(level->get_statistics(), level->get_target_time())) { + level->set_perfect(true); + } + + m_parent.save_state(); + + if (old_level_state != level->is_solved()) { + // Try to detect the next direction to which we should walk + // FIXME: Mostly a hack + Direction dir = Direction::NONE; + + int dirdata = available_directions_at(m_tux->get_tile_pos()); + // first, test for crossroads + if (dirdata == Tile::WORLDMAP_CNSE || + dirdata == Tile::WORLDMAP_CNSW || + dirdata == Tile::WORLDMAP_CNEW || + dirdata == Tile::WORLDMAP_CSEW || + dirdata == Tile::WORLDMAP_CNSEW) + dir = Direction::NONE; + else if (dirdata & Tile::WORLDMAP_NORTH + && m_tux->m_back_direction != Direction::NORTH) + dir = Direction::NORTH; + else if (dirdata & Tile::WORLDMAP_SOUTH + && m_tux->m_back_direction != Direction::SOUTH) + dir = Direction::SOUTH; + else if (dirdata & Tile::WORLDMAP_EAST + && m_tux->m_back_direction != Direction::EAST) + dir = Direction::EAST; + else if (dirdata & Tile::WORLDMAP_WEST + && m_tux->m_back_direction != Direction::WEST) + dir = Direction::WEST; + + if (dir != Direction::NONE) { + m_tux->set_direction(dir); + } + } + + if (!level->get_extro_script().empty()) { + try { + run_script(level->get_extro_script(), "WorldMapSector:extro_script"); + } catch(std::exception& e) { + log_warning << "Couldn't run level-extro-script: " << e.what() << std::endl; + } + } +} + +SpawnPoint* +WorldMapSector::get_spawnpoint_by_name(const std::string& spawnpoint_name) const +{ + for (const auto& sp : m_spawnpoints) + { + if (sp->get_name() == spawnpoint_name) + return sp.get(); + } + return nullptr; +} + +bool +WorldMapSector::path_ok(const Direction& direction, const Vector& old_pos, Vector* new_pos) const +{ + *new_pos = get_next_tile(old_pos, direction); + + if (!(new_pos->x >= 0 && new_pos->x < get_tiles_width() + && new_pos->y >= 0 && new_pos->y < get_tiles_height())) + { // New position is outsite the tilemap + return false; + } + else + { // Check if the tile allows us to go to new_pos + int old_tile_data = tile_data_at(old_pos); + int new_tile_data = tile_data_at(*new_pos); + switch (direction) + { + case Direction::WEST: + return (old_tile_data & Tile::WORLDMAP_WEST + && new_tile_data & Tile::WORLDMAP_EAST); + + case Direction::EAST: + return (old_tile_data & Tile::WORLDMAP_EAST + && new_tile_data & Tile::WORLDMAP_WEST); + + case Direction::NORTH: + return (old_tile_data & Tile::WORLDMAP_NORTH + && new_tile_data & Tile::WORLDMAP_SOUTH); + + case Direction::SOUTH: + return (old_tile_data & Tile::WORLDMAP_SOUTH + && new_tile_data & Tile::WORLDMAP_NORTH); + + case Direction::NONE: + log_warning << "path_ok() can't walk if direction is NONE" << std::endl; + assert(false); + } + return false; + } +} + +void +WorldMapSector::move_to_spawnpoint(const std::string& spawnpoint, bool pan) +{ + auto sp = get_spawnpoint_by_name(spawnpoint); + if (sp != nullptr) { + Vector p = sp->get_pos(); + m_tux->set_tile_pos(p); + m_tux->set_direction(sp->get_auto_dir()); + if (pan) { + m_camera->pan(); + } + return; + } + + log_warning << "Spawnpoint '" << spawnpoint << "' not found." << std::endl; + if (spawnpoint != "main" && m_main_is_default) { + move_to_spawnpoint("main"); + } +} + +void +WorldMapSector::set_initial_fade_tilemap(const std::string& tilemap_name, int direction) +{ + m_initial_fade_tilemap = tilemap_name; + m_fade_direction = direction; +} + +void +WorldMapSector::set_initial_spawnpoint(const std::string& spawnpoint_name) +{ + m_force_spawnpoint = spawnpoint_name; + + // If spawnpoint we specified can not be found, + // don't bother moving to the main spawnpoint. + m_main_is_default = false; +} + + +bool +WorldMapSector::before_object_add(GameObject& object) +{ + m_squirrel_environment->try_expose(object); + return true; +} + +void +WorldMapSector::before_object_remove(GameObject& object) +{ + m_squirrel_environment->try_unexpose(object); +} + + +TileSet* +WorldMapSector::get_tileset() const +{ + return m_parent.m_tileset; +} + +Vector +WorldMapSector::get_tux_pos() const +{ + return m_tux->get_pos(); +} + +} // namespace worldmap + +/* EOF */ diff --git a/src/worldmap/worldmap_sector.hpp b/src/worldmap/worldmap_sector.hpp new file mode 100644 index 00000000000..9627b5e4752 --- /dev/null +++ b/src/worldmap/worldmap_sector.hpp @@ -0,0 +1,147 @@ +// SuperTux +// Copyright (C) 2004 Ingo Ruhnke +// Copyright (C) 2006 Christoph Sommer +// 2023 Vankata453 +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#ifndef HEADER_SUPERTUX_WORLDMAP_WORLDMAP_SECTOR_HPP +#define HEADER_SUPERTUX_WORLDMAP_WORLDMAP_SECTOR_HPP + +#include "supertux/sector_base.hpp" + +#include "worldmap/tux.hpp" + +namespace worldmap { + +class Camera; +class SpawnPoint; +class WorldMap; + +/** Represents one of (potentially) multiple, separate parts of a WorldMap. + WorldMap variant of Sector, utilizing only its base features. */ +class WorldMapSector final : public Base::Sector +{ + friend class WorldMapSectorParser; + friend class WorldMapState; + +public: + static WorldMapSector* current(); + +public: + WorldMapSector(WorldMap& parent); + ~WorldMapSector() override; + + void finish_construction(bool) override; + + void setup(); + void leave(); + + void draw(DrawingContext& context) override; + void update(float dt_sec) override; + + Vector get_next_tile(const Vector& pos, const Direction& direction) const; + + /** gets a bitfield of Tile::WORLDMAP_NORTH | Tile::WORLDMAP_WEST | + ... values, which indicates the directions Tux can move to when + at the given position. */ + int available_directions_at(const Vector& pos) const; + + /** returns a bitfield representing the union of all + Tile::WORLDMAP_XXX values of all solid tiles at the given + position */ + int tile_data_at(const Vector& pos) const; + + size_t level_count() const; + size_t solved_level_count() const; + + /** gets called from the GameSession when a level has been successfully + finished */ + void finished_level(Level* level); + + /** Get a spawnpoint by its name @param name The name of the + spawnpoint @return spawnpoint corresponding to that name */ + SpawnPoint* get_spawnpoint_by_name(const std::string& spawnpoint_name) const; + + template + T* at_object() const + { + for (auto& obj : get_objects_by_type()) + if (obj.get_tile_pos() == m_tux->get_tile_pos()) + return &obj; + + return nullptr; + } + template + T* at_object(const Vector& pos) const + { + for (auto& obj : get_objects_by_type()) + if (obj.get_tile_pos() == pos) + return &obj; + + return nullptr; + } + + /** Check if it is possible to walk from \a pos into \a direction, + if possible, write the new position to \a new_pos */ + bool path_ok(const Direction& direction, const Vector& old_pos, Vector* new_pos) const; + + /** Moves Tux to the given spawnpoint + @param spawnpoint Name of the spawnpoint to move to + @param pan Pan the camera during to new spawnpoint */ + void move_to_spawnpoint(const std::string& spawnpoint, bool pan = false); + + /** Sets the name of the tilemap that should fade when worldmap is set up. */ + void set_initial_fade_tilemap(const std::string& tilemap_name, int direction); + + /** Sets the initial spawnpoint on worldmap setup */ + void set_initial_spawnpoint(const std::string& spawnpoint_name); + + bool in_worldmap() const override { return true; } + + TileSet* get_tileset() const override; + WorldMap& get_worldmap() const { return m_parent; } + Camera& get_camera() const { return *m_camera; } + Tux& get_tux() const { return *m_tux; } + Vector get_tux_pos() const; + +protected: + void draw_status(DrawingContext& context); + + bool before_object_add(GameObject& object) override; + void before_object_remove(GameObject& object) override; + +private: + WorldMap& m_parent; + + std::unique_ptr m_camera; + Tux* m_tux; + + std::vector > m_spawnpoints; + std::string m_force_spawnpoint; + + bool m_main_is_default; + std::string m_initial_fade_tilemap; + int m_fade_direction; + +private: + WorldMapSector(const WorldMapSector&) = delete; + WorldMapSector& operator=(const WorldMapSector&) = delete; +}; + +} // namespace worldmap + +#endif + +/* EOF */ diff --git a/src/worldmap/worldmap_sector_parser.cpp b/src/worldmap/worldmap_sector_parser.cpp new file mode 100644 index 00000000000..1c4f92673b3 --- /dev/null +++ b/src/worldmap/worldmap_sector_parser.cpp @@ -0,0 +1,69 @@ +// SuperTux +// Copyright (C) 2023 Vankata453 +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include "worldmap/worldmap_sector_parser.hpp" + +#include "object/tilemap.hpp" +#include "supertux/d_scope.hpp" +#include "supertux/game_object_factory.hpp" +#include "worldmap/spawn_point.hpp" +#include "worldmap/worldmap_sector.hpp" + +namespace worldmap { + +std::unique_ptr +WorldMapSectorParser::from_reader(WorldMap& worldmap, const ReaderMapping& reader) +{ + auto sector = std::make_unique(worldmap); + BIND_WORLDMAP_SECTOR(*sector); + WorldMapSectorParser parser(*sector); + parser.parse(reader); + return sector; +} + + +WorldMapSectorParser::WorldMapSectorParser(WorldMapSector& sector) : + SectorParser(sector, false) +{ +} + +WorldMapSector& +WorldMapSectorParser::get_sector() const +{ + return static_cast(m_sector); +} + +bool +WorldMapSectorParser::parse_object_additional(const std::string& name, const ReaderMapping& reader) +{ + if (name == "worldmap-spawnpoint") // Custom rule for adding spawnpoints + { + get_sector().m_spawnpoints.push_back(std::make_unique(reader)); + return true; + } + else if (name == "tilemap") // Custom rule for adding tilemaps on worldmaps + { + get_sector().add(get_sector().get_tileset(), reader); + return true; + } + + // Proceed adding the object only if it's flagged as allowed for worldmaps + return !GameObjectFactory::instance().has_params(name, ObjectFactory::RegisteredObjectParam::OBJ_PARAM_WORLDMAP); +} + +} // namespace worldmap + +/* EOF */ diff --git a/src/worldmap/worldmap_parser.hpp b/src/worldmap/worldmap_sector_parser.hpp similarity index 53% rename from src/worldmap/worldmap_parser.hpp rename to src/worldmap/worldmap_sector_parser.hpp index 758f492632a..4460faf0bfd 100644 --- a/src/worldmap/worldmap_parser.hpp +++ b/src/worldmap/worldmap_sector_parser.hpp @@ -1,6 +1,5 @@ // SuperTux -// Copyright (C) 2004-2018 Ingo Ruhnke -// 2006 Christoph Sommer +// Copyright (C) 2023 Vankata453 // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -15,30 +14,32 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -#ifndef HEADER_SUPERTUX_WORLDMAP_WORLDMAP_PARSER_HPP -#define HEADER_SUPERTUX_WORLDMAP_WORLDMAP_PARSER_HPP +#ifndef HEADER_SUPERTUX_WORLDMAP_WORLDMAP_SECTOR_PARSER_HPP +#define HEADER_SUPERTUX_WORLDMAP_WORLDMAP_SECTOR_PARSER_HPP -#include +#include "supertux/sector_parser.hpp" namespace worldmap { -class LevelTile; class WorldMap; +class WorldMapSector; -class WorldMapParser +class WorldMapSectorParser final : public SectorParser { public: - WorldMapParser(WorldMap& worldmap); + static std::unique_ptr from_reader(WorldMap& worldmap, const ReaderMapping& sector); - void load_worldmap(const std::string& filename); - void load_level_information(LevelTile& level); +private: + WorldMapSectorParser(WorldMapSector& sector); private: - WorldMap& m_worldmap; + WorldMapSector& get_sector() const; + + bool parse_object_additional(const std::string& name, const ReaderMapping& reader) override; private: - WorldMapParser(const WorldMapParser&) = delete; - WorldMapParser& operator=(const WorldMapParser&) = delete; + WorldMapSectorParser(const WorldMapSectorParser&) = delete; + WorldMapSectorParser& operator=(const WorldMapSectorParser&) = delete; }; } // namespace worldmap diff --git a/src/worldmap/worldmap_state.cpp b/src/worldmap/worldmap_state.cpp index f54ee502704..7490308450c 100644 --- a/src/worldmap/worldmap_state.cpp +++ b/src/worldmap/worldmap_state.cpp @@ -1,6 +1,7 @@ // SuperTux // Copyright (C) 2004-2018 Ingo Ruhnke // 2006 Christoph Sommer +// 2023 Vankata453 // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -23,6 +24,7 @@ #include "supertux/savegame.hpp" #include "supertux/tile.hpp" #include "util/log.hpp" +#include "worldmap/direction.hpp" #include "worldmap/level_tile.hpp" #include "worldmap/sprite_change.hpp" #include "worldmap/tux.hpp" @@ -31,260 +33,368 @@ namespace worldmap { WorldMapState::WorldMapState(WorldMap& worldmap) : - m_worldmap(worldmap) + m_worldmap(worldmap), + m_position_was_reset(false) { } + void WorldMapState::load_state() { log_debug << "loading worldmap state" << std::endl; - SquirrelVM& vm = SquirrelVirtualMachine::current()->get_vm(); + WORLDMAP_STATE_SQUIRREL_VM_GUARD; SQInteger oldtop = sq_gettop(vm.get_vm()); try { - // get state table + /** Get state table. **/ sq_pushroottable(vm.get_vm()); vm.get_table_entry("state"); + + /** Get state table for all worldmaps. **/ vm.get_table_entry("worlds"); - // if a non-canonical entry is present, replace them with a canonical one - if (m_worldmap.m_map_filename != "/levels/world2/worldmap.stwm") { - std::string old_map_filename = m_worldmap.m_map_filename.substr(1); - if (vm.has_property(old_map_filename.c_str())) { - vm.rename_table_entry(old_map_filename.c_str(), m_worldmap.m_map_filename.c_str()); - } + // If a non-canonical entry is present, replace it with a canonical one. + const std::string old_map_filename = m_worldmap.m_map_filename.substr(1); + if (vm.has_property(old_map_filename.c_str())) + { + vm.rename_table_entry(old_map_filename.c_str(), m_worldmap.m_map_filename.c_str()); } + /** Get state table for the current worldmap. **/ vm.get_table_entry(m_worldmap.m_map_filename); - // load tux - vm.get_table_entry("tux"); + // Load the current sector. + if (vm.has_property("sector")) // Load the current sector, only if a "sector" property exists. + { + const std::string sector_name = vm.read_string("sector"); + if (!m_worldmap.m_sector) // If the worldmap doesn't have a current sector, try setting the new sector. + m_worldmap.set_sector(sector_name, "", false); + + WORLDMAP_STATE_SECTOR_GUARD; - Vector p(0.0f, 0.0f); - bool position_was_reset = false; - if (!vm.get_float("x", p.x) || !vm.get_float("y", p.y)) + /** Get state table for the current sector. **/ + vm.get_table_entry(sector.get_name().c_str()); + } + else // Sector property does not exist, which may indicate outdated save file. { - log_warning << "Player position not set, respawning." << std::endl; - m_worldmap.move_to_spawnpoint("main"); - position_was_reset = true; + if (!m_worldmap.m_sector) // If the worldmap doesn't have a current sector, try setting the main one. + m_worldmap.set_sector("main", "", false); } - std::string back_str = vm.read_string("back"); - m_worldmap.m_tux->m_back_direction = string_to_direction(back_str); - m_worldmap.m_tux->set_tile_pos(p); - - int tile_data = m_worldmap.tile_data_at(p); - if (!( tile_data & ( Tile::WORLDMAP_NORTH | Tile::WORLDMAP_SOUTH | Tile::WORLDMAP_WEST | Tile::WORLDMAP_EAST ))) { - log_warning << "Player at illegal position " << p.x << ", " << p.y << " respawning." << std::endl; - m_worldmap.move_to_spawnpoint("main"); - position_was_reset = true; + if (!m_worldmap.m_sector) + { + // Quit loading worldmap state, if there is still no current sector loaded. + throw std::runtime_error("No sector set."); } - sq_pop(vm.get_vm(), 1); + /** Load objects. **/ + load_tux(); + load_levels(); + load_tilemap_visibility(); + load_sprite_change_objects(); + } + catch (std::exception& err) + { + log_warning << "Not loading worldmap state: " << err.what() << std::endl; - // load levels - vm.get_table_entry("levels"); - for (auto& level : m_worldmap.get_objects_by_type()) { - sq_pushstring(vm.get_vm(), level.get_level_filename().c_str(), -1); - if (SQ_SUCCEEDED(sq_get(vm.get_vm(), -2))) - { - bool solved = false; - vm.get_bool("solved", solved); - level.set_solved(solved); + // Set default properties. + if (!m_worldmap.m_sector) + m_worldmap.set_sector("main", "", false); // If no current sector is present, set it to "main", or the default one. + m_worldmap.get_sector().move_to_spawnpoint("main"); // Move Tux to the "main" spawnpoint. - bool perfect = false; - vm.get_bool("perfect", perfect); - level.set_perfect(perfect); + // Create a new initial save. + save_state(); + } + sq_settop(vm.get_vm(), oldtop); - level.update_sprite_action(); - level.get_statistics().unserialize_from_squirrel(vm); - sq_pop(vm.get_vm(), 1); - } + m_worldmap.m_in_level = false; +} + + +/** Load Tux **/ +void +WorldMapState::load_tux() +{ + WORLDMAP_STATE_SQUIRREL_VM_GUARD; + WORLDMAP_STATE_SECTOR_GUARD; + + vm.get_table_entry("tux"); + Vector p(0.0f, 0.0f); + if (!vm.get_float("x", p.x) || !vm.get_float("y", p.y)) + { + log_warning << "Player position not set, respawning." << std::endl; + sector.move_to_spawnpoint("main"); + m_position_was_reset = true; + } + std::string back_str = vm.read_string("back"); + sector.m_tux->m_back_direction = string_to_direction(back_str); + sector.m_tux->set_tile_pos(p); + + int tile_data = sector.tile_data_at(p); + if (!( tile_data & ( Tile::WORLDMAP_NORTH | Tile::WORLDMAP_SOUTH | Tile::WORLDMAP_WEST | Tile::WORLDMAP_EAST ))) { + log_warning << "Player at illegal position " << p.x << ", " << p.y << " respawning." << std::endl; + sector.move_to_spawnpoint("main"); + m_position_was_reset = true; + } + sq_pop(vm.get_vm(), 1); +} + +/** Load levels **/ +void +WorldMapState::load_levels() +{ + WORLDMAP_STATE_SQUIRREL_VM_GUARD; + WORLDMAP_STATE_SECTOR_GUARD; + + vm.get_table_entry("levels"); + for (auto& level : sector.get_objects_by_type()) { + sq_pushstring(vm.get_vm(), level.get_level_filename().c_str(), -1); + if (SQ_SUCCEEDED(sq_get(vm.get_vm(), -2))) + { + bool solved = false; + vm.get_bool("solved", solved); + level.set_solved(solved); + + bool perfect = false; + vm.get_bool("perfect", perfect); + level.set_perfect(perfect); + + level.update_sprite_action(); + level.get_statistics().unserialize_from_squirrel(vm); + sq_pop(vm.get_vm(), 1); } + } + sq_pop(vm.get_vm(), 1); +} - // leave levels table - sq_pop(vm.get_vm(), 1); +/** Load tilemap visibility **/ +void +WorldMapState::load_tilemap_visibility() +{ + WORLDMAP_STATE_SQUIRREL_VM_GUARD; + WORLDMAP_STATE_SECTOR_GUARD; - try { - if(!position_was_reset) + try + { + if (!m_position_was_reset) + { + vm.get_table_entry("tilemaps"); + sq_pushnull(vm.get_vm()); // Null-iterator + while (SQ_SUCCEEDED(sq_next(vm.get_vm(), -2))) { - vm.get_table_entry("tilemaps"); - sq_pushnull(vm.get_vm()); // Null-iterator - while (SQ_SUCCEEDED(sq_next(vm.get_vm(), -2))) + const char* key; // Name of specific tilemap table + if (SQ_SUCCEEDED(sq_getstring(vm.get_vm(), -2, &key))) { - const char* key; // Name of specific tilemap table - if (SQ_SUCCEEDED(sq_getstring(vm.get_vm(), -2, &key))) + auto tilemap = sector.get_object_by_name(key); + if (tilemap != nullptr) { - auto tilemap = m_worldmap.get_object_by_name(key); - if (tilemap != nullptr) + sq_pushnull(vm.get_vm()); // null iterator (inner); + while (SQ_SUCCEEDED(sq_next(vm.get_vm(), -2))) { - sq_pushnull(vm.get_vm()); // null iterator (inner); - while (SQ_SUCCEEDED(sq_next(vm.get_vm(), -2))) + const char* property_key; + if (SQ_SUCCEEDED(sq_getstring(vm.get_vm(), -2, &property_key))) { - const char* property_key; - if (SQ_SUCCEEDED(sq_getstring(vm.get_vm(), -2, &property_key))) + auto propKey = std::string(property_key); + if (propKey == "alpha") { - auto propKey = std::string(property_key); - if (propKey == "alpha") + float alpha_value = 1.0; + if (SQ_SUCCEEDED(sq_getfloat(vm.get_vm(), -1, &alpha_value))) { - float alpha_value = 1.0; - if (SQ_SUCCEEDED(sq_getfloat(vm.get_vm(), -1, &alpha_value))) - { - tilemap->set_alpha(alpha_value); - } + tilemap->set_alpha(alpha_value); } } - sq_pop(vm.get_vm(), 2); // Pop key/value from the stack } - sq_pop(vm.get_vm(), 1); // Pop null iterator + sq_pop(vm.get_vm(), 2); // Pop key/value from the stack } + sq_pop(vm.get_vm(), 1); // Pop null iterator } - sq_pop(vm.get_vm(), 2); // Pop key value pair from stack } - sq_pop(vm.get_vm(), 1); // Pop null - sq_pop(vm.get_vm(), 1); // leave tilemaps table + sq_pop(vm.get_vm(), 2); // Pop key value pair from stack } + sq_pop(vm.get_vm(), 1); // Pop null + sq_pop(vm.get_vm(), 1); // leave tilemaps table } - catch(const SquirrelError&) - { - // Failed to get tilemap entry. This could indicate - // that no savable tilemaps have been found. In any - // case: This is not severe at all. - } + } + catch(const SquirrelError&) + { + // Failed to get tilemap entry. This could indicate + // that no savable tilemaps have been found. In any + // case: This is not severe at all. + } +} + +/** Load sprite change objects **/ +void +WorldMapState::load_sprite_change_objects() +{ + WORLDMAP_STATE_SQUIRREL_VM_GUARD; + WORLDMAP_STATE_SECTOR_GUARD; - if (m_worldmap.get_object_count() > 0) + if (sector.get_object_count() > 0) + { + vm.get_table_entry("sprite-changes"); + for (auto& sc : sector.get_objects_by_type()) { - // load sprite change action: - vm.get_table_entry("sprite-changes"); - for (auto& sc : m_worldmap.get_objects_by_type()) - { - auto key = std::to_string(int(sc.get_pos().x)) + "_" + - std::to_string(int(sc.get_pos().y)); - sq_pushstring(vm.get_vm(), key.c_str(), -1); - if (SQ_SUCCEEDED(sq_get(vm.get_vm(), -2))) { - bool show_stay_action = false; - if (!vm.get_bool("show-stay-action", show_stay_action)) + auto key = std::to_string(int(sc.get_pos().x)) + "_" + + std::to_string(int(sc.get_pos().y)); + sq_pushstring(vm.get_vm(), key.c_str(), -1); + if (SQ_SUCCEEDED(sq_get(vm.get_vm(), -2))) { + bool show_stay_action = false; + if (!vm.get_bool("show-stay-action", show_stay_action)) + { + sc.clear_stay_action(/* propagate = */ false); + } + else + { + if (show_stay_action) { - sc.clear_stay_action(/* propagate = */ false); + sc.set_stay_action(); } else { - if (show_stay_action) - { - sc.set_stay_action(); - } - else - { - sc.clear_stay_action(/* propagate = */ false); - } + sc.clear_stay_action(/* propagate = */ false); } - sq_pop(vm.get_vm(), 1); } + sq_pop(vm.get_vm(), 1); } - - // Leave sprite changes table - sq_pop(vm.get_vm(), 1); } - - } catch(std::exception& e) { - log_debug << "Not loading worldmap state: " << e.what() << std::endl; - save_state(); // make new initial save - m_worldmap.move_to_spawnpoint("main"); // set tux to main spawnpoint + sq_pop(vm.get_vm(), 1); // Leave sprite change objects table. } - sq_settop(vm.get_vm(), oldtop); - - m_worldmap.m_in_level = false; } + void WorldMapState::save_state() const { + WorldMapSector& sector = m_worldmap.get_sector(); + SquirrelVM& vm = SquirrelVirtualMachine::current()->get_vm(); SQInteger oldtop = sq_gettop(vm.get_vm()); try { - // get state table + /** Get state table. **/ sq_pushroottable(vm.get_vm()); vm.get_table_entry("state"); + + /** Get or create state table for all worldmaps. **/ vm.get_or_create_table_entry("worlds"); - vm.delete_table_entry(m_worldmap.m_map_filename.c_str()); + /** Get or create state table for the current worldmap. **/ + vm.get_or_create_table_entry(m_worldmap.m_map_filename.c_str()); - // construct new table for this worldmap - vm.begin_table(m_worldmap.m_map_filename.c_str()); + // Save the current sector. + vm.store_string("sector", sector.get_name()); - // store tux - vm.begin_table("tux"); + /** Delete the table entry for the current sector and construct a new one. **/ + vm.delete_table_entry(sector.get_name().c_str()); + vm.begin_table(sector.get_name().c_str()); - vm.store_float("x", m_worldmap.m_tux->get_tile_pos().x); - vm.store_float("y", m_worldmap.m_tux->get_tile_pos().y); - vm.store_string("back", direction_to_string(m_worldmap.m_tux->m_back_direction)); + /** Save objects. **/ + save_tux(); + save_levels(); + save_tilemap_visibility(); + save_sprite_change_objects(); - vm.end_table("tux"); + /** Push the current sector into the current worldmap table. **/ + vm.end_table(sector.get_name().c_str()); + } + catch (std::exception& err) + { + log_warning << "Failed to save worldmap state: " << err.what() << std::endl; - // sprite change objects: - if (m_worldmap.get_object_count() > 0) - { - vm.begin_table("sprite-changes"); + sq_settop(vm.get_vm(), oldtop); + } - for (const auto& sc : m_worldmap.get_objects_by_type()) - { - auto key = std::to_string(int(sc.get_pos().x)) + "_" + - std::to_string(int(sc.get_pos().y)); - vm.begin_table(key.c_str()); - vm.store_bool("show-stay-action", sc.show_stay_action()); - vm.end_table(key.c_str()); - } + sq_settop(vm.get_vm(), oldtop); - vm.end_table("sprite-changes"); - } + m_worldmap.m_savegame.save(); +} - // tilemap visibility - sq_pushstring(vm.get_vm(), "tilemaps", -1); - sq_newtable(vm.get_vm()); - for (auto& tilemap : m_worldmap.get_objects_by_type<::TileMap>()) + +/** Save Tux **/ +void +WorldMapState::save_tux() const +{ + WORLDMAP_STATE_SQUIRREL_VM_GUARD; + WORLDMAP_STATE_SECTOR_GUARD; + + vm.begin_table("tux"); + vm.store_float("x", sector.m_tux->get_tile_pos().x); + vm.store_float("y", sector.m_tux->get_tile_pos().y); + vm.store_string("back", direction_to_string(sector.m_tux->m_back_direction)); + vm.end_table("tux"); +} + +/** Save levels **/ +void +WorldMapState::save_levels() const +{ + WORLDMAP_STATE_SQUIRREL_VM_GUARD; + WORLDMAP_STATE_SECTOR_GUARD; + + vm.begin_table("levels"); + for (const auto& level : sector.get_objects_by_type()) + { + vm.begin_table(level.get_level_filename().c_str()); + + vm.store_bool("solved", level.is_solved()); + vm.store_bool("perfect", level.is_perfect()); + + level.get_statistics().serialize_to_squirrel(vm); + vm.end_table(level.get_level_filename().c_str()); + } + vm.end_table("levels"); +} + +/** Save tilemap visibility **/ +void +WorldMapState::save_tilemap_visibility() const +{ + WORLDMAP_STATE_SQUIRREL_VM_GUARD; + WORLDMAP_STATE_SECTOR_GUARD; + + sq_pushstring(vm.get_vm(), "tilemaps", -1); + sq_newtable(vm.get_vm()); + for (auto& tilemap : sector.get_objects_by_type<::TileMap>()) + { + if (!tilemap.get_name().empty()) { - if (!tilemap.get_name().empty()) + sq_pushstring(vm.get_vm(), tilemap.get_name().c_str(), -1); + sq_newtable(vm.get_vm()); + vm.store_float("alpha", tilemap.get_alpha()); + if (SQ_FAILED(sq_createslot(vm.get_vm(), -3))) { - sq_pushstring(vm.get_vm(), tilemap.get_name().c_str(), -1); - sq_newtable(vm.get_vm()); - vm.store_float("alpha", tilemap.get_alpha()); - if (SQ_FAILED(sq_createslot(vm.get_vm(), -3))) - { - throw std::runtime_error("failed to create '" + m_worldmap.m_name + "' table entry"); - } + throw std::runtime_error("failed to create '" + m_worldmap.m_name + "' table entry"); } } - if (SQ_FAILED(sq_createslot(vm.get_vm(), -3))) - { - throw std::runtime_error("failed to create '" + m_worldmap.m_name + "' table entry"); - } + } + if (SQ_FAILED(sq_createslot(vm.get_vm(), -3))) + { + throw std::runtime_error("failed to create '" + m_worldmap.m_name + "' table entry"); + } +} - // levels... - vm.begin_table("levels"); +/** Save sprite change objects **/ +void +WorldMapState::save_sprite_change_objects() const +{ + WORLDMAP_STATE_SQUIRREL_VM_GUARD; + WORLDMAP_STATE_SECTOR_GUARD; - for (const auto& level : m_worldmap.get_objects_by_type()) + if (sector.get_object_count() > 0) + { + vm.begin_table("sprite-changes"); + for (const auto& sc : sector.get_objects_by_type()) { - vm.begin_table(level.get_level_filename().c_str()); - - vm.store_bool("solved", level.is_solved()); - vm.store_bool("perfect", level.is_perfect()); - - level.get_statistics().serialize_to_squirrel(vm); - vm.end_table(level.get_level_filename().c_str()); + auto key = std::to_string(int(sc.get_pos().x)) + "_" + + std::to_string(int(sc.get_pos().y)); + vm.begin_table(key.c_str()); + vm.store_bool("show-stay-action", sc.show_stay_action()); + vm.end_table(key.c_str()); } - vm.end_table("levels"); - - // push world into worlds table - vm.end_table(m_worldmap.m_map_filename.c_str()); - } catch(std::exception& ) { - sq_settop(vm.get_vm(), oldtop); + vm.end_table("sprite-changes"); } - - sq_settop(vm.get_vm(), oldtop); - - m_worldmap.m_savegame.save(); } } // namespace worldmap diff --git a/src/worldmap/worldmap_state.hpp b/src/worldmap/worldmap_state.hpp index 3635aec669a..edaaac205ca 100644 --- a/src/worldmap/worldmap_state.hpp +++ b/src/worldmap/worldmap_state.hpp @@ -1,6 +1,7 @@ // SuperTux // Copyright (C) 2004-2018 Ingo Ruhnke // 2006 Christoph Sommer +// 2023 Vankata453 // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -20,9 +21,18 @@ namespace worldmap { +/** Macro to allow quick and easy access to the current Squirrel VM. **/ +#define WORLDMAP_STATE_SQUIRREL_VM_GUARD \ + SquirrelVM& vm = SquirrelVirtualMachine::current()->get_vm() + +/** Macro to allow quick and easy access to the current WorldMapSector. **/ +#define WORLDMAP_STATE_SECTOR_GUARD \ + WorldMapSector& sector = m_worldmap.get_sector() + + class WorldMap; -class WorldMapState +class WorldMapState final { public: WorldMapState(WorldMap& worldmap); @@ -30,9 +40,23 @@ class WorldMapState void load_state(); void save_state() const; +private: + void load_tux(); + void load_levels(); + void load_tilemap_visibility(); + void load_sprite_change_objects(); + + void save_tux() const; + void save_levels() const; + void save_tilemap_visibility() const; + void save_sprite_change_objects() const; + private: WorldMap& m_worldmap; + /** Variables, related to loading. **/ + bool m_position_was_reset; + private: WorldMapState(const WorldMapState&) = delete; WorldMapState& operator=(const WorldMapState&) = delete; From 41cfbe28c0caa7c8a3bc9c1745bc412e86387b7e Mon Sep 17 00:00:00 2001 From: Tobias Markus Date: Sun, 16 Jul 2023 23:09:30 +0200 Subject: [PATCH 13/50] Fix spelling [ci skip] --- src/supertux/game_object_manager.hpp | 2 +- src/supertux/menu/profile_name_menu.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/supertux/game_object_manager.hpp b/src/supertux/game_object_manager.hpp index 857c2546e6f..7e7e1a31e84 100644 --- a/src/supertux/game_object_manager.hpp +++ b/src/supertux/game_object_manager.hpp @@ -199,7 +199,7 @@ class GameObjectManager void redo(); /** Save object change in the undo stack with given data. - Used to save an object's previous state before a change had occured. */ + Used to save an object's previous state before a change had occurred. */ void save_object_change(GameObject& object, const std::string& data); /** Clear undo/redo stacks. */ diff --git a/src/supertux/menu/profile_name_menu.cpp b/src/supertux/menu/profile_name_menu.cpp index fae36d24ed3..61fee2f331b 100644 --- a/src/supertux/menu/profile_name_menu.cpp +++ b/src/supertux/menu/profile_name_menu.cpp @@ -70,7 +70,7 @@ ProfileNameMenu::menu_action(MenuItem& item) if (!PHYSFS_mkdir(profile_path.c_str())) { log_warning << "Error creating folder for profile " << id << std::endl; - Dialog::show_message(_("An error occured while creating the profile.")); + Dialog::show_message(_("An error occurred while creating the profile.")); return; } From 9ee107d350baf1207db34362cf142eca9b74afd2 Mon Sep 17 00:00:00 2001 From: Tobias Markus Date: Mon, 17 Jul 2023 06:57:32 +0200 Subject: [PATCH 14/50] Adress FIXME comment in Owl code --- src/badguy/owl.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/badguy/owl.cpp b/src/badguy/owl.cpp index 5f9ff56383c..2eeb3de7320 100644 --- a/src/badguy/owl.cpp +++ b/src/badguy/owl.cpp @@ -110,7 +110,9 @@ Owl::active_update (float dt_sec) if (carried_object != nullptr) { if (!is_above_player ()) { Vector obj_pos = get_anchor_pos(m_col.m_bbox, ANCHOR_BOTTOM); - obj_pos.x -= 16.f; /* FIXME: Actually do use the half width of the carried object here. */ + auto obj = dynamic_cast(carried_object); + auto verticalOffset = obj != nullptr ? obj->get_bbox().get_width() / 2.f : 16.f; + obj_pos.x -= verticalOffset; obj_pos.y += 3.f; /* Move a little away from the hitbox (the body). Looks nicer. */ //To drop enemie before leave the screen From 18051d42cb1e2ab061610ccfe4486cd98d65d32b Mon Sep 17 00:00:00 2001 From: Vankata453 <78196474+Vankata453@users.noreply.github.com> Date: Mon, 17 Jul 2023 14:52:46 +0300 Subject: [PATCH 15/50] Replace dynamic with static casts in `Dispenser::launch_object()` The object to be launched is already verified to be a `MovingObject`. A `dynamic_cast` to check if the object is a `BadGuy` has already been performed before initializing the object to be launched. --- src/badguy/dispenser.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/badguy/dispenser.cpp b/src/badguy/dispenser.cpp index 7e6ab3e18d4..d6d1241c442 100644 --- a/src/badguy/dispenser.cpp +++ b/src/badguy/dispenser.cpp @@ -208,7 +208,7 @@ Dispenser::launch_object() { throw std::runtime_error("Creating " + object->get_class_name() + " object failed."); } - auto moving_object = dynamic_cast(game_object.get()); + auto moving_object = static_cast(game_object.get()); Rectf object_bbox = moving_object->get_bbox(); Vector spawnpoint(0.0f, 0.0f); @@ -255,7 +255,7 @@ Dispenser::launch_object() if (obj_badguy) // The object is a badguy { - auto badguy = dynamic_cast(moving_object); + auto badguy = static_cast(moving_object); /* We don't want to count dispensed badguys in level stats */ badguy->m_countMe = false; From bcf63da5c0be52f3a5a91dd434eede587bf9a51c Mon Sep 17 00:00:00 2001 From: Carsten Wirtz Date: Tue, 18 Jul 2023 20:24:15 +0200 Subject: [PATCH 16/50] Delete leftover test file from #2351 [ci skip] --- data/images/background/corrupted_forest/test | 1 - 1 file changed, 1 deletion(-) delete mode 100644 data/images/background/corrupted_forest/test diff --git a/data/images/background/corrupted_forest/test b/data/images/background/corrupted_forest/test deleted file mode 100644 index b2888c437aa..00000000000 --- a/data/images/background/corrupted_forest/test +++ /dev/null @@ -1 +0,0 @@ -dgbyfjgfjduku From 501228b8f055bc683d93d0adb4d80a4174d2ea2c Mon Sep 17 00:00:00 2001 From: Tobias Markus Date: Wed, 19 Jul 2023 00:45:33 +0200 Subject: [PATCH 17/50] Abstract PhysFS enumerate into PhysfsUtil (#2544) * Abstract PhysFS enumerate into Physfsutil * Restore comment [ci skip] * Remove unneeded capture * Include missing header * Fix -Wshadow * Fix -Wshadow --- src/addon/addon_manager.cpp | 41 ++++++++----------- src/gui/menu_filesystem.cpp | 33 ++++++--------- src/physfs/physfs_file_system.cpp | 12 ++---- src/physfs/util.cpp | 28 +++++++++---- src/physfs/util.hpp | 4 ++ src/supertux/levelset.cpp | 22 ++++------ src/supertux/menu/contrib_menu.cpp | 31 ++++---------- .../menu/editor_levelset_select_menu.cpp | 15 +++---- src/supertux/menu/profile_menu.cpp | 22 ++++------ src/video/bitmap_font.cpp | 13 +++--- 10 files changed, 96 insertions(+), 125 deletions(-) diff --git a/src/addon/addon_manager.cpp b/src/addon/addon_manager.cpp index 94802a60cf1..c0f0439324d 100644 --- a/src/addon/addon_manager.cpp +++ b/src/addon/addon_manager.cpp @@ -545,15 +545,13 @@ std::vector AddonManager::get_local_addon_screenshots(const AddonId& addon_id) { std::vector screenshots; - std::unique_ptr rc(PHYSFS_enumerateFiles(m_screenshots_cache_directory.c_str()), PHYSFS_freeList); - for (char** i = rc.get(); *i != nullptr; ++i) - { + physfsutil::enumerate_files(m_screenshots_cache_directory, [&screenshots, &addon_id, this](const std::string& filename) { // Push any files from the cache directory, starting with the ID of the add-on. - if (StringUtil::starts_with(*i, addon_id)) + if (StringUtil::starts_with(filename, addon_id)) { - screenshots.push_back(FileSystem::join(m_screenshots_cache_directory, *i)); + screenshots.push_back(FileSystem::join(m_screenshots_cache_directory, filename)); } - } + }); return screenshots; } @@ -753,28 +751,24 @@ AddonManager::scan_for_archives() const std::vector archives; // Search for archives and add them to the search path - std::unique_ptr - rc(PHYSFS_enumerateFiles(m_addon_directory.c_str()), - PHYSFS_freeList); - for (char** i = rc.get(); *i != nullptr; ++i) - { - const std::string fullpath = FileSystem::join(m_addon_directory, *i); + physfsutil::enumerate_files(m_addon_directory, [this, &archives](const std::string& filename) { + const std::string fullpath = FileSystem::join(m_addon_directory, filename); if (physfsutil::is_directory(fullpath)) { // ignore dot files (e.g. '.git/'), as well as the addon cache directory - if ((*i)[0] != '.' && fullpath != m_cache_directory) { + if (filename[0] != '.' && fullpath != m_cache_directory) { archives.push_back(fullpath); } } else { - if (StringUtil::has_suffix(StringUtil::tolower(*i), ".zip")) { + if (StringUtil::has_suffix(StringUtil::tolower(filename), ".zip")) { if (PHYSFS_exists(fullpath.c_str())) { archives.push_back(fullpath); } } } - } + }); return archives; } @@ -782,14 +776,11 @@ AddonManager::scan_for_archives() const std::string AddonManager::scan_for_info(const std::string& archive_os_path) const { - std::unique_ptr - rc2(PHYSFS_enumerateFiles("/"), - PHYSFS_freeList); - for (char** j = rc2.get(); *j != nullptr; ++j) - { - if (StringUtil::has_suffix(*j, ".nfo")) + std::string nfoFilename = std::string(); + physfsutil::enumerate_files("/", [archive_os_path, &nfoFilename](const std::string& file) { + if (StringUtil::has_suffix(file, ".nfo")) { - std::string nfo_filename = FileSystem::join("/", *j); + std::string nfo_filename = FileSystem::join("/", file); // make sure it's in the current archive_os_path const char* realdir = PHYSFS_getRealDir(nfo_filename.c_str()); @@ -801,13 +792,13 @@ AddonManager::scan_for_info(const std::string& archive_os_path) const { if (realdir == archive_os_path) { - return nfo_filename; + nfoFilename = nfo_filename; } } } - } + }); - return std::string(); + return nfoFilename; } void diff --git a/src/gui/menu_filesystem.cpp b/src/gui/menu_filesystem.cpp index a37c70d6217..2fc4e864df5 100644 --- a/src/gui/menu_filesystem.cpp +++ b/src/gui/menu_filesystem.cpp @@ -71,31 +71,24 @@ FileSystemMenu::refresh_items() if (m_directory != "/") { m_directories.push_back(".."); } - - char** dir_files = PHYSFS_enumerateFiles(m_directory.c_str()); - if (dir_files) - { - for (const char* const* file = dir_files; *file != nullptr; ++file) + physfsutil::enumerate_files(m_directory, [this](const std::string& file) { + std::string filepath = FileSystem::join(m_directory, file); + if (physfsutil::is_directory(filepath)) { - std::string filepath = FileSystem::join(m_directory, *file); - if (physfsutil::is_directory(filepath)) - { - m_directories.push_back(*file); + m_directories.push_back(file); + } + else + { + if (AddonManager::current()->is_from_old_addon(filepath)) { + return; } - else - { - if (AddonManager::current()->is_from_old_addon(filepath)) { - continue; - } - if (has_right_suffix(*file)) - { - m_files.push_back(*file); - } + if (has_right_suffix(file)) + { + m_files.push_back(file); } } - PHYSFS_freeList(dir_files); - } + }); for (const auto& item : m_directories) { diff --git a/src/physfs/physfs_file_system.cpp b/src/physfs/physfs_file_system.cpp index dbb8d20a00c..2356e086c97 100644 --- a/src/physfs/physfs_file_system.cpp +++ b/src/physfs/physfs_file_system.cpp @@ -19,6 +19,7 @@ #include #include "physfs/ifile_stream.hpp" +#include "physfs/util.hpp" PhysFSFileSystem::PhysFSFileSystem() { @@ -28,14 +29,9 @@ std::vector PhysFSFileSystem::open_directory(const std::string& pathname) { std::vector files; - - char** directory = PHYSFS_enumerateFiles(pathname.c_str()); - for (char** i = directory; *i != nullptr; ++i) - { - files.push_back(*i); - } - PHYSFS_freeList(directory); - + physfsutil::enumerate_files(pathname, [&files](const std::string& file) { + files.push_back(file); + }); return files; } diff --git a/src/physfs/util.cpp b/src/physfs/util.cpp index f237d041515..6c00ff23085 100644 --- a/src/physfs/util.cpp +++ b/src/physfs/util.cpp @@ -18,6 +18,7 @@ #include +#include "physfs/physfs_file_system.hpp" #include "util/file_system.hpp" namespace physfsutil { @@ -66,16 +67,12 @@ bool remove(const std::string& filename) void remove_content(const std::string& dir) { PHYSFS_UTIL_DIRECTORY_GUARD; - - char** files = PHYSFS_enumerateFiles(dir.c_str()); - for (const char* const* file = files; *file != nullptr; file++) - { - std::string path = FileSystem::join(dir, *file); + enumerate_files(dir, [&dir](const std::string& file) { + std::string path = FileSystem::join(dir, file); if (is_directory(path)) remove_with_content(path); PHYSFS_delete(path.c_str()); - } - PHYSFS_freeList(files); + }); } void remove_with_content(const std::string& dir) @@ -86,6 +83,23 @@ void remove_with_content(const std::string& dir) remove(dir); } +bool enumerate_files(const std::string& pathname, std::function callback) +{ + std::unique_ptr + files(PHYSFS_enumerateFiles(pathname.c_str()), + PHYSFS_freeList); + + if(files == nullptr) + return false; + + for (const char* const* filename = files.get(); *filename != nullptr; ++filename) + { + callback(*filename); + } + + return true; +} + } // namespace physfsutil /* EOF */ diff --git a/src/physfs/util.hpp b/src/physfs/util.hpp index 597b3151403..2e525b87402 100644 --- a/src/physfs/util.hpp +++ b/src/physfs/util.hpp @@ -17,6 +17,7 @@ #ifndef HEADER_SUPERTUX_PHYSFS_UTIL_HPP #define HEADER_SUPERTUX_PHYSFS_UTIL_HPP +#include #include namespace physfsutil { @@ -37,6 +38,9 @@ void remove_content(const std::string& dir); /** Removes directory with content */ void remove_with_content(const std::string& dir); +/** Open directory and call callback for each file */ +bool enumerate_files(const std::string& pathname, std::function callback); + } // namespace physfsutil #endif diff --git a/src/supertux/levelset.cpp b/src/supertux/levelset.cpp index 420c572d93e..4fc2991e791 100644 --- a/src/supertux/levelset.cpp +++ b/src/supertux/levelset.cpp @@ -48,25 +48,17 @@ void Levelset::walk_directory(const std::string& directory, bool recursively) { bool is_basedir = (directory == m_basedir); - char** files = PHYSFS_enumerateFiles(directory.c_str()); - if (!files) - { - log_warning << "Couldn't read subset dir '" << directory << "'" << std::endl; - return; - } - - for (const char* const* filename = files; *filename != nullptr; ++filename) - { - auto filepath = FileSystem::join(directory.c_str(), *filename); + bool enumerateSuccess = physfsutil::enumerate_files(directory, [directory, is_basedir, recursively, this](const auto& filename) { + auto filepath = FileSystem::join(directory, filename); if (physfsutil::is_directory(filepath) && recursively) { walk_directory(filepath, true); } - if (StringUtil::has_suffix(*filename, ".stl")) + if (StringUtil::has_suffix(filename, ".stl")) { if (is_basedir) { - m_levels.push_back(*filename); + m_levels.push_back(filename); } else { @@ -75,8 +67,12 @@ Levelset::walk_directory(const std::string& directory, bool recursively) m_levels.push_back(filepath); } } + }); + + if (!enumerateSuccess) + { + log_warning << "Couldn't read subset dir '" << directory << "'" << std::endl; } - PHYSFS_freeList(files); } /* EOF */ diff --git a/src/supertux/menu/contrib_menu.cpp b/src/supertux/menu/contrib_menu.cpp index 70d15eaab03..c8af865d31c 100644 --- a/src/supertux/menu/contrib_menu.cpp +++ b/src/supertux/menu/contrib_menu.cpp @@ -39,44 +39,31 @@ ContribMenu::ContribMenu() : { // Generating contrib levels list by making use of Level Subset std::vector level_worlds; - - std::unique_ptr - files(PHYSFS_enumerateFiles("levels"), - PHYSFS_freeList); - for (const char* const* filename = files.get(); *filename != nullptr; ++filename) - { - std::string filepath = FileSystem::join("levels", *filename); + physfsutil::enumerate_files("levels", [&level_worlds](const std::string& filename) { + std::string filepath = FileSystem::join("levels", filename); if (physfsutil::is_directory(filepath)) { level_worlds.push_back(filepath); } - } + }); - std::unique_ptr - addons(PHYSFS_enumerateFiles("custom"), - PHYSFS_freeList); - for (const char* const* addondir = addons.get(); *addondir != nullptr; ++addondir) - { - std::string addonpath = FileSystem::join("custom", *addondir); + physfsutil::enumerate_files("custom", [&level_worlds](const std::string& addon_filename) { + std::string addonpath = FileSystem::join("custom", addon_filename); if (physfsutil::is_directory(addonpath)) { std::string addonlevelpath = FileSystem::join(addonpath.c_str(), "levels"); if (physfsutil::is_directory(addonlevelpath)) { - std::unique_ptr - addonfiles(PHYSFS_enumerateFiles(addonlevelpath.c_str()), - PHYSFS_freeList); - for (const char* const* filename = addonfiles.get(); *filename != nullptr; ++filename) - { - std::string filepath = FileSystem::join(addonlevelpath.c_str(), *filename); + physfsutil::enumerate_files(addonlevelpath, [addonlevelpath, &level_worlds](const std::string& filename) { + std::string filepath = FileSystem::join(addonlevelpath.c_str(), filename); if (physfsutil::is_directory(filepath)) { level_worlds.push_back(filepath); } - } + }); } } - } + }); add_label(_("Contrib Levels")); add_hl(); diff --git a/src/supertux/menu/editor_levelset_select_menu.cpp b/src/supertux/menu/editor_levelset_select_menu.cpp index 778e85f0c1b..2066eb80f7d 100644 --- a/src/supertux/menu/editor_levelset_select_menu.cpp +++ b/src/supertux/menu/editor_levelset_select_menu.cpp @@ -55,21 +55,18 @@ void EditorLevelsetSelectMenu::initialize() { Editor::current()->m_deactivate_request = true; - // Generating contrib levels list by making use of Level Subset - std::vector level_worlds; m_contrib_worlds.clear(); - std::unique_ptr - files(PHYSFS_enumerateFiles("levels"), - PHYSFS_freeList); - for (const char* const* filename = files.get(); *filename != nullptr; ++filename) - { - std::string filepath = FileSystem::join("levels", *filename); + // Generating contrib levels list by making use of Level Subset + std::vector level_worlds; + + physfsutil::enumerate_files("levels", [&level_worlds](const auto& filename) { + std::string filepath = FileSystem::join("levels", filename); if (physfsutil::is_directory(filepath)) { level_worlds.push_back(filepath); } - } + }); add_label(_("Choose World")); add_hl(); diff --git a/src/supertux/menu/profile_menu.cpp b/src/supertux/menu/profile_menu.cpp index 8eb0d115629..6d10d2098e3 100644 --- a/src/supertux/menu/profile_menu.cpp +++ b/src/supertux/menu/profile_menu.cpp @@ -24,6 +24,7 @@ #include "gui/dialog.hpp" #include "gui/menu_manager.hpp" #include "gui/menu_item.hpp" +#include "physfs/util.hpp" #include "supertux/gameconfig.hpp" #include "supertux/globals.hpp" #include "supertux/menu/profile_name_menu.hpp" @@ -222,13 +223,10 @@ namespace savegames_util { std::vector get_savegames() { std::vector savegames; - char **rc = PHYSFS_enumerateFiles("/"); - char **i; - for (i = rc; *i != nullptr; i++) - { - if (std::string(*i).substr(0, 7) == "profile") savegames.push_back(std::stoi(std::string(*i).substr(7))); - } - PHYSFS_freeList(rc); + physfsutil::enumerate_files("/", [&savegames](const std::string& filename) { + if (std::string(filename).substr(0, 7) == "profile") + savegames.push_back(std::stoi(std::string(filename).substr(7))); + }); std::sort(savegames.begin(), savegames.end()); return savegames; } @@ -236,14 +234,10 @@ namespace savegames_util { void delete_savegames(int idx, bool reset) { const auto& profile_path = "profile" + std::to_string(idx); - std::unique_ptr - files(PHYSFS_enumerateFiles(profile_path.c_str()), - PHYSFS_freeList); - for (const char* const* filename = files.get(); *filename != nullptr; ++filename) - { - std::string filepath = FileSystem::join(profile_path.c_str(), *filename); + physfsutil::enumerate_files(profile_path, [profile_path](const std::string& filename) { + std::string filepath = FileSystem::join(profile_path.c_str(), filename); PHYSFS_delete(filepath.c_str()); - } + }); if (!reset) PHYSFS_delete(profile_path.c_str()); } } // namespace savegames_util diff --git a/src/video/bitmap_font.cpp b/src/video/bitmap_font.cpp index c847591e83e..72c87f6c35b 100644 --- a/src/video/bitmap_font.cpp +++ b/src/video/bitmap_font.cpp @@ -23,6 +23,7 @@ #include #include "physfs/physfs_sdl.hpp" +#include "physfs/util.hpp" #include "util/file_system.hpp" #include "util/log.hpp" #include "util/reader_document.hpp" @@ -70,20 +71,18 @@ BitmapFont::BitmapFont(GlyphWidth glyph_width_, const std::string fontname = FileSystem::basename(filename); // scan for prefix-filename in addons search path - char **rc = PHYSFS_enumerateFiles(fontdir.c_str()); - for (char **i = rc; *i != nullptr; i++) { - std::string filename_(*i); - if ( filename_.rfind(fontname) != std::string::npos ) { + physfsutil::enumerate_files(fontdir, [fontdir, fontname, this](const std::string& file) { + std::string filepath = FileSystem::join(fontdir, file); + if (file.rfind(fontname) != std::string::npos) { try { - loadFontFile(fontdir + filename_); + loadFontFile(filepath); } catch(const std::exception& e) { log_fatal << "Couldn't load font file: " << e.what() << std::endl; } } - } - PHYSFS_freeList(rc); + }); } void From ebf3d1223087814b189ec0a095be3e284bb63349 Mon Sep 17 00:00:00 2001 From: Tobias Markus Date: Wed, 19 Jul 2023 01:48:59 +0200 Subject: [PATCH 18/50] Change assertion to if statement to prevent crashes while calling from scripting --- src/object/tilemap.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/object/tilemap.cpp b/src/object/tilemap.cpp index a9aadd9f5a4..d3a3c744a42 100644 --- a/src/object/tilemap.cpp +++ b/src/object/tilemap.cpp @@ -677,7 +677,9 @@ TileMap::get_tile_at(const Vector& pos) const void TileMap::change(int x, int y, uint32_t newtile) { - assert(x >= 0 && x < m_width && y >= 0 && y < m_height); + if(x < 0 || x >= m_width || y < 0 || y >= m_height) + return; + m_tiles[y*m_width + x] = newtile; } From 3a41b420e467955c9a9efd51fc5909436e747ff9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Pale=C4=8Dek?= Date: Wed, 19 Jul 2023 12:59:57 +0200 Subject: [PATCH 19/50] Add a replace tool to the editor, clean up overlay_widget.cpp (#2539) This tool was in the old Mono editor but has never been implemented in the in-game editor. It finds all instances of a tile in a tilemap and replaces them with the desired tile. This can be especially useful when a player creates their own tileset that doesn't have any autotiles for their custom tiles. With this feature, they can create a level using a theme that does have autotiles and then simply replace the tiles from the theme with corresponding tiles from their custom theme. It can also be used to change a level's theme in general. --- data/images/engine/editor/select-mode3.png | Bin 0 -> 8551 bytes src/editor/overlay_widget.cpp | 469 +++++++++++++-------- src/editor/overlay_widget.hpp | 1 + src/editor/toolbox_widget.cpp | 1 + 4 files changed, 294 insertions(+), 177 deletions(-) create mode 100644 data/images/engine/editor/select-mode3.png diff --git a/data/images/engine/editor/select-mode3.png b/data/images/engine/editor/select-mode3.png new file mode 100644 index 0000000000000000000000000000000000000000..ac80da9ede9a3616c85e84dc6fd9feccf8bc48ed GIT binary patch literal 8551 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 literal 0 HcmV?d00001 diff --git a/src/editor/overlay_widget.cpp b/src/editor/overlay_widget.cpp index 3be7f1b7f47..310b107ba16 100644 --- a/src/editor/overlay_widget.cpp +++ b/src/editor/overlay_widget.cpp @@ -84,12 +84,14 @@ EditorOverlayWidget::~EditorOverlayWidget() void EditorOverlayWidget::update(float dt_sec) { - if (m_hovered_object && !m_hovered_object->is_valid()) { + if (m_hovered_object && !m_hovered_object->is_valid()) + { m_hovered_object = nullptr; m_object_tip = nullptr; } - if (m_selected_object && !m_selected_object->is_valid()) { + if (m_selected_object && !m_selected_object->is_valid()) + { delete_markers(); } } @@ -109,11 +111,11 @@ EditorOverlayWidget::delete_markers() { auto* sector = m_editor.get_sector(); - if (m_selected_object && m_selected_object->is_valid()) { + if (m_selected_object && m_selected_object->is_valid()) m_selected_object->editor_deselect(); - } - for (auto& marker : sector->get_objects_by_type()) { + for (auto& marker : sector->get_objects_by_type()) + { marker.remove_me(); } @@ -127,18 +129,24 @@ EditorOverlayWidget::drag_rect() const { int start_x, start_y, end_x, end_y; - if (m_drag_start.x < m_sector_pos.x) { + if (m_drag_start.x < m_sector_pos.x) + { start_x = static_cast(m_drag_start.x); end_x = static_cast(m_sector_pos.x); - } else { + } + else + { start_x = static_cast(m_sector_pos.x); end_x = static_cast(m_drag_start.x); } - if (m_drag_start.y < m_sector_pos.y) { + if (m_drag_start.y < m_sector_pos.y) + { start_y = static_cast(m_drag_start.y); end_y = static_cast(m_sector_pos.y); - } else { + } + else + { start_y = static_cast(m_sector_pos.y); end_y = static_cast(m_drag_start.y); } @@ -153,9 +161,7 @@ void EditorOverlayWidget::input_tile(const Vector& pos, uint32_t tile) { auto tilemap = m_editor.get_selected_tilemap(); - if (!tilemap || !is_position_inside_tilemap(tilemap, pos)) { - return; - } + if (!tilemap || !is_position_inside_tilemap(tilemap, pos)) return; tilemap->save_state(); tilemap->change(static_cast(pos.x), static_cast(pos.y), tile); @@ -165,9 +171,7 @@ void EditorOverlayWidget::autotile(const Vector& pos, uint32_t tile) { auto tilemap = m_editor.get_selected_tilemap(); - if (!tilemap || !is_position_inside_tilemap(tilemap, pos)) { - return; - } + if (!tilemap || !is_position_inside_tilemap(tilemap, pos)) return; tilemap->save_state(); tilemap->autotile(static_cast(pos.x), static_cast(pos.y), tile); @@ -197,9 +201,7 @@ EditorOverlayWidget::autotile_corner(const Vector& pos, uint32_t tile, TileMap::AutotileCornerOperation op) { auto tilemap = m_editor.get_selected_tilemap(); - if (!tilemap || !is_position_inside_tilemap(tilemap, pos)) { - return; - } + if (!tilemap || !is_position_inside_tilemap(tilemap, pos)) return; tilemap->save_state(); tilemap->autotile_corner(static_cast(pos.x), static_cast(pos.y), tile, op); @@ -233,23 +235,33 @@ EditorOverlayWidget::put_tile(const Vector& target_tile) Vector hovered_corner = target_tile + Vector(0.5f, 0.5f); auto tiles = m_editor.get_tiles(); Vector add_tile(0.0f, 0.0f); - for (add_tile.x = static_cast(tiles->m_width) - 1.0f; add_tile.x >= 0.0f; add_tile.x--) { - for (add_tile.y = static_cast(tiles->m_height) - 1.0f; add_tile.y >= 0; add_tile.y--) { + for (add_tile.x = static_cast(tiles->m_width) - 1.0f; add_tile.x >= 0.0f; add_tile.x--) + { + for (add_tile.y = static_cast(tiles->m_height) - 1.0f; add_tile.y >= 0; add_tile.y--) + { uint32_t tile = tiles->pos(static_cast(add_tile.x), static_cast(add_tile.y)); auto tilemap = m_editor.get_selected_tilemap(); - if (g_config->editor_autotile_mode && ((tilemap && tilemap->get_autotileset(tile)) || tile == 0)) { - if (tile == 0) { + if (g_config->editor_autotile_mode && ((tilemap && tilemap->get_autotileset(tile)) || tile == 0)) + { + if (tile == 0) + { tilemap->autotile_erase(target_tile + add_tile, hovered_corner + add_tile); - } else if (tilemap->get_autotileset(tile)->is_corner()) { + } + else if (tilemap->get_autotileset(tile)->is_corner()) + { input_autotile_corner(hovered_corner + add_tile, tile, target_tile + add_tile); - } else { + } + else + { input_autotile(target_tile + add_tile, tile); } - } else { + } + else + { input_tile(target_tile + add_tile, tile); } @@ -262,15 +274,16 @@ namespace { // segment from pos1 to pos2 (similarly to a line drawing algorithm) std::vector rasterize_line_segment(Vector pos1, Vector pos2) { - if (pos1 == pos2) - return std::vector {pos1}; + if (pos1 == pos2) return std::vector {pos1}; // An integer position (x, y) contains all floating point vectors in // [x, x+1) x [y, y+1) std::vector positions; Vector diff = pos2 - pos1; - if (fabsf(diff.x) > fabsf(diff.y)) { + if (fabsf(diff.x) > fabsf(diff.y)) + { // Go along X, from left to right - if (diff.x < 0) { + if (diff.x < 0) + { Vector tmp = pos1; pos1 = pos2; pos2 = tmp; @@ -280,10 +293,12 @@ namespace { float y_step = diff.y / diff.x; // The x coordinate of the first vertical grid line right of pos1 float x_first_gridline = floorf(pos1.x + 1.0f); - for (float x = x_first_gridline; x < pos2.x; ++x) { + for (float x = x_first_gridline; x < pos2.x; ++x) + { // The y coordinate where our line intersects the vertical grid line float y = pos1.y + (x - pos1.x) * y_step; - if (floorf(y) != floorf(y_prev)) { + if (floorf(y) != floorf(y_prev)) + { // The current position is one horizontal grid line higher than // the previous one, // so add the position left to the current vertical grid line @@ -293,14 +308,18 @@ namespace { // Add the position right to the current vertical grid line positions.emplace_back(Vector(x + 0.5f, y)); } - if (x_first_gridline > pos2.x && floorf(pos2.y) != floorf(pos1.y)) { + if (x_first_gridline > pos2.x && floorf(pos2.y) != floorf(pos1.y)) + { // Special case: a single horizontal grid line is crossed with an acute // angle but no vertical grid line, so the for loop was skipped positions.emplace_back(pos2); } - } else { + } + else + { // Go along Y, from top to bottom - if (diff.y < 0) { + if (diff.y < 0) + { Vector tmp = pos1; pos1 = pos2; pos2 = tmp; @@ -309,20 +328,23 @@ namespace { float x_prev = pos1.x; float x_step = diff.x / diff.y; float y_first_gridline = floorf(pos1.y + 1.0f); - for (float y = y_first_gridline; y < pos2.y; ++y) { + for (float y = y_first_gridline; y < pos2.y; ++y) + { float x = pos1.x + (y - pos1.y) * x_step; - if (floorf(x) != floorf(x_prev)) { + if (floorf(x) != floorf(x_prev)) + { positions.emplace_back(Vector(x, y - 0.5f)); x_prev = x; } positions.emplace_back(Vector(x, y + 0.5f)); } - if (y_first_gridline > pos2.y && floorf(pos2.x) != floorf(pos1.x)) { + if (y_first_gridline > pos2.y && floorf(pos2.x) != floorf(pos1.x)) + { positions.emplace_back(pos2); } } return positions; - }; + } } // namespace void @@ -332,7 +354,8 @@ EditorOverlayWidget::put_next_tiles() int expired_ms = static_cast(std::chrono::duration_cast< std::chrono::milliseconds>(time_now - m_time_prev_put_tile).count()); m_time_prev_put_tile = time_now; - if (expired_ms > 70) { + if (expired_ms > 70) + { // Avoid drawing lines when the user has hold the left mouse button for some // time while not putting a tile put_tile(m_hovered_tile); @@ -342,7 +365,8 @@ EditorOverlayWidget::put_next_tiles() // Interpolate on a sub-grid with twice width and height because autotiling // needs to know the closest corner for (const Vector &pos : rasterize_line_segment(m_hovered_tile_prev * 2.0f, - m_hovered_tile * 2.0f)) { + m_hovered_tile * 2.0f)) + { put_tile(pos * 0.5f); } m_hovered_tile_prev = m_hovered_tile; @@ -361,9 +385,11 @@ EditorOverlayWidget::preview_rectangle() m_rectangle_preview->m_width = static_cast(dr.get_width()) + 1; m_rectangle_preview->m_height = static_cast(dr.get_height()) + 1; int y_ = sgn_y ? 0 : static_cast(-dr.get_height()); - for (int y = static_cast(dr.get_top()); y <= static_cast(dr.get_bottom()); y++, y_++) { + for (int y = static_cast(dr.get_top()); y <= static_cast(dr.get_bottom()); y++, y_++) + { int x_ = sgn_x ? 0 : static_cast(-dr.get_width()); - for (int x = static_cast(dr.get_left()); x <= static_cast(dr.get_right()); x++, x_++) { + for (int x = static_cast(dr.get_left()); x <= static_cast(dr.get_right()); x++, x_++) + { m_rectangle_preview->m_tiles.push_back(m_editor.get_tiles()->pos(x_, y_)); } } @@ -380,14 +406,15 @@ EditorOverlayWidget::draw_rectangle() bool sgn_y = m_drag_start.y < m_sector_pos.y; int x_ = sgn_x ? 0 : static_cast(-dr.get_width()); - for (int x = static_cast(dr.get_left()); x <= static_cast(dr.get_right()); x++, x_++) { + for (int x = static_cast(dr.get_left()); x <= static_cast(dr.get_right()); x++, x_++) + { int y_ = sgn_y ? 0 : static_cast(-dr.get_height()); - for (int y = static_cast(dr.get_top()); y <= static_cast(dr.get_bottom()); y++, y_++) { - if (g_config->editor_autotile_mode) { - input_autotile( Vector(static_cast(x), static_cast(y)), m_editor.get_tiles()->pos(x_, y_) ); - } else { - input_tile( Vector(static_cast(x), static_cast(y)), m_editor.get_tiles()->pos(x_, y_) ); - } + for (int y = static_cast(dr.get_top()); y <= static_cast(dr.get_bottom()); y++, y_++) + { + if (g_config->editor_autotile_mode) + input_autotile(Vector(static_cast(x), static_cast(y)), m_editor.get_tiles()->pos(x_, y_)); + else + input_tile(Vector(static_cast(x), static_cast(y)), m_editor.get_tiles()->pos(x_, y_)); } } } @@ -397,12 +424,15 @@ EditorOverlayWidget::check_tiles_for_fill(uint32_t replace_tile, uint32_t target_tile, uint32_t third_tile) const { - if (g_config->editor_autotile_mode) { + if (g_config->editor_autotile_mode) + { return m_editor.get_tileset()->get_autotileset_from_tile(replace_tile) == m_editor.get_tileset()->get_autotileset_from_tile(target_tile) && m_editor.get_tileset()->get_autotileset_from_tile(replace_tile) != m_editor.get_tileset()->get_autotileset_from_tile(third_tile); - } else { + } + else + { return replace_tile == target_tile && replace_tile != third_tile; } } @@ -412,14 +442,13 @@ EditorOverlayWidget::fill() { auto tiles = m_editor.get_tiles(); auto tilemap = m_editor.get_selected_tilemap(); - if (!tilemap) { - return; - } + if (!tilemap) return; // The tile that is going to be replaced: - Uint32 replace_tile = tilemap->get_tile_id(static_cast(m_hovered_tile.x), static_cast(m_hovered_tile.y)); + uint32_t replace_tile = tilemap->get_tile_id(static_cast(m_hovered_tile.x), static_cast(m_hovered_tile.y)); - if (replace_tile == tiles->pos(0, 0)) { + if (replace_tile == tiles->pos(0, 0)) + { // Replacing by the same tiles shouldn't do anything. return; } @@ -429,9 +458,11 @@ EditorOverlayWidget::fill() pos_stack.push_back(m_hovered_tile); // Passing recursively trough all tiles to be replaced... - while (pos_stack.size()) { + while (pos_stack.size()) + { - if (pos_stack.size() > 1000000) { + if (pos_stack.size() > 1000000) + { log_warning << "More than 1'000'000 tiles in stack to fill, STOP" << std::endl; return; } @@ -453,10 +484,12 @@ EditorOverlayWidget::fill() // Going left... pos_ = pos + Vector(-1, 0); - if (pos_.x >= 0) { + if (pos_.x >= 0) + { if (check_tiles_for_fill(replace_tile, tilemap->get_tile_id(static_cast(pos_.x), static_cast(pos_.y)), - tiles->pos(static_cast(tpos.x - 1), static_cast(tpos.y)))) { + tiles->pos(static_cast(tpos.x - 1), static_cast(tpos.y)))) + { pos_stack.push_back( pos_ ); continue; } @@ -464,10 +497,12 @@ EditorOverlayWidget::fill() // Going right... pos_ = pos + Vector(1, 0); - if (pos_.x < static_cast(tilemap->get_width())) { + if (pos_.x < static_cast(tilemap->get_width())) + { if (check_tiles_for_fill(replace_tile, tilemap->get_tile_id(static_cast(pos_.x), static_cast(pos_.y)), - tiles->pos(static_cast(tpos.x + 1), static_cast(tpos.y)))) { + tiles->pos(static_cast(tpos.x + 1), static_cast(tpos.y)))) + { pos_stack.push_back( pos_ ); continue; } @@ -475,10 +510,12 @@ EditorOverlayWidget::fill() // Going up... pos_ = pos + Vector(0, -1); - if (pos_.y >= 0) { + if (pos_.y >= 0) + { if (check_tiles_for_fill(replace_tile, tilemap->get_tile_id(static_cast(pos_.x), static_cast(pos_.y)), - tiles->pos(static_cast(tpos.x), static_cast(tpos.y - 1)))) { + tiles->pos(static_cast(tpos.x), static_cast(tpos.y - 1)))) + { pos_stack.push_back( pos_ ); continue; } @@ -486,25 +523,51 @@ EditorOverlayWidget::fill() // Going down... pos_ = pos + Vector(0, 1); - if (pos_.y < static_cast(tilemap->get_height())) { + if (pos_.y < static_cast(tilemap->get_height())) + { if (check_tiles_for_fill(replace_tile, tilemap->get_tile_id(static_cast(pos_.x), static_cast(pos_.y)), - tiles->pos(static_cast(tpos.x), static_cast(tpos.y + 1)))) { + tiles->pos(static_cast(tpos.x), static_cast(tpos.y + 1)))) + { pos_stack.push_back( pos_ ); continue; } } // Autotile happens after directional detection (because of borders; see snow tileset) - if (g_config->editor_autotile_mode) { + if (g_config->editor_autotile_mode) input_autotile(pos, tiles->pos(static_cast(tpos.x), static_cast(tpos.y))); - } // When tiles on each side are already filled or occupied by another tiles, it ends. pos_stack.pop_back(); } } +void +EditorOverlayWidget::replace() +{ + auto tilemap = m_editor.get_selected_tilemap(); + uint32_t replace_tile = tilemap->get_tile_id(static_cast(m_hovered_tile.x), static_cast(m_hovered_tile.y)); + + // Don't do anything if the old and new tiles are the same tile. + if (m_editor.get_tiles()->m_width == 1 && m_editor.get_tiles()->m_height == 1 && replace_tile == m_editor.get_tiles()->pos(0, 0)) return; + + tilemap->save_state(); + for (int x = 0; x < tilemap->get_width(); ++x) + { + for (int y = 0; y < tilemap->get_height(); ++y) + { + if (tilemap->get_tile_id(x, y) == replace_tile) + { + tilemap->change(x, y, m_editor.get_tiles()->pos( + (x - static_cast(m_hovered_tile.x)) % m_editor.get_tiles()->m_width, + (y - static_cast(m_hovered_tile.y)) % m_editor.get_tiles()->m_height) + ); + } + } + } +} + void EditorOverlayWidget::hover_object() { @@ -519,8 +582,10 @@ EditorOverlayWidget::hover_object() for (auto& moving_object : m_editor.get_sector()->get_objects_by_type()) { Rectf bbox = moving_object.get_bbox(); - if (bbox.contains(m_sector_pos)) { - if (&moving_object != m_hovered_object) { + if (bbox.contains(m_sector_pos)) + { + if (&moving_object != m_hovered_object) + { // Ignore BezierMarkers if ctrl isn't pressed... (1/2) auto* bezier_marker = dynamic_cast(&moving_object); @@ -577,7 +642,8 @@ EditorOverlayWidget::edit_path(PathGameObject* path, GameObject* new_marked_obje if (!path) return; delete_markers(); - if (!path->is_valid()) { + if (!path->is_valid()) + { m_edited_path = nullptr; return; } @@ -604,15 +670,16 @@ EditorOverlayWidget::select_object() delete_markers(); if (!m_dragged_object || !m_dragged_object->is_valid()) return; - if (m_dragged_object->has_variable_size()) { + if (m_dragged_object->has_variable_size()) + { m_selected_object = m_dragged_object; m_dragged_object->editor_select(); return; } auto path_obj = dynamic_cast(m_dragged_object.get()); - if (path_obj && path_obj->get_path_gameobject()) - { + + if (path_obj && path_obj->get_path_gameobject()) { edit_path(path_obj->get_path_gameobject(), m_dragged_object.get()); } } @@ -620,7 +687,8 @@ EditorOverlayWidget::select_object() void EditorOverlayWidget::grab_object() { - if (m_hovered_object) { + if (m_hovered_object) + { if (!m_hovered_object->is_valid()) { m_hovered_object = nullptr; @@ -633,9 +701,8 @@ EditorOverlayWidget::grab_object() m_dragged_object->save_state(); auto* pm = dynamic_cast(m_hovered_object.get()); - if (!pm) { - select_object(); - } + if (!pm) select_object(); + m_last_node_marker = dynamic_cast(pm); } } @@ -648,7 +715,9 @@ EditorOverlayWidget::grab_object() m_edited_path->is_valid()) { // do nothing - } else { + } + else + { delete_markers(); } } @@ -657,8 +726,10 @@ EditorOverlayWidget::grab_object() void EditorOverlayWidget::clone_object() { - if (m_hovered_object && m_hovered_object->is_saveable()) { - if (!m_hovered_object->is_valid()) { + if (m_hovered_object && m_hovered_object->is_saveable()) + { + if (!m_hovered_object->is_valid()) + { m_hovered_object = nullptr; return; } @@ -705,20 +776,22 @@ EditorOverlayWidget::show_object_menu(GameObject& object) void EditorOverlayWidget::move_object() { - if (m_dragged_object) { - if (!m_dragged_object->is_valid()) { + if (m_dragged_object) + { + if (!m_dragged_object->is_valid()) + { m_dragged_object = nullptr; return; } Vector new_pos = m_sector_pos - m_obj_mouse_desync; - if (g_config->editor_snap_to_grid) { + if (g_config->editor_snap_to_grid) + { auto& snap_grid_size = snap_grid_sizes[g_config->editor_selected_snap_grid_size]; new_pos = glm::floor(new_pos / static_cast(snap_grid_size)) * static_cast(snap_grid_size); auto pm = dynamic_cast(m_dragged_object.get()); - if (pm) { + if (pm) new_pos -= pm->get_offset(); - } } // TODO: Temporarily disabled during ongoing discussion @@ -745,9 +818,11 @@ EditorOverlayWidget::rubber_object() if (!m_edited_path) { delete_markers(); } + if (m_dragged_object) { m_dragged_object->editor_delete(); } + m_last_node_marker = nullptr; } @@ -756,11 +831,13 @@ EditorOverlayWidget::rubber_rect() { delete_markers(); Rectf dr = drag_rect(); - for (auto& moving_object : m_editor.get_sector()->get_objects_by_type()) { + for (auto& moving_object : m_editor.get_sector()->get_objects_by_type()) + { Rectf bbox = moving_object.get_bbox(); if (dr.contains(bbox)) { moving_object.editor_delete(); } + } m_last_node_marker = nullptr; } @@ -772,7 +849,8 @@ EditorOverlayWidget::update_node_iterators() if (!m_edited_path->is_valid()) return; auto* sector = m_editor.get_sector(); - for (auto& moving_object : sector->get_objects_by_type()) { + for (auto& moving_object : sector->get_objects_by_type()) + { auto marker = dynamic_cast(&moving_object); if (marker) { marker->update_iterator(); @@ -834,7 +912,8 @@ EditorOverlayWidget::put_object() object->after_editor_set(); auto* mo = dynamic_cast (object.get()); - if (mo && !g_config->editor_snap_to_grid) { + if (mo && !g_config->editor_snap_to_grid) + { auto bbox = mo->get_bbox(); mo->move_to(mo->get_pos() - Vector(bbox.get_width() / 2, bbox.get_height() / 2)); } @@ -851,8 +930,7 @@ EditorOverlayWidget::put_object() void EditorOverlayWidget::process_left_click() { - if (MenuManager::instance().has_dialog()) - return; + if (MenuManager::instance().has_dialog()) return; m_dragging = true; m_dragging_right = false; m_drag_start = m_sector_pos; @@ -876,6 +954,10 @@ EditorOverlayWidget::process_left_click() fill(); break; + case 3: + replace(); + break; + default: break; } @@ -896,11 +978,12 @@ EditorOverlayWidget::process_left_click() break; } - if (!m_editor.get_tileselect_object().empty()) { - if (!m_dragged_object) { - put_object(); - } - } else { + if (!m_editor.get_tileselect_object().empty()) + { + if (!m_dragged_object) put_object(); + } + else + { rubber_object(); } break; @@ -977,9 +1060,7 @@ EditorOverlayWidget::update_tile_selection() Rectf select = tile_drag_rect(); auto tiles = m_editor.get_tiles(); auto tilemap = m_editor.get_selected_tilemap(); - if (!tilemap) { - return; - } + if (!tilemap) return; tiles->m_tiles.clear(); tiles->m_width = static_cast(select.get_width()); @@ -987,11 +1068,16 @@ EditorOverlayWidget::update_tile_selection() int w = static_cast(tilemap->get_width()); int h = static_cast(tilemap->get_height()); - for (int y = static_cast(select.get_top()); y < static_cast(select.get_bottom()); y++) { - for (int x = static_cast(select.get_left()); x < static_cast(select.get_right()); x++) { - if ( x < 0 || y < 0 || x >= w || y >= h) { + for (int y = static_cast(select.get_top()); y < static_cast(select.get_bottom()); y++) + { + for (int x = static_cast(select.get_left()); x < static_cast(select.get_right()); x++) + { + if ( x < 0 || y < 0 || x >= w || y >= h) + { tiles->m_tiles.push_back(0); - } else { + } + else + { tiles->m_tiles.push_back(tilemap->get_tile_id(x, y)); } } @@ -1015,12 +1101,15 @@ EditorOverlayWidget::on_mouse_button_up(const SDL_MouseButtonEvent& button) } else if (m_editor.get_tileselect_input_type() == EditorToolboxWidget::InputType::OBJECT) { - if (m_dragging && m_dragged_object) + if (m_dragging && m_dragged_object) { m_dragged_object->check_state(); + } } } else if (button.button == SDL_BUTTON_MIDDLE) + { m_scrolling = false; + } m_dragging = false; @@ -1062,10 +1151,14 @@ EditorOverlayWidget::on_mouse_motion(const SDL_MouseMotionEvent& motion) switch (m_editor.get_tileselect_input_type()) { case EditorToolboxWidget::InputType::TILE: - if (m_dragging_right) { + if (m_dragging_right) + { update_tile_selection(); - } else { - switch (m_editor.get_tileselect_select_mode()) { + } + else + { + switch (m_editor.get_tileselect_select_mode()) + { case 0: put_next_tiles(); break; @@ -1079,11 +1172,15 @@ EditorOverlayWidget::on_mouse_motion(const SDL_MouseMotionEvent& motion) break; case EditorToolboxWidget::InputType::OBJECT: - if (m_editor.get_tileselect_object().empty()) { - if (m_editor.get_tileselect_select_mode() == 1) { + if (m_editor.get_tileselect_object().empty()) + { + if (m_editor.get_tileselect_select_mode() == 1) + { rubber_rect(); } - } else { + } + else + { move_object(); } break; @@ -1109,11 +1206,11 @@ bool EditorOverlayWidget::on_key_up(const SDL_KeyboardEvent& key) { auto sym = key.keysym.sym; - if (sym == SDLK_LSHIFT) - { + if (sym == SDLK_LSHIFT) { g_config->editor_snap_to_grid = !g_config->editor_snap_to_grid; } - if (sym == SDLK_LCTRL || sym == SDLK_RCTRL) { + if (sym == SDLK_LCTRL || sym == SDLK_RCTRL) + { if (action_pressed) { g_config->editor_autotile_mode = !g_config->editor_autotile_mode; @@ -1138,7 +1235,8 @@ EditorOverlayWidget::on_key_down(const SDL_KeyboardEvent& key) if (sym == SDLK_F7 || sym == SDLK_LSHIFT) { g_config->editor_snap_to_grid = !g_config->editor_snap_to_grid; } - if (sym == SDLK_F5 || ((sym == SDLK_LCTRL || sym == SDLK_RCTRL) && !action_pressed)) { + if (sym == SDLK_F5 || ((sym == SDLK_LCTRL || sym == SDLK_RCTRL) && !action_pressed)) + { g_config->editor_autotile_mode = !g_config->editor_autotile_mode; action_pressed = true; // Hovered objects depend on which keys are pressed @@ -1153,8 +1251,7 @@ EditorOverlayWidget::on_key_down(const SDL_KeyboardEvent& key) void EditorOverlayWidget::update_pos() { - if(m_editor.get_sector() == nullptr) - return; + if(m_editor.get_sector() == nullptr) return; m_sector_pos = m_mouse_pos + m_editor.get_sector()->get_camera().get_translation(); m_hovered_tile = sp_to_tp(m_sector_pos); @@ -1166,23 +1263,22 @@ EditorOverlayWidget::update_pos() void EditorOverlayWidget::draw_tile_tip(DrawingContext& context) { - if ( m_editor.get_tileselect_input_type() == EditorToolboxWidget::InputType::TILE ) { - + if (m_editor.get_tileselect_input_type() == EditorToolboxWidget::InputType::TILE) + { auto tilemap = m_editor.get_selected_tilemap(); - if (!tilemap) { - return; - } + if (!tilemap) return; - if (m_editor.get_tiles()->empty()) - return; + if (m_editor.get_tiles()->empty()) return; Vector screen_corner = context.get_cliprect().p2() + m_editor.get_sector()->get_camera().get_translation(); Vector drawn_tile = m_hovered_tile; // FIXME: Why is this initialised if it's going to be overwritten right below? auto tiles = m_editor.get_tiles(); - for (drawn_tile.x = static_cast(tiles->m_width) - 1.0f; drawn_tile.x >= 0.0f; drawn_tile.x--) { - for (drawn_tile.y = static_cast(tiles->m_height) - 1.0f; drawn_tile.y >= 0.0f; drawn_tile.y--) { + for (drawn_tile.x = static_cast(tiles->m_width) - 1.0f; drawn_tile.x >= 0.0f; drawn_tile.x--) + { + for (drawn_tile.y = static_cast(tiles->m_height) - 1.0f; drawn_tile.y >= 0.0f; drawn_tile.y--) + { Vector on_tile = m_hovered_tile + drawn_tile; if (on_tile.x < 0 || @@ -1190,18 +1286,19 @@ EditorOverlayWidget::draw_tile_tip(DrawingContext& context) on_tile.x >= static_cast(tilemap->get_width()) || on_tile.y >= static_cast(tilemap->get_height()) || on_tile.x >= ceilf(screen_corner.x / 32) || - on_tile.y >= ceilf(screen_corner.y / 32)) { + on_tile.y >= ceilf(screen_corner.y / 32)) + { continue; } uint32_t tile_id = tiles->pos(static_cast(drawn_tile.x), static_cast(drawn_tile.y)); draw_tile(context.color(), *m_editor.get_tileset(), tile_id, align_to_tilemap(on_tile) - m_editor.get_sector()->get_camera().get_translation(), LAYER_GUI-11, Color(1, 1, 1, 0.5)); - /*if (tile_id) { - const Tile* tg_tile = m_editor.get_tileset()->get( tile_id ); - tg_tile->draw(context.color(), tp_to_sp(on_tile) - m_editor.get_sector()->camera->get_translation(), - LAYER_GUI-11, Color(1, 1, 1, 0.5)); - }*/ + //if (tile_id) { + //const Tile* tg_tile = m_editor.get_tileset()->get( tile_id ); + //tg_tile->draw(context.color(), tp_to_sp(on_tile) - m_editor.get_sector()->camera->get_translation(), + // LAYER_GUI-11, Color(1, 1, 1, 0.5)); + //} } } } @@ -1211,12 +1308,9 @@ void EditorOverlayWidget::draw_rectangle_preview(DrawingContext& context) { auto tilemap = m_editor.get_selected_tilemap(); - if (!tilemap) { - return; - } + if (!tilemap) return; - if (m_rectangle_preview->empty()) - return; + if (m_rectangle_preview->empty()) return; Vector screen_corner = context.get_cliprect().p2() + m_editor.get_sector()->get_camera().get_translation(); @@ -1225,8 +1319,10 @@ EditorOverlayWidget::draw_rectangle_preview(DrawingContext& context) std::min(sp_to_tp(m_drag_start).y, m_hovered_tile.y)); auto tiles = m_rectangle_preview.get(); - for (drawn_tile.x = static_cast(tiles->m_width) - 1.0f; drawn_tile.x >= 0.0f; drawn_tile.x--) { - for (drawn_tile.y = static_cast(tiles->m_height) - 1.0f; drawn_tile.y >= 0.0f; drawn_tile.y--) { + for (drawn_tile.x = static_cast(tiles->m_width) - 1.0f; drawn_tile.x >= 0.0f; drawn_tile.x--) + { + for (drawn_tile.y = static_cast(tiles->m_height) - 1.0f; drawn_tile.y >= 0.0f; drawn_tile.y--) + { Vector on_tile = corner + drawn_tile; if (on_tile.x < 0 || @@ -1234,7 +1330,8 @@ EditorOverlayWidget::draw_rectangle_preview(DrawingContext& context) on_tile.x >= static_cast(tilemap->get_width()) || on_tile.y >= static_cast(tilemap->get_height()) || on_tile.x >= ceilf(screen_corner.x / 32) || - on_tile.y >= ceilf(screen_corner.y / 32)) { + on_tile.y >= ceilf(screen_corner.y / 32)) + { continue; } uint32_t tile_id = tiles->pos(static_cast(drawn_tile.x), static_cast(drawn_tile.y)); @@ -1250,8 +1347,7 @@ EditorOverlayWidget::draw_tile_grid(DrawingContext& context, int tile_size, bool draw_shadow) const { auto current_tm = m_editor.get_selected_tilemap(); - if (current_tm == nullptr) - return; + if (current_tm == nullptr) return; int tm_width = current_tm->get_width() * (32 / tile_size); int tm_height = current_tm->get_height() * (32 / tile_size); @@ -1272,12 +1368,14 @@ EditorOverlayWidget::draw_tile_grid(DrawingContext& context, int tile_size, { context.color().draw_line(from, to, col, current_tm->get_layer()); }; - if (draw_shadow) { + if (draw_shadow) + { Vector viewport_scale = VideoSystem::current()->get_viewport().get_scale(); const Color shadow_colour(0.0f, 0.0f, 0.0f, 0.05f); const Vector shadow_offset(1.0f / viewport_scale.x, 1.0f / viewport_scale.y); - for (int i = static_cast(start.x); i <= static_cast(end.x); i++) { + for (int i = static_cast(start.x); i <= static_cast(end.x); i++) + { line_start = tile_screen_pos(Vector(static_cast(i), 0.0f), tile_size) + shadow_offset; line_end = tile_screen_pos(Vector(static_cast(i), end.y), @@ -1285,7 +1383,8 @@ EditorOverlayWidget::draw_tile_grid(DrawingContext& context, int tile_size, draw_line(line_start, line_end, shadow_colour); } - for (int i = static_cast(start.y); i <= static_cast(end.y); i++) { + for (int i = static_cast(start.y); i <= static_cast(end.y); i++) + { line_start = tile_screen_pos(Vector(0.0f, static_cast(i)), tile_size) + shadow_offset; line_end = tile_screen_pos(Vector(end.x, static_cast(i)), @@ -1295,13 +1394,15 @@ EditorOverlayWidget::draw_tile_grid(DrawingContext& context, int tile_size, } const Color line_color(1.f, 1.f, 1.f, 0.2f); - for (int i = static_cast(start.x); i <= static_cast(end.x); i++) { + for (int i = static_cast(start.x); i <= static_cast(end.x); i++) + { line_start = tile_screen_pos(Vector(static_cast(i), 0.0f), tile_size); line_end = tile_screen_pos(Vector(static_cast(i), end.y), tile_size); draw_line(line_start, line_end, line_color); } - for (int i = static_cast(start.y); i <= static_cast(end.y); i++) { + for (int i = static_cast(start.y); i <= static_cast(end.y); i++) + { line_start = tile_screen_pos(Vector(0.0f, static_cast(i)), tile_size); line_end = tile_screen_pos(Vector(end.x, static_cast(i)), tile_size); draw_line(line_start, line_end, line_color); @@ -1311,11 +1412,10 @@ EditorOverlayWidget::draw_tile_grid(DrawingContext& context, int tile_size, void EditorOverlayWidget::draw_tilemap_border(DrawingContext& context) { - if ( !m_editor.get_selected_tilemap() ) return; + if (!m_editor.get_selected_tilemap()) return; auto current_tm = m_editor.get_selected_tilemap(); - if (!current_tm) - return; + if (!current_tm) return; Vector start = tile_screen_pos( Vector(0, 0) ); Vector end = tile_screen_pos( Vector(static_cast(current_tm->get_width()), @@ -1334,15 +1434,20 @@ EditorOverlayWidget::draw_path(DrawingContext& context) if (!m_selected_object->is_valid()) return; if (!m_edited_path->is_valid()) return; - for (auto i = m_edited_path->get_path().m_nodes.begin(); i != m_edited_path->get_path().m_nodes.end(); ++i) { + for (auto i = m_edited_path->get_path().m_nodes.begin(); i != m_edited_path->get_path().m_nodes.end(); ++i) + { auto j = i+1; Path::Node* node1 = &(*i); Path::Node* node2; - if (j == m_edited_path->get_path().m_nodes.end()) { - if (m_edited_path->get_path().m_mode == WalkMode::CIRCULAR) { + if (j == m_edited_path->get_path().m_nodes.end()) + { + if (m_edited_path->get_path().m_mode == WalkMode::CIRCULAR) + { //loop to the first node node2 = &(*m_edited_path->get_path().m_nodes.begin()); - } else { + } + else + { // Just draw the bezier lines auto cam_translation = m_editor.get_sector()->get_camera().get_translation(); context.color().draw_line(node1->position - cam_translation, @@ -1353,7 +1458,9 @@ EditorOverlayWidget::draw_path(DrawingContext& context) Color(0, 0, 1), LAYER_GUI - 21); continue; } - } else { + } + else + { node2 = &(*j); } auto cam_translation = m_editor.get_sector()->get_camera().get_translation(); @@ -1384,11 +1491,13 @@ EditorOverlayWidget::draw(DrawingContext& context) draw_rectangle_preview(context); draw_path(context); - if (g_config->editor_render_grid) { + if (g_config->editor_render_grid) + { draw_tile_grid(context, 32, true); draw_tilemap_border(context); auto snap_grid_size = snap_grid_sizes[g_config->editor_selected_snap_grid_size]; - if (snap_grid_size != 32) { + if (snap_grid_size != 32) + { draw_tile_grid(context, snap_grid_size, false); } } @@ -1398,7 +1507,8 @@ EditorOverlayWidget::draw(DrawingContext& context) } if (m_dragging && m_editor.get_tileselect_select_mode() == 1 - && !m_dragging_right) { + && !m_dragging_right) + { // Draw selection rectangle... auto cam_translation = m_editor.get_sector()->get_camera().get_translation(); Vector p0 = m_drag_start - cam_translation; @@ -1406,9 +1516,11 @@ EditorOverlayWidget::draw(DrawingContext& context) if (p0.x > p3.x) { std::swap(p0.x, p3.x); } + if (p0.y > p3.y) { std::swap(p0.y, p3.y); } + Vector p1 = Vector(p0.x, p3.y); Vector p2 = Vector(p3.x, p0.y); @@ -1425,27 +1537,39 @@ EditorOverlayWidget::draw(DrawingContext& context) Color(0.0f, 1.0f, 0.0f, 0.2f), 0.0f, LAYER_GUI-5); } - if (m_dragging && m_dragging_right) { + if (m_dragging && m_dragging_right) + { context.color().draw_filled_rect(selection_draw_rect(), Color(0.2f, 0.4f, 1.0f, 0.6f), 0.0f, LAYER_GUI-13); } - if (g_config->editor_autotile_help) { + if (g_config->editor_autotile_help) + { if (m_editor.get_tileset()->get_autotileset_from_tile(m_editor.get_tiles()->pos(0, 0)) != nullptr) { - if (g_config->editor_autotile_mode) { + if (g_config->editor_autotile_mode) + { context.color().draw_text(Resources::normal_font, _("Autotile mode is on"), Vector(144, 16), ALIGN_LEFT, LAYER_OBJECTS+1, EditorOverlayWidget::text_autotile_active_color); - } else { + } + else + { context.color().draw_text(Resources::normal_font, _("Hold Ctrl to enable autotile"), Vector(144, 16), ALIGN_LEFT, LAYER_OBJECTS+1, EditorOverlayWidget::text_autotile_available_color); } - } else if (g_config->editor_autotile_mode) { - if (m_editor.get_tiles()->pos(0, 0) == 0) { + } + else if (g_config->editor_autotile_mode) + { + if (m_editor.get_tiles()->pos(0, 0) == 0) + { context.color().draw_text(Resources::normal_font, _("Autotile erasing mode is on"), Vector(144, 16), ALIGN_LEFT, LAYER_OBJECTS+1, EditorOverlayWidget::text_autotile_active_color); - } else { + } + else + { context.color().draw_text(Resources::normal_font, _("Selected tile isn't autotileable"), Vector(144, 16), ALIGN_LEFT, LAYER_OBJECTS+1, EditorOverlayWidget::text_autotile_error_color); } - } else if (m_editor.get_tiles()->pos(0, 0) == 0) { + } + else if (m_editor.get_tiles()->pos(0, 0) == 0) + { context.color().draw_text(Resources::normal_font, _("Hold Ctrl to enable autotile erasing"), Vector(144, 16), ALIGN_LEFT, LAYER_OBJECTS+1, EditorOverlayWidget::text_autotile_available_color); } } @@ -1456,10 +1580,7 @@ Vector EditorOverlayWidget::tp_to_sp(const Vector& tp, int tile_size) const { auto tilemap = m_editor.get_selected_tilemap(); - if (!tilemap) - { - return Vector(0, 0); - } + if (!tilemap) return Vector(0, 0); Vector sp = tp * static_cast(tile_size); return sp + tilemap->get_offset(); @@ -1469,10 +1590,7 @@ Vector EditorOverlayWidget::sp_to_tp(const Vector& sp, int tile_size) const { auto tilemap = m_editor.get_selected_tilemap(); - if (!tilemap) - { - return Vector(0, 0); - } + if (!tilemap) return Vector(0, 0); Vector sp_ = sp - tilemap->get_offset(); return sp_ / static_cast(tile_size); @@ -1489,10 +1607,7 @@ Vector EditorOverlayWidget::align_to_tilemap(const Vector& sp, int tile_size) const { auto tilemap = m_editor.get_selected_tilemap(); - if (!tilemap) - { - return Vector(0, 0); - } + if (!tilemap) return Vector(0, 0); Vector sp_ = sp + tilemap->get_offset() / static_cast(tile_size); return glm::trunc(sp_) * static_cast(tile_size); diff --git a/src/editor/overlay_widget.hpp b/src/editor/overlay_widget.hpp index 63e0b2801ca..ac322d94cc8 100644 --- a/src/editor/overlay_widget.hpp +++ b/src/editor/overlay_widget.hpp @@ -83,6 +83,7 @@ class EditorOverlayWidget final : public Widget void preview_rectangle(); bool check_tiles_for_fill(uint32_t replace_tile, uint32_t target_tile, uint32_t third_tile) const; void fill(); + void replace(); void put_object(); void rubber_object(); diff --git a/src/editor/toolbox_widget.cpp b/src/editor/toolbox_widget.cpp index a5b38f8bb64..62076b34346 100644 --- a/src/editor/toolbox_widget.cpp +++ b/src/editor/toolbox_widget.cpp @@ -65,6 +65,7 @@ EditorToolboxWidget::EditorToolboxWidget(Editor& editor) : { m_select_mode->push_mode("images/engine/editor/select-mode1.png"); m_select_mode->push_mode("images/engine/editor/select-mode2.png"); + m_select_mode->push_mode("images/engine/editor/select-mode3.png"); m_move_mode->push_mode("images/engine/editor/move-mode1.png"); m_undo_mode->push_mode("images/engine/editor/redo.png"); //settings_mode->push_mode("images/engine/editor/settings-mode1.png"); From e085026680815d44baab3112b3900cf02d86db60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Pale=C4=8Dek?= Date: Thu, 20 Jul 2023 10:49:01 +0200 Subject: [PATCH 20/50] Fix oklab colour picker rendering when using SDL (#2549) * A dirty fix * Fix the weird bars * A proper fix --- src/video/sdl/sdl_painter.cpp | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/video/sdl/sdl_painter.cpp b/src/video/sdl/sdl_painter.cpp index 062a5c81ce8..b8a2b76c4a1 100644 --- a/src/video/sdl/sdl_painter.cpp +++ b/src/video/sdl/sdl_painter.cpp @@ -278,19 +278,20 @@ SDLPainter::draw_gradient(const GradientRequest& request) for (int i = 0; i < n; ++i) { SDL_Rect rect; + if (direction == VERTICAL || direction == VERTICAL_SECTOR) { rect.x = static_cast(region.get_left()); - rect.y = static_cast(region.get_bottom() * static_cast(i) / static_cast(n)); - rect.w = static_cast(region.get_right()); - rect.h = static_cast((region.get_bottom() * static_cast(i+1) / static_cast(n)) - static_cast(rect.y)); + rect.y = static_cast(region.get_top() + (region.get_bottom() - region.get_top()) * static_cast(i) / static_cast(n)); + rect.w = static_cast(region.get_right() - region.get_left()); + rect.h = static_cast(ceilf((region.get_bottom() - region.get_top()) / static_cast(n))); } else { - rect.x = static_cast(region.get_right() * static_cast(i) / static_cast(n)); + rect.x = static_cast(region.get_left() + (region.get_right() - region.get_left()) * static_cast(i) / static_cast(n)); rect.y = static_cast(region.get_top()); - rect.w = static_cast((region.get_right() * static_cast(i+1) / static_cast(n)) - static_cast(rect.x)); - rect.h = static_cast(region.get_bottom()); + rect.w = static_cast(ceilf((region.get_right() - region.get_left()) / static_cast(n))); + rect.h = static_cast(region.get_bottom() - region.get_top()); } float p = static_cast(i+1) / static_cast(n); From 63390054e0182b705fbaea140c9b0ef98a37094c Mon Sep 17 00:00:00 2001 From: MatusGuy <85036874+MatusGuy@users.noreply.github.com> Date: Thu, 20 Jul 2023 10:21:14 +0100 Subject: [PATCH 21/50] Ability to set bonus block coin sprite for coin, coin rain and coin explosion (#2546) This should close #2487 --- src/object/bonus_block.cpp | 19 ++++++++++++------- src/object/bonus_block.hpp | 1 + src/object/coin.cpp | 8 ++++---- src/object/coin.hpp | 6 ++++-- src/object/coin_explode.cpp | 23 ++++++++++++----------- src/object/coin_explode.hpp | 4 +++- src/object/coin_rain.cpp | 9 +++++---- src/object/coin_rain.hpp | 4 +++- 8 files changed, 44 insertions(+), 30 deletions(-) diff --git a/src/object/bonus_block.cpp b/src/object/bonus_block.cpp index b0eb391da96..b89fc7aadd3 100644 --- a/src/object/bonus_block.cpp +++ b/src/object/bonus_block.cpp @@ -69,7 +69,8 @@ BonusBlock::BonusBlock(const Vector& pos, int tile_data) : m_hit_counter(1), m_script(), m_lightsprite(), - m_custom_sx() + m_custom_sx(), + m_coin_sprite("images/objects/coin/coin.sprite") { m_default_sprite_name = "images/objects/bonus_block/bonusblock.sprite"; @@ -86,7 +87,8 @@ BonusBlock::BonusBlock(const ReaderMapping& mapping) : m_hit_counter(1), m_script(), m_lightsprite(), - m_custom_sx() + m_custom_sx(), + m_coin_sprite("images/objects/coin/coin.sprite") { m_default_sprite_name = "images/objects/bonus_block/bonusblock.sprite"; @@ -138,6 +140,8 @@ BonusBlock::BonusBlock(const ReaderMapping& mapping) : } } } + } else if (token == "coin-sprite") { + iter.get(m_coin_sprite); } else if (token == "custom-contents") { // handled elsewhere } else { @@ -219,8 +223,9 @@ BonusBlock::get_settings() "1up", "custom", "script", "light", "light-on", "trampoline", "portabletrampoline", "rain", "explode", "rock", "potion"}, static_cast(Content::COIN), "contents"); result.add_sexp(_("Custom Content"), "custom-contents", m_custom_sx); + result.add_sprite(_("Coin sprite"), &m_coin_sprite, "coin-sprite", "images/objects/coin/coin.sprite"); - result.reorder({"script", "count", "contents", "sprite", "x", "y"}); + result.reorder({"script", "count", "contents", "coin-sprite", "sprite", "x", "y"}); return result; } @@ -289,7 +294,7 @@ BonusBlock::try_open(Player* player) switch (m_contents) { case Content::COIN: { - Sector::get().add(get_pos(), true); + Sector::get().add(get_pos(), true, m_coin_sprite); SoundManager::current()->play("sounds/coin.wav", get_pos()); player->get_status().add_coins(1, false); if (m_hit_counter != 0 && !m_parent_dispenser) @@ -379,13 +384,13 @@ BonusBlock::try_open(Player* player) } case Content::RAIN: { - Sector::get().add(get_pos(), true, !m_parent_dispenser); + Sector::get().add(get_pos(), true, !m_parent_dispenser, m_coin_sprite); play_upgrade_sound = true; break; } case Content::EXPLODE: { - Sector::get().add(get_pos() + Vector (0, -40), !m_parent_dispenser); + Sector::get().add(get_pos() + Vector (0, -40), !m_parent_dispenser, m_coin_sprite); play_upgrade_sound = true; break; } @@ -529,7 +534,7 @@ BonusBlock::try_drop(Player *player) } case Content::EXPLODE: { - Sector::get().add(get_pos() + Vector (0, 40), !m_parent_dispenser); + Sector::get().add(get_pos() + Vector (0, 40), !m_parent_dispenser, m_coin_sprite); play_upgrade_sound = true; countdown = true; break; diff --git a/src/object/bonus_block.hpp b/src/object/bonus_block.hpp index 9412e033f3d..28a8c143ba9 100644 --- a/src/object/bonus_block.hpp +++ b/src/object/bonus_block.hpp @@ -85,6 +85,7 @@ class BonusBlock final : public Block std::string m_script; SurfacePtr m_lightsprite; sexp::Value m_custom_sx; + std::string m_coin_sprite; private: BonusBlock(const BonusBlock&) = delete; diff --git a/src/object/coin.cpp b/src/object/coin.cpp index 5228e290a27..62a5cf97f14 100644 --- a/src/object/coin.cpp +++ b/src/object/coin.cpp @@ -28,8 +28,8 @@ #include "util/reader_mapping.hpp" #include "util/writer.hpp" -Coin::Coin(const Vector& pos, bool count_stats) : - MovingSprite(pos, "images/objects/coin/coin.sprite", LAYER_OBJECTS - 1, COLGROUP_TOUCHABLE), +Coin::Coin(const Vector& pos, bool count_stats, const std::string& sprite_path) : + MovingSprite(pos, sprite_path, LAYER_OBJECTS - 1, COLGROUP_TOUCHABLE), PathObject(), m_offset(0.0f, 0.0f), m_from_tilemap(false), @@ -210,8 +210,8 @@ Coin::collision(GameObject& other, const CollisionHit& ) } /* The following defines a coin subject to gravity */ -HeavyCoin::HeavyCoin(const Vector& pos, const Vector& init_velocity, bool count_stats) : - Coin(pos, count_stats), +HeavyCoin::HeavyCoin(const Vector& pos, const Vector& init_velocity, bool count_stats, const std::string& sprite_path) : + Coin(pos, count_stats, sprite_path), m_physic(), m_last_hit() { diff --git a/src/object/coin.hpp b/src/object/coin.hpp index dfdb4a1cbcc..f67d7539307 100644 --- a/src/object/coin.hpp +++ b/src/object/coin.hpp @@ -32,7 +32,8 @@ class Coin : public MovingSprite, friend class HeavyCoin; public: - Coin(const Vector& pos, bool count_stats = true); + Coin(const Vector& pos, bool count_stats = true, + const std::string& sprite_path = "images/objects/coin/coin.sprite"); Coin(const ReaderMapping& reader, bool count_stats = true); virtual void finish_construction() override; @@ -73,7 +74,8 @@ friend class HeavyCoin; class HeavyCoin final : public Coin { public: - HeavyCoin(const Vector& pos, const Vector& init_velocity, bool count_stats = true); + HeavyCoin(const Vector& pos, const Vector& init_velocity, bool count_stats = true, + const std::string& sprite_path = "images/objects/coin/coin.sprite"); HeavyCoin(const ReaderMapping& reader, bool count_stats = true); virtual void update(float dt_sec) override; diff --git a/src/object/coin_explode.cpp b/src/object/coin_explode.cpp index 8f88a4ea2ac..c465f549590 100644 --- a/src/object/coin_explode.cpp +++ b/src/object/coin_explode.cpp @@ -20,7 +20,8 @@ #include "object/coin.hpp" #include "supertux/sector.hpp" -CoinExplode::CoinExplode(const Vector& pos, bool count_stats) : +CoinExplode::CoinExplode(const Vector& pos, bool count_stats, const std::string& sprite) : + m_sprite(sprite), position(pos), m_count_stats(count_stats) { @@ -32,16 +33,16 @@ CoinExplode::update(float ) float mag = 100.0f; // madnitude that coins are to be thrown float rand = 30.0f; // max variation to be subtracted from magnitide - Sector::get().add(position, Vector(2.5, -4.5) * (mag - gameRandom.randf(rand)), m_count_stats); - Sector::get().add(position, Vector(2, -5) * (mag - gameRandom.randf(rand)), m_count_stats); - Sector::get().add(position, Vector(1.5, -5.5) * (mag - gameRandom.randf(rand)), m_count_stats); - Sector::get().add(position, Vector(1, -6) * (mag+gameRandom.randf(rand)), m_count_stats); - Sector::get().add(position, Vector(0.5, -6.5) * (mag - gameRandom.randf(rand)), m_count_stats); - Sector::get().add(position, Vector(-2.5, -4.5) * (mag - gameRandom.randf(rand)), m_count_stats); - Sector::get().add(position, Vector(-2, -5) * (mag - gameRandom.randf(rand)), m_count_stats); - Sector::get().add(position, Vector(-1.5, -5.5) * (mag - gameRandom.randf(rand)), m_count_stats); - Sector::get().add(position, Vector(-1, -6) * (mag+gameRandom.randf(rand)), m_count_stats); - Sector::get().add(position, Vector(-0.5, -6.5) * (mag - gameRandom.randf(rand)), m_count_stats); + Sector::get().add(position, Vector(2.5, -4.5) * (mag - gameRandom.randf(rand)), m_count_stats, m_sprite); + Sector::get().add(position, Vector(2, -5) * (mag - gameRandom.randf(rand)), m_count_stats, m_sprite); + Sector::get().add(position, Vector(1.5, -5.5) * (mag - gameRandom.randf(rand)), m_count_stats, m_sprite); + Sector::get().add(position, Vector(1, -6) * (mag+gameRandom.randf(rand)), m_count_stats, m_sprite); + Sector::get().add(position, Vector(0.5, -6.5) * (mag - gameRandom.randf(rand)), m_count_stats, m_sprite); + Sector::get().add(position, Vector(-2.5, -4.5) * (mag - gameRandom.randf(rand)), m_count_stats, m_sprite); + Sector::get().add(position, Vector(-2, -5) * (mag - gameRandom.randf(rand)), m_count_stats, m_sprite); + Sector::get().add(position, Vector(-1.5, -5.5) * (mag - gameRandom.randf(rand)), m_count_stats, m_sprite); + Sector::get().add(position, Vector(-1, -6) * (mag+gameRandom.randf(rand)), m_count_stats, m_sprite); + Sector::get().add(position, Vector(-0.5, -6.5) * (mag - gameRandom.randf(rand)), m_count_stats, m_sprite); remove_me(); } diff --git a/src/object/coin_explode.hpp b/src/object/coin_explode.hpp index 8b09ff43f01..b8ec2233e8d 100644 --- a/src/object/coin_explode.hpp +++ b/src/object/coin_explode.hpp @@ -23,7 +23,8 @@ class CoinExplode final : public GameObject { public: - CoinExplode(const Vector& pos, bool count_stats = true); + CoinExplode(const Vector& pos, bool count_stats = true, + const std::string& sprite_path = "images/objects/coin/coin.sprite"); virtual void update(float dt_sec) override; virtual void draw(DrawingContext& context) override; virtual bool is_saveable() const override { @@ -31,6 +32,7 @@ class CoinExplode final : public GameObject } private: + std::string m_sprite; Vector position; const bool m_count_stats; }; diff --git a/src/object/coin_rain.cpp b/src/object/coin_rain.cpp index b33e29450ab..92c3f81aab7 100644 --- a/src/object/coin_rain.cpp +++ b/src/object/coin_rain.cpp @@ -24,8 +24,9 @@ static const float DROP_TIME = .1f; // time duration between "drops" of coin rain -CoinRain::CoinRain(const Vector& pos, bool emerge, bool count_stats) : - sprite(SpriteManager::current()->create("images/objects/coin/coin.sprite")), +CoinRain::CoinRain(const Vector& pos, bool emerge, bool count_stats, const std::string& sprite_path) : + sprite(SpriteManager::current()->create(sprite_path)), + m_sprite_path(sprite_path), position(pos), emerge_distance(0), timer(), @@ -50,7 +51,7 @@ CoinRain::update(float dt_sec) else if (counter==0){ drop = gameRandom.rand(10); Sector::get().add(Vector(position.x + 32.0f * static_cast((drop < 5) ? -drop - 1 : drop - 4), -32.0f), - Vector(0, 0), m_count_stats); + Vector(0, 0), m_count_stats, m_sprite_path); counter++; timer.start(DROP_TIME); } // finally the remainder of the coins drop in a determined but appears to be a random order @@ -59,7 +60,7 @@ CoinRain::update(float dt_sec) drop += 7; if (drop >= 10) drop -=10; Sector::get().add(Vector(position.x + 32.0f * static_cast((drop < 5) ? -drop - 1 : drop - 4), -32.0f), - Vector(0, 0), m_count_stats); + Vector(0, 0), m_count_stats, m_sprite_path); counter++; timer.start(DROP_TIME); } else { diff --git a/src/object/coin_rain.hpp b/src/object/coin_rain.hpp index 7c76d3ed9c8..55a8ce5b222 100644 --- a/src/object/coin_rain.hpp +++ b/src/object/coin_rain.hpp @@ -25,7 +25,8 @@ class CoinRain final : public GameObject { public: - CoinRain(const Vector& pos, bool emerge=false, bool count_stats = true); + CoinRain(const Vector& pos, bool emerge=false, bool count_stats = true, + const std::string& sprite_path = "images/objects/coin/coin.sprite"); virtual void update(float dt_sec) override; virtual void draw(DrawingContext& context) override; virtual bool is_saveable() const override { @@ -34,6 +35,7 @@ class CoinRain final : public GameObject private: SpritePtr sprite; + std::string m_sprite_path; Vector position; float emerge_distance; Timer timer; From 36365cd426a4989c7618bfd6fe335285de2a2b34 Mon Sep 17 00:00:00 2001 From: MatusGuy <85036874+MatusGuy@users.noreply.github.com> Date: Thu, 20 Jul 2023 17:31:00 +0100 Subject: [PATCH 22/50] Earth powerup stone form breaks bricks below without stopping (#2535) This should close #2532 --- src/object/brick.cpp | 13 ++----------- src/object/player.cpp | 20 +++++++++++++++----- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/object/brick.cpp b/src/object/brick.cpp index d6b7c54a53d..72cb89573f1 100644 --- a/src/object/brick.cpp +++ b/src/object/brick.cpp @@ -67,10 +67,7 @@ HitResponse Brick::collision(GameObject& other, const CollisionHit& hit) { auto player = dynamic_cast (&other); - if (player) { - if (player->m_does_buttjump) try_break(player); - if (player->is_stone() && player->get_velocity().y >= 280) try_break(player); // stoneform breaks through bricks - } + if (player && player->m_does_buttjump) try_break(player); auto badguy = dynamic_cast (&other); if (badguy) { @@ -166,13 +163,7 @@ HitResponse HeavyBrick::collision(GameObject& other, const CollisionHit& hit) { auto player = dynamic_cast(&other); - if (player) - { - if (player->is_stone() && player->get_velocity().y >= 280) - try_break(player); - else if (player->m_does_buttjump) - ricochet(&other); - } + if (player && player->m_does_buttjump) ricochet(&other); auto crusher = dynamic_cast (&other); if (crusher) diff --git a/src/object/player.cpp b/src/object/player.cpp index a3739d3dfe2..c2d4e246311 100644 --- a/src/object/player.cpp +++ b/src/object/player.cpp @@ -745,19 +745,29 @@ Player::update(float dt_sec) } } - if (m_does_buttjump) + if (m_does_buttjump || (m_stone && m_physic.get_velocity_y() > 30.f && !m_coyote_timer.started())) { Rectf downbox = get_bbox().grown(-1.f); downbox.set_bottom(get_bbox().get_bottom() + 16.f); for (auto& brick : Sector::get().get_objects_by_type()) { - if (downbox.contains(brick.get_bbox()) && brick.get_class_name() != "heavy-brick") { + // stoneform breaks through any kind of bricks + if (downbox.contains(brick.get_bbox()) && (m_stone || !dynamic_cast(&brick))) brick.try_break(this, is_big()); - } } for (auto& badguy : Sector::get().get_objects_by_type()) { - if (downbox.contains(badguy.get_bbox()) && badguy.is_snipable()) { + if (downbox.contains(badguy.get_bbox()) && badguy.is_snipable()) badguy.kill_fall(); - } + } + } + + // break bricks above without stopping + if (m_stone && m_physic.get_velocity_y() < 30.f) + { + Rectf topbox = get_bbox().grown(-1.f); + topbox.set_top(get_bbox().get_top() - 16.f); + for (auto& brick : Sector::get().get_objects_by_type()) { + if (topbox.contains(brick.get_bbox())) + brick.try_break(this, is_big()); } } From 6d4220e58c64e8c32e865f8c76ade7c1ca7f21ff Mon Sep 17 00:00:00 2001 From: Vankata453 <78196474+Vankata453@users.noreply.github.com> Date: Fri, 21 Jul 2023 18:31:08 +0300 Subject: [PATCH 23/50] Get active `Sector`, instead of `GameObjectManager`, in scripting The previous method of getting the current `GameObjectManager` from its `DynamicScopedRef` was proven ineffective, when executing commands from the console, which require it. Since the only usage of this function was to initialize a `FloatingImage` from scripting, it was modified, so it only checks for an active `Sector`. --- src/scripting/floating_image.cpp | 2 +- src/scripting/game_object.cpp | 10 +++++----- src/scripting/game_object.hpp | 5 +++-- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/scripting/floating_image.cpp b/src/scripting/floating_image.cpp index 642af02db89..436309a2ea7 100644 --- a/src/scripting/floating_image.cpp +++ b/src/scripting/floating_image.cpp @@ -24,7 +24,7 @@ namespace scripting { FloatingImage::FloatingImage(const std::string& spritefile) : - GameObject(get_game_object_manager().add<::FloatingImage>(spritefile).get_uid(), get_game_object_manager()) + GameObject(get_sector().add<::FloatingImage>(spritefile).get_uid(), get_sector()) { } diff --git a/src/scripting/game_object.cpp b/src/scripting/game_object.cpp index bbc368601c4..05ff99ac6d4 100644 --- a/src/scripting/game_object.cpp +++ b/src/scripting/game_object.cpp @@ -17,16 +17,16 @@ #include "scripting/game_object.hpp" -#include "supertux/d_scope.hpp" +#include "supertux/sector.hpp" namespace scripting { -::GameObjectManager& get_game_object_manager() +::Sector& get_sector() { - if (d_gameobject_manager) - return *d_gameobject_manager.get(); + if (::Sector::current()) + return ::Sector::get(); - throw std::runtime_error("Unable to perform scripting GameObject action: No active GameObjectManager."); + throw std::runtime_error("Unable to perform scripting action: No active Sector."); } } // namespace scripting diff --git a/src/scripting/game_object.hpp b/src/scripting/game_object.hpp index fd697406dbd..bb4adf9ffe3 100644 --- a/src/scripting/game_object.hpp +++ b/src/scripting/game_object.hpp @@ -1,5 +1,6 @@ // SuperTux // Copyright (C) 2018 Ingo Ruhnke +// 2023 Vankata453 // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -77,11 +78,11 @@ } \ auto& object = *object_ptr -class GameObjectManager; +class Sector; namespace scripting { -::GameObjectManager& get_game_object_manager(); +::Sector& get_sector(); template class GameObject From c3cb7f9a24a5eed2cf1118da0e2c823157aa275a Mon Sep 17 00:00:00 2001 From: Vankata453 <78196474+Vankata453@users.noreply.github.com> Date: Fri, 21 Jul 2023 18:37:37 +0300 Subject: [PATCH 24/50] Pass only the main `GameObject` to `scripting::GameObject`'s constructor Instead of passing the main `GameObject`'s UID and parent `GameObjectManager` separately to the constructor of `scripting::GameObject`, now only the main `GameObject` itself is passed, and `scripting::GameObject`'s constructor gets its required values from that object. --- src/scripting/badguy.hpp | 4 ++-- src/scripting/dispenser.hpp | 8 ++++---- src/scripting/floating_image.cpp | 2 +- src/scripting/game_object.hpp | 10 +++++++--- src/scripting/willowisp.hpp | 8 ++++---- src/squirrel/exposed_object.hpp | 2 +- 6 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/scripting/badguy.hpp b/src/scripting/badguy.hpp index b0410515448..c33a0a7b197 100644 --- a/src/scripting/badguy.hpp +++ b/src/scripting/badguy.hpp @@ -39,8 +39,8 @@ class BadGuy { #ifndef SCRIPTING_API public: - BadGuy(UID uid, ::GameObjectManager& parent) : - GameObject<::BadGuy>(uid, parent) + BadGuy(const ::GameObject& object) : + GameObject<::BadGuy>(object) {} private: diff --git a/src/scripting/dispenser.hpp b/src/scripting/dispenser.hpp index 445d4e0b804..c3c63456df7 100644 --- a/src/scripting/dispenser.hpp +++ b/src/scripting/dispenser.hpp @@ -38,10 +38,10 @@ class Dispenser final : public scripting::BadGuy { #ifndef SCRIPTING_API public: - Dispenser(UID uid, ::GameObjectManager& parent) : - GameObject<::BadGuy>(uid, parent), - GameObject<::Dispenser>(uid, parent), - BadGuy(uid, parent) + Dispenser(const ::GameObject& object) : + GameObject<::BadGuy>(object), + GameObject<::Dispenser>(object), + BadGuy(object) {} private: diff --git a/src/scripting/floating_image.cpp b/src/scripting/floating_image.cpp index 436309a2ea7..1ce30c23fc0 100644 --- a/src/scripting/floating_image.cpp +++ b/src/scripting/floating_image.cpp @@ -24,7 +24,7 @@ namespace scripting { FloatingImage::FloatingImage(const std::string& spritefile) : - GameObject(get_sector().add<::FloatingImage>(spritefile).get_uid(), get_sector()) + GameObject(get_sector().add<::FloatingImage>(spritefile)) { } diff --git a/src/scripting/game_object.hpp b/src/scripting/game_object.hpp index bb4adf9ffe3..3c17b0342f6 100644 --- a/src/scripting/game_object.hpp +++ b/src/scripting/game_object.hpp @@ -88,9 +88,9 @@ template class GameObject { public: - GameObject(UID uid, ::GameObjectManager& parent) : - m_uid(uid), - m_parent(parent) + GameObject(const ::GameObject& object) : + m_uid(object.get_uid()), + m_parent(*object.get_parent()) {} T* get_object_ptr() const @@ -101,6 +101,10 @@ class GameObject protected: UID m_uid; ::GameObjectManager& m_parent; + +private: + GameObject(const GameObject&) = delete; + GameObject& operator=(const GameObject&) = delete; }; } // namespace scripting diff --git a/src/scripting/willowisp.hpp b/src/scripting/willowisp.hpp index 033f2d32fdb..f1b7d4d25cb 100644 --- a/src/scripting/willowisp.hpp +++ b/src/scripting/willowisp.hpp @@ -40,10 +40,10 @@ class WillOWisp final : public scripting::BadGuy { #ifndef SCRIPTING_API public: - WillOWisp(UID uid, ::GameObjectManager& parent) : - GameObject<::BadGuy>(uid, parent), - GameObject<::WillOWisp>(uid, parent), - BadGuy(uid, parent) + WillOWisp(const ::GameObject& object) : + GameObject<::BadGuy>(object), + GameObject<::WillOWisp>(object), + BadGuy(object) {} private: diff --git a/src/squirrel/exposed_object.hpp b/src/squirrel/exposed_object.hpp index 08419c6437d..ad28bb8fd28 100644 --- a/src/squirrel/exposed_object.hpp +++ b/src/squirrel/exposed_object.hpp @@ -75,7 +75,7 @@ class ExposedObject : virtual public ScriptInterface log_debug << "Exposing " << m_parent->get_class_name() << " object " << name << std::endl; - auto object = std::make_unique(m_parent->get_uid(), *m_parent->get_parent()); + auto object = std::make_unique(*m_parent); expose_object(vm, table_idx, std::move(object), name); } From 2ff06d486e4c2aac09159763012a15dd8a87f54b Mon Sep 17 00:00:00 2001 From: Vankata453 <78196474+Vankata453@users.noreply.github.com> Date: Fri, 21 Jul 2023 18:56:50 +0300 Subject: [PATCH 25/50] Remove warnings, used for debugging purposes in "cheat_menu.cpp" [ci skip] --- src/supertux/menu/cheat_menu.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/supertux/menu/cheat_menu.cpp b/src/supertux/menu/cheat_menu.cpp index b5ff1c469b2..17c2461ba69 100644 --- a/src/supertux/menu/cheat_menu.cpp +++ b/src/supertux/menu/cheat_menu.cpp @@ -81,7 +81,6 @@ CheatMenu::menu_action(MenuItem& item) case MNID_FIRE: MenuManager::instance().push_menu(std::make_unique([](Player& player, int count){ - log_warning << player.get_id() << std::endl; player.set_bonus(FIRE_BONUS); player.get_status().max_fire_bullets[player.get_id()] = count; })); @@ -89,7 +88,6 @@ CheatMenu::menu_action(MenuItem& item) case MNID_ICE: MenuManager::instance().push_menu(std::make_unique([](Player& player, int count){ - log_warning << player.get_id() << std::endl; player.set_bonus(ICE_BONUS); player.get_status().max_ice_bullets[player.get_id()] = count; })); @@ -97,7 +95,6 @@ CheatMenu::menu_action(MenuItem& item) case MNID_AIR: MenuManager::instance().push_menu(std::make_unique([](Player& player, int count){ - log_warning << player.get_id() << std::endl; player.set_bonus(AIR_BONUS); player.get_status().max_air_time[player.get_id()] = count; })); @@ -105,7 +102,6 @@ CheatMenu::menu_action(MenuItem& item) case MNID_EARTH: MenuManager::instance().push_menu(std::make_unique([](Player& player, int count){ - log_warning << player.get_id() << std::endl; player.set_bonus(EARTH_BONUS); player.get_status().max_earth_time[player.get_id()] = count; })); From f9e73dae25bc06231385131c364c9b5bf2653571 Mon Sep 17 00:00:00 2001 From: MatusGuy <85036874+MatusGuy@users.noreply.github.com> Date: Fri, 21 Jul 2023 17:07:09 +0100 Subject: [PATCH 26/50] Ability to open debug menu in editor (#2537) --- src/editor/editor.cpp | 13 ++++++++----- src/supertux/menu/debug_menu.cpp | 9 +++++++++ src/supertux/menu/debug_menu.hpp | 1 + 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/editor/editor.cpp b/src/editor/editor.cpp index e1167993656..066996786f4 100644 --- a/src/editor/editor.cpp +++ b/src/editor/editor.cpp @@ -404,13 +404,19 @@ Editor::update_keyboard(const Controller& controller) return; } - - if (!MenuManager::instance().has_dialog()) + if (MenuManager::instance().current_menu() == nullptr) { if (controller.pressed(Control::ESCAPE)) { esc_press(); return; } + if (controller.pressed(Control::DEBUG_MENU) && g_config->developer_mode) + { + m_enabled = false; + m_overlay_widget->delete_markers(); + MenuManager::instance().set_menu(MenuStorage::DEBUG_MENU); + return; + } if (controller.hold(Control::LEFT)) { scroll({ -m_scroll_speed, 0.0f }); } @@ -682,7 +688,6 @@ Editor::setup() m_enabled = true; m_toolbox_widget->update_mouse_icon(); } - } void @@ -741,8 +746,6 @@ Editor::event(const SDL_Event& ev) } } - - if (ev.type == SDL_KEYDOWN && ev.key.keysym.sym == SDLK_F6) { Compositor::s_render_lighting = !Compositor::s_render_lighting; return; diff --git a/src/supertux/menu/debug_menu.cpp b/src/supertux/menu/debug_menu.cpp index 5af92dfe0cd..44c3401bc92 100644 --- a/src/supertux/menu/debug_menu.cpp +++ b/src/supertux/menu/debug_menu.cpp @@ -19,6 +19,7 @@ #include #include +#include "editor/editor.hpp" #include "gui/item_stringselect.hpp" #include "supertux/debug.hpp" #include "supertux/gameconfig.hpp" @@ -76,6 +77,14 @@ DebugMenu::DebugMenu() : add_back(_("Back")); } +DebugMenu::~DebugMenu() +{ + auto editor = Editor::current(); + + if (editor == nullptr) return; + editor->m_reactivate_request = true; +} + void DebugMenu::menu_action(MenuItem& item) { diff --git a/src/supertux/menu/debug_menu.hpp b/src/supertux/menu/debug_menu.hpp index 7b075050680..e8a98a90c7b 100644 --- a/src/supertux/menu/debug_menu.hpp +++ b/src/supertux/menu/debug_menu.hpp @@ -30,6 +30,7 @@ class DebugMenu final : public Menu public: DebugMenu(); + virtual ~DebugMenu() override; virtual void menu_action(MenuItem& item) override; From b132c25f88afea5dad9c53281977e1c67d643125 Mon Sep 17 00:00:00 2001 From: Vankata453 <78196474+Vankata453@users.noreply.github.com> Date: Fri, 21 Jul 2023 19:08:31 +0300 Subject: [PATCH 27/50] Fix worldmap sector spawnpoint issues, testing from worldmap spawnpoints (#2547) Fixes issues, related to loading a worldmap with a forced spawnpoint. It can now be set both with `set_initial_spawnpoint` on `WorldMap`, and by providing it to the initializer of the class. The forced spawnpoint overrides the saved Tux position state on the worldmap sector. This PR also adds the ability to initialize a worldmap with a forced sector, allowing for teleporting on a different sector, other than "main", on a different worldmap. Another addition is the "Test from here" option for worldmap spawnpoints. The worldmap will be tested from the current sector, Tux being on the position of the chosen spawnpoint to test from. --- src/editor/editor.cpp | 2 +- src/supertux/game_manager.cpp | 15 +++++++++++++-- src/supertux/game_manager.hpp | 5 ++++- src/supertux/game_session.cpp | 2 +- src/worldmap/spawn_point.cpp | 2 ++ src/worldmap/tux.cpp | 7 ++++++- src/worldmap/tux.hpp | 4 +++- src/worldmap/world_select.cpp | 3 +-- src/worldmap/worldmap.cpp | 28 +++++++++++++++++++++++++--- src/worldmap/worldmap.hpp | 11 +++++++++-- src/worldmap/worldmap_sector.cpp | 25 +++---------------------- src/worldmap/worldmap_sector.hpp | 6 ------ src/worldmap/worldmap_state.cpp | 3 +-- 13 files changed, 69 insertions(+), 44 deletions(-) diff --git a/src/editor/editor.cpp b/src/editor/editor.cpp index 066996786f4..9e3c4a746c9 100644 --- a/src/editor/editor.cpp +++ b/src/editor/editor.cpp @@ -341,7 +341,7 @@ Editor::test_level(const std::optional>& test_pos } else { - GameManager::current()->start_worldmap(*current_world, "", m_autosave_levelfile); + GameManager::current()->start_worldmap(*current_world, m_autosave_levelfile, test_pos); } } diff --git a/src/supertux/game_manager.cpp b/src/supertux/game_manager.cpp index a145ac76c55..370fdfb143c 100644 --- a/src/supertux/game_manager.cpp +++ b/src/supertux/game_manager.cpp @@ -28,6 +28,7 @@ #include "util/reader.hpp" #include "util/reader_document.hpp" #include "util/reader_mapping.hpp" +#include "worldmap/tux.hpp" #include "worldmap/worldmap.hpp" #include "worldmap/worldmap_screen.hpp" @@ -52,7 +53,8 @@ GameManager::start_level(const World& world, const std::string& level_filename, } void -GameManager::start_worldmap(const World& world, const std::string& spawnpoint, const std::string& worldmap_filename) +GameManager::start_worldmap(const World& world, const std::string& worldmap_filename, + const std::string& sector, const std::string& spawnpoint) { try { @@ -74,7 +76,7 @@ GameManager::start_worldmap(const World& world, const std::string& spawnpoint, c filename = world.get_worldmap_filename(); } - auto worldmap = std::make_unique(filename, *m_savegame, spawnpoint); + auto worldmap = std::make_unique(filename, *m_savegame, sector, spawnpoint); auto worldmap_screen = std::make_unique(std::move(worldmap)); ScreenManager::current()->push_screen(std::move(worldmap_screen)); } @@ -84,6 +86,15 @@ GameManager::start_worldmap(const World& world, const std::string& spawnpoint, c } } +void +GameManager::start_worldmap(const World& world, const std::string& worldmap_filename, + const std::optional>& start_pos) +{ + start_worldmap(world, worldmap_filename, start_pos ? start_pos->first : ""); + if (start_pos) + worldmap::WorldMapSector::current()->get_tux().set_initial_pos(start_pos->second); +} + bool GameManager::load_next_worldmap() { diff --git a/src/supertux/game_manager.hpp b/src/supertux/game_manager.hpp index 9be381f48b7..edc4baf64b6 100644 --- a/src/supertux/game_manager.hpp +++ b/src/supertux/game_manager.hpp @@ -31,7 +31,10 @@ class GameManager final : public Currenton public: GameManager(); - void start_worldmap(const World& world, const std::string& spawnpoint = "", const std::string& worldmap_filename = ""); + void start_worldmap(const World& world, const std::string& worldmap_filename = "", + const std::string& sector = "", const std::string& spawnpoint = ""); + void start_worldmap(const World& world, const std::string& worldmap_filename, + const std::optional>& start_pos); void start_level(const World& world, const std::string& level_filename, const std::optional>& start_pos = std::nullopt); diff --git a/src/supertux/game_session.cpp b/src/supertux/game_session.cpp index 919a5dba5de..1b8ec5af3a9 100644 --- a/src/supertux/game_session.cpp +++ b/src/supertux/game_session.cpp @@ -805,7 +805,7 @@ GameSession::start_sequence(Player* caller, Sequence seq, const SequenceData* da } if (!data->spawnpoint.empty()) { - worldmap_sector->set_initial_spawnpoint(data->spawnpoint); + worldmap_sector->get_worldmap().set_initial_spawnpoint(data->spawnpoint); } } } diff --git a/src/worldmap/spawn_point.cpp b/src/worldmap/spawn_point.cpp index 964b4dd6246..a00828b4e77 100644 --- a/src/worldmap/spawn_point.cpp +++ b/src/worldmap/spawn_point.cpp @@ -80,6 +80,8 @@ SpawnPointObject::get_settings() result.reorder({"auto-dir", "name", "x", "y"}); + result.add_test_from_here(); + return result; } diff --git a/src/worldmap/tux.cpp b/src/worldmap/tux.cpp index 5f2b82a67e9..383c5bd3297 100644 --- a/src/worldmap/tux.cpp +++ b/src/worldmap/tux.cpp @@ -46,7 +46,8 @@ Tux::Tux(WorldMap* worldmap) : m_controller(InputManager::current()->get_controller()), m_input_direction(Direction::NONE), m_direction(Direction::NONE), - m_tile_pos(0.0f, 0.0f), + m_initial_tile_pos(), + m_tile_pos(), m_offset(0), m_moving(false), m_ghost_mode(false) @@ -371,6 +372,10 @@ Tux::update(float dt_sec) void Tux::setup() { + // Set initial tile position, if provided + if (m_initial_tile_pos != Vector()) + m_tile_pos = m_initial_tile_pos; + // check if we already touch a SpriteChange object auto sprite_change = m_worldmap->get_sector().at_object(m_tile_pos); change_sprite(sprite_change); diff --git a/src/worldmap/tux.hpp b/src/worldmap/tux.hpp index 7fd559ecb51..60031875974 100644 --- a/src/worldmap/tux.hpp +++ b/src/worldmap/tux.hpp @@ -51,7 +51,8 @@ class Tux final : public GameObject Vector get_pos() const; Vector get_axis() const; Vector get_tile_pos() const { return m_tile_pos; } - void set_tile_pos(const Vector& p) { m_tile_pos = p; } + void set_initial_pos(const Vector& pos) { m_initial_tile_pos = pos / 32.f; } + void set_tile_pos(const Vector& pos) { m_tile_pos = pos; } void process_special_tile(SpecialTile* special_tile); @@ -75,6 +76,7 @@ class Tux final : public GameObject Direction m_input_direction; Direction m_direction; + Vector m_initial_tile_pos; Vector m_tile_pos; /** Length by which tux is away from its current tile, length is in input_direction direction */ diff --git a/src/worldmap/world_select.cpp b/src/worldmap/world_select.cpp index 2d5dd30fd26..fa9b2492787 100644 --- a/src/worldmap/world_select.cpp +++ b/src/worldmap/world_select.cpp @@ -247,8 +247,7 @@ WorldSelect::update(float dt_sec, const Controller& controller) if (controller.pressed(Control::JUMP) && m_worlds[m_selected_world].unlocked) { m_enabled = false; ScreenManager::current()->pop_screen(std::make_unique(FadeToBlack::Direction::FADEOUT, 0.25f)); - worldmap::WorldMap::current()->change(m_worlds[m_selected_world].filename, "main"); - log_warning << m_worlds[m_selected_world].filename << std::endl; + worldmap::WorldMap::current()->change(m_worlds[m_selected_world].filename, "", "main"); return; } } diff --git a/src/worldmap/worldmap.cpp b/src/worldmap/worldmap.cpp index 3549d8cd057..d8dd88aa761 100644 --- a/src/worldmap/worldmap.cpp +++ b/src/worldmap/worldmap.cpp @@ -45,9 +45,11 @@ namespace worldmap { -WorldMap::WorldMap(const std::string& filename, Savegame& savegame, const std::string& force_spawnpoint) : +WorldMap::WorldMap(const std::string& filename, Savegame& savegame, + const std::string& force_sector, const std::string& force_spawnpoint) : m_sector(), m_sectors(), + m_force_spawnpoint(force_spawnpoint), m_savegame(savegame), m_tileset(), m_name(), @@ -86,6 +88,10 @@ WorldMap::WorldMap(const std::string& filename, Savegame& savegame, const std::s if (iter.get_key() == "sector") add_sector(WorldMapSectorParser::from_reader(*this, iter.as_mapping())); } + + /** Force the initial sector, if provided */ + if (!force_sector.empty()) + set_sector(force_sector, "", false); } @@ -97,6 +103,13 @@ WorldMap::setup() load_state(); m_sector->setup(); + /** Force the initial spawnpoint, if provided */ + if (!m_force_spawnpoint.empty()) + { + m_sector->move_to_spawnpoint(m_force_spawnpoint); + m_force_spawnpoint.clear(); + } + m_in_world_select = false; } @@ -230,10 +243,11 @@ WorldMap::save_state() void -WorldMap::change(const std::string& filename, const std::string& force_spawnpoint_) +WorldMap::change(const std::string& filename, const std::string& force_sector, + const std::string& force_spawnpoint) { // Schedule worldmap to be changed to next frame. - m_next_worldmap = std::make_unique(filename, m_savegame, force_spawnpoint_); + m_next_worldmap = std::make_unique(filename, m_savegame, force_sector, force_spawnpoint); } @@ -254,6 +268,12 @@ WorldMap::set_passive_message(const std::string& message, float time) m_passive_message_timer.start(time); } +void +WorldMap::set_initial_spawnpoint(const std::string& spawnpoint) +{ + m_force_spawnpoint = spawnpoint; +} + WorldMapSector* WorldMap::get_sector(const std::string& name) const @@ -300,6 +320,8 @@ WorldMap::set_sector(const std::string& name, const std::string& spawnpoint, m_sector = get_sector(0); // In that case, assign the first sector. } + m_sector->move_to_spawnpoint("main"); + // Set up the new sector. if (perform_full_setup) load_state(); diff --git a/src/worldmap/worldmap.hpp b/src/worldmap/worldmap.hpp index e7e791eb693..a5dd97c22f2 100644 --- a/src/worldmap/worldmap.hpp +++ b/src/worldmap/worldmap.hpp @@ -41,7 +41,8 @@ class WorldMap final : public Currenton static Color s_teleporter_message_color; public: - WorldMap(const std::string& filename, Savegame& savegame, const std::string& force_spawnpoint = ""); + WorldMap(const std::string& filename, Savegame& savegame, + const std::string& force_sector = "", const std::string& force_spawnpoint = ""); void setup(); void leave(); @@ -61,7 +62,8 @@ class WorldMap final : public Currenton /** switch to another worldmap. filename is relative to data root path */ - void change(const std::string& filename, const std::string& force_spawnpoint = ""); + void change(const std::string& filename, const std::string& force_sector = "", + const std::string& force_spawnpoint = ""); /** Mark all levels as solved or unsolved */ void set_levels_solved(bool solved, bool perfect); @@ -69,6 +71,9 @@ class WorldMap final : public Currenton /** Sets the passive message with specific time **/ void set_passive_message(const std::string& message, float time); + /** Sets the initial spawnpoint to be forced on next setup */ + void set_initial_spawnpoint(const std::string& spawnpoint); + const std::string& get_title() const { return m_name; } Savegame& get_savegame() const { return m_savegame; } const std::string& get_levels_path() const { return m_levels_path; } @@ -88,6 +93,8 @@ class WorldMap final : public Currenton WorldMapSector* m_sector; /* The currently active sector. */ std::vector > m_sectors; + std::string m_force_spawnpoint; + Savegame& m_savegame; TileSet* m_tileset; diff --git a/src/worldmap/worldmap_sector.cpp b/src/worldmap/worldmap_sector.cpp index e680bb94c36..c1f140e2c35 100644 --- a/src/worldmap/worldmap_sector.cpp +++ b/src/worldmap/worldmap_sector.cpp @@ -64,8 +64,6 @@ WorldMapSector::WorldMapSector(WorldMap& parent) : m_camera(new Camera), m_tux(&add(&parent)), m_spawnpoints(), - m_force_spawnpoint(), - m_main_is_default(true), m_initial_fade_tilemap(), m_fade_direction() { @@ -131,14 +129,6 @@ WorldMapSector::setup() m_squirrel_environment->expose_self(); m_squirrel_environment->expose("settings", std::make_unique(this)); - /** Force spawnpoint, if the property is set. **/ - if (!m_force_spawnpoint.empty()) - { - move_to_spawnpoint(m_force_spawnpoint, false); - m_force_spawnpoint.clear(); - m_main_is_default = true; - } - /** Perform scripting related actions. **/ // Run default.nut just before init script try @@ -290,7 +280,8 @@ WorldMapSector::update(float dt_sec) if (!teleporter->get_worldmap().empty()) { // Change worldmap. - m_parent.change(teleporter->get_worldmap(), teleporter->get_spawnpoint()); + m_parent.change(teleporter->get_worldmap(), teleporter->get_sector(), + teleporter->get_spawnpoint()); } else { @@ -555,7 +546,7 @@ WorldMapSector::move_to_spawnpoint(const std::string& spawnpoint, bool pan) } log_warning << "Spawnpoint '" << spawnpoint << "' not found." << std::endl; - if (spawnpoint != "main" && m_main_is_default) { + if (spawnpoint != "main") { move_to_spawnpoint("main"); } } @@ -567,16 +558,6 @@ WorldMapSector::set_initial_fade_tilemap(const std::string& tilemap_name, int di m_fade_direction = direction; } -void -WorldMapSector::set_initial_spawnpoint(const std::string& spawnpoint_name) -{ - m_force_spawnpoint = spawnpoint_name; - - // If spawnpoint we specified can not be found, - // don't bother moving to the main spawnpoint. - m_main_is_default = false; -} - bool WorldMapSector::before_object_add(GameObject& object) diff --git a/src/worldmap/worldmap_sector.hpp b/src/worldmap/worldmap_sector.hpp index 9627b5e4752..94019d0937a 100644 --- a/src/worldmap/worldmap_sector.hpp +++ b/src/worldmap/worldmap_sector.hpp @@ -105,9 +105,6 @@ class WorldMapSector final : public Base::Sector /** Sets the name of the tilemap that should fade when worldmap is set up. */ void set_initial_fade_tilemap(const std::string& tilemap_name, int direction); - /** Sets the initial spawnpoint on worldmap setup */ - void set_initial_spawnpoint(const std::string& spawnpoint_name); - bool in_worldmap() const override { return true; } TileSet* get_tileset() const override; @@ -127,11 +124,8 @@ class WorldMapSector final : public Base::Sector std::unique_ptr m_camera; Tux* m_tux; - std::vector > m_spawnpoints; - std::string m_force_spawnpoint; - bool m_main_is_default; std::string m_initial_fade_tilemap; int m_fade_direction; diff --git a/src/worldmap/worldmap_state.cpp b/src/worldmap/worldmap_state.cpp index 7490308450c..a9825f75995 100644 --- a/src/worldmap/worldmap_state.cpp +++ b/src/worldmap/worldmap_state.cpp @@ -101,7 +101,6 @@ WorldMapState::load_state() // Set default properties. if (!m_worldmap.m_sector) m_worldmap.set_sector("main", "", false); // If no current sector is present, set it to "main", or the default one. - m_worldmap.get_sector().move_to_spawnpoint("main"); // Move Tux to the "main" spawnpoint. // Create a new initial save. save_state(); @@ -147,7 +146,7 @@ WorldMapState::load_levels() WORLDMAP_STATE_SQUIRREL_VM_GUARD; WORLDMAP_STATE_SECTOR_GUARD; - vm.get_table_entry("levels"); + vm.get_or_create_table_entry("levels"); for (auto& level : sector.get_objects_by_type()) { sq_pushstring(vm.get_vm(), level.get_level_filename().c_str(), -1); if (SQ_SUCCEEDED(sq_get(vm.get_vm(), -2))) From 47f349f8e72dfd0862c966e52e6c5ff28d2c479e Mon Sep 17 00:00:00 2001 From: Vankata453 <78196474+Vankata453@users.noreply.github.com> Date: Fri, 21 Jul 2023 22:12:23 +0300 Subject: [PATCH 28/50] Fix sprite fallback issues (#2551) Previously, missing `.sprite` files, when attempted to be loaded, would immediately close the game, due to throwing fatal errors. The issue is now fixed. Such errors are caught, and the fallback/dummy texture takes the sprite's place. Other than `.sprite` file load errors being caught, single-image sprites also throw an error, which is later caught, when the chosen image fails to load. `MovingSprite`s now return to their default sprite, if a custom-set sprite fails to load. Fixes #2341. Closes #2250. --- src/object/moving_sprite.cpp | 34 +++++-------- src/sprite/sprite_data.cpp | 36 ++++++++++++++ src/sprite/sprite_data.hpp | 20 ++++++-- src/sprite/sprite_manager.cpp | 93 +++++++++++++++++++++-------------- src/sprite/sprite_manager.hpp | 16 +++++- src/video/texture_manager.cpp | 17 ++++--- src/video/texture_manager.hpp | 10 ++-- 7 files changed, 151 insertions(+), 75 deletions(-) diff --git a/src/object/moving_sprite.cpp b/src/object/moving_sprite.cpp index a34c5911628..ac8ac83aef9 100644 --- a/src/object/moving_sprite.cpp +++ b/src/object/moving_sprite.cpp @@ -67,13 +67,10 @@ MovingSprite::MovingSprite(const ReaderMapping& reader, const std::string& sprit m_sprite = SpriteManager::current()->create(m_default_sprite_name); m_sprite_found = false; } - else + else if (!change_sprite(m_sprite_name)) // If sprite change fails, change back to default. { - if (!change_sprite(m_sprite_name)) // If sprite change fails, change back to default. - { - m_sprite = SpriteManager::current()->create(m_default_sprite_name); - m_sprite_found = false; - } + change_sprite(m_default_sprite_name); + m_sprite_found = false; } update_hitbox(); @@ -91,10 +88,8 @@ MovingSprite::MovingSprite(const ReaderMapping& reader, int layer_, CollisionGro { reader.get("x", m_col.m_bbox.get_left()); reader.get("y", m_col.m_bbox.get_top()); - if (!reader.get("sprite", m_sprite_name)) - throw std::runtime_error("no sprite name set"); + m_sprite_found = reader.get("sprite", m_sprite_name); - m_sprite_found = true; //m_default_sprite_name = m_sprite_name; m_sprite = SpriteManager::current()->create(m_sprite_name); update_hitbox(); @@ -182,20 +177,10 @@ MovingSprite::set_action(const std::string& action, int loops, AnchorPoint ancho bool MovingSprite::change_sprite(const std::string& new_sprite_name) { - SpritePtr new_sprite; - try - { - new_sprite = SpriteManager::current()->create(m_sprite_name); - } - catch (std::exception& err) - { - log_warning << "Sprite change failed: Sprite '" << new_sprite_name << "' cannot be loaded: " << err.what() << std::endl; - return false; - } - - m_sprite = std::move(new_sprite); + m_sprite = SpriteManager::current()->create(new_sprite_name); m_sprite_name = new_sprite_name; - return true; + + return SpriteManager::current()->last_load_successful(); } ObjectSettings @@ -216,7 +201,10 @@ MovingSprite::after_editor_set() MovingObject::after_editor_set(); std::string current_action = m_sprite->get_action(); - m_sprite = SpriteManager::current()->create(m_sprite_name); + if (!change_sprite(m_sprite_name)) // If sprite change fails, change back to default. + { + change_sprite(m_default_sprite_name); + } m_sprite->set_action(current_action); update_hitbox(); diff --git a/src/sprite/sprite_data.cpp b/src/sprite/sprite_data.cpp index debdcbafaaa..15935aecdff 100644 --- a/src/sprite/sprite_data.cpp +++ b/src/sprite/sprite_data.cpp @@ -27,6 +27,7 @@ #include "util/reader_mapping.hpp" #include "util/reader_object.hpp" #include "video/surface.hpp" +#include "video/texture_manager.hpp" SpriteData::Action::Action() : name(), @@ -61,6 +62,41 @@ SpriteData::SpriteData(const ReaderMapping& mapping) : throw std::runtime_error("Error: Sprite without actions."); } +SpriteData::SpriteData(const std::string& image) : + actions(), + name() +{ + auto surface = Surface::from_file(image); + if (!TextureManager::current()->last_load_successful()) + throw std::runtime_error("Cannot load image."); + + auto action = create_action_from_surface(surface); + action->name = "default"; + actions[action->name] = std::move(action); +} + +SpriteData::SpriteData() : + actions(), + name() +{ + auto surface = Surface::from_texture(TextureManager::current()->create_dummy_texture()); + auto action = create_action_from_surface(surface); + action->name = "default"; + actions[action->name] = std::move(action); +} + +std::unique_ptr +SpriteData::create_action_from_surface(SurfacePtr surface) +{ + auto action = std::make_unique(); + + action->hitbox_w = static_cast(surface->get_width()); + action->hitbox_h = static_cast(surface->get_height()); + action->surfaces.push_back(surface); + + return action; +} + void SpriteData::parse_action(const ReaderMapping& mapping) { diff --git a/src/sprite/sprite_data.hpp b/src/sprite/sprite_data.hpp index 3d2e140cf9f..30e6d0cfec6 100644 --- a/src/sprite/sprite_data.hpp +++ b/src/sprite/sprite_data.hpp @@ -27,9 +27,18 @@ class ReaderMapping; class SpriteData final { + friend class Sprite; + public: - /** cur has to be a pointer to data in the form of ((hitbox 5 10 0 0) ...) */ - SpriteData(const ReaderMapping& cur); + /** + * Sprite from data. + * `mapping` has to be a pointer to data in the form of "((hitbox 5 10 0 0) ...)". + */ + SpriteData(const ReaderMapping& mapping); + /** Single-image sprite */ + SpriteData(const std::string& image); + /** Dummy texture sprite */ + SpriteData(); const std::string& get_name() const { @@ -37,8 +46,6 @@ class SpriteData final } private: - friend class Sprite; - struct Action { Action(); @@ -77,12 +84,15 @@ class SpriteData final std::vector surfaces; }; - typedef std::map > Actions; + typedef std::map > Actions; + + static std::unique_ptr create_action_from_surface(SurfacePtr surface); void parse_action(const ReaderMapping& mapping); /** Get an action */ const Action* get_action(const std::string& act) const; +private: Actions actions; std::string name; }; diff --git a/src/sprite/sprite_manager.cpp b/src/sprite/sprite_manager.cpp index a1bf50d0b88..db21bd7b5f7 100644 --- a/src/sprite/sprite_manager.cpp +++ b/src/sprite/sprite_manager.cpp @@ -1,5 +1,6 @@ // SuperTux // Copyright (C) 2006 Matthias Braun +// 2023 Vankata453 // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -16,73 +17,93 @@ #include "sprite/sprite_manager.hpp" +#include +#include + #include "sprite/sprite.hpp" #include "util/file_system.hpp" +#include "util/log.hpp" #include "util/reader_document.hpp" #include "util/reader_mapping.hpp" #include "util/string_util.hpp" -#include +std::unique_ptr SpriteManager::s_dummy_sprite_data = nullptr; SpriteManager::SpriteManager() : - sprites() + m_sprites(), + m_load_successful(false) { + if (!s_dummy_sprite_data) + s_dummy_sprite_data.reset(new SpriteData()); } SpritePtr SpriteManager::create(const std::string& name) { - Sprites::iterator i = sprites.find(name); + Sprites::iterator i = m_sprites.find(name); SpriteData* data; - if (i == sprites.end()) { - // try loading the spritefile - data = load(name); - if (data == nullptr) { - std::stringstream msg; - msg << "Sprite '" << name << "' not found."; - throw std::runtime_error(msg.str()); + if (i == m_sprites.end()) + { + // Try loading the sprite file. + try + { + data = load(name); } - } else { + catch (const std::exception& err) + { + log_warning << "Error loading sprite '" << name << "', using dummy texture: " << err.what() << std::endl; + m_load_successful = false; + return SpritePtr(new Sprite(*s_dummy_sprite_data)); // Return a dummy sprite. + } + } + else + { data = i->second.get(); } + m_load_successful = true; return SpritePtr(new Sprite(*data)); } SpriteData* SpriteManager::load(const std::string& filename) { - ReaderDocument doc = [filename](){ - try { - if (StringUtil::has_suffix(filename, ".sprite")) { - return ReaderDocument::from_file(filename); - } else { - std::stringstream text; - text << "(supertux-sprite (action " - << "(name \"default\") " - << "(images \"" << FileSystem::basename(filename) << "\")))"; - return ReaderDocument::from_stream(text, filename); - } - } catch(const std::exception& e) { + std::unique_ptr sprite_data; + + if (StringUtil::has_suffix(filename, ".sprite")) + { + std::optional doc; + try + { + doc = ReaderDocument::from_file(filename); + } + catch (const std::exception& err) + { std::ostringstream msg; msg << "Parse error when trying to load sprite '" << filename - << "': " << e.what() << "\n"; + << "': " << err.what(); throw std::runtime_error(msg.str()); } - }(); + auto root = doc->get_root(); - auto root = doc.get_root(); - - if (root.get_name() != "supertux-sprite") { - std::ostringstream msg; - msg << "'" << filename << "' is not a supertux-sprite file"; - throw std::runtime_error(msg.str()); - } else { - auto data = std::make_unique(root.get_mapping()); - sprites[filename] = std::move(data); - - return sprites[filename].get(); + if (root.get_name() != "supertux-sprite") + { + std::ostringstream msg; + msg << "'" << filename << "' is not a supertux-sprite file"; + throw std::runtime_error(msg.str()); + } + else + { + sprite_data = std::make_unique(root.get_mapping()); + } } + else + { + sprite_data = std::make_unique(filename); + } + + m_sprites[filename] = std::move(sprite_data); + return m_sprites[filename].get(); } /* EOF */ diff --git a/src/sprite/sprite_manager.hpp b/src/sprite/sprite_manager.hpp index f6aed1a573f..381e04f96a8 100644 --- a/src/sprite/sprite_manager.hpp +++ b/src/sprite/sprite_manager.hpp @@ -1,5 +1,6 @@ // SuperTux // Copyright (C) 2006 Matthias Braun +// 2023 Vankata453 // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -17,29 +18,40 @@ #ifndef HEADER_SUPERTUX_SPRITE_SPRITE_MANAGER_HPP #define HEADER_SUPERTUX_SPRITE_SPRITE_MANAGER_HPP +#include "util/currenton.hpp" + #include #include #include #include "sprite/sprite_ptr.hpp" -#include "util/currenton.hpp" class SpriteData; class SpriteManager final : public Currenton { +private: + static std::unique_ptr s_dummy_sprite_data; + private: typedef std::map > Sprites; - Sprites sprites; + Sprites m_sprites; + bool m_load_successful; public: SpriteManager(); + bool last_load_successful() const { return m_load_successful; } + /** loads a sprite. */ SpritePtr create(const std::string& filename); private: SpriteData* load(const std::string& filename); + +private: + SpriteManager(const SpriteManager&) = delete; + SpriteManager& operator=(const SpriteManager&) = delete; }; #endif diff --git a/src/video/texture_manager.cpp b/src/video/texture_manager.cpp index 1b950bdc715..a9849659a16 100644 --- a/src/video/texture_manager.cpp +++ b/src/video/texture_manager.cpp @@ -75,9 +75,12 @@ GLenum string2filter(const std::string& text) } // namespace +const std::string TextureManager::s_dummy_texture = "images/engine/missing.png"; + TextureManager::TextureManager() : m_image_textures(), - m_surfaces() + m_surfaces(), + m_load_successful(false) { } @@ -256,6 +259,7 @@ TextureManager::reap_cache_entry(const Texture::Key& key) TexturePtr TextureManager::create_image_texture(const std::string& filename, const Rect& rect, const Sampler& sampler) { + m_load_successful = true; try { return create_image_texture_raw(filename, rect, sampler); @@ -263,6 +267,7 @@ TextureManager::create_image_texture(const std::string& filename, const Rect& re catch(const std::exception& err) { log_warning << "Couldn't load texture '" << filename << "' (now using dummy texture): " << err.what() << std::endl; + m_load_successful = false; return create_dummy_texture(); } } @@ -355,6 +360,7 @@ TextureManager::create_image_texture_raw(const std::string& filename, const Rect TexturePtr TextureManager::create_image_texture(const std::string& filename, const Sampler& sampler) { + m_load_successful = true; try { return create_image_texture_raw(filename, sampler); @@ -362,6 +368,7 @@ TextureManager::create_image_texture(const std::string& filename, const Sampler& catch (const std::exception& err) { log_warning << "Couldn't load texture '" << filename << "' (now using dummy texture): " << err.what() << std::endl; + m_load_successful = false; return create_dummy_texture(); } } @@ -387,26 +394,24 @@ TextureManager::create_image_texture_raw(const std::string& filename, const Samp TexturePtr TextureManager::create_dummy_texture() { - const std::string dummy_texture_fname = "images/engine/missing.png"; - // on error, try loading placeholder file try { - TexturePtr tex = create_image_texture_raw(dummy_texture_fname, Sampler()); + TexturePtr tex = create_image_texture_raw(s_dummy_texture, Sampler()); return tex; } catch (const std::exception& err) { // on error (when loading placeholder), try using empty surface, // when that fails to, just give up - SDLSurfacePtr image(SDL_CreateRGBSurface(0, 1024, 1024, 8, 0, 0, 0, 0)); + SDLSurfacePtr image(SDL_CreateRGBSurface(0, 128, 128, 8, 0, 0, 0, 0)); if (!image) { throw; } else { - log_warning << "Couldn't load texture '" << dummy_texture_fname << "' (now using empty one): " << err.what() << std::endl; + log_warning << "Couldn't load texture '" << s_dummy_texture << "' (now using empty one): " << err.what() << std::endl; TexturePtr texture = VideoSystem::current()->new_texture(*image); return texture; } diff --git a/src/video/texture_manager.hpp b/src/video/texture_manager.hpp index 1b7afb976c9..73c80930ee9 100644 --- a/src/video/texture_manager.hpp +++ b/src/video/texture_manager.hpp @@ -39,9 +39,11 @@ struct SDL_Surface; class TextureManager final : public Currenton { -public: friend class Texture; +private: + static const std::string s_dummy_texture; + public: TextureManager(); ~TextureManager() override; @@ -51,9 +53,12 @@ class TextureManager final : public Currenton TexturePtr get(const std::string& filename, const std::optional& rect, const Sampler& sampler = Sampler()); + TexturePtr create_dummy_texture(); void debug_print(std::ostream& out) const; + bool last_load_successful() const { return m_load_successful; } + private: const SDL_Surface& get_surface(const std::string& filename); void reap_cache_entry(const Texture::Key& key); @@ -67,11 +72,10 @@ class TextureManager final : public Currenton TexturePtr create_image_texture_raw(const std::string& filename, const Sampler& sampler); TexturePtr create_image_texture_raw(const std::string& filename, const Rect& rect, const Sampler& sampler); - TexturePtr create_dummy_texture(); - private: std::map > m_image_textures; std::map m_surfaces; + bool m_load_successful; private: TextureManager(const TextureManager&) = delete; From 1720b77237b1ba12b7dfa87a0a649e02a8ec6551 Mon Sep 17 00:00:00 2001 From: HybridDog <3192173+HybridDog@users.noreply.github.com> Date: Sat, 22 Jul 2023 21:05:38 +0200 Subject: [PATCH 29/50] Code style changes to satisfy CppCheck (#2553) * `heavy_coin` was of type `HeavyCoin * const` and not `const HeavyCoin *`. I've added asterisks to the other `auto`s for pointer types, too, for consistency. * void pointer arithmetic has undefined behaviour, so the return value of a unique_ptr's get method call in `StreamSoundSource::fillBufferAndQueue` needs to be converted to char*. --- src/audio/stream_sound_source.cpp | 4 ++-- src/badguy/crusher.cpp | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/audio/stream_sound_source.cpp b/src/audio/stream_sound_source.cpp index ee8a5e3cc59..2297a81c2d9 100644 --- a/src/audio/stream_sound_source.cpp +++ b/src/audio/stream_sound_source.cpp @@ -138,8 +138,8 @@ StreamSoundSource::fillBufferAndQueue(ALuint buffer) std::unique_ptr bufferdata(new char[STREAMFRAGMENTSIZE]); size_t bytesread = 0; do { - bytesread += m_file->read(bufferdata.get() + bytesread, - STREAMFRAGMENTSIZE - bytesread); + bytesread += m_file->read(static_cast(bufferdata.get()) + bytesread, + STREAMFRAGMENTSIZE - bytesread); // end of sound file if (bytesread < STREAMFRAGMENTSIZE) { if (m_looping) diff --git a/src/badguy/crusher.cpp b/src/badguy/crusher.cpp index c7af75e8544..7193dd7b8be 100644 --- a/src/badguy/crusher.cpp +++ b/src/badguy/crusher.cpp @@ -96,7 +96,7 @@ Crusher::on_type_change(int old_type) HitResponse Crusher::collision(GameObject& other, const CollisionHit& hit) { - auto player = dynamic_cast(&other); + auto* player = dynamic_cast(&other); // If the other object is the player, and the collision is at the // bottom of the crusher, hurt the player. @@ -110,12 +110,12 @@ Crusher::collision(GameObject& other, const CollisionHit& hit) return FORCE_MOVE; } - auto badguy = dynamic_cast(&other); + auto* badguy = dynamic_cast(&other); if (badguy && m_state == CRUSHING) { badguy->kill_fall(); } - const auto heavy_coin = dynamic_cast(&other); + const auto* heavy_coin = dynamic_cast(&other); if (heavy_coin) { return ABORT_MOVE; } From 7e8f0bab222e107046601cf2b2876aa7942ce803 Mon Sep 17 00:00:00 2001 From: MatusGuy <85036874+MatusGuy@users.noreply.github.com> Date: Sat, 22 Jul 2023 20:58:06 +0100 Subject: [PATCH 30/50] Allow placing tiles in edges with rect fill tool (#2555) Placing tiles in edges with rect fill tool just won't work for some reason. This fixes that. --- src/editor/overlay_widget.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/editor/overlay_widget.cpp b/src/editor/overlay_widget.cpp index 310b107ba16..636eff6cbf3 100644 --- a/src/editor/overlay_widget.cpp +++ b/src/editor/overlay_widget.cpp @@ -1616,9 +1616,9 @@ EditorOverlayWidget::align_to_tilemap(const Vector& sp, int tile_size) const bool EditorOverlayWidget::is_position_inside_tilemap(const TileMap* tilemap, const Vector& pos) const { - return pos.x > 0 && pos.y > 0 && - pos.x <= static_cast(tilemap->get_width()) && - pos.y <= static_cast(tilemap->get_height()); + return pos.x >= 0 && pos.y >= 0 && + pos.x < static_cast(tilemap->get_width()) && + pos.y < static_cast(tilemap->get_height()); } /* EOF */ From cf5313c5378f50f62911962222b0e871f19602e6 Mon Sep 17 00:00:00 2001 From: Tobias Markus Date: Sun, 23 Jul 2023 22:31:35 +0200 Subject: [PATCH 31/50] Add set_action to Decal scripting Fixes #2557 --- src/scripting/decal.cpp | 7 +++++++ src/scripting/decal.hpp | 5 +++++ src/scripting/wrapper.cpp | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+) diff --git a/src/scripting/decal.cpp b/src/scripting/decal.cpp index 55cd5859bd3..472ea2221d2 100644 --- a/src/scripting/decal.cpp +++ b/src/scripting/decal.cpp @@ -47,6 +47,13 @@ Decal::fade_out(float time) object.fade_out(time); } +void +Decal::set_action(const std::string& action) +{ + SCRIPT_GUARD_VOID; + object.set_action(action); +} + } // namespace scripting /* EOF */ diff --git a/src/scripting/decal.hpp b/src/scripting/decal.hpp index cf65af140bc..3e5cf0a1d0c 100644 --- a/src/scripting/decal.hpp +++ b/src/scripting/decal.hpp @@ -65,6 +65,11 @@ class Decal final * @param float $time */ void fade_out(float time); + /** + * Sets the action for this decal + * @param float $time + */ + void set_action(const std::string& action); }; } // namespace scripting diff --git a/src/scripting/wrapper.cpp b/src/scripting/wrapper.cpp index 7d316bc8251..e61e5653580 100644 --- a/src/scripting/wrapper.cpp +++ b/src/scripting/wrapper.cpp @@ -4920,6 +4920,36 @@ static SQInteger Decal_fade_out_wrapper(HSQUIRRELVM vm) } +static SQInteger Decal_set_action_wrapper(HSQUIRRELVM vm) +{ + SQUserPointer data; + if(SQ_FAILED(sq_getinstanceup(vm, 1, &data, nullptr, SQTrue)) || !data) { + sq_throwerror(vm, _SC("'set_action' called without instance")); + return SQ_ERROR; + } + scripting::Decal* _this = reinterpret_cast (data); + + const SQChar* arg0; + if(SQ_FAILED(sq_getstring(vm, 2, &arg0))) { + sq_throwerror(vm, _SC("Argument 1 not a string")); + return SQ_ERROR; + } + + try { + _this->set_action(arg0); + + return 0; + + } catch(std::exception& e) { + sq_throwerror(vm, e.what()); + return SQ_ERROR; + } catch(...) { + sq_throwerror(vm, _SC("Unexpected exception while executing function 'set_action'")); + return SQ_ERROR; + } + +} + static SQInteger Dispenser_release_hook(SQUserPointer ptr, SQInteger ) { scripting::Dispenser* _this = reinterpret_cast (ptr); @@ -14951,6 +14981,13 @@ void register_supertux_wrapper(HSQUIRRELVM v) throw SquirrelError(v, "Couldn't register function 'fade_out'"); } + sq_pushstring(v, "set_action", -1); + sq_newclosure(v, &Decal_set_action_wrapper, 0); + sq_setparamscheck(v, SQ_MATCHTYPEMASKSTRING, ".s"); + if(SQ_FAILED(sq_createslot(v, -3))) { + throw SquirrelError(v, "Couldn't register function 'set_action'"); + } + if(SQ_FAILED(sq_createslot(v, -3))) { throw SquirrelError(v, "Couldn't register class 'Decal'"); } From 17be3f481761239f2e888a3c12462e8c6766408e Mon Sep 17 00:00:00 2001 From: Vankata453 <78196474+Vankata453@users.noreply.github.com> Date: Mon, 24 Jul 2023 01:28:08 +0300 Subject: [PATCH 32/50] Fix comment for `scripting::Decal::set_action` --- src/scripting/decal.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/scripting/decal.hpp b/src/scripting/decal.hpp index 3e5cf0a1d0c..d14a1a4693d 100644 --- a/src/scripting/decal.hpp +++ b/src/scripting/decal.hpp @@ -66,8 +66,8 @@ class Decal final */ void fade_out(float time); /** - * Sets the action for this decal - * @param float $time + * Sets the action for the decal's sprite. + * @param string $action */ void set_action(const std::string& action); }; From 81172c355cd9daf9eb1f78ddf1f95b81f2b745ed Mon Sep 17 00:00:00 2001 From: Tobias Markus Date: Mon, 24 Jul 2023 02:00:42 +0200 Subject: [PATCH 33/50] Implement stop_looping_sounds / play_looping_sounds in AmbientSound Fixes #1703 --- src/object/ambient_sound.cpp | 12 ++++++++++++ src/object/ambient_sound.hpp | 3 +++ 2 files changed, 15 insertions(+) diff --git a/src/object/ambient_sound.cpp b/src/object/ambient_sound.cpp index d68d33ea36e..863980891a3 100644 --- a/src/object/ambient_sound.cpp +++ b/src/object/ambient_sound.cpp @@ -258,4 +258,16 @@ AmbientSound::draw(DrawingContext& context) } } +void +AmbientSound::stop_looping_sounds() +{ + stop_playing(); +} + +void +AmbientSound::play_looping_sounds() +{ + start_playing(); +} + /* EOF */ diff --git a/src/object/ambient_sound.hpp b/src/object/ambient_sound.hpp index 0c6a2fa7bd7..7e94aceb81f 100644 --- a/src/object/ambient_sound.hpp +++ b/src/object/ambient_sound.hpp @@ -82,6 +82,9 @@ class AmbientSound final : public MovingObject, virtual int get_layer() const override { return LAYER_OBJECTS; } + virtual void stop_looping_sounds() override; + virtual void play_looping_sounds() override; + protected: virtual void update(float dt_sec) override; virtual void start_playing(); From a09c630ffbb6441096fc849031021a1f2d3aa6b8 Mon Sep 17 00:00:00 2001 From: Vankata453 <78196474+Vankata453@users.noreply.github.com> Date: Mon, 24 Jul 2023 15:24:47 +0300 Subject: [PATCH 34/50] Fix sprite hardcoding issues, convert more objects to `MovingSprite` Some objects contain duplicate code for setting their sprite, which in some cases overrides the custom sprite preference. Such duplicate code has been removed. Some objects inherit `MovingObject`, whilst managing a sprite. Many of such objects now use the `MovingSprite` base class. Triggers haven't been converted yet. That is to come with a future PR. Additionally, `m_sprite->set_action` calls on `MovingSprite` objects have been replaced with `set_action` calls. This makes sure that when the action is updated, the object's hitbox is too. --- src/badguy/flame.cpp | 3 --- src/badguy/goldbomb.cpp | 10 ---------- src/badguy/haywire.cpp | 11 ----------- src/badguy/kamikazesnowball.cpp | 10 +++------- src/badguy/kamikazesnowball.hpp | 3 ++- src/badguy/mrbomb.cpp | 11 ----------- src/badguy/mriceblock.cpp | 4 ++-- src/badguy/mriceblock.hpp | 3 ++- src/badguy/short_fuse.cpp | 11 ----------- src/badguy/smartblock.cpp | 11 +---------- src/object/block.cpp | 35 +++++---------------------------- src/object/block.hpp | 18 +++-------------- src/object/bonus_block.cpp | 20 +++++++------------ src/object/brick.cpp | 13 +++++------- src/object/brick.hpp | 7 ++++--- src/object/bumper.cpp | 6 +++--- src/object/candle.cpp | 10 +++++----- src/object/conveyor_belt.cpp | 31 +++++++++++++++++------------ src/object/conveyor_belt.hpp | 12 +++++------ src/object/firefly.cpp | 19 +++--------------- src/object/invisible_block.cpp | 15 ++++++-------- src/object/ispy.cpp | 12 +++++------ src/object/lantern.cpp | 8 ++++---- src/object/lit_object.cpp | 35 +++++++-------------------------- src/object/lit_object.hpp | 20 ++++++------------- src/object/magicblock.cpp | 4 ++-- src/object/moving_sprite.cpp | 2 +- src/object/moving_sprite.hpp | 2 +- src/object/rublight.cpp | 8 ++++---- src/object/rusty_trampoline.cpp | 15 +++++++------- src/object/shard.cpp | 2 +- src/object/skull_tile.cpp | 4 ++-- src/object/torch.cpp | 28 +++++++++----------------- src/object/torch.hpp | 14 ++++--------- src/object/trampoline.cpp | 13 ++++++------ src/object/unstable_tile.cpp | 5 +++-- src/object/water_drop.cpp | 2 +- src/object/weak_block.cpp | 8 ++++---- 38 files changed, 144 insertions(+), 301 deletions(-) diff --git a/src/badguy/flame.cpp b/src/badguy/flame.cpp index edbb7541621..69b3527ecda 100644 --- a/src/badguy/flame.cpp +++ b/src/badguy/flame.cpp @@ -45,9 +45,6 @@ Flame::Flame(const ReaderMapping& reader, const std::string& sprite) : m_countMe = false; SoundManager::current()->preload(FLAME_SOUND); - reader.get("sprite", m_sprite_name, m_sprite_name.c_str()); - m_sprite = SpriteManager::current()->create(m_sprite_name); - set_colgroup_active(COLGROUP_TOUCHABLE); m_lightsprite->set_color(Color(0.21f, 0.13f, 0.08f)); diff --git a/src/badguy/goldbomb.cpp b/src/badguy/goldbomb.cpp index ccb78fc5313..72866d516c2 100644 --- a/src/badguy/goldbomb.cpp +++ b/src/badguy/goldbomb.cpp @@ -41,16 +41,6 @@ GoldBomb::GoldBomb(const ReaderMapping& reader) : //Prevent stutter when Tux jumps on Gold Bomb SoundManager::current()->preload("sounds/explosion.wav"); - //Check if we need another sprite - if ( !reader.get( "sprite", m_sprite_name ) ){ - return; - } - if (m_sprite_name.empty()) { - m_sprite_name = "images/creatures/gold_bomb/gold_bomb.sprite"; - return; - } - //Replace sprite - m_sprite = SpriteManager::current()->create( m_sprite_name ); m_exploding_sprite->set_action("default", 1); } diff --git a/src/badguy/haywire.cpp b/src/badguy/haywire.cpp index 2ebd7b798e7..cdcfaa954b5 100644 --- a/src/badguy/haywire.cpp +++ b/src/badguy/haywire.cpp @@ -56,17 +56,6 @@ Haywire::Haywire(const ReaderMapping& reader) : //Prevent stutter when Tux jumps on Mr Bomb SoundManager::current()->preload("sounds/explosion.wav"); - - //Check if we need another sprite - if ( !reader.get( "sprite", m_sprite_name ) ){ - return; - } - if (m_sprite_name.empty()) { - m_sprite_name = "images/creatures/haywire/haywire.sprite"; - return; - } - //Replace sprite - m_sprite = SpriteManager::current()->create( m_sprite_name ); } Direction diff --git a/src/badguy/kamikazesnowball.cpp b/src/badguy/kamikazesnowball.cpp index 307b0a2a3c1..ab3bb98f36d 100644 --- a/src/badguy/kamikazesnowball.cpp +++ b/src/badguy/kamikazesnowball.cpp @@ -17,8 +17,6 @@ #include "badguy/kamikazesnowball.hpp" #include "audio/sound_manager.hpp" -#include "sprite/sprite.hpp" -#include "sprite/sprite_manager.hpp" namespace{ static const float KAMIKAZE_SPEED = 200; @@ -26,8 +24,8 @@ namespace{ const std::string SPLAT_SOUND = "sounds/splat.wav"; } -KamikazeSnowball::KamikazeSnowball(const ReaderMapping& reader) : - BadGuy(reader, "images/creatures/snowball/kamikaze-snowball.sprite") +KamikazeSnowball::KamikazeSnowball(const ReaderMapping& reader, const std::string& sprite_name) : + BadGuy(reader, sprite_name) { SoundManager::current()->preload(SPLAT_SOUND); set_action (m_dir == Direction::LEFT ? "left" : "right", /* loops = */ -1); @@ -100,10 +98,8 @@ KamikazeSnowball::collision_player(Player& player, const CollisionHit& hit) } LeafShot::LeafShot(const ReaderMapping& reader) : - KamikazeSnowball(reader) + KamikazeSnowball(reader, "images/creatures/leafshot/leafshot.sprite") { - m_sprite_name = "images/creatures/leafshot/leafshot.sprite"; - m_sprite = SpriteManager::current()->create(m_sprite_name); } void diff --git a/src/badguy/kamikazesnowball.hpp b/src/badguy/kamikazesnowball.hpp index 864674c0359..33a3907f301 100644 --- a/src/badguy/kamikazesnowball.hpp +++ b/src/badguy/kamikazesnowball.hpp @@ -24,7 +24,8 @@ class KamikazeSnowball : public BadGuy { public: - KamikazeSnowball(const ReaderMapping& reader); + KamikazeSnowball(const ReaderMapping& reader, + const std::string& sprite_name = "images/creatures/snowball/kamikaze-snowball.sprite"); virtual void initialize() override; virtual void collision_solid(const CollisionHit& hit) override; diff --git a/src/badguy/mrbomb.cpp b/src/badguy/mrbomb.cpp index 255b5229b7b..03e309456ae 100644 --- a/src/badguy/mrbomb.cpp +++ b/src/badguy/mrbomb.cpp @@ -36,17 +36,6 @@ MrBomb::MrBomb(const ReaderMapping& reader) : //Prevent stutter when Tux jumps on Mr Bomb SoundManager::current()->preload("sounds/explosion.wav"); - - //Check if we need another sprite - if ( !reader.get( "sprite", m_sprite_name ) ){ - return; - } - if (m_sprite_name.empty()) { - m_sprite_name = "images/creatures/mr_bomb/mr_bomb.sprite"; - return; - } - //Replace sprite - m_sprite = SpriteManager::current()->create( m_sprite_name ); } HitResponse diff --git a/src/badguy/mriceblock.cpp b/src/badguy/mriceblock.cpp index aff55f08c53..d8792fb6c5e 100644 --- a/src/badguy/mriceblock.cpp +++ b/src/badguy/mriceblock.cpp @@ -30,8 +30,8 @@ namespace { const float NOKICK_TIME = 0.1f; } -MrIceBlock::MrIceBlock(const ReaderMapping& reader) : - WalkingBadguy(reader, "images/creatures/iceblock/iceblock.sprite", "left", "right"), +MrIceBlock::MrIceBlock(const ReaderMapping& reader, const std::string& sprite_name) : + WalkingBadguy(reader, sprite_name, "left", "right"), ice_state(ICESTATE_NORMAL), nokick_timer(), flat_timer(), diff --git a/src/badguy/mriceblock.hpp b/src/badguy/mriceblock.hpp index 53d6047e0aa..200e316bc31 100644 --- a/src/badguy/mriceblock.hpp +++ b/src/badguy/mriceblock.hpp @@ -22,7 +22,8 @@ class MrIceBlock : public WalkingBadguy { public: - MrIceBlock(const ReaderMapping& reader); + MrIceBlock(const ReaderMapping& reader, + const std::string& sprite_name = "images/creatures/iceblock/iceblock.sprite"); virtual void initialize() override; virtual HitResponse collision(GameObject& object, const CollisionHit& hit) override; diff --git a/src/badguy/short_fuse.cpp b/src/badguy/short_fuse.cpp index 3c17cb56808..165f1f920b3 100644 --- a/src/badguy/short_fuse.cpp +++ b/src/badguy/short_fuse.cpp @@ -31,17 +31,6 @@ ShortFuse::ShortFuse(const ReaderMapping& reader) : walk_speed = 100; max_drop_height = 16; - //Check if we need another sprite - if ( !reader.get( "sprite", m_sprite_name ) ){ - return; - } - if (m_sprite_name.empty()) { - m_sprite_name = "images/creatures/short_fuse/short_fuse.sprite"; - return; - } - //Replace sprite - m_sprite = SpriteManager::current()->create( m_sprite_name ); - SoundManager::current()->preload("sounds/firecracker.ogg"); } diff --git a/src/badguy/smartblock.cpp b/src/badguy/smartblock.cpp index f24265df905..5c6681e542c 100644 --- a/src/badguy/smartblock.cpp +++ b/src/badguy/smartblock.cpp @@ -16,19 +16,10 @@ #include "badguy/smartblock.hpp" -#include "sprite/sprite_manager.hpp" -#include "util/reader_mapping.hpp" - SmartBlock::SmartBlock(const ReaderMapping& reader) : - MrIceBlock(reader) + MrIceBlock(reader, "images/creatures/iceblock/smart_block.sprite") { max_drop_height = 16; - m_default_sprite_name = "images/creatures/iceblock/smart_block.sprite"; - - if (!reader.get("sprite", m_sprite_name)) { - m_sprite_name = m_default_sprite_name; - } - m_sprite = SpriteManager::current()->create(m_sprite_name); } /* EOF */ diff --git a/src/object/block.cpp b/src/object/block.cpp index 7f726dfe100..735cf42aa65 100644 --- a/src/object/block.cpp +++ b/src/object/block.cpp @@ -26,8 +26,6 @@ #include "object/growup.hpp" #include "object/player.hpp" #include "object/sprite_particle.hpp" -#include "sprite/sprite.hpp" -#include "sprite/sprite_manager.hpp" #include "supertux/constants.hpp" #include "supertux/flip_level_transformer.hpp" #include "supertux/sector.hpp" @@ -38,16 +36,13 @@ static const float BOUNCY_BRICK_MAX_OFFSET = 8; static const float BOUNCY_BRICK_SPEED = 90; static const float BUMP_ROTATION_ANGLE = 10; -Block::Block(SpritePtr newsprite) : - m_sprite(std::move(newsprite)), - m_sprite_name(), - m_default_sprite_name(), +Block::Block(const Vector& pos, const std::string& sprite_file) : + MovingSprite(pos, sprite_file), m_bouncing(false), m_breaking(false), m_bounce_dir(0), m_bounce_offset(0), - m_original_y(-1), - m_flip(NO_FLIP) + m_original_y(-1) { m_col.m_bbox.set_size(32, 32.1f); set_group(COLGROUP_STATIC); @@ -56,28 +51,13 @@ Block::Block(SpritePtr newsprite) : } Block::Block(const ReaderMapping& mapping, const std::string& sprite_file) : - m_sprite(), - m_sprite_name(), - m_default_sprite_name(), + MovingSprite(mapping, sprite_file), m_bouncing(false), m_breaking(false), m_bounce_dir(0), m_bounce_offset(0), - m_original_y(-1), - m_flip(NO_FLIP) + m_original_y(-1) { - mapping.get("x", m_col.m_bbox.get_left()); - mapping.get("y", m_col.m_bbox.get_top()); - - std::string sf; - mapping.get("sprite", sf); - if (sf.empty() || !PHYSFS_exists(sf.c_str())) { - sf = sprite_file; - } - m_sprite = SpriteManager::current()->create(sf); - m_sprite_name = sf; - m_default_sprite_name = sprite_file; - m_col.m_bbox.set_size(32, 32.1f); set_group(COLGROUP_STATIC); SoundManager::current()->preload("sounds/upgrade.wav"); @@ -237,11 +217,6 @@ Block::get_settings() return result; } -void Block::after_editor_set() -{ - m_sprite = SpriteManager::current()->create(m_sprite_name); -} - void Block::on_flip(float height) { diff --git a/src/object/block.hpp b/src/object/block.hpp index 17fd1f74a68..1b9eb6ca806 100644 --- a/src/object/block.hpp +++ b/src/object/block.hpp @@ -17,30 +17,24 @@ #ifndef HEADER_SUPERTUX_OBJECT_BLOCK_HPP #define HEADER_SUPERTUX_OBJECT_BLOCK_HPP -#include "sprite/sprite.hpp" -#include "sprite/sprite_ptr.hpp" -#include "supertux/moving_object.hpp" -#include "video/flip.hpp" +#include "object/moving_sprite.hpp" class Player; class ReaderMapping; -class Block : public MovingObject +class Block : public MovingSprite { friend class FlipLevelTransformer; public: - Block(SpritePtr sprite); + Block(const Vector& pos, const std::string& sprite_file); Block(const ReaderMapping& mapping, const std::string& sprite_file); virtual HitResponse collision(GameObject& other, const CollisionHit& hit) override; virtual void update(float dt_sec) override; virtual void draw(DrawingContext& context) override; - virtual std::string get_default_sprite_name() const { return m_default_sprite_name; } - virtual ObjectSettings get_settings() override; - virtual void after_editor_set() override; virtual void on_flip(float height) override; @@ -55,18 +49,12 @@ class Block : public MovingObject void break_me(); protected: - SpritePtr m_sprite; - std::string m_sprite_name; - std::string m_default_sprite_name; bool m_bouncing; bool m_breaking; float m_bounce_dir; float m_bounce_offset; float m_original_y; -private: - Flip m_flip; - private: Block(const Block&) = delete; Block& operator=(const Block&) = delete; diff --git a/src/object/bonus_block.cpp b/src/object/bonus_block.cpp index b89fc7aadd3..3ca508c22b3 100644 --- a/src/object/bonus_block.cpp +++ b/src/object/bonus_block.cpp @@ -36,7 +36,6 @@ #include "object/specialriser.hpp" #include "object/star.hpp" #include "object/trampoline.hpp" -#include "sprite/sprite_manager.hpp" #include "supertux/constants.hpp" #include "supertux/game_object_factory.hpp" #include "supertux/level.hpp" @@ -63,7 +62,7 @@ const float upgrade_sound_gain = 0.3f; } // namespace BonusBlock::BonusBlock(const Vector& pos, int tile_data) : - Block(SpriteManager::current()->create("images/objects/bonus_block/bonusblock.sprite")), + Block(pos, "images/objects/bonus_block/bonusblock.sprite"), m_contents(), m_object(), m_hit_counter(1), @@ -72,10 +71,7 @@ BonusBlock::BonusBlock(const Vector& pos, int tile_data) : m_custom_sx(), m_coin_sprite("images/objects/coin/coin.sprite") { - m_default_sprite_name = "images/objects/bonus_block/bonusblock.sprite"; - - m_col.m_bbox.set_pos(pos); - m_sprite->set_action("normal"); + set_action("normal"); m_contents = get_content_by_data(tile_data); preload_contents(tile_data); } @@ -90,8 +86,6 @@ BonusBlock::BonusBlock(const ReaderMapping& mapping) : m_custom_sx(), m_coin_sprite("images/objects/coin/coin.sprite") { - m_default_sprite_name = "images/objects/bonus_block/bonusblock.sprite"; - auto iter = mapping.get_iter(); while (iter.next()) { const std::string& token = iter.get_key(); @@ -172,7 +166,7 @@ BonusBlock::BonusBlock(const ReaderMapping& mapping) : SoundManager::current()->preload("sounds/switch.ogg"); m_lightsprite = Surface::from_file("/images/objects/lightmap_light/bonusblock_light.png"); if (m_contents == Content::LIGHT_ON) { - m_sprite->set_action("on"); + set_action("on"); } } } @@ -354,9 +348,9 @@ BonusBlock::try_open(Player* player) case Content::LIGHT_ON: { if (m_sprite->get_action() == "on") - m_sprite->set_action("off"); + set_action("off"); else - m_sprite->set_action("on"); + set_action("on"); SoundManager::current()->play("sounds/switch.ogg", get_pos()); break; } @@ -406,7 +400,7 @@ BonusBlock::try_open(Player* player) start_bounce(player); if (m_hit_counter <= 0 || m_contents == Content::LIGHT || m_contents == Content::LIGHT_ON) { //use 0 to allow infinite hits } else if (m_hit_counter == 1) { - m_sprite->set_action("empty"); + set_action("empty"); } else { m_hit_counter--; } @@ -550,7 +544,7 @@ BonusBlock::try_drop(Player *player) if (countdown) { // only decrease hit counter if try_open was not called if (m_hit_counter == 1) { - m_sprite->set_action("empty"); + set_action("empty"); } else { m_hit_counter--; } diff --git a/src/object/brick.cpp b/src/object/brick.cpp index 72cb89573f1..fc9b3829968 100644 --- a/src/object/brick.cpp +++ b/src/object/brick.cpp @@ -24,18 +24,15 @@ #include "object/explosion.hpp" #include "object/player.hpp" #include "object/portable.hpp" -#include "sprite/sprite.hpp" -#include "sprite/sprite_manager.hpp" #include "supertux/constants.hpp" #include "supertux/sector.hpp" #include "util/reader_mapping.hpp" -Brick::Brick(const Vector& pos, int data, const std::string& spriteName) : - Block(SpriteManager::current()->create(spriteName)), +Brick::Brick(const Vector& pos, int data, const std::string& sprite_name) : + Block(pos, sprite_name), m_breakable(false), m_coin_counter(0) { - m_col.m_bbox.set_pos(pos); if (data == 1) { m_coin_counter = 5; } else { @@ -43,8 +40,8 @@ Brick::Brick(const Vector& pos, int data, const std::string& spriteName) : } } -Brick::Brick(const ReaderMapping& mapping, const std::string& spriteName) : - Block(mapping, spriteName), +Brick::Brick(const ReaderMapping& mapping, const std::string& sprite_name) : + Block(mapping, sprite_name), m_breakable(), m_coin_counter(0) { @@ -115,7 +112,7 @@ Brick::try_break(Player* player, bool slider) Player& player_one = *Sector::get().get_players()[0]; player_one.get_status().add_coins(1); if (m_coin_counter == 0) - m_sprite->set_action("empty"); + set_action("empty"); start_bounce(player); } else if (m_breakable) { if (player) { diff --git a/src/object/brick.hpp b/src/object/brick.hpp index bc427aef399..2a1b5353c89 100644 --- a/src/object/brick.hpp +++ b/src/object/brick.hpp @@ -17,14 +17,15 @@ #ifndef HEADER_SUPERTUX_OBJECT_BRICK_HPP #define HEADER_SUPERTUX_OBJECT_BRICK_HPP -#include "badguy/crusher.hpp" #include "object/block.hpp" +#include "badguy/crusher.hpp" + class Brick : public Block { public: - Brick(const Vector& pos, int data, const std::string& spriteName); - Brick(const ReaderMapping& mapping, const std::string& spriteName = "images/objects/bonus_block/brick.sprite"); + Brick(const Vector& pos, int data, const std::string& sprite_name); + Brick(const ReaderMapping& mapping, const std::string& sprite_name = "images/objects/bonus_block/brick.sprite"); virtual HitResponse collision(GameObject& other, const CollisionHit& hit) override; virtual ObjectSettings get_settings() override; diff --git a/src/object/bumper.cpp b/src/object/bumper.cpp index cd71ef39de7..b267cf646f6 100644 --- a/src/object/bumper.cpp +++ b/src/object/bumper.cpp @@ -35,7 +35,7 @@ Bumper::Bumper(const ReaderMapping& reader) : left() { reader.get("left", left); - m_sprite->set_action(left ? "left-normal" : "right-normal"); + set_action(left ? "left-normal" : "right-normal"); physic.enable_gravity(false); } @@ -56,7 +56,7 @@ Bumper::update(float dt_sec) { if (m_sprite->animation_done()) { - m_sprite->set_action(left ? "left-normal" : "right-normal"); + set_action(left ? "left-normal" : "right-normal"); } m_col.set_movement(physic.get_movement (dt_sec)); } @@ -71,7 +71,7 @@ Bumper::collision(GameObject& other, const CollisionHit& hit) player->get_physic().set_velocity(0.f, player->is_swimming() ? 0.f : BOUNCE_Y); player->sideways_push(BOUNCE_DIR); SoundManager::current()->play(TRAMPOLINE_SOUND, get_pos()); - m_sprite->set_action((left ? "left-swinging" : "right-swinging"), 1); + set_action((left ? "left-swinging" : "right-swinging"), 1); } auto bumper = dynamic_cast (&other); diff --git a/src/object/candle.cpp b/src/object/candle.cpp index 1483df84854..f415920016c 100644 --- a/src/object/candle.cpp +++ b/src/object/candle.cpp @@ -52,9 +52,9 @@ Candle::Candle(const ReaderMapping& mapping) : } if (burning) { - m_sprite->set_action("on"); + set_action("on"); } else { - m_sprite->set_action("off"); + set_action("off"); } } @@ -67,7 +67,7 @@ Candle::after_editor_set() candle_light_1->set_color(lightcolor); candle_light_2->set_color(lightcolor); - m_sprite->set_action(burning ? "on" : "off"); + set_action(burning ? "on" : "off"); } ObjectSettings @@ -136,9 +136,9 @@ Candle::set_burning(bool burning_) if (burning == burning_) return; burning = burning_; if (burning_) { - m_sprite->set_action("on"); + set_action("on"); } else { - m_sprite->set_action("off"); + set_action("off"); } //puff smoke for flickering light sources only if (flicker) puff_smoke(); diff --git a/src/object/conveyor_belt.cpp b/src/object/conveyor_belt.cpp index 5583626bf0a..8bbf7a81b9e 100644 --- a/src/object/conveyor_belt.cpp +++ b/src/object/conveyor_belt.cpp @@ -24,15 +24,14 @@ #include "util/reader_mapping.hpp" ConveyorBelt::ConveyorBelt(const ReaderMapping &reader) : - MovingObject(reader), // TODO: sprite + MovingSprite(reader, "images/objects/conveyor_belt/conveyor.sprite"), ExposedObject(this), m_running(true), m_dir(Direction::LEFT), m_length(1), m_speed(1.0f), m_frame(0.0f), - m_frame_index(0), - m_sprite(SpriteManager::current()->create("images/objects/conveyor_belt/conveyor.sprite")) + m_frame_index(0) { set_group(COLGROUP_STATIC); reader.get("running", m_running); @@ -47,18 +46,16 @@ ConveyorBelt::ConveyorBelt(const ReaderMapping &reader) : if (m_length <= 0) m_length = 1; - m_col.m_bbox.set_size(32.0f * static_cast(m_length), 32.0f); - if (!m_running) - m_sprite->set_action("stopped"); + set_action("stopped"); else - m_sprite->set_action(m_dir); + set_action(m_dir); } ObjectSettings ConveyorBelt::get_settings() { - ObjectSettings result = MovingObject::get_settings(); + ObjectSettings result = MovingSprite::get_settings(); result.add_direction(_("Direction"), &m_dir, Direction::LEFT, "direction"); result.add_float(_("Speed"), &m_speed, "speed", 1.0f); @@ -117,26 +114,34 @@ ConveyorBelt::draw(DrawingContext &context) } } +void +ConveyorBelt::update_hitbox() +{ + m_col.m_bbox.set_size(m_sprite->get_current_hitbox_width() * static_cast(m_length), + m_sprite->get_current_hitbox_height()); +} + void ConveyorBelt::after_editor_set() { + MovingSprite::after_editor_set(); + if (m_length <= 0) m_length = 1; - m_col.m_bbox.set_size(32.0f * static_cast(m_length), 32.0f); } void ConveyorBelt::start() { m_running = true; - m_sprite->set_action(m_dir); + set_action(m_dir); } void ConveyorBelt::stop() { m_running = false; - m_sprite->set_action("stopped"); + set_action("stopped"); } void @@ -144,7 +149,7 @@ ConveyorBelt::move_left() { m_dir = Direction::LEFT; if (m_running) - m_sprite->set_action("left"); + set_action("left"); } void @@ -152,7 +157,7 @@ ConveyorBelt::move_right() { m_dir = Direction::RIGHT; if (m_running) - m_sprite->set_action("right"); + set_action("right"); } void diff --git a/src/object/conveyor_belt.hpp b/src/object/conveyor_belt.hpp index a8c0c742b76..c3228811b1e 100644 --- a/src/object/conveyor_belt.hpp +++ b/src/object/conveyor_belt.hpp @@ -17,17 +17,14 @@ #ifndef HEADER_SUPERTUX_OBJECT_CONVEYOR_BELT_HPP #define HEADER_SUPERTUX_OBJECT_CONVEYOR_BELT_HPP +#include "object/moving_sprite.hpp" #include "squirrel/exposed_object.hpp" -#include "supertux/moving_object.hpp" #include "scripting/conveyor_belt.hpp" #include "supertux/timer.hpp" -#include "video/layer.hpp" - -class Sprite; /** This class represents a platform that moves entities riding it. */ -class ConveyorBelt final : public MovingObject, +class ConveyorBelt final : public MovingSprite, public ExposedObject { public: @@ -66,6 +63,9 @@ class ConveyorBelt final : public MovingObject, /** Changes the shifting speed of the conveyor. */ void set_speed(float target_speed); +private: + void update_hitbox() override; + private: bool m_running; Direction m_dir; @@ -77,8 +77,6 @@ class ConveyorBelt final : public MovingObject, const float MAX_SPEED = 32.0f; - std::unique_ptr m_sprite; - private: ConveyorBelt(const ConveyorBelt&) = delete; ConveyorBelt& operator=(const ConveyorBelt&) = delete; diff --git a/src/object/firefly.cpp b/src/object/firefly.cpp index 21c2ccbe85f..b579e8bc9bb 100644 --- a/src/object/firefly.cpp +++ b/src/object/firefly.cpp @@ -39,19 +39,6 @@ Firefly::Firefly(const ReaderMapping& mapping) : activated(false), initial_position(get_pos()) { - if (!mapping.get( "sprite", m_sprite_name)){ - update_state(); - return; - } - if (m_sprite_name.empty()) { - m_sprite_name = "images/objects/resetpoints/default-resetpoint.sprite"; - update_state(); - return; - } - //Replace sprite - m_sprite = SpriteManager::current()->create( m_sprite_name ); - m_col.m_bbox.set_size(m_sprite->get_current_hitbox_width(), m_sprite->get_current_hitbox_height()); - if (m_sprite_name.find("torch", 0) != std::string::npos) { m_sprite_light = SpriteManager::current()->create("images/objects/lightmap_light/lightmap_light-small.sprite"); m_sprite_light->set_blend(Blend::ADD); @@ -101,11 +88,11 @@ Firefly::update_state() active_checkpoint_spawnpoint->sector == Sector::get().get_name() && active_checkpoint_spawnpoint->position == initial_position) // Is activated. { - m_sprite->set_action("ringing"); + set_action("ringing"); } else // Is deactivated. { - m_sprite->set_action("normal"); + set_action("normal"); } } @@ -142,7 +129,7 @@ Firefly::collision(GameObject& other, const CollisionHit& ) SoundManager::current()->play("sounds/savebell2.wav", get_pos()); } - m_sprite->set_action("ringing"); + set_action("ringing"); GameSession::current()->set_checkpoint_pos(Sector::get().get_name(), initial_position); } diff --git a/src/object/invisible_block.cpp b/src/object/invisible_block.cpp index 5d7774f5382..403c010b9e6 100644 --- a/src/object/invisible_block.cpp +++ b/src/object/invisible_block.cpp @@ -19,22 +19,19 @@ #include "audio/sound_manager.hpp" #include "editor/editor.hpp" #include "object/player.hpp" -#include "sprite/sprite.hpp" -#include "sprite/sprite_manager.hpp" #include "supertux/constants.hpp" InvisibleBlock::InvisibleBlock(const Vector& pos) : - Block(SpriteManager::current()->create("images/objects/bonus_block/invisibleblock.sprite")), - visible(false) + Block(pos, "images/objects/bonus_block/invisibleblock.sprite"), + visible(false) { - m_col.m_bbox.set_pos(pos); SoundManager::current()->preload("sounds/brick.wav"); - m_sprite->set_action("default-editor"); + set_action("default-editor"); } InvisibleBlock::InvisibleBlock(const ReaderMapping& mapping) : - Block(mapping, "images/objects/bonus_block/invisibleblock.sprite"), - visible(false) + Block(mapping, "images/objects/bonus_block/invisibleblock.sprite"), + visible(false) { SoundManager::current()->preload("sounds/brick.wav"); } @@ -77,7 +74,7 @@ InvisibleBlock::hit(Player& player) if (visible) return; - m_sprite->set_action("empty"); + set_action("empty"); start_bounce(&player); set_group(COLGROUP_STATIC); visible = true; diff --git a/src/object/ispy.cpp b/src/object/ispy.cpp index d6f795976c0..cd0fef1b0a1 100644 --- a/src/object/ispy.cpp +++ b/src/object/ispy.cpp @@ -128,10 +128,10 @@ Ispy::set_sprite_action(const std::string& action, int loops) { switch (m_dir) { - case Direction::DOWN: m_sprite->set_action(action + "-down", loops); break; - case Direction::UP: m_sprite->set_action(action + "-up", loops); break; - case Direction::LEFT: m_sprite->set_action(action + "-left", loops); break; - case Direction::RIGHT: m_sprite->set_action(action + "-right", loops); break; + case Direction::DOWN: set_action(action + "-down", loops); break; + case Direction::UP: set_action(action + "-up", loops); break; + case Direction::LEFT: set_action(action + "-left", loops); break; + case Direction::RIGHT: set_action(action + "-right", loops); break; default: break; } } @@ -143,12 +143,12 @@ Ispy::on_flip(float height) if (m_dir == Direction::UP) { m_dir = Direction::DOWN; - m_sprite->set_action("idle-down"); + set_action("idle-down"); } else if (m_dir == Direction::DOWN) { m_dir = Direction::UP; - m_sprite->set_action("idle-up"); + set_action("idle-up"); } } diff --git a/src/object/lantern.cpp b/src/object/lantern.cpp index f5f4907e6f6..9b979d0602e 100644 --- a/src/object/lantern.cpp +++ b/src/object/lantern.cpp @@ -79,10 +79,10 @@ Lantern::updateColor(){ lightsprite->set_color(lightcolor); //Turn lantern off if light is black if (lightcolor.red == 0 && lightcolor.green == 0 && lightcolor.blue == 0){ - m_sprite->set_action("off"); + set_action("off"); m_sprite->set_color(Color(1.0f, 1.0f, 1.0f)); } else { - m_sprite->set_action("normal"); + set_action("normal"); m_sprite->set_color(lightcolor); } } @@ -126,7 +126,7 @@ Lantern::grab(MovingObject& object, const Vector& pos, Direction dir) // if lantern is not lit, draw it as opened if (is_open()) { - m_sprite->set_action("off-open"); + set_action("off-open"); } } @@ -136,7 +136,7 @@ Lantern::ungrab(MovingObject& object, Direction dir) { // if lantern is not lit, it was drawn as opened while grabbed. Now draw it as closed again if (is_open()) { - m_sprite->set_action("off"); + set_action("off"); } Rock::ungrab(object, dir); diff --git a/src/object/lit_object.cpp b/src/object/lit_object.cpp index 607fd29cd86..eacd456c5a3 100644 --- a/src/object/lit_object.cpp +++ b/src/object/lit_object.cpp @@ -22,41 +22,29 @@ #include "util/reader_mapping.hpp" LitObject::LitObject(const ReaderMapping& reader) : - MovingObject(reader), + MovingSprite(reader, "images/objects/lightflower/lightflower1.sprite"), ExposedObject(this), m_light_offset(-6.f, -17.f), - m_sprite_name("images/objects/lightflower/lightflower1.sprite"), m_light_sprite_name("images/objects/lightflower/light/glow_light.sprite"), m_sprite_action("default"), m_light_sprite_action("default"), - m_sprite(), - m_light_sprite(), - m_layer(0), - m_flip(NO_FLIP) + m_light_sprite() { - reader.get("x", m_col.m_bbox.get_left()); - reader.get("y", m_col.m_bbox.get_top()); - reader.get("light-offset-x", m_light_offset.x); reader.get("light-offset-y", m_light_offset.y); - reader.get("sprite", m_sprite_name); reader.get("light-sprite", m_light_sprite_name); reader.get("layer", m_layer, 0); reader.get("action", m_sprite_action); reader.get("light-action", m_light_sprite_action); - m_sprite = SpriteManager::current()->create(m_sprite_name); m_light_sprite = SpriteManager::current()->create(m_light_sprite_name); m_light_sprite->set_blend(Blend::ADD); - m_sprite->set_action(m_sprite_action); + set_action(m_sprite_action); m_light_sprite->set_action(m_light_sprite_action); - m_col.m_bbox.set_size(static_cast(m_sprite->get_width()), - static_cast(m_sprite->get_height())); - set_group(COLGROUP_DISABLED); } @@ -75,9 +63,8 @@ LitObject::update(float) ObjectSettings LitObject::get_settings() { - ObjectSettings result = MovingObject::get_settings(); + ObjectSettings result = MovingSprite::get_settings(); - result.add_sprite(_("Sprite"), &m_sprite_name, "sprite", std::string("images/objects/lightflower/lightflower1.sprite")); result.add_sprite(_("Light sprite"), &m_light_sprite_name, "light-sprite", std::string("images/objects/lightflower/light/glow_light.sprite")); result.add_int(_("Layer"), &m_layer, "layer", 0); @@ -93,15 +80,13 @@ LitObject::get_settings() void LitObject::after_editor_set() { - m_sprite = SpriteManager::current()->create(m_sprite_name); + MovingSprite::after_editor_set(); + m_light_sprite = SpriteManager::current()->create(m_light_sprite_name); m_light_sprite->set_blend(Blend::ADD); - m_sprite->set_action(m_sprite_action); + set_action(m_sprite_action); m_light_sprite->set_action(m_light_sprite_action); - - m_col.m_bbox.set_size(static_cast(m_sprite->get_width()), - static_cast(m_sprite->get_height())); } void @@ -117,12 +102,6 @@ LitObject::get_action() const return m_sprite->get_action(); } -void -LitObject::set_action(const std::string& action) -{ - m_sprite->set_action(action); -} - const std::string& LitObject::get_light_action() const { diff --git a/src/object/lit_object.hpp b/src/object/lit_object.hpp index b22146e76d2..b44baf1b18c 100644 --- a/src/object/lit_object.hpp +++ b/src/object/lit_object.hpp @@ -17,17 +17,15 @@ #ifndef HEADER_SUPERTUX_OBJECT_LIT_OBJECT_HPP #define HEADER_SUPERTUX_OBJECT_LIT_OBJECT_HPP -#include "scripting/lit_object.hpp" -#include "sprite/sprite_ptr.hpp" +#include "object/moving_sprite.hpp" #include "squirrel/exposed_object.hpp" -#include "supertux/moving_object.hpp" -#include "video/flip.hpp" + +#include "scripting/lit_object.hpp" class ReaderMapping; -class LitObject final : - public MovingObject, - public ExposedObject +class LitObject final : public MovingSprite, + public ExposedObject { public: LitObject(const ReaderMapping& reader); @@ -35,8 +33,7 @@ class LitObject final : virtual void draw(DrawingContext& context) override; virtual void update(float) override; - virtual HitResponse collision(GameObject&, const CollisionHit&) override - { return ABORT_MOVE; } + virtual HitResponse collision(GameObject&, const CollisionHit&) override { return ABORT_MOVE; } static std::string class_name() { return "lit-object"; } virtual std::string get_class_name() const override { return class_name(); } @@ -51,20 +48,15 @@ class LitObject final : virtual void on_flip(float height) override; const std::string& get_action() const; - void set_action(const std::string& action); const std::string& get_light_action() const; void set_light_action(const std::string& action); private: Vector m_light_offset; - std::string m_sprite_name; std::string m_light_sprite_name; std::string m_sprite_action; std::string m_light_sprite_action; - SpritePtr m_sprite; SpritePtr m_light_sprite; - int m_layer; - Flip m_flip; private: LitObject(const LitObject&) = delete; diff --git a/src/object/magicblock.cpp b/src/object/magicblock.cpp index f88072905f1..66b92910051 100644 --- a/src/object/magicblock.cpp +++ b/src/object/magicblock.cpp @@ -176,11 +176,11 @@ MagicBlock::update(float dt_sec) if (m_is_solid) { m_solid_time+=dt_sec; m_color.alpha = ALPHA_SOLID; - m_sprite->set_action("solid"); + set_action("solid"); set_group(COLGROUP_STATIC); } else { m_color.alpha = ALPHA_NONSOLID; - m_sprite->set_action("normal"); + set_action("normal"); set_group(COLGROUP_DISABLED); } } diff --git a/src/object/moving_sprite.cpp b/src/object/moving_sprite.cpp index ac8ac83aef9..2f0f7b9c53e 100644 --- a/src/object/moving_sprite.cpp +++ b/src/object/moving_sprite.cpp @@ -64,7 +64,7 @@ MovingSprite::MovingSprite(const ReaderMapping& reader, const std::string& sprit //Make the sprite go default when the sprite file is invalid if (m_sprite_name.empty() || !PHYSFS_exists(m_sprite_name.c_str())) { - m_sprite = SpriteManager::current()->create(m_default_sprite_name); + change_sprite(m_default_sprite_name); m_sprite_found = false; } else if (!change_sprite(m_sprite_name)) // If sprite change fails, change back to default. diff --git a/src/object/moving_sprite.hpp b/src/object/moving_sprite.hpp index fa3579d6d4e..97a56ee6aba 100644 --- a/src/object/moving_sprite.hpp +++ b/src/object/moving_sprite.hpp @@ -97,7 +97,7 @@ class MovingSprite : public MovingObject protected: /** Update hitbox, based on sprite. */ - void update_hitbox(); + virtual void update_hitbox(); protected: std::string m_sprite_name; diff --git a/src/object/rublight.cpp b/src/object/rublight.cpp index 90b134d22c6..afe021e8326 100644 --- a/src/object/rublight.cpp +++ b/src/object/rublight.cpp @@ -37,7 +37,7 @@ RubLight::RubLight(const ReaderMapping& mapping) : fading_speed(5.0f), strength_multiplier(1.0f) { - m_sprite->set_action("inactive"); + set_action("inactive"); std::vector vColor; if (mapping.get("color", vColor)) @@ -95,7 +95,7 @@ void RubLight::rub(float strength) { if (strength <= 0) return; - m_sprite->set_action("active"); + set_action("active"); strength *= strength_multiplier; stored_energy = std::max(stored_energy, strength); if (state == STATE_DARK) @@ -107,12 +107,12 @@ RubLight::update(float dt_sec) { if (m_sprite->get_action() == "active" && m_sprite->animation_done()) { - m_sprite->set_action("inactive"); + set_action("inactive"); } switch (state) { case STATE_DARK: - m_sprite->set_action("inactive"); + set_action("inactive"); break; case STATE_FADING: diff --git a/src/object/rusty_trampoline.cpp b/src/object/rusty_trampoline.cpp index a82bfd3a967..9b3ce246bb5 100644 --- a/src/object/rusty_trampoline.cpp +++ b/src/object/rusty_trampoline.cpp @@ -49,7 +49,7 @@ RustyTrampoline::update(float dt_sec) if (counter < 1) { remove_me(); } else { - m_sprite->set_action("normal"); + set_action("normal"); } } @@ -90,9 +90,9 @@ RustyTrampoline::collision(GameObject& other, const CollisionHit& hit) SoundManager::current()->play(BOUNCE_SOUND, get_pos()); counter--; if (counter > 0) { - m_sprite->set_action("swinging", 1); + set_action("swinging", 1); } else { - m_sprite->set_action("breaking", 1); + set_action("breaking", 1); } return FORCE_MOVE; @@ -109,9 +109,9 @@ RustyTrampoline::collision(GameObject& other, const CollisionHit& hit) SoundManager::current()->play(BOUNCE_SOUND, get_pos()); counter--; if (counter > 0) { - m_sprite->set_action("swinging", 1); + set_action("swinging", 1); } else { - m_sprite->set_action("breaking", 1); + set_action("breaking", 1); } return FORCE_MOVE; } @@ -132,9 +132,10 @@ RustyTrampoline::grab(MovingObject& object, const Vector& pos, Direction dir) { } void -RustyTrampoline::ungrab(MovingObject& object, Direction dir) { +RustyTrampoline::ungrab(MovingObject& object, Direction dir) +{ Rock::ungrab(object, dir); - m_sprite->set_action("breaking", 1); + set_action("breaking", 1); counter = 0; //remove in update() } diff --git a/src/object/shard.cpp b/src/object/shard.cpp index 68a6631fa66..228c31dda61 100644 --- a/src/object/shard.cpp +++ b/src/object/shard.cpp @@ -39,7 +39,7 @@ Shard::Shard(const Vector& pos, const Vector& velocity) : { m_physic.enable_gravity(true); m_physic.set_velocity(velocity); - m_sprite->set_action("default"); + set_action("default"); SoundManager::current()->preload("sounds/crystallo-shardhit.ogg"); } diff --git a/src/object/skull_tile.cpp b/src/object/skull_tile.cpp index 8d18faf9823..acf85cd0a3c 100644 --- a/src/object/skull_tile.cpp +++ b/src/object/skull_tile.cpp @@ -91,7 +91,7 @@ SkullTile::update(float dt_sec) } m_col.set_movement(physic.get_movement(dt_sec)); } else if (hit) { - m_sprite->set_action("mad", -1); + set_action("mad", -1); if (timer.check()) { falling = true; physic.enable_gravity(true); @@ -101,7 +101,7 @@ SkullTile::update(float dt_sec) timer.start(FALLTIME); } } else { - m_sprite->set_action("normal", -1); + set_action("normal", -1); timer.stop(); } hit = false; diff --git a/src/object/torch.cpp b/src/object/torch.cpp index 0235316c9f6..2f88383fd3c 100644 --- a/src/object/torch.cpp +++ b/src/object/torch.cpp @@ -24,31 +24,20 @@ #include "util/reader_mapping.hpp" Torch::Torch(const ReaderMapping& reader) : - MovingObject(reader), + MovingSprite(reader, "images/objects/torch/torch1.sprite"), ExposedObject(this), m_light_color(1.f, 1.f, 1.f), - m_torch(), m_flame(SpriteManager::current()->create("images/objects/torch/flame.sprite")), m_flame_glow(SpriteManager::current()->create("images/objects/torch/flame_glow.sprite")), m_flame_light(SpriteManager::current()->create("images/objects/torch/flame_light.sprite")), - m_burning(true), - sprite_name("images/objects/torch/torch1.sprite"), - m_layer(0), - m_flip(NO_FLIP) + m_burning(true) { - reader.get("x", m_col.m_bbox.get_left()); - reader.get("y", m_col.m_bbox.get_top()); - - reader.get("sprite", sprite_name); reader.get("burning", m_burning, true); reader.get("layer", m_layer, 0); std::vector vColor; if (!reader.get("color", vColor)) vColor = { 1.f, 1.f, 1.f }; - m_torch = SpriteManager::current()->create(sprite_name); - m_col.m_bbox.set_size(static_cast(m_torch->get_width()), - static_cast(m_torch->get_height())); m_flame_glow->set_blend(Blend::ADD); m_flame_light->set_blend(Blend::ADD); if (vColor.size() >= 3) @@ -78,7 +67,7 @@ Torch::draw(DrawingContext& context) m_flame_glow->set_action(m_light_color.greyscale() >= 1.f ? "default" : "greyscale"); } - m_torch->draw(context.color(), get_pos(), m_layer - 1, m_flip); + m_sprite->draw(context.color(), get_pos(), m_layer - 1, m_flip); } void @@ -100,21 +89,22 @@ Torch::collision(GameObject& other, const CollisionHit& ) ObjectSettings Torch::get_settings() { - ObjectSettings result = MovingObject::get_settings(); + ObjectSettings result = MovingSprite::get_settings(); result.add_bool(_("Burning"), &m_burning, "burning", true); - result.add_sprite(_("Sprite"), &sprite_name, "sprite", std::string("images/objects/torch/torch1.sprite")); result.add_int(_("Layer"), &m_layer, "layer", 0); result.add_color(_("Color"), &m_light_color, "color", Color::WHITE); - result.reorder({"sprite", "layer", "color", "x", "y"}); + result.reorder({"burning", "sprite", "layer", "color", "x", "y"}); return result; } -void Torch::after_editor_set() +void +Torch::after_editor_set() { - m_torch = SpriteManager::current()->create(sprite_name); + MovingSprite::after_editor_set(); + m_flame->set_color(m_light_color); m_flame_glow->set_color(m_light_color); m_flame_light->set_color(m_light_color); diff --git a/src/object/torch.hpp b/src/object/torch.hpp index 6dd340b4c19..029542a1ea9 100644 --- a/src/object/torch.hpp +++ b/src/object/torch.hpp @@ -18,17 +18,15 @@ #ifndef HEADER_SUPERTUX_OBJECT_TORCH_HPP #define HEADER_SUPERTUX_OBJECT_TORCH_HPP +#include "object/moving_sprite.hpp" #include "squirrel/exposed_object.hpp" + #include "scripting/torch.hpp" -#include "sprite/sprite_ptr.hpp" -#include "supertux/moving_object.hpp" -#include "video/flip.hpp" class ReaderMapping; -class Torch final : - public MovingObject, - public ExposedObject +class Torch final : public MovingSprite, + public ExposedObject { public: Torch(const ReaderMapping& reader); @@ -59,14 +57,10 @@ class Torch final : private: Color m_light_color; - SpritePtr m_torch; SpritePtr m_flame; SpritePtr m_flame_glow; SpritePtr m_flame_light; bool m_burning; - std::string sprite_name; - int m_layer; /**< The layer (z-pos) of the torch */ - Flip m_flip; private: Torch(const Torch&) = delete; diff --git a/src/object/trampoline.cpp b/src/object/trampoline.cpp index 3d4e039e314..f0b318ed0dc 100644 --- a/src/object/trampoline.cpp +++ b/src/object/trampoline.cpp @@ -46,7 +46,7 @@ Trampoline::Trampoline(const ReaderMapping& mapping) : m_sprite_name = "images/objects/trampoline/trampoline_fix.sprite"; m_default_sprite_name = m_sprite_name; m_sprite = SpriteManager::current()->create(m_sprite_name); - m_sprite->set_action("normal"); + set_action("normal"); } } } @@ -59,7 +59,7 @@ Trampoline::Trampoline(const Vector& pos, bool port) : if (!port) { m_sprite_name = "images/objects/trampoline/trampoline_fix.sprite"; m_sprite = SpriteManager::current()->create(m_sprite_name); - m_sprite->set_action("normal"); + set_action("normal"); } } @@ -67,7 +67,7 @@ void Trampoline::update(float dt_sec) { if (m_sprite->animation_done()) { - m_sprite->set_action("normal"); + set_action("normal"); } Rock::update(dt_sec); @@ -111,7 +111,7 @@ Trampoline::collision(GameObject& other, const CollisionHit& hit) } player->get_physic().set_velocity_y(vy); SoundManager::current()->play(TRAMPOLINE_SOUND, get_pos()); - m_sprite->set_action("swinging", 1); + set_action("swinging", 1); return FORCE_MOVE; } } @@ -124,7 +124,7 @@ Trampoline::collision(GameObject& other, const CollisionHit& hit) vy = VY_INITIAL; walking_badguy->set_velocity_y(vy); SoundManager::current()->play(TRAMPOLINE_SOUND, get_pos()); - m_sprite->set_action("swinging", 1); + set_action("swinging", 1); return FORCE_MOVE; } } @@ -134,7 +134,8 @@ Trampoline::collision(GameObject& other, const CollisionHit& hit) } void -Trampoline::grab(MovingObject& object, const Vector& pos, Direction dir) { +Trampoline::grab(MovingObject& object, const Vector& pos, Direction dir) +{ m_sprite->set_animation_loops(0); Rock::grab(object, pos, dir); } diff --git a/src/object/unstable_tile.cpp b/src/object/unstable_tile.cpp index 08fcaae4963..d9460511a1f 100644 --- a/src/object/unstable_tile.cpp +++ b/src/object/unstable_tile.cpp @@ -41,7 +41,8 @@ UnstableTile::UnstableTile(const ReaderMapping& mapping) : m_alpha(1.f), m_original_pos(m_col.get_pos()) { - m_sprite->set_action("normal"); + set_action("normal"); + physic.set_gravity_modifier(.98f); physic.enable_gravity(false); } @@ -151,7 +152,7 @@ UnstableTile::revive() m_col.set_movement(Vector(0.0f, 0.0f)); m_revive_timer.stop(); m_respawn.reset(new FadeHelper(&m_alpha, FADE_IN_TIME, 1.f)); - m_sprite->set_action("normal"); + set_action("normal"); } void diff --git a/src/object/water_drop.cpp b/src/object/water_drop.cpp index 36f5f7a33d4..e006048c116 100644 --- a/src/object/water_drop.cpp +++ b/src/object/water_drop.cpp @@ -49,7 +49,7 @@ WaterDrop::collision_solid(const CollisionHit& hit) wd_state = WDS_SPLASH; physic.enable_gravity(false); SoundManager::current()->play("sounds/splash.ogg", get_pos()); - m_sprite->set_action("splash", 1); + set_action("splash", 1); // spawn water particles for (int i = 50; i; i--) { diff --git a/src/object/weak_block.cpp b/src/object/weak_block.cpp index 3dd541c02c7..5a1bc01cf2d 100644 --- a/src/object/weak_block.cpp +++ b/src/object/weak_block.cpp @@ -36,14 +36,14 @@ WeakBlock::WeakBlock(const ReaderMapping& mapping) : linked(true), lightsprite(SpriteManager::current()->create("images/objects/lightmap_light/lightmap_light-small.sprite")) { - m_sprite->set_action("normal"); + set_action("normal"); //Check if this weakblock destroys adjacent weakblocks if (mapping.get("linked", linked)){ if (! linked){ m_default_sprite_name = "images/objects/weak_block/meltbox.sprite"; m_sprite_name = m_default_sprite_name; m_sprite = SpriteManager::current()->create(m_sprite_name); - m_sprite->set_action("normal"); + set_action("normal"); } } @@ -137,7 +137,7 @@ WeakBlock::update(float ) if (m_sprite->animation_done()) { state = STATE_DISINTEGRATING; - m_sprite->set_action("disintegrating", 1); + set_action("disintegrating", 1); spreadHit(); set_group(COLGROUP_DISABLED); lightsprite = SpriteManager::current()->create("images/objects/lightmap_light/lightmap_light-tiny.sprite"); @@ -173,7 +173,7 @@ WeakBlock::startBurning() { if (state != STATE_NORMAL) return; state = STATE_BURNING; - m_sprite->set_action("burning", 1); + set_action("burning", 1); // FIXME: Not hardcode these sounds? if (m_sprite_name == "images/objects/weak_block/meltbox.sprite") { SoundManager::current()->play("sounds/sizzle.ogg", get_pos()); From 263ec93e0b1f2cec34079c794e13c457c7ba860f Mon Sep 17 00:00:00 2001 From: Vankata453 <78196474+Vankata453@users.noreply.github.com> Date: Mon, 24 Jul 2023 17:43:44 +0300 Subject: [PATCH 35/50] Remove unneeded `get_settings()` override in `Block` The `Block::get_settings()` function only adds the sprite as an option to the settings from `MovingObject`. Since the previous commit, the `Block` class inherits `MovingSprite`, which already adds this option to the object settings, so this override isn't needed anymore. --- src/object/block.cpp | 10 ---------- src/object/block.hpp | 2 -- 2 files changed, 12 deletions(-) diff --git a/src/object/block.cpp b/src/object/block.cpp index 735cf42aa65..cff5261efe8 100644 --- a/src/object/block.cpp +++ b/src/object/block.cpp @@ -207,16 +207,6 @@ Block::break_me() remove_me(); } -ObjectSettings -Block::get_settings() -{ - ObjectSettings result = MovingObject::get_settings(); - - result.add_sprite(_("Sprite"), &m_sprite_name, "sprite", m_default_sprite_name); - - return result; -} - void Block::on_flip(float height) { diff --git a/src/object/block.hpp b/src/object/block.hpp index 1b9eb6ca806..8e647bf4852 100644 --- a/src/object/block.hpp +++ b/src/object/block.hpp @@ -34,8 +34,6 @@ class Block : public MovingSprite virtual void update(float dt_sec) override; virtual void draw(DrawingContext& context) override; - virtual ObjectSettings get_settings() override; - virtual void on_flip(float height) override; virtual int get_layer() const override { return LAYER_OBJECTS + 1; } From da388077705aa8d28a28ae6ba40536c401dffe61 Mon Sep 17 00:00:00 2001 From: Vankata453 <78196474+Vankata453@users.noreply.github.com> Date: Tue, 25 Jul 2023 13:37:37 +0300 Subject: [PATCH 36/50] Improvements to trigger objects code Issues improvements to the code of trigger objects. Triggers, utilizing sprites, instead of managing sprites on their own, now inherit the new `SpritedTrigger` class, which inherits both `MovingSprite` and `TriggerBase`. Triggers, which do not utilize sprites, inherit the new `Trigger` class, which inherits both `MovingObject` and `TriggerBase`. A lot of unneeded leftover code has been removed. This includes unused constructors, leftover variables and functions. --- src/trigger/climbable.cpp | 37 ++------------ src/trigger/climbable.hpp | 9 +--- src/trigger/door.cpp | 79 +++++++----------------------- src/trigger/door.hpp | 20 +++----- src/trigger/scripttrigger.cpp | 49 ++---------------- src/trigger/scripttrigger.hpp | 10 +--- src/trigger/secretarea_trigger.cpp | 45 ++--------------- src/trigger/secretarea_trigger.hpp | 11 ++--- src/trigger/sequence_trigger.cpp | 51 ++++--------------- src/trigger/sequence_trigger.hpp | 9 +--- src/trigger/switch.cpp | 53 ++++++-------------- src/trigger/switch.hpp | 12 +---- src/trigger/text_area.cpp | 28 ++--------- src/trigger/text_area.hpp | 4 +- src/trigger/trigger_base.cpp | 41 ++++++++-------- src/trigger/trigger_base.hpp | 63 +++++++++++++++++++----- 16 files changed, 148 insertions(+), 373 deletions(-) diff --git a/src/trigger/climbable.cpp b/src/trigger/climbable.cpp index cc96cdcd056..c47ed435f1d 100644 --- a/src/trigger/climbable.cpp +++ b/src/trigger/climbable.cpp @@ -34,31 +34,14 @@ const float POSITION_FIX_AY = 50; // y-wise acceleration applied to player when } Climbable::Climbable(const ReaderMapping& reader) : + Trigger(reader), climbed_by(), trying_to_climb(), - message(), - new_size(0.0f, 0.0f) + message() { - reader.get("x", m_col.m_bbox.get_left()); - reader.get("y", m_col.m_bbox.get_top()); - float w = 32, h = 32; - reader.get("width", w); - reader.get("height", h); - m_col.m_bbox.set_size(w, h); - new_size.x = w; - new_size.y = h; reader.get("message", message); } -Climbable::Climbable(const Rectf& area) : - climbed_by(), - trying_to_climb(), - message(), - new_size(0.0f, 0.0f) -{ - m_col.m_bbox = area; -} - Climbable::~Climbable() { for (auto* player : climbed_by) @@ -71,13 +54,7 @@ Climbable::~Climbable() ObjectSettings Climbable::get_settings() { - new_size.x = m_col.m_bbox.get_width(); - new_size.y = m_col.m_bbox.get_height(); - - ObjectSettings result = TriggerBase::get_settings(); - - // result.add_float(_("Width"), &new_size.x, "width"); - // result.add_float(_("Height"), &new_size.y, "height"); + ObjectSettings result = Trigger::get_settings(); result.add_translatable_text(_("Message"), &message, "message"); @@ -86,15 +63,11 @@ Climbable::get_settings() return result; } -void -Climbable::after_editor_set() { - m_col.m_bbox.set_size(new_size.x, new_size.y); -} - void Climbable::update(float dt_sec) { - TriggerBase::update(dt_sec); + Trigger::update(dt_sec); + auto it = climbed_by.begin(); while (it != climbed_by.end()) { diff --git a/src/trigger/climbable.hpp b/src/trigger/climbable.hpp index fe35c4f1d7e..ad72a58945e 100644 --- a/src/trigger/climbable.hpp +++ b/src/trigger/climbable.hpp @@ -24,11 +24,8 @@ #include "supertux/timer.hpp" class Color; -class DrawingContext; -class Player; -class ReaderMapping; -class Climbable final : public TriggerBase +class Climbable final : public Trigger { private: struct ClimbPlayer @@ -42,7 +39,6 @@ class Climbable final : public TriggerBase public: Climbable(const ReaderMapping& reader); - Climbable(const Rectf& area); ~Climbable() override; static std::string class_name() { return "climbable"; } @@ -52,7 +48,6 @@ class Climbable final : public TriggerBase virtual bool has_variable_size() const override { return true; } virtual ObjectSettings get_settings() override; - virtual void after_editor_set() override; virtual void event(Player& player, EventType type) override; virtual void update(float dt_sec) override; @@ -67,8 +62,6 @@ class Climbable final : public TriggerBase std::string message; private: - Vector new_size; - Climbable(const Climbable&) = delete; Climbable& operator=(const Climbable&) = delete; }; diff --git a/src/trigger/door.cpp b/src/trigger/door.cpp index 2f5dc6df4c7..3b9e7f0f1f7 100644 --- a/src/trigger/door.cpp +++ b/src/trigger/door.cpp @@ -30,73 +30,32 @@ #include "util/reader_mapping.hpp" Door::Door(const ReaderMapping& mapping) : - TriggerBase(mapping), + SpritedTrigger(mapping, "images/objects/door/door.sprite"), state(CLOSED), target_sector(), target_spawnpoint(), script(), - sprite_name("images/objects/door/door.sprite"), - sprite(), lock_sprite(SpriteManager::current()->create("images/objects/door/door_lock.sprite")), stay_open_timer(), unlocking_timer(), lock_warn_timer(), - m_flip(NO_FLIP), m_locked(), lock_color(Color::WHITE) { - mapping.get("x", m_col.m_bbox.get_left()); - mapping.get("y", m_col.m_bbox.get_top()); mapping.get("sector", target_sector); mapping.get("spawnpoint", target_spawnpoint); - mapping.get("sprite", sprite_name); + mapping.get("script", script); mapping.get("locked", m_locked); state = m_locked ? DoorState::LOCKED : DoorState::CLOSED; - mapping.get("script", script); - - sprite = SpriteManager::current()->create(sprite_name); - sprite->set_action("closed"); - m_col.m_bbox.set_size(sprite->get_current_hitbox_width(), sprite->get_current_hitbox_height()); + set_action("closed"); std::vector vColor; - if (mapping.get("lock-color", vColor)) { + if (mapping.get("lock-color", vColor)) lock_color = Color(vColor); - } else - { lock_color = Color::WHITE; - } - lock_sprite->set_color(lock_color); - - SoundManager::current()->preload("sounds/door.wav"); - // TODO: Add proper sounds - SoundManager::current()->preload("sounds/locked.ogg"); - SoundManager::current()->preload("sounds/turnkey.ogg"); -} - -Door::Door(int x, int y, const std::string& sector, const std::string& spawnpoint) : - TriggerBase(), - state(CLOSED), - target_sector(sector), - target_spawnpoint(spawnpoint), - script(), - sprite_name("images/objects/door/door.sprite"), - sprite(SpriteManager::current()->create(sprite_name)), - lock_sprite(SpriteManager::current()->create("images/objects/door/door_lock.sprite")), - stay_open_timer(), - unlocking_timer(), - lock_warn_timer(), - m_flip(NO_FLIP), - lock_color() -{ - state = m_locked ? DoorState::LOCKED : DoorState::CLOSED; - m_col.m_bbox.set_pos(Vector(static_cast(x), static_cast(y))); - - sprite->set_action("closed"); - m_col.m_bbox.set_size(sprite->get_current_hitbox_width(), sprite->get_current_hitbox_height()); - lock_sprite->set_color(lock_color); SoundManager::current()->preload("sounds/door.wav"); @@ -108,9 +67,8 @@ Door::Door(int x, int y, const std::string& sector, const std::string& spawnpoin ObjectSettings Door::get_settings() { - ObjectSettings result = TriggerBase::get_settings(); + ObjectSettings result = SpritedTrigger::get_settings(); - result.add_sprite(_("Sprite"), &sprite_name, "sprite", std::string("images/objects/door/door.sprite")); result.add_script(_("Script"), &script, "script"); result.add_text(_("Sector"), &target_sector, "sector"); result.add_text(_("Spawn point"), &target_spawnpoint, "spawnpoint"); @@ -123,14 +81,11 @@ Door::get_settings() } void -Door::after_editor_set() { - sprite = SpriteManager::current()->create(sprite_name); - m_col.m_bbox.set_size(sprite->get_current_hitbox_width(), sprite->get_current_hitbox_height()); - lock_sprite->set_color(lock_color); -} - -Door::~Door() +Door::after_editor_set() { + SpritedTrigger::after_editor_set(); + + lock_sprite->set_color(lock_color); } void @@ -141,9 +96,9 @@ Door::update(float ) break; case OPENING: // if door has finished opening, start timer and keep door open - if (sprite->animation_done()) { + if (m_sprite->animation_done()) { state = OPEN; - sprite->set_action("open"); + set_action("open"); stay_open_timer.start(1.0); } break; @@ -151,14 +106,14 @@ Door::update(float ) // if door was open long enough, start closing it if (stay_open_timer.check()) { state = CLOSING; - sprite->set_action("closing", 1); + set_action("closing", 1); } break; case CLOSING: // if door has finished closing, keep it shut - if (sprite->animation_done()) { + if (m_sprite->animation_done()) { state = CLOSED; - sprite->set_action("closed"); + set_action("closed"); } break; case LOCKED: @@ -181,7 +136,7 @@ Door::update(float ) void Door::draw(DrawingContext& context) { - sprite->draw(context.color(), m_col.m_bbox.p1(), LAYER_BACKGROUNDTILES+1, m_flip); + m_sprite->draw(context.color(), m_col.m_bbox.p1(), LAYER_BACKGROUNDTILES+1, m_flip); if (state == DoorState::LOCKED || state == DoorState::UNLOCKING) { @@ -201,7 +156,7 @@ Door::event(Player& , EventType type) if (type == EVENT_ACTIVATE) { state = OPENING; SoundManager::current()->play("sounds/door.wav", get_pos()); - sprite->set_action("opening", 1); + set_action("opening", 1); ScreenManager::current()->set_screen_fade(std::make_unique(FadeToBlack::FADEOUT, 1.0f)); } break; @@ -236,7 +191,7 @@ Door::collision(GameObject& other, const CollisionHit& hit_) if (player) { state = CLOSING; - sprite->set_action("closing", 1); + set_action("closing", 1); if (!script.empty()) { Sector::get().run_script(script, "Door"); } diff --git a/src/trigger/door.hpp b/src/trigger/door.hpp index 884a4f24eae..7d9c542fcf7 100644 --- a/src/trigger/door.hpp +++ b/src/trigger/door.hpp @@ -17,19 +17,14 @@ #ifndef HEADER_SUPERTUX_TRIGGER_DOOR_HPP #define HEADER_SUPERTUX_TRIGGER_DOOR_HPP -#include "supertux/timer.hpp" #include "trigger/trigger_base.hpp" -#include "video/flip.hpp" -class Player; -class ReaderMapping; +#include "supertux/timer.hpp" -class Door final : public TriggerBase +class Door final : public SpritedTrigger { public: Door(const ReaderMapping& reader); - Door(int x, int y, const std::string& sector, const std::string& spawnpoint); - ~Door() override; static std::string class_name() { return "door"; } virtual std::string get_class_name() const override { return class_name(); } @@ -42,10 +37,14 @@ class Door final : public TriggerBase virtual void update(float dt_sec) override; virtual void draw(DrawingContext& context) override; virtual void event(Player& player, EventType type) override; + virtual HitResponse collision(GameObject& other, const CollisionHit& hit) override; + virtual void on_flip(float height) override; - virtual bool is_locked() const { return m_locked; } - virtual void unlock(); + + bool is_locked() const { return m_locked; } + void unlock(); + Color get_lock_color() const { return lock_color; } private: @@ -63,13 +62,10 @@ class Door final : public TriggerBase std::string target_sector; /**< target sector to teleport to */ std::string target_spawnpoint; /**< target spawnpoint to teleport to */ std::string script; - std::string sprite_name; - SpritePtr sprite; /**< "door" sprite to render */ SpritePtr lock_sprite; Timer stay_open_timer; /**< time until door will close again */ Timer unlocking_timer; Timer lock_warn_timer; - Flip m_flip; bool m_locked; Color lock_color; diff --git a/src/trigger/scripttrigger.cpp b/src/trigger/scripttrigger.cpp index 81d480d0ead..98683307a4c 100644 --- a/src/trigger/scripttrigger.cpp +++ b/src/trigger/scripttrigger.cpp @@ -24,26 +24,18 @@ #include "video/drawing_context.hpp" ScriptTrigger::ScriptTrigger(const ReaderMapping& reader) : - TriggerBase(reader), + Trigger(reader), triggerevent(), script(), - new_size(0.0f, 0.0f), must_activate(false), oneshot(false), runcount(0) { - if (m_col.m_bbox.get_width() == 0.f) - m_col.m_bbox.set_width(32.f); - - if (m_col.m_bbox.get_height() == 0.f) - m_col.m_bbox.set_height(32.f); - reader.get("script", script); reader.get("button", must_activate); reader.get("oneshot", oneshot); - if (script.empty()) { + if (script.empty() && !Editor::is_active()) log_warning << "No script set in script trigger" << std::endl; - } if (must_activate) triggerevent = EVENT_ACTIVATE; @@ -51,26 +43,10 @@ ScriptTrigger::ScriptTrigger(const ReaderMapping& reader) : triggerevent = EVENT_TOUCH; } -ScriptTrigger::ScriptTrigger(const Vector& pos, const std::string& script_) : - TriggerBase(), - triggerevent(EVENT_TOUCH), - script(script_), - new_size(0.0f, 0.0f), - must_activate(), - oneshot(false), - runcount(0) -{ - m_col.m_bbox.set_pos(pos); - m_col.m_bbox.set_size(32, 32); -} - ObjectSettings ScriptTrigger::get_settings() { - new_size.x = m_col.m_bbox.get_width(); - new_size.y = m_col.m_bbox.get_height(); - - ObjectSettings result = TriggerBase::get_settings(); + ObjectSettings result = Trigger::get_settings(); result.add_script(_("Script"), &script, "script"); result.add_bool(_("Button"), &must_activate, "button"); @@ -81,25 +57,11 @@ ScriptTrigger::get_settings() return result; } -void -ScriptTrigger::after_editor_set() { - //m_col.m_bbox.set_size(new_size.x, new_size.y); - if (must_activate) { - triggerevent = EVENT_ACTIVATE; - } else { - triggerevent = EVENT_TOUCH; - } -} - void ScriptTrigger::event(Player& , EventType type) { - if (type != triggerevent) - return; - - if (oneshot && runcount >= 1) { + if (type != triggerevent || (oneshot && runcount >= 1)) return; - } Sector::get().run_script(script, "ScriptTrigger"); runcount++; @@ -108,10 +70,9 @@ ScriptTrigger::event(Player& , EventType type) void ScriptTrigger::draw(DrawingContext& context) { - if (Editor::is_active() || g_debug.show_collision_rects) { + if (Editor::is_active() || g_debug.show_collision_rects) context.color().draw_filled_rect(m_col.m_bbox, Color(1.0f, 0.0f, 1.0f, 0.6f), 0.0f, LAYER_OBJECTS); - } } /* EOF */ diff --git a/src/trigger/scripttrigger.hpp b/src/trigger/scripttrigger.hpp index a5923593ebd..538990e8752 100644 --- a/src/trigger/scripttrigger.hpp +++ b/src/trigger/scripttrigger.hpp @@ -19,14 +19,10 @@ #include "trigger/trigger_base.hpp" -class ReaderMapping; -class Writer; - -class ScriptTrigger final : public TriggerBase +class ScriptTrigger final : public Trigger { public: ScriptTrigger(const ReaderMapping& reader); - ScriptTrigger(const Vector& pos, const std::string& script); static std::string class_name() { return "scripttrigger"; } virtual std::string get_class_name() const override { return class_name(); } @@ -35,17 +31,13 @@ class ScriptTrigger final : public TriggerBase virtual bool has_variable_size() const override { return true; } virtual ObjectSettings get_settings() override; - virtual void after_editor_set() override; virtual void event(Player& player, EventType type) override; virtual void draw(DrawingContext& context) override; - void write(Writer& writer); - private: EventType triggerevent; std::string script; - Vector new_size; bool must_activate; bool oneshot; int runcount; diff --git a/src/trigger/secretarea_trigger.cpp b/src/trigger/secretarea_trigger.cpp index c8c0ba3a796..857e0c009c6 100644 --- a/src/trigger/secretarea_trigger.cpp +++ b/src/trigger/secretarea_trigger.cpp @@ -32,48 +32,25 @@ static const float MESSAGE_TIME=3.5; SecretAreaTrigger::SecretAreaTrigger(const ReaderMapping& reader) : - TriggerBase(reader), + Trigger(reader), message_timer(), message_displayed(false), message(), fade_tilemap(), - script(), - new_size(0.0f, 0.0f) + script() { - reader.get("x", m_col.m_bbox.get_left()); - reader.get("y", m_col.m_bbox.get_top()); - float w,h; - reader.get("width", w, 32.0f); - reader.get("height", h, 32.0f); - m_col.m_bbox.set_size(w, h); - new_size.x = w; - new_size.y = h; reader.get("fade-tilemap", fade_tilemap); reader.get("message", message); - if (message.empty() && !Editor::is_active()) { - message = _("You found a secret area!"); - } reader.get("script", script); -} -SecretAreaTrigger::SecretAreaTrigger(const Rectf& area, const std::string& fade_tilemap_) : - message_timer(), - message_displayed(false), - message(_("You found a secret area!")), - fade_tilemap(fade_tilemap_), - script(), - new_size(0.0f, 0.0f) -{ - m_col.m_bbox = area; + if (message.empty() && !Editor::is_active()) + message = _("You found a secret area!"); } ObjectSettings SecretAreaTrigger::get_settings() { - new_size.x = m_col.m_bbox.get_width(); - new_size.y = m_col.m_bbox.get_height(); - - ObjectSettings result = TriggerBase::get_settings(); + ObjectSettings result = Trigger::get_settings(); result.add_text(_("Name"), &m_name); result.add_text(_("Fade tilemap"), &fade_tilemap, "fade-tilemap"); @@ -85,18 +62,6 @@ SecretAreaTrigger::get_settings() return result; } -void -SecretAreaTrigger::after_editor_set() -{ - m_col.m_bbox.set_size(new_size.x, new_size.y); -} - -std::string -SecretAreaTrigger::get_fade_tilemap_name() const -{ - return fade_tilemap; -} - void SecretAreaTrigger::draw(DrawingContext& context) { diff --git a/src/trigger/secretarea_trigger.hpp b/src/trigger/secretarea_trigger.hpp index c6d7b2c04b0..a32a237b5bb 100644 --- a/src/trigger/secretarea_trigger.hpp +++ b/src/trigger/secretarea_trigger.hpp @@ -22,15 +22,14 @@ #include "supertux/timer.hpp" class Color; -class DrawingContext; -class ReaderMapping; -class SecretAreaTrigger final : public TriggerBase +class SecretAreaTrigger final : public Trigger { +public: static Color text_color; + public: SecretAreaTrigger(const ReaderMapping& reader); - SecretAreaTrigger(const Rectf& area, const std::string& fade_tilemap = ""); static std::string class_name() { return "secretarea"; } virtual std::string get_class_name() const override { return class_name(); } @@ -39,12 +38,11 @@ class SecretAreaTrigger final : public TriggerBase virtual bool has_variable_size() const override { return true; } virtual ObjectSettings get_settings() override; - virtual void after_editor_set() override; virtual void event(Player& player, EventType type) override; virtual void draw(DrawingContext& context) override; - std::string get_fade_tilemap_name() const; + const std::string& get_fade_tilemap_name() const { return fade_tilemap; } private: Timer message_timer; @@ -52,7 +50,6 @@ class SecretAreaTrigger final : public TriggerBase std::string message; /**< message to display, default "You found a secret area!" */ std::string fade_tilemap; /**< tilemap to fade away when trigger is activated, or empty if you don't care */ std::string script; /**< optional script to run when trigger is activated */ - Vector new_size; private: SecretAreaTrigger(const SecretAreaTrigger&) = delete; diff --git a/src/trigger/sequence_trigger.cpp b/src/trigger/sequence_trigger.cpp index 95e42d372ad..299bf7f1c58 100644 --- a/src/trigger/sequence_trigger.cpp +++ b/src/trigger/sequence_trigger.cpp @@ -24,53 +24,26 @@ #include "video/drawing_context.hpp" SequenceTrigger::SequenceTrigger(const ReaderMapping& reader) : + Trigger(reader), triggerevent(EVENT_TOUCH), sequence(SEQ_ENDSEQUENCE), - new_size(0.0f, 0.0f), new_spawnpoint(), fade_tilemap(), fade() { - reader.get("x", m_col.m_bbox.get_left(), 0.0f); - reader.get("y", m_col.m_bbox.get_top(), 0.0f); - float w, h; - reader.get("width", w, 32.0f); - reader.get("height", h, 32.0f); - m_col.m_bbox.set_size(w, h); - new_size.x = w; - new_size.y = h; std::string sequence_name; - if (reader.get("sequence", sequence_name)) { + if (reader.get("sequence", sequence_name)) sequence = string_to_sequence(sequence_name); - } reader.get("new_spawnpoint", new_spawnpoint); reader.get("fade_tilemap", fade_tilemap); reader.get("fade", reinterpret_cast(fade)); } -SequenceTrigger::SequenceTrigger(const Vector& pos, const std::string& sequence_name) : - triggerevent(EVENT_TOUCH), - sequence(string_to_sequence(sequence_name)), - new_size(0.0f, 0.0f), - new_spawnpoint(), - fade_tilemap(), - fade() -{ - m_col.m_bbox.set_pos(pos); - m_col.m_bbox.set_size(32, 32); -} - ObjectSettings SequenceTrigger::get_settings() { - new_size.x = m_col.m_bbox.get_width(); - new_size.y = m_col.m_bbox.get_height(); - - ObjectSettings result = TriggerBase::get_settings(); - - //result.add_float(_("Width"), &new_size.x, "width"); - //result.add_float(_("Height"), &new_size.y, "height"); + ObjectSettings result = Trigger::get_settings(); result.add_enum(_("Sequence"), reinterpret_cast(&sequence), {_("end sequence"), _("stop Tux"), _("fireworks")}, @@ -88,19 +61,14 @@ SequenceTrigger::get_settings() return result; } -void -SequenceTrigger::after_editor_set() -{ - m_col.m_bbox.set_size(new_size.x, new_size.y); -} - void SequenceTrigger::event(Player& player, EventType type) { - if (type == triggerevent) { - auto data = SequenceData(new_spawnpoint, fade_tilemap, fade); - player.trigger_sequence(sequence, &data); - } + if (type != triggerevent) + return; + + auto data = SequenceData(new_spawnpoint, fade_tilemap, fade); + player.trigger_sequence(sequence, &data); } std::string @@ -112,10 +80,9 @@ SequenceTrigger::get_sequence_name() const void SequenceTrigger::draw(DrawingContext& context) { - if (Editor::is_active() || g_debug.show_collision_rects) { + if (Editor::is_active() || g_debug.show_collision_rects) context.color().draw_filled_rect(m_col.m_bbox, Color(1.0f, 0.0f, 0.0f, 0.6f), 0.0f, LAYER_OBJECTS); - } } /* EOF */ diff --git a/src/trigger/sequence_trigger.hpp b/src/trigger/sequence_trigger.hpp index cf60df6f56a..4de2fcb446b 100644 --- a/src/trigger/sequence_trigger.hpp +++ b/src/trigger/sequence_trigger.hpp @@ -17,17 +17,14 @@ #ifndef HEADER_SUPERTUX_TRIGGER_SEQUENCE_TRIGGER_HPP #define HEADER_SUPERTUX_TRIGGER_SEQUENCE_TRIGGER_HPP -#include "supertux/sequence.hpp" #include "trigger/trigger_base.hpp" -class Player; -class ReaderMapping; +#include "supertux/sequence.hpp" -class SequenceTrigger final : public TriggerBase +class SequenceTrigger final : public Trigger { public: SequenceTrigger(const ReaderMapping& reader); - SequenceTrigger(const Vector& pos, const std::string& sequence_name); static std::string class_name() { return "sequencetrigger"; } virtual std::string get_class_name() const override { return class_name(); } @@ -36,7 +33,6 @@ class SequenceTrigger final : public TriggerBase virtual bool has_variable_size() const override { return true; } virtual ObjectSettings get_settings() override; - virtual void after_editor_set() override; virtual void event(Player& player, EventType type) override; virtual void draw(DrawingContext& context) override; @@ -46,7 +42,6 @@ class SequenceTrigger final : public TriggerBase private: EventType triggerevent; Sequence sequence; - Vector new_size; std::string new_spawnpoint; std::string fade_tilemap; TilemapFadeType fade; diff --git a/src/trigger/switch.cpp b/src/trigger/switch.cpp index 23f958f1d38..4c0fba4ccb3 100644 --- a/src/trigger/switch.cpp +++ b/src/trigger/switch.cpp @@ -19,36 +19,26 @@ #include #include "audio/sound_manager.hpp" -#include "sprite/sprite.hpp" -#include "sprite/sprite_manager.hpp" #include "supertux/flip_level_transformer.hpp" #include "supertux/sector.hpp" #include "util/log.hpp" #include "util/reader_mapping.hpp" namespace { -const std::string SWITCH_SOUND = "sounds/switch.ogg"; -} + const std::string SWITCH_SOUND = "sounds/switch.ogg"; +} // namespace Switch::Switch(const ReaderMapping& reader) : - sprite_name(), - sprite(), + SpritedTrigger(reader, "images/objects/switch/left.sprite"), script(), off_script(), state(OFF), - bistable(), - m_flip(NO_FLIP) + bistable() { - if (!reader.get("x", m_col.m_bbox.get_left())) throw std::runtime_error("no x position set"); - if (!reader.get("y", m_col.m_bbox.get_top())) throw std::runtime_error("no y position set"); - if (!reader.get("sprite", sprite_name)) sprite_name = "images/objects/switch/left.sprite"; - sprite = SpriteManager::current()->create(sprite_name); - m_col.m_bbox.set_size(sprite->get_current_hitbox_width(), sprite->get_current_hitbox_height()); - reader.get("script", script); bistable = reader.get("off-script", off_script); - SoundManager::current()->preload( SWITCH_SOUND ); + SoundManager::current()->preload(SWITCH_SOUND); } Switch::~Switch() @@ -58,9 +48,8 @@ Switch::~Switch() ObjectSettings Switch::get_settings() { - ObjectSettings result = TriggerBase::get_settings(); + ObjectSettings result = SpritedTrigger::get_settings(); - result.add_sprite(_("Sprite"), &sprite_name, "sprite", std::string("images/objects/switch/left.sprite")); result.add_script(_("Turn on script"), &script, "script"); result.add_script(_("Turn off script"), &off_script, "off-script"); @@ -69,12 +58,6 @@ Switch::get_settings() return result; } -void -Switch::after_editor_set() { - sprite = SpriteManager::current()->create(sprite_name); - m_col.m_bbox.set_size(sprite->get_current_hitbox_width(), sprite->get_current_hitbox_height()); -} - void Switch::update(float ) { @@ -82,42 +65,36 @@ Switch::update(float ) case OFF: break; case TURN_ON: - if (sprite->animation_done()) { + if (m_sprite->animation_done()) { std::ostringstream location; location << "switch" << m_col.m_bbox.p1(); Sector::get().run_script(script, location.str()); - sprite->set_action("on", 1); + set_action("on", 1); state = ON; } break; case ON: - if (sprite->animation_done() && !bistable) { - sprite->set_action("turnoff", 1); + if (m_sprite->animation_done() && !bistable) { + set_action("turnoff", 1); state = TURN_OFF; } break; case TURN_OFF: - if (sprite->animation_done()) { + if (m_sprite->animation_done()) { if (bistable) { std::ostringstream location; location << "switch" << m_col.m_bbox.p1(); Sector::get().run_script(off_script, location.str()); } - sprite->set_action("off"); + set_action("off"); state = OFF; } break; } } -void -Switch::draw(DrawingContext& context) -{ - sprite->draw(context.color(), m_col.m_bbox.p1(), LAYER_TILES, m_flip); -} - void Switch::event(Player& , EventType type) { @@ -125,7 +102,7 @@ Switch::event(Player& , EventType type) switch (state) { case OFF: - sprite->set_action("turnon", 1); + set_action("turnon", 1); SoundManager::current()->play(SWITCH_SOUND, get_pos()); state = TURN_ON; break; @@ -133,7 +110,7 @@ Switch::event(Player& , EventType type) break; case ON: if (bistable) { - sprite->set_action("turnoff", 1); + set_action("turnoff", 1); SoundManager::current()->play(SWITCH_SOUND, get_pos()); state = TURN_OFF; } @@ -146,7 +123,7 @@ Switch::event(Player& , EventType type) void Switch::on_flip(float height) { - TriggerBase::on_flip(height); + SpritedTrigger::on_flip(height); FlipLevelTransformer::transform_flip(m_flip); } diff --git a/src/trigger/switch.hpp b/src/trigger/switch.hpp index af9ba21563a..19a5d728c33 100644 --- a/src/trigger/switch.hpp +++ b/src/trigger/switch.hpp @@ -17,14 +17,9 @@ #ifndef HEADER_SUPERTUX_TRIGGER_SWITCH_HPP #define HEADER_SUPERTUX_TRIGGER_SWITCH_HPP -#include - #include "trigger/trigger_base.hpp" -#include "video/flip.hpp" - -class ReaderMapping; -class Switch final : public TriggerBase +class Switch final : public SpritedTrigger { public: Switch(const ReaderMapping& reader); @@ -36,10 +31,8 @@ class Switch final : public TriggerBase virtual std::string get_display_name() const override { return display_name(); } virtual ObjectSettings get_settings() override; - virtual void after_editor_set() override; virtual void update(float dt_sec) override; - virtual void draw(DrawingContext& context) override; virtual void event(Player& player, EventType type) override; virtual void on_flip(float height) override; @@ -53,13 +46,10 @@ class Switch final : public TriggerBase }; private: - std::string sprite_name; - SpritePtr sprite; std::string script; std::string off_script; SwitchState state; bool bistable; - Flip m_flip; private: Switch(const Switch&) = delete; diff --git a/src/trigger/text_area.cpp b/src/trigger/text_area.cpp index 6f5968b791a..de904bc3dd4 100644 --- a/src/trigger/text_area.cpp +++ b/src/trigger/text_area.cpp @@ -27,7 +27,7 @@ #include "video/layer.hpp" TextArea::TextArea(const ReaderMapping& mapping) : - TriggerBase(mapping), + Trigger(mapping), m_once(false), m_items(), m_delay(4.0f), @@ -38,12 +38,6 @@ TextArea::TextArea(const ReaderMapping& mapping) : m_anchor(AnchorPoint::ANCHOR_MIDDLE), m_anchor_offset(0, 0) { - float w, h; - - mapping.get("x", m_col.m_bbox.get_left(), 0.0f); - mapping.get("y", m_col.m_bbox.get_top(), 0.0f); - mapping.get("width", w, 32.0f); - mapping.get("height", h, 32.0f); mapping.get("strings", m_items); mapping.get("delay", m_delay); mapping.get("once", m_once); @@ -54,22 +48,6 @@ TextArea::TextArea(const ReaderMapping& mapping) : std::string anchor; if (mapping.get("anchor-point", anchor)) m_anchor = string_to_anchor_point(anchor); - - m_col.m_bbox.set_size(w, h); -} - -TextArea::TextArea(const Vector& pos) : - m_once(false), - m_items(), - m_delay(4.0f), - m_fade_delay(1.0f), - m_current_text(0), - m_status(Status::NOT_STARTED), - m_timer(), - m_anchor(AnchorPoint::ANCHOR_MIDDLE) -{ - m_col.m_bbox.set_pos(pos); - m_col.m_bbox.set_size(32, 32); } void @@ -113,7 +91,7 @@ TextArea::event(Player& player, EventType type) void TextArea::update(float dt_sec) { - TriggerBase::update(dt_sec); + Trigger::update(dt_sec); if (m_timer.check()) { @@ -158,7 +136,7 @@ TextArea::update(float dt_sec) ObjectSettings TextArea::get_settings() { - ObjectSettings settings = TriggerBase::get_settings(); + ObjectSettings settings = Trigger::get_settings(); settings.add_bool(_("Once"), &m_once, "once"); settings.add_float(_("Text change time"), &m_delay, "delay"); diff --git a/src/trigger/text_area.hpp b/src/trigger/text_area.hpp index ae0a6061165..0c0b3d9df31 100644 --- a/src/trigger/text_area.hpp +++ b/src/trigger/text_area.hpp @@ -19,9 +19,10 @@ #define HEADER_SUPERTUX_TRIGGER_TEXT_AREA_HPP #include "trigger/trigger_base.hpp" + #include "supertux/timer.hpp" -class TextArea final : public TriggerBase +class TextArea final : public Trigger { private: enum class Status @@ -35,7 +36,6 @@ class TextArea final : public TriggerBase public: TextArea(const ReaderMapping& mapping); - TextArea(const Vector& pos); virtual void draw(DrawingContext& context) override; virtual void event(Player& player, EventType type) override; diff --git a/src/trigger/trigger_base.cpp b/src/trigger/trigger_base.cpp index 034b66274f0..9e0a3af2177 100644 --- a/src/trigger/trigger_base.cpp +++ b/src/trigger/trigger_base.cpp @@ -19,21 +19,10 @@ #include "object/player.hpp" #include "sprite/sprite.hpp" -TriggerBase::TriggerBase(const ReaderMapping& mapping) : - MovingObject(mapping), - m_sprite(), - m_hit(), - m_losetouch_listeners() -{ - set_group(COLGROUP_TOUCHABLE); -} - TriggerBase::TriggerBase() : - m_sprite(), m_hit(), m_losetouch_listeners() { - set_group(COLGROUP_TOUCHABLE); } TriggerBase::~TriggerBase() @@ -46,7 +35,7 @@ TriggerBase::~TriggerBase() } void -TriggerBase::update(float ) +TriggerBase::update() { for (unsigned i = 0; i < m_losetouch_listeners.size(); i++) { @@ -62,15 +51,6 @@ TriggerBase::update(float ) m_hit.clear(); } -void -TriggerBase::draw(DrawingContext& context) -{ - if (!m_sprite.get()) - return; - - m_sprite->draw(context.color(), get_pos(), LAYER_TILES+1); -} - HitResponse TriggerBase::collision(GameObject& other, const CollisionHit& ) { @@ -96,4 +76,23 @@ TriggerBase::object_removed(GameObject* object) m_losetouch_listeners.end()); } + +Trigger::Trigger(const ReaderMapping& reader) : + MovingObject(reader) +{ + set_group(COLGROUP_TOUCHABLE); + + if (m_col.m_bbox.get_width() == 0.f) + m_col.m_bbox.set_width(32.f); + + if (m_col.m_bbox.get_height() == 0.f) + m_col.m_bbox.set_height(32.f); +} + + +SpritedTrigger::SpritedTrigger(const ReaderMapping& reader, const std::string& sprite_name) : + MovingSprite(reader, sprite_name, LAYER_TILES + 1, COLGROUP_TOUCHABLE) +{ +} + /* EOF */ diff --git a/src/trigger/trigger_base.hpp b/src/trigger/trigger_base.hpp index 176e8426cf2..8a8941d9776 100644 --- a/src/trigger/trigger_base.hpp +++ b/src/trigger/trigger_base.hpp @@ -17,21 +17,18 @@ #ifndef HEADER_SUPERTUX_TRIGGER_TRIGGER_BASE_HPP #define HEADER_SUPERTUX_TRIGGER_TRIGGER_BASE_HPP -#include - +#include "object/moving_sprite.hpp" #include "supertux/moving_object.hpp" #include "supertux/object_remove_listener.hpp" -#include "sprite/sprite_ptr.hpp" -#include "video/layer.hpp" +#include class Player; /** This class is the base class for all objects you can interact with in some way. There are several interaction types defined like touch and activate */ -class TriggerBase : public MovingObject, - public ObjectRemoveListener +class TriggerBase : public ObjectRemoveListener { public: enum EventType { @@ -41,24 +38,20 @@ class TriggerBase : public MovingObject, }; public: - TriggerBase(const ReaderMapping& mapping); TriggerBase(); ~TriggerBase() override; - virtual void update(float dt_sec) override; - virtual void draw(DrawingContext& context) override; - virtual HitResponse collision(GameObject& other, const CollisionHit& hit) override; - /** Receive trigger events */ virtual void event(Player& player, EventType type) = 0; /** Called by GameObject destructor of an object in losetouch_listeners */ virtual void object_removed(GameObject* object) override; - virtual int get_layer() const override { return LAYER_TILES + 1; } +protected: + void update(); + HitResponse collision(GameObject& other, const CollisionHit& hit); private: - SpritePtr m_sprite; std::vector m_hit; /** Players that will be informed when we lose touch with them */ @@ -69,6 +62,50 @@ class TriggerBase : public MovingObject, TriggerBase& operator=(const TriggerBase&) = delete; }; + +class Trigger : public MovingObject, + public TriggerBase +{ +public: + Trigger(const ReaderMapping& reader); + + virtual void update(float) override + { + TriggerBase::update(); + } + virtual HitResponse collision(GameObject& other, const CollisionHit& hit) override + { + return TriggerBase::collision(other, hit); + } + + int get_layer() const override { return LAYER_TILES + 1; } + +private: + Trigger(const Trigger&) = delete; + Trigger& operator=(const Trigger&) = delete; +}; + + +class SpritedTrigger : public MovingSprite, + public TriggerBase +{ +public: + SpritedTrigger(const ReaderMapping& reader, const std::string& sprite_name); + + virtual void update(float) override + { + TriggerBase::update(); + } + virtual HitResponse collision(GameObject& other, const CollisionHit& hit) override + { + return TriggerBase::collision(other, hit); + } + +private: + SpritedTrigger(const SpritedTrigger&) = delete; + SpritedTrigger& operator=(const SpritedTrigger&) = delete; +}; + #endif /* EOF */ From d437562b3293db22b90159db5dcc766c3f825ef7 Mon Sep 17 00:00:00 2001 From: MatusGuy <85036874+MatusGuy@users.noreply.github.com> Date: Tue, 25 Jul 2023 22:57:42 +0100 Subject: [PATCH 37/50] Prevent propagating movement to statics (#2552) --- src/collision/collision_object.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/collision/collision_object.cpp b/src/collision/collision_object.cpp index 8dcfcc45950..25b7ab8a0e3 100644 --- a/src/collision/collision_object.cpp +++ b/src/collision/collision_object.cpp @@ -81,6 +81,7 @@ CollisionObject::clear_bottom_collision_list() void CollisionObject::propagate_movement(const Vector& movement) { for (CollisionObject* other_object : m_objects_hit_bottom) { + if (other_object->get_group() == COLGROUP_STATIC) continue; m_ground_movement_manager->register_movement(*this, *other_object, movement); other_object->propagate_movement(movement); } From 9e13a5abb05b9eaabf4b2bfdf92b4293293b5c6d Mon Sep 17 00:00:00 2001 From: Vankata453 <78196474+Vankata453@users.noreply.github.com> Date: Tue, 25 Jul 2023 23:44:28 +0300 Subject: [PATCH 38/50] Fix translation issue with object display names Previously, `ObjectFactory` saved copies of object display names on game startup, which would cause them to not be translatable, unless the game is restarted. This is now fixed, since a factory function for getting the display name of an object is used instead. The `m_other_display_names` member variable of `ObjectFactory` was removed as well. Its only use was to save the name of the node marker, since it is a part of the objects in the editor, while not being an actual object, but rather a tool. It is planned to move the node marker to the toolbar in the future, so this variable would then be redundant. --- src/editor/toolbox_widget.cpp | 6 +++-- src/object/path.hpp | 2 -- src/supertux/game_object_factory.cpp | 10 +++++--- src/supertux/object_factory.cpp | 21 ++++++----------- src/supertux/object_factory.hpp | 34 +++++++++++++--------------- 5 files changed, 34 insertions(+), 39 deletions(-) diff --git a/src/editor/toolbox_widget.cpp b/src/editor/toolbox_widget.cpp index 62076b34346..a6c9d50e00a 100644 --- a/src/editor/toolbox_widget.cpp +++ b/src/editor/toolbox_widget.cpp @@ -449,8 +449,10 @@ EditorToolboxWidget::on_mouse_motion(const SDL_MouseMotionEvent& motion) try { obj_name = GameObjectFactory::instance().get_display_name(obj_class); } - catch (std::exception& err) { - log_warning << "Unable to find name for object with class \"" << obj_class << "\": " << err.what() << std::endl; + catch (std::exception&) { + // NOTE: Temporarily commented out, so hovering over node marker doesn't show a warning. + // When the node marker is moved as a tool, this should be uncommented. + // log_warning << "Unable to find name for object with class \"" << obj_class << "\": " << err.what() << std::endl; } m_object_tip = std::make_unique(obj_name); } diff --git a/src/object/path.hpp b/src/object/path.hpp index b00f0629b76..04bee8ee6dc 100644 --- a/src/object/path.hpp +++ b/src/object/path.hpp @@ -73,8 +73,6 @@ class Path final easing() {} - static std::string display_name() { return _("Path Node"); } - Path& get_parent() const { return *parent; } }; diff --git a/src/supertux/game_object_factory.cpp b/src/supertux/game_object_factory.cpp index cbcfd321052..7eb9bb1c3d5 100644 --- a/src/supertux/game_object_factory.cpp +++ b/src/supertux/game_object_factory.cpp @@ -266,7 +266,6 @@ GameObjectFactory::init_factories() add_factory("leveltime"); add_factory("lit-object"); add_factory("magicblock"); - add_display_name("#node", Path::Node::display_name()); add_factory("particle-zone"); add_factory("platform"); add_factory("pneumatic-platform"); @@ -309,10 +308,15 @@ GameObjectFactory::init_factories() add_factory("teleporter", OBJ_PARAM_WORLDMAP); add_factory("worldmap-spawnpoint", OBJ_PARAM_WORLDMAP); - add_factory("tilemap", TileMap::display_name(), [](const ReaderMapping& reader) { + add_factory("tilemap", { + [](const ReaderMapping& reader) { auto tileset = TileManager::current()->get_tileset(Level::current()->get_tileset()); return std::make_unique(tileset, reader); - }); + }, + []() { + return TileMap::display_name(); + } + }); } std::unique_ptr diff --git a/src/supertux/object_factory.cpp b/src/supertux/object_factory.cpp index 6eb135dca78..058a4d819dd 100644 --- a/src/supertux/object_factory.cpp +++ b/src/supertux/object_factory.cpp @@ -26,9 +26,7 @@ ObjectFactory::ObjectFactory() : m_badguys_names(), m_badguys_params(), m_objects_names(), - m_objects_display_names(), m_objects_params(), - m_other_display_names(), m_adding_badguys(false) { } @@ -46,29 +44,24 @@ ObjectFactory::create(const std::string& name, const ReaderMapping& reader) cons } else { - return it->second(reader); + return it->second.create(reader); } } std::string ObjectFactory::get_display_name(const std::string& name) const { - auto it = std::find(m_objects_names.begin(), m_objects_names.end(), name); + auto it = factories.find(name); - if (it == m_objects_names.end()) + if (it == factories.end()) { - auto it_other_names = m_other_display_names.find(name); // Attempt to find display name in non-factory object names. - if (it_other_names == m_other_display_names.end()) - { - std::stringstream msg; - msg << "No display name for object '" << name << "' found."; - throw std::runtime_error(msg.str()); - } - return it_other_names->second; + std::stringstream msg; + msg << "No factory for object '" << name << "' found. Unable to get display name."; + throw std::runtime_error(msg.str()); } else { - return m_objects_display_names[std::distance(m_objects_names.begin(), it)]; + return it->second.get_display_name(); } } diff --git a/src/supertux/object_factory.hpp b/src/supertux/object_factory.hpp index f123847dc86..c9b28071ce7 100644 --- a/src/supertux/object_factory.hpp +++ b/src/supertux/object_factory.hpp @@ -27,22 +27,23 @@ #include "math/fwd.hpp" #include "supertux/direction.hpp" -class ReaderMapping; class GameObject; +class ReaderMapping; class ObjectFactory { private: - typedef std::function (const ReaderMapping&)> FactoryFunction; - typedef std::map Factories; + struct FactoryFunctions { + std::function (const ReaderMapping&)> create; + std::function get_display_name; + }; + typedef std::map Factories; Factories factories; std::vector m_badguys_names; std::vector m_badguys_params; std::vector m_objects_names; - std::vector m_objects_display_names; std::vector m_objects_params; - std::map m_other_display_names; // Stores display names for non-factory objects. protected: bool m_adding_badguys; @@ -71,13 +72,7 @@ class ObjectFactory protected: ObjectFactory(); - void add_display_name(const char* class_name, const std::string& display_name) - { - assert(m_other_display_names.find(class_name) == m_other_display_names.end()); - m_other_display_names[class_name] = display_name; - } - - void add_factory(const char* name, const std::string& display_name, const FactoryFunction& func, uint8_t obj_params = 0) + void add_factory(const char* name, const FactoryFunctions functions, uint8_t obj_params = 0) { assert(factories.find(name) == factories.end()); if (m_adding_badguys) @@ -86,17 +81,20 @@ class ObjectFactory m_badguys_params.push_back(obj_params); } m_objects_names.push_back(name); - m_objects_display_names.push_back(display_name); m_objects_params.push_back(obj_params); - factories[name] = func; + factories[name] = std::move(functions); } template - void add_factory(const char* class_name, uint8_t obj_params = 0, const std::string& display_name = "") + void add_factory(const char* class_name, uint8_t obj_params = 0) { - add_factory(class_name, display_name.empty() ? C::display_name() : display_name, - [](const ReaderMapping& reader) { - return std::make_unique(reader); + add_factory(class_name, { + [](const ReaderMapping& reader) { + return std::make_unique(reader); + }, + []() { + return C::display_name(); + } }, obj_params); } }; From 1f62f93c54bbdd63ab0305b1b6ff9953d6ad62f7 Mon Sep 17 00:00:00 2001 From: Carsten Wirtz Date: Wed, 26 Jul 2023 16:05:16 +0200 Subject: [PATCH 39/50] Add invisible retro block sprite for display in editor [ci skip] --- .../images/objects/bonus_block/box-invisible.png | Bin 0 -> 9487 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 data/images/objects/bonus_block/box-invisible.png diff --git a/data/images/objects/bonus_block/box-invisible.png b/data/images/objects/bonus_block/box-invisible.png new file mode 100644 index 0000000000000000000000000000000000000000..e5c0f3a5ca4af0ce209dba898e8977a5b8b1202e GIT binary patch literal 9487 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 Radp3O8tIwoKGAlK{4bp;$D{xN literal 0 HcmV?d00001 From c31af16c9a7eb0d680bcd5ca766771bedf97bf71 Mon Sep 17 00:00:00 2001 From: Carsten Wirtz Date: Fri, 28 Jul 2023 00:21:41 +0200 Subject: [PATCH 40/50] Fix missing frozen sprite for Old Bomb [ci skip] --- data/images/creatures/mr_bomb/old_bomb/old_bomb.sprite | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/data/images/creatures/mr_bomb/old_bomb/old_bomb.sprite b/data/images/creatures/mr_bomb/old_bomb/old_bomb.sprite index a1082df2752..c9911b2c7b9 100644 --- a/data/images/creatures/mr_bomb/old_bomb/old_bomb.sprite +++ b/data/images/creatures/mr_bomb/old_bomb/old_bomb.sprite @@ -20,12 +20,12 @@ (action (name "iced-left") - (hitbox 5 8 32 32) - (images "iced-left.png")) + (hitbox 14 19 32 32) + (images "left-0.png")) (action (name "iced-right") - (hitbox 5 8 32 32) + (hitbox 14 19 32 32) (mirror-action "iced-left")) (action From 28c4786f7994856d68ccea9283c407a4aed375d6 Mon Sep 17 00:00:00 2001 From: Marty <85036874+MatusGuy@users.noreply.github.com> Date: Mon, 31 Jul 2023 12:01:38 +0100 Subject: [PATCH 41/50] Fix buttjump hitbox to not include actual player (#2562) This prevents being able to buttjump to kill enemies while they're being grabbed --- src/object/player.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/object/player.cpp b/src/object/player.cpp index c2d4e246311..57b3441db05 100644 --- a/src/object/player.cpp +++ b/src/object/player.cpp @@ -748,14 +748,15 @@ Player::update(float dt_sec) if (m_does_buttjump || (m_stone && m_physic.get_velocity_y() > 30.f && !m_coyote_timer.started())) { Rectf downbox = get_bbox().grown(-1.f); - downbox.set_bottom(get_bbox().get_bottom() + 16.f); + downbox.set_top(get_bbox().get_bottom()); + downbox.set_bottom(downbox.get_bottom() + 16.f); for (auto& brick : Sector::get().get_objects_by_type()) { // stoneform breaks through any kind of bricks if (downbox.contains(brick.get_bbox()) && (m_stone || !dynamic_cast(&brick))) brick.try_break(this, is_big()); } for (auto& badguy : Sector::get().get_objects_by_type()) { - if (downbox.contains(badguy.get_bbox()) && badguy.is_snipable()) + if (downbox.contains(badguy.get_bbox()) && badguy.is_snipable() && !badguy.is_grabbed()) badguy.kill_fall(); } } From 8652dadf77f5143176a7f81eb9bc8fe979b1974f Mon Sep 17 00:00:00 2001 From: Tobias Markus Date: Mon, 31 Jul 2023 21:09:38 +0200 Subject: [PATCH 42/50] Remove unneeded Rect declaration in DrawingContext::get_viewport() --- src/video/drawing_context.cpp | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/video/drawing_context.cpp b/src/video/drawing_context.cpp index dcf619f3812..437f4fbdb32 100644 --- a/src/video/drawing_context.cpp +++ b/src/video/drawing_context.cpp @@ -114,13 +114,6 @@ DrawingContext::pop_transform() const Rect DrawingContext::get_viewport() const { - Rect tmp( - static_cast(static_cast(m_viewport.left) / transform().scale), - static_cast(static_cast(m_viewport.top) / transform().scale), - static_cast(static_cast(m_viewport.right) / transform().scale), - static_cast(static_cast(m_viewport.bottom) / transform().scale) - ); - return m_viewport; } From 2447cd6becbfc44d57f5f580eb73f83885660b2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Pale=C4=8Dek?= Date: Mon, 31 Jul 2023 21:16:43 +0200 Subject: [PATCH 43/50] Improvements to Dart and DartTrap (#2554) Removes all hard-coded coordinates and offsets in darttrap.cpp, now the darts will always spawn in the centre of the hitbox's height (width for up and down darts). This makes it much more intuitive to create custom sprites for DartTrap. The default sprite is adjusted for this in a way that won't affect existing levels. It also makes it possible to change the sprite of the dispensed Dart from DartTrap's menu in the editor. Darts and DartTraps now also support UP and DOWN directions, in addition to LEFT and RIGHT. This PR also cleans up darttrap.cpp/hpp according to the guidelines, namely adding the m_ prefix to member variables. --- data/images/creatures/dart/dart.sprite | 18 ++- data/images/creatures/dart/flying-down.png | Bin 0 -> 10823 bytes .../dart/{flying.png => flying-left.png} | Bin .../images/creatures/darttrap/darttrap.sprite | 76 ++++++++---- data/images/creatures/darttrap/down-1.png | Bin 0 -> 11296 bytes data/images/creatures/darttrap/down-2.png | Bin 0 -> 11639 bytes data/images/creatures/darttrap/down-3.png | Bin 0 -> 11704 bytes data/images/creatures/darttrap/down-4.png | Bin 0 -> 10963 bytes data/images/creatures/darttrap/down-idle.png | Bin 0 -> 10897 bytes .../creatures/darttrap/{d1.png => left-1.png} | Bin .../creatures/darttrap/{d2.png => left-2.png} | Bin .../darttrap/{d3-.png => left-3.png} | Bin .../creatures/darttrap/{d4.png => left-4.png} | Bin .../darttrap/{d-idle.png => left-idle.png} | Bin data/images/engine/editor/objects.stoi | 4 +- src/badguy/dart.cpp | 29 ++++- src/badguy/dart.hpp | 3 +- src/badguy/darttrap.cpp | 117 +++++++++++------- src/badguy/darttrap.hpp | 18 +-- 19 files changed, 181 insertions(+), 84 deletions(-) create mode 100644 data/images/creatures/dart/flying-down.png rename data/images/creatures/dart/{flying.png => flying-left.png} (100%) create mode 100644 data/images/creatures/darttrap/down-1.png create mode 100644 data/images/creatures/darttrap/down-2.png create mode 100644 data/images/creatures/darttrap/down-3.png create mode 100644 data/images/creatures/darttrap/down-4.png create mode 100644 data/images/creatures/darttrap/down-idle.png rename data/images/creatures/darttrap/{d1.png => left-1.png} (100%) rename data/images/creatures/darttrap/{d2.png => left-2.png} (100%) rename data/images/creatures/darttrap/{d3-.png => left-3.png} (100%) rename data/images/creatures/darttrap/{d4.png => left-4.png} (100%) rename data/images/creatures/darttrap/{d-idle.png => left-idle.png} (100%) diff --git a/data/images/creatures/dart/dart.sprite b/data/images/creatures/dart/dart.sprite index b98b7ef44ab..88c8f79114a 100644 --- a/data/images/creatures/dart/dart.sprite +++ b/data/images/creatures/dart/dart.sprite @@ -2,11 +2,23 @@ (action (name "flying-left") (hitbox 10 6 12 7) - (images "flying.png") - ) + (images "flying-left.png") + ) (action (name "flying-right") (hitbox 10 6 12 7) (mirror-action "flying-left") - ) ) + (action + (name "flying-down") + (hitbox 6 10 7 12) + (images "flying-down.png") + ) + ;; This will never show up since the editor uses the flipped down texture + ;; But it prevents warnings for missing actions + (action + (name "editor-up") + (hitbox 6 10 7 12) + (images "flying-down.png") + ) +) diff --git a/data/images/creatures/dart/flying-down.png b/data/images/creatures/dart/flying-down.png new file mode 100644 index 0000000000000000000000000000000000000000..e2452125de214eae021c6245788c62e0a85dfc0b GIT binary patch literal 10823 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 n`*_}25~}uDwHWnyegg= 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 literal 0 HcmV?d00001 diff --git a/data/images/creatures/darttrap/down-2.png b/data/images/creatures/darttrap/down-2.png new file mode 100644 index 0000000000000000000000000000000000000000..06fd8edec4d875d65d8cefc167ddd7fa61e9e215 GIT binary patch literal 11639 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 literal 0 HcmV?d00001 diff --git a/data/images/creatures/darttrap/down-3.png b/data/images/creatures/darttrap/down-3.png new file mode 100644 index 0000000000000000000000000000000000000000..b516cfa38a64b93e502760a3dd440d0fe682330f GIT binary patch literal 11704 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 YhIUrKs{PJ(ob3O4TE?2?>drU+3mjCC-~a#s literal 0 HcmV?d00001 diff --git a/data/images/creatures/darttrap/down-idle.png b/data/images/creatures/darttrap/down-idle.png new file mode 100644 index 0000000000000000000000000000000000000000..87588aeb743c13eb4f582123ed9f75cdefac2f2d GIT binary patch literal 10897 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 Y0Pf*RM2bR^$mxEsp`xo?rf41fUz$~OU;qFB literal 0 HcmV?d00001 diff --git a/data/images/creatures/darttrap/d1.png b/data/images/creatures/darttrap/left-1.png similarity index 100% rename from data/images/creatures/darttrap/d1.png rename to data/images/creatures/darttrap/left-1.png diff --git a/data/images/creatures/darttrap/d2.png b/data/images/creatures/darttrap/left-2.png similarity index 100% rename from data/images/creatures/darttrap/d2.png rename to data/images/creatures/darttrap/left-2.png diff --git a/data/images/creatures/darttrap/d3-.png b/data/images/creatures/darttrap/left-3.png similarity index 100% rename from data/images/creatures/darttrap/d3-.png rename to data/images/creatures/darttrap/left-3.png diff --git a/data/images/creatures/darttrap/d4.png b/data/images/creatures/darttrap/left-4.png similarity index 100% rename from data/images/creatures/darttrap/d4.png rename to data/images/creatures/darttrap/left-4.png diff --git a/data/images/creatures/darttrap/d-idle.png b/data/images/creatures/darttrap/left-idle.png similarity index 100% rename from data/images/creatures/darttrap/d-idle.png rename to data/images/creatures/darttrap/left-idle.png diff --git a/data/images/engine/editor/objects.stoi b/data/images/engine/editor/objects.stoi index df295ab97ea..0880d34252b 100644 --- a/data/images/engine/editor/objects.stoi +++ b/data/images/engine/editor/objects.stoi @@ -155,7 +155,7 @@ (icon "images/creatures/angrystone/charging-0.png")) (object (class "darttrap") - (icon "images/creatures/darttrap/d1.png")) + (icon "images/creatures/darttrap/left-1.png")) (object (class "dispenser") (icon "images/creatures/dispenser/cannon_left.png")) @@ -192,7 +192,7 @@ (icon "images/creatures/skydive/skydive1.png")) (object (class "dart") - (icon "images/creatures/dart/flying.png")) + (icon "images/creatures/dart/flying-left.png")) ) (objectgroup diff --git a/src/badguy/dart.cpp b/src/badguy/dart.cpp index 59afdf8cc4a..1dbc1194ebd 100644 --- a/src/badguy/dart.cpp +++ b/src/badguy/dart.cpp @@ -39,13 +39,14 @@ Dart::Dart(const ReaderMapping& reader) : SoundManager::current()->preload("sounds/stomp.wav"); } -Dart::Dart(const Vector& pos, Direction d, const BadGuy* parent_ = nullptr) : - BadGuy(pos, d, "images/creatures/dart/dart.sprite"), +Dart::Dart(const Vector& pos, Direction d, const BadGuy* parent_, const std::string& sprite, Flip flip) : + BadGuy(pos, d, sprite), parent(parent_), sound_source() { m_physic.enable_gravity(false); m_countMe = false; + m_flip = flip; SoundManager::current()->preload(DART_SOUND); SoundManager::current()->preload("sounds/darthit.wav"); SoundManager::current()->preload("sounds/stomp.wav"); @@ -64,8 +65,10 @@ Dart::updatePointers(const GameObject* from_object, GameObject* to_object) void Dart::initialize() { - m_physic.set_velocity_x(m_dir == Direction::LEFT ? -::DART_SPEED : ::DART_SPEED); - set_action("flying", m_dir); + m_physic.set_velocity_x(m_dir == Direction::LEFT ? -::DART_SPEED : m_dir == Direction::RIGHT ? ::DART_SPEED : 0); + m_physic.set_velocity_y(m_dir == Direction::UP ? -::DART_SPEED : m_dir == Direction::DOWN ? ::DART_SPEED : 0); + set_action("flying", m_dir == Direction::UP ? Direction::DOWN : m_dir); + if (m_dir == Direction::UP) m_flip = VERTICAL_FLIP; } void @@ -143,11 +146,29 @@ Dart::play_looping_sounds() } } +void +Dart::after_editor_set() +{ + BadGuy::after_editor_set(); + if ((m_dir == Direction::UP && m_flip == NO_FLIP) || (m_dir == Direction::DOWN && m_flip == VERTICAL_FLIP)) + FlipLevelTransformer::transform_flip(m_flip); +} + void Dart::on_flip(float height) { BadGuy::on_flip(height); FlipLevelTransformer::transform_flip(m_flip); + if (m_dir == Direction::UP) + { + m_dir = Direction::DOWN; + m_physic.set_velocity_y(::DART_SPEED); + } + else if (m_dir == Direction::DOWN) + { + m_dir = Direction::UP; + m_physic.set_velocity_y(-::DART_SPEED); + } } /* EOF */ diff --git a/src/badguy/dart.hpp b/src/badguy/dart.hpp index 49e1263f748..489d441977a 100644 --- a/src/badguy/dart.hpp +++ b/src/badguy/dart.hpp @@ -26,7 +26,7 @@ class Dart final : public BadGuy { public: Dart(const ReaderMapping& reader); - Dart(const Vector& pos, Direction d, const BadGuy* parent); + Dart(const Vector& pos, Direction d, const BadGuy* parent, const std::string& sprite = "images/creatures/dart/dart.sprite", Flip flip = NO_FLIP); virtual void initialize() override; virtual void activate() override; @@ -51,6 +51,7 @@ class Dart final : public BadGuy virtual void stop_looping_sounds() override; virtual void play_looping_sounds() override; + virtual void after_editor_set() override; virtual void on_flip(float height) override; protected: diff --git a/src/badguy/darttrap.cpp b/src/badguy/darttrap.cpp index 97c5692c6eb..4b886a90629 100644 --- a/src/badguy/darttrap.cpp +++ b/src/badguy/darttrap.cpp @@ -26,64 +26,62 @@ #include "util/log.hpp" #include "util/reader_mapping.hpp" -namespace { -const float MUZZLE_Y = 25; /**< [px] muzzle y-offset from top */ -} - DartTrap::DartTrap(const ReaderMapping& reader) : BadGuy(reader, "images/creatures/darttrap/darttrap.sprite", LAYER_TILES-1), - enabled(true), - initial_delay(), - fire_delay(), - ammo(), - state(IDLE), - fire_timer() + m_enabled(true), + m_initial_delay(), + m_fire_delay(), + m_ammo(), + m_dart_sprite("images/creatures/dart/dart.sprite"), + m_state(IDLE), + m_fire_timer() { - reader.get("enabled", enabled, true); - reader.get("initial-delay", initial_delay, 0.0f); - reader.get("fire-delay", fire_delay, 2.0f); - reader.get("ammo", ammo, -1); + reader.get("enabled", m_enabled, true); + reader.get("initial-delay", m_initial_delay, 0.0f); + reader.get("fire-delay", m_fire_delay, 2.0f); + reader.get("ammo", m_ammo, -1); + reader.get("dart-sprite", m_dart_sprite, "images/creatures/dart/dart.sprite"); m_countMe = false; SoundManager::current()->preload("sounds/dartfire.wav"); if (m_start_dir == Direction::AUTO) { log_warning << "Setting a DartTrap's direction to AUTO is no good idea" << std::endl; } - state = IDLE; + m_state = IDLE; set_colgroup_active(COLGROUP_DISABLED); if (!Editor::is_active()) { - if (initial_delay == 0) initial_delay = 0.1f; + if (m_initial_delay == 0) m_initial_delay = 0.1f; } } void DartTrap::initialize() { - set_action("idle", m_dir); + set_action("idle", m_dir == Direction::UP ? Direction::DOWN : m_dir); + if (m_dir == Direction::UP) m_flip = VERTICAL_FLIP; } void DartTrap::activate() { - fire_timer.start(initial_delay); + m_fire_timer.start(m_initial_delay); } HitResponse -DartTrap::collision_player(Player& , const CollisionHit& ) +DartTrap::collision_player(Player&, const CollisionHit& ) { return ABORT_MOVE; } void -DartTrap::active_update(float ) +DartTrap::active_update(float) { - if (!enabled) { - return; - } - switch (state) { + if (!m_enabled) return; + + switch (m_state) { case IDLE: - if ((ammo != 0) && (fire_timer.check())) { - if (ammo > 0) ammo--; + if ((m_ammo != 0) && (m_fire_timer.check())) { + if (m_ammo > 0) m_ammo--; load(); - fire_timer.start(fire_delay); + m_fire_timer.start(m_fire_delay); } break; @@ -101,25 +99,41 @@ DartTrap::active_update(float ) void DartTrap::load() { - state = LOADING; - set_action("loading", m_dir, 1); + m_state = LOADING; + set_action("loading", m_dir == Direction::UP ? Direction::DOWN : m_dir, 1); } void DartTrap::fire() { - float px = get_pos().x; - if (m_dir == Direction::RIGHT) px += 5; - float py = get_pos().y; - if (m_flip == NO_FLIP) - py += MUZZLE_Y; - else - py += (m_col.m_bbox.get_height() - MUZZLE_Y - 7.0f); - SoundManager::current()->play("sounds/dartfire.wav", get_pos()); - Sector::get().add(Vector(px, py), m_dir, this); - state = IDLE; - set_action("idle", m_dir); + Dart &dart = Sector::get().add(Vector(0.f, 0.f), m_dir, this, m_dart_sprite, m_flip); + + Vector pos; + switch (m_dir) + { + case Direction::RIGHT: + pos = Vector(get_pos().x, + get_pos().y + m_col.m_bbox.get_height() / 2 - dart.get_bbox().get_height() / 2); + break; + case Direction::UP: + pos = Vector(get_pos().x + m_col.m_bbox.get_width() / 2 - dart.get_bbox().get_width() / 2, + get_pos().y + m_col.m_bbox.get_height() - dart.get_bbox().get_height()); + break; + case Direction::DOWN: + pos = Vector(get_pos().x + m_col.m_bbox.get_width() / 2 - dart.get_bbox().get_width() / 2, + get_pos().y); + break; + default: + pos = Vector(get_pos().x + m_col.m_bbox.get_width() - dart.get_bbox().get_width(), + get_pos().y + m_col.m_bbox.get_height() / 2 - dart.get_bbox().get_height() / 2); + break; + } + + dart.set_pos(pos); + + m_state = IDLE; + set_action("idle", m_dir == Direction::UP ? Direction::DOWN : m_dir); } ObjectSettings @@ -127,21 +141,34 @@ DartTrap::get_settings() { ObjectSettings result = BadGuy::get_settings(); - result.add_float(_("Initial delay"), &initial_delay, "initial-delay"); - result.add_bool(_("Enabled"), &enabled, "enabled", true); - result.add_float(_("Fire delay"), &fire_delay, "fire-delay"); - result.add_int(_("Ammo"), &ammo, "ammo"); + result.add_float(_("Initial delay"), &m_initial_delay, "initial-delay"); + result.add_bool(_("Enabled"), &m_enabled, "enabled", true); + result.add_float(_("Fire delay"), &m_fire_delay, "fire-delay"); + result.add_int(_("Ammo"), &m_ammo, "ammo"); + result.add_sprite(_("Dart sprite"), &m_dart_sprite, "dart-sprite", "images/creatures/dart/dart.sprite"); - result.reorder({"initial-delay", "fire-delay", "ammo", "direction", "x", "y"}); + result.reorder({"initial-delay", "fire-delay", "ammo", "direction", "x", "y", "dart-sprite"}); return result; } +void +DartTrap::after_editor_set() +{ + BadGuy::after_editor_set(); + if ((m_dir == Direction::UP && m_flip == NO_FLIP) || (m_dir == Direction::DOWN && m_flip == VERTICAL_FLIP)) + FlipLevelTransformer::transform_flip(m_flip); +} + void DartTrap::on_flip(float height) { BadGuy::on_flip(height); FlipLevelTransformer::transform_flip(m_flip); + if (m_dir == Direction::UP) + m_dir = Direction::DOWN; + else if (m_dir == Direction::DOWN) + m_dir = Direction::UP; } /* EOF */ diff --git a/src/badguy/darttrap.hpp b/src/badguy/darttrap.hpp index cae9e83dcde..a5d31e4cf5e 100644 --- a/src/badguy/darttrap.hpp +++ b/src/badguy/darttrap.hpp @@ -37,6 +37,7 @@ class DartTrap final : public BadGuy virtual ObjectSettings get_settings() override; + virtual void after_editor_set() override; virtual void on_flip(float height) override; protected: @@ -44,17 +45,18 @@ class DartTrap final : public BadGuy IDLE, LOADING }; - void load(); /**< load a shot */ - void fire(); /**< fire a shot */ + void load(); + void fire(); private: - bool enabled; /** Is DartTrap enabled **/ - float initial_delay; /**< time to wait before firing first shot */ - float fire_delay; /**< reload time */ - int ammo; /**< ammo left (-1 means unlimited) */ + bool m_enabled; + float m_initial_delay; + float m_fire_delay; + int m_ammo; // ammo left (-1 means unlimited) + std::string m_dart_sprite; - State state; /**< current state */ - Timer fire_timer; /**< time until new shot is fired */ + State m_state; + Timer m_fire_timer; private: DartTrap(const DartTrap&) = delete; From d08fbc63460ee031efed5c2e3db2052a968b3968 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Pale=C4=8Dek?= Date: Mon, 31 Jul 2023 22:25:39 +0200 Subject: [PATCH 44/50] Fix editor not updating direction of badguys, conveyor belts and bumpers (#2556) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes the editor not updating sprite actions of badguys, bumpers and conveyor belts upon changing their direction. It also fixes the formatting in all the files I touched - indentation, `m_` prefix for member variables and CRLF → LF. --- src/badguy/badguy.cpp | 2 +- src/object/bumper.cpp | 196 ++++++++++---------- src/object/bumper.hpp | 109 +++++------ src/object/conveyor_belt.cpp | 1 + src/object/fallblock.cpp | 348 +++++++++++++++++------------------ src/object/fallblock.hpp | 139 +++++++------- 6 files changed, 405 insertions(+), 390 deletions(-) diff --git a/src/badguy/badguy.cpp b/src/badguy/badguy.cpp index a59fe916861..537e886e981 100644 --- a/src/badguy/badguy.cpp +++ b/src/badguy/badguy.cpp @@ -1085,7 +1085,7 @@ BadGuy::after_editor_set() } else { - std::string action_str = dir_to_string(m_dir); + std::string action_str = dir_to_string(m_start_dir); if (m_sprite->has_action("editor-" + action_str)) { set_action("editor-" + action_str); diff --git a/src/object/bumper.cpp b/src/object/bumper.cpp index b267cf646f6..0fd0caa3fd2 100644 --- a/src/object/bumper.cpp +++ b/src/object/bumper.cpp @@ -1,93 +1,103 @@ -// Copyright (C) 2020 Daniel Ward -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -#include "object/bumper.hpp" - -#include "audio/sound_manager.hpp" -#include "object/player.hpp" -#include "sprite/sprite.hpp" -#include "sprite/sprite_manager.hpp" -#include "supertux/flip_level_transformer.hpp" -#include "supertux/sector.hpp" -#include "util/reader_mapping.hpp" - -namespace { -const std::string TRAMPOLINE_SOUND = "sounds/trampoline.wav"; -const float BOUNCE_Y = -450.0f; -const float BOUNCE_X = 700.0f; -} - -Bumper::Bumper(const ReaderMapping& reader) : - MovingSprite(reader, "images/objects/trampoline/bumper.sprite", LAYER_OBJECTS, COLGROUP_MOVING), - physic(), - left() -{ - reader.get("left", left); - set_action(left ? "left-normal" : "right-normal"); - physic.enable_gravity(false); -} - -ObjectSettings -Bumper::get_settings() -{ - ObjectSettings result = MovingSprite::get_settings(); - - result.add_bool(_("Facing Left"), &left, "left", false); - - result.reorder({"left", "sprite", "x", "y"}); - - return result; -} - -void -Bumper::update(float dt_sec) -{ - if (m_sprite->animation_done()) - { - set_action(left ? "left-normal" : "right-normal"); - } - m_col.set_movement(physic.get_movement (dt_sec)); -} - -HitResponse -Bumper::collision(GameObject& other, const CollisionHit& hit) -{ - auto player = dynamic_cast (&other); - if (player) - { - float BOUNCE_DIR = left ? -BOUNCE_X : BOUNCE_X; - player->get_physic().set_velocity(0.f, player->is_swimming() ? 0.f : BOUNCE_Y); - player->sideways_push(BOUNCE_DIR); - SoundManager::current()->play(TRAMPOLINE_SOUND, get_pos()); - set_action((left ? "left-swinging" : "right-swinging"), 1); - } - - auto bumper = dynamic_cast (&other); - if (bumper) - { - physic.set_velocity_y(0); - return FORCE_MOVE; - } - return ABORT_MOVE; -} - -void -Bumper::on_flip(float height) -{ - MovingSprite::on_flip(height); - FlipLevelTransformer::transform_flip(m_flip); -} - -/* EOF */ +// Copyright (C) 2020 Daniel Ward +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include "object/bumper.hpp" + +#include "audio/sound_manager.hpp" +#include "object/player.hpp" +#include "sprite/sprite.hpp" +#include "sprite/sprite_manager.hpp" +#include "supertux/flip_level_transformer.hpp" +#include "supertux/sector.hpp" +#include "util/reader_mapping.hpp" + +namespace { +const std::string TRAMPOLINE_SOUND = "sounds/trampoline.wav"; +const float BOUNCE_Y = -450.0f; +const float BOUNCE_X = 700.0f; +} + +Bumper::Bumper(const ReaderMapping& reader) : + MovingSprite(reader, "images/objects/trampoline/bumper.sprite", LAYER_OBJECTS, COLGROUP_MOVING), + m_physic(), + m_facing_left() +{ + reader.get("left", m_facing_left); + set_action(m_facing_left ? "left-normal" : "right-normal"); + m_physic.enable_gravity(false); +} + +ObjectSettings +Bumper::get_settings() +{ + ObjectSettings result = MovingSprite::get_settings(); + + result.add_bool(_("Facing Left"), &m_facing_left, "left", false); + result.reorder({"left", "sprite", "x", "y"}); + return result; +} + +void +Bumper::update(float dt_sec) +{ + if (m_sprite->animation_done()) + { + set_action(m_facing_left ? "left-normal" : "right-normal"); + } + m_col.set_movement(m_physic.get_movement (dt_sec)); +} + +HitResponse +Bumper::collision(GameObject& other, const CollisionHit& hit) +{ + auto player = dynamic_cast (&other); + if (player) + { + float BOUNCE_DIR = m_facing_left ? -BOUNCE_X : BOUNCE_X; + player->get_physic().set_velocity(0.f, player->is_swimming() ? 0.f : BOUNCE_Y); + player->sideways_push(BOUNCE_DIR); + SoundManager::current()->play(TRAMPOLINE_SOUND, get_pos()); + set_action((m_facing_left ? "left-swinging" : "right-swinging"), 1); + } + auto bumper = dynamic_cast (&other); + if (bumper) + { + m_physic.set_velocity_y(0); + return FORCE_MOVE; + } + return ABORT_MOVE; +} + +Physic& +Bumper::get_physic() +{ + return m_physic; +} + +void +Bumper::after_editor_set() +{ + MovingSprite::after_editor_set(); + set_action(m_facing_left ? "left-normal" : "right-normal"); +} + +void +Bumper::on_flip(float height) +{ + MovingSprite::on_flip(height); + FlipLevelTransformer::transform_flip(m_flip); +} + +/* EOF */ diff --git a/src/object/bumper.hpp b/src/object/bumper.hpp index 0d755131f26..65258cffac7 100644 --- a/src/object/bumper.hpp +++ b/src/object/bumper.hpp @@ -1,53 +1,56 @@ -// Copyright (C) 2020 Daniel Ward -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -#ifndef HEADER_SUPERTUX_OBJECT_BUMPER_HPP -#define HEADER_SUPERTUX_OBJECT_BUMPER_HPP - -#include "object/moving_sprite.hpp" -#include "supertux/physic.hpp" - -class Player; - -class Bumper final : public MovingSprite -{ -public: - Bumper(const ReaderMapping& reader); - - virtual ObjectSettings get_settings() override; - - virtual void update(float dt_sec) override; - virtual HitResponse collision(GameObject& other, const CollisionHit& hit) override; - - static std::string class_name() { return "bumper"; } - virtual std::string get_class_name() const override { return class_name(); } - static std::string display_name() { return _("Bumper"); } - virtual std::string get_display_name() const override { return display_name(); } - - virtual void on_flip(float height) override; - - Physic physic; - -private: - bool left; - -private: - Bumper(const Bumper&) = delete; - Bumper& operator=(const Bumper&) = delete; -}; - -#endif - -/* EOF */ +// Copyright (C) 2020 Daniel Ward +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#ifndef HEADER_SUPERTUX_OBJECT_BUMPER_HPP +#define HEADER_SUPERTUX_OBJECT_BUMPER_HPP + +#include "object/moving_sprite.hpp" + +#include "supertux/physic.hpp" + +class Player; + +class Bumper final : public MovingSprite +{ +public: + Bumper(const ReaderMapping& reader); + + virtual ObjectSettings get_settings() override; + + virtual void update(float dt_sec) override; + virtual HitResponse collision(GameObject& other, const CollisionHit& hit) override; + + static std::string class_name() { return "bumper"; } + virtual std::string get_class_name() const override { return class_name(); } + static std::string display_name() { return _("Bumper"); } + virtual std::string get_display_name() const override { return display_name(); } + + virtual void after_editor_set() override; + virtual void on_flip(float height) override; + + Physic& get_physic(); + +private: + Physic m_physic; + bool m_facing_left; + +private: + Bumper(const Bumper&) = delete; + Bumper& operator=(const Bumper&) = delete; +}; + +#endif + +/* EOF */ diff --git a/src/object/conveyor_belt.cpp b/src/object/conveyor_belt.cpp index 8bbf7a81b9e..97af510e8a3 100644 --- a/src/object/conveyor_belt.cpp +++ b/src/object/conveyor_belt.cpp @@ -128,6 +128,7 @@ ConveyorBelt::after_editor_set() if (m_length <= 0) m_length = 1; + set_action(dir_to_string(m_dir)); } void diff --git a/src/object/fallblock.cpp b/src/object/fallblock.cpp index a311e9c21de..4cf0c254012 100644 --- a/src/object/fallblock.cpp +++ b/src/object/fallblock.cpp @@ -1,174 +1,174 @@ -// Copyright (C) 2020 Daniel Ward -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -#include "object/fallblock.hpp" - -#include "audio/sound_manager.hpp" -#include "object/bumper.hpp" -#include "object/player.hpp" -#include "object/camera.hpp" -#include "sprite/sprite.hpp" -#include "supertux/flip_level_transformer.hpp" -#include "supertux/sector.hpp" -#include "supertux/tile.hpp" -#include "math/random.hpp" -#include "util/reader_mapping.hpp" - -FallBlock::FallBlock(const ReaderMapping& reader) : - MovingSprite(reader, "images/objects/fallblock/cave-4x4.sprite", LAYER_OBJECTS, COLGROUP_STATIC), - state(IDLE), - physic(), - timer() -{ - SoundManager::current()->preload("sounds/cracking.wav"); - SoundManager::current()->preload("sounds/thud.ogg"); - physic.enable_gravity(false); -} - -void -FallBlock::update(float dt_sec) -{ - switch (state) - { - case IDLE: - set_group(COLGROUP_STATIC); - if (found_victim_down()) - { - state = SHAKE; - SoundManager::current()->play("sounds/cracking.wav", get_pos()); - timer.start(0.5f); - } - break; - case SHAKE: - if (timer.check()) - { - state = FALL; - physic.reset(); - physic.enable_gravity(true); - } - break; - case FALL: - m_col.set_movement(physic.get_movement (dt_sec)); - set_group(COLGROUP_MOVING_STATIC); - break; - case LAND: - m_col.set_movement(physic.get_movement (dt_sec)); - set_group(COLGROUP_MOVING_STATIC); - break; - } - for (auto& bumper : Sector::get().get_objects_by_type()) - { - Rectf bumper_bbox = bumper.get_bbox(); - if ((bumper_bbox.get_left() < (m_col.m_bbox.get_right() + 8)) - && (bumper_bbox.get_right() > (m_col.m_bbox.get_left() - 8)) - && (bumper_bbox.get_bottom() > (m_col.m_bbox.get_top() - 8)) - && (bumper_bbox.get_top() < (m_col.m_bbox.get_bottom() + 8))) - { - switch (state) - { - case IDLE: - break; - case SHAKE: - break; - case FALL: - bumper.physic.enable_gravity(true); - break; - case LAND: - bumper.physic.enable_gravity(false); - bumper.physic.set_gravity_modifier(0.f); - bumper.physic.set_velocity_y(0.f); - bumper.physic.reset(); - break; - } - } - } -} - -HitResponse -FallBlock::collision(GameObject& other, const CollisionHit& hit) -{ - auto fallblock = dynamic_cast (&other); - if (fallblock && hit.bottom && (state == FALL || state == LAND)) - { - physic.set_velocity_y(0.0f); - return CONTINUE; - } - - auto player = dynamic_cast(&other); - if (state == IDLE && player && player->get_bbox().get_bottom() < m_col.m_bbox.get_top()) - { - state = SHAKE; - SoundManager::current()->play("sounds/cracking.wav", get_pos()); - timer.start(0.5f); - } - return FORCE_MOVE; -} - -void -FallBlock::collision_solid(const CollisionHit& hit) -{ - if (hit.top || hit.bottom || hit.crush) - { - physic.set_velocity(0.0f, 0.0f); - } - - if (state == FALL && hit.bottom) - { - Sector::get().get_camera().shake(0.125f, 0.0f, 10.0f); - SoundManager::current()->play("sounds/thud.ogg", get_pos()); - state = LAND; - } -} - -void -FallBlock::draw(DrawingContext& context) -{ - Vector pos = get_pos(); - // shaking - if (state == SHAKE) - { - pos.x += static_cast(graphicsRandom.rand(-8, 8)); - pos.y += static_cast(graphicsRandom.rand(-5, 5)); - } - MovingSprite::draw(context); -} - -bool -FallBlock::found_victim_down() const -{ - if (auto* player = Sector::get().get_nearest_player(m_col.m_bbox)) - { - const Rectf& player_bbox = player->get_bbox(); - Rectf crush_area_down = Rectf(m_col.m_bbox.get_left()+1, m_col.m_bbox.get_bottom(), - m_col.m_bbox.get_right()-1, std::max(m_col.m_bbox.get_bottom(),player_bbox.get_top()-1)); - if ((player_bbox.get_top() >= m_col.m_bbox.get_bottom()) - && (player_bbox.get_right() > (m_col.m_bbox.get_left() - 4)) - && (player_bbox.get_left() < (m_col.m_bbox.get_right() + 4)) - && (Sector::get().is_free_of_statics(crush_area_down, this, false))) - { - return true; - } - } - return false; -} - -void -FallBlock::on_flip(float height) -{ - MovingSprite::on_flip(height); - FlipLevelTransformer::transform_flip(m_flip); -} - -/* EOF */ +// Copyright (C) 2020 Daniel Ward +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include "object/fallblock.hpp" + +#include "audio/sound_manager.hpp" +#include "object/bumper.hpp" +#include "object/player.hpp" +#include "object/camera.hpp" +#include "sprite/sprite.hpp" +#include "supertux/flip_level_transformer.hpp" +#include "supertux/sector.hpp" +#include "supertux/tile.hpp" +#include "math/random.hpp" +#include "util/reader_mapping.hpp" + +FallBlock::FallBlock(const ReaderMapping& reader) : + MovingSprite(reader, "images/objects/fallblock/cave-4x4.sprite", LAYER_OBJECTS, COLGROUP_STATIC), + m_state(IDLE), + m_physic(), + m_timer() +{ + SoundManager::current()->preload("sounds/cracking.wav"); + SoundManager::current()->preload("sounds/thud.ogg"); + m_physic.enable_gravity(false); +} + +void +FallBlock::update(float dt_sec) +{ + switch (m_state) + { + case IDLE: + set_group(COLGROUP_STATIC); + if (found_victim_down()) + { + m_state = SHAKE; + SoundManager::current()->play("sounds/cracking.wav", get_pos()); + m_timer.start(0.5f); + } + break; + case SHAKE: + if (m_timer.check()) + { + m_state = FALL; + m_physic.reset(); + m_physic.enable_gravity(true); + } + break; + case FALL: + m_col.set_movement(m_physic.get_movement (dt_sec)); + set_group(COLGROUP_MOVING_STATIC); + break; + case LAND: + m_col.set_movement(m_physic.get_movement (dt_sec)); + set_group(COLGROUP_MOVING_STATIC); + break; + } + for (auto& bumper : Sector::get().get_objects_by_type()) + { + Rectf bumper_bbox = bumper.get_bbox(); + if ((bumper_bbox.get_left() < (m_col.m_bbox.get_right() + 8)) + && (bumper_bbox.get_right() > (m_col.m_bbox.get_left() - 8)) + && (bumper_bbox.get_bottom() > (m_col.m_bbox.get_top() - 8)) + && (bumper_bbox.get_top() < (m_col.m_bbox.get_bottom() + 8))) + { + switch (m_state) + { + case IDLE: + break; + case SHAKE: + break; + case FALL: + bumper.get_physic().enable_gravity(true); + break; + case LAND: + bumper.get_physic().enable_gravity(false); + bumper.get_physic().set_gravity_modifier(0.f); + bumper.get_physic().set_velocity_y(0.f); + bumper.get_physic().reset(); + break; + } + } + } +} + +HitResponse +FallBlock::collision(GameObject& other, const CollisionHit& hit) +{ + auto fallblock = dynamic_cast (&other); + if (fallblock && hit.bottom && (m_state == FALL || m_state == LAND)) + { + m_physic.set_velocity_y(0.0f); + return CONTINUE; + } + + auto player = dynamic_cast(&other); + if (m_state == IDLE && player && player->get_bbox().get_bottom() < m_col.m_bbox.get_top()) + { + m_state = SHAKE; + SoundManager::current()->play("sounds/cracking.wav", get_pos()); + m_timer.start(0.5f); + } + return FORCE_MOVE; +} + +void +FallBlock::collision_solid(const CollisionHit& hit) +{ + if (hit.top || hit.bottom || hit.crush) + { + m_physic.set_velocity(0.0f, 0.0f); + } + + if (m_state == FALL && hit.bottom) + { + Sector::get().get_camera().shake(0.125f, 0.0f, 10.0f); + SoundManager::current()->play("sounds/thud.ogg", get_pos()); + m_state = LAND; + } +} + +void +FallBlock::draw(DrawingContext& context) +{ + Vector pos = get_pos(); + // shaking + if (m_state == SHAKE) + { + pos.x += static_cast(graphicsRandom.rand(-8, 8)); + pos.y += static_cast(graphicsRandom.rand(-5, 5)); + } + MovingSprite::draw(context); +} + +bool +FallBlock::found_victim_down() const +{ + if (auto* player = Sector::get().get_nearest_player(m_col.m_bbox)) + { + const Rectf& player_bbox = player->get_bbox(); + Rectf crush_area_down = Rectf(m_col.m_bbox.get_left()+1, m_col.m_bbox.get_bottom(), + m_col.m_bbox.get_right()-1, std::max(m_col.m_bbox.get_bottom(),player_bbox.get_top()-1)); + if ((player_bbox.get_top() >= m_col.m_bbox.get_bottom()) + && (player_bbox.get_right() > (m_col.m_bbox.get_left() - 4)) + && (player_bbox.get_left() < (m_col.m_bbox.get_right() + 4)) + && (Sector::get().is_free_of_statics(crush_area_down, this, false))) + { + return true; + } + } + return false; +} + +void +FallBlock::on_flip(float height) +{ + MovingSprite::on_flip(height); + FlipLevelTransformer::transform_flip(m_flip); +} + +/* EOF */ diff --git a/src/object/fallblock.hpp b/src/object/fallblock.hpp index b84b4222921..ab3b6c4a03e 100644 --- a/src/object/fallblock.hpp +++ b/src/object/fallblock.hpp @@ -1,69 +1,70 @@ -// Copyright (C) 2020 Daniel Ward -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -#ifndef HEADER_SUPERTUX_OBJECT_FALLBLOCK_HPP -#define HEADER_SUPERTUX_OBJECT_FALLBLOCK_HPP - -#include "object/moving_sprite.hpp" -#include "supertux/physic.hpp" -#include "supertux/timer.hpp" - -class Player; - -class FallBlock : public MovingSprite - -{ -public: - FallBlock(const ReaderMapping& reader); - - virtual void update(float dt_sec) override; - - virtual HitResponse collision(GameObject& other, const CollisionHit& hit) override; - virtual void collision_solid(const CollisionHit& hit) override; - - virtual void draw(DrawingContext& context) override; - - static std::string class_name() { return "fallblock"; } - virtual std::string get_class_name() const override { return class_name(); } - static std::string display_name() { return _("Falling Platform"); } - virtual std::string get_display_name() const override { return display_name(); } - - virtual void on_flip(float height) override; - -protected: - enum State - { - IDLE, - SHAKE, - FALL, - LAND - }; - -private: - State state; - - Physic physic; - Timer timer; - - bool found_victim_down() const; - -private: - FallBlock(const FallBlock&) = delete; - FallBlock& operator=(const FallBlock&) = delete; -}; - -#endif - -/* EOF */ +// Copyright (C) 2020 Daniel Ward +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#ifndef HEADER_SUPERTUX_OBJECT_FALLBLOCK_HPP +#define HEADER_SUPERTUX_OBJECT_FALLBLOCK_HPP + +#include "object/moving_sprite.hpp" + +#include "supertux/physic.hpp" +#include "supertux/timer.hpp" + +class Player; + +class FallBlock : public MovingSprite + +{ +public: + FallBlock(const ReaderMapping& reader); + + virtual void update(float dt_sec) override; + + virtual HitResponse collision(GameObject& other, const CollisionHit& hit) override; + virtual void collision_solid(const CollisionHit& hit) override; + + virtual void draw(DrawingContext& context) override; + + static std::string class_name() { return "fallblock"; } + virtual std::string get_class_name() const override { return class_name(); } + static std::string display_name() { return _("Falling Platform"); } + virtual std::string get_display_name() const override { return display_name(); } + + virtual void on_flip(float height) override; + +protected: + enum State + { + IDLE, + SHAKE, + FALL, + LAND + }; + +private: + State m_state; + + Physic m_physic; + Timer m_timer; + +private: + bool found_victim_down() const; + + FallBlock(const FallBlock&) = delete; + FallBlock& operator=(const FallBlock&) = delete; +}; + +#endif + +/* EOF */ From 9b8a475a5aca29a8dee1ae9402aabb2f1ccb096a Mon Sep 17 00:00:00 2001 From: Tobias Markus Date: Tue, 1 Aug 2023 03:15:32 +0200 Subject: [PATCH 45/50] Reduce code duplication in PlayerStatus --- src/supertux/player_status.cpp | 67 +++++++++++++--------------------- src/supertux/player_status.hpp | 3 ++ 2 files changed, 28 insertions(+), 42 deletions(-) diff --git a/src/supertux/player_status.cpp b/src/supertux/player_status.cpp index de21effb5cf..f42e17f05ef 100644 --- a/src/supertux/player_status.cpp +++ b/src/supertux/player_status.cpp @@ -213,30 +213,7 @@ PlayerStatus::read(const ReaderMapping& mapping) } auto map = iter.as_mapping(); - - std::string bonusname; - if (map.get("bonus", bonusname)) { - if (bonusname == "none") { - bonus[id] = NO_BONUS; - } else if (bonusname == "growup") { - bonus[id] = GROWUP_BONUS; - } else if (bonusname == "fireflower") { - bonus[id] = FIRE_BONUS; - } else if (bonusname == "iceflower") { - bonus[id] = ICE_BONUS; - } else if (bonusname == "airflower") { - bonus[id] = AIR_BONUS; - } else if (bonusname == "earthflower") { - bonus[id] = EARTH_BONUS; - } else { - log_warning << "Unknown bonus '" << bonusname << "' in savefile for player " << (id + 1) << std::endl; - bonus[id] = NO_BONUS; - } - } - map.get("fireflowers", max_fire_bullets[id]); - map.get("iceflowers", max_ice_bullets[id]); - map.get("airflowers", max_air_time[id]); - map.get("earthflowers", max_earth_time[id]); + parse_bonus_mapping(map, id); } } catch (const std::exception& e) @@ -245,34 +222,40 @@ PlayerStatus::read(const ReaderMapping& mapping) } } + parse_bonus_mapping(mapping, 0); + + mapping.get("coins", coins); + + mapping.get("worldmap-sprite", worldmap_sprite); + mapping.get("last-worldmap", last_worldmap); +} + +void +PlayerStatus::parse_bonus_mapping(const ReaderMapping& map, int id) +{ std::string bonusname; - if (mapping.get("bonus", bonusname)) { + if (map.get("bonus", bonusname)) { if (bonusname == "none") { - bonus[0] = NO_BONUS; + bonus[id] = NO_BONUS; } else if (bonusname == "growup") { - bonus[0] = GROWUP_BONUS; + bonus[id] = GROWUP_BONUS; } else if (bonusname == "fireflower") { - bonus[0] = FIRE_BONUS; + bonus[id] = FIRE_BONUS; } else if (bonusname == "iceflower") { - bonus[0] = ICE_BONUS; + bonus[id] = ICE_BONUS; } else if (bonusname == "airflower") { - bonus[0] = AIR_BONUS; + bonus[id] = AIR_BONUS; } else if (bonusname == "earthflower") { - bonus[0] = EARTH_BONUS; + bonus[id] = EARTH_BONUS; } else { - log_warning << "Unknown bonus '" << bonusname << "' in savefile" << std::endl; - bonus[0] = NO_BONUS; + log_warning << "Unknown bonus '" << bonusname << "' in savefile for player " << (id + 1) << std::endl; + bonus[id] = NO_BONUS; } } - mapping.get("fireflowers", max_fire_bullets[0]); - mapping.get("iceflowers", max_ice_bullets[0]); - mapping.get("airflowers", max_air_time[0]); - mapping.get("earthflowers", max_earth_time[0]); - - mapping.get("coins", coins); - - mapping.get("worldmap-sprite", worldmap_sprite); - mapping.get("last-worldmap", last_worldmap); + map.get("fireflowers", max_fire_bullets[id]); + map.get("iceflowers", max_ice_bullets[id]); + map.get("airflowers", max_air_time[id]); + map.get("earthflowers", max_earth_time[id]); } std::string diff --git a/src/supertux/player_status.hpp b/src/supertux/player_status.hpp index b705e3c2a5d..4be8f1bd832 100644 --- a/src/supertux/player_status.hpp +++ b/src/supertux/player_status.hpp @@ -56,6 +56,9 @@ class PlayerStatus final void add_player(); void remove_player(int player_id); +private: + void parse_bonus_mapping(const ReaderMapping& map, int id); + public: int m_num_players; From c290e2a427ccfdab859624beba5ba643127a12c8 Mon Sep 17 00:00:00 2001 From: Tobias Markus Date: Tue, 1 Aug 2023 03:50:04 +0200 Subject: [PATCH 46/50] Don't clear menu stack twice --- src/supertux/menu/game_menu.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/supertux/menu/game_menu.cpp b/src/supertux/menu/game_menu.cpp index bb225b5c57c..2202621a286 100644 --- a/src/supertux/menu/game_menu.cpp +++ b/src/supertux/menu/game_menu.cpp @@ -40,8 +40,7 @@ GameMenu::GameMenu() : GameSession::current()->reset_checkpoint_button = true; }), - abort_callback ( [] { - MenuManager::instance().clear_menu_stack(); + abort_callback([] { GameSession::current()->abort_level(); }) { From c1666ff89559601c5f1fbfc6f5ce0e0a25d6d997 Mon Sep 17 00:00:00 2001 From: Tobias Markus Date: Tue, 1 Aug 2023 04:03:22 +0200 Subject: [PATCH 47/50] Remove str2dir function --- src/badguy/badguy.cpp | 8 +------- src/badguy/badguy.hpp | 3 --- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/src/badguy/badguy.cpp b/src/badguy/badguy.cpp index 537e886e981..2f56b9b4b9c 100644 --- a/src/badguy/badguy.cpp +++ b/src/badguy/badguy.cpp @@ -111,7 +111,7 @@ BadGuy::BadGuy(const ReaderMapping& reader, const std::string& sprite_name_, int { std::string dir_str = "auto"; reader.get("direction", dir_str); - m_start_dir = str2dir( dir_str ); + m_start_dir = string_to_dir(dir_str); m_dir = m_start_dir; reader.get("dead-script", m_dead_script); @@ -290,12 +290,6 @@ BadGuy::update(float dt_sec) m_on_ground_flag = false; } -Direction -BadGuy::str2dir(const std::string& dir_str) const -{ - return string_to_dir(dir_str); -} - void BadGuy::initialize() { diff --git a/src/badguy/badguy.hpp b/src/badguy/badguy.hpp index 754aae527d3..8010bc5f2dc 100644 --- a/src/badguy/badguy.hpp +++ b/src/badguy/badguy.hpp @@ -209,9 +209,6 @@ class BadGuy : public MovingSprite, pixels. Minimum value for height is 1 pixel */ bool might_fall(int height = 1) const; - /** Get Direction from String. */ - Direction str2dir(const std::string& dir_str) const; - /** Update on_ground_flag judging by solid collision @c hit. This gets called from the base implementation of collision_solid, so call this when overriding collision_solid's default From a4d0d323de4de363d5cd17d4f414b3f8475a913d Mon Sep 17 00:00:00 2001 From: mrkubax10 Date: Tue, 1 Aug 2023 11:38:33 +0200 Subject: [PATCH 48/50] Fixes and updates for Ubuntu Touch port (#2510) Updated to UT 20.04 and Clickable 7 --- .github/workflows/ubuntu-touch.yml | 22 +++++----- clickable.json | 42 ------------------ clickable.yaml | 39 +++++++++++++++++ mk/clickable/BROKEN-build-with-glbinding.json | 43 ------------------- mk/clickable/build-with-glew.json | 41 ------------------ mk/clickable/clickable-glbinding.yaml | 41 ++++++++++++++++++ mk/clickable/clickable-glew.yaml | 40 +++++++++++++++++ mk/clickable/supertux2.apparmor | 2 +- 8 files changed, 132 insertions(+), 138 deletions(-) delete mode 100644 clickable.json create mode 100644 clickable.yaml delete mode 100644 mk/clickable/BROKEN-build-with-glbinding.json delete mode 100644 mk/clickable/build-with-glew.json create mode 100644 mk/clickable/clickable-glbinding.yaml create mode 100644 mk/clickable/clickable-glew.yaml diff --git a/.github/workflows/ubuntu-touch.yml b/.github/workflows/ubuntu-touch.yml index cc925aca769..01a43d11c1d 100644 --- a/.github/workflows/ubuntu-touch.yml +++ b/.github/workflows/ubuntu-touch.yml @@ -67,7 +67,7 @@ jobs: ARCH: ${{ matrix.arch }} run: | ~/.local/bin/clickable build --verbose ${BUILD_TYPE} --arch ${ARCH} \ - --config mk/clickable/build-with-glew.json + --config mk/clickable/clickable-glew.yaml - name: Build (OpenGL with GLbinding) if: ${{ matrix.opengl == 'glbinding' }} @@ -76,7 +76,7 @@ jobs: ARCH: ${{ matrix.arch }} run: | ~/.local/bin/clickable build --verbose ${BUILD_TYPE} --arch ${ARCH} \ - --config mk/clickable/build-with-glbinding.json + --config mk/clickable/clickable-glbinding.yaml - name: Build (no OpenGL) if: ${{ matrix.opengl == 'sdl' }} @@ -92,12 +92,12 @@ jobs: path: build.clickable/*.click if-no-files-found: ignore - - name: Publish to Open Store - if: ${{ github.ref == 'refs/heads/master' && matrix.build_type == 'Release' && matrix.opengl == 'sdl' }} - env: - ARCH: ${{ matrix.arch }} - OPENSTORE_KEY: ${{ secrets.OPENSTORE_KEY }} - run: | - ~/.local/bin/clickable publish "* $(git log -1 --pretty=%B | \ - head -1)" --apikey ${OPENSTORE_KEY} \ - --arch ${ARCH} + #- name: Publish to Open Store + # if: ${{ github.ref == 'refs/heads/master' && matrix.build_type == 'Release' && matrix.opengl == 'sdl' }} + # env: + # ARCH: ${{ matrix.arch }} + # OPENSTORE_KEY: ${{ secrets.OPENSTORE_KEY }} + # run: | + # ~/.local/bin/clickable publish "* $(git log -1 --pretty=%B | \ + # head -1)" --apikey ${OPENSTORE_KEY} \ + # --arch ${ARCH} diff --git a/clickable.json b/clickable.json deleted file mode 100644 index 8aa9a5d5544..00000000000 --- a/clickable.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "builder": "cmake", - "build_dir": "build.clickable", - "dependencies_target": [ - "libogg-dev", - "libvorbis-dev", - "libopenal-dev", - "libsdl2-dev", - "libsdl2-image-dev", - "libfreetype6-dev", - "libcurl4-openssl-dev", - "libglew-dev", - "libharfbuzz-dev", - "libfribidi-dev", - "libglm-dev", - "zlib1g-dev" - ], - "install_lib": [ - "/usr/lib/${ARCH_TRIPLET}/libogg.so*", - "/usr/lib/${ARCH_TRIPLET}/libvorbis.so*", - "/usr/lib/${ARCH_TRIPLET}/libvorbisfile.so*", - "/usr/lib/${ARCH_TRIPLET}/libopenal.so*", - "/usr/lib/${ARCH_TRIPLET}/libSDL2.so*", - "/usr/lib/${ARCH_TRIPLET}/libSDL2-2.0.so*", - "/usr/lib/${ARCH_TRIPLET}/libSDL2_image.so*", - "/usr/lib/${ARCH_TRIPLET}/libSDL2_image-2.0.so*", - "/usr/lib/${ARCH_TRIPLET}/libfreetype.so*", - "/usr/lib/${ARCH_TRIPLET}/libcurl.so*", - "/usr/lib/${ARCH_TRIPLET}/libGLEW.so*", - "/usr/lib/${ARCH_TRIPLET}/libharfbuzz.so*", - "/usr/lib/${ARCH_TRIPLET}/libfribidi.so*", - "/usr/lib/${ARCH_TRIPLET}/libasound.so*", - "/usr/lib/${ARCH_TRIPLET}/libsndio.so*", - "/lib/${ARCH_TRIPLET}/libz.so*" - ], - "build_args": [ - "-DUBUNTU_TOUCH=ON", - "-DENABLE_OPENGL=OFF", - "-DWARNINGS=ON", - "-DWERROR=ON" - ] -} diff --git a/clickable.yaml b/clickable.yaml new file mode 100644 index 00000000000..0cafe4b9485 --- /dev/null +++ b/clickable.yaml @@ -0,0 +1,39 @@ +clickable_minimum_required: 7 +framework: "ubuntu-sdk-20.04" +builder: "cmake" +build_dir: "build.clickable" +dependencies_target: + - "libogg-dev" + - "libvorbis-dev" + - "libopenal-dev" + - "libsdl2-dev" + - "libsdl2-image-dev" + - "libfreetype6-dev" + - "libcurl4-openssl-dev" + - "libharfbuzz-dev" + - "libfribidi-dev" + - "libglm-dev" + - "zlib1g-dev" +install_lib: + - "/usr/lib/${ARCH_TRIPLET}/libogg.so*" + - "/usr/lib/${ARCH_TRIPLET}/libvorbis.so*" + - "/usr/lib/${ARCH_TRIPLET}/libvorbisfile.so*" + - "/usr/lib/${ARCH_TRIPLET}/libopenal.so*" + - "/usr/lib/${ARCH_TRIPLET}/libSDL2.so*" + - "/usr/lib/${ARCH_TRIPLET}/libSDL2-2.0.so*" + - "/usr/lib/${ARCH_TRIPLET}/libSDL2_image.so*" + - "/usr/lib/${ARCH_TRIPLET}/libSDL2_image-2.0.so*" + - "/usr/lib/${ARCH_TRIPLET}/libfreetype.so*" + - "/usr/lib/${ARCH_TRIPLET}/libcurl.so*" + - "/usr/lib/${ARCH_TRIPLET}/libharfbuzz.so*" + - "/usr/lib/${ARCH_TRIPLET}/libfribidi.so*" + - "/usr/lib/${ARCH_TRIPLET}/libasound.so*" + - "/usr/lib/${ARCH_TRIPLET}/libsndio.so*" + - "/lib/${ARCH_TRIPLET}/libz.so.1.2.11" +build_args: + - "-DUBUNTU_TOUCH=ON" + - "-DENABLE_OPENGL=OFF" + - "-DWARNINGS=ON" + - "-DWERROR=ON" + - "-DCLICK_ARCH=${ARCH}" + - "-DCLICK_FRAMEWORK=ubuntu-sdk-20.04" diff --git a/mk/clickable/BROKEN-build-with-glbinding.json b/mk/clickable/BROKEN-build-with-glbinding.json deleted file mode 100644 index 28f73c94c70..00000000000 --- a/mk/clickable/BROKEN-build-with-glbinding.json +++ /dev/null @@ -1,43 +0,0 @@ -// This build won't work, as glbinding isn't available on Ubuntu 16.04. -{ - "builder": "cmake", - "build_dir": "build.clickable", - "dependencies_target": [ - "libogg-dev", - "libvorbis-dev", - "libopenal-dev", - "libsdl2-dev", - "libsdl2-image-dev", - "libfreetype6-dev", - "libcurl4-openssl-dev", - "libglbinding-dev", - "libharfbuzz-dev", - "libfribidi-dev", - "libglm-dev", - "zlib1g-dev" - ], - "install_lib": [ - "/usr/lib/${ARCH_TRIPLET}/libogg.so*", - "/usr/lib/${ARCH_TRIPLET}/libvorbis.so*", - "/usr/lib/${ARCH_TRIPLET}/libvorbisfile.so*", - "/usr/lib/${ARCH_TRIPLET}/libopenal.so*", - "/usr/lib/${ARCH_TRIPLET}/libSDL2.so*", - "/usr/lib/${ARCH_TRIPLET}/libSDL2-2.0.so*", - "/usr/lib/${ARCH_TRIPLET}/libSDL2_image.so*", - "/usr/lib/${ARCH_TRIPLET}/libSDL2_image-2.0.so*", - "/usr/lib/${ARCH_TRIPLET}/libfreetype.so*", - "/usr/lib/${ARCH_TRIPLET}/libcurl.so*", - "/usr/lib/${ARCH_TRIPLET}/libglbinding.so*", - "/usr/lib/${ARCH_TRIPLET}/libharfbuzz.so*", - "/usr/lib/${ARCH_TRIPLET}/libfribidi.so*", - "/usr/lib/${ARCH_TRIPLET}/libasound.so*", - "/usr/lib/${ARCH_TRIPLET}/libsndio.so*", - "/lib/${ARCH_TRIPLET}/libz.so*" - ], - "build_args": [ - "-DUBUNTU_TOUCH=ON", - "-DGLBINDING_ENABLED=ON", - "-DWARNINGS=ON", - "-DWERROR=ON" - ] -} diff --git a/mk/clickable/build-with-glew.json b/mk/clickable/build-with-glew.json deleted file mode 100644 index a793e6f3d5d..00000000000 --- a/mk/clickable/build-with-glew.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "builder": "cmake", - "build_dir": "build.clickable", - "dependencies_target": [ - "libogg-dev", - "libvorbis-dev", - "libopenal-dev", - "libsdl2-dev", - "libsdl2-image-dev", - "libfreetype6-dev", - "libcurl4-openssl-dev", - "libglew-dev", - "libharfbuzz-dev", - "libfribidi-dev", - "libglm-dev", - "zlib1g-dev" - ], - "install_lib": [ - "/usr/lib/${ARCH_TRIPLET}/libogg.so*", - "/usr/lib/${ARCH_TRIPLET}/libvorbis.so*", - "/usr/lib/${ARCH_TRIPLET}/libvorbisfile.so*", - "/usr/lib/${ARCH_TRIPLET}/libopenal.so*", - "/usr/lib/${ARCH_TRIPLET}/libSDL2.so*", - "/usr/lib/${ARCH_TRIPLET}/libSDL2-2.0.so*", - "/usr/lib/${ARCH_TRIPLET}/libSDL2_image.so*", - "/usr/lib/${ARCH_TRIPLET}/libSDL2_image-2.0.so*", - "/usr/lib/${ARCH_TRIPLET}/libfreetype.so*", - "/usr/lib/${ARCH_TRIPLET}/libcurl.so*", - "/usr/lib/${ARCH_TRIPLET}/libGLEW.so*", - "/usr/lib/${ARCH_TRIPLET}/libharfbuzz.so*", - "/usr/lib/${ARCH_TRIPLET}/libfribidi.so*", - "/usr/lib/${ARCH_TRIPLET}/libasound.so*", - "/usr/lib/${ARCH_TRIPLET}/libsndio.so*", - "/lib/${ARCH_TRIPLET}/libz.so*" - ], - "build_args": [ - "-DUBUNTU_TOUCH=ON", - "-DWARNINGS=ON", - "-DWERROR=ON" - ] -} diff --git a/mk/clickable/clickable-glbinding.yaml b/mk/clickable/clickable-glbinding.yaml new file mode 100644 index 00000000000..4eb72183dc5 --- /dev/null +++ b/mk/clickable/clickable-glbinding.yaml @@ -0,0 +1,41 @@ +clickable_minimum_required: 7 +framework: "ubuntu-sdk-20.04" +builder: "cmake" +build_dir: "build.clickable" +dependencies_target: + - "libogg-dev" + - "libvorbis-dev" + - "libopenal-dev" + - "libsdl2-dev" + - "libsdl2-image-dev" + - "libfreetype6-dev" + - "libcurl4-openssl-dev" + - "libglbinding-dev" + - "libharfbuzz-dev" + - "libfribidi-dev" + - "libglm-dev" + - "zlib1g-dev" +install_lib: + - "/usr/lib/${ARCH_TRIPLET}/libogg.so*" + - "/usr/lib/${ARCH_TRIPLET}/libvorbis.so*" + - "/usr/lib/${ARCH_TRIPLET}/libvorbisfile.so*" + - "/usr/lib/${ARCH_TRIPLET}/libopenal.so*" + - "/usr/lib/${ARCH_TRIPLET}/libSDL2.so*" + - "/usr/lib/${ARCH_TRIPLET}/libSDL2-2.0.so*" + - "/usr/lib/${ARCH_TRIPLET}/libSDL2_image.so*" + - "/usr/lib/${ARCH_TRIPLET}/libSDL2_image-2.0.so*" + - "/usr/lib/${ARCH_TRIPLET}/libfreetype.so*" + - "/usr/lib/${ARCH_TRIPLET}/libcurl.so*" + - "/usr/lib/${ARCH_TRIPLET}/libglbinding.so*" + - "/usr/lib/${ARCH_TRIPLET}/libharfbuzz.so*" + - "/usr/lib/${ARCH_TRIPLET}/libfribidi.so*" + - "/usr/lib/${ARCH_TRIPLET}/libasound.so*" + - "/usr/lib/${ARCH_TRIPLET}/libsndio.so*" + - "/lib/${ARCH_TRIPLET}/libz.so.1.2.11" +build_args: + - "-DUBUNTU_TOUCH=ON" + - "-DGLBINDING_ENABLED=ON" + - "-DWARNINGS=ON" + - "-DWERROR=ON" + - "-DCLICK_ARCH=${ARCH}" + - "-DCLICK_FRAMEWORK=ubuntu-sdk-20.04" diff --git a/mk/clickable/clickable-glew.yaml b/mk/clickable/clickable-glew.yaml new file mode 100644 index 00000000000..a75c6b0a989 --- /dev/null +++ b/mk/clickable/clickable-glew.yaml @@ -0,0 +1,40 @@ +clickable_minimum_required: 7 +framework: "ubuntu-sdk-20.04" +builder: "cmake" +build_dir: "build.clickable" +dependencies_target: + - "libogg-dev" + - "libvorbis-dev" + - "libopenal-dev" + - "libsdl2-dev" + - "libsdl2-image-dev" + - "libfreetype6-dev" + - "libcurl4-openssl-dev" + - "libglew-dev" + - "libharfbuzz-dev" + - "libfribidi-dev" + - "libglm-dev" + - "zlib1g-dev" +install_lib: + - "/usr/lib/${ARCH_TRIPLET}/libogg.so*" + - "/usr/lib/${ARCH_TRIPLET}/libvorbis.so*" + - "/usr/lib/${ARCH_TRIPLET}/libvorbisfile.so*" + - "/usr/lib/${ARCH_TRIPLET}/libopenal.so*" + - "/usr/lib/${ARCH_TRIPLET}/libSDL2.so*" + - "/usr/lib/${ARCH_TRIPLET}/libSDL2-2.0.so*" + - "/usr/lib/${ARCH_TRIPLET}/libSDL2_image.so*" + - "/usr/lib/${ARCH_TRIPLET}/libSDL2_image-2.0.so*" + - "/usr/lib/${ARCH_TRIPLET}/libfreetype.so*" + - "/usr/lib/${ARCH_TRIPLET}/libcurl.so*" + - "/usr/lib/${ARCH_TRIPLET}/libGLEW.so*" + - "/usr/lib/${ARCH_TRIPLET}/libharfbuzz.so*" + - "/usr/lib/${ARCH_TRIPLET}/libfribidi.so*" + - "/usr/lib/${ARCH_TRIPLET}/libasound.so*" + - "/usr/lib/${ARCH_TRIPLET}/libsndio.so*" + - "/lib/${ARCH_TRIPLET}/libz.so.1.2.11" +build_args: + - "-DUBUNTU_TOUCH=ON" + - "-DWARNINGS=ON" + - "-DWERROR=ON" + - "-DCLICK_ARCH=${ARCH}" + - "-DCLICK_FRAMEWORK=ubuntu-sdk-20.04" diff --git a/mk/clickable/supertux2.apparmor b/mk/clickable/supertux2.apparmor index f5a7bc82791..e34364a5d08 100644 --- a/mk/clickable/supertux2.apparmor +++ b/mk/clickable/supertux2.apparmor @@ -3,5 +3,5 @@ "audio", "networking" ], - "policy_version": 16.04 + "policy_version": 20.04 } From 54c198c5b47f13b09109ab7000c8c31106d545f4 Mon Sep 17 00:00:00 2001 From: Tobias Markus Date: Tue, 1 Aug 2023 17:15:28 +0200 Subject: [PATCH 49/50] Use std::map for addons instead of list (#2565) * Use std::map for addons instead of list * Use C++17 stuff * More C++17 --- src/addon/addon_manager.cpp | 83 +++++++++++++++---------------------- src/addon/addon_manager.hpp | 9 ++-- src/addon/downloader.cpp | 57 +++++++++---------------- src/addon/downloader.hpp | 3 +- 4 files changed, 59 insertions(+), 93 deletions(-) diff --git a/src/addon/addon_manager.cpp b/src/addon/addon_manager.cpp index c0f0439324d..9127e11b14d 100644 --- a/src/addon/addon_manager.cpp +++ b/src/addon/addon_manager.cpp @@ -80,18 +80,14 @@ MD5 md5_from_archive(const std::string& filename) } } -static Addon& get_addon(const AddonManager::AddonList& list, const AddonId& id, +static Addon& get_addon(const AddonManager::AddonMap& list, const AddonId& id, bool installed) { - auto it = std::find_if(list.begin(), list.end(), - [&id](const std::unique_ptr& addon) - { - return addon->get_id() == id; - }); + auto it = list.find(id); if (it != list.end()) { - return **it; + return *(it->second); } else { @@ -100,15 +96,14 @@ static Addon& get_addon(const AddonManager::AddonList& list, const AddonId& id, } } -static std::vector get_addons(const AddonManager::AddonList& list) +static std::vector get_addons(const AddonManager::AddonMap& list) { // Use a map for storing sorted addon titles with their respective IDs. std::map sorted_titles; - std::for_each(list.begin(), list.end(), - [&](const std::unique_ptr& addon) - { - sorted_titles.insert({addon->get_title(), addon->get_id()}); - }); + for (const auto& [id, addon] : list) + { + sorted_titles.insert({addon->get_title(), id}); + } std::vector results; results.reserve(sorted_titles.size()); std::transform(sorted_titles.begin(), sorted_titles.end(), @@ -223,9 +218,9 @@ AddonManager::~AddonManager() { // sync enabled/disabled add-ons into the config for saving m_addon_config.clear(); - for (const auto& addon : m_installed_addons) + for (const auto& [id, addon] : m_installed_addons) { - m_addon_config.push_back({addon->get_id(), addon->is_enabled()}); + m_addon_config.push_back({id, addon->is_enabled()}); } // Delete the add-on cache directory, if it exists. @@ -307,17 +302,13 @@ TransferStatusListPtr AddonManager::request_install_addon(const AddonId& addon_id) { // remove addon if it already exists - auto it = std::find_if(m_installed_addons.begin(), m_installed_addons.end(), - [&addon_id](const std::unique_ptr& addon) - { - return addon->get_id() == addon_id; - }); + auto it = m_installed_addons.find(addon_id); if (it != m_installed_addons.end()) { log_debug << "reinstalling addon " << addon_id << std::endl; - if ((*it)->is_enabled()) + if (it->second->is_enabled()) { - disable_addon((*it)->get_id()); + disable_addon(it->first); } m_installed_addons.erase(it); } @@ -413,17 +404,13 @@ void AddonManager::install_addon(const AddonId& addon_id) { { // remove addon if it already exists - auto it = std::find_if(m_installed_addons.begin(), m_installed_addons.end(), - [&addon_id](const std::unique_ptr& addon) - { - return addon->get_id() == addon_id; - }); + auto it = m_installed_addons.find(addon_id); if (it != m_installed_addons.end()) { log_debug << "reinstalling addon " << addon_id << std::endl; - if ((*it)->is_enabled()) + if (it->second->is_enabled()) { - disable_addon((*it)->get_id()); + disable_addon(it->first); } m_installed_addons.erase(it); } @@ -489,11 +476,7 @@ AddonManager::uninstall_addon(const AddonId& addon_id) disable_addon(addon_id); } log_debug << "deleting file \"" << addon.get_install_filename() << "\"" << std::endl; - const auto it = std::find_if(m_installed_addons.begin(), m_installed_addons.end(), - [&addon](const std::unique_ptr& rhs) - { - return addon.get_id() == rhs->get_id(); - }); + const auto it = m_installed_addons.find(addon.get_id()); if (it != m_installed_addons.end()) { if (PHYSFS_delete(FileSystem::join(m_addon_directory, addon.get_filename()).c_str()) == 0) @@ -568,10 +551,10 @@ AddonManager::enable_addon(const AddonId& addon_id) { if (addon.get_type() == Addon::RESOURCEPACK) { - for (const auto& installed_addon : m_installed_addons) + for (const auto& [id, addon] : m_installed_addons) { - if (installed_addon->get_type() == Addon::RESOURCEPACK && - installed_addon->is_enabled()) + if (addon->get_type() == Addon::RESOURCEPACK && + addon->is_enabled()) { throw std::runtime_error(_("Only one resource pack is allowed to be enabled at a time.")); } @@ -662,9 +645,9 @@ AddonManager::is_old_enabled_addon(const std::unique_ptr& addon) const bool AddonManager::is_old_addon_enabled() const { auto it = std::find_if(m_installed_addons.begin(), m_installed_addons.end(), - [this](const std::unique_ptr& addon) + [this](const auto& addon) { - return is_old_enabled_addon(addon); + return is_old_enabled_addon(addon.second); }); return it != m_installed_addons.end(); @@ -673,9 +656,9 @@ AddonManager::is_old_addon_enabled() const { void AddonManager::disable_old_addons() { - for (auto& addon : m_installed_addons) { + for (auto& [id, addon] : m_installed_addons) { if (is_old_enabled_addon(addon)) { - disable_addon(addon->get_id()); + disable_addon(id); } } } @@ -684,7 +667,7 @@ void AddonManager::mount_old_addons() { std::string mountpoint; - for (auto& addon : m_installed_addons) { + for (auto& [id, addon] : m_installed_addons) { if (is_old_enabled_addon(addon)) { if (PHYSFS_mount(addon->get_install_filename().c_str(), mountpoint.c_str(), !addon->overrides_data()) == 0) { @@ -698,7 +681,7 @@ AddonManager::mount_old_addons() void AddonManager::unmount_old_addons() { - for (auto& addon : m_installed_addons) { + for (auto& [id, addon] : m_installed_addons) { if (is_old_enabled_addon(addon)) { if (PHYSFS_unmount(addon->get_install_filename().c_str()) == 0) { @@ -713,7 +696,7 @@ bool AddonManager::is_from_old_addon(const std::string& filename) const { std::string real_path = PHYSFS_getRealDir(filename.c_str()); - for (auto& addon : m_installed_addons) { + for (auto& [id, addon] : m_installed_addons) { if (is_old_enabled_addon(addon) && addon->get_install_filename() == real_path) { return true; @@ -736,11 +719,11 @@ std::vector AddonManager::get_depending_addons(const std::string& id) const { std::vector addons; - for (auto& addon : m_installed_addons) + for (auto& [id, addon] : m_installed_addons) { const auto& dependencies = addon->get_dependencies(); if (std::find(dependencies.begin(), dependencies.end(), id) != dependencies.end()) - addons.push_back(addon->get_id()); + addons.push_back(id); } return addons; } @@ -846,7 +829,7 @@ AddonManager::add_installed_archive(const std::string& archive, const std::strin // save addon title and author on stack before std::move const std::string addon_title = addon->get_title(); const std::string addon_author = addon->get_author(); - m_installed_addons.push_back(std::move(addon)); + m_installed_addons[addon_id] = std::move(addon); if(user_install) { try @@ -892,10 +875,10 @@ AddonManager::add_installed_addons() } } -AddonManager::AddonList +AddonManager::AddonMap AddonManager::parse_addon_infos(const std::string& filename) const { - AddonList m_addons; + AddonMap m_addons; try { @@ -920,7 +903,7 @@ AddonManager::parse_addon_infos(const std::string& filename) const try { std::unique_ptr addon = Addon::parse(addon_node.get_mapping()); - m_addons.push_back(std::move(addon)); + m_addons[addon->get_id()] = std::move(addon); } catch(const std::exception& e) { diff --git a/src/addon/addon_manager.hpp b/src/addon/addon_manager.hpp index 4a8ac7164c8..7e3e9a0d9e9 100644 --- a/src/addon/addon_manager.hpp +++ b/src/addon/addon_manager.hpp @@ -20,6 +20,7 @@ #include #include +#include #include #include "addon/downloader.hpp" @@ -35,7 +36,7 @@ typedef std::string AddonId; class AddonManager final : public Currenton { public: - using AddonList = std::vector >; + using AddonMap = std::map >; private: Downloader m_downloader; @@ -45,8 +46,8 @@ class AddonManager final : public Currenton std::string m_repository_url; std::vector& m_addon_config; - AddonList m_installed_addons; - AddonList m_repository_addons; + AddonMap m_installed_addons; + AddonMap m_repository_addons; bool m_initialized; bool m_has_been_updated; @@ -108,7 +109,7 @@ class AddonManager final : public Currenton std::vector scan_for_archives() const; void add_installed_addons(); - AddonList parse_addon_infos(const std::string& filename) const; + AddonMap parse_addon_infos(const std::string& filename) const; /** add \a archive, given as physfs path, to the list of installed archives */ diff --git a/src/addon/downloader.cpp b/src/addon/downloader.cpp index 2406980e068..9e71d606264 100644 --- a/src/addon/downloader.cpp +++ b/src/addon/downloader.cpp @@ -402,7 +402,7 @@ Downloader::~Downloader() #ifndef EMSCRIPTEN for (auto& transfer : m_transfers) { - curl_multi_remove_handle(m_multi_handle, transfer->get_curl_handle()); + curl_multi_remove_handle(m_multi_handle, transfer.second->get_curl_handle()); } #endif m_transfers.clear(); @@ -478,21 +478,17 @@ Downloader::download(const std::string& url, const std::string& filename) void Downloader::abort(TransferId id) { - auto it = std::find_if(m_transfers.begin(), m_transfers.end(), - [&id](const std::unique_ptr& rhs) - { - return id == rhs->get_id(); - }); + auto it = m_transfers.find(id); if (it == m_transfers.end()) { log_warning << "transfer not found: " << id << std::endl; } else { - TransferStatusPtr status = (*it)->get_status(); + TransferStatusPtr status = (it->second)->get_status(); #ifndef EMSCRIPTEN - curl_multi_remove_handle(m_multi_handle, (*it)->get_curl_handle()); + curl_multi_remove_handle(m_multi_handle, it->second->get_curl_handle()); #endif m_transfers.erase(it); @@ -540,12 +536,12 @@ Downloader::update() curl_multi_remove_handle(m_multi_handle, msg->easy_handle); auto it = std::find_if(m_transfers.begin(), m_transfers.end(), - [&msg](const std::unique_ptr& rhs) { - return rhs->get_curl_handle() == msg->easy_handle; + [&msg](const auto& rhs) { + return rhs.second->get_curl_handle() == msg->easy_handle; }); assert(it != m_transfers.end()); - TransferStatusPtr status = (*it)->get_status(); - status->error_msg = (*it)->get_error_buffer(); + TransferStatusPtr status = it->second->get_status(); + status->error_msg = it->second->get_error_buffer(); m_transfers.erase(it); if (resultfromcurl == CURLE_OK) @@ -603,44 +599,37 @@ Downloader::request_download(const std::string& url, const std::string& outfile) #ifndef EMSCRIPTEN curl_multi_add_handle(m_multi_handle, transfer->get_curl_handle()); #endif - m_transfers.push_back(std::move(transfer)); - return m_transfers.back()->get_status(); + auto transferId = transfer->get_id(); + m_transfers[transferId] = std::move(transfer); + return m_transfers[transferId]->get_status(); } #ifdef EMSCRIPTEN void Downloader::onDownloadProgress(int id, int loaded, int total) { - auto it = std::find_if(m_transfers.begin(), m_transfers.end(), - [&id](const std::unique_ptr& rhs) - { - return id == rhs->get_id(); - }); + auto it = m_transfers.find(id); if (it == m_transfers.end()) { log_warning << "transfer not found: " << id << std::endl; } else { - (*it)->on_progress(static_cast(loaded), static_cast(total), 0.0, 0.0); + (it->second)->on_progress(static_cast(loaded), static_cast(total), 0.0, 0.0); } } void Downloader::onDownloadFinished(int id) { - auto it = std::find_if(m_transfers.begin(), m_transfers.end(), - [&id](const std::unique_ptr& rhs) - { - return id == rhs->get_id(); - }); + auto it = m_transfers.find(id); if (it == m_transfers.end()) { log_warning << "transfer not found: " << id << std::endl; } else { - for (const auto& callback : (*it)->get_status()->callbacks) + for (const auto& callback : it->second->get_status()->callbacks) { try { @@ -657,18 +646,14 @@ Downloader::onDownloadFinished(int id) void Downloader::onDownloadError(int id) { - auto it = std::find_if(m_transfers.begin(), m_transfers.end(), - [&id](const std::unique_ptr& rhs) - { - return id == rhs->get_id(); - }); + auto it = m_transfers.find(id); if (it == m_transfers.end()) { log_warning << "transfer not found: " << id << std::endl; } else { - for (const auto& callback : (*it)->get_status()->callbacks) + for (const auto& callback : it->second->get_status()->callbacks) { try { @@ -685,18 +670,14 @@ Downloader::onDownloadError(int id) void Downloader::onDownloadAborted(int id) { - auto it = std::find_if(m_transfers.begin(), m_transfers.end(), - [&id](const std::unique_ptr& rhs) - { - return id == rhs->get_id(); - }); + auto it = m_transfers.find(id); if (it == m_transfers.end()) { log_warning << "transfer not found: " << id << std::endl; } else { - for (const auto& callback : (*it)->get_status()->callbacks) + for (const auto& callback : it->second->get_status()->callbacks) { try { diff --git a/src/addon/downloader.hpp b/src/addon/downloader.hpp index 181af539560..5e6bfb8de54 100644 --- a/src/addon/downloader.hpp +++ b/src/addon/downloader.hpp @@ -24,6 +24,7 @@ #include #endif #include +#include #include #include @@ -120,7 +121,7 @@ class Downloader final #ifndef EMSCRIPTEN CURLM* m_multi_handle; #endif - std::vector > m_transfers; + std::map > m_transfers; int m_next_transfer_id; float m_last_update_time; From 6161ee6c4f0bc91ccd67609e2a217367300812cf Mon Sep 17 00:00:00 2001 From: Tobias Markus Date: Tue, 1 Aug 2023 18:29:36 +0200 Subject: [PATCH 50/50] Pause target timer via scripting (#2513) * Pause target timer via scripting Closes #2504 * Fix stuff * Add functions to Level table * Pause / Start LevelTime objects * Use get_objects_by_type --- data/scripts/default.nut | 2 ++ src/scripting/level.cpp | 14 ++++++++++ src/scripting/level.hpp | 10 +++++++ src/scripting/wrapper.cpp | 52 +++++++++++++++++++++++++++++++++++ src/supertux/game_session.cpp | 22 +++++++++++---- src/supertux/game_session.hpp | 2 ++ 6 files changed, 97 insertions(+), 5 deletions(-) diff --git a/data/scripts/default.nut b/data/scripts/default.nut index 7f674e51c47..082c35831cc 100644 --- a/data/scripts/default.nut +++ b/data/scripts/default.nut @@ -13,6 +13,8 @@ Level <- { set_respawn_pos=Level_set_respawn_pos, flip_vertically=Level_flip_vertically, toggle_pause=Level_toggle_pause, + pause_target_timer=Level_pause_target_timer, + resume_target_timer=Level_resume_target_timer, edit=Level_edit }; diff --git a/src/scripting/level.cpp b/src/scripting/level.cpp index 184fe5532c2..f513ccc07f8 100644 --- a/src/scripting/level.cpp +++ b/src/scripting/level.cpp @@ -86,6 +86,20 @@ Level_edit(bool edit_mode) game_session.set_editmode(edit_mode); } +void +Level_pause_target_timer() +{ + SCRIPT_GUARD_GAMESESSION; + game_session.set_target_timer_paused(true); +} + +void +Level_resume_target_timer() +{ + SCRIPT_GUARD_GAMESESSION; + game_session.set_target_timer_paused(false); +} + } // namespace scripting /* EOF */ diff --git a/src/scripting/level.hpp b/src/scripting/level.hpp index 970631c8f59..a35e1dafc15 100644 --- a/src/scripting/level.hpp +++ b/src/scripting/level.hpp @@ -109,6 +109,16 @@ void Level_toggle_pause(); */ void Level_edit(bool edit_mode); +/** + * Pauses the target timer. + */ +void Level_pause_target_timer(); + +/** + * Resumes the target timer. + */ +void Level_resume_target_timer(); + #ifdef DOXYGEN_SCRIPTING } #endif diff --git a/src/scripting/wrapper.cpp b/src/scripting/wrapper.cpp index e61e5653580..14f9e9de1d8 100644 --- a/src/scripting/wrapper.cpp +++ b/src/scripting/wrapper.cpp @@ -12471,6 +12471,44 @@ static SQInteger Level_edit_wrapper(HSQUIRRELVM vm) } +static SQInteger Level_pause_target_timer_wrapper(HSQUIRRELVM vm) +{ + (void) vm; + + try { + scripting::Level_pause_target_timer(); + + return 0; + + } catch(std::exception& e) { + sq_throwerror(vm, e.what()); + return SQ_ERROR; + } catch(...) { + sq_throwerror(vm, _SC("Unexpected exception while executing function 'Level_pause_target_timer'")); + return SQ_ERROR; + } + +} + +static SQInteger Level_resume_target_timer_wrapper(HSQUIRRELVM vm) +{ + (void) vm; + + try { + scripting::Level_resume_target_timer(); + + return 0; + + } catch(std::exception& e) { + sq_throwerror(vm, e.what()); + return SQ_ERROR; + } catch(...) { + sq_throwerror(vm, _SC("Unexpected exception while executing function 'Level_resume_target_timer'")); + return SQ_ERROR; + } + +} + } // namespace wrapper void create_squirrel_instance(HSQUIRRELVM v, scripting::AmbientSound* object, bool setup_releasehook) { @@ -13747,6 +13785,20 @@ void register_supertux_wrapper(HSQUIRRELVM v) throw SquirrelError(v, "Couldn't register function 'Level_edit'"); } + sq_pushstring(v, "Level_pause_target_timer", -1); + sq_newclosure(v, &Level_pause_target_timer_wrapper, 0); + sq_setparamscheck(v, SQ_MATCHTYPEMASKSTRING, "."); + if(SQ_FAILED(sq_createslot(v, -3))) { + throw SquirrelError(v, "Couldn't register function 'Level_pause_target_timer'"); + } + + sq_pushstring(v, "Level_resume_target_timer", -1); + sq_newclosure(v, &Level_resume_target_timer_wrapper, 0); + sq_setparamscheck(v, SQ_MATCHTYPEMASKSTRING, "."); + if(SQ_FAILED(sq_createslot(v, -3))) { + throw SquirrelError(v, "Couldn't register function 'Level_resume_target_timer'"); + } + // Register class AmbientSound sq_pushstring(v, "AmbientSound", -1); if(sq_newclass(v, SQFalse) < 0) { diff --git a/src/supertux/game_session.cpp b/src/supertux/game_session.cpp index 1b8ec5af3a9..83ac9159592 100644 --- a/src/supertux/game_session.cpp +++ b/src/supertux/game_session.cpp @@ -75,6 +75,7 @@ GameSession::GameSession(const std::string& levelfile_, Savegame& savegame, Stat m_max_ice_bullets_at_start(), m_active(false), m_end_seq_started(false), + m_pause_target_timer(false), m_current_cutscene_text(), m_endsequence_timer() { @@ -110,6 +111,7 @@ GameSession::reset_level() clear_respawn_points(); m_activated_checkpoint = nullptr; + m_pause_target_timer = false; } int @@ -535,7 +537,7 @@ GameSession::update(float dt_sec, const Controller& controller) assert(m_currentsector != nullptr); // Update the world if (!m_end_sequence || !m_end_sequence->is_running()) { - if (!m_level->m_is_in_cutscene) + if (!m_level->m_is_in_cutscene && !m_pause_target_timer) { m_play_time += dt_sec; m_level->m_stats.finish(m_play_time); @@ -824,11 +826,21 @@ GameSession::start_sequence(Player* caller, Sequence seq, const SequenceData* da } // Stop all clocks. - for (const auto& obj : m_currentsector->get_objects()) + for (LevelTime& lt : m_currentsector->get_objects_by_type()) { - auto lt = dynamic_cast(obj.get()); - if (lt) - lt->stop(); + lt.stop(); + } +} +void +GameSession::set_target_timer_paused(bool paused) +{ + m_pause_target_timer = paused; + for (LevelTime& lt : m_currentsector->get_objects_by_type()) + { + if(paused) + lt.stop(); + else + lt.start(); } } diff --git a/src/supertux/game_session.hpp b/src/supertux/game_session.hpp index da569a7b5f9..75ed63dd953 100644 --- a/src/supertux/game_session.hpp +++ b/src/supertux/game_session.hpp @@ -107,6 +107,7 @@ class GameSession final : public Screen, Level& get_current_level() const { return *m_level; } void start_sequence(Player* caller, Sequence seq, const SequenceData* data = nullptr); + void set_target_timer_paused(bool paused); /** * returns the "working directory" usually this is the directory where the @@ -187,6 +188,7 @@ class GameSession final : public Screen, bool m_active; /** Game active? **/ bool m_end_seq_started; + bool m_pause_target_timer; std::unique_ptr m_current_cutscene_text;