From d8e64348cdd4ef9f2443d199274a7241cd15d41b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=C3=A9o=20C=C3=A9zard?= Date: Wed, 3 Jan 2024 19:58:49 +0100 Subject: [PATCH] Good --- assets/{ => sounds}/error.mp3 | Bin assets/textures/background.png | Bin 44359 -> 8648 bytes assets/textures/dada.png | Bin 0 -> 8645 bytes assets/textures/dodo.png | Bin 0 -> 8681 bytes src/engine/ecs.py | 9 +- src/main.py | 203 +++++++++++++++++++++++++++++++-- src/plugins/assets.py | 45 ++++++++ src/plugins/click.py | 5 +- src/plugins/defaults.py | 4 +- src/plugins/hover.py | 4 +- src/plugins/physics.py | 194 +++++++++++++++++++++++++++++++ src/plugins/render.py | 94 +++++++++++---- src/plugins/sound.py | 63 +++++----- 13 files changed, 552 insertions(+), 69 deletions(-) rename assets/{ => sounds}/error.mp3 (100%) create mode 100644 assets/textures/dada.png create mode 100644 assets/textures/dodo.png create mode 100644 src/plugins/assets.py create mode 100644 src/plugins/physics.py diff --git a/assets/error.mp3 b/assets/sounds/error.mp3 similarity index 100% rename from assets/error.mp3 rename to assets/sounds/error.mp3 diff --git a/assets/textures/background.png b/assets/textures/background.png index 7341a339515140cdca71f774da7ec9c3309827d7..01be4e0a65e28d0627e850fee643a7332b2b8227 100644 GIT binary patch literal 8648 zcmeAS@N?(olHy`uVBq!ia0y~yU|qn#z+%C{1{C@Ks2V81z*rpQ?!>U}oXkrghb7(7 z*O7r?V?XzwL{=c5v%n*=n1Ml034|F}J-R!Ifk83W)5S5QBJS-CL&gRJfddEFWo2LA zmy}9yblQAZ_v3`uk-ueuS{WED-u&YRGu$OFfN2$;3@|OkW&)-;nWaEh;}&C z0CE?DLZSmRh@N1ufDuG{NQ`P94LSyf(PRV6kfT{>X zO{3AK5vUv(Z5oaCq=3m_v==$rGy)X|qoW6-BPXDAFxosAZ61Kq!D#bfw0Qtb2BXb` z(dGfDI2dgnj5ZHI>0q>ZFxor-rGwGt!D#aUm<&dn2cyjcP;oFo%>#qaj7uJI%}G;A QT?%rAr>mdKI;Vst0GY|yI{*Lx literal 44359 zcmeHwc~nzZ*Ed8^5Gx=m4oreWtqNEt1c9_g(W>AGC=jq(v7&%7Nq|7qD#%pofL0kQ zDl$eyM23)5s^G++ASyB_AP7R3Vt|n3+c#lIXmacy@ArJ~`X0Jg*K+C2z2}~@$KU?# zeS^1Hn13~K)0{ST zy`-eHq&Ap+x8oS$Q?-9+inl$F=Un`>rbM^$Zn@Q)=jC_tFSbpTeLnTv6zv^<<>GG! z&zduTeods!)um?Nht3~AXPrveDo(+S<)>uUZeD-p)YRqg*4?^pbKsZj3D@szk@5EO z&M`8|&0t5bmZxc!-*qZ!Z_0FPdb;;_m7ngVH8Jxwd{4U9*imnE_LT&XC|z^#fl|^q z895~_(LekuYcaL`k@Bd&9lVab9XGv{q%-~g2V$~|^3QzwQ2iE7$Nnv4O2Y>$g`kOazlDxdVldY0>E?KwWazpLnk6JXSbbQ%7qITk`faW5BmD%?zXi}epy+WOrklg zm=(6Y$)ss@N#dKf&xx(!#cdvj6=y9?cC~%_v}1wA6$8(wU0p|4n3@K(TzU5F+4jOv zuBV56cNdfW7mwCgGJ)6EO4A!)rJI`ecE5f-(B05nQWjrwZ=kO-?={U~(``mG!#N`( zql`v7-?M_j<8&A7@i#Xjm$^9hu=(eJ!@eNXcm zxxBW-e4VDU+Te`=+6v8O&sle?ToxH8Ylzb>895mlE|nhu`^mbrctKqjPUyI+ebE@m9723B0U9zegWv7Z=Jzh(@w3Vq{U4-lZ zJ9zy{oR(NCMBbokvujtdlG(}!9Fi&Gd9U8SU971-T}PqWB0*)QmH3yGd8{yL{E4oz za$HVT+qzTJ=7}28A(PpKd$vErZ+T{*#o;G9xU2uP0Pr^#-N@P5*+KY*X=-8XbPx-> z*_z|$VH3JI&dJFMwXf+!9MQ)oJ1z*8d^<;P?^W7L zyYQ|qE~q_Cwfa{#+p}j5cC%HNpCMKjm)0r}!kEjKLnbW}D>~A+Mp-agkIlC$G?(99 zqb)(eQih}w^on1m3cdgnvyfn^(2{*H~$b=S7P&@r;~EqbCQX5%F4Tmt?)&Mr~Z;nn>t4uDYgmA*GdcA zF{#WYOYyCnh(iEkq3%~W+Cr!O|OKC*YJ2v4MeoaQn3?7$Zy1cYmzIp4D)550i z>|Fg7E2d>*2FIvYU-Od9kd5e=N-kqxHm8(+&Ge~mC0J8ic2d4M|3cWn)6-R7)zpMP z1+7XT*r+Qj4O)(egt}+0B==6=-o)7c#P^%O6Fx>YYjS30X3>F2*&avzppj!dmPBu+ zKC86%wm)*wI=}0Dg{-RGmb^vlh4;Ju>s$Hx8Z#H&4s7?z720mgX~#ZpbeQ$ShP!)z zP*4%F(RETd4v8n_6wN(Xgv)vH@e22G_|QL3Rhr@di(|{#Lkas|3Y$FocQUiW9)4$G z)V3N#?Vv5`P41Dw9fPB)B={cw6r-2l``*U(qVPr*ZqQ;y}XY5|F|C1wa9?~2#t}gvvS6za+FfZxFix-^g}U5^(fH<_@qcV@g!dHM2X z5{1M}t}tf3-R}QhCH7`JjmI5Fc-L%^+me)=)Jmao*Xv#+;_B9)@>UksqAO(jz`(#v zH7f51aPB#2EK{rWXGG{%VMB5~;zQ)KIj) zs;bK9wsW?U@QsUa;3<_Mb|$w2O^#^)9wrsUcMoLut#vuAY02E?2=2|C_h%MO_V9(Z z(spz1>kZr|7jKSPzWat|W7Ef&S*DL$bEga6G5klzpMRcrG_mN$y;{bIL|N$R6KUPP zHQg}>duo~r{7BrUfc*a8*xulRxo2XGh>Y$CUT=iz;TT=7cfYE}v@7vUgzs7`Pliyu z$RA`O+G4?QP~%1{FuK!IzJ|50^uhh7rE&bT?(WKOj!{iu!T0YZBq}?I`u0 zo^i2F^3CI3td1P9JmjS+2UcE=35T5x<8|p-Q-~GG;SIY>ydt|xOnE6Kj#5Iml2iK?3o;wNg?oYQ58KK$ z8FUrPHOftW&uRyrf##>3Av9gEbx9;a0AWZB<2;P*zS6V<>GUnUrw%|_uRkx+d z^u?pdRU3VAhFuv`1i=b>O7HWi{5zETA2pW~=M^?rMtj%n;?$dR_j*G38&EQDo#n>b zqfeGpM1Uo|kneHF`{VT}D}Weo`W3D{x9f_K1<6=k+UAv9Hg4T@(@5Dnw7B!!r62h( zM&>A}HTuTIx+GOXtWeQhX-(^|NUChz3AuB{yePY)2ZN2mW6Qj4?T$Q{R&hen#2~!> z@}-eGOXJv6Z0zk**D2PW@5<`4kz}n66pS{>Z}qS^}CwnfBg8ds11Mt;h3Jxkzdo_ z9M3i?-O8SFufHXk<$2|V@bdK2_+fk**kP+hUDcRp$#uDfr!3_lWG*{eKX1~Z}l>P=e08WI{$ z2L<2v2?AUQD2MV^I#RfoLT0yUA$C4DMCa#t6P+?#IFF|Y34Q(Rnp z*xs{Zr&H7YG~qOotfOS(;1FR4v7W}H@gsJEqp+r@^ar--tH^|D_dtv-d_%{L+8k$R z=SXaGT(V{lhn25rn4NN95~-Y6x5;Wgh3ejax4-{=qi%5KuOd2o7qhnePV!@TvtgYr z-sITx^U1SaeGq|U9K7RU>0fNb$UE30n&C8E#mAQgy|vmW9sTBlxr`bA#`M*T_2nJR z4EnEz*7il$CUV;u+_e7wN}2}e7XqmNs8F@|0=~tU1Cp2VYeUYU6tO0*`TS~c)AC0L z-uYD>&f2NTepP5u-2_@NgX-X5HluNxkW;K%Jh9wUJ)5{_>*m8xR5ApaFxh=q9gWAP zmAtN+y(GGp$7NZAMl|reBWn9!X7M3M63OIfZIlDa=bA`5PtI6U>XN0S1NIur9fxoa z@-mg)S{|o%nbDq2;rUo3rs!8U&eEvu(b)yb&S}~6k1o(7mW62B4`mK~J-Kq2ygy^+ z%sE{X!>?bzJ|aPv4~|so&iq)LJMF zB4>PkD>HLiHW8wTgKITX!iR&pU5@VV8DOkyvpf1aPMg|MyQ3Lv+El-S=+_~v#--(B zOLOUaVb5j|MsI03`1jYh7n)deug+Fh6_`Eq#T`XD5j(62K8?x_nQt3405fQz@cJpq zaP;mDWhDXJZnj~2+ywihkLFDN^WH zDK+dGmX)h-Rn=#zpGFub%+)Wxg~d)cI@THDpf)NeJ0$<<(p1Y;z^{6Ke-eAj z%#(fFvzy8(zsIuMb1B67*|j#`_8F&^O@SAExM)$ zO>ee;oM@R`k<g0OdB*(-S24JK#-t?7(Nnhq=BtAW3PJa_5<@$iQjzUSHiKGoZ5AIGs7*|a-Xz)rRPspn+28;^8WY- zu3gLe9Uf`=G(>LS*m!njHPSo%KQGx;8tV7TYFi&&daGTU4; z4I=No@XC!I2CXJ2fm-|c`m&{*+8O7{O>bn-s%p;0dqb%ut} zKYZA~uN>Sc%ILD^5Q=GY!{7_n0b7rWL%c2Sw zn1BdnRc(Hnv_WwCYFp|+Urg&RC72#|acS?F5dEvpI)!E|=SW{~4w;{8SC>xegJY zcC?djbD7yC{_T#A>@}^hl~qlVlXZj~G`(~BjN}DzQBhG5SvNlNT^s$RykGa_+R*CG zfdN1MscX*n>uPxPOFr9tE#Gyw;o`A${frFgBx6Su`qboZ_+a zzNXj!`m*kX>Hr?6EF`b@fnNCM2O`2f6}M>7qR1!UIRH%^jx^l#$qQorx*Qj@cc;`3 z$o?JMU!L&UM|FbHr&F_C3|<8n4<=xOH(xsC)tfhGHmUH#DrISbH-Y3W-gNyT7ng_a zJ$c>5A(@J^HH@XHIXT2vPnX36JX=w@W7)E0>G`5b-uDU7H9Th=4@*Rf6r0K)gAi%``y`z82mnYc9LXO^_${PfG;PzDZcZVr{@68$_ramyUj9IhG(=hrc zYQm|soR*}dYL`OMwXavWmR~i$E9(Jd?qMmGe4Q%(nM|T-=29O#cwm#eqZhu}!Uo`ZFeJ_Z8vZsFX&=o)_$#!8v@_M!Br4EX5U7Tdxen^7>-So*Yy3 zIA<*!@ZZ9KH@i8v13%9@De}`5GwVAzimbQpC4UZBc;9B>V=ankf^`W_{>XHQ=fgr( zp{{Z0`+S4QAuhn7k*G{gZAV?CxkjRf@2B(=F{C%$x{!pmoLQ7+&~{|<5crCTF8#>V z_5Q9~ATAsDJ+{8LV5tf}4xcfBeE$6TR~H|7J^GUY-tzZs-i!V}HT(ZO;2`u4(zyEh zg0Q~+lrw>Ubdbbi(ZDMM?3bo}k1Q`d<;1@9th84hwQ=+Gjg8een2!qZPbj>FYbTiY z?J6DsBxajGrrdWnn$vo&>z%hCSu*D7`E0=SoO((G#+-u1{lSN(?ZRj6P92FU{*pD} zc1BnSTmS6$X|JYH?-p~v50K+;oflOaZ#oW3G0pv~b%hHPOBJ>q@*VExnJ8hwIE z2N{F>y5=n6@jhnNl6+UV)N(pLS_b62;+WbugQf?G8jIhQAWN{=w~SJ^o_fo0s8%$8 zQ=LD<*C8`K{YY~zKxy2jmRRn`*vZwaTj&QcP-}0Ly1()}z<#HT8ZWIm0N=+H?VnzH z;xDZc({qfBsVK2Bw`OM;A@)dViMdHvWDAE8!28b9w6)0})BfU)GuC<9Z&Zas>OlN2 z{ds;I$(t0*lU{LF_e2yuv+ms*H#dP`N~*}FemQDRUAE*1;(?)2A9Ftss;+`!4FtH6P)6~bDU-GUkB`KOI8jJN}7 zoRd8k>g^X4qlPMpP(m&H$)~6F`9{6ebqqZcw>#D$v#RZ&lT!jD1^GLTd+SbA1Pl}g z?8wc{Z3->NLi>t}ik8(SJ{Aoz5J!#56wVE1E>n-|3ew!)RAv|}8~3kR7+dDNp1^)n zxz1%3vr&E6Ckn38pL(;^@V%1H1hA?j<%#**>LA4mt!5P{nvM#DwdUyeFW93J3!;1E z$kL_BgZMW-?jj$ldF8wog2|Yl+$Rb_CiuFqWwueKKp8md!iDhrf3FzI87E8!2a|*; zkY?|IlVK+ol&_<(I)w1{K)Qi^C^wwX7f8Key>S7_<`02i!0#d}2J{yEe4=3Y)m?t; zzY{**HESJv*0y2cA^?NY!ytmzmK}~gNud#b4LT400OTxy`IjQ;mW^eQ@Nt>X1DHf7 z3V==jZr=u{d4q{D5Ft7gdAEoVQYzYOa1=Y@FNgpNP?~dcTJY^%S&x%D2P1tfntI<` z%^-PuLclOCVj%iqv;2WU2PBBBLgTTlH53wb7Mn9{Riwx#>CGYI0@^A`sr^|2?5t(Q z5iv0u(QLr9r@f-)hCvRIA2~&g$%)+#Q zYNcSwc+20xDizJPe_~O13;41$6V6A=1oJXsJ*G#>KP@RSPXh~D>8U;p4rsaUz&Q-O zS4}R{6WN(f$MG$x(<8mXhqZ0!y>!o9_yg1bSA$b+l{Eeb5Mhb5ucjEuzYd!GD{K~p8$RX_Vupc~DfT|qEwh)Sf zM?DblcwZLsBW+xywcGMpVI#*s>bfn2k&=IObl7m>HM|Rhe`u6_*dpKD*x1;GJ0l!B zjVw(`84FE4bS&{Da1_G`i}3M-P?5WEkd!sX7yPtOTK&=vk-T-}zS{f<(*+;^V6qGs z5=Q==e^cPh0B|j!E1(G_)z#HOc+l7E?3p97ArzVm_Vmu~e|0m!lChgrA6hw5dHLtc za?+ldpMC(A%*91_*iVgmq}(~xi{x`C6fyF+jdSLmi#`r`m4;k^8|5(_y{7OAQ+T?k=vi-$xK0j0S z5v}Km-bRWnuX-S>N|`Khwg?f0(x5WCfJCeMqBaRE2^8`h?tY*$@LJa>sN^uw?|8$ z<8xUnD=Tf8$jwJ+wGjyWy>itmfk^@qi%_a1x-i((6Mw~F6g^RTjHpz{<>514FaEOK z0^6spkOOCF9VEFp=Yt+69O1Zjval(Wiq3uw{Dz)oZb;S>1`S&4pg%I-_!PM*FN|{zdp&T9n=CEO zPlYXx1MW3y2LL4z#1Jl-yHTFy`SWd=37BedsGMnTo%5WZ~3)i zB&Z%sFN~iKQtB>t#GbDbj>3npqua^>3 zc+aVhB|riwVJra})Q(0V6OJW7S^%YtB|u;!8ZoBFSOO#?HY40!r<69ORAH#Kz-V3%k-crSA@AddC~yiix(&=F&Kqc+AwcpkP!+W)=T+}YAY zUDBeJ2KqIHl#~=RIql2u(d8~6SPquEP$k|*S%CfdfU0nh;fN&?j!v*zDiXPT`_>Mi z?1|y);*}9lcDbj$V7G(`w!-^>Jrhc&&88HN)-6` z29#W(3h@+5L5%ix)X<9L;cfMJe0qV3za^UFLy8z1Vg5hKl?KYL+n z7ERpai5MaSQwx-X^2U8ZD4<1ZP$X zoEkri-+U_^7{3poK|(!^qZC7CzFl0k3%|GytYipE+8VPnlRBUjB2Mc;=E)0y1;cE}NtO;~gzV?hX=8f7>E&X>m zcXxMSx-2r76}Aqs^IIxC1-n&JT#T|^dzmslP>MtFHa@krt<8yC5ZBbygsN-;6H8Ef z4&NkcR-7`%WXG!J?Mx!-q_N^Be>f4^54X3wU=_sAG9DxBQ_*vTqF-{w8La4sE;}z? zy!f1eGb7woEqX5u&BDM<;QRbf&=9s!c82F2FHk8};pSGf2{$qfIZseBY^0DYaQ(p1 zB9t{HEp5<_z*HxMNWdg5J{uD?`|6>&R+kJgwoLvTxClU{M3`rc6)f1XH83I- zYFB~dI%`ln&?JI#RzI|4scmm)x`9fIHC<>mF*d`XCs~Vs9wgsUPu8#c7EdEkiqgejqqRlvR#jU| z{Pv~a;#X}}LA>6ZyQvX3#y3CPkGkk(tEd0+75Idg5~ua;*Zr+OUbMAB6M^49)HbjSb0SK48)Rh(vl-A9u!IW=T>VMO3F*Abp1)pvT`Z^Y&(e;(VOJV z2-vDi_INHFoq6yT-^9GGwRO?aL;C_rwA#qM;zL(_9HGw5pxiv+r+tA0-6+&$$JV?w z-#)I#B`ZWt5)+MygJy1%KW4>M-3>!r6aXDG@OshxjuNw?WvcfG6o~6q#T6b2Mcm5N z)RbA4IAdnqc4vuWI+v*)TcZK@lB)K!Z;%w6-&D0(JY8@r2Fn+5*?Nuxtw_qASHDPX ziXVvoG462Hdc$wg5_PMV0WL|p-_5`*Ts*@}icBQm_(dS*nh}KvZTPL9Mv9asX4}0~ z_*%l6%;H*eFty@wIBOBl%V)Im)yjcViFjUzpGj-{_~xKQ@xv`mesoN?F>4RY3nZ=4 zKnWC^ooiw^O=p}(!_eHMQ4R}O(pM}Juys=^TM-MoRqY~Jch+H)wKHj%UCQR-s02(Es#*Ss ziw^v815+xbh`&0t5=4A~!Ss-|s5@2N(Ssn)P5(L zw2*`7N3%mcbjJ0xGS46mFSj~PAek?gBVG^my#K(#z?wrV5f?d936)?rJq`}fG#G8H z9Y5d_OTIBT`Rg4(&b-~6UUA@?-#L6OE7BKMZV--m8mPIz4`)>^LAdKl1zR*Xe^naw zWS+!}{KD}K(5te9r)Cu;)~M9E%jDN%)Bhgg0$|T(5>cT-NmGW&!NBl~;zOmncp1#z)j*&NM}QEnd>T#QAP}`jdWutVf!|aFAT|c`IAo zIpBI|S6AI>+~dn@0%p-NS+{ zhiDJ=zzCwWzhQKXNBD)9 z5-0z1DTz%!=JJcDhZIpEB{2C*xzAowQMZG`QO9Cf6p9jUfuSe_N+rT;IObNr&9SJfenS18)?Xix`75 ziAB>Gtl<)ZX-woQTc;t!8+a#zZMyLbBD=y@-NkZHD(pOB4~Wz>3`{HEEwOw3sqD7c zo#B4R3jjOjW%hT9D;f8GP0nZNuwdozl9HbWqdo-LvulQ5{&My1%jYGjBV z%6D^Dr|63BCT)`Q^Y>pH4rd2REkf&B><`YL3|zK8#)8iNapD@hbHIN`lFG zlDqOf>{T(fLfgdtd;uEaPbDB^nBLu&5;03+(FQajfT!(xewkjJ|MB{G?p?Kwqv0=L zE6|_H0zhr1wanSvLQ>{(5;B(`_rZP80Fg@;pd$*+cMqf1{8Rc(zNLfY1$CJKpN{U2 z2$s~+`qU1?$z-(k;xr@75h~ov;iL)>aRk^62v93C2{S)nJP=mn#d1Ebj0B1HX9UZc znf#S*vlwCR_$Cp9l>x>>Jdkcq4K#_iU`n2wa}1z&mj>A$uUu4?3*QG5XeSB@Eos7# zP{jfzgu!oT`3f`vX{H|@36(P&e?TuzhbHmMJQtq)d~Np3I0uR~;_zOzc?rz)1h-d_a8+$zq|B>l^BRCU3>8KiurDx;*EvmGoS}u+ng>DRT=g$h zhF&pP7-sBmpoX#rm|elFK}6wRgf<8&LsWn$C4(8Ip7xkpp=}}`C;K13#<-l2yq(2) zfuH5WBl@e3E*>2{VE$&Astu)5nfx?Gef7}%xltrJnJKu07YiewRpaEYv#Zq~6!4-s?4<OXSL=rehBu}H`*ct#2q;pzx5DxT~&0@i=Y8foVWrC`D4!6)+ z!UUCb5F|!=?E8XMgs%|EXpif8=Z3Ndm|elFfr&;0LL0Q00UWs1!OSGYRF_ImzFHwG z?{N&az#KsCh>+nmu_gUCr44@)U%LfO=w<13S*`BIy;)#j(&G9gU0mT390QeLF7s`M zSHlkRBc_zrV^w~$mEAD%5>;daDifv(Xl!gacz^;J;?)KVpozSTGloB|CmQ-d;swPQ zXl%jtcOFAQI<>X6g3Ux#Q_B_d2o%ixw1e@GOru)9c{^svN}84-E;_XGGIpoWmU4*j z9cqLgI<;f&Wz4;dxtB5b^1s!+%qv@VYKRLk=4!@V%~&QgmdT7|GB}z2x?`CPlmN#v z8LXvZERz|_Wd7@cGK@F@nei8L>{a?I(%pqwapx#q^Zt{sg0h>Z4vrvHj z1fK(t<^TYTK%f~6SHTmXG)_T0kwCG=z-5*{YFU5WNW_-a)QxH?Sb3SxIAbq38Cg5{ z4ZP6^v^EZkq&WaUkMWfMt6MIy79)@+v}6KZ5R*(a2?N?enmWQKpH1ce(~{K(SH8)3dy_>XzGW;ooXqX!Z_4aO9Y+;g6SyW*%&f4RdjF? z`eCxMei$riLN^w5{LeRA_V@Xrj^){YJ}H1JzejRgJ4Quu0g@(=5fE}$7f*msb|)a{ z?|HQh_{tiN2wXnIZdSEf@#V?zsDu#^a%M6>p1_EXXgkJYQw6+dBp3s+C{SqbsFosf zIOxg1^ng;i2%!g`ymAG4xA_H%(SbFt0>L)pHR>WJ(H;Y&kn?V?nIxW|1Yc*wpfn`o69&ha z$!OR9o4GL&3jdjO>L%pf` zf1x{u<7a|mf9}t~V6_9hllon`Q^YpR;mxoCN2|KNY94F8{BLf)*L3<2vdtLGWqB{ng4EG?zKmCWP6~YWYQw>9au6g3K(hj&!)fE zOH-l3QZP}hVUr^F<4za+NxF#x&a?PCS?Srf@$`*}&E+bP1xb8l$OCQckyt`?O490?vUjTvb4uNRy2cOw_WUa0aQw*tV4E)A ziFaLj8TA5~IQ;PqHmzu%&7WclV?fQ`;^4S^R)yl`X{8$9&*-z47q{^s4VY_kY0xMw zc&Crp3Adfr_>!jXe%uuC5~`)`W*DuV9l0oO`(YR>D83fwVprGJwnSTLVVqI41!~>t zHsy-O3)W4w|!NkZ+E%g$ANBC5ToKEYTzC-eO6cq#aA!&(cowC^2{{~z8i^nL&U diff --git a/assets/textures/dada.png b/assets/textures/dada.png new file mode 100644 index 0000000000000000000000000000000000000000..773c20d79ef0541ccbf31a9b8d86350d4453cd21 GIT binary patch literal 8645 zcmeI!F-yZx6b0ZDlSoquh&UAotIkdi1rd@EX(Cvp2%>}NAi60OL2$5&AWlkuK}U5c zWa;AK>>%RgB1$)hF0K~!o%{%=GlZM>;1LMt-uGRu)T(~5R0QCsNn8iqHsDkW!)E8V zcWgFbHR{y}ou`RUvv799US7wB=JM)U zCD!ISeAXb+as4{QhP|Mc(YRm^G%C%_q-#P~q6K$RS0YeZ|Y5)KL literal 0 HcmV?d00001 diff --git a/assets/textures/dodo.png b/assets/textures/dodo.png new file mode 100644 index 0000000000000000000000000000000000000000..88f011c63cd0261f4b025766313d4df8e2b946e6 GIT binary patch literal 8681 zcmeAS@N?(olHy`uVBq!ia0y~yU|qn#z+%C{1{C@Ks2V81z*rpQ?!>U}oXkrghb7(7 z*O7r?V?XzwL{=c5v%n*=n1Ml034|F}J-R!IfkCm()5S5QBJS;tg@R0u3@itmwOFPJ zbg(;%w73h3alG+15I=N&_ItDF*@tA-t-qZQG=_oUz^T4sMi3(*l*t4{GjK9WfvAQS z23`>D;M4$e6oZ1I12c%8Ag}0q>ZFxor-CWF!D!D#aUR2+;p4@R2@ zpmZ?WJQ!^rfYQNe^I)`j089p>&4bbA0jM|_Z61s^4?yW)w0SU0n+FFZ>Z45r`w}^q Q90A3%r>mdKI;Vst0Ba{;{Qv*} literal 0 HcmV?d00001 diff --git a/src/engine/ecs.py b/src/engine/ecs.py index a94a1f0..717eba1 100644 --- a/src/engine/ecs.py +++ b/src/engine/ecs.py @@ -146,10 +146,17 @@ class World(Entity): - `entity`: l'entité dans lequelle ajouter le composant. - `component`: le composant à ajouter. """ + if isinstance(component, tuple): + for c in component: # type: ignore + if c is not None: + self.set_component(entity, c) # type: ignore + return self.__components.setdefault(entity.identifier, {})[type(component)] = component self.__entities.setdefault(type(component), set()).add(entity.identifier) - def get_component[T]( + def get_component[ + T + ]( self, entity: "Entity", component_type: type[T], default: Optional[T] = None ) -> T: """ diff --git a/src/main.py b/src/main.py index 06f1d8f..33f6a2d 100644 --- a/src/main.py +++ b/src/main.py @@ -3,9 +3,58 @@ Module d'exemple de l'utilisation du moteur de jeu. """ from engine import Scene, start_game -from engine.ecs import World -from plugins import defaults -from plugins.render import Origin, Position, Scale, Texture +from engine.ecs import Entity, World +from engine.math import Vec2 +from plugins import defaults, physics +from plugins import render +from plugins.inputs import Held +from plugins.render import ( + Origin, + Position, + Scale, + SpriteBundle, +) +from plugins.timing import Delta + + +def lol(a: Entity, b: Entity): + if Bounce in b: + speed = a[physics.Velocity].length + a[physics.Velocity] = a[physics.Velocity].normalized + a[physics.Velocity].y = (a[Position].y - b[Position].y) * 0.005 + a[physics.Velocity] = a[physics.Velocity].normalized * min( + (speed * 1.5), 1000.0 + ) + return True + + +class Bounce: + pass + + +def lol_simul(a: Entity, b: Entity): + lol(a, b) + return RightWall not in b + + +class Simulated: + pass + + +class Bar: + pass + + +class RightWall: + pass + + +class BallFollow(int): + pass + + +class Mine: + pass def __initialize(world: World): @@ -13,16 +62,154 @@ def __initialize(world: World): Initialise les ressources pour le moteur de jeu. """ world.new_entity().set( - Texture("background.png", 0), - # Scale(1000, 1000), - # Origin(0.5, 0.5), - # Position(600, 600), + SpriteBundle( + "background.png", + -1, + scale=Vec2(render.WIDTH, render.HEIGHT), + ), ) + # world.new_entity().set( + # SpriteBundle( + # "dodo.png", + # 0, + # position=Vec2(800, 500), + # origin=Vec2(0.5, 0.5), + # scale=Vec2(400, 300), + # ), + # physics.Solid(), + # ) + + world.new_entity().set( + SpriteBundle( + "dodo.png", + 0, + position=Vec2(100, 100), + origin=Vec2(0, 0), + scale=Vec2(render.WIDTH - 200, 10), + ), + physics.Solid(), + ) + + world.new_entity().set( + SpriteBundle( + "dodo.png", + 0, + position=Vec2(100, render.HEIGHT - 100), + origin=Vec2(0, 1), + scale=Vec2(render.WIDTH - 200, 10), + ), + physics.Solid(), + ) + + world.new_entity().set( + SpriteBundle( + "dodo.png", + 0, + position=Vec2(render.WIDTH - 100, 100), + origin=Vec2(1, 0), + scale=Vec2(10, render.HEIGHT - 200), + ), + physics.Solid(), + RightWall(), + ) + world.new_entity().set( + SpriteBundle( + "dodo.png", + 0, + position=Vec2(100, 100), + origin=Vec2(0, 0), + scale=Vec2(10, render.HEIGHT - 200), + ), + physics.Solid(), + ) + + world.new_entity().set( + SpriteBundle( + "dodo.png", + 0, + position=Vec2(render.WIDTH - 130, render.HEIGHT / 2), + origin=Vec2(0.5, 0.5), + scale=Vec2(10, 200), + ), + physics.Solid(), + Bar(), + Bounce(), + ) + + world.new_entity().set( + SpriteBundle( + "dodo.png", + 0, + position=Vec2(130, render.HEIGHT / 2), + origin=Vec2(0.5, 0.5), + scale=Vec2(10, 200), + ), + physics.Solid(), + Mine(), + Bounce(), + ) + + world.new_entity().set( + SpriteBundle( + "dada.png", + 1, + scale=Vec2(10), + position=Vec2(500, 500), + origin=Vec2(0.5, 0.5), + ), + physics.Velocity(Vec2(200, 100)), + physics.CollisionHandler(lol), + ) + + +def __update(world: World): + """ + Test. + """ + for entity in world.query(Mine, Position): + if "z" in world[Held]: + entity[Position].y -= 300 * world[Delta] + if "s" in world[Held]: + entity[Position].y += 300 * world[Delta] + + ball = max( + world.query(Position, physics.Velocity, physics.CollisionHandler), + key=lambda e: e[Position].x, + ) + for bar in world.query(Bar): + bar.remove(physics.Solid) + entity = world.new_entity() + entity.set( + Position(ball[Position]), + Scale(ball[Scale]), + physics.Velocity(ball[physics.Velocity]), + Origin(ball[Origin]), + physics.CollisionHandler(lol_simul), + ) + physics.move_entity(entity, entity[physics.Velocity] * 500) + target = entity[Position].y + for bar in world.query(Bar): + diff = target - bar[Position].y + bar[Position].y += (diff / abs(diff)) * 300 * world[Delta] + bar.set(physics.Solid()) + entity.destroy() + + # ball.set(Simulated()) + # for entity in world.query(Bar): + # entity.remove(physics.Solid) + # last_position = Vec2(ball[Position]) + # last_velocity = Vec2(ball[physics.Velocity]) + # physics.move_entity(ball, ball[physics.Velocity] * 5000) + # ball[Position] = last_position + # ball[physics.Velocity] = last_velocity + # for entity in world.query(Bar): + # entity.set(physics.Solid()) + # ball.remove(Simulated) MENU = Scene( [__initialize], - [], + [__update], [], ) diff --git a/src/plugins/assets.py b/src/plugins/assets.py new file mode 100644 index 0000000..852e6f4 --- /dev/null +++ b/src/plugins/assets.py @@ -0,0 +1,45 @@ +""" +Ce module contient des utilitaires pour le chargement des ressources du jeu. +""" + +import pygame + + +def load_texture(name: str, cache: dict[str, pygame.Surface] = {}) -> pygame.Surface: + """ + Charge une texture et la renvoi. + """ + surface = cache.get(name) + if surface is None: + surface = pygame.image.load(f"assets/textures/{name}").convert_alpha() + cache[name] = surface + return surface + + +def load_sound( + name: str, cache: dict[str, pygame.mixer.Sound] = {} +) -> pygame.mixer.Sound: + """ + Charge un son et le renvoi. + """ + sound = cache.get(name) + if sound is None: + sound = pygame.mixer.Sound(f"assets/sounds/{name}") + cache[name] = sound + return sound + + +def load_text( + text: str, + size: int, + color: pygame.Color, + cache: dict[tuple[str, int, tuple[int, int, int]], pygame.Surface] = {}, +) -> pygame.Surface: + """ + Charge un texte et le renvoi. + """ + surface = cache.get((text, size, (color.r, color.g, color.b))) + if surface is None: + surface = pygame.font.Font("assets/font.ttf", size).render(text, True, color) + cache[(text, size, (color.r, color.g, color.b))] = surface + return surface diff --git a/src/plugins/click.py b/src/plugins/click.py index eb58691..47db260 100644 --- a/src/plugins/click.py +++ b/src/plugins/click.py @@ -2,13 +2,12 @@ Un plugin permettant de savoir si l'on a cliqué sur une entité. """ -from tkinter import Scale from typing import Callable from engine import GlobalPlugin from engine.ecs import Entity, World from plugins.hover import Hovered from plugins.inputs import Pressed -from plugins.render import Position +from plugins.render import Origin, Position, Scale class Clicked: @@ -31,7 +30,7 @@ def __update_clicked(world: World): Met à jour les composants `Clicked`. """ mouse_click = "button_1" in world[Pressed] - sprite_entities = world.query(Position, Scale) + sprite_entities = world.query(Position, Scale, Origin) for entity in sprite_entities: if Hovered in entity and mouse_click: entity[Clicked] = Clicked() diff --git a/src/plugins/defaults.py b/src/plugins/defaults.py index fe5b434..2ad6ca4 100644 --- a/src/plugins/defaults.py +++ b/src/plugins/defaults.py @@ -2,14 +2,16 @@ Plugin qui rassemple tous les plugins globaux. """ -from plugins import display, inputs, sound, render, timing, hover +from plugins import click, display, inputs, physics, sound, render, timing, hover PLUGIN = ( display.PLUGIN + timing.PLUGIN + inputs.PLUGIN + + physics.PLUGIN + hover.PLUGIN + + click.PLUGIN + sound.PLUGIN + render.PLUGIN ) diff --git a/src/plugins/hover.py b/src/plugins/hover.py index d5c32e3..c458e70 100644 --- a/src/plugins/hover.py +++ b/src/plugins/hover.py @@ -44,10 +44,10 @@ def __update_hovered(world: World): """ # On met à jour les composants mouse_position = world[MousePosition] - for entity in world.query(Position, Scale): + for entity in world.query(Position, Scale, Origin): # Récupération de la position et taille de l'entité size = entity[Scale] - position = entity[Position] - (entity.get(Origin, Origin(0)) * size) + position = entity[Position] - (entity[Origin] * size) # On détermine si la souris est sur l'entité if ( diff --git a/src/plugins/physics.py b/src/plugins/physics.py new file mode 100644 index 0000000..8879d4f --- /dev/null +++ b/src/plugins/physics.py @@ -0,0 +1,194 @@ +""" +Plugin implémentant une physique exacte pour des collisions AABB. +""" + +from typing import Callable +from engine import GlobalPlugin +from engine.ecs import Entity, World +from engine.math import Vec2 +from plugins.render import Origin, Position, Scale +from plugins.timing import Delta + + +class Solid: + """ + Composant représentant un objet (de préférence imobille pour que la simulation soit prédictible) qui ne laisse pas passer les objets dynamiques. + """ + + +class Velocity(Vec2): + """ + Composant donnant la vélocité d'un objet. + """ + + +class CollisionHandler: + """ + Composant permettant de traiter les collisions. + """ + + def __init__(self, callback: Callable[[Entity, Entity], bool]): + self.callback = callback + + +class AABB: + """ + Définit une boite. + """ + + def __init__(self, min: Vec2, max: Vec2, entity: Entity): + self.min = min + self.max = max + self.entity = entity + + @staticmethod + def from_entity(entity: Entity): + min = entity[Position] - entity[Origin] * entity[Scale] + return AABB(min, min + entity[Scale], entity) + + def entity_position(self, entity: Entity): + scale = self.max - self.min + entity[Position] = self.min + entity[Origin] * scale + entity[Scale] = scale + + def __contains__(self, point: Vec2): + return ( + self.min.x <= point.x <= self.max.x and self.min.y <= point.y <= self.max.y + ) + + def move(self, movement: Vec2): + self.min += movement + self.max += movement + + +def line_to_line(sa: Vec2, ea: Vec2, sb: Vec2, eb: Vec2): + """ + Renvoie la collision entre deux lignes. + """ + if sa.x == ea.x: + sa.x += 0.0001 + if sb.x == eb.x: + sb.x += 0.0001 + if sa.y == ea.y: + sa.y += 0.0001 + if sb.y == eb.y: + sb.y += 0.0001 + + divisor = (eb.y - sb.y) * (ea.x - sa.x) - (eb.x - sb.x) * (ea.y - sa.y) + if divisor == 0: + uA = 0 + else: + uA = ((eb.x - sb.x) * (sa.y - sb.y) - (eb.y - sb.y) * (sa.x - sb.x)) / (divisor) + divisor = (eb.y - sb.y) * (ea.x - sa.x) - (eb.x - sb.x) * (ea.y - sa.y) + if divisor == 0: + uB = 0 + else: + uB = ((ea.x - sa.x) * (sa.y - sb.y) - (ea.y - sa.y) * (sa.x - sb.x)) / (divisor) + if uA >= 0 and uA <= 1 and uB >= 0 and uB <= 1: + return ( + Vec2((uA * (ea.x - sa.x)), (uA * (ea.y - sa.y))).length / (ea - sa).length + ) + return 1.0 + + +def line_to_aabb(start: Vec2, end: Vec2, aabb: AABB): + """ + Renvoie la collision entre une ligne et une AABB. + """ + left = line_to_line(start, end, aabb.min, Vec2(aabb.min.x, aabb.max.y)) + right = line_to_line(start, end, Vec2(aabb.max.x, aabb.min.y), aabb.max) + bottom = line_to_line(start, end, aabb.min, Vec2(aabb.max.x, aabb.min.y)) + top = line_to_line(start, end, Vec2(aabb.min.x, aabb.max.y), aabb.max) + t = min([left, right, bottom, top]) + if t == left: + normal = Vec2(-1, 0) + elif t == right: + normal = Vec2(1, 0) + elif t == bottom: + normal = Vec2(0, -1) + elif t == top: + normal = Vec2(0, 1) + else: + normal = Vec2(0, 0) + return t, normal + + +def aabb_to_aabb(moving: AABB, static: AABB, movement: Vec2): + """ + Renvoie la collision entre deux AABB. + """ + size = (moving.max - moving.min) / 2 + static = AABB(static.min - size, static.max + size, static.entity) + start_pos = moving.min + size + return line_to_aabb(start_pos, start_pos + movement, static) + + +def aabb_to_aabbs(moving: AABB, statics: list[AABB], movement: Vec2): + """ + Renvoie la collision entre deux AABB. + """ + t = 1.0 + normal = Vec2(0, 0) + entity = None + for static in statics: + if static.entity == moving.entity: + continue + result = aabb_to_aabb(moving, static, movement) + if result[0] < t: + t = result[0] + normal = result[1] + entity = static.entity + return t, normal, entity + + +def move_entity(entity: Entity, movement: Vec2, disable_callback: bool = False): + world = entity.world + aabb = AABB.from_entity(entity) + others = [ + AABB.from_entity(other) for other in world.query(Solid, Position, Scale, Origin) + ] + counter = 0 + while movement.length > 0.0001 and counter < 50: + t, normal, obstacle = aabb_to_aabbs(aabb, others, movement) + if t == 1.0: + step = movement + else: + step = movement * max(t - 0.000001, 0) + aabb.move(step) + aabb.entity_position(entity) + movement -= step + if normal.x != 0: + movement.x *= -1 + entity[Velocity].x *= -1 + if normal.y != 0: + movement.y *= -1 + entity[Velocity].y *= -1 + movement /= entity[Velocity] + if obstacle is not None and not disable_callback: + if not entity.get( + CollisionHandler, CollisionHandler(lambda e, o: True) + ).callback(entity, obstacle): + break + if not obstacle.get( + CollisionHandler, CollisionHandler(lambda e, o: True) + ).callback(obstacle, entity): + break + movement *= entity[Velocity] + counter += 1 + + +def __apply_velocity(world: World): + """ + Applique la vélocité a toutes les entitées. + """ + delta = world[Delta] + for entity in world.query(Velocity, Position, Scale, Origin): + move_entity(entity, entity[Velocity] * delta) + + +PLUGIN = GlobalPlugin( + [], + [__apply_velocity], + [], + [], +) diff --git a/src/plugins/render.py b/src/plugins/render.py index 9bf659b..299b450 100644 --- a/src/plugins/render.py +++ b/src/plugins/render.py @@ -3,9 +3,10 @@ Un plugin qui s'occupe de rendre des choses dans la fenetre. """ import pygame -from engine import GlobalPlugin, KeepAlive +from engine import GlobalPlugin from engine.ecs import World from engine.math import Vec2 +from plugins import assets WIDTH = 1440 @@ -28,6 +29,52 @@ def calculate_surface_rect() -> tuple[float, float, float, float]: return offset, 0.0, target_width, float(height) +class SpriteBundle: + """ + Un assemblage de composants permettant de faire une sprite. + """ + + def __new__( + cls, + texture: str, + order: float, + position: Vec2 = Vec2(0), + scale: Vec2 = Vec2(128), + origin: Vec2 = Vec2(0), + ): + return ( + Texture(texture), + Order(order), + Position(position), + Scale(scale), + Origin(origin), + ) + + +class TextBundle: + """ + Un assemblage de composants permettant de faire un texte. + """ + + def __new__( + cls, + text: str, + order: float, + size: int = 50, + color: pygame.Color = pygame.Color(255, 255, 255), + position: Vec2 = Vec2(0), + origin: Vec2 = Vec2(0), + ): + return ( + Text(text), + TextSize(size), + TextColor(color), + Position(position), + Order(order), + Origin(origin), + ) + + class Texture(str): """ Composant donnant le nom de la texture d'une entité. @@ -58,37 +105,44 @@ class Origin(Vec2): """ -class Surface(KeepAlive, pygame.Surface): +class Text(str): """ - Ressource qui stocke la surface de rendu du jeu. + Composant donnant le texte d'une entité. """ -def __initialize(world: World): +class TextSize(int): """ - Prépare le monde pour la gestion du rendu. + Composant donnant la taille du texte d'une entité. """ - world.set(Surface((WIDTH, HEIGHT))) -def __render(world: World, cache: dict[str, pygame.Surface] = {}): +class TextColor(pygame.Color): + """ + Composant donnant la couleur du texte d'une entité. + """ + + +def __render(world: World, surface: pygame.Surface = pygame.Surface((WIDTH, HEIGHT))): """ Rend le monde du jeu sur la surface puis l'affiche sur la fenetre. """ # On rend le monde sur la surface - surface: Surface = world[Surface] - entities = sorted(world.query(Texture), key=lambda entity: entity.get(Order, -1)) + entities = world.query(Texture, Position, Order, Scale, Origin) + entities.update(world.query(Text, Position, Order, Origin, TextSize, TextColor)) + entities = sorted(entities, key=lambda entity: entity[Order]) for entity in entities: - texture_name = entity[Texture] - texture = cache.get(texture_name) - if texture is None: - texture = pygame.image.load(f"assets/textures/{texture_name}") - cache[texture_name] = texture - scale = entity.get(Scale, Scale(128)) - texture = pygame.transform.scale(texture, (scale.x, scale.y)) - position = ( - entity.get(Position, Position(0)) - (entity.get(Origin, Origin(0))) * scale - ) + if Text in entity: + texture = assets.load_text( + entity[Text], entity[TextSize], entity[TextColor] + ) + scale = Scale(texture.get_width(), texture.get_height()) + else: + texture = entity[Texture] + texture = assets.load_texture(texture) + scale = entity[Scale] + texture = pygame.transform.scale(texture, (scale.x, scale.y)) + position = entity[Position] - entity[Origin] * scale surface.blit(texture, (position.x, position.y)) # On affiche la surface sur la fenetre @@ -103,7 +157,7 @@ def __render(world: World, cache: dict[str, pygame.Surface] = {}): PLUGIN = GlobalPlugin( - [__initialize], + [], [], [__render], [], diff --git a/src/plugins/sound.py b/src/plugins/sound.py index 5b358f3..f095244 100644 --- a/src/plugins/sound.py +++ b/src/plugins/sound.py @@ -5,43 +5,38 @@ Un plugin permettant de jouer des sons. from typing import Callable import pygame -from engine import GlobalPlugin, KeepAlive +from engine import GlobalPlugin from engine.ecs import Entity, World +from plugins import assets -class Channels(KeepAlive, dict[Entity, pygame.mixer.Channel]): - """ - Ressource qui stoque les sons actuellement joués dans le jeu. - """ - - -class Sound: +class Sound(str): """ Composant permettant de jouer un son. """ - def __init__( - self, - sound: str, - loop: bool = False, - volume: float = 1.0, - fade_ms: int = 0, - callback: Callable[[World, Entity], object] = lambda _w, _e: None, - ): - self.sound = sound - self.loop = loop - self.volume = volume - self.fade_ms = fade_ms + +class Volume(float): + """ + Composant donnant le volume d'un son. + """ + + +class Loop: + """ + Composant indiquant si le son joué par l'entité doit se relancer en boucle. + """ + + +class SoundCallback: + """ + Composant donnant une fonction qui sera appelée à la fin du son. + """ + + def __init__(self, callback: Callable[[World, Entity], object]): self.callback = callback -def __initialize(world: World): - """ - Ajoute les ressources utiles pour le plugin. - """ - world.set(Channels()) - - def __update_sounds( world: World, channels: dict[Entity, pygame.mixer.Channel] = {}, @@ -51,23 +46,23 @@ def __update_sounds( Met à jour les sons du jeu. """ # Ajout des sons non gérés - channels = world[Channels] sound_entities = world.query(Sound) for entity in sound_entities: if entity not in channels: - sound = entity[Sound] - channel = sound.sound.play(sound.loop, fade_ms=sound.fade_ms) + sound_name = entity[Sound] + sound = assets.load_sound(sound_name) + channel = sound.play(Loop in entity) if channel is not None: # type: ignore - channel.set_volume(sound.volume) + channel.set_volume(entity.get(Volume, 1.0)) channels[entity] = channel # On supprime les sons qui sont arrêtés ou qui n'ont plus d'entité channels_to_remove: list[Entity] = [] for entity, channel in channels.items(): if not channel.get_busy() and Sound in entity: - callback = entity[Sound].callback + callback = entity.get(SoundCallback, SoundCallback(lambda w, e: None)) del entity[Sound] - callback(world, entity) + callback.callback(world, entity) channels_to_remove.append(entity) elif entity not in sound_entities: channel.stop() @@ -77,7 +72,7 @@ def __update_sounds( PLUGIN = GlobalPlugin( - [__initialize], + [], [], [__update_sounds], [],