From 34177bf951c09662224108c031097a326f149d9b Mon Sep 17 00:00:00 2001 From: luisgulo Date: Fri, 9 Oct 2026 18:58:05 +0200 Subject: [PATCH] Ajuste velocidad alas libelula --- esquema-mapa.png | Bin 0 -> 47232 bytes libelula.py | 24 +- previos/dragonfly_game_v3.0.py | 555 +++++++++++++++ previos/dragonfly_game_v4.1.py | 702 +++++++++++++++++++ previos/dragonfly_game_v4.3.py | 675 ++++++++++++++++++ previos/dragonfly_game_v5.0.py | 668 ++++++++++++++++++ previos/dragonfly_game_v5.3.py | 649 +++++++++++++++++ previos/dragonfly_game_v5.4.py | 676 ++++++++++++++++++ previos/dragonfly_game_v5.5.py | 694 ++++++++++++++++++ previos/dragonfly_game_v6.1.py | 696 ++++++++++++++++++ previos/dragonfly_game_v6.2.py | 736 +++++++++++++++++++ previos/dragonfly_game_v6.3.py | 797 +++++++++++++++++++++ previos/dragonfly_game_v6.4.py | 828 ++++++++++++++++++++++ previos/dragonfly_game_v6.5.py | 834 ++++++++++++++++++++++ previos/dragonfly_game_v7.0.py | 830 ++++++++++++++++++++++ previos/dragonfly_game_v7.2.py | 827 ++++++++++++++++++++++ previos/dragonfly_game_v8.0.py | 827 ++++++++++++++++++++++ previos/libelula_v8.2.py | 968 +++++++++++++++++++++++++ previos/libelula_v8.3.py | 982 ++++++++++++++++++++++++++ previos/libelula_v8.4.py | 972 ++++++++++++++++++++++++++ previos/libelula_v9.0.py | 1139 ++++++++++++++++++++++++++++++ previos/libelula_v9.2.py | 1199 +++++++++++++++++++++++++++++++ previos/libelula_v9.3.py | 1199 +++++++++++++++++++++++++++++++ previos/libelula_v9.4.py | 1204 ++++++++++++++++++++++++++++++++ previos/libelula_v9.5.py | 1204 ++++++++++++++++++++++++++++++++ radar-ayuda-captura.png | Bin 0 -> 13380 bytes 26 files changed, 19876 insertions(+), 9 deletions(-) create mode 100644 esquema-mapa.png create mode 100644 previos/dragonfly_game_v3.0.py create mode 100644 previos/dragonfly_game_v4.1.py create mode 100644 previos/dragonfly_game_v4.3.py create mode 100644 previos/dragonfly_game_v5.0.py create mode 100644 previos/dragonfly_game_v5.3.py create mode 100644 previos/dragonfly_game_v5.4.py create mode 100644 previos/dragonfly_game_v5.5.py create mode 100644 previos/dragonfly_game_v6.1.py create mode 100644 previos/dragonfly_game_v6.2.py create mode 100644 previos/dragonfly_game_v6.3.py create mode 100644 previos/dragonfly_game_v6.4.py create mode 100644 previos/dragonfly_game_v6.5.py create mode 100644 previos/dragonfly_game_v7.0.py create mode 100644 previos/dragonfly_game_v7.2.py create mode 100644 previos/dragonfly_game_v8.0.py create mode 100644 previos/libelula_v8.2.py create mode 100644 previos/libelula_v8.3.py create mode 100644 previos/libelula_v8.4.py create mode 100644 previos/libelula_v9.0.py create mode 100644 previos/libelula_v9.2.py create mode 100644 previos/libelula_v9.3.py create mode 100644 previos/libelula_v9.4.py create mode 100644 previos/libelula_v9.5.py create mode 100644 radar-ayuda-captura.png diff --git a/esquema-mapa.png b/esquema-mapa.png new file mode 100644 index 0000000000000000000000000000000000000000..a220ef33598c51b1ec5ab47e5676fb752dc0e55f GIT binary patch literal 47232 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

hV@vqE2od0`)tr3eRJP`8QTqk3)*-)9N zMiiYiJ`DLe>RE3XLFU1svB}2rt13TiI+Fg5@o2FNF1GhZRRw0bp2nCm4@_EcnCd~|P@wTJQ0L(`h8*!qd|ws7WY4F?C#?p~D$PNp z?XK-_)@c6E~3xi^r;I z=w+O4c*;FRYxe5Ua9ZjNx>I_>gSq~=)k?Zdl_`9m%E$34R!vTB(PGOn+(K*-sI^UkgEUv& z-`A;2(T$_C5tXzapz5l8S(To=2fiRh@}CzbaUekKlo#&?sq9T3vUjVi9FZT`4*f;? zDIr>V?c^q&cB$6*!%Bx2Btd}~dxuf!1iinpQSYn1bp_UM?K?lMAa1(&di#G|@$OzO zkyZ-(O9KsR5FWiQk`dr)xJQ3OD1U=FsiR^bbPfyG6D%^7`5?YNYjC-Il+Iy}p<7@y z@+b8~!uU3Z``@fjvRUZ=xTbBCNLAK13xBEDcPyW%>!e@eZ*rbPT>g~4 z={FfaN{kU489?NXekjviv=apC=(A=Czr3@&;ZA8c~{wBHR62VV-E1F&nui%``l{A5gxqLF+*PhCN! z%K5&B%v)dn-4pB>wsf2SoyL(+hD>jgU@BX~h*Si^Y-j&W-K~=;#uo{U+Qu5veGh$F z5#A035_^pQc9uyPH(rVa#+7FuOgZ^f47}C-UR9wbmn+vH^l29l`l%JuhjzM1&WXj_ z#)YP2z_p_`9dRq`1UP=I%wnsb7pQ{4{9=fz?zILio{pj52 zW=2!wm!ho%>Fr6&CZy#=^%HLf zBPk+E`~iZ{TO!&8~Jxw_X|k5W?0ez5R{<*)~F7vF^v+uIxz;0OS7%biF%UP>y$%k0DwQ z-sOYM6WhwLy)VY|^2gA&1RwHvOwc73fh|*|f)+I^`TEY~Hn6l-%YLKSDR1Gg%}~d^ z!(^cMWLJem3q4x3^*YmV#%uJcIl=KoI;k*FcK{3OYC?_;s9Py@&ZRuY$tG@vQ(KdY z`?7QRljHziPJ%d$5>m7Oc@VEEQ9nxsQpf`cNCBhzNnj`W*TZBls#0{l<+u$31fc@|cs%kQO}5JHIF9(^HC!T( zElXsvD0D}GaA)V%n&C3O3EcVD9{sb^g(A*_9o>sL-rYU-yg`}`;&gOwUwM1trOAqx zW$me|gbMN}IW1orUq*m#>Ulk7Q^XO)|S|qLD~+wMkYjw=7c2JD6G~zFrA@ z%g0F!eovqOvjmIiNc|tn9En3h)pok5Otd%;0L+ zEX(rprOG_i3#rgu;Y`J-rB5j`KVh72?G*5tVYY`$cK=0rJ*iG8a~m@6rYkJ%NG#u z3|JR8Mt;ylhtTeVt1IH94|;hme;*&lai$~aqd8@_DHU(Fz>YRoTD@suLEq|B)9KCQ zYP`v5`T9Ie%KiVnAt1y9D<-5pG7w3mj+ReKZP&XZUymd#=Gkvrt1@9d)l=dJ8gt@u zU0xSfhmWTdm6A*Dl-#p=N+%<(FA>ykHP-do8nv_?4QcB5Lc6+Fe#ntIw3nsZmGx`1 zfq=mw62uNC-T4siR1o^98%bg>f!&gBxC;JPj+mhNve{rUcWJN(SR?)n43m7Fc5z$v zB5Q^1Aah|!JPvSeoAVj(cVL#0N-zLwO{1#aw?4jOV`@NjDr5t7H->1~`I$w^tu+?7 ze-Fyyszo5eeG)Cbci^}oVKTN&(O zzSTC|whw=nz%m3l(Hw&e^8@Q|PH5%#+Rep6)HJ#i=9HF&kK`ytRrCSKzFBS&6MC%H z!E(*wBZN!c=rLIp;;*TF&<-F6*=s#Fj1|9|AUwVinPW#|Ki0bDv0tYal?infN`nN8 z1uT2_cM){^Sg0TZnaY?F_mVfM&%j*}iUzTG;wKG_MqNX9k-O2NJF|meD{W|q_72M2 z{&T}WnYW_GI5J(wp-Et9mB2V6Zn8|Zx5{mRnT5pe9eYRh%gwG_U2Fa`N1^vuWeO(J zWe||{1DdiQfQq$GC9Zh8B(I~v%N)D-4*@^jgBn?tL!_4CRR5(4t{w>&G$r`Ukj9 z)l}=#05WMe(AnU6oKj|p2Jl~4>UCl~wK+Sxj|+GGwA-B>T7P`a0|~9lwYwvC66jMv znvsh^+5;U45AXYLk>}G^3<0J&geEw8fV8$^>*y`jp*Q4c(HLdbY_!9ew1z;K>-sE@ z_MtnjkO%)%n1&MMn5N>v9c^>Y;}j<{PDa)L45Ji>V!rmCAc6PZ#IHHW`u=z%+St2k-eXa6`}Vt*OL)7dG^*->#yNEh~=ak|K`dbt4d z_Z)A$pN(OrgqYX>@t%E(A0x2gm2Rbw{+k1^7uK1M$ZEZwa~#mRX5c(7P@2{nYu(eA z(=f$nES&uvodBEGBqAKvG7?0V9If80Sd8<3T7b*&!0v`IoeW#@AiOaLnomT=E!+Yc zWY%+nZt4#2AdXoy%Xp6erPl3qo++r5LS%sIoUp{|WG=G<#Ty+25`(Iny;#pX@G<#$ zW#?rSjrmRF=x6c2U!N;Gmcq{SBm`nOT5$)(ihEjbJUQ$<8q6T`{7uz9m~J0&{3HOQ z=2MVcsXzM;2oa2$_z|6Fo zH5>ox?*CF`@Y+o6hG00@uJ8Fk%T_(RU{Gl)v3NUDHdZ3h2#7b(M4{wCZ!bxT84%Q> zah&F;=Ts$|5{0X;Y@8T;O(S+qNXKJ`d4zfr)xYyQ_^bP8vARn-RvRn#BwWqFg+JAo zQ`&>s_7x5UM$4H1)eH}q&e+tlb}Mhujms>{9l_><$Q|O~bW_(-8p*l5s_~Uan!p zhQLX14>{4B47MbKSdV3Kw_q(bPeZ0sgq2X4x!vCAJ0Dz*_XDsWb6C0Tv?)#c_hQC` zEV35lRi=gjdUt1k!w$)SXA;k{GoC72o+klJ17|Jqpoz{}*ARa(V`Erl(fk4cwmeH2*A|neFn>7(K!wn&nVG%1^|{CJsVWuvAviIcvrM(Q-O`$e3hyHH zS8e^O#uqABmkBm{6`fL_ynRPJHA2ztte@@yv3NpShxE3vOQccs!P0>A0YMD1F~@Y- zL4@sjf;i6`D?lad*iZHr+UnSg_UA|=in#K;;NnF^vynNfj4tZ%_z1#mz*d}F4ZUzobIR|I z2LiUX2lI~238$MBpf@AcE0{i~x^_-VU;IEE@CebiejANMNQa;NBpxGI@C6MBG??jo zsy*h77?CBFJl_x!#sY+bzu&Vz2SC!}&IDqQk>#qMP{K2FxN^fO0};b`DAChz5a-s= z?~(Tda${GM-qhdEFK^vXw2w6ubap?V#ZPQK{$EpH9Tw&D#k&HEgwkD7f^?U}mo5S6 zW<{ihC55FyI+ZSorCYj7LIGiE1Qr%p8tLx);_u$)x&Oa2@64GqXU_SYVt)ze{a~Y_ z$y_Ds$jm^>#q|)YM&blmw}VbF35VkNDbO{$OKqU#1Y;DfQOX4nx^BaCmP7ekYhlG8 z&j5;U7|-+5?!h7f^BRJ9sINLqLn6EJ)Y*hx_-Gj(q%aq7X)G?4bdKT)WWPo~Y0LXH zwt#iV4Y66kT680|aYCWs<_qjVKmZUI5#zE9u0DEFlYbJrHB6EK*bNy1k4IAEHxU0( z#^C{kEGN$%wM^LEVU;8PmP<93B)_s1-X%^*_5?>=W!X-0K&&?#Qsu3i>Se|a?j5B1 z6+aI66FGT{8Y^^oFS@ZF<2?r^K`@#hh-L^-5&2Zcf0=z5ad}v0Dj-4l+e+H_S+tZC zl&!2kwtsv-qNv3)x1dF$+)O` zw|j%_euHeD0aMHEXb#+Y_&nK7WM!))V_(A6J?@n+9;m~4j|H07SV{K|e*52;c#ndO zmEU-!(fLCTp_6CM&a(|KTec=X%I+k<4&|vy4mOS3L+XYrS+#$L1MfHxI3KA+#`3{{ zYic6z&kuQ;JMkqJ!~Jj^Hrb+AS%+!#1Kw})@v6tg-jtfJ^BXPBk+e6}JOwoN?H@pZ zXo2y-9;s z95dDZyvG!r5W)6Yr_JQl+S=wIQN4dlV?}#qLkMDGu#zj8H1^dVjw`Figz_43tONoCyRP#OVUkik6B4Ac*RxK5$Opk)Fxw{k`F!S|9u&brC_`s55Zrb2ZK*r2Y`d zD#(m0N%xmy`t^m+HlAdft*=4E;?JX!*H|wey#@_2T7Yn6V+m&5elNESaeWDB%F+j3 z<$rF{O#lY?aEuZWYN&V+@FPXS8f>5@7=V*0OsBi`(qw4z@hTD`yfCvZ-62eeLg|`z zG)4P6kDN{I5U8K%Jk%S>@V1hK8Oys3=#kLZmh2R<0|EMHIV~N<_j=S589)L|(_UQx zJ9=LA$MNW2rxjvQAVHFoX4dGGxx?_4DGytf{$SPPS#xzC(`CuN15VNc-W&C(%PQw< zZi7w=IKem%imH-Z3?cEEndxD&4py|1RB?cKFToojonx!K%4uLVV*FMUkvtOc)+Q7i zB>u9B7s;~)8J8vo1=IljBkq#Fx$j7M!!}0mi?NCIa;1^=;1r|1$$GVl~wxscw%O0ztibLsm>ruu{q8y^A;ngg+7h)zk>A@(je%ZK&d zsxvRZ%v(vIkt_c1q*xeaf{iij9r@C@wrLMgXJ7*s)sxPl-`abgqQUtfP17Q=zeANF z9jTeA>L%IYfV_Vywp3Blp*ja1YH|1WMi1>x%}GE@dkY=ee}rw&>99j{1Ar!cG}+Nb zi$hVnQ!%lERunVYx8m4NjK#x^Ka_zK8O%1zDUUS3&0|oNbdE>BS~|k5yeCU=>>T>B z+s@zaZ!w`L6A*2=kSeN^=~u1Q67ac}x4$uGAa`X2=+K;lzF4!}>H{>t#f=ybT$$iT z_mOe(qA~)NX$JqxyLA2UWz+nrIGEN4g8-d_><`#_Tm8)A%qOAzKgka@o$x2anaCPT zjEm`v&ZLFQvYEMJae5v1)~-u_lH3MgZor$7 z0}>8z-?dDpkI_d0Q0BaMOVeX}e{LYdJoLb^it>h=;W$T8!yyeN<#ZSUxPJ~$u5A@c z7di|J{mOg;O(1z2A2x-IdC9;6xMBTg?SSIfCfVO;WLotVuq6Nzv+klrrnzgH&GIVl zz4w0vOUETQ{GMw~8H*+VMZf;3{ z6jf4yu1Di+tnJ07N4RaW4J80n@hn@2^&in!&w%W^>W150*{=z!wCXnR|B%Z@nd?g< ztC~jY{-3LUDysuKZ{gfOsqnG7jp4}~c-pi|m0h2{ z|M}{SP|e{tuD;Tt4?Rn}JF>htGV~9^mg664Wh3@huC;p`$NVa(rSYn0XT+fYRB*D5 zr$SlljCD*_Rh$}M<_)q8qqn4iXwmf^j3!TnJZP(_G^K93=v_c94<*HC>u$=eaG zv>B-2k$v*4g#4(!I#{DPwh;RtV=|u0FjC!eF?XHE`_=oL#uP0Yx8A>SGPQp{a&?4T zHeQD@>BXdp*|ERVVGOewIafnYyt!3t_i<)~UrfV{gB@z@>-zMBSQ7x8>-vNVG(%|~uF_{#nr9_0*LhNL3dT@%Dq*L*KLFDkZX;c@6b=u1={&7wf~Sj2mlp(1i28SS!DDDg1C z`)jj*0mKhToZ8%&k}%0|VyKG>xK&6zgdK_x5Y>z}>=Wsu0Q|w@Eh-f6P{E9<@1p=R zzA2$b&^zOvGL23xSXg{ownq0yh!Rmz~yVEmIRocz& z&^N@~=fToE(hkSj~+sOCI$;!$vN;`>l;9N#pRxnj0>PHabw!u=lsDT_?Acj!zPS zD$bxqTw!}PWW|4)9+6t+%mM9@`$^8K<=}~N)Y16}&06Ckd?^?NLc72Y*&jBV@Lm{4WF~Rn zPrMhov*B5@qg=^?|8)n;UA0nxIkwmeYaS2eGl>Y{SO&1cWlE1s=RF;tLJ!#6?beCv$EUhneF_+daSYn(n zjoj3y#3oStz2mBjWsL>G@0wY^q$W_72{^NE8riH6m{ifWGx$;2olfVpZ}K0WH83YK zhs(j=$|1oDUz2=ABWyT!F@GB=spob3hL!~rH2pVuFPj2Le@gg7Rxc%W)%yZ&Ux7m= zAGojYL7$*b0eN=_2!Xr)NQj{~Dy!)8HN7~E5 z`>EunUc!Um{P0OqTQXz6L?)`#1McK*HFiO?}q9xkHp zcrS$gF*AXEM9E!ZZe!FyvfTz~Zi{hHNDu;I$@NxU3@zym`MYN8rRI#|DjueQ%MVl; zL-XueBQh(YKQA)TJ###5eOI8CL5|Y()hAnXEc?n{*HBd2|91+A$SZQbHnAC>Z&*s;)4CMq^w>9tP z8~0Y|cF{A{?JVi8I0<(*o7QPIgPvcxr*Pu)meW(v51J@zR(HN_J;_Jx8 zP$wjRgjGhHA6TsBoul2=%UZ>{UYcFYG6#)4(Tl%V9+EXXWqKBE4(KRP3ssViL+wRC z5=d5qM)@6^`^a0WZ(=5dzrB7rc)Pm>sc>u4=him4R_CTvQx)OS6BSKUxIzOAEi+bs z9NSj*K_oyZ0WFUr*D$T-yo#6a8C=S7zOnf?7J6xSzI_uyKQCKr&+(Pi@P~V2I;{uYOg_dIWbS(*` zAWbgdr1tRPf)i~>(pH*GXLZ)AbPATxT%wDZvrU@W(*O@$!f z5^;?#?hKv}#*4Ky+-CZm-km{&1-y*6GDc|Zvu|cyqi76Kk3hh*rWUBVG$|%uOLt@Y zJPT8sX${Xaj!K|M0~-1C-fG!-)L_mFD-8Ns$(~v!vLp0+1r5J9T2JODglRvtJtjkc z@~}5OxAk7e$Oi5B>&Lu3$5;&5vSFyC-Rrxk7i-Y&CD9mi`)jOzXP0XZx{4FutHTs$ z$$uN7k8aa-Zc58@XVLJN(D(eKw7?i{Tgj=N$NC|f<m|wwTkMBNiXNJQ65HUPlio(5jnT*k z{Cit!T)G@ya&6CNj%FCj zN(d&f=Bpu0feRd3<^|BD^K5rsZ>^G*OsGs)ugu$(7{jXKW;@}2S)=93XD~(2zP}|H zG+K6m+hlQf5Cu<<(bhxh*hiJ_>eYRt@-+c!X`OUiwZV43tlrUzWP9S`f}Xx>3dB8Y zSJ^Q(;}Nj9Pa}%^xG8N~M1D1##6H-ICr-F$v>O-bZ2#zi(J8`e&EIjR#R( zQ}FAZC!hBRS^hsCGaa+BGxTo{@NyF!>6z$TD(4qc z;T*zsh*3NShSK!AS^15=sPzm%VR-F??)mVgdMu?3*aleJ`EzC5l}d@i0>@GMkH;o} z_@LR5F@Az~f_JY_d2sN=(g}f06MA+ezecP~iwM`Vna9x>HZEGPNGp8}J}zY6gC~~< zz*^(S+aI5HJMc?4;xti?`$`^3Oo$wo3i# zd(#n?g}#>PIQ9qOnXwlB+0eemCf^O5L=!7dBPEyFouwTu1N7eD$hN1aSALu|V>KeN zaEQyLevhZjRTtZLn9jP+a5VF!Tbf+=cg`{fO1{hyGbETC^T22LwJ1bwQar+fEZt0H z^}KLx{(5S;E^!Pa)Y<1}Zs!((uGB5ztbVLmCfb%Vno~P$aK1vqp%=&ps;GXU0^NnRqwU zT%2cVe{>)y=y1CW)Y%KX8$)jYWn4De^0c&_s&)|;U{78PwVd$x==QVh7E9D0Fa5%g z!H{0jM5KClpTQ4R2%8vppn!6)7YPQU45qNxOC78C47tSE&_Rd*<7%w*@U{R;w<=ql zR|~2XF)^=4t2NqAh2gDjIc5EWP^&jzk_eP3@q^`@*Wu%rm|`yFCd3YxcSeGVV`C688egGl1P8@8~={I z;iNZlWf1b{|=;RbxnjU-$cygcfEClS%=ZGaW&A{ zK3%GFSg9=NrE>#WmT`?nN~ZI~M#K@J$BVK}hLow`;2%XgSsOfn+6y9a64vSB%cpti1-uaoAV@i8T-$l)-Iaia%+idGuSVNfUbA>i68EA~Bx%T*n77q%mVfAmpgfI5 zrjlHA0xPzN+4W`i^@iSZ>)Ufu%0*SO^w3`tJo#k|8zEyp!|iWBWpf+($%!e3T~Efv zZHDv?Q`-cC0I|x0)?^{+U4GcbvgosXL7O$xcV;dV&8wa`%!;r;`;2J z<3?$z;abfawzsXFC)NXLk;gaJr#vut95eCP_I8`jmUr!D&fH?%-v`M)NOB+J?JE7< z)z{h{2BYQHdp>z!QCf4cm4=S>>*I*TJ*v{?%lm+^z3khe`(sJe2c$7WB)KznXPYDj zs+SczTz=rMuRT`~8JAxQEN5zPcc8Yzg?_Nj8OG}h&Q907C?#sscivd&!p+kRq0=H5JP!8(yZDZ|ggE#P_x~ZlHdNA`;;{z_14VOEb$71m*_xHBej@G5*}0y-?!t5 z9Z6n5k^IE!$^5mis#N=FDP)XAF`CAZ`SLJ02m~T?SC*63DLJtB+_Y~t?|IHvi2VN9 zv-(GDo$0X;>k(qs(1OQ;qzwCiiusnglBQgAxZ_!Bx^-42bGq+Iga?$qr1&*C3jqO& zNf(*0Y5ZI*s(#^)*mGuKG9Q;|yitcccFZPV`Ev6qB+(Ni1LyRW2zlPpzyK*w1O zm2JDTvL1Toz+Klqio-C3M!qN45QMTKk&=It96sswqxY{0RVQNxjUr-;2>DzYVg?S|e)X zY_|lQt2aGaCv%27x6D$A5-g9k$=qNA0pAFVu+TbB-fs3imZyvpSoV5v>w`jR5;N{h z7TASu5{{?)T7;1&8eF13RnLlRZq74*RR;#=yBF`*afmw0x(z3e;y(iw2Y9bk;c*G* z_*a)wmU0z+%edQrMV769dRy9&;p}39VSRNcPYm5j%VYe)Ee4zTa@UIQ*Gp*j)K+Y$ z_O(_GmkYs|NzVjELnm0RwCxvvt%LMqI9~e;z6G92#~t;zHTri7%{EsFj!q)&nIRq@ z)~us*J2N$D&Dbz%k znWc`DG|EU-+lx-E6l#>Aq=C&pSmiR5)aQ+#{T?cZ82G_#l_nPF!mL z<%N^MIz=9iyT-a9`+NOedUeF>nj`bHl*gpjoCFRsD`zPaLJs zi8B2hTnX!so1CsxrlgRFU_3D@l5~v&)TI`98qf#>E`@t$l=P?_p>^ArmdetUD(Ffb z^Zg_tvRD=TUNf@zm1y>g(&6kY-Zlx#Tg9!FjeQ#aGtx;1Q78}R8WAk_T_Er}>U8}l zCl@O?b;s#%8(+>&c4)%5N+}7(Z7OBH6M=QDnQ4DUR$oZzedeA$x=+r#cHh^xyk~?7 zuL`gWJ3Bu#%mbarhKD4dw7^LmmgVUYL*W`;amj0NB26XTMP%;vz1ErIMh_DXG`2^? z2n5J)xA4_{tG1WHo!w!gyyVZD>?A!USJOG-GuDV|I`p5eD088k@zDCdKS*P-b{>B) z1iy2c6ss<)kI@x`TlZEg5!&XT+F2|MuoSZH?F@TL4^ox1Mr8Fs(Sfz}w0sX|SutcX zJ=C3f6)OE1m7>r0B|j)Xb}9c62(dbcRg|+ayDmB}rlDEqGXSlxUk8GVUIZ+oV;rKR zVqw3)$CYQ)K0;65z}BEy6sX7aG5oj>M$iM zU|XjQJiCj8?XGgg+{VDVJW{4>dqyFZCieY@nP-2r6`iyGnbRxPMX{xGj-1Xp^?k1E zG~Qkk*SJAmaupFmY2}WpR&g&r|NHFk7|WA{Sw@9z3-&(ycQb=k0;P~j>&6cK*dIxc z;~u|C8KKdx`z8W(6ngsZ1;Zfl=PVJ{LG%jF(;&$i( zd`WqOb2ufjI1GDX`8 zmCCWdy^wlE=V4lSAxlS+(gNqJ9*3>#P|K_bL%y}(#$isYnvI6MEHks0nBf76@@t_k zt&sw2GSM$J58KSQSxxPEOTLG2J*~QLTQ|In?!~tS3a2apxf~5+@z=N?DAzPOvO(? z<;DQAiJ5XQ97eDKTpEBMOf_gZqWz>N&avNzN{V1vB5D>mW8iYDURox1Dw}?f0T?1{ z>`JPY<5LZ$9=ivo0;aAn9?K?mk;(2bO}W=AKSV|c%Qye;f-!4rTi0P=pN6PP%^nj; zcgUM}OMDLt`H^d?svU*Jz$ei9d2HU!dL2k)S#`87Thx0XM_GZVaA6f{pt2L8qFJtT z(ezK;>SU-?v+YuqsKVyI(wNm+xc+vHw7T7WuGdbN)e#7GFSnh~+-x~vcI$kARyC44 z_th=kUQJqh-0s4imot=N>OF)U$wJi04H^WuT2(7Hca^482uaw@$LD!&s2_Q5E)1jfV`f0U+7DDl--U1Xh%T{Vem(u`QI-y zuqry%O(hP&{_Nkp|HGe%=j+88(5(v1rMXfogZ8^y4fi)WWbOPar!0f%YgUfdwwT-m zT4tF7TExtnFZ&u zySKiXB6e;3M#G0)-}54pb8+FK$s@^2JKIcP^a21lmKMhr&+Pz~OoI4wy*`M#m7QmD zUp?9^!29%Z&vUO~{M_!g!s~@EFW@l8?~*XQt1R1}+P`hAI*UUTY%WndqSWlPC5;Qy~qOjy~!(4_1<8IaRrTm~LEobHSaGhi`%7xpDDKQy{ z$yI|cqida9}_Huqz%oUj!+_pFlZ~fj(Qx=J9?v55T``_LCfTZWT_4#}?wi zhoI&J=YyhLKFl;tVo2TK-E`r$F55C!rglgi^m^!Cb%=hI2Y^(xrXt`@#XA=J z&3(_Og7`?@#niqgK5I>bcGGkwc5o38Bq%WvhWYMbvU8g z)Zf>b_ZZh`tnTG^6b*>OC-GtFlpQQ`Ea9+#1tD(nvn(IC^nKYY2H!^@#|V&QoxQCO z8>5q^mD(CN!LIftFSjcbVef+VOO0FA;XYYOe$>kxrttHP?xt-ph%tWvqqI8rz<)@7|2ne6iBl#a{Mb#KbBat;T%C6CS+`(SV4*`4~R+e~|rnu;7pRh%I7YA51E zqFrogs`N*r)Y`)_kT_M%N@=mrki`xb~ z6y_%+9ix=slj7?U_`JWe9CypSAKa=^!n@vCrMhX@FdkFBL_>r69RjoemHRb^zVt7m zuR8;3sfZ7pU-&ZXCAMh8hypfpJS5YEYh`C-d(~zv1)92tz{RENY>j)=OLCkb>r z9h~MN>;Tq3SRn@6;7YAeSdg4jm>4H3Qy{O=lI7B4CI*1ycD+X%M2kQT{wA5>L{Bct zkr_fS*IV|x{xR>Ok=?gVaoggI3=FTM{d7*j6=8~g{^!6$E~w@4YEE5#f&SI!^Z4ui z3Mu`(c*QxSP3N7dxiPU=Akyh?2@j8{_4N6m98>L|p)zqS1}$*M&0kD0nNfh1w0p*WR5t$nPr(NHTs` zz~2%tyBxzI;Z{dZzozqj&r@k5RTTGkrDr~Ia>Qo+@mlvj_PpLFvd417@_yuEa&DJw zKiHdfuYM}*7A>?ELhz3&S!UFxTzV4{w&oS>Vfan7ahs1po8Leku8NoMmE-1xh)PqG z4X~!hr>qpQkv&*@+OO!jU38_=Pk%(x2?iCfD3*l`1yRt{mUMKQ!c3BSXzq{It zx6C@tgw{Hjn%wxM6hcfcxb$NB(u4d)K%kVJJ?pNDCmT`B3w4)(V>aK4ML?DC8-zYOC%i0p?=r47VTWy9Ym~mWr${GLVQK0-DQFU&-5xp*RV7 zh0k32a%G13QWD#hTvB-hPWbI&^?&8h!l;Vdo^tpYT5V)ALEUz9eSu z+^L!Gjn{+-?)<)8>~pQDG7~nRdj~VtaFdtZTjKnmS-WeG)jgM_cEy|)T4VVC)LRQ$ zbTZ)q?jQA0(U*h-ynf8pZIQF&jTOoO{tc)~zM6SWow_C$er}3TtFiw8*?OV`Le`fN zYb=dsyI|H%Ur}X3W}0}@5-4`angT;F?E9$rL;241k#hg zuRqz)y*pvI_Vdt56eTr1(~<};=Sg?&k>~b8K8G7=npY^=GCSkh4{t~RAOq#hpn2Xa zRm>$)jCG9FtW@vrda()vvWH zCjg$VoFX(&{oFL6zq9eua)-b5zNvs2TGZB7xEUhDH~DS%oPv<_$FG3yCiIS9%hnvp zk78NU8cb}9lL~{Y6@ZIL)KyVAlFV+voL9?lT>mj)_Iq!ddA^eXlQZ?0v|90#F{$Yr zcggeJ-T21ww-VX*Vlq91qifip91pan_A-8gXCE@YrO(+~E~f9(GbHdiiaspEtcJYz|)QGv}LZpI|Fg@=18 zpyeqjNjY3_>!Qm+q5!3gr8(J(ylSAy8{wJ|F51d&AQ_cnHai>1?{A+e&U)yZjO!6u?ho z+mZhhuctQD)$S!h9G};<40bjhC_r$2+*Nv?t|Vz&1;3{IBY(!nPaj6dNM?~@=kJVI z!x8rG{@Wf6VCxekHdTRD^UF!Pgxe=`N^br;heY)0AcB zX}Itm!3p4h-@L_64F_O^uuq)ruaiWDGZKT}q0lbm_}he~bCvGg$vVP_5cIO-Uz%RA zY$iu7sF?gDG0i^rS#$|5to6b3l(hCXA+Jf?>oZ<^}ZOT34@$uhbfgIiZ^_|9v7R(CqxxBqix@+l?%v9KmTNjt#$S-h+D^w->ox zzg-Ar?u(fJjxn(&@am&(W3|lOo@yE}ubPpZPHD-$sX#a`0}M(-M-B=wW)6r@A`Z%p zt~Jng6P+!YtKn6y>`O4JtcD)W3&hOfhY2mJ2E_M#0C6Ce>Sbn-Jv~-_ zt&Z^E{*duJtQGJ(Jo0hy*kv0|I?3NFbhjTKdVcm@?_?z9B)xw8yLqvn%qi~FH$wr^ z9R_t`P#5W#psRG_)!iFz8%EzkR3uF{VVN*&=Iw2Fd4Y=LPkA&ly^N#YO!eTi4}g__ zAcp%PqcU4W@Y|-wz-vq;b>#RXALvQ!>TvLSo3l{ZYbxOPh-I;hrL)K#7Q4cCV&j|{ zt(7bUUr~|#Q3C}nC*7NZPR8hfee!z0_tH)8-1L1Rqa&g#zar1K1y^bbb=V42mUY&g zb_)7J@AY6fXM6yZ)Ylp10{LFr`!RZ((->dM8UXMJk0G+L7v*t`1+Rk!CX^O>R&y}^ zZ@}{E2QQ@3?W7xn=>$a-?(pAnF_Dpl(2^!eDeze3Mb`8S*u}ieoRCfmFX3gE+X|q> z0XwrYCq6Agq%qe!0tFZX$(4h+BmDGnH6mSS>MUnAQ0~u-%DBuNdF&}lsWeh}^);h( zBb0h#Z~z0eLlj`}!=P^IUxlPZhvNMfzQe5VmD?NKwa)A+2A%gBrbU5xgRj9k<8Sra zFSSJk$F*5u^*$f$%#f+0sh?p5sPYVY9a+M~s z2SO~Y0x>8AG;{5%mu(xaMTO(@IL~0LB@hK;H}AChZCIOu_NMH^I0?aTuk@Htve5u5 z+rt$XobI*KklTL=mHjGi=ptNct!MuvS`X2nVmz_7f@s$(<8yh4BtXb|a=j_No0MUQ zG#KR3o!!$n-uF=9eRI;}Z@E2el ztcyBJ`T~@Vm|fk?6Esx-nqB~$QrqVDva@|uL<;h8AF>n=6!y|PxPN5STU9wX-4ARP P1X7k)m#dU{7xaGs2k^A# literal 0 HcmV?d00001 diff --git a/libelula.py b/libelula.py index 1bc9614..b5bc3b6 100755 --- a/libelula.py +++ b/libelula.py @@ -409,24 +409,30 @@ class Frog: def draw_water_surface(time_counter): glColor3f(*COLOR_WATER) - grid_size = 15 + # Cuadrícula más densa y fluida para el estanque principal + grid_size = 8 # Reducido de 15 a 8 para mayor resolución en las ondas for x in range(int(-POND_RADIUS), int(POND_RADIUS), grid_size * 2): glBegin(GL_LINE_STRIP) - for y in range(int(-POND_RADIUS), int(-POND_RADIUS) + int(POND_RADIUS)*2, grid_size): + for y in range(int(-POND_RADIUS), int(-POND_RADIUS) + int(POND_RADIUS)*2, grid_size * 2): dist = math.hypot(x, y) if dist < POND_RADIUS - 30: - z = 1.0 * math.sin(dist * 0.015 - time_counter * 3.0) + 0.5 * math.cos(x * 0.01 + time_counter) + # Combinación de ondas complejas para dar un aspecto orgánico y natural + z = (1.2 * math.sin(dist * 0.015 - time_counter * 3.5) + + 0.6 * math.cos(x * 0.02 + time_counter * 2.0) + + 0.4 * math.sin(y * 0.025 - time_counter * 1.8)) glVertex3f(x, y, z) glEnd() - for y in range(int(STREAM_MIN_Y), int(STREAM_MAX_Y), 15): + # Ondas mejoradas para los arroyos + for y in range(int(STREAM_MIN_Y), int(STREAM_MAX_Y), 20): if abs(y) < POND_RADIUS: continue cx = get_river_center(y) glBegin(GL_LINE_STRIP) - for dx in range(-100, 110, 10): + for dx in range(-80, 90, 8): x = cx + dx - z = 0.5 * math.sin(y * 0.03 + dx * 0.08 + time_counter * 4.0) + z = (0.8 * math.sin(y * 0.04 + dx * 0.1 + time_counter * 4.5) + + 0.3 * math.cos(x * 0.05 - time_counter * 2.5)) glVertex3f(x, y, z) glEnd() @@ -676,8 +682,8 @@ def draw_start_screen(font_title, font_subtitle, font_text, wing_angle, menu_win glPopMatrix() # OBJETIVO DEL JUEGO - obj_line1 = font_text.render("Objetivo: Vuela libre por el estanque y sus arroyos capturando moscas", True, (255, 255, 255)) - obj_line2 = font_text.render("y mosquitos. Una vez cazados debes devorarlos posado en un junco.", True, (255, 255, 255)) + obj_line1 = font_text.render("Objetivo: Vuela libre por el estanque y sus arroyos capturando moscas y mosquitos.", True, (255, 255, 255)) + obj_line2 = font_text.render("Una vez cazados debes devorarlos posado en un junco.", True, (255, 255, 255)) draw_text_2d(obj_line1, 70, HEIGHT - 170) draw_text_2d(obj_line2, 70, HEIGHT - 195) @@ -1165,7 +1171,7 @@ def main(): py += WORLD_LEN # Aleteo rápido cuando está volando o maniobrando - wing_timer += 140.0 if (dive_phase != "none" or auto_landing) else (60.0 if speed > 0.1 else 100.0) + wing_timer += 180.0 if (dive_phase != "none" or auto_landing) else (40.0 if speed > 0.1 else 140.0) wing_angle = math.sin(wing_timer) * 35.0 frog.update() diff --git a/previos/dragonfly_game_v3.0.py b/previos/dragonfly_game_v3.0.py new file mode 100644 index 0000000..2b8d994 --- /dev/null +++ b/previos/dragonfly_game_v3.0.py @@ -0,0 +1,555 @@ +import sys +import math +import random +import pygame +from pygame.locals import * +from OpenGL.GL import * +from OpenGL.GLU import * + +# ========================================== +# CONFIGURACIÓN Y CONSTANTES +# ========================================== +WIDTH, HEIGHT = 800, 600 + +# Colores (R, G, B) +COLOR_BG = (0.12, 0.15, 0.12) # Fondo ambiente +COLOR_WATER = (0.0, 0.5, 0.85) # Agua animada (Cian/Azul) +COLOR_LAND = (0.45, 0.32, 0.15) # Tierra / Relieve +COLOR_LAND_RIVER = (0.40, 0.28, 0.12) # Tierra en riachuelos +COLOR_ISLAND = (0.5, 0.38, 0.18) # Islas +COLOR_REED = (0.15, 0.85, 0.25) # Juncos verdes +COLOR_DRAGONFLY = (0.0, 1.0, 1.0) # Libélula alámbrica (Cian) +COLOR_MOSQUITO = (1.0, 0.2, 0.2) # Mosquitos (Rojo) +COLOR_FLY = (1.0, 0.9, 0.0) # Moscas (Amarillo/Dorado) + +# Dimensiones del Mundo +POND_RADIUS = 1800.0 # Estanque de 1800u de superficie +STREAM_LEN = 900.0 # Longitud de cada arroyo +STREAM_MIN_Y = -(POND_RADIUS + STREAM_LEN) # -2700.0 +STREAM_MAX_Y = (POND_RADIUS + STREAM_LEN) # 2700.0 +WORLD_LEN = STREAM_MAX_Y - STREAM_MIN_Y + +# Cantidades de insectos +MOSQUITO_MIN, MOSQUITO_MAX = 10, 20 +FLY_MIN, FLY_MAX = 2, 7 + +# ========================================== +# ELEMENTOS DEL MAPA +# ========================================== +REEDS = [ + # Juncos en islas del estanque principal + (-250, 350, 0, 20), (-160, -500, 0, 22), (500, 200, 0, 24), (-700, 200, 0, 21), (200, -800, 0, 23), + (-900, -300, 0, 25), (760, -600, 0, 22), (900, 500, 0, 26), (-500, 900, 0, 23), (600, -1000, 0, 22), + (-1200, 400, 0, 24), (1300, -300, 0, 20), (-1400, -700, 0, 25), (1100, 1200, 0, 23), (-360, -1300, 0, 22), + # Juncos en el arroyo inferior + (-90, -1950, 0, 20), (-110, -2150, 0, 23), (-80, -2350, 0, 19), (-100, -2550, 0, 22), + # Juncos en el arroyo superior + (90, 1950, 0, 22), (110, 2150, 0, 20), (85, 2350, 0, 24), (105, 2550, 0, 21) +] + +ISLANDS = [ + (-250, 350, 90), (-160, -500, 85), (500, 200, 100), (-700, 200, 110), (200, -800, 95), + (-900, -300, 120), (760, -600, 90), (900, 500, 105), (-500, 900, 115), (600, -1000, 100) +] + +def get_river_center(y): + """Devuelve el centro X del arroyo fuera de los límites del estanque central.""" + if y < -POND_RADIUS: + dy = y + POND_RADIUS + return -50 + 100 * math.sin(dy * 0.008) + elif y > POND_RADIUS: + dy = y - POND_RADIUS + return 50 + 100 * math.sin(dy * 0.008) + else: + return (y / POND_RADIUS) * 50.0 + +def is_over_water(x, y): + """Comprueba si unas coordenadas (x, y) están dentro del agua.""" + dist_center = math.hypot(x, y) + if dist_center < POND_RADIUS - 50: + return True + if abs(y) >= POND_RADIUS: + cx = get_river_center(y) + if abs(x - cx) < 80: + return True + return False + +def get_random_water_position(): + """Genera una posición inicial aleatoria sobre el agua.""" + while True: + if random.random() < 0.7: + rad = random.uniform(0, math.pi * 2) + r = random.uniform(100, POND_RADIUS - 150) + x = r * math.cos(rad) + y = r * math.sin(rad) + else: + y = random.choice([random.uniform(POND_RADIUS, STREAM_MAX_Y), random.uniform(STREAM_MIN_Y, -POND_RADIUS)]) + cx = get_river_center(y) + x = cx + random.uniform(-60, 60) + + if is_over_water(x, y): + z = random.uniform(5.0, 35.0) + return x, y, z + +# ========================================== +# CLASES DE INSECTOS +# ========================================== +class Insect: + def __init__(self, insect_type): + self.type = insect_type + self.x, self.y, self.z = get_random_water_position() + self.yaw = random.uniform(0, 360) + self.timer = random.uniform(0, 100) + + if self.type == "mosquito": + self.speed = 1.8 + self.size = 1.2 + else: # Fly + self.speed = 0.9 + self.size = 1.6 + + def update(self, player_pos): + self.timer += 0.05 + px, py, pz = player_pos + dist_to_player = math.hypot(self.x - px, self.y - py) + + if self.type == "mosquito": + if dist_to_player < 100: + escape_angle = math.degrees(math.atan2(self.y - py, self.x - px)) + self.yaw += (escape_angle - self.yaw) * 0.05 + + self.yaw += math.sin(self.timer * 3) * 1.5 + self.z += math.sin(self.timer * 1.5) * 0.2 + + elif self.type == "fly": + if dist_to_player < 120: + escape_angle = math.degrees(math.atan2(self.y - py, self.x - px)) + self.yaw = escape_angle + random.uniform(-45, 45) + self.z += random.choice([-1.5, 1.5]) + else: + if random.random() < 0.08: + self.yaw += random.uniform(-60, 60) + + self.yaw += math.sin(self.timer * 6) * 4.0 + self.z += math.cos(self.timer * 4) * 0.6 + + self.z = max(3.0, min(50.0, self.z)) + + rad = math.radians(self.yaw) + next_x = self.x - math.sin(rad) * self.speed + next_y = self.y + math.cos(rad) * self.speed + + if is_over_water(next_x, next_y): + self.x, self.y = next_x, next_y + else: + self.yaw += 140.0 + random.uniform(-20, 20) + + if self.y > STREAM_MAX_Y: + self.y -= WORLD_LEN + elif self.y < STREAM_MIN_Y: + self.y += WORLD_LEN + + def draw(self, attached=False): + if self.type == "mosquito": + glColor3f(*COLOR_MOSQUITO) + else: + glColor3f(*COLOR_FLY) + + glPushMatrix() + if not attached: + glTranslatef(self.x, self.y, self.z) + glRotatef(self.yaw, 0, 0, 1) + + s = self.size + glBegin(GL_LINES) + # Cuerpo + glVertex3f(0, s, 0) + glVertex3f(0, -s, 0) + # Alas + w_offset = 0.0 if attached else math.sin(self.timer * 30) * (s * 0.8) + glVertex3f(0, 0, 0) + glVertex3f(-s * 1.5, 0, w_offset) + glVertex3f(0, 0, 0) + glVertex3f(s * 1.5, 0, w_offset) + glEnd() + + glPopMatrix() + +# ========================================== +# RENDERIZADO DEL ESCENARIO +# ========================================== +def draw_water_surface(time_counter): + """Dibuja una malla 3D animada para el agua.""" + glColor3f(*COLOR_WATER) + + grid_size = 30 + for x in range(int(-POND_RADIUS), int(POND_RADIUS), grid_size): + glBegin(GL_LINE_STRIP) + for y in range(int(-POND_RADIUS), int(POND_RADIUS), grid_size): + dist = math.hypot(x, y) + if dist < POND_RADIUS - 30: + z = 1.0 * math.sin(dist * 0.015 - time_counter * 3.0) + 0.5 * math.cos(x * 0.01 + time_counter) + glVertex3f(x, y, z) + glEnd() + + for y in range(int(STREAM_MIN_Y), int(STREAM_MAX_Y), 20): + if abs(y) < POND_RADIUS: + continue + cx = get_river_center(y) + glBegin(GL_LINE_STRIP) + for dx in range(-100, 110, 15): + x = cx + dx + z = 0.5 * math.sin(y * 0.03 + dx * 0.08 + time_counter * 4.0) + glVertex3f(x, y, z) + glEnd() + +def draw_terrain(): + """Dibuja el terreno del mapa.""" + glColor3f(*COLOR_LAND) + + grid_step = 120 + for y in range(int(STREAM_MIN_Y - 300), int(STREAM_MAX_Y + 300), grid_step): + glBegin(GL_LINE_STRIP) + for x in range(-2500, 2500, grid_step): + z = 1.5 * math.sin(x * 0.005) + 1.5 * math.cos(y * 0.005) + glVertex3f(x, y, z) + glEnd() + + width_river = 100.0 + margin_land = 70.0 + glColor3f(*COLOR_LAND_RIVER) + + for y in range(int(STREAM_MIN_Y), int(STREAM_MAX_Y), 30): + if abs(y) < POND_RADIUS: + continue + cx = get_river_center(y) + + # Orilla Izquierda + glBegin(GL_LINE_STRIP) + for dx in range(int(-width_river - margin_land), int(-width_river), 15): + x = cx + dx + z = 0.6 * math.sin(y * 0.02 + x * 0.02) + 0.3 + glVertex3f(x, y, z) + glEnd() + + # Orilla Derecha + glBegin(GL_LINE_STRIP) + for dx in range(int(width_river), int(width_river + margin_land), 15): + x = cx + dx + z = 0.6 * math.sin(y * 0.02 + x * 0.02) + 0.3 + glVertex3f(x, y, z) + glEnd() + + for ix, iy, ir in ISLANDS: + glColor3f(*COLOR_ISLAND) + for layer in range(1, 4): + rad_layer = ir * (1 - layer * 0.25) + height = layer * 1.2 + glBegin(GL_LINE_LOOP) + for i in range(16): + angle = 2.0 * math.pi * i / 16 + dx = rad_layer * math.cos(angle) + dy = rad_layer * math.sin(angle) + glVertex3f(ix + dx, iy + dy, height) + glEnd() + +def draw_map(time_counter): + """Dibuja todo el mapa.""" + draw_terrain() + draw_water_surface(time_counter) + + glColor3f(*COLOR_REED) + glBegin(GL_LINES) + for rx, ry, rz, rh in REEDS: + glVertex3f(rx, ry, rz) + glVertex3f(rx, ry, rz + rh) + glVertex3f(rx, ry, rz) + glVertex3f(rx - 1.5, ry + 0.5, rz + rh * 0.7) + glVertex3f(rx, ry, rz) + glVertex3f(rx + 1.5, ry - 0.5, rz + rh * 0.8) + glEnd() + +# ========================================== +# MODELO 3D DE LA LIBÉLULA +# ========================================== +def draw_dragonfly(wing_angle, caught_insect=None): + """Dibuja la libélula y el insecto atrapado en sus patas si tiene uno.""" + glColor3f(*COLOR_DRAGONFLY) + + # Cuerpo + glBegin(GL_LINES) + glVertex3f(0, 1.5, 0) + glVertex3f(0, -3.0, 0) + glVertex3f(-0.4, 0.6, 0) + glVertex3f(0.4, 0.6, 0) + glVertex3f(-0.3, -0.6, 0) + glVertex3f(0.3, -0.6, 0) + + # Patas (Visibles sujetando al insecto) + glVertex3f(-0.3, 0.5, 0) + glVertex3f(-0.2, 0.2, -0.6) + glVertex3f(0.3, 0.5, 0) + glVertex3f(0.2, 0.2, -0.6) + glVertex3f(-0.2, -0.2, 0) + glVertex3f(-0.1, -0.3, -0.6) + glVertex3f(0.2, -0.2, 0) + glVertex3f(0.1, -0.3, -0.6) + glEnd() + + # Cabeza + glBegin(GL_LINE_LOOP) + for i in range(8): + angle = 2.0 * math.pi * i / 8 + glVertex3f(0.3 * math.cos(angle), 1.4 + 0.3 * math.sin(angle), 0) + glEnd() + + # Alas + rad_wing = math.radians(wing_angle) + w_z = math.sin(rad_wing) * 1.2 if wing_angle != 0 else 0.0 + + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(-4.5, 1.0, w_z) + glVertex3f(-3.8, 0.2, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(4.5, 1.0, w_z) + glVertex3f(3.8, 0.2, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(-3.8, -0.2, w_z) + glVertex3f(-3.0, -0.8, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(3.8, -0.2, w_z) + glVertex3f(3.0, -0.8, w_z) + glEnd() + + # Si lleva un insecto atrapado, se dibuja sujeto bajo su tórax + if caught_insect: + glPushMatrix() + glTranslatef(0.0, 0.2, -0.6) # Posición bajo las patas + caught_insect.draw(attached=True) + glPopMatrix() + +# ========================================== +# BUCLE PRINCIPAL Y LÓGICA +# ========================================== +def main(): + pygame.init() + pygame.display.set_mode((WIDTH, HEIGHT), DOUBLEBUF | OPENGL) + + glEnable(GL_DEPTH_TEST) + glClearColor(*COLOR_BG, 1.0) + + glMatrixMode(GL_PROJECTION) + gluPerspective(50, (WIDTH / HEIGHT), 0.1, 5000.0) + glMatrixMode(GL_MODELVIEW) + + # Estado de la Libélula + px, py, pz = 0.0, 0.0, 15.0 + yaw = 0.0 + roll = 0.0 + pitch = 0.0 + max_roll = 30.0 + max_pitch = 20.0 + + speed = 0.0 + max_speed = 3.0 + acceleration = 0.06 + deceleration = 0.03 + + min_altitude = 1.0 + max_altitude = 120.0 + + wing_timer = 0.0 + water_timer = 0.0 + is_perched = False + + # Estado de Caza + caught_insect = None # Insecto sujeto actualmente en las patas + eaten_mosquitos = 0 # Contador de mosquitos comidos + eaten_flies = 0 # Contador de moscas comidas + + def update_title(): + status = " (Llevando presa a un junco...)" if caught_insect else "" + pygame.display.set_caption( + f"Libélula 3D | Mosquitos cazados: {eaten_mosquitos} | Moscas cazadas: {eaten_flies}{status}" + ) + + update_title() + + # Inicializar Insectos + insects = [] + num_mosquitos = random.randint(MOSQUITO_MIN, MOSQUITO_MAX) + num_flies = random.randint(FLY_MIN, FLY_MAX) + + for _ in range(num_mosquitos): + insects.append(Insect("mosquito")) + for _ in range(num_flies): + insects.append(Insect("fly")) + + clock = pygame.time.Clock() + + running = True + while running: + dt = clock.tick(60) / 1000.0 + water_timer += dt + + for event in pygame.event.get(): + if event.type == QUIT or (event.type == KEYDOWN and event.key == K_ESCAPE): + running = False + + keys = pygame.key.get_pressed() + + # CONTROLES Y MOVIMIENTO DE LA LIBÉLULA + if is_perched: + if keys[K_LEFT]: + yaw += 3.0 + if keys[K_RIGHT]: + yaw -= 3.0 + + if keys[K_UP]: + is_perched = False + speed = 0.5 + + wing_angle = 0.0 + pitch = 0.0 + roll = 0.0 + else: + if keys[K_UP]: + speed = min(speed + acceleration, max_speed) + elif keys[K_DOWN]: + speed = max(speed - deceleration, 0.0) + + # Intentar posarse en la punta de un junco + if speed < 0.3: + for rx, ry, rz, rh in REEDS: + dist = math.hypot(px - rx, py - ry) + if dist < 8.0 and abs(pz - (rz + rh)) < 8.0: + px, py, pz = rx, ry, rz + rh + speed = 0.0 + roll = 0.0 + pitch = 0.0 + is_perched = True + + # Si lleva una presa al posarse, se la come + if caught_insect: + if caught_insect.type == "mosquito": + eaten_mosquitos += 1 + else: + eaten_flies += 1 + caught_insect = None + update_title() + break + + target_pitch = 0.0 + if keys[K_w]: + pz = min(pz + 0.8, max_altitude) + target_pitch = max_pitch + elif keys[K_s]: + pz = max(pz - 0.8, min_altitude) + target_pitch = -max_pitch + + pitch += (target_pitch - pitch) * 0.15 + + target_roll = 0.0 + if keys[K_LEFT]: + yaw += 2.8 + target_roll = -max_roll + elif keys[K_RIGHT]: + yaw -= 2.8 + target_roll = max_roll + + roll += (target_roll - roll) * 0.15 + + rad_yaw = math.radians(yaw) + px += -math.sin(rad_yaw) * speed + py += math.cos(rad_yaw) * speed + + if abs(py) < POND_RADIUS: + max_limit_x = POND_RADIUS + 200.0 + else: + cx = get_river_center(py) + max_limit_x = abs(cx) + 200.0 + + px = max(-max_limit_x, min(max_limit_x, px)) + + if py > STREAM_MAX_Y: + py -= WORLD_LEN + elif py < STREAM_MIN_Y: + py += WORLD_LEN + + wing_timer += 60.0 if speed > 0.1 else 30.0 + wing_angle = math.sin(wing_timer) * 35.0 + + # ACTUALIZACIÓN DE INSECTOS + current_mosquitos = 0 + current_flies = 0 + + for insect in insects[:]: + insect.update((px, py, pz)) + + if insect.type == "mosquito": + current_mosquitos += 1 + else: + current_flies += 1 + + # Detección de contacto/choque con la libélula + if not caught_insect: + dist_to_dragonfly = math.sqrt((px - insect.x)**2 + (py - insect.y)**2 + (pz - insect.z)**2) + if dist_to_dragonfly < 6.0: # Rozamiento o choque + caught_insect = insect + insects.remove(insect) + update_title() + + # Mantener poblaciones dentro del rango + if current_mosquitos < MOSQUITO_MIN: + insects.append(Insect("mosquito")) + if current_flies < FLY_MIN: + insects.append(Insect("fly")) + + # RENDERIZADO + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) + glLoadIdentity() + + cam_dist = 18.0 + cam_height = 8.0 + rad_yaw = math.radians(yaw) + + cam_x = px + math.sin(rad_yaw) * cam_dist + cam_y = py - math.cos(rad_yaw) * cam_dist + cam_z = pz + cam_height + + gluLookAt(cam_x, cam_y, cam_z, + px, py, pz + 0.5, + 0, 0, 1) + + draw_map(water_timer) + + # Dibujar Insectos en el mapa + for insect in insects: + insect.draw() + + # Dibujar Libélula (con el insecto en las patas si lo ha capturado) + glPushMatrix() + glTranslatef(px, py, pz) + glRotatef(yaw, 0, 0, 1) + glRotatef(pitch, 1, 0, 0) + glRotatef(roll, 0, 1, 0) + draw_dragonfly(wing_angle, caught_insect) + glPopMatrix() + + pygame.display.flip() + + pygame.quit() + sys.exit() + +if __name__ == "__main__": + main() diff --git a/previos/dragonfly_game_v4.1.py b/previos/dragonfly_game_v4.1.py new file mode 100644 index 0000000..4567d4f --- /dev/null +++ b/previos/dragonfly_game_v4.1.py @@ -0,0 +1,702 @@ +import sys +import math +import random +import pygame +from pygame.locals import * +from OpenGL.GL import * +from OpenGL.GLU import * + +# ========================================== +# CONFIGURACIÓN Y CONSTANTES +# ========================================== +WIDTH, HEIGHT = 800, 600 + +# Colores (R, G, B) +COLOR_BG = (0.12, 0.15, 0.12) # Fondo ambiente +COLOR_WATER = (0.0, 0.5, 0.85) # Agua animada (Cian/Azul) +COLOR_LAND = (0.45, 0.32, 0.15) # Tierra / Relieve +COLOR_LAND_RIVER = (0.40, 0.28, 0.12) # Tierra en riachuelos +COLOR_ISLAND = (0.5, 0.38, 0.18) # Islas +COLOR_REED = (0.15, 0.85, 0.25) # Juncos verdes +COLOR_DRAGONFLY = (0.0, 1.0, 1.0) # Libélula alámbrica (Cian) +COLOR_MOSQUITO = (1.0, 0.2, 0.2) # Mosquitos (Rojo) +COLOR_FLY = (1.0, 0.9, 0.0) # Moscas (Amarillo/Dorado) +COLOR_HUD = (1.0, 1.0, 1.0) # Retícula HUD (Blanco) + +# Dimensiones del Mundo +POND_RADIUS = 1800.0 # Estanque de 1800u de superficie +STREAM_LEN = 900.0 # Longitud de cada arroyo +STREAM_MIN_Y = -(POND_RADIUS + STREAM_LEN) # -2700.0 +STREAM_MAX_Y = (POND_RADIUS + STREAM_LEN) # 2700.0 +WORLD_LEN = STREAM_MAX_Y - STREAM_MIN_Y + +# Cantidades de insectos +MOSQUITO_MIN, MOSQUITO_MAX = 10, 20 +FLY_MIN, FLY_MAX = 2, 7 + +# ========================================== +# ELEMENTOS DEL MAPA +# ========================================== +REEDS = [ + # Juncos en islas del estanque principal + (-250, 350, 0, 20), (-160, -500, 0, 22), (500, 200, 0, 24), (-700, 200, 0, 21), (200, -800, 0, 23), + (-900, -300, 0, 25), (760, -600, 0, 22), (900, 500, 0, 26), (-500, 900, 0, 23), (600, -1000, 0, 22), + (-1200, 400, 0, 24), (1300, -300, 0, 20), (-1400, -700, 0, 25), (1100, 1200, 0, 23), (-360, -1300, 0, 22), + # Juncos en el arroyo inferior + (-90, -1950, 0, 20), (-110, -2150, 0, 23), (-80, -2350, 0, 19), (-100, -2550, 0, 22), + # Juncos en el arroyo superior + (90, 1950, 0, 22), (110, 2150, 0, 20), (85, 2350, 0, 24), (105, 2550, 0, 21) +] + +ISLANDS = [ + (-250, 350, 90), (-160, -500, 85), (500, 200, 100), (-700, 200, 110), (200, -800, 95), + (-900, -300, 120), (760, -600, 90), (900, 500, 105), (-500, 900, 115), (600, -1000, 100) +] + +def get_river_center(y): + """Devuelve el centro X del arroyo fuera de los límites del estanque central.""" + if y < -POND_RADIUS: + dy = y + POND_RADIUS + return -50 + 100 * math.sin(dy * 0.008) + elif y > POND_RADIUS: + dy = y - POND_RADIUS + return 50 + 100 * math.sin(dy * 0.008) + else: + return (y / POND_RADIUS) * 50.0 + +def is_over_water(x, y): + """Comprueba si unas coordenadas (x, y) están dentro del agua.""" + dist_center = math.hypot(x, y) + if dist_center < POND_RADIUS - 50: + return True + if abs(y) >= POND_RADIUS: + cx = get_river_center(y) + if abs(x - cx) < 80: + return True + return False + +def get_random_water_position(): + """Genera una posición inicial aleatoria sobre el agua.""" + while True: + if random.random() < 0.7: + rad = random.uniform(0, math.pi * 2) + r = random.uniform(100, POND_RADIUS - 150) + x = r * math.cos(rad) + y = r * math.sin(rad) + else: + y = random.choice([random.uniform(POND_RADIUS, STREAM_MAX_Y), random.uniform(STREAM_MIN_Y, -POND_RADIUS)]) + cx = get_river_center(y) + x = cx + random.uniform(-60, 60) + + if is_over_water(x, y): + z = random.uniform(5.0, 35.0) + return x, y, z + +# ========================================== +# CLASES DE INSECTOS +# ========================================== +class Insect: + def __init__(self, insect_type): + self.type = insect_type + self.x, self.y, self.z = get_random_water_position() + self.yaw = random.uniform(0, 360) + self.timer = random.uniform(0, 100) + + if self.type == "mosquito": + self.speed = 1.8 + self.size = 1.2 + else: # Fly + self.speed = 0.9 + self.size = 1.6 + + def update(self, player_pos): + self.timer += 0.05 + px, py, pz = player_pos + dist_to_player = math.hypot(self.x - px, self.y - py) + + if self.type == "mosquito": + if dist_to_player < 100: + escape_angle = math.degrees(math.atan2(self.y - py, self.x - px)) + self.yaw += (escape_angle - self.yaw) * 0.05 + + self.yaw += math.sin(self.timer * 3) * 1.5 + self.z += math.sin(self.timer * 1.5) * 0.2 + + elif self.type == "fly": + if dist_to_player < 120: + escape_angle = math.degrees(math.atan2(self.y - py, self.x - px)) + self.yaw = escape_angle + random.uniform(-45, 45) + self.z += random.choice([-1.5, 1.5]) + else: + if random.random() < 0.08: + self.yaw += random.uniform(-60, 60) + + self.yaw += math.sin(self.timer * 6) * 4.0 + self.z += math.cos(self.timer * 4) * 0.6 + + self.z = max(3.0, min(50.0, self.z)) + + rad = math.radians(self.yaw) + next_x = self.x - math.sin(rad) * self.speed + next_y = self.y + math.cos(rad) * self.speed + + if is_over_water(next_x, next_y): + self.x, self.y = next_x, next_y + else: + self.yaw += 140.0 + random.uniform(-20, 20) + + if self.y > STREAM_MAX_Y: + self.y -= WORLD_LEN + elif self.y < STREAM_MIN_Y: + self.y += WORLD_LEN + + def draw(self, attached=False): + if self.type == "mosquito": + glColor3f(*COLOR_MOSQUITO) + else: + glColor3f(*COLOR_FLY) + + glPushMatrix() + if not attached: + glTranslatef(self.x, self.y, self.z) + glRotatef(self.yaw, 0, 0, 1) + + s = self.size + glBegin(GL_LINES) + glVertex3f(0, s, 0) + glVertex3f(0, -s, 0) + w_offset = 0.0 if attached else math.sin(self.timer * 30) * (s * 0.8) + glVertex3f(0, 0, 0) + glVertex3f(-s * 1.5, 0, w_offset) + glVertex3f(0, 0, 0) + glVertex3f(s * 1.5, 0, w_offset) + glEnd() + + glPopMatrix() + +# ========================================== +# RENDERIZADO DE ESCENARIO Y MODELOS +# ========================================== +def draw_water_surface(time_counter): + glColor3f(*COLOR_WATER) + + grid_size = 30 + for x in range(int(-POND_RADIUS), int(POND_RADIUS), grid_size): + glBegin(GL_LINE_STRIP) + for y in range(int(-POND_RADIUS), int(POND_RADIUS), grid_size): + dist = math.hypot(x, y) + if dist < POND_RADIUS - 30: + z = 1.0 * math.sin(dist * 0.015 - time_counter * 3.0) + 0.5 * math.cos(x * 0.01 + time_counter) + glVertex3f(x, y, z) + glEnd() + + for y in range(int(STREAM_MIN_Y), int(STREAM_MAX_Y), 20): + if abs(y) < POND_RADIUS: + continue + cx = get_river_center(y) + glBegin(GL_LINE_STRIP) + for dx in range(-100, 110, 15): + x = cx + dx + z = 0.5 * math.sin(y * 0.03 + dx * 0.08 + time_counter * 4.0) + glVertex3f(x, y, z) + glEnd() + +def draw_terrain(): + glColor3f(*COLOR_LAND) + + grid_step = 120 + for y in range(int(STREAM_MIN_Y - 300), int(STREAM_MAX_Y + 300), grid_step): + glBegin(GL_LINE_STRIP) + for x in range(-2500, 2500, grid_step): + z = 1.5 * math.sin(x * 0.005) + 1.5 * math.cos(y * 0.005) + glVertex3f(x, y, z) + glEnd() + + width_river = 100.0 + margin_land = 70.0 + glColor3f(*COLOR_LAND_RIVER) + + for y in range(int(STREAM_MIN_Y), int(STREAM_MAX_Y), 30): + if abs(y) < POND_RADIUS: + continue + cx = get_river_center(y) + + glBegin(GL_LINE_STRIP) + for dx in range(int(-width_river - margin_land), int(-width_river), 15): + x = cx + dx + z = 0.6 * math.sin(y * 0.02 + x * 0.02) + 0.3 + glVertex3f(x, y, z) + glEnd() + + glBegin(GL_LINE_STRIP) + for dx in range(int(width_river), int(width_river + margin_land), 15): + x = cx + dx + z = 0.6 * math.sin(y * 0.02 + x * 0.02) + 0.3 + glVertex3f(x, y, z) + glEnd() + + for ix, iy, ir in ISLANDS: + glColor3f(*COLOR_ISLAND) + for layer in range(1, 4): + rad_layer = ir * (1 - layer * 0.25) + height = layer * 1.2 + glBegin(GL_LINE_LOOP) + for i in range(16): + angle = 2.0 * math.pi * i / 16 + dx = rad_layer * math.cos(angle) + dy = rad_layer * math.sin(angle) + glVertex3f(ix + dx, iy + dy, height) + glEnd() + +def draw_map(time_counter): + draw_terrain() + draw_water_surface(time_counter) + + glColor3f(*COLOR_REED) + glBegin(GL_LINES) + for rx, ry, rz, rh in REEDS: + glVertex3f(rx, ry, rz) + glVertex3f(rx, ry, rz + rh) + glVertex3f(rx, ry, rz) + glVertex3f(rx - 1.5, ry + 0.5, rz + rh * 0.7) + glVertex3f(rx, ry, rz) + glVertex3f(rx + 1.5, ry - 0.5, rz + rh * 0.8) + glEnd() + +def draw_dragonfly(wing_angle, caught_insect=None): + glColor3f(*COLOR_DRAGONFLY) + + glBegin(GL_LINES) + glVertex3f(0, 1.5, 0) + glVertex3f(0, -3.0, 0) + glVertex3f(-0.4, 0.6, 0) + glVertex3f(0.4, 0.6, 0) + glVertex3f(-0.3, -0.6, 0) + glVertex3f(0.3, -0.6, 0) + + glVertex3f(-0.3, 0.5, 0) + glVertex3f(-0.2, 0.2, -0.6) + glVertex3f(0.3, 0.5, 0) + glVertex3f(0.2, 0.2, -0.6) + glVertex3f(-0.2, -0.2, 0) + glVertex3f(-0.1, -0.3, -0.6) + glVertex3f(0.2, -0.2, 0) + glVertex3f(0.1, -0.3, -0.6) + glEnd() + + glBegin(GL_LINE_LOOP) + for i in range(8): + angle = 2.0 * math.pi * i / 8 + glVertex3f(0.3 * math.cos(angle), 1.4 + 0.3 * math.sin(angle), 0) + glEnd() + + rad_wing = math.radians(wing_angle) + w_z = math.sin(rad_wing) * 1.2 if wing_angle != 0 else 0.0 + + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(-4.5, 1.0, w_z) + glVertex3f(-3.8, 0.2, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(4.5, 1.0, w_z) + glVertex3f(3.8, 0.2, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(-3.8, -0.2, w_z) + glVertex3f(-3.0, -0.8, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(3.8, -0.2, w_z) + glVertex3f(3.0, -0.8, w_z) + glEnd() + + if caught_insect: + glPushMatrix() + glTranslatef(0.0, 0.2, -0.6) + caught_insect.draw(attached=True) + glPopMatrix() + +# ========================================== +# INTERFAZ 2D / RADAR / HUD +# ========================================== +def draw_hud(player_pos, yaw, insects, eaten_mosquitos, eaten_flies, font): + """Dibuja el Radar central y los contadores en 2D superpuestos.""" + glDisable(GL_DEPTH_TEST) + + # Cambiar a proyección 2D ortogonal + glMatrixMode(GL_PROJECTION) + glPushMatrix() + glLoadIdentity() + gluOrtho2D(0, WIDTH, 0, HEIGHT) + glMatrixMode(GL_MODELVIEW) + glPushMatrix() + glLoadIdentity() + + px, py, pz = player_pos + + # ---------------------------------- + # 1. RADAR / MIRILLA (Centro Superior) + # ---------------------------------- + cx, cy = WIDTH // 2, HEIGHT - 70 + radius = 45 + + # Dibujar Círculo exterior + glColor3f(*COLOR_HUD) + glBegin(GL_LINE_LOOP) + for i in range(32): + angle = 2.0 * math.pi * i / 32 + glVertex2f(cx + radius * math.cos(angle), cy + radius * math.sin(angle)) + glEnd() + + # Dibujar Cruz Central y Horizonte en línea discontinua + glBegin(GL_LINES) + glVertex2f(cx, cy + radius) + glVertex2f(cx, cy + radius - 10) + glVertex2f(cx, cy - radius) + glVertex2f(cx, cy - radius + 10) + glVertex2f(cx - radius, cy) + glVertex2f(cx - radius + 10, cy) + glVertex2f(cx + radius, cy) + glVertex2f(cx + radius - 10, cy) + glEnd() + + # Cruz pequeña central + glBegin(GL_LINES) + glVertex2f(cx - 4, cy) + glVertex2f(cx + 4, cy) + glVertex2f(cx, cy - 4) + glVertex2f(cx, cy + 4) + glEnd() + + # Buscar el insecto más cercano al frente + closest_insect = None + min_dist = 999999.0 + relative_angle = 0.0 + diff_z = 0.0 + + rad_yaw = math.radians(yaw) + # Vector dirección del jugador + dir_x = -math.sin(rad_yaw) + dir_y = math.cos(rad_yaw) + + for insect in insects: + dx = insect.x - px + dy = insect.y - py + dist = math.hypot(dx, dy) + + if dist < min_dist and dist > 0.1: + # Calcular ángulo relativo respecto a hacia dónde mira la libélula + angle_to_insect = math.degrees(math.atan2(dy, dx)) + # Ajuste de ángulo respecto al YAW + heading = (angle_to_insect - (90 - yaw)) % 360 + if heading > 180: + heading -= 360 + + # Solo mostrar si está en un cono frontal de visión (120 grados) + if abs(heading) < 60: + min_dist = dist + closest_insect = insect + relative_angle = heading + diff_z = insect.z - pz # Por encima (+) o debajo (-) + + # Si hay una presa cercana al frente, dibujarla en la mirilla del radar + if closest_insect: + # Mapear posición horizontal (-60° a +60° -> -radius a +radius) + radar_x = cx - (relative_angle / 60.0) * (radius - 8) + # Mapear altura (-20u a +20u de diferencia -> -radius a +radius) + radar_y = cy + (diff_z / 20.0) * (radius - 8) + + # Clampear dentro del círculo del radar + dist_from_center = math.hypot(radar_x - cx, radar_y - cy) + if dist_from_center > (radius - 6): + factor = (radius - 6) / dist_from_center + radar_x = cx + (radar_x - cx) * factor + radar_y = cy + (radar_y - cy) * factor + + # Color del punto según la especie + if closest_insect.type == "mosquito": + glColor3f(*COLOR_MOSQUITO) + else: + glColor3f(*COLOR_FLY) + + # Dibujar punto e indicador de la presa + glBegin(GL_TRIANGLE_FAN) + for i in range(12): + ang = 2.0 * math.pi * i / 12 + glVertex2f(radar_x + 4 * math.cos(ang), radar_y + 4 * math.sin(ang)) + glEnd() + + # Pequeñas alas en el radar + glBegin(GL_LINES) + glVertex2f(radar_x - 7, radar_y) + glVertex2f(radar_x + 7, radar_y) + glEnd() + + # ---------------------------------- + # 2. ICONOS Y CONTADORES HUD + # ---------------------------------- + # Marcador Izquierdo: Mosquitos + glColor3f(*COLOR_MOSQUITO) + glBegin(GL_LINES) + # Dibujar pequeño ícono de mosquito + glVertex2f(25, HEIGHT - 25) + glVertex2f(25, HEIGHT - 45) + glVertex2f(25, HEIGHT - 35) + glVertex2f(10, HEIGHT - 35) + glVertex2f(25, HEIGHT - 35) + glVertex2f(40, HEIGHT - 35) + glEnd() + + # Marcador Derecho: Moscas + glColor3f(*COLOR_FLY) + glBegin(GL_LINES) + # Dibujar pequeño ícono de mosca + glVertex2f(WIDTH - 45, HEIGHT - 25) + glVertex2f(WIDTH - 45, HEIGHT - 45) + glVertex2f(WIDTH - 45, HEIGHT - 35) + glVertex2f(WIDTH - 60, HEIGHT - 35) + glVertex2f(WIDTH - 45, HEIGHT - 35) + glVertex2f(WIDTH - 30, HEIGHT - 35) + glEnd() + + # Restaurar matrices 3D + glPopMatrix() + glMatrixMode(GL_PROJECTION) + glPopMatrix() + glMatrixMode(GL_MODELVIEW) + glEnable(GL_DEPTH_TEST) + + # Renderizar el texto de los contadores mediante imágenes PyGame + txt_mosq = font.render(f"x {eaten_mosquitos}", True, (255, 60, 60)) + txt_flies = font.render(f"x {eaten_flies}", True, (255, 230, 0)) + + surface = pygame.display.get_surface() + + # Crear textura rápida para el texto en pantalla + draw_text_pygame(txt_mosq, 50, 20) + draw_text_pygame(txt_flies, WIDTH - 110, 20) + +def draw_text_pygame(text_surface, x, y): + """Dibuja texto de PyGame en la pantalla de OpenGL.""" + text_data = pygame.image.tostring(text_surface, "RGBA", True) + w, h = text_surface.get_size() + + glEnable(GL_BLEND) + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) + glRasterPos2d(x, HEIGHT - y - h) + glDrawPixels(w, h, GL_RGBA, GL_UNSIGNED_BYTE, text_data) + glDisable(GL_BLEND) + +# ========================================== +# BUCLE PRINCIPAL Y LÓGICA +# ========================================== +def main(): + pygame.init() + pygame.font.init() + font = pygame.font.SysFont("Arial", 26, bold=True) + + pygame.display.set_mode((WIDTH, HEIGHT), DOUBLEBUF | OPENGL) + pygame.display.set_caption("Simulador de Vuelo de Libélula 3D") + + glEnable(GL_DEPTH_TEST) + glClearColor(*COLOR_BG, 1.0) + + glMatrixMode(GL_PROJECTION) + gluPerspective(50, (WIDTH / HEIGHT), 0.1, 5000.0) + glMatrixMode(GL_MODELVIEW) + + # Estado de la Libélula + px, py, pz = 0.0, 0.0, 15.0 + yaw = 0.0 + roll = 0.0 + pitch = 0.0 + max_roll = 30.0 + max_pitch = 20.0 + + speed = 0.0 + max_speed = 3.0 + acceleration = 0.06 + deceleration = 0.03 + + min_altitude = 1.0 + max_altitude = 120.0 + + wing_timer = 0.0 + water_timer = 0.0 + is_perched = False + + # Estado de Caza + caught_insect = None + eaten_mosquitos = 0 + eaten_flies = 0 + + # Inicializar Insectos + insects = [] + num_mosquitos = random.randint(MOSQUITO_MIN, MOSQUITO_MAX) + num_flies = random.randint(FLY_MIN, FLY_MAX) + + for _ in range(num_mosquitos): + insects.append(Insect("mosquito")) + for _ in range(num_flies): + insects.append(Insect("fly")) + + clock = pygame.time.Clock() + + running = True + while running: + dt = clock.tick(60) / 1000.0 + water_timer += dt + + for event in pygame.event.get(): + if event.type == QUIT or (event.type == KEYDOWN and event.key == K_ESCAPE): + running = False + + keys = pygame.key.get_pressed() + + # CONTROLES Y MOVIMIENTO DE LA LIBÉLULA + if is_perched: + if keys[K_LEFT]: + yaw += 3.0 + if keys[K_RIGHT]: + yaw -= 3.0 + + if keys[K_UP]: + is_perched = False + speed = 0.5 + + wing_angle = 0.0 + pitch = 0.0 + roll = 0.0 + else: + if keys[K_UP]: + speed = min(speed + acceleration, max_speed) + elif keys[K_DOWN]: + speed = max(speed - deceleration, 0.0) + + if speed < 0.3: + for rx, ry, rz, rh in REEDS: + dist = math.hypot(px - rx, py - ry) + if dist < 8.0 and abs(pz - (rz + rh)) < 8.0: + px, py, pz = rx, ry, rz + rh + speed = 0.0 + roll = 0.0 + pitch = 0.0 + is_perched = True + + if caught_insect: + if caught_insect.type == "mosquito": + eaten_mosquitos += 1 + else: + eaten_flies += 1 + caught_insect = None + break + + target_pitch = 0.0 + if keys[K_w]: + pz = min(pz + 0.8, max_altitude) + target_pitch = max_pitch + elif keys[K_s]: + pz = max(pz - 0.8, min_altitude) + target_pitch = -max_pitch + + pitch += (target_pitch - pitch) * 0.15 + + target_roll = 0.0 + if keys[K_LEFT]: + yaw += 2.8 + target_roll = -max_roll + elif keys[K_RIGHT]: + yaw -= 2.8 + target_roll = max_roll + + roll += (target_roll - roll) * 0.15 + + rad_yaw = math.radians(yaw) + px += -math.sin(rad_yaw) * speed + py += math.cos(rad_yaw) * speed + + if abs(py) < POND_RADIUS: + max_limit_x = POND_RADIUS + 200.0 + else: + cx = get_river_center(py) + max_limit_x = abs(cx) + 200.0 + + px = max(-max_limit_x, min(max_limit_x, px)) + + if py > STREAM_MAX_Y: + py -= WORLD_LEN + elif py < STREAM_MIN_Y: + py += WORLD_LEN + + wing_timer += 60.0 if speed > 0.1 else 30.0 + wing_angle = math.sin(wing_timer) * 35.0 + + # ACTUALIZACIÓN DE INSECTOS + current_mosquitos = 0 + current_flies = 0 + + for insect in insects[:]: + insect.update((px, py, pz)) + + if insect.type == "mosquito": + current_mosquitos += 1 + else: + current_flies += 1 + + if not caught_insect: + dist_to_dragonfly = math.sqrt((px - insect.x)**2 + (py - insect.y)**2 + (pz - insect.z)**2) + if dist_to_dragonfly < 6.0: + caught_insect = insect + insects.remove(insect) + + if current_mosquitos < MOSQUITO_MIN: + insects.append(Insect("mosquito")) + if current_flies < FLY_MIN: + insects.append(Insect("fly")) + + # RENDERIZADO + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) + glLoadIdentity() + + cam_dist = 18.0 + cam_height = 8.0 + rad_yaw = math.radians(yaw) + + cam_x = px + math.sin(rad_yaw) * cam_dist + cam_y = py - math.cos(rad_yaw) * cam_dist + cam_z = pz + cam_height + + gluLookAt(cam_x, cam_y, cam_z, + px, py, pz + 0.5, + 0, 0, 1) + + draw_map(water_timer) + + for insect in insects: + insect.draw() + + glPushMatrix() + glTranslatef(px, py, pz) + glRotatef(yaw, 0, 0, 1) + glRotatef(pitch, 1, 0, 0) + glRotatef(roll, 0, 1, 0) + draw_dragonfly(wing_angle, caught_insect) + glPopMatrix() + + # DIBUJAR INTERFAZ (HUD) Y RADAR + draw_hud((px, py, pz), yaw, insects, eaten_mosquitos, eaten_flies, font) + + pygame.display.flip() + + pygame.quit() + sys.exit() + +if __name__ == "__main__": + main() diff --git a/previos/dragonfly_game_v4.3.py b/previos/dragonfly_game_v4.3.py new file mode 100644 index 0000000..768da4e --- /dev/null +++ b/previos/dragonfly_game_v4.3.py @@ -0,0 +1,675 @@ +import sys +import math +import random +import pygame +from pygame.locals import * +from OpenGL.GL import * +from OpenGL.GLU import * + +# ========================================== +# CONFIGURACIÓN Y CONSTANTES +# ========================================== +WIDTH, HEIGHT = 800, 600 + +# Colores (R, G, B) +COLOR_BG = (0.12, 0.15, 0.12) # Fondo ambiente +COLOR_WATER = (0.0, 0.5, 0.85) # Agua animada (Cian/Azul) +COLOR_LAND = (0.45, 0.32, 0.15) # Tierra / Relieve +COLOR_LAND_RIVER = (0.40, 0.28, 0.12) # Tierra en riachuelos +COLOR_ISLAND = (0.5, 0.38, 0.18) # Islas +COLOR_REED = (0.15, 0.85, 0.25) # Juncos verdes +COLOR_DRAGONFLY = (0.0, 1.0, 1.0) # Libélula alámbrica (Cian) +COLOR_MOSQUITO = (1.0, 0.9, 0.0) # Mosquitos (Amarillo) +COLOR_FLY = (0.8, 0.25, 0.15) # Moscas (Rojo-Marrón) +COLOR_HUD = (1.0, 1.0, 1.0) # Retícula HUD (Blanco) + +# Dimensiones del Mundo +POND_RADIUS = 1800.0 # Estanque de 1800u de superficie +STREAM_LEN = 900.0 # Longitud de cada arroyo +STREAM_MIN_Y = -(POND_RADIUS + STREAM_LEN) # -2700.0 +STREAM_MAX_Y = (POND_RADIUS + STREAM_LEN) # 2700.0 +WORLD_LEN = STREAM_MAX_Y - STREAM_MIN_Y + +# Cantidades de insectos +MOSQUITO_MIN, MOSQUITO_MAX = 10, 20 +FLY_MIN, FLY_MAX = 2, 7 + +# ========================================== +# ELEMENTOS DEL MAPA +# ========================================== +REEDS = [ + # Juncos en islas del estanque principal + (-250, 350, 0, 20), (-160, -500, 0, 22), (500, 200, 0, 24), (-700, 200, 0, 21), (200, -800, 0, 23), + (-900, -300, 0, 25), (760, -600, 0, 22), (900, 500, 0, 26), (-500, 900, 0, 23), (600, -1000, 0, 22), + (-1200, 400, 0, 24), (1300, -300, 0, 20), (-1400, -700, 0, 25), (1100, 1200, 0, 23), (-360, -1300, 0, 22), + # Juncos en el arroyo inferior + (-90, -1950, 0, 20), (-110, -2150, 0, 23), (-80, -2350, 0, 19), (-100, -2550, 0, 22), + # Juncos en el arroyo superior + (90, 1950, 0, 22), (110, 2150, 0, 20), (85, 2350, 0, 24), (105, 2550, 0, 21) +] + +ISLANDS = [ + (-250, 350, 90), (-160, -500, 85), (500, 200, 100), (-700, 200, 110), (200, -800, 95), + (-900, -300, 120), (760, -600, 90), (900, 500, 105), (-500, 900, 115), (600, -1000, 100) +] + +def get_river_center(y): + """Devuelve el centro X del arroyo fuera de los límites del estanque central.""" + if y < -POND_RADIUS: + dy = y + POND_RADIUS + return -50 + 100 * math.sin(dy * 0.008) + elif y > POND_RADIUS: + dy = y - POND_RADIUS + return 50 + 100 * math.sin(dy * 0.008) + else: + return (y / POND_RADIUS) * 50.0 + +def is_over_water(x, y): + """Comprueba si unas coordenadas (x, y) están dentro del agua.""" + dist_center = math.hypot(x, y) + if dist_center < POND_RADIUS - 50: + return True + if abs(y) >= POND_RADIUS: + cx = get_river_center(y) + if abs(x - cx) < 80: + return True + return False + +def get_random_water_position(): + """Genera una posición inicial aleatoria sobre el agua.""" + while True: + if random.random() < 0.7: + rad = random.uniform(0, math.pi * 2) + r = random.uniform(100, POND_RADIUS - 150) + x = r * math.cos(rad) + y = r * math.sin(rad) + else: + y = random.choice([random.uniform(POND_RADIUS, STREAM_MAX_Y), random.uniform(STREAM_MIN_Y, -POND_RADIUS)]) + cx = get_river_center(y) + x = cx + random.uniform(-60, 60) + + if is_over_water(x, y): + z = random.uniform(5.0, 35.0) + return x, y, z + +# ========================================== +# CLASES DE INSECTOS +# ========================================== +class Insect: + def __init__(self, insect_type): + self.type = insect_type + self.x, self.y, self.z = get_random_water_position() + self.yaw = random.uniform(0, 360) + self.timer = random.uniform(0, 100) + + if self.type == "mosquito": + self.speed = 1.8 + self.size = 1.2 + else: # Fly + self.speed = 0.9 + self.size = 1.6 + + def update(self, player_pos): + self.timer += 0.05 + px, py, pz = player_pos + dist_to_player = math.hypot(self.x - px, self.y - py) + + if self.type == "mosquito": + if dist_to_player < 100: + escape_angle = math.degrees(math.atan2(self.y - py, self.x - px)) + self.yaw += (escape_angle - self.yaw) * 0.05 + + self.yaw += math.sin(self.timer * 3) * 1.5 + self.z += math.sin(self.timer * 1.5) * 0.2 + + elif self.type == "fly": + if dist_to_player < 120: + escape_angle = math.degrees(math.atan2(self.y - py, self.x - px)) + self.yaw = escape_angle + random.uniform(-45, 45) + self.z += random.choice([-1.5, 1.5]) + else: + if random.random() < 0.08: + self.yaw += random.uniform(-60, 60) + + self.yaw += math.sin(self.timer * 6) * 4.0 + self.z += math.cos(self.timer * 4) * 0.6 + + self.z = max(3.0, min(50.0, self.z)) + + rad = math.radians(self.yaw) + next_x = self.x - math.sin(rad) * self.speed + next_y = self.y + math.cos(rad) * self.speed + + if is_over_water(next_x, next_y): + self.x, self.y = next_x, next_y + else: + self.yaw += 140.0 + random.uniform(-20, 20) + + if self.y > STREAM_MAX_Y: + self.y -= WORLD_LEN + elif self.y < STREAM_MIN_Y: + self.y += WORLD_LEN + + def draw(self, attached=False): + if self.type == "mosquito": + glColor3f(*COLOR_MOSQUITO) + else: + glColor3f(*COLOR_FLY) + + glPushMatrix() + if not attached: + glTranslatef(self.x, self.y, self.z) + glRotatef(self.yaw, 0, 0, 1) + + s = self.size + glBegin(GL_LINES) + glVertex3f(0, s, 0) + glVertex3f(0, -s, 0) + w_offset = 0.0 if attached else math.sin(self.timer * 30) * (s * 0.8) + glVertex3f(0, 0, 0) + glVertex3f(-s * 1.5, 0, w_offset) + glVertex3f(0, 0, 0) + glVertex3f(s * 1.5, 0, w_offset) + glEnd() + + glPopMatrix() + +# ========================================== +# RENDERIZADO DE ESCENARIO Y MODELOS +# ========================================== +def draw_water_surface(time_counter): + glColor3f(*COLOR_WATER) + + grid_size = 30 + for x in range(int(-POND_RADIUS), int(POND_RADIUS), grid_size): + glBegin(GL_LINE_STRIP) + for y in range(int(-POND_RADIUS), int(POND_RADIUS), grid_size): + dist = math.hypot(x, y) + if dist < POND_RADIUS - 30: + z = 1.0 * math.sin(dist * 0.015 - time_counter * 3.0) + 0.5 * math.cos(x * 0.01 + time_counter) + glVertex3f(x, y, z) + glEnd() + + for y in range(int(STREAM_MIN_Y), int(STREAM_MAX_Y), 20): + if abs(y) < POND_RADIUS: + continue + cx = get_river_center(y) + glBegin(GL_LINE_STRIP) + for dx in range(-100, 110, 15): + x = cx + dx + z = 0.5 * math.sin(y * 0.03 + dx * 0.08 + time_counter * 4.0) + glVertex3f(x, y, z) + glEnd() + +def draw_terrain(): + glColor3f(*COLOR_LAND) + + grid_step = 120 + for y in range(int(STREAM_MIN_Y - 300), int(STREAM_MAX_Y + 300), grid_step): + glBegin(GL_LINE_STRIP) + for x in range(-2500, 2500, grid_step): + z = 1.5 * math.sin(x * 0.005) + 1.5 * math.cos(y * 0.005) + glVertex3f(x, y, z) + glEnd() + + width_river = 100.0 + margin_land = 70.0 + glColor3f(*COLOR_LAND_RIVER) + + for y in range(int(STREAM_MIN_Y), int(STREAM_MAX_Y), 30): + if abs(y) < POND_RADIUS: + continue + cx = get_river_center(y) + + glBegin(GL_LINE_STRIP) + for dx in range(int(-width_river - margin_land), int(-width_river), 15): + x = cx + dx + z = 0.6 * math.sin(y * 0.02 + x * 0.02) + 0.3 + glVertex3f(x, y, z) + glEnd() + + glBegin(GL_LINE_STRIP) + for dx in range(int(width_river), int(width_river + margin_land), 15): + x = cx + dx + z = 0.6 * math.sin(y * 0.02 + x * 0.02) + 0.3 + glVertex3f(x, y, z) + glEnd() + + for ix, iy, ir in ISLANDS: + glColor3f(*COLOR_ISLAND) + for layer in range(1, 4): + rad_layer = ir * (1 - layer * 0.25) + height = layer * 1.2 + glBegin(GL_LINE_LOOP) + for i in range(16): + angle = 2.0 * math.pi * i / 16 + dx = rad_layer * math.cos(angle) + dy = rad_layer * math.sin(angle) + glVertex3f(ix + dx, iy + dy, height) + glEnd() + +def draw_map(time_counter): + draw_terrain() + draw_water_surface(time_counter) + + glColor3f(*COLOR_REED) + glBegin(GL_LINES) + for rx, ry, rz, rh in REEDS: + glVertex3f(rx, ry, rz) + glVertex3f(rx, ry, rz + rh) + glVertex3f(rx, ry, rz) + glVertex3f(rx - 1.5, ry + 0.5, rz + rh * 0.7) + glVertex3f(rx, ry, rz) + glVertex3f(rx + 1.5, ry - 0.5, rz + rh * 0.8) + glEnd() + +def draw_dragonfly(wing_angle, caught_insect=None): + glColor3f(*COLOR_DRAGONFLY) + + glBegin(GL_LINES) + glVertex3f(0, 1.5, 0) + glVertex3f(0, -3.0, 0) + glVertex3f(-0.4, 0.6, 0) + glVertex3f(0.4, 0.6, 0) + glVertex3f(-0.3, -0.6, 0) + glVertex3f(0.3, -0.6, 0) + + glVertex3f(-0.3, 0.5, 0) + glVertex3f(-0.2, 0.2, -0.6) + glVertex3f(0.3, 0.5, 0) + glVertex3f(0.2, 0.2, -0.6) + glVertex3f(-0.2, -0.2, 0) + glVertex3f(-0.1, -0.3, -0.6) + glVertex3f(0.2, -0.2, 0) + glVertex3f(0.1, -0.3, -0.6) + glEnd() + + glBegin(GL_LINE_LOOP) + for i in range(8): + angle = 2.0 * math.pi * i / 8 + glVertex3f(0.3 * math.cos(angle), 1.4 + 0.3 * math.sin(angle), 0) + glEnd() + + rad_wing = math.radians(wing_angle) + w_z = math.sin(rad_wing) * 1.2 if wing_angle != 0 else 0.0 + + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(-4.5, 1.0, w_z) + glVertex3f(-3.8, 0.2, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(4.5, 1.0, w_z) + glVertex3f(3.8, 0.2, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(-3.8, -0.2, w_z) + glVertex3f(-3.0, -0.8, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(3.8, -0.2, w_z) + glVertex3f(3.0, -0.8, w_z) + glEnd() + + if caught_insect: + glPushMatrix() + glTranslatef(0.0, 0.2, -0.6) + caught_insect.draw(attached=True) + glPopMatrix() + +# ========================================== +# INTERFAZ 2D / RADAR REDUCIDO / HUD +# ========================================== +def draw_hud(player_pos, yaw, insects, eaten_mosquitos, eaten_flies, font): + """Dibuja el Radar reducido y los contadores en 2D superpuestos fijados arriba.""" + glDisable(GL_DEPTH_TEST) + + glMatrixMode(GL_PROJECTION) + glPushMatrix() + glLoadIdentity() + gluOrtho2D(0, WIDTH, 0, HEIGHT) + glMatrixMode(GL_MODELVIEW) + glPushMatrix() + glLoadIdentity() + + px, py, pz = player_pos + + # ---------------------------------- + # 1. RADAR REDUCIDO A LA MITAD (Centro Superior) + # ---------------------------------- + cx, cy = WIDTH // 2, HEIGHT - 35 + radius = 22 # Tamaño reducido a la mitad (era 45) + + # Dibujar Círculo exterior + glColor3f(*COLOR_HUD) + glBegin(GL_LINE_LOOP) + for i in range(24): + angle = 2.0 * math.pi * i / 24 + glVertex2f(cx + radius * math.cos(angle), cy + radius * math.sin(angle)) + glEnd() + + # Dibujar Cruz Central + glBegin(GL_LINES) + glVertex2f(cx, cy + radius) + glVertex2f(cx, cy + radius - 5) + glVertex2f(cx, cy - radius) + glVertex2f(cx, cy - radius + 5) + glVertex2f(cx - radius, cy) + glVertex2f(cx - radius + 5, cy) + glVertex2f(cx + radius, cy) + glVertex2f(cx + radius - 5, cy) + glEnd() + + # Cruz pequeña central + glBegin(GL_LINES) + glVertex2f(cx - 2, cy) + glVertex2f(cx + 2, cy) + glVertex2f(cx, cy - 2) + glVertex2f(cx, cy + 2) + glEnd() + + # Buscar el insecto más cercano al frente + closest_insect = None + min_dist = 999999.0 + relative_angle = 0.0 + diff_z = 0.0 + + rad_yaw = math.radians(yaw) + + for insect in insects: + dx = insect.x - px + dy = insect.y - py + dist = math.hypot(dx, dy) + + if dist < min_dist and dist > 0.1: + angle_to_insect = math.degrees(math.atan2(dy, dx)) + heading = (angle_to_insect - (90 - yaw)) % 360 + if heading > 180: + heading -= 360 + + if abs(heading) < 60: + min_dist = dist + closest_insect = insect + relative_angle = heading + diff_z = insect.z - pz + + # Si hay una presa cercana al frente, se dibuja un pequeño círculo genérico blanco + if closest_insect: + radar_x = cx - (relative_angle / 60.0) * (radius - 4) + radar_y = cy + (diff_z / 20.0) * (radius - 4) + + dist_from_center = math.hypot(radar_x - cx, radar_y - cy) + if dist_from_center > (radius - 3): + factor = (radius - 3) / dist_from_center + radar_x = cx + (radar_x - cx) * factor + radar_y = cy + (radar_y - cy) * factor + + # Círculo vacío blanco (genérico) + glColor3f(1.0, 1.0, 1.0) + glBegin(GL_LINE_LOOP) + for i in range(8): + ang = 2.0 * math.pi * i / 8 + glVertex2f(radar_x + 3 * math.cos(ang), radar_y + 3 * math.sin(ang)) + glEnd() + + # ---------------------------------- + # 2. ICONOS Y CONTADORES HUD FIJOS + # ---------------------------------- + # Marcador Izquierdo: Mosquitos (Amarillo) + glColor3f(*COLOR_MOSQUITO) + glBegin(GL_LINES) + glVertex2f(25, HEIGHT - 20) + glVertex2f(25, HEIGHT - 40) + glVertex2f(25, HEIGHT - 30) + glVertex2f(12, HEIGHT - 30) + glVertex2f(25, HEIGHT - 30) + glVertex2f(38, HEIGHT - 30) + glEnd() + + # Marcador Derecho: Moscas (Rojo-Marrón) + glColor3f(*COLOR_FLY) + glBegin(GL_LINES) + glVertex2f(WIDTH - 45, HEIGHT - 20) + glVertex2f(WIDTH - 45, HEIGHT - 40) + glVertex2f(WIDTH - 45, HEIGHT - 30) + glVertex2f(WIDTH - 58, HEIGHT - 30) + glVertex2f(WIDTH - 45, HEIGHT - 30) + glVertex2f(WIDTH - 32, HEIGHT - 30) + glEnd() + + glPopMatrix() + glMatrixMode(GL_PROJECTION) + glPopMatrix() + glMatrixMode(GL_MODELVIEW) + glEnable(GL_DEPTH_TEST) + + # Textos fija de conteo + txt_mosq = font.render(f"x {eaten_mosquitos}", True, (255, 230, 0)) # Texto amarillo + txt_flies = font.render(f"x {eaten_flies}", True, (200, 65, 40)) # Texto rojo-marrón + + draw_text_pygame(txt_mosq, 48, 18) + draw_text_pygame(txt_flies, WIDTH - 110, 18) + +def draw_text_pygame(text_surface, x, y): + """Dibuja texto de PyGame en la pantalla de OpenGL.""" + text_data = pygame.image.tostring(text_surface, "RGBA", True) + w, h = text_surface.get_size() + + glEnable(GL_BLEND) + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) + glRasterPos2d(x, HEIGHT - y - h) + glDrawPixels(w, h, GL_RGBA, GL_UNSIGNED_BYTE, text_data) + glDisable(GL_BLEND) + +# ========================================== +# BUCLE PRINCIPAL Y LÓGICA +# ========================================== +def main(): + pygame.init() + pygame.font.init() + font = pygame.font.SysFont("Arial", 22, bold=True) + + pygame.display.set_mode((WIDTH, HEIGHT), DOUBLEBUF | OPENGL) + pygame.display.set_caption("Simulador de Vuelo de Libélula 3D") + + glEnable(GL_DEPTH_TEST) + glClearColor(*COLOR_BG, 1.0) + + glMatrixMode(GL_PROJECTION) + gluPerspective(50, (WIDTH / HEIGHT), 0.1, 5000.0) + glMatrixMode(GL_MODELVIEW) + + # Estado de la Libélula + px, py, pz = 0.0, 0.0, 15.0 + yaw = 0.0 + roll = 0.0 + pitch = 0.0 + max_roll = 30.0 + max_pitch = 20.0 + + speed = 0.0 + max_speed = 3.0 + acceleration = 0.06 + deceleration = 0.03 + + min_altitude = 1.0 + max_altitude = 120.0 + + wing_timer = 0.0 + water_timer = 0.0 + is_perched = False + + # Estado de Caza + caught_insect = None + eaten_mosquitos = 0 + eaten_flies = 0 + + # Inicializar Insectos + insects = [] + num_mosquitos = random.randint(MOSQUITO_MIN, MOSQUITO_MAX) + num_flies = random.randint(FLY_MIN, FLY_MAX) + + for _ in range(num_mosquitos): + insects.append(Insect("mosquito")) + for _ in range(num_flies): + insects.append(Insect("fly")) + + clock = pygame.time.Clock() + + running = True + while running: + dt = clock.tick(60) / 1000.0 + water_timer += dt + + for event in pygame.event.get(): + if event.type == QUIT or (event.type == KEYDOWN and event.key == K_ESCAPE): + running = False + + keys = pygame.key.get_pressed() + + # CONTROLES Y MOVIMIENTO DE LA LIBÉLULA + if is_perched: + if keys[K_LEFT]: + yaw += 3.0 + if keys[K_RIGHT]: + yaw -= 3.0 + + if keys[K_UP]: + is_perched = False + speed = 0.5 + + wing_angle = 0.0 + pitch = 0.0 + roll = 0.0 + else: + if keys[K_UP]: + speed = min(speed + acceleration, max_speed) + elif keys[K_DOWN]: + speed = max(speed - deceleration, 0.0) + + if speed < 0.3: + for rx, ry, rz, rh in REEDS: + dist = math.hypot(px - rx, py - ry) + if dist < 8.0 and abs(pz - (rz + rh)) < 8.0: + px, py, pz = rx, ry, rz + rh + speed = 0.0 + roll = 0.0 + pitch = 0.0 + is_perched = True + + if caught_insect: + if caught_insect.type == "mosquito": + eaten_mosquitos += 1 + else: + eaten_flies += 1 + caught_insect = None + break + + target_pitch = 0.0 + if keys[K_w]: + pz = min(pz + 0.8, max_altitude) + target_pitch = max_pitch + elif keys[K_s]: + pz = max(pz - 0.8, min_altitude) + target_pitch = -max_pitch + + pitch += (target_pitch - pitch) * 0.15 + + target_roll = 0.0 + if keys[K_LEFT]: + yaw += 2.8 + target_roll = -max_roll + elif keys[K_RIGHT]: + yaw -= 2.8 + target_roll = max_roll + + roll += (target_roll - roll) * 0.15 + + rad_yaw = math.radians(yaw) + px += -math.sin(rad_yaw) * speed + py += math.cos(rad_yaw) * speed + + if abs(py) < POND_RADIUS: + max_limit_x = POND_RADIUS + 200.0 + else: + cx = get_river_center(py) + max_limit_x = abs(cx) + 200.0 + + px = max(-max_limit_x, min(max_limit_x, px)) + + if py > STREAM_MAX_Y: + py -= WORLD_LEN + elif py < STREAM_MIN_Y: + py += WORLD_LEN + + wing_timer += 60.0 if speed > 0.1 else 30.0 + wing_angle = math.sin(wing_timer) * 35.0 + + # ACTUALIZACIÓN DE INSECTOS + current_mosquitos = 0 + current_flies = 0 + + for insect in insects[:]: + insect.update((px, py, pz)) + + if insect.type == "mosquito": + current_mosquitos += 1 + else: + current_flies += 1 + + if not caught_insect: + dist_to_dragonfly = math.sqrt((px - insect.x)**2 + (py - insect.y)**2 + (pz - insect.z)**2) + if dist_to_dragonfly < 6.0: + caught_insect = insect + insects.remove(insect) + + if current_mosquitos < MOSQUITO_MIN: + insects.append(Insect("mosquito")) + if current_flies < FLY_MIN: + insects.append(Insect("fly")) + + # RENDERIZADO + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) + glLoadIdentity() + + cam_dist = 18.0 + cam_height = 8.0 + rad_yaw = math.radians(yaw) + + cam_x = px + math.sin(rad_yaw) * cam_dist + cam_y = py - math.cos(rad_yaw) * cam_dist + cam_z = pz + cam_height + + gluLookAt(cam_x, cam_y, cam_z, + px, py, pz + 0.5, + 0, 0, 1) + + draw_map(water_timer) + + for insect in insects: + insect.draw() + + glPushMatrix() + glTranslatef(px, py, pz) + glRotatef(yaw, 0, 0, 1) + glRotatef(pitch, 1, 0, 0) + glRotatef(roll, 0, 1, 0) + draw_dragonfly(wing_angle, caught_insect) + glPopMatrix() + + # DIBUJAR INTERFAZ (HUD) Y RADAR + draw_hud((px, py, pz), yaw, insects, eaten_mosquitos, eaten_flies, font) + + pygame.display.flip() + + pygame.quit() + sys.exit() + +if __name__ == "__main__": + main() diff --git a/previos/dragonfly_game_v5.0.py b/previos/dragonfly_game_v5.0.py new file mode 100644 index 0000000..f849066 --- /dev/null +++ b/previos/dragonfly_game_v5.0.py @@ -0,0 +1,668 @@ +import sys +import math +import random +import pygame +from pygame.locals import * +from OpenGL.GL import * +from OpenGL.GLU import * + +# ========================================== +# CONFIGURACIÓN Y CONSTANTES +# ========================================== +WIDTH, HEIGHT = 800, 600 + +# Colores (R, G, B) +COLOR_BG = (0.12, 0.15, 0.12) # Fondo ambiente +COLOR_WATER = (0.0, 0.5, 0.85) # Agua animada (Cian/Azul) +COLOR_LAND = (0.45, 0.32, 0.15) # Tierra / Relieve +COLOR_LAND_RIVER = (0.40, 0.28, 0.12) # Tierra en riachuelos +COLOR_ISLAND = (0.5, 0.38, 0.18) # Islas +COLOR_REED = (0.15, 0.85, 0.25) # Juncos verdes +COLOR_DRAGONFLY = (0.0, 1.0, 1.0) # Libélula alámbrica (Cian) +COLOR_MOSQUITO = (1.0, 0.9, 0.0) # Mosquitos (Amarillo) +COLOR_FLY = (0.8, 0.25, 0.15) # Moscas (Rojo-Marrón) +COLOR_HUD = (1.0, 1.0, 1.0) # Retícula HUD (Blanco) + +# Dimensiones del Mundo +POND_RADIUS = 1800.0 +STREAM_LEN = 900.0 +STREAM_MIN_Y = -(POND_RADIUS + STREAM_LEN) +STREAM_MAX_Y = (POND_RADIUS + STREAM_LEN) +WORLD_LEN = STREAM_MAX_Y - STREAM_MIN_Y + +# Cantidades de insectos +MOSQUITO_MIN, MOSQUITO_MAX = 10, 20 +FLY_MIN, FLY_MAX = 2, 7 + +# ========================================== +# ELEMENTOS DEL MAPA +# ========================================== +REEDS = [ + (-250, 350, 0, 20), (-160, -500, 0, 22), (500, 200, 0, 24), (-700, 200, 0, 21), (200, -800, 0, 23), + (-900, -300, 0, 25), (760, -600, 0, 22), (900, 500, 0, 26), (-500, 900, 0, 23), (600, -1000, 0, 22), + (-1200, 400, 0, 24), (1300, -300, 0, 20), (-1400, -700, 0, 25), (1100, 1200, 0, 23), (-360, -1300, 0, 22), + (-90, -1950, 0, 20), (-110, -2150, 0, 23), (-80, -2350, 0, 19), (-100, -2550, 0, 22), + (90, 1950, 0, 22), (110, 2150, 0, 20), (85, 2350, 0, 24), (105, 2550, 0, 21) +] + +ISLANDS = [ + (-250, 350, 90), (-160, -500, 85), (500, 200, 100), (-700, 200, 110), (200, -800, 95), + (-900, -300, 120), (760, -600, 90), (900, 500, 105), (-500, 900, 115), (600, -1000, 100) +] + +def get_river_center(y): + if y < -POND_RADIUS: + dy = y + POND_RADIUS + return -50 + 100 * math.sin(dy * 0.008) + elif y > POND_RADIUS: + dy = y - POND_RADIUS + return 50 + 100 * math.sin(dy * 0.008) + else: + return (y / POND_RADIUS) * 50.0 + +def is_over_water(x, y): + dist_center = math.hypot(x, y) + if dist_center < POND_RADIUS - 50: + return True + if abs(y) >= POND_RADIUS: + cx = get_river_center(y) + if abs(x - cx) < 80: + return True + return False + +def get_random_water_position(): + while True: + if random.random() < 0.7: + rad = random.uniform(0, math.pi * 2) + r = random.uniform(100, POND_RADIUS - 150) + x = r * math.cos(rad) + y = r * math.sin(rad) + else: + y = random.choice([random.uniform(POND_RADIUS, STREAM_MAX_Y), random.uniform(STREAM_MIN_Y, -POND_RADIUS)]) + cx = get_river_center(y) + x = cx + random.uniform(-60, 60) + + if is_over_water(x, y): + z = random.uniform(5.0, 35.0) + return x, y, z + +# ========================================== +# CLASES DE INSECTOS +# ========================================== +class Insect: + def __init__(self, insect_type): + self.type = insect_type + self.x, self.y, self.z = get_random_water_position() + self.yaw = random.uniform(0, 360) + self.timer = random.uniform(0, 100) + + if self.type == "mosquito": + self.speed = 1.8 + self.size = 1.2 + else: + self.speed = 0.9 + self.size = 1.6 + + def update(self, player_pos): + self.timer += 0.05 + px, py, pz = player_pos + dist_to_player = math.hypot(self.x - px, self.y - py) + + if self.type == "mosquito": + if dist_to_player < 100: + escape_angle = math.degrees(math.atan2(self.y - py, self.x - px)) + self.yaw += (escape_angle - self.yaw) * 0.05 + + self.yaw += math.sin(self.timer * 3) * 1.5 + self.z += math.sin(self.timer * 1.5) * 0.2 + + elif self.type == "fly": + if dist_to_player < 120: + escape_angle = math.degrees(math.atan2(self.y - py, self.x - px)) + self.yaw = escape_angle + random.uniform(-45, 45) + self.z += random.choice([-1.5, 1.5]) + else: + if random.random() < 0.08: + self.yaw += random.uniform(-60, 60) + + self.yaw += math.sin(self.timer * 6) * 4.0 + self.z += math.cos(self.timer * 4) * 0.6 + + self.z = max(3.0, min(50.0, self.z)) + + rad = math.radians(self.yaw) + next_x = self.x - math.sin(rad) * self.speed + next_y = self.y + math.cos(rad) * self.speed + + if is_over_water(next_x, next_y): + self.x, self.y = next_x, next_y + else: + self.yaw += 140.0 + random.uniform(-20, 20) + + if self.y > STREAM_MAX_Y: + self.y -= WORLD_LEN + elif self.y < STREAM_MIN_Y: + self.y += WORLD_LEN + + def draw(self, attached=False): + if self.type == "mosquito": + glColor3f(*COLOR_MOSQUITO) + else: + glColor3f(*COLOR_FLY) + + glPushMatrix() + if not attached: + glTranslatef(self.x, self.y, self.z) + glRotatef(self.yaw, 0, 0, 1) + + s = self.size + glBegin(GL_LINES) + glVertex3f(0, s, 0) + glVertex3f(0, -s, 0) + w_offset = 0.0 if attached else math.sin(self.timer * 30) * (s * 0.8) + glVertex3f(0, 0, 0) + glVertex3f(-s * 1.5, 0, w_offset) + glVertex3f(0, 0, 0) + glVertex3f(s * 1.5, 0, w_offset) + glEnd() + + glPopMatrix() + +# ========================================== +# RENDERIZADO DE ESCENARIO Y MODELOS +# ========================================== +def draw_water_surface(time_counter): + glColor3f(*COLOR_WATER) + + grid_size = 30 + for x in range(int(-POND_RADIUS), int(POND_RADIUS), grid_size): + glBegin(GL_LINE_STRIP) + for y in range(int(-POND_RADIUS), int(POND_RADIUS), grid_size): + dist = math.hypot(x, y) + if dist < POND_RADIUS - 30: + z = 1.0 * math.sin(dist * 0.015 - time_counter * 3.0) + 0.5 * math.cos(x * 0.01 + time_counter) + glVertex3f(x, y, z) + glEnd() + + for y in range(int(STREAM_MIN_Y), int(STREAM_MAX_Y), 20): + if abs(y) < POND_RADIUS: + continue + cx = get_river_center(y) + glBegin(GL_LINE_STRIP) + for dx in range(-100, 110, 15): + x = cx + dx + z = 0.5 * math.sin(y * 0.03 + dx * 0.08 + time_counter * 4.0) + glVertex3f(x, y, z) + glEnd() + +def draw_terrain(): + glColor3f(*COLOR_LAND) + + grid_step = 120 + for y in range(int(STREAM_MIN_Y - 300), int(STREAM_MAX_Y + 300), grid_step): + glBegin(GL_LINE_STRIP) + for x in range(-2500, 2500, grid_step): + z = 1.5 * math.sin(x * 0.005) + 1.5 * math.cos(y * 0.005) + glVertex3f(x, y, z) + glEnd() + + width_river = 100.0 + margin_land = 70.0 + glColor3f(*COLOR_LAND_RIVER) + + for y in range(int(STREAM_MIN_Y), int(STREAM_MAX_Y), 30): + if abs(y) < POND_RADIUS: + continue + cx = get_river_center(y) + + glBegin(GL_LINE_STRIP) + for dx in range(int(-width_river - margin_land), int(-width_river), 15): + x = cx + dx + z = 0.6 * math.sin(y * 0.02 + x * 0.02) + 0.3 + glVertex3f(x, y, z) + glEnd() + + glBegin(GL_LINE_STRIP) + for dx in range(int(width_river), int(width_river + margin_land), 15): + x = cx + dx + z = 0.6 * math.sin(y * 0.02 + x * 0.02) + 0.3 + glVertex3f(x, y, z) + glEnd() + + for ix, iy, ir in ISLANDS: + glColor3f(*COLOR_ISLAND) + for layer in range(1, 4): + rad_layer = ir * (1 - layer * 0.25) + height = layer * 1.2 + glBegin(GL_LINE_LOOP) + for i in range(16): + angle = 2.0 * math.pi * i / 16 + dx = rad_layer * math.cos(angle) + dy = rad_layer * math.sin(angle) + glVertex3f(ix + dx, iy + dy, height) + glEnd() + +def draw_map(time_counter): + draw_terrain() + draw_water_surface(time_counter) + + glColor3f(*COLOR_REED) + glBegin(GL_LINES) + for rx, ry, rz, rh in REEDS: + glVertex3f(rx, ry, rz) + glVertex3f(rx, ry, rz + rh) + glVertex3f(rx, ry, rz) + glVertex3f(rx - 1.5, ry + 0.5, rz + rh * 0.7) + glVertex3f(rx, ry, rz) + glVertex3f(rx + 1.5, ry - 0.5, rz + rh * 0.8) + glEnd() + +def draw_dragonfly(wing_angle, caught_insect=None): + glColor3f(*COLOR_DRAGONFLY) + + glBegin(GL_LINES) + glVertex3f(0, 1.5, 0) + glVertex3f(0, -3.0, 0) + glVertex3f(-0.4, 0.6, 0) + glVertex3f(0.4, 0.6, 0) + glVertex3f(-0.3, -0.6, 0) + glVertex3f(0.3, -0.6, 0) + + glVertex3f(-0.3, 0.5, 0) + glVertex3f(-0.2, 0.2, -0.6) + glVertex3f(0.3, 0.5, 0) + glVertex3f(0.2, 0.2, -0.6) + glVertex3f(-0.2, -0.2, 0) + glVertex3f(-0.1, -0.3, -0.6) + glVertex3f(0.2, -0.2, 0) + glVertex3f(0.1, -0.3, -0.6) + glEnd() + + glBegin(GL_LINE_LOOP) + for i in range(8): + angle = 2.0 * math.pi * i / 8 + glVertex3f(0.3 * math.cos(angle), 1.4 + 0.3 * math.sin(angle), 0) + glEnd() + + rad_wing = math.radians(wing_angle) + w_z = math.sin(rad_wing) * 1.2 if wing_angle != 0 else 0.0 + + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(-4.5, 1.0, w_z) + glVertex3f(-3.8, 0.2, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(4.5, 1.0, w_z) + glVertex3f(3.8, 0.2, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(-3.8, -0.2, w_z) + glVertex3f(-3.0, -0.8, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(3.8, -0.2, w_z) + glVertex3f(3.0, -0.8, w_z) + glEnd() + + if caught_insect: + glPushMatrix() + glTranslatef(0.0, 0.2, -0.6) + caught_insect.draw(attached=True) + glPopMatrix() + +# ========================================== +# INTERFAZ 2D / RADAR Y TEXTOS FIJOS EN PANTALLA +# ========================================== +def draw_text_2d(text_surface, x, y): + """Renderiza texto de PyGame fijado en coordenadas 2D absolutas de pantalla.""" + text_data = pygame.image.tostring(text_surface, "RGBA", True) + w, h = text_surface.get_size() + + glEnable(GL_BLEND) + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) + + #glRasterPos utiliza la matriz 2D activa + glRasterPos2i(x, y) + glDrawPixels(w, h, GL_RGBA, GL_UNSIGNED_BYTE, text_data) + glDisable(GL_BLEND) + +def draw_hud(player_pos, yaw, insects, eaten_mosquitos, eaten_flies, font): + """Dibuja todo el HUD fijado en el plano 2D de la pantalla.""" + glDisable(GL_DEPTH_TEST) + + # 1. Configurar proyección 2D ortogonal estricta + glMatrixMode(GL_PROJECTION) + glPushMatrix() + glLoadIdentity() + gluOrtho2D(0, WIDTH, 0, HEIGHT) + + glMatrixMode(GL_MODELVIEW) + glPushMatrix() + glLoadIdentity() + + px, py, pz = player_pos + + # ---------------------------------- + # A. RADAR REDUCIDO (Centro Superior) + # ---------------------------------- + cx, cy = WIDTH // 2, HEIGHT - 35 + radius = 22 + + # Círculo del radar + glColor3f(*COLOR_HUD) + glBegin(GL_LINE_LOOP) + for i in range(24): + angle = 2.0 * math.pi * i / 24 + glVertex2f(cx + radius * math.cos(angle), cy + radius * math.sin(angle)) + glEnd() + + # Cruz + glBegin(GL_LINES) + glVertex2f(cx, cy + radius) + glVertex2f(cx, cy + radius - 5) + glVertex2f(cx, cy - radius) + glVertex2f(cx, cy - radius + 5) + glVertex2f(cx - radius, cy) + glVertex2f(cx - radius + 5, cy) + glVertex2f(cx + radius, cy) + glVertex2f(cx + radius - 5, cy) + glEnd() + + # Buscar presa cercana al frente + closest_insect = None + min_dist = 999999.0 + relative_angle = 0.0 + diff_z = 0.0 + + rad_yaw = math.radians(yaw) + + for insect in insects: + dx = insect.x - px + dy = insect.y - py + dist = math.hypot(dx, dy) + + if dist < min_dist and dist > 0.1: + angle_to_insect = math.degrees(math.atan2(dy, dx)) + heading = (angle_to_insect - (90 - yaw)) % 360 + if heading > 180: + heading -= 360 + + if abs(heading) < 60: + min_dist = dist + closest_insect = insect + relative_angle = heading + diff_z = insect.z - pz + + # Círculo blanco genérico en radar + if closest_insect: + radar_x = cx - (relative_angle / 60.0) * (radius - 4) + radar_y = cy + (diff_z / 20.0) * (radius - 4) + + dist_from_center = math.hypot(radar_x - cx, radar_y - cy) + if dist_from_center > (radius - 3): + factor = (radius - 3) / dist_from_center + radar_x = cx + (radar_x - cx) * factor + radar_y = cy + (radar_y - cy) * factor + + glColor3f(1.0, 1.0, 1.0) + glBegin(GL_LINE_LOOP) + for i in range(8): + ang = 2.0 * math.pi * i / 8 + glVertex2f(radar_x + 3 * math.cos(ang), radar_y + 3 * math.sin(ang)) + glEnd() + + # ---------------------------------- + # B. ÍCONOS SUPERIORES FIJOS + # ---------------------------------- + # Ícono Mosquito (Izquierda - Amarillo) + glColor3f(*COLOR_MOSQUITO) + glBegin(GL_LINES) + glVertex2f(25, HEIGHT - 20) + glVertex2f(25, HEIGHT - 40) + glVertex2f(25, HEIGHT - 30) + glVertex2f(12, HEIGHT - 30) + glVertex2f(25, HEIGHT - 30) + glVertex2f(38, HEIGHT - 30) + glEnd() + + # Ícono Mosca (Derecha - Rojo/Marrón) + glColor3f(*COLOR_FLY) + glBegin(GL_LINES) + glVertex2f(WIDTH - 85, HEIGHT - 20) + glVertex2f(WIDTH - 85, HEIGHT - 40) + glVertex2f(WIDTH - 85, HEIGHT - 30) + glVertex2f(WIDTH - 98, HEIGHT - 30) + glVertex2f(WIDTH - 85, HEIGHT - 30) + glVertex2f(WIDTH - 72, HEIGHT - 30) + glEnd() + + # ---------------------------------- + # C. RENDERIZADO DE TEXTOS 2D FIJOS + # ---------------------------------- + txt_mosq = font.render(f"x {eaten_mosquitos}", True, (255, 230, 0)) + txt_flies = font.render(f"x {eaten_flies}", True, (200, 65, 40)) + + # Posicionar texto exacto en coordenadas de pantalla 2D + draw_text_2d(txt_mosq, 45, HEIGHT - 40) + draw_text_2d(txt_flies, WIDTH - 65, HEIGHT - 40) + + # Restaurar matrices + glMatrixMode(GL_PROJECTION) + glPopMatrix() + glMatrixMode(GL_MODELVIEW) + glPopMatrix() + glEnable(GL_DEPTH_TEST) + +# ========================================== +# BUCLE PRINCIPAL +# ========================================== +def main(): + pygame.init() + pygame.font.init() + font = pygame.font.SysFont("Arial", 22, bold=True) + + pygame.display.set_mode((WIDTH, HEIGHT), DOUBLEBUF | OPENGL) + pygame.display.set_caption("Simulador de Vuelo de Libélula 3D") + + glEnable(GL_DEPTH_TEST) + glClearColor(*COLOR_BG, 1.0) + + glMatrixMode(GL_PROJECTION) + gluPerspective(50, (WIDTH / HEIGHT), 0.1, 5000.0) + glMatrixMode(GL_MODELVIEW) + + # Estado de la Libélula + px, py, pz = 0.0, 0.0, 15.0 + yaw = 0.0 + roll = 0.0 + pitch = 0.0 + max_roll = 30.0 + max_pitch = 20.0 + + speed = 0.0 + max_speed = 3.0 + acceleration = 0.06 + deceleration = 0.03 + + min_altitude = 1.0 + max_altitude = 120.0 + + wing_timer = 0.0 + water_timer = 0.0 + is_perched = False + + # Estado de Caza + caught_insect = None + eaten_mosquitos = 0 + eaten_flies = 0 + + # Inicializar Insectos + insects = [] + num_mosquitos = random.randint(MOSQUITO_MIN, MOSQUITO_MAX) + num_flies = random.randint(FLY_MIN, FLY_MAX) + + for _ in range(num_mosquitos): + insects.append(Insect("mosquito")) + for _ in range(num_flies): + insects.append(Insect("fly")) + + clock = pygame.time.Clock() + + running = True + while running: + dt = clock.tick(60) / 1000.0 + water_timer += dt + + for event in pygame.event.get(): + if event.type == QUIT or (event.type == KEYDOWN and event.key == K_ESCAPE): + running = False + + keys = pygame.key.get_pressed() + + # CONTROLES Y MOVIMIENTO DE LA LIBÉLULA + if is_perched: + if keys[K_LEFT]: + yaw += 3.0 + if keys[K_RIGHT]: + yaw -= 3.0 + + if keys[K_UP]: + is_perched = False + speed = 0.5 + + wing_angle = 0.0 + pitch = 0.0 + roll = 0.0 + else: + if keys[K_UP]: + speed = min(speed + acceleration, max_speed) + elif keys[K_DOWN]: + speed = max(speed - deceleration, 0.0) + + if speed < 0.3: + for rx, ry, rz, rh in REEDS: + dist = math.hypot(px - rx, py - ry) + if dist < 8.0 and abs(pz - (rz + rh)) < 8.0: + px, py, pz = rx, ry, rz + rh + speed = 0.0 + roll = 0.0 + pitch = 0.0 + is_perched = True + + if caught_insect: + if caught_insect.type == "mosquito": + eaten_mosquitos += 1 + else: + eaten_flies += 1 + caught_insect = None + break + + target_pitch = 0.0 + if keys[K_w]: + pz = min(pz + 0.8, max_altitude) + target_pitch = max_pitch + elif keys[K_s]: + pz = max(pz - 0.8, min_altitude) + target_pitch = -max_pitch + + pitch += (target_pitch - pitch) * 0.15 + + target_roll = 0.0 + if keys[K_LEFT]: + yaw += 2.8 + target_roll = -max_roll + elif keys[K_RIGHT]: + yaw -= 2.8 + target_roll = max_roll + + roll += (target_roll - roll) * 0.15 + + rad_yaw = math.radians(yaw) + px += -math.sin(rad_yaw) * speed + py += math.cos(rad_yaw) * speed + + if abs(py) < POND_RADIUS: + max_limit_x = POND_RADIUS + 200.0 + else: + cx = get_river_center(py) + max_limit_x = abs(cx) + 200.0 + + px = max(-max_limit_x, min(max_limit_x, px)) + + if py > STREAM_MAX_Y: + py -= WORLD_LEN + elif py < STREAM_MIN_Y: + py += WORLD_LEN + + wing_timer += 60.0 if speed > 0.1 else 30.0 + wing_angle = math.sin(wing_timer) * 35.0 + + # ACTUALIZACIÓN DE INSECTOS + current_mosquitos = 0 + current_flies = 0 + + for insect in insects[:]: + insect.update((px, py, pz)) + + if insect.type == "mosquito": + current_mosquitos += 1 + else: + current_flies += 1 + + if not caught_insect: + dist_to_dragonfly = math.sqrt((px - insect.x)**2 + (py - insect.y)**2 + (pz - insect.z)**2) + if dist_to_dragonfly < 6.0: + caught_insect = insect + insects.remove(insect) + + if current_mosquitos < MOSQUITO_MIN: + insects.append(Insect("mosquito")) + if current_flies < FLY_MIN: + insects.append(Insect("fly")) + + # RENDERIZADO + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) + glLoadIdentity() + + cam_dist = 18.0 + cam_height = 8.0 + rad_yaw = math.radians(yaw) + + cam_x = px + math.sin(rad_yaw) * cam_dist + cam_y = py - math.cos(rad_yaw) * cam_dist + cam_z = pz + cam_height + + gluLookAt(cam_x, cam_y, cam_z, + px, py, pz + 0.5, + 0, 0, 1) + + draw_map(water_timer) + + for insect in insects: + insect.draw() + + glPushMatrix() + glTranslatef(px, py, pz) + glRotatef(yaw, 0, 0, 1) + glRotatef(pitch, 1, 0, 0) + glRotatef(roll, 0, 1, 0) + draw_dragonfly(wing_angle, caught_insect) + glPopMatrix() + + # DIBUJAR INTERFAZ (HUD) Y TEXTOS FIJOS EN 2D + draw_hud((px, py, pz), yaw, insects, eaten_mosquitos, eaten_flies, font) + + pygame.display.flip() + + pygame.quit() + sys.exit() + +if __name__ == "__main__": + main() diff --git a/previos/dragonfly_game_v5.3.py b/previos/dragonfly_game_v5.3.py new file mode 100644 index 0000000..5e7312e --- /dev/null +++ b/previos/dragonfly_game_v5.3.py @@ -0,0 +1,649 @@ +import sys +import math +import random +import pygame +from pygame.locals import * +from OpenGL.GL import * +from OpenGL.GLU import * + +# ========================================== +# CONFIGURACIÓN Y CONSTANTES +# ========================================== +WIDTH, HEIGHT = 800, 600 + +# Colores (R, G, B) +COLOR_BG = (0.12, 0.15, 0.12) # Fondo ambiente +COLOR_WATER = (0.0, 0.5, 0.85) # Agua animada (Cian/Azul) +COLOR_LAND = (0.45, 0.32, 0.15) # Tierra / Relieve +COLOR_LAND_RIVER = (0.40, 0.28, 0.12) # Tierra en riachuelos +COLOR_ISLAND = (0.5, 0.38, 0.18) # Islas +COLOR_REED = (0.15, 0.85, 0.25) # Juncos verdes +COLOR_DRAGONFLY = (0.0, 1.0, 1.0) # Libélula alámbrica (Cian) +COLOR_MOSQUITO = (1.0, 0.9, 0.0) # Mosquitos (Amarillo) +COLOR_FLY = (0.8, 0.25, 0.15) # Moscas (Rojo-Marrón) +COLOR_HUD = (1.0, 1.0, 1.0) # Retícula HUD (Blanco) + +# Dimensiones del Mundo +POND_RADIUS = 1800.0 +STREAM_LEN = 900.0 +STREAM_MIN_Y = -(POND_RADIUS + STREAM_LEN) +STREAM_MAX_Y = (POND_RADIUS + STREAM_LEN) +WORLD_LEN = STREAM_MAX_Y - STREAM_MIN_Y + +# Cantidades de insectos +MOSQUITO_MIN, MOSQUITO_MAX = 10, 20 +FLY_MIN, FLY_MAX = 2, 7 + +# ========================================== +# ELEMENTOS DEL MAPA +# ========================================== +REEDS = [ + (-250, 350, 0, 20), (-160, -500, 0, 22), (500, 200, 0, 24), (-700, 200, 0, 21), (200, -800, 0, 23), + (-900, -300, 0, 25), (760, -600, 0, 22), (900, 500, 0, 26), (-500, 900, 0, 23), (600, -1000, 0, 22), + (-1200, 400, 0, 24), (1300, -300, 0, 20), (-1400, -700, 0, 25), (1100, 1200, 0, 23), (-360, -1300, 0, 22), + (-90, -1950, 0, 20), (-110, -2150, 0, 23), (-80, -2350, 0, 19), (-100, -2550, 0, 22), + (90, 1950, 0, 22), (110, 2150, 0, 20), (85, 2350, 0, 24), (105, 2550, 0, 21) +] + +ISLANDS = [ + (-250, 350, 90), (-160, -500, 85), (500, 200, 100), (-700, 200, 110), (200, -800, 95), + (-900, -300, 120), (760, -600, 90), (900, 500, 105), (-500, 900, 115), (600, -1000, 100) +] + +def get_river_center(y): + if y < -POND_RADIUS: + dy = y + POND_RADIUS + return -50 + 100 * math.sin(dy * 0.008) + elif y > POND_RADIUS: + dy = y - POND_RADIUS + return 50 + 100 * math.sin(dy * 0.008) + else: + return (y / POND_RADIUS) * 50.0 + +def is_over_water(x, y): + dist_center = math.hypot(x, y) + if dist_center < POND_RADIUS - 50: + return True + if abs(y) >= POND_RADIUS: + cx = get_river_center(y) + if abs(x - cx) < 80: + return True + return False + +def get_random_water_position(): + while True: + if random.random() < 0.7: + rad = random.uniform(0, math.pi * 2) + r = random.uniform(100, POND_RADIUS - 150) + x = r * math.cos(rad) + y = r * math.sin(rad) + else: + y = random.choice([random.uniform(POND_RADIUS, STREAM_MAX_Y), random.uniform(STREAM_MIN_Y, -POND_RADIUS)]) + cx = get_river_center(y) + x = cx + random.uniform(-60, 60) + + if is_over_water(x, y): + z = random.uniform(5.0, 35.0) + return x, y, z + +# ========================================== +# CLASES DE INSECTOS +# ========================================== +class Insect: + def __init__(self, insect_type): + self.type = insect_type + self.x, self.y, self.z = get_random_water_position() + self.yaw = random.uniform(0, 360) + self.timer = random.uniform(0, 100) + + if self.type == "mosquito": + self.speed = 1.8 + self.size = 1.2 + else: # Fly + self.speed = 0.9 + self.size = 1.6 + + def update(self, player_pos): + self.timer += 0.05 + px, py, pz = player_pos + dist_to_player = math.hypot(self.x - px, self.y - py) + + if self.type == "mosquito": + # VENTAJA TÁCTICA: Si la libélula vuela por encima de ellos (pz > self.z), + # los mosquitos NO la detectan y no intentan huir. + can_detect = (pz <= self.z) + + if can_detect and dist_to_player < 100: + escape_angle = math.degrees(math.atan2(self.y - py, self.x - px)) + self.yaw += (escape_angle - self.yaw) * 0.05 + + self.yaw += math.sin(self.timer * 3) * 1.5 + self.z += math.sin(self.timer * 1.5) * 0.2 + + elif self.type == "fly": + # Las moscas sí detectan siempre a la libélula desde cualquier ángulo/altura + if dist_to_player < 120: + escape_angle = math.degrees(math.atan2(self.y - py, self.x - px)) + self.yaw = escape_angle + random.uniform(-45, 45) + self.z += random.choice([-1.5, 1.5]) + else: + if random.random() < 0.08: + self.yaw += random.uniform(-60, 60) + + self.yaw += math.sin(self.timer * 6) * 4.0 + self.z += math.cos(self.timer * 4) * 0.6 + + self.z = max(3.0, min(50.0, self.z)) + + rad = math.radians(self.yaw) + next_x = self.x - math.sin(rad) * self.speed + next_y = self.y + math.cos(rad) * self.speed + + if is_over_water(next_x, next_y): + self.x, self.y = next_x, next_y + else: + self.yaw += 140.0 + random.uniform(-20, 20) + + if self.y > STREAM_MAX_Y: + self.y -= WORLD_LEN + elif self.y < STREAM_MIN_Y: + self.y += WORLD_LEN + + def draw(self, attached=False): + if self.type == "mosquito": + glColor3f(*COLOR_MOSQUITO) + else: + glColor3f(*COLOR_FLY) + + glPushMatrix() + if not attached: + glTranslatef(self.x, self.y, self.z) + glRotatef(self.yaw, 0, 0, 1) + + s = self.size + glBegin(GL_LINES) + glVertex3f(0, s, 0) + glVertex3f(0, -s, 0) + w_offset = 0.0 if attached else math.sin(self.timer * 30) * (s * 0.8) + glVertex3f(0, 0, 0) + glVertex3f(-s * 1.5, 0, w_offset) + glVertex3f(0, 0, 0) + glVertex3f(s * 1.5, 0, w_offset) + glEnd() + + glPopMatrix() + +# ========================================== +# RENDERIZADO DE ESCENARIO Y MODELOS +# ========================================== +def draw_water_surface(time_counter): + glColor3f(*COLOR_WATER) + + grid_size = 30 + for x in range(int(-POND_RADIUS), int(POND_RADIUS), grid_size): + glBegin(GL_LINE_STRIP) + for y in range(int(-POND_RADIUS), int(POND_RADIUS), grid_size): + dist = math.hypot(x, y) + if dist < POND_RADIUS - 30: + z = 1.0 * math.sin(dist * 0.015 - time_counter * 3.0) + 0.5 * math.cos(x * 0.01 + time_counter) + glVertex3f(x, y, z) + glEnd() + + for y in range(int(STREAM_MIN_Y), int(STREAM_MAX_Y), 20): + if abs(y) < POND_RADIUS: + continue + cx = get_river_center(y) + glBegin(GL_LINE_STRIP) + for dx in range(-100, 110, 15): + x = cx + dx + z = 0.5 * math.sin(y * 0.03 + dx * 0.08 + time_counter * 4.0) + glVertex3f(x, y, z) + glEnd() + +def draw_terrain(): + glColor3f(*COLOR_LAND) + + grid_step = 120 + for y in range(int(STREAM_MIN_Y - 300), int(STREAM_MAX_Y + 300), grid_step): + glBegin(GL_LINE_STRIP) + for x in range(-2500, 2500, grid_step): + z = 1.5 * math.sin(x * 0.005) + 1.5 * math.cos(y * 0.005) + glVertex3f(x, y, z) + glEnd() + + width_river = 100.0 + margin_land = 70.0 + glColor3f(*COLOR_LAND_RIVER) + + for y in range(int(STREAM_MIN_Y), int(STREAM_MAX_Y), 30): + if abs(y) < POND_RADIUS: + continue + cx = get_river_center(y) + + glBegin(GL_LINE_STRIP) + for dx in range(int(-width_river - margin_land), int(-width_river), 15): + x = cx + dx + z = 0.6 * math.sin(y * 0.02 + x * 0.02) + 0.3 + glVertex3f(x, y, z) + glEnd() + + glBegin(GL_LINE_STRIP) + for dx in range(int(width_river), int(width_river + margin_land), 15): + x = cx + dx + z = 0.6 * math.sin(y * 0.02 + x * 0.02) + 0.3 + glVertex3f(x, y, z) + glEnd() + + for ix, iy, ir in ISLANDS: + glColor3f(*COLOR_ISLAND) + for layer in range(1, 4): + rad_layer = ir * (1 - layer * 0.25) + height = layer * 1.2 + glBegin(GL_LINE_LOOP) + for i in range(16): + angle = 2.0 * math.pi * i / 16 + dx = rad_layer * math.cos(angle) + dy = rad_layer * math.sin(angle) + glVertex3f(ix + dx, iy + dy, height) + glEnd() + +def draw_map(time_counter): + draw_terrain() + draw_water_surface(time_counter) + + glColor3f(*COLOR_REED) + glBegin(GL_LINES) + for rx, ry, rz, rh in REEDS: + glVertex3f(rx, ry, rz) + glVertex3f(rx, ry, rz + rh) + glVertex3f(rx, ry, rz) + glVertex3f(rx - 1.5, ry + 0.5, rz + rh * 0.7) + glVertex3f(rx, ry, rz) + glVertex3f(rx + 1.5, ry - 0.5, rz + rh * 0.8) + glEnd() + +def draw_dragonfly(wing_angle, caught_insect=None): + glColor3f(*COLOR_DRAGONFLY) + + glBegin(GL_LINES) + glVertex3f(0, 1.5, 0) + glVertex3f(0, -3.0, 0) + glVertex3f(-0.4, 0.6, 0) + glVertex3f(0.4, 0.6, 0) + glVertex3f(-0.3, -0.6, 0) + glVertex3f(0.3, -0.6, 0) + + glVertex3f(-0.3, 0.5, 0) + glVertex3f(-0.2, 0.2, -0.6) + glVertex3f(0.3, 0.5, 0) + glVertex3f(0.2, 0.2, -0.6) + glVertex3f(-0.2, -0.2, 0) + glVertex3f(-0.1, -0.3, -0.6) + glVertex3f(0.2, -0.2, 0) + glVertex3f(0.1, -0.3, -0.6) + glEnd() + + glBegin(GL_LINE_LOOP) + for i in range(8): + angle = 2.0 * math.pi * i / 8 + glVertex3f(0.3 * math.cos(angle), 1.4 + 0.3 * math.sin(angle), 0) + glEnd() + + rad_wing = math.radians(wing_angle) + w_z = math.sin(rad_wing) * 1.2 if wing_angle != 0 else 0.0 + + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(-4.5, 1.0, w_z) + glVertex3f(-3.8, 0.2, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(4.5, 1.0, w_z) + glVertex3f(3.8, 0.2, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(-3.8, -0.2, w_z) + glVertex3f(-3.0, -0.8, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(3.8, -0.2, w_z) + glVertex3f(3.0, -0.8, w_z) + glEnd() + + if caught_insect: + glPushMatrix() + glTranslatef(0.0, 0.2, -0.6) + caught_insect.draw(attached=True) + glPopMatrix() + +# ========================================== +# INTERFAZ 2D / RADAR Y TEXTOS FIJOS +# ========================================== +def draw_text_2d(text_surface, x, y): + text_data = pygame.image.tostring(text_surface, "RGBA", True) + w, h = text_surface.get_size() + + glEnable(GL_BLEND) + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) + + glRasterPos2i(x, y) + glDrawPixels(w, h, GL_RGBA, GL_UNSIGNED_BYTE, text_data) + glDisable(GL_BLEND) + +def draw_hud(player_pos, yaw, insects, eaten_mosquitos, eaten_flies, font): + glDisable(GL_DEPTH_TEST) + + glMatrixMode(GL_PROJECTION) + glPushMatrix() + glLoadIdentity() + gluOrtho2D(0, WIDTH, 0, HEIGHT) + + glMatrixMode(GL_MODELVIEW) + glPushMatrix() + glLoadIdentity() + + px, py, pz = player_pos + + # RADAR REDUCIDO + cx, cy = WIDTH // 2, HEIGHT - 35 + radius = 22 + + glColor3f(*COLOR_HUD) + glBegin(GL_LINE_LOOP) + for i in range(24): + angle = 2.0 * math.pi * i / 24 + glVertex2f(cx + radius * math.cos(angle), cy + radius * math.sin(angle)) + glEnd() + + glBegin(GL_LINES) + glVertex2f(cx, cy + radius) + glVertex2f(cx, cy + radius - 5) + glVertex2f(cx, cy - radius) + glVertex2f(cx, cy - radius + 5) + glVertex2f(cx - radius, cy) + glVertex2f(cx - radius + 5, cy) + glVertex2f(cx + radius, cy) + glVertex2f(cx + radius - 5, cy) + glEnd() + + closest_insect = None + min_dist = 999999.0 + relative_angle = 0.0 + diff_z = 0.0 + + rad_yaw = math.radians(yaw) + + for insect in insects: + dx = insect.x - px + dy = insect.y - py + dist = math.hypot(dx, dy) + + if dist < min_dist and dist > 0.1: + angle_to_insect = math.degrees(math.atan2(dy, dx)) + heading = (angle_to_insect - (90 - yaw)) % 360 + if heading > 180: + heading -= 360 + + if abs(heading) < 60: + min_dist = dist + closest_insect = insect + relative_angle = heading + diff_z = insect.z - pz + + if closest_insect: + radar_x = cx - (relative_angle / 60.0) * (radius - 4) + radar_y = cy + (diff_z / 20.0) * (radius - 4) + + dist_from_center = math.hypot(radar_x - cx, radar_y - cy) + if dist_from_center > (radius - 3): + factor = (radius - 3) / dist_from_center + radar_x = cx + (radar_x - cx) * factor + radar_y = cy + (radar_y - cy) * factor + + glColor3f(1.0, 1.0, 1.0) + glBegin(GL_LINE_LOOP) + for i in range(8): + ang = 2.0 * math.pi * i / 8 + glVertex2f(radar_x + 3 * math.cos(ang), radar_y + 3 * math.sin(ang)) + glEnd() + + # ÍCONOS SUPERIORES + glColor3f(*COLOR_MOSQUITO) + glBegin(GL_LINES) + glVertex2f(25, HEIGHT - 20) + glVertex2f(25, HEIGHT - 40) + glVertex2f(25, HEIGHT - 30) + glVertex2f(12, HEIGHT - 30) + glVertex2f(25, HEIGHT - 30) + glVertex2f(38, HEIGHT - 30) + glEnd() + + glColor3f(*COLOR_FLY) + glBegin(GL_LINES) + glVertex2f(WIDTH - 85, HEIGHT - 20) + glVertex2f(WIDTH - 85, HEIGHT - 40) + glVertex2f(WIDTH - 85, HEIGHT - 30) + glVertex2f(WIDTH - 98, HEIGHT - 30) + glVertex2f(WIDTH - 85, HEIGHT - 30) + glVertex2f(WIDTH - 32, HEIGHT - 30) + glEnd() + + # TEXTOS + txt_mosq = font.render(f"x {eaten_mosquitos}", True, (255, 230, 0)) + txt_flies = font.render(f"x {eaten_flies}", True, (200, 65, 40)) + + draw_text_2d(txt_mosq, 45, HEIGHT - 40) + draw_text_2d(txt_flies, WIDTH - 65, HEIGHT - 40) + + glMatrixMode(GL_PROJECTION) + glPopMatrix() + glMatrixMode(GL_MODELVIEW) + glPopMatrix() + glEnable(GL_DEPTH_TEST) + +# ========================================== +# BUCLE PRINCIPAL +# ========================================== +def main(): + pygame.init() + pygame.font.init() + font = pygame.font.SysFont("Arial", 22, bold=True) + + pygame.display.set_mode((WIDTH, HEIGHT), DOUBLEBUF | OPENGL) + pygame.display.set_caption("Simulador de Vuelo de Libélula 3D") + + glEnable(GL_DEPTH_TEST) + glClearColor(*COLOR_BG, 1.0) + + glMatrixMode(GL_PROJECTION) + gluPerspective(50, (WIDTH / HEIGHT), 0.1, 5000.0) + glMatrixMode(GL_MODELVIEW) + + # Estado de la Libélula + px, py, pz = 0.0, 0.0, 15.0 + yaw = 0.0 + roll = 0.0 + pitch = 0.0 + max_roll = 30.0 + max_pitch = 20.0 + + speed = 0.0 + max_speed = 3.0 + acceleration = 0.06 + deceleration = 0.03 + + min_altitude = 1.0 + max_altitude = 120.0 + + wing_timer = 0.0 + water_timer = 0.0 + is_perched = False + + caught_insect = None + eaten_mosquitos = 0 + eaten_flies = 0 + + insects = [] + num_mosquitos = random.randint(MOSQUITO_MIN, MOSQUITO_MAX) + num_flies = random.randint(FLY_MIN, FLY_MAX) + + for _ in range(num_mosquitos): + insects.append(Insect("mosquito")) + for _ in range(num_flies): + insects.append(Insect("fly")) + + clock = pygame.time.Clock() + + running = True + while running: + dt = clock.tick(60) / 1000.0 + water_timer += dt + + for event in pygame.event.get(): + if event.type == QUIT or (event.type == KEYDOWN and event.key == K_ESCAPE): + running = False + + keys = pygame.key.get_pressed() + + if is_perched: + if keys[K_LEFT]: + yaw += 3.0 + if keys[K_RIGHT]: + yaw -= 3.0 + + if keys[K_UP]: + is_perched = False + speed = 0.5 + + wing_angle = 0.0 + pitch = 0.0 + roll = 0.0 + else: + if keys[K_UP]: + speed = min(speed + acceleration, max_speed) + elif keys[K_DOWN]: + speed = max(speed - deceleration, 0.0) + + if speed < 0.3: + for rx, ry, rz, rh in REEDS: + dist = math.hypot(px - rx, py - ry) + if dist < 8.0 and abs(pz - (rz + rh)) < 8.0: + px, py, pz = rx, ry, rz + rh + speed = 0.0 + roll = 0.0 + pitch = 0.0 + is_perched = True + + if caught_insect: + if caught_insect.type == "mosquito": + eaten_mosquitos += 1 + else: + eaten_flies += 1 + caught_insect = None + break + + target_pitch = 0.0 + if keys[K_w]: + pz = min(pz + 0.8, max_altitude) + target_pitch = max_pitch + elif keys[K_s]: + pz = max(pz - 0.8, min_altitude) + target_pitch = -max_pitch + + pitch += (target_pitch - pitch) * 0.15 + + target_roll = 0.0 + if keys[K_LEFT]: + yaw += 2.8 + target_roll = -max_roll + elif keys[K_RIGHT]: + yaw -= 2.8 + target_roll = max_roll + + roll += (target_roll - roll) * 0.15 + + rad_yaw = math.radians(yaw) + px += -math.sin(rad_yaw) * speed + py += math.cos(rad_yaw) * speed + + if abs(py) < POND_RADIUS: + max_limit_x = POND_RADIUS + 200.0 + else: + cx = get_river_center(py) + max_limit_x = abs(cx) + 200.0 + + px = max(-max_limit_x, min(max_limit_x, px)) + + if py > STREAM_MAX_Y: + py -= WORLD_LEN + elif py < STREAM_MIN_Y: + py += WORLD_LEN + + wing_timer += 60.0 if speed > 0.1 else 30.0 + wing_angle = math.sin(wing_timer) * 35.0 + + current_mosquitos = 0 + current_flies = 0 + + for insect in insects[:]: + insect.update((px, py, pz)) + + if insect.type == "mosquito": + current_mosquitos += 1 + else: + current_flies += 1 + + if not caught_insect: + dist_to_dragonfly = math.sqrt((px - insect.x)**2 + (py - insect.y)**2 + (pz - insect.z)**2) + if dist_to_dragonfly < 6.0: + caught_insect = insect + insects.remove(insect) + + if current_mosquitos < MOSQUITO_MIN: + insects.append(Insect("mosquito")) + if current_flies < FLY_MIN: + insects.append(Insect("fly")) + + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) + glLoadIdentity() + + cam_dist = 18.0 + cam_height = 8.0 + rad_yaw = math.radians(yaw) + + cam_x = px + math.sin(rad_yaw) * cam_dist + cam_y = py - math.cos(rad_yaw) * cam_dist + cam_z = pz + cam_height + + gluLookAt(cam_x, cam_y, cam_z, + px, py, pz + 0.5, + 0, 0, 1) + + draw_map(water_timer) + + for insect in insects: + insect.draw() + + glPushMatrix() + glTranslatef(px, py, pz) + glRotatef(yaw, 0, 0, 1) + glRotatef(pitch, 1, 0, 0) + glRotatef(roll, 0, 1, 0) + draw_dragonfly(wing_angle, caught_insect) + glPopMatrix() + + draw_hud((px, py, pz), yaw, insects, eaten_mosquitos, eaten_flies, font) + + pygame.display.flip() + + pygame.quit() + sys.exit() + +if __name__ == "__main__": + main() diff --git a/previos/dragonfly_game_v5.4.py b/previos/dragonfly_game_v5.4.py new file mode 100644 index 0000000..702c37b --- /dev/null +++ b/previos/dragonfly_game_v5.4.py @@ -0,0 +1,676 @@ +import sys +import math +import random +import pygame +from pygame.locals import * +from OpenGL.GL import * +from OpenGL.GLU import * + +# ========================================== +# CONFIGURACIÓN Y CONSTANTES +# ========================================== +WIDTH, HEIGHT = 800, 600 + +# Colores (R, G, B) +COLOR_BG = (0.12, 0.15, 0.12) # Fondo ambiente +COLOR_WATER = (0.0, 0.5, 0.85) # Agua animada (Cian/Azul) +COLOR_LAND = (0.45, 0.32, 0.15) # Tierra / Relieve +COLOR_LAND_RIVER = (0.40, 0.28, 0.12) # Tierra en riachuelos +COLOR_ISLAND = (0.5, 0.38, 0.18) # Islas +COLOR_REED = (0.15, 0.85, 0.25) # Juncos verdes +COLOR_DRAGONFLY = (0.0, 1.0, 1.0) # Libélula alámbrica (Cian) +COLOR_MOSQUITO = (1.0, 0.9, 0.0) # Mosquitos (Amarillo) +COLOR_FLY = (0.8, 0.25, 0.15) # Moscas (Rojo-Marrón) +COLOR_HUD = (1.0, 1.0, 1.0) # Retícula HUD (Blanco) + +# Dimensiones del Mundo +POND_RADIUS = 1800.0 +STREAM_LEN = 900.0 +STREAM_MIN_Y = -(POND_RADIUS + STREAM_LEN) +STREAM_MAX_Y = (POND_RADIUS + STREAM_LEN) +WORLD_LEN = STREAM_MAX_Y - STREAM_MIN_Y + +# Cantidades de insectos +MOSQUITO_MIN, MOSQUITO_MAX = 10, 20 +FLY_MIN, FLY_MAX = 2, 7 + +# ========================================== +# ELEMENTOS DEL MAPA +# ========================================== +REEDS = [ + (-250, 350, 0, 20), (-160, -500, 0, 22), (500, 200, 0, 24), (-700, 200, 0, 21), (200, -800, 0, 23), + (-900, -300, 0, 25), (760, -600, 0, 22), (900, 500, 0, 26), (-500, 900, 0, 23), (600, -1000, 0, 22), + (-1200, 400, 0, 24), (1300, -300, 0, 20), (-1400, -700, 0, 25), (1100, 1200, 0, 23), (-360, -1300, 0, 22), + (-90, -1950, 0, 20), (-110, -2150, 0, 23), (-80, -2350, 0, 19), (-100, -2550, 0, 22), + (90, 1950, 0, 22), (110, 2150, 0, 20), (85, 2350, 0, 24), (105, 2550, 0, 21) +] + +ISLANDS = [ + (-250, 350, 90), (-160, -500, 85), (500, 200, 100), (-700, 200, 110), (200, -800, 95), + (-900, -300, 120), (760, -600, 90), (900, 500, 105), (-500, 900, 115), (600, -1000, 100) +] + +def get_river_center(y): + if y < -POND_RADIUS: + dy = y + POND_RADIUS + return -50 + 100 * math.sin(dy * 0.008) + elif y > POND_RADIUS: + dy = y - POND_RADIUS + return 50 + 100 * math.sin(dy * 0.008) + else: + return (y / POND_RADIUS) * 50.0 + +def is_over_water(x, y): + dist_center = math.hypot(x, y) + if dist_center < POND_RADIUS - 50: + return True + if abs(y) >= POND_RADIUS: + cx = get_river_center(y) + if abs(x - cx) < 80: + return True + return False + +def get_random_water_position(): + while True: + if random.random() < 0.7: + rad = random.uniform(0, math.pi * 2) + r = random.uniform(100, POND_RADIUS - 150) + x = r * math.cos(rad) + y = r * math.sin(rad) + else: + y = random.choice([random.uniform(POND_RADIUS, STREAM_MAX_Y), random.uniform(STREAM_MIN_Y, -POND_RADIUS)]) + cx = get_river_center(y) + x = cx + random.uniform(-60, 60) + + if is_over_water(x, y): + z = random.uniform(5.0, 35.0) + return x, y, z + +# ========================================== +# CLASES DE INSECTOS +# ========================================== +class Insect: + def __init__(self, insect_type): + self.type = insect_type + self.x, self.y, self.z = get_random_water_position() + self.yaw = random.uniform(0, 360) + self.timer = random.uniform(0, 100) + + if self.type == "mosquito": + self.speed = 1.8 + self.size = 1.2 + else: # Fly + self.speed = 0.9 + self.size = 1.6 + + def update(self, player_pos): + self.timer += 0.05 + px, py, pz = player_pos + dist_to_player = math.hypot(self.x - px, self.y - py) + + if self.type == "mosquito": + can_detect = (pz <= self.z) + + if can_detect and dist_to_player < 100: + escape_angle = math.degrees(math.atan2(self.y - py, self.x - px)) + self.yaw += (escape_angle - self.yaw) * 0.05 + + self.yaw += math.sin(self.timer * 3) * 1.5 + self.z += math.sin(self.timer * 1.5) * 0.2 + + elif self.type == "fly": + if dist_to_player < 120: + escape_angle = math.degrees(math.atan2(self.y - py, self.x - px)) + self.yaw = escape_angle + random.uniform(-45, 45) + self.z += random.choice([-1.5, 1.5]) + else: + if random.random() < 0.08: + self.yaw += random.uniform(-60, 60) + + self.yaw += math.sin(self.timer * 6) * 4.0 + self.z += math.cos(self.timer * 4) * 0.6 + + self.z = max(3.0, min(50.0, self.z)) + + rad = math.radians(self.yaw) + next_x = self.x - math.sin(rad) * self.speed + next_y = self.y + math.cos(rad) * self.speed + + if is_over_water(next_x, next_y): + self.x, self.y = next_x, next_y + else: + self.yaw += 140.0 + random.uniform(-20, 20) + + if self.y > STREAM_MAX_Y: + self.y -= WORLD_LEN + elif self.y < STREAM_MIN_Y: + self.y += WORLD_LEN + + def draw(self, attached=False): + if self.type == "mosquito": + glColor3f(*COLOR_MOSQUITO) + else: + glColor3f(*COLOR_FLY) + + glPushMatrix() + if not attached: + glTranslatef(self.x, self.y, self.z) + glRotatef(self.yaw, 0, 0, 1) + + s = self.size + glBegin(GL_LINES) + glVertex3f(0, s, 0) + glVertex3f(0, -s, 0) + w_offset = 0.0 if attached else math.sin(self.timer * 30) * (s * 0.8) + glVertex3f(0, 0, 0) + glVertex3f(-s * 1.5, 0, w_offset) + glVertex3f(0, 0, 0) + glVertex3f(s * 1.5, 0, w_offset) + glEnd() + + glPopMatrix() + +# ========================================== +# RENDERIZADO DE ESCENARIO Y MODELOS +# ========================================== +def draw_water_surface(time_counter): + glColor3f(*COLOR_WATER) + + grid_size = 30 + for x in range(int(-POND_RADIUS), int(POND_RADIUS), grid_size): + glBegin(GL_LINE_STRIP) + for y in range(int(-POND_RADIUS), int(POND_RADIUS), grid_size): + dist = math.hypot(x, y) + if dist < POND_RADIUS - 30: + z = 1.0 * math.sin(dist * 0.015 - time_counter * 3.0) + 0.5 * math.cos(x * 0.01 + time_counter) + glVertex3f(x, y, z) + glEnd() + + for y in range(int(STREAM_MIN_Y), int(STREAM_MAX_Y), 20): + if abs(y) < POND_RADIUS: + continue + cx = get_river_center(y) + glBegin(GL_LINE_STRIP) + for dx in range(-100, 110, 15): + x = cx + dx + z = 0.5 * math.sin(y * 0.03 + dx * 0.08 + time_counter * 4.0) + glVertex3f(x, y, z) + glEnd() + +def draw_terrain(): + glColor3f(*COLOR_LAND) + + grid_step = 120 + for y in range(int(STREAM_MIN_Y - 300), int(STREAM_MAX_Y + 300), grid_step): + glBegin(GL_LINE_STRIP) + for x in range(-2500, 2500, grid_step): + z = 1.5 * math.sin(x * 0.005) + 1.5 * math.cos(y * 0.005) + glVertex3f(x, y, z) + glEnd() + + width_river = 100.0 + margin_land = 70.0 + glColor3f(*COLOR_LAND_RIVER) + + for y in range(int(STREAM_MIN_Y), int(STREAM_MAX_Y), 30): + if abs(y) < POND_RADIUS: + continue + cx = get_river_center(y) + + glBegin(GL_LINE_STRIP) + for dx in range(int(-width_river - margin_land), int(-width_river), 15): + x = cx + dx + z = 0.6 * math.sin(y * 0.02 + x * 0.02) + 0.3 + glVertex3f(x, y, z) + glEnd() + + glBegin(GL_LINE_STRIP) + for dx in range(int(width_river), int(width_river + margin_land), 15): + x = cx + dx + z = 0.6 * math.sin(y * 0.02 + x * 0.02) + 0.3 + glVertex3f(x, y, z) + glEnd() + + for ix, iy, ir in ISLANDS: + glColor3f(*COLOR_ISLAND) + for layer in range(1, 4): + rad_layer = ir * (1 - layer * 0.25) + height = layer * 1.2 + glBegin(GL_LINE_LOOP) + for i in range(16): + angle = 2.0 * math.pi * i / 16 + dx = rad_layer * math.cos(angle) + dy = rad_layer * math.sin(angle) + glVertex3f(ix + dx, iy + dy, height) + glEnd() + +def draw_map(time_counter): + draw_terrain() + draw_water_surface(time_counter) + + glColor3f(*COLOR_REED) + glBegin(GL_LINES) + for rx, ry, rz, rh in REEDS: + glVertex3f(rx, ry, rz) + glVertex3f(rx, ry, rz + rh) + glVertex3f(rx, ry, rz) + glVertex3f(rx - 1.5, ry + 0.5, rz + rh * 0.7) + glVertex3f(rx, ry, rz) + glVertex3f(rx + 1.5, ry - 0.5, rz + rh * 0.8) + glEnd() + +def draw_dragonfly(wing_angle, caught_insect=None): + glColor3f(*COLOR_DRAGONFLY) + + glBegin(GL_LINES) + glVertex3f(0, 1.5, 0) + glVertex3f(0, -3.0, 0) + glVertex3f(-0.4, 0.6, 0) + glVertex3f(0.4, 0.6, 0) + glVertex3f(-0.3, -0.6, 0) + glVertex3f(0.3, -0.6, 0) + + glVertex3f(-0.3, 0.5, 0) + glVertex3f(-0.2, 0.2, -0.6) + glVertex3f(0.3, 0.5, 0) + glVertex3f(0.2, 0.2, -0.6) + glVertex3f(-0.2, -0.2, 0) + glVertex3f(-0.1, -0.3, -0.6) + glVertex3f(0.2, -0.2, 0) + glVertex3f(0.1, -0.3, -0.6) + glEnd() + + glBegin(GL_LINE_LOOP) + for i in range(8): + angle = 2.0 * math.pi * i / 8 + glVertex3f(0.3 * math.cos(angle), 1.4 + 0.3 * math.sin(angle), 0) + glEnd() + + rad_wing = math.radians(wing_angle) + w_z = math.sin(rad_wing) * 1.2 if wing_angle != 0 else 0.0 + + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(-4.5, 1.0, w_z) + glVertex3f(-3.8, 0.2, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(4.5, 1.0, w_z) + glVertex3f(3.8, 0.2, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(-3.8, -0.2, w_z) + glVertex3f(-3.0, -0.8, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(3.8, -0.2, w_z) + glVertex3f(3.0, -0.8, w_z) + glEnd() + + if caught_insect: + glPushMatrix() + glTranslatef(0.0, 0.2, -0.6) + caught_insect.draw(attached=True) + glPopMatrix() + +# ========================================== +# INTERFAZ 2D / RADAR Y TEXTOS FIJOS +# ========================================== +def draw_text_2d(text_surface, x, y): + text_data = pygame.image.tostring(text_surface, "RGBA", True) + w, h = text_surface.get_size() + + glEnable(GL_BLEND) + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) + + glRasterPos2i(x, y) + glDrawPixels(w, h, GL_RGBA, GL_UNSIGNED_BYTE, text_data) + glDisable(GL_BLEND) + +def draw_hud(player_pos, yaw, insects, eaten_mosquitos, eaten_flies, font): + glDisable(GL_DEPTH_TEST) + + glMatrixMode(GL_PROJECTION) + glPushMatrix() + glLoadIdentity() + gluOrtho2D(0, WIDTH, 0, HEIGHT) + + glMatrixMode(GL_MODELVIEW) + glPushMatrix() + glLoadIdentity() + + px, py, pz = player_pos + + # RADAR REDUCIDO + cx, cy = WIDTH // 2, HEIGHT - 35 + radius = 22 + + glColor3f(*COLOR_HUD) + glBegin(GL_LINE_LOOP) + for i in range(24): + angle = 2.0 * math.pi * i / 24 + glVertex2f(cx + radius * math.cos(angle), cy + radius * math.sin(angle)) + glEnd() + + glBegin(GL_LINES) + glVertex2f(cx, cy + radius) + glVertex2f(cx, cy + radius - 5) + glVertex2f(cx, cy - radius) + glVertex2f(cx, cy - radius + 5) + glVertex2f(cx - radius, cy) + glVertex2f(cx - radius + 5, cy) + glVertex2f(cx + radius, cy) + glVertex2f(cx + radius - 5, cy) + glEnd() + + closest_insect = None + min_dist = 999999.0 + relative_angle = 0.0 + diff_z = 0.0 + + rad_yaw = math.radians(yaw) + + for insect in insects: + dx = insect.x - px + dy = insect.y - py + dist = math.hypot(dx, dy) + + if dist < min_dist and dist > 0.1: + angle_to_insect = math.degrees(math.atan2(dy, dx)) + heading = (angle_to_insect - (90 - yaw)) % 360 + if heading > 180: + heading -= 360 + + if abs(heading) < 60: + min_dist = dist + closest_insect = insect + relative_angle = heading + diff_z = insect.z - pz + + if closest_insect: + radar_x = cx - (relative_angle / 60.0) * (radius - 4) + radar_y = cy + (diff_z / 20.0) * (radius - 4) + + dist_from_center = math.hypot(radar_x - cx, radar_y - cy) + if dist_from_center > (radius - 3): + factor = (radius - 3) / dist_from_center + radar_x = cx + (radar_x - cx) * factor + radar_y = cy + (radar_y - cy) * factor + + glColor3f(1.0, 1.0, 1.0) + glBegin(GL_LINE_LOOP) + for i in range(8): + ang = 2.0 * math.pi * i / 8 + glVertex2f(radar_x + 3 * math.cos(ang), radar_y + 3 * math.sin(ang)) + glEnd() + + # ÍCONOS SUPERIORES + glColor3f(*COLOR_MOSQUITO) + glBegin(GL_LINES) + glVertex2f(25, HEIGHT - 20) + glVertex2f(25, HEIGHT - 40) + glVertex2f(25, HEIGHT - 30) + glVertex2f(12, HEIGHT - 30) + glVertex2f(25, HEIGHT - 30) + glVertex2f(38, HEIGHT - 30) + glEnd() + + glColor3f(*COLOR_FLY) + glBegin(GL_LINES) + glVertex2f(WIDTH - 85, HEIGHT - 20) + glVertex2f(WIDTH - 85, HEIGHT - 40) + glVertex2f(WIDTH - 85, HEIGHT - 30) + glVertex2f(WIDTH - 98, HEIGHT - 30) + glVertex2f(WIDTH - 85, HEIGHT - 30) + glVertex2f(WIDTH - 32, HEIGHT - 30) + glEnd() + + # TEXTOS + txt_mosq = font.render(f"x {eaten_mosquitos}", True, (255, 230, 0)) + txt_flies = font.render(f"x {eaten_flies}", True, (200, 65, 40)) + + draw_text_2d(txt_mosq, 45, HEIGHT - 40) + draw_text_2d(txt_flies, WIDTH - 65, HEIGHT - 40) + + glMatrixMode(GL_PROJECTION) + glPopMatrix() + glMatrixMode(GL_MODELVIEW) + glPopMatrix() + glEnable(GL_DEPTH_TEST) + +# ========================================== +# BUCLE PRINCIPAL +# ========================================== +def main(): + pygame.init() + pygame.font.init() + font = pygame.font.SysFont("Arial", 22, bold=True) + + pygame.display.set_mode((WIDTH, HEIGHT), DOUBLEBUF | OPENGL) + pygame.display.set_caption("Simulador de Vuelo de Libélula 3D") + + glEnable(GL_DEPTH_TEST) + glClearColor(*COLOR_BG, 1.0) + + glMatrixMode(GL_PROJECTION) + gluPerspective(50, (WIDTH / HEIGHT), 0.1, 5000.0) + glMatrixMode(GL_MODELVIEW) + + # Estado de la Libélula + px, py, pz = 0.0, 0.0, 15.0 + yaw = 0.0 + roll = 0.0 + pitch = 0.0 + max_roll = 30.0 + max_pitch = 20.0 + + speed = 0.0 + max_speed = 3.0 + acceleration = 0.06 + deceleration = 0.03 + + min_altitude = 1.0 + max_altitude = 120.0 + + wing_timer = 0.0 + water_timer = 0.0 + is_perched = False + is_diving = False # Estado de picado automático + + caught_insect = None + eaten_mosquitos = 0 + eaten_flies = 0 + + insects = [] + num_mosquitos = random.randint(MOSQUITO_MIN, MOSQUITO_MAX) + num_flies = random.randint(FLY_MIN, FLY_MAX) + + for _ in range(num_mosquitos): + insects.append(Insect("mosquito")) + for _ in range(num_flies): + insects.append(Insect("fly")) + + clock = pygame.time.Clock() + + running = True + while running: + dt = clock.tick(60) / 1000.0 + water_timer += dt + + for event in pygame.event.get(): + if event.type == QUIT or (event.type == KEYDOWN and event.key == K_ESCAPE): + running = False + # Activar movimiento automático de PICADO al pulsar la barra ESPACIO + elif event.type == KEYDOWN and event.key == K_SPACE and not is_perched: + is_diving = True + + keys = pygame.key.get_pressed() + + if is_perched: + # En el junco, las flechas izquierda/derecha giran + if keys[K_LEFT]: + yaw += 3.0 + if keys[K_RIGHT]: + yaw -= 3.0 + + # Despegar con W (Acelerar) + if keys[K_w]: + is_perched = False + speed = 0.5 + + wing_angle = 0.0 + pitch = 0.0 + roll = 0.0 + is_diving = False + else: + # ---------------------------------- + # CONTROLES Y MOVIMIENTO REORDENADOS + # ---------------------------------- + if is_diving: + # MODO PICADO AUTOMÁTICO (Caída a 45º a máxima velocidad) + speed = max_speed * 1.5 + pz -= 1.2 + target_pitch = -45.0 + + # Desactivar picado al tocar suelo/agua o soltar control + if pz <= min_altitude: + pz = min_altitude + is_diving = False + else: + # Controles estándar + # W: Acelerar / S: Frenar (hasta vuelo estático) + if keys[K_w]: + speed = min(speed + acceleration, max_speed) + elif keys[K_s]: + speed = max(speed - deceleration, 0.0) + + # Si está casi detenida, intentar posarse en un junco cercano + if speed < 0.3: + for rx, ry, rz, rh in REEDS: + dist = math.hypot(px - rx, py - ry) + if dist < 8.0 and abs(pz - (rz + rh)) < 8.0: + px, py, pz = rx, ry, rz + rh + speed = 0.0 + roll = 0.0 + pitch = 0.0 + is_perched = True + + if caught_insect: + if caught_insect.type == "mosquito": + eaten_mosquitos += 1 + else: + eaten_flies += 1 + caught_insect = None + break + + # Flecha Arriba: Subir altitud / Flecha Abajo: Bajar altitud + target_pitch = 0.0 + if keys[K_UP]: + pz = min(pz + 0.8, max_altitude) + target_pitch = max_pitch + elif keys[K_DOWN]: + pz = max(pz - 0.8, min_altitude) + target_pitch = -max_pitch + + # Transición suave de cabeceo (Pitch) + pitch += (target_pitch - pitch) * 0.15 + + # Flecha Izquierda / Derecha: Giro lateral + Alabeo (Roll) + target_roll = 0.0 + if keys[K_LEFT]: + yaw += 2.8 + target_roll = -max_roll + elif keys[K_RIGHT]: + yaw -= 2.8 + target_roll = max_roll + + roll += (target_roll - roll) * 0.15 + + # Traslación hacia adelante según la dirección (Yaw) + rad_yaw = math.radians(yaw) + px += -math.sin(rad_yaw) * speed + py += math.cos(rad_yaw) * speed + + # Muros invisibles + if abs(py) < POND_RADIUS: + max_limit_x = POND_RADIUS + 200.0 + else: + cx = get_river_center(py) + max_limit_x = abs(cx) + 200.0 + + px = max(-max_limit_x, min(max_limit_x, px)) + + # Bucle continuo en arroyos + if py > STREAM_MAX_Y: + py -= WORLD_LEN + elif py < STREAM_MIN_Y: + py += WORLD_LEN + + wing_timer += 80.0 if is_diving else (60.0 if speed > 0.1 else 30.0) + wing_angle = math.sin(wing_timer) * 35.0 + + current_mosquitos = 0 + current_flies = 0 + + for insect in insects[:]: + insect.update((px, py, pz)) + + if insect.type == "mosquito": + current_mosquitos += 1 + else: + current_flies += 1 + + if not caught_insect: + dist_to_dragonfly = math.sqrt((px - insect.x)**2 + (py - insect.y)**2 + (pz - insect.z)**2) + if dist_to_dragonfly < 6.0: + caught_insect = insect + insects.remove(insect) + + if current_mosquitos < MOSQUITO_MIN: + insects.append(Insect("mosquito")) + if current_flies < FLY_MIN: + insects.append(Insect("fly")) + + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) + glLoadIdentity() + + cam_dist = 18.0 + cam_height = 8.0 + rad_yaw = math.radians(yaw) + + cam_x = px + math.sin(rad_yaw) * cam_dist + cam_y = py - math.cos(rad_yaw) * cam_dist + cam_z = pz + cam_height + + gluLookAt(cam_x, cam_y, cam_z, + px, py, pz + 0.5, + 0, 0, 1) + + draw_map(water_timer) + + for insect in insects: + insect.draw() + + glPushMatrix() + glTranslatef(px, py, pz) + glRotatef(yaw, 0, 0, 1) + glRotatef(pitch, 1, 0, 0) + glRotatef(roll, 0, 1, 0) + draw_dragonfly(wing_angle, caught_insect) + glPopMatrix() + + draw_hud((px, py, pz), yaw, insects, eaten_mosquitos, eaten_flies, font) + + pygame.display.flip() + + pygame.quit() + sys.exit() + +if __name__ == "__main__": + main() diff --git a/previos/dragonfly_game_v5.5.py b/previos/dragonfly_game_v5.5.py new file mode 100644 index 0000000..1726e49 --- /dev/null +++ b/previos/dragonfly_game_v5.5.py @@ -0,0 +1,694 @@ +import sys +import math +import random +import pygame +from pygame.locals import * +from OpenGL.GL import * +from OpenGL.GLU import * + +# ========================================== +# CONFIGURACIÓN Y CONSTANTES +# ========================================== +WIDTH, HEIGHT = 800, 600 + +# Colores (R, G, B) +COLOR_BG = (0.12, 0.15, 0.12) # Fondo ambiente +COLOR_WATER = (0.0, 0.5, 0.85) # Agua animada (Cian/Azul) +COLOR_LAND = (0.45, 0.32, 0.15) # Tierra / Relieve +COLOR_LAND_RIVER = (0.40, 0.28, 0.12) # Tierra en riachuelos +COLOR_ISLAND = (0.5, 0.38, 0.18) # Islas +COLOR_REED = (0.15, 0.85, 0.25) # Juncos verdes +COLOR_DRAGONFLY = (0.0, 1.0, 1.0) # Libélula alámbrica (Cian) +COLOR_MOSQUITO = (1.0, 0.9, 0.0) # Mosquitos (Amarillo) +COLOR_FLY = (0.8, 0.25, 0.15) # Moscas (Rojo-Marrón) +COLOR_HUD = (1.0, 1.0, 1.0) # Retícula HUD (Blanco) + +# Dimensiones del Mundo +POND_RADIUS = 1800.0 +STREAM_LEN = 900.0 +STREAM_MIN_Y = -(POND_RADIUS + STREAM_LEN) +STREAM_MAX_Y = (POND_RADIUS + STREAM_LEN) +WORLD_LEN = STREAM_MAX_Y - STREAM_MIN_Y + +# Cantidades de insectos +MOSQUITO_MIN, MOSQUITO_MAX = 10, 20 +FLY_MIN, FLY_MAX = 2, 7 + +# ========================================== +# ELEMENTOS DEL MAPA +# ========================================== +REEDS = [ + (-250, 350, 0, 20), (-160, -500, 0, 22), (500, 200, 0, 24), (-700, 200, 0, 21), (200, -800, 0, 23), + (-900, -300, 0, 25), (760, -600, 0, 22), (900, 500, 0, 26), (-500, 900, 0, 23), (600, -1000, 0, 22), + (-1200, 400, 0, 24), (1300, -300, 0, 20), (-1400, -700, 0, 25), (1100, 1200, 0, 23), (-360, -1300, 0, 22), + (-90, -1950, 0, 20), (-110, -2150, 0, 23), (-80, -2350, 0, 19), (-100, -2550, 0, 22), + (90, 1950, 0, 22), (110, 2150, 0, 20), (85, 2350, 0, 24), (105, 2550, 0, 21) +] + +ISLANDS = [ + (-250, 350, 90), (-160, -500, 85), (500, 200, 100), (-700, 200, 110), (200, -800, 95), + (-900, -300, 120), (760, -600, 90), (900, 500, 105), (-500, 900, 115), (600, -1000, 100) +] + +def get_river_center(y): + if y < -POND_RADIUS: + dy = y + POND_RADIUS + return -50 + 100 * math.sin(dy * 0.008) + elif y > POND_RADIUS: + dy = y - POND_RADIUS + return 50 + 100 * math.sin(dy * 0.008) + else: + return (y / POND_RADIUS) * 50.0 + +def is_over_water(x, y): + dist_center = math.hypot(x, y) + if dist_center < POND_RADIUS - 50: + return True + if abs(y) >= POND_RADIUS: + cx = get_river_center(y) + if abs(x - cx) < 80: + return True + return False + +def get_random_water_position(): + while True: + if random.random() < 0.7: + rad = random.uniform(0, math.pi * 2) + r = random.uniform(100, POND_RADIUS - 150) + x = r * math.cos(rad) + y = r * math.sin(rad) + else: + y = random.choice([random.uniform(POND_RADIUS, STREAM_MAX_Y), random.uniform(STREAM_MIN_Y, -POND_RADIUS)]) + cx = get_river_center(y) + x = cx + random.uniform(-60, 60) + + if is_over_water(x, y): + z = random.uniform(5.0, 35.0) + return x, y, z + +# ========================================== +# CLASES DE INSECTOS +# ========================================== +class Insect: + def __init__(self, insect_type): + self.type = insect_type + self.x, self.y, self.z = get_random_water_position() + self.yaw = random.uniform(0, 360) + self.timer = random.uniform(0, 100) + + if self.type == "mosquito": + self.speed = 1.8 + self.size = 1.2 + else: # Fly + self.speed = 0.9 + self.size = 1.6 + + def update(self, player_pos): + self.timer += 0.05 + px, py, pz = player_pos + dist_to_player = math.hypot(self.x - px, self.y - py) + + if self.type == "mosquito": + can_detect = (pz <= self.z) + + if can_detect and dist_to_player < 100: + escape_angle = math.degrees(math.atan2(self.y - py, self.x - px)) + self.yaw += (escape_angle - self.yaw) * 0.05 + + self.yaw += math.sin(self.timer * 3) * 1.5 + self.z += math.sin(self.timer * 1.5) * 0.2 + + elif self.type == "fly": + if dist_to_player < 120: + escape_angle = math.degrees(math.atan2(self.y - py, self.x - px)) + self.yaw = escape_angle + random.uniform(-45, 45) + self.z += random.choice([-1.5, 1.5]) + else: + if random.random() < 0.08: + self.yaw += random.uniform(-60, 60) + + self.yaw += math.sin(self.timer * 6) * 4.0 + self.z += math.cos(self.timer * 4) * 0.6 + + self.z = max(3.0, min(50.0, self.z)) + + rad = math.radians(self.yaw) + next_x = self.x - math.sin(rad) * self.speed + next_y = self.y + math.cos(rad) * self.speed + + if is_over_water(next_x, next_y): + self.x, self.y = next_x, next_y + else: + self.yaw += 140.0 + random.uniform(-20, 20) + + if self.y > STREAM_MAX_Y: + self.y -= WORLD_LEN + elif self.y < STREAM_MIN_Y: + self.y += WORLD_LEN + + def draw(self, attached=False): + if self.type == "mosquito": + glColor3f(*COLOR_MOSQUITO) + else: + glColor3f(*COLOR_FLY) + + glPushMatrix() + if not attached: + glTranslatef(self.x, self.y, self.z) + glRotatef(self.yaw, 0, 0, 1) + + s = self.size + glBegin(GL_LINES) + glVertex3f(0, s, 0) + glVertex3f(0, -s, 0) + w_offset = 0.0 if attached else math.sin(self.timer * 30) * (s * 0.8) + glVertex3f(0, 0, 0) + glVertex3f(-s * 1.5, 0, w_offset) + glVertex3f(0, 0, 0) + glVertex3f(s * 1.5, 0, w_offset) + glEnd() + + glPopMatrix() + +# ========================================== +# RENDERIZADO DE ESCENARIO Y MODELOS +# ========================================== +def draw_water_surface(time_counter): + glColor3f(*COLOR_WATER) + + grid_size = 30 + for x in range(int(-POND_RADIUS), int(POND_RADIUS), grid_size): + glBegin(GL_LINE_STRIP) + for y in range(int(-POND_RADIUS), int(POND_RADIUS), grid_size): + dist = math.hypot(x, y) + if dist < POND_RADIUS - 30: + z = 1.0 * math.sin(dist * 0.015 - time_counter * 3.0) + 0.5 * math.cos(x * 0.01 + time_counter) + glVertex3f(x, y, z) + glEnd() + + for y in range(int(STREAM_MIN_Y), int(STREAM_MAX_Y), 20): + if abs(y) < POND_RADIUS: + continue + cx = get_river_center(y) + glBegin(GL_LINE_STRIP) + for dx in range(-100, 110, 15): + x = cx + dx + z = 0.5 * math.sin(y * 0.03 + dx * 0.08 + time_counter * 4.0) + glVertex3f(x, y, z) + glEnd() + +def draw_terrain(): + glColor3f(*COLOR_LAND) + + grid_step = 120 + for y in range(int(STREAM_MIN_Y - 300), int(STREAM_MAX_Y + 300), grid_step): + glBegin(GL_LINE_STRIP) + for x in range(-2500, 2500, grid_step): + z = 1.5 * math.sin(x * 0.005) + 1.5 * math.cos(y * 0.005) + glVertex3f(x, y, z) + glEnd() + + width_river = 100.0 + margin_land = 70.0 + glColor3f(*COLOR_LAND_RIVER) + + for y in range(int(STREAM_MIN_Y), int(STREAM_MAX_Y), 30): + if abs(y) < POND_RADIUS: + continue + cx = get_river_center(y) + + glBegin(GL_LINE_STRIP) + for dx in range(int(-width_river - margin_land), int(-width_river), 15): + x = cx + dx + z = 0.6 * math.sin(y * 0.02 + x * 0.02) + 0.3 + glVertex3f(x, y, z) + glEnd() + + glBegin(GL_LINE_STRIP) + for dx in range(int(width_river), int(width_river + margin_land), 15): + x = cx + dx + z = 0.6 * math.sin(y * 0.02 + x * 0.02) + 0.3 + glVertex3f(x, y, z) + glEnd() + + for ix, iy, ir in ISLANDS: + glColor3f(*COLOR_ISLAND) + for layer in range(1, 4): + rad_layer = ir * (1 - layer * 0.25) + height = layer * 1.2 + glBegin(GL_LINE_LOOP) + for i in range(16): + angle = 2.0 * math.pi * i / 16 + dx = rad_layer * math.cos(angle) + dy = rad_layer * math.sin(angle) + glVertex3f(ix + dx, iy + dy, height) + glEnd() + +def draw_map(time_counter): + draw_terrain() + draw_water_surface(time_counter) + + glColor3f(*COLOR_REED) + glBegin(GL_LINES) + for rx, ry, rz, rh in REEDS: + glVertex3f(rx, ry, rz) + glVertex3f(rx, ry, rz + rh) + glVertex3f(rx, ry, rz) + glVertex3f(rx - 1.5, ry + 0.5, rz + rh * 0.7) + glVertex3f(rx, ry, rz) + glVertex3f(rx + 1.5, ry - 0.5, rz + rh * 0.8) + glEnd() + +def draw_dragonfly(wing_angle, caught_insect=None): + glColor3f(*COLOR_DRAGONFLY) + + glBegin(GL_LINES) + glVertex3f(0, 1.5, 0) + glVertex3f(0, -3.0, 0) + glVertex3f(-0.4, 0.6, 0) + glVertex3f(0.4, 0.6, 0) + glVertex3f(-0.3, -0.6, 0) + glVertex3f(0.3, -0.6, 0) + + glVertex3f(-0.3, 0.5, 0) + glVertex3f(-0.2, 0.2, -0.6) + glVertex3f(0.3, 0.5, 0) + glVertex3f(0.2, 0.2, -0.6) + glVertex3f(-0.2, -0.2, 0) + glVertex3f(-0.1, -0.3, -0.6) + glVertex3f(0.2, -0.2, 0) + glVertex3f(0.1, -0.3, -0.6) + glEnd() + + glBegin(GL_LINE_LOOP) + for i in range(8): + angle = 2.0 * math.pi * i / 8 + glVertex3f(0.3 * math.cos(angle), 1.4 + 0.3 * math.sin(angle), 0) + glEnd() + + rad_wing = math.radians(wing_angle) + w_z = math.sin(rad_wing) * 1.2 if wing_angle != 0 else 0.0 + + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(-4.5, 1.0, w_z) + glVertex3f(-3.8, 0.2, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(4.5, 1.0, w_z) + glVertex3f(3.8, 0.2, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(-3.8, -0.2, w_z) + glVertex3f(-3.0, -0.8, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(3.8, -0.2, w_z) + glVertex3f(3.0, -0.8, w_z) + glEnd() + + if caught_insect: + glPushMatrix() + glTranslatef(0.0, 0.2, -0.6) + caught_insect.draw(attached=True) + glPopMatrix() + +# ========================================== +# INTERFAZ 2D / RADAR Y TEXTOS FIJOS +# ========================================== +def draw_text_2d(text_surface, x, y): + text_data = pygame.image.tostring(text_surface, "RGBA", True) + w, h = text_surface.get_size() + + glEnable(GL_BLEND) + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) + + glRasterPos2i(x, y) + glDrawPixels(w, h, GL_RGBA, GL_UNSIGNED_BYTE, text_data) + glDisable(GL_BLEND) + +def draw_hud(player_pos, yaw, insects, eaten_mosquitos, eaten_flies, font): + glDisable(GL_DEPTH_TEST) + + glMatrixMode(GL_PROJECTION) + glPushMatrix() + glLoadIdentity() + gluOrtho2D(0, WIDTH, 0, HEIGHT) + + glMatrixMode(GL_MODELVIEW) + glPushMatrix() + glLoadIdentity() + + px, py, pz = player_pos + + # RADAR REDUCIDO + cx, cy = WIDTH // 2, HEIGHT - 35 + radius = 22 + + glColor3f(*COLOR_HUD) + glBegin(GL_LINE_LOOP) + for i in range(24): + angle = 2.0 * math.pi * i / 24 + glVertex2f(cx + radius * math.cos(angle), cy + radius * math.sin(angle)) + glEnd() + + glBegin(GL_LINES) + glVertex2f(cx, cy + radius) + glVertex2f(cx, cy + radius - 5) + glVertex2f(cx, cy - radius) + glVertex2f(cx, cy - radius + 5) + glVertex2f(cx - radius, cy) + glVertex2f(cx - radius + 5, cy) + glVertex2f(cx + radius, cy) + glVertex2f(cx + radius - 5, cy) + glEnd() + + closest_insect = None + min_dist = 999999.0 + relative_angle = 0.0 + diff_z = 0.0 + + rad_yaw = math.radians(yaw) + + for insect in insects: + dx = insect.x - px + dy = insect.y - py + dist = math.hypot(dx, dy) + + if dist < min_dist and dist > 0.1: + angle_to_insect = math.degrees(math.atan2(dy, dx)) + heading = (angle_to_insect - (90 - yaw)) % 360 + if heading > 180: + heading -= 360 + + if abs(heading) < 60: + min_dist = dist + closest_insect = insect + relative_angle = heading + diff_z = insect.z - pz + + if closest_insect: + radar_x = cx - (relative_angle / 60.0) * (radius - 4) + radar_y = cy + (diff_z / 20.0) * (radius - 4) + + dist_from_center = math.hypot(radar_x - cx, radar_y - cy) + if dist_from_center > (radius - 3): + factor = (radius - 3) / dist_from_center + radar_x = cx + (radar_x - cx) * factor + radar_y = cy + (radar_y - cy) * factor + + glColor3f(1.0, 1.0, 1.0) + glBegin(GL_LINE_LOOP) + for i in range(8): + ang = 2.0 * math.pi * i / 8 + glVertex2f(radar_x + 3 * math.cos(ang), radar_y + 3 * math.sin(ang)) + glEnd() + + # ÍCONOS SUPERIORES + glColor3f(*COLOR_MOSQUITO) + glBegin(GL_LINES) + glVertex2f(25, HEIGHT - 20) + glVertex2f(25, HEIGHT - 40) + glVertex2f(25, HEIGHT - 30) + glVertex2f(12, HEIGHT - 30) + glVertex2f(25, HEIGHT - 30) + glVertex2f(38, HEIGHT - 30) + glEnd() + + glColor3f(*COLOR_FLY) + glBegin(GL_LINES) + glVertex2f(WIDTH - 85, HEIGHT - 20) + glVertex2f(WIDTH - 85, HEIGHT - 40) + glVertex2f(WIDTH - 85, HEIGHT - 30) + glVertex2f(WIDTH - 98, HEIGHT - 30) + glVertex2f(WIDTH - 85, HEIGHT - 30) + glVertex2f(WIDTH - 32, HEIGHT - 30) + glEnd() + + # TEXTOS + txt_mosq = font.render(f"x {eaten_mosquitos}", True, (255, 230, 0)) + txt_flies = font.render(f"x {eaten_flies}", True, (200, 65, 40)) + + draw_text_2d(txt_mosq, 45, HEIGHT - 40) + draw_text_2d(txt_flies, WIDTH - 65, HEIGHT - 40) + + glMatrixMode(GL_PROJECTION) + glPopMatrix() + glMatrixMode(GL_MODELVIEW) + glPopMatrix() + glEnable(GL_DEPTH_TEST) + +# ========================================== +# BUCLE PRINCIPAL +# ========================================== +def main(): + pygame.init() + pygame.font.init() + font = pygame.font.SysFont("Arial", 22, bold=True) + + pygame.display.set_mode((WIDTH, HEIGHT), DOUBLEBUF | OPENGL) + pygame.display.set_caption("Simulador de Vuelo de Libélula 3D") + + glEnable(GL_DEPTH_TEST) + glClearColor(*COLOR_BG, 1.0) + + glMatrixMode(GL_PROJECTION) + gluPerspective(50, (WIDTH / HEIGHT), 0.1, 5000.0) + glMatrixMode(GL_MODELVIEW) + + # Estado de la Libélula + px, py, pz = 0.0, 0.0, 15.0 + yaw = 0.0 + roll = 0.0 + pitch = 0.0 + max_roll = 30.0 + max_pitch = 20.0 + + speed = 0.0 + max_speed = 3.0 + acceleration = 0.06 + deceleration = 0.03 + + min_altitude = 1.0 + max_altitude = 120.0 + + wing_timer = 0.0 + water_timer = 0.0 + is_perched = False + + # Estado de Picado y Recuperación + dive_phase = "none" # "none", "diving" o "recovering" + pre_dive_alt = 15.0 # Almacena la altitud previa al picado + pre_dive_speed = 0.0 # Almacena la velocidad previa al picado + + caught_insect = None + eaten_mosquitos = 0 + eaten_flies = 0 + + insects = [] + num_mosquitos = random.randint(MOSQUITO_MIN, MOSQUITO_MAX) + num_flies = random.randint(FLY_MIN, FLY_MAX) + + for _ in range(num_mosquitos): + insects.append(Insect("mosquito")) + for _ in range(num_flies): + insects.append(Insect("fly")) + + clock = pygame.time.Clock() + + running = True + while running: + dt = clock.tick(60) / 1000.0 + water_timer += dt + + for event in pygame.event.get(): + if event.type == QUIT or (event.type == KEYDOWN and event.key == K_ESCAPE): + running = False + # Activar movimiento automático de PICADO al pulsar ESPACIO + elif event.type == KEYDOWN and event.key == K_SPACE and not is_perched and dive_phase == "none": + dive_phase = "diving" + pre_dive_alt = pz + pre_dive_speed = speed + + keys = pygame.key.get_pressed() + + if is_perched: + if keys[K_LEFT]: + yaw += 3.0 + if keys[K_RIGHT]: + yaw -= 3.0 + + if keys[K_w]: + is_perched = False + speed = 0.5 + + wing_angle = 0.0 + pitch = 0.0 + roll = 0.0 + dive_phase = "none" + else: + # ---------------------------------- + # GESTIÓN DE FASES DE PICADO Y RECUPERACIÓN + # ---------------------------------- + if dive_phase == "diving": + # FASE 1: CAÍDA EN PICADO A 45º Y MÁXIMA VELOCIDAD + speed = max_speed * 1.5 + pz -= 1.2 + target_pitch = -45.0 + + # Pasa a recuperación si toca agua/suelo o alcanza el límite inferior + if pz <= min_altitude: + pz = min_altitude + dive_phase = "recovering" + + elif dive_phase == "recovering": + # FASE 2: RECUPERACIÓN AUTOMÁTICA HASTA ALTITUD Y VELOCIDAD PREVIAS + target_pitch = 30.0 # Inclinación ascendente + pz += 0.8 + + # Ajustar suavemente la velocidad a la inicial antes del picado + speed += (pre_dive_speed - speed) * 0.1 + + # Al alcanzar la altitud original o superior, finaliza la maniobra + if pz >= pre_dive_alt: + pz = pre_dive_alt + speed = pre_dive_speed + dive_phase = "none" + else: + # VUELO ESTÁNDAR + if keys[K_w]: + speed = min(speed + acceleration, max_speed) + elif keys[K_s]: + speed = max(speed - deceleration, 0.0) + + if speed < 0.3: + for rx, ry, rz, rh in REEDS: + dist = math.hypot(px - rx, py - ry) + if dist < 8.0 and abs(pz - (rz + rh)) < 8.0: + px, py, pz = rx, ry, rz + rh + speed = 0.0 + roll = 0.0 + pitch = 0.0 + is_perched = True + + if caught_insect: + if caught_insect.type == "mosquito": + eaten_mosquitos += 1 + else: + eaten_flies += 1 + caught_insect = None + break + + target_pitch = 0.0 + if keys[K_UP]: + pz = min(pz + 0.8, max_altitude) + target_pitch = max_pitch + elif keys[K_DOWN]: + pz = max(pz - 0.8, min_altitude) + target_pitch = -max_pitch + + # Transición suave del ángulo Pitch + pitch += (target_pitch - pitch) * 0.15 + + # Giros laterales con Flechas Izquierda / Derecha + target_roll = 0.0 + if keys[K_LEFT]: + yaw += 2.8 + target_roll = -max_roll + elif keys[K_RIGHT]: + yaw -= 2.8 + target_roll = max_roll + + roll += (target_roll - roll) * 0.15 + + # Traslación + rad_yaw = math.radians(yaw) + px += -math.sin(rad_yaw) * speed + py += math.cos(rad_yaw) * speed + + # Muros invisibles + if abs(py) < POND_RADIUS: + max_limit_x = POND_RADIUS + 200.0 + else: + cx = get_river_center(py) + max_limit_x = abs(cx) + 200.0 + + px = max(-max_limit_x, min(max_limit_x, px)) + + if py > STREAM_MAX_Y: + py -= WORLD_LEN + elif py < STREAM_MIN_Y: + py += WORLD_LEN + + wing_timer += 80.0 if dive_phase != "none" else (60.0 if speed > 0.1 else 30.0) + wing_angle = math.sin(wing_timer) * 35.0 + + current_mosquitos = 0 + current_flies = 0 + + for insect in insects[:]: + insect.update((px, py, pz)) + + if insect.type == "mosquito": + current_mosquitos += 1 + else: + current_flies += 1 + + if not caught_insect: + dist_to_dragonfly = math.sqrt((px - insect.x)**2 + (py - insect.y)**2 + (pz - insect.z)**2) + if dist_to_dragonfly < 6.0: + caught_insect = insect + insects.remove(insect) + + # SI CAZA MIENTRAS ESTÁ EN PICADO, INICIA INMEDIATAMENTE LA RECUPERACIÓN DE ALTITUD + if dive_phase == "diving": + dive_phase = "recovering" + + if current_mosquitos < MOSQUITO_MIN: + insects.append(Insect("mosquito")) + if current_flies < FLY_MIN: + insects.append(Insect("fly")) + + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) + glLoadIdentity() + + cam_dist = 18.0 + cam_height = 8.0 + rad_yaw = math.radians(yaw) + + cam_x = px + math.sin(rad_yaw) * cam_dist + cam_y = py - math.cos(rad_yaw) * cam_dist + cam_z = pz + cam_height + + gluLookAt(cam_x, cam_y, cam_z, + px, py, pz + 0.5, + 0, 0, 1) + + draw_map(water_timer) + + for insect in insects: + insect.draw() + + glPushMatrix() + glTranslatef(px, py, pz) + glRotatef(yaw, 0, 0, 1) + glRotatef(pitch, 1, 0, 0) + glRotatef(roll, 0, 1, 0) + draw_dragonfly(wing_angle, caught_insect) + glPopMatrix() + + draw_hud((px, py, pz), yaw, insects, eaten_mosquitos, eaten_flies, font) + + pygame.display.flip() + + pygame.quit() + sys.exit() + +if __name__ == "__main__": + main() diff --git a/previos/dragonfly_game_v6.1.py b/previos/dragonfly_game_v6.1.py new file mode 100644 index 0000000..41f4293 --- /dev/null +++ b/previos/dragonfly_game_v6.1.py @@ -0,0 +1,696 @@ +import sys +import math +import random +import pygame +from pygame.locals import * +from OpenGL.GL import * +from OpenGL.GLU import * + +# ========================================== +# CONFIGURACIÓN Y CONSTANTES +# ========================================== +WIDTH, HEIGHT = 800, 600 + +# Colores (R, G, B) +COLOR_BG = (0.12, 0.15, 0.12) # Fondo ambiente +COLOR_WATER = (0.0, 0.5, 0.85) # Agua animada (Cian/Azul) +COLOR_LAND = (0.45, 0.32, 0.15) # Tierra / Relieve +COLOR_LAND_RIVER = (0.40, 0.28, 0.12) # Tierra en riachuelos +COLOR_ISLAND = (0.5, 0.38, 0.18) # Islas +COLOR_REED = (0.15, 0.85, 0.25) # Juncos verdes +COLOR_CATTAIL = (0.35, 0.18, 0.08) # Espiga marrón oscuro de los juncos +COLOR_DRAGONFLY = (0.0, 1.0, 1.0) # Libélula alámbrica (Cian) +COLOR_MOSQUITO = (1.0, 0.9, 0.0) # Mosquitos (Amarillo) +COLOR_FLY = (0.8, 0.25, 0.15) # Moscas (Rojo-Marrón) +COLOR_HUD = (1.0, 1.0, 1.0) # Retícula HUD (Blanco) + +# Dimensiones del Mundo +POND_RADIUS = 1800.0 +STREAM_LEN = 900.0 +STREAM_MIN_Y = -(POND_RADIUS + STREAM_LEN) +STREAM_MAX_Y = (POND_RADIUS + STREAM_LEN) +WORLD_LEN = STREAM_MAX_Y - STREAM_MIN_Y + +# Cantidades de insectos +MOSQUITO_MIN, MOSQUITO_MAX = 10, 20 +FLY_MIN, FLY_MAX = 2, 7 + +# ========================================== +# ELEMENTOS DEL MAPA +# ========================================== +REEDS = [ + (-250, 350, 0, 20), (-160, -500, 0, 22), (500, 200, 0, 24), (-700, 200, 0, 21), (200, -800, 0, 23), + (-900, -300, 0, 25), (760, -600, 0, 22), (900, 500, 0, 26), (-500, 900, 0, 23), (600, -1000, 0, 22), + (-1200, 400, 0, 24), (1300, -300, 0, 20), (-1400, -700, 0, 25), (1100, 1200, 0, 23), (-360, -1300, 0, 22), + (-90, -1950, 0, 20), (-110, -2150, 0, 23), (-80, -2350, 0, 19), (-100, -2550, 0, 22), + (90, 1950, 0, 22), (110, 2150, 0, 20), (85, 2350, 0, 24), (105, 2550, 0, 21) +] + +ISLANDS = [ + (-250, 350, 90), (-160, -500, 85), (500, 200, 100), (-700, 200, 110), (200, -800, 95), + (-900, -300, 120), (760, -600, 90), (900, 500, 105), (-500, 900, 115), (600, -1000, 100) +] + +def get_river_center(y): + if y < -POND_RADIUS: + dy = y + POND_RADIUS + return -50 + 100 * math.sin(dy * 0.008) + elif y > POND_RADIUS: + dy = y - POND_RADIUS + return 50 + 100 * math.sin(dy * 0.008) + else: + return (y / POND_RADIUS) * 50.0 + +def is_over_water(x, y): + dist_center = math.hypot(x, y) + if dist_center < POND_RADIUS - 50: + return True + if abs(y) >= POND_RADIUS: + cx = get_river_center(y) + if abs(x - cx) < 80: + return True + return False + +def get_random_water_position(): + while True: + if random.random() < 0.7: + rad = random.uniform(0, math.pi * 2) + r = random.uniform(100, POND_RADIUS - 150) + x = r * math.cos(rad) + y = r * math.sin(rad) + else: + y = random.choice([random.uniform(POND_RADIUS, STREAM_MAX_Y), random.uniform(STREAM_MIN_Y, -POND_RADIUS)]) + cx = get_river_center(y) + x = cx + random.uniform(-60, 60) + + if is_over_water(x, y): + z = random.uniform(5.0, 35.0) + return x, y, z + +# ========================================== +# CLASES DE INSECTOS +# ========================================== +class Insect: + def __init__(self, insect_type): + self.type = insect_type + self.x, self.y, self.z = get_random_water_position() + self.yaw = random.uniform(0, 360) + self.timer = random.uniform(0, 100) + + if self.type == "mosquito": + self.speed = 1.8 + self.size = 1.2 + else: + self.speed = 0.9 + self.size = 1.6 + + def update(self, player_pos): + self.timer += 0.05 + px, py, pz = player_pos + dist_to_player = math.hypot(self.x - px, self.y - py) + + if self.type == "mosquito": + can_detect = (pz <= self.z) + + if can_detect and dist_to_player < 100: + escape_angle = math.degrees(math.atan2(self.y - py, self.x - px)) + self.yaw += (escape_angle - self.yaw) * 0.05 + + self.yaw += math.sin(self.timer * 3) * 1.5 + self.z += math.sin(self.timer * 1.5) * 0.2 + + elif self.type == "fly": + if dist_to_player < 120: + escape_angle = math.degrees(math.atan2(self.y - py, self.x - px)) + self.yaw = escape_angle + random.uniform(-45, 45) + self.z += random.choice([-1.5, 1.5]) + else: + if random.random() < 0.08: + self.yaw += random.uniform(-60, 60) + + self.yaw += math.sin(self.timer * 6) * 4.0 + self.z += math.cos(self.timer * 4) * 0.6 + + self.z = max(3.0, min(50.0, self.z)) + + rad = math.radians(self.yaw) + next_x = self.x - math.sin(rad) * self.speed + next_y = self.y + math.cos(rad) * self.speed + + if is_over_water(next_x, next_y): + self.x, self.y = next_x, next_y + else: + self.yaw += 140.0 + random.uniform(-20, 20) + + if self.y > STREAM_MAX_Y: + self.y -= WORLD_LEN + elif self.y < STREAM_MIN_Y: + self.y += WORLD_LEN + + def draw(self, attached=False): + if self.type == "mosquito": + glColor3f(*COLOR_MOSQUITO) + else: + glColor3f(*COLOR_FLY) + + glPushMatrix() + if not attached: + glTranslatef(self.x, self.y, self.z) + glRotatef(self.yaw, 0, 0, 1) + + s = self.size + glBegin(GL_LINES) + glVertex3f(0, s, 0) + glVertex3f(0, -s, 0) + w_offset = 0.0 if attached else math.sin(self.timer * 30) * (s * 0.8) + glVertex3f(0, 0, 0) + glVertex3f(-s * 1.5, 0, w_offset) + glVertex3f(0, 0, 0) + glVertex3f(s * 1.5, 0, w_offset) + glEnd() + + glPopMatrix() + +# ========================================== +# RENDERIZADO DE ESCENARIO Y MODELOS +# ========================================== +def draw_water_surface(time_counter): + glColor3f(*COLOR_WATER) + + grid_size = 30 + for x in range(int(-POND_RADIUS), int(POND_RADIUS), grid_size): + glBegin(GL_LINE_STRIP) + for y in range(int(-POND_RADIUS), int(POND_RADIUS), grid_size): + dist = math.hypot(x, y) + if dist < POND_RADIUS - 30: + z = 1.0 * math.sin(dist * 0.015 - time_counter * 3.0) + 0.5 * math.cos(x * 0.01 + time_counter) + glVertex3f(x, y, z) + glEnd() + + for y in range(int(STREAM_MIN_Y), int(STREAM_MAX_Y), 20): + if abs(y) < POND_RADIUS: + continue + cx = get_river_center(y) + glBegin(GL_LINE_STRIP) + for dx in range(-100, 110, 15): + x = cx + dx + z = 0.5 * math.sin(y * 0.03 + dx * 0.08 + time_counter * 4.0) + glVertex3f(x, y, z) + glEnd() + +def draw_terrain(): + glColor3f(*COLOR_LAND) + + grid_step = 120 + for y in range(int(STREAM_MIN_Y - 300), int(STREAM_MAX_Y + 300), grid_step): + glBegin(GL_LINE_STRIP) + for x in range(-2500, 2500, grid_step): + z = 1.5 * math.sin(x * 0.005) + 1.5 * math.cos(y * 0.005) + glVertex3f(x, y, z) + glEnd() + + width_river = 100.0 + margin_land = 70.0 + glColor3f(*COLOR_LAND_RIVER) + + for y in range(int(STREAM_MIN_Y), int(STREAM_MAX_Y), 30): + if abs(y) < POND_RADIUS: + continue + cx = get_river_center(y) + + glBegin(GL_LINE_STRIP) + for dx in range(int(-width_river - margin_land), int(-width_river), 15): + x = cx + dx + z = 0.6 * math.sin(y * 0.02 + x * 0.02) + 0.3 + glVertex3f(x, y, z) + glEnd() + + glBegin(GL_LINE_STRIP) + for dx in range(int(width_river), int(width_river + margin_land), 15): + x = cx + dx + z = 0.6 * math.sin(y * 0.02 + x * 0.02) + 0.3 + glVertex3f(x, y, z) + glEnd() + + for ix, iy, ir in ISLANDS: + glColor3f(*COLOR_ISLAND) + for layer in range(1, 4): + rad_layer = ir * (1 - layer * 0.25) + height = layer * 1.2 + glBegin(GL_LINE_LOOP) + for i in range(16): + angle = 2.0 * math.pi * i / 16 + dx = rad_layer * math.cos(angle) + dy = rad_layer * math.sin(angle) + glVertex3f(ix + dx, iy + dy, height) + glEnd() + +def draw_map(time_counter): + draw_terrain() + draw_water_surface(time_counter) + + # Juncos realistas con múltiples varas en distintas direcciones y espigas marrones (cattails) + for rx, ry, rz, rh in REEDS: + # Tallo principal vertical + glColor3f(*COLOR_REED) + glBegin(GL_LINES) + glVertex3f(rx, ry, rz) + glVertex3f(rx, ry, rz + rh) + + # Varas secundarias inclinadas en diferentes direcciones + offsets = [(-1.8, 0.8), (1.5, -0.7), (-0.8, -1.6), (1.2, 1.4)] + for ox, oy in offsets: + glVertex3f(rx, ry, rz) + glVertex3f(rx + ox, ry + oy, rz + rh * 0.8) + glEnd() + + # Espigas / cilindros marrón oscuro en la punta de cada vara + glColor3f(*COLOR_CATTAIL) + glBegin(GL_LINES) + # Punta principal + glVertex3f(rx, ry, rz + rh - 3.5) + glVertex3f(rx, ry, rz + rh) + # Puntas secundarias + for ox, oy in offsets: + glVertex3f(rx + ox, ry + oy, (rz + rh * 0.8) - 3.0) + glVertex3f(rx + ox, ry + oy, rz + rh * 0.8) + glEnd() + +def draw_dragonfly(wing_angle, caught_insect=None): + glColor3f(*COLOR_DRAGONFLY) + + glBegin(GL_LINES) + glVertex3f(0, 1.5, 0) + glVertex3f(0, -3.0, 0) + glVertex3f(-0.4, 0.6, 0) + glVertex3f(0.4, 0.6, 0) + glVertex3f(-0.3, -0.6, 0) + glVertex3f(0.3, -0.6, 0) + + glVertex3f(-0.3, 0.5, 0) + glVertex3f(-0.2, 0.2, -0.6) + glVertex3f(0.3, 0.5, 0) + glVertex3f(0.2, 0.2, -0.6) + glVertex3f(-0.2, -0.2, 0) + glVertex3f(-0.1, -0.3, -0.6) + glVertex3f(0.2, -0.2, 0) + glVertex3f(0.1, -0.3, -0.6) + glEnd() + + glBegin(GL_LINE_LOOP) + for i in range(8): + angle = 2.0 * math.pi * i / 8 + glVertex3f(0.3 * math.cos(angle), 1.4 + 0.3 * math.sin(angle), 0) + glEnd() + + rad_wing = math.radians(wing_angle) + w_z = math.sin(rad_wing) * 1.2 if wing_angle != 0 else 0.0 + + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(-4.5, 1.0, w_z) + glVertex3f(-3.8, 0.2, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(4.5, 1.0, w_z) + glVertex3f(3.8, 0.2, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(-3.8, -0.2, w_z) + glVertex3f(-3.0, -0.8, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(3.8, -0.2, w_z) + glVertex3f(3.0, -0.8, w_z) + glEnd() + + if caught_insect: + glPushMatrix() + glTranslatef(0.0, 0.2, -0.6) + caught_insect.draw(attached=True) + glPopMatrix() + +# ========================================== +# INTERFAZ 2D / RADAR Y TEXTOS FIJOS (LIMPIA) +# ========================================== +def draw_text_2d(text_surface, x, y): + text_data = pygame.image.tostring(text_surface, "RGBA", True) + w, h = text_surface.get_size() + + glEnable(GL_BLEND) + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) + + glRasterPos2i(x, y) + glDrawPixels(w, h, GL_RGBA, GL_UNSIGNED_BYTE, text_data) + glDisable(GL_BLEND) + +def draw_hud(player_pos, yaw, insects, eaten_mosquitos, eaten_flies, font): + glDisable(GL_DEPTH_TEST) + + glMatrixMode(GL_PROJECTION) + glPushMatrix() + glLoadIdentity() + gluOrtho2D(0, WIDTH, 0, HEIGHT) + + glMatrixMode(GL_MODELVIEW) + glPushMatrix() + glLoadIdentity() + + px, py, pz = player_pos + + # 1. RADAR REDUCIDO (Centro Superior) + cx, cy = WIDTH // 2, HEIGHT - 35 + radius = 22 + + glColor3f(*COLOR_HUD) + glBegin(GL_LINE_LOOP) + for i in range(24): + angle = 2.0 * math.pi * i / 24 + glVertex2f(cx + radius * math.cos(angle), cy + radius * math.sin(angle)) + glEnd() + + glBegin(GL_LINES) + glVertex2f(cx, cy + radius) + glVertex2f(cx, cy + radius - 5) + glVertex2f(cx, cy - radius) + glVertex2f(cx, cy - radius + 5) + glVertex2f(cx - radius, cy) + glVertex2f(cx - radius + 5, cy) + glVertex2f(cx + radius, cy) + glVertex2f(cx + radius - 5, cy) + glEnd() + + closest_insect = None + min_dist = 999999.0 + relative_angle = 0.0 + diff_z = 0.0 + + rad_yaw = math.radians(yaw) + + for insect in insects: + dx = insect.x - px + dy = insect.y - py + dist = math.hypot(dx, dy) + + if dist < min_dist and dist > 0.1: + angle_to_insect = math.degrees(math.atan2(dy, dx)) + heading = (angle_to_insect - (90 - yaw)) % 360 + if heading > 180: + heading -= 360 + + if abs(heading) < 60: + min_dist = dist + closest_insect = insect + relative_angle = heading + diff_z = insect.z - pz + + if closest_insect: + radar_x = cx - (relative_angle / 60.0) * (radius - 4) + radar_y = cy + (diff_z / 20.0) * (radius - 4) + + dist_from_center = math.hypot(radar_x - cx, radar_y - cy) + if dist_from_center > (radius - 3): + factor = (radius - 3) / dist_from_center + radar_x = cx + (radar_x - cx) * factor + radar_y = cy + (radar_y - cy) * factor + + glColor3f(1.0, 1.0, 1.0) + glBegin(GL_LINE_LOOP) + for i in range(8): + ang = 2.0 * math.pi * i / 8 + glVertex2f(radar_x + 3 * math.cos(ang), radar_y + 3 * math.sin(ang)) + glEnd() + + # 2. ÍCONOS SUPERIORES (SIN CRUCES, MÁS LIMPIO) + glColor3f(*COLOR_MOSQUITO) + glBegin(GL_LINES) + glVertex2f(25, HEIGHT - 30) + glVertex2f(12, HEIGHT - 30) + glVertex2f(25, HEIGHT - 30) + glVertex2f(38, HEIGHT - 30) + glEnd() + + glColor3f(*COLOR_FLY) + glBegin(GL_LINES) + glVertex2f(WIDTH - 85, HEIGHT - 30) + glVertex2f(WIDTH - 98, HEIGHT - 30) + glVertex2f(WIDTH - 85, HEIGHT - 30) + glVertex2f(WIDTH - 72, HEIGHT - 30) + glEnd() + + # 3. TEXTOS + txt_mosq = font.render(f"x {eaten_mosquitos}", True, (255, 230, 0)) + txt_flies = font.render(f"x {eaten_flies}", True, (200, 65, 40)) + + draw_text_2d(txt_mosq, 45, HEIGHT - 40) + draw_text_2d(txt_flies, WIDTH - 65, HEIGHT - 40) + + glMatrixMode(GL_PROJECTION) + glPopMatrix() + glMatrixMode(GL_MODELVIEW) + glPopMatrix() + glEnable(GL_DEPTH_TEST) + +# ========================================== +# BUCLE PRINCIPAL +# ========================================== +def main(): + pygame.init() + pygame.font.init() + font = pygame.font.SysFont("Arial", 22, bold=True) + + pygame.display.set_mode((WIDTH, HEIGHT), DOUBLEBUF | OPENGL) + pygame.display.set_caption("Simulador de Vuelo de Libélula 3D") + + glEnable(GL_DEPTH_TEST) + glClearColor(*COLOR_BG, 1.0) + + glMatrixMode(GL_PROJECTION) + gluPerspective(50, (WIDTH / HEIGHT), 0.1, 5000.0) + glMatrixMode(GL_MODELVIEW) + + # Estado de la Libélula + px, py, pz = 0.0, 0.0, 15.0 + yaw = 0.0 + roll = 0.0 + pitch = 0.0 + max_roll = 30.0 + max_pitch = 20.0 + + speed = 0.0 + max_speed = 3.0 + acceleration = 0.06 + deceleration = 0.03 + + min_altitude = 1.0 + max_altitude = 120.0 + + wing_timer = 0.0 + water_timer = 0.0 + is_perched = False + + dive_phase = "none" + pre_dive_alt = 15.0 + pre_dive_speed = 0.0 + + caught_insect = None + eaten_mosquitos = 0 + eaten_flies = 0 + + insects = [] + num_mosquitos = random.randint(MOSQUITO_MIN, MOSQUITO_MAX) + num_flies = random.randint(FLY_MIN, FLY_MAX) + + for _ in range(num_mosquitos): + insects.append(Insect("mosquito")) + for _ in range(num_flies): + insects.append(Insect("fly")) + + clock = pygame.time.Clock() + + running = True + while running: + dt = clock.tick(60) / 1000.0 + water_timer += dt + + for event in pygame.event.get(): + if event.type == QUIT or (event.type == KEYDOWN and event.key == K_ESCAPE): + running = False + elif event.type == KEYDOWN and event.key == K_SPACE and not is_perched and dive_phase == "none": + dive_phase = "diving" + pre_dive_alt = pz + pre_dive_speed = speed + + keys = pygame.key.get_pressed() + + if is_perched: + # Flecha Izquierda / Derecha para girar posada + if keys[K_LEFT]: + yaw += 3.0 + if keys[K_RIGHT]: + yaw -= 3.0 + + # W para acelerar y despegar + if keys[K_w]: + is_perched = False + speed = 0.5 + + wing_angle = 0.0 + pitch = 0.0 + roll = 0.0 + dive_phase = "none" + else: + if dive_phase == "diving": + speed = max_speed * 1.5 + pz -= 1.2 + target_pitch = -45.0 + + if pz <= min_altitude: + pz = min_altitude + dive_phase = "recovering" + + elif dive_phase == "recovering": + target_pitch = 30.0 + pz += 0.8 + speed += (pre_dive_speed - speed) * 0.1 + + if pz >= pre_dive_alt: + pz = pre_dive_alt + speed = pre_dive_speed + dive_phase = "none" + else: + # W: Acelerar / S: Frenar + if keys[K_w]: + speed = min(speed + acceleration, max_speed) + elif keys[K_s]: + speed = max(speed - deceleration, 0.0) + + if speed < 0.3: + for rx, ry, rz, rh in REEDS: + dist = math.hypot(px - rx, py - ry) + # Radio de detección ampliado y ajustado a la altura del junco para facilitar el aterrizaje + if dist < 10.0 and abs(pz - (rz + rh)) < 10.0: + px, py, pz = rx, ry, rz + rh + speed = 0.0 + roll = 0.0 + pitch = 0.0 + is_perched = True + + if caught_insect: + if caught_insect.type == "mosquito": + eaten_mosquitos += 1 + else: + eaten_flies += 1 + caught_insect = None + break + + # Flecha Arriba: Subir / Flecha Abajo: Bajar + target_pitch = 0.0 + if keys[K_UP]: + pz = min(pz + 0.8, max_altitude) + target_pitch = max_pitch + elif keys[K_DOWN]: + pz = max(pz - 0.8, min_altitude) + target_pitch = -max_pitch + + pitch += (target_pitch - pitch) * 0.15 + + # Flecha Izquierda / Derecha: Moverse lateralmente o girar en vuelo + target_roll = 0.0 + if keys[K_LEFT]: + yaw += 2.8 + target_roll = -max_roll + elif keys[K_RIGHT]: + yaw -= 2.8 + target_roll = max_roll + + roll += (target_roll - roll) * 0.15 + + rad_yaw = math.radians(yaw) + px += -math.sin(rad_yaw) * speed + py += math.cos(rad_yaw) * speed + + if abs(py) < POND_RADIUS: + max_limit_x = POND_RADIUS + 200.0 + else: + cx = get_river_center(py) + max_limit_x = abs(cx) + 200.0 + + px = max(-max_limit_x, min(max_limit_x, px)) + + if py > STREAM_MAX_Y: + py -= WORLD_LEN + elif py < STREAM_MIN_Y: + py += WORLD_LEN + + wing_timer += 80.0 if dive_phase != "none" else (60.0 if speed > 0.1 else 30.0) + wing_angle = math.sin(wing_timer) * 35.0 + + current_mosquitos = 0 + current_flies = 0 + + for insect in insects[:]: + insect.update((px, py, pz)) + + if insect.type == "mosquito": + current_mosquitos += 1 + else: + current_flies += 1 + + if not caught_insect: + dist_to_dragonfly = math.sqrt((px - insect.x)**2 + (py - insect.y)**2 + (pz - insect.z)**2) + if dist_to_dragonfly < 6.0: + caught_insect = insect + insects.remove(insect) + + if dive_phase == "diving": + dive_phase = "recovering" + + if current_mosquitos < MOSQUITO_MIN: + insects.append(Insect("mosquito")) + if current_flies < FLY_MIN: + insects.append(Insect("fly")) + + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) + glLoadIdentity() + + cam_dist = 18.0 + cam_height = 8.0 + rad_yaw = math.radians(yaw) + + cam_x = px + math.sin(rad_yaw) * cam_dist + cam_y = py - math.cos(rad_yaw) * cam_dist + cam_z = pz + cam_height + + gluLookAt(cam_x, cam_y, cam_z, + px, py, pz + 0.5, + 0, 0, 1) + + draw_map(water_timer) + + for insect in insects: + insect.draw() + + glPushMatrix() + glTranslatef(px, py, pz) + glRotatef(yaw, 0, 0, 1) + glRotatef(pitch, 1, 0, 0) + glRotatef(roll, 0, 1, 0) + draw_dragonfly(wing_angle, caught_insect) + glPopMatrix() + + draw_hud((px, py, pz), yaw, insects, eaten_mosquitos, eaten_flies, font) + + pygame.display.flip() + + pygame.quit() + sys.exit() + +if __name__ == "__main__": + main() diff --git a/previos/dragonfly_game_v6.2.py b/previos/dragonfly_game_v6.2.py new file mode 100644 index 0000000..9f1571d --- /dev/null +++ b/previos/dragonfly_game_v6.2.py @@ -0,0 +1,736 @@ +import sys +import math +import random +import pygame +from pygame.locals import * +from OpenGL.GL import * +from OpenGL.GLU import * + +# ========================================== +# CONFIGURACIÓN Y CONSTANTES +# ========================================== +WIDTH, HEIGHT = 800, 600 + +# Colores (R, G, B) +COLOR_BG = (0.12, 0.15, 0.12) # Fondo ambiente +COLOR_WATER = (0.0, 0.5, 0.85) # Agua animada (Cian/Azul) +COLOR_LAND = (0.45, 0.32, 0.15) # Tierra / Relieve +COLOR_LAND_RIVER = (0.40, 0.28, 0.12) # Tierra en riachuelos +COLOR_ISLAND = (0.5, 0.38, 0.18) # Islas +COLOR_REED = (0.15, 0.85, 0.25) # Juncos verdes +COLOR_CATTAIL = (0.35, 0.18, 0.08) # Espigas marrones densas +COLOR_DRAGONFLY = (0.0, 1.0, 1.0) # Libélula alámbrica (Cian) +COLOR_MOSQUITO = (1.0, 0.9, 0.0) # Mosquitos (Amarillo) +COLOR_FLY = (0.8, 0.25, 0.15) # Moscas (Rojo-Marrón) +COLOR_HUD = (1.0, 1.0, 1.0) # Retícula HUD (Blanco) + +# Dimensiones del Mundo +POND_RADIUS = 1800.0 +STREAM_LEN = 900.0 +STREAM_MIN_Y = -(POND_RADIUS + STREAM_LEN) +STREAM_MAX_Y = (POND_RADIUS + STREAM_LEN) +WORLD_LEN = STREAM_MAX_Y - STREAM_MIN_Y + +# Cantidades de insectos +MOSQUITO_MIN, MOSQUITO_MAX = 10, 20 +FLY_MIN, FLY_MAX = 2, 7 + +# ========================================== +# ELEMENTOS DEL MAPA +# ========================================== +REEDS = [ + (-250, 350, 0, 20), (-160, -500, 0, 22), (500, 200, 0, 24), (-700, 200, 0, 21), (200, -800, 0, 23), + (-900, -300, 0, 25), (760, -600, 0, 22), (900, 500, 0, 26), (-500, 900, 0, 23), (600, -1000, 0, 22), + (-1200, 400, 0, 24), (1300, -300, 0, 20), (-1400, -700, 0, 25), (1100, 1200, 0, 23), (-360, -1300, 0, 22), + (-90, -1950, 0, 20), (-110, -2150, 0, 23), (-80, -2350, 0, 19), (-100, -2550, 0, 22), + (90, 1950, 0, 22), (110, 2150, 0, 20), (85, 2350, 0, 24), (105, 2550, 0, 21) +] + +ISLANDS = [ + (-250, 350, 90), (-160, -500, 85), (500, 200, 100), (-700, 200, 110), (200, -800, 95), + (-900, -300, 120), (760, -600, 90), (900, 500, 105), (-500, 900, 115), (600, -1000, 100) +] + +def get_river_center(y): + if y < -POND_RADIUS: + dy = y + POND_RADIUS + return -50 + 100 * math.sin(dy * 0.008) + elif y > POND_RADIUS: + dy = y - POND_RADIUS + return 50 + 100 * math.sin(dy * 0.008) + else: + return (y / POND_RADIUS) * 50.0 + +def is_over_water(x, y): + dist_center = math.hypot(x, y) + if dist_center < POND_RADIUS - 50: + return True + if abs(y) >= POND_RADIUS: + cx = get_river_center(y) + if abs(x - cx) < 80: + return True + return False + +def get_random_water_position(): + while True: + if random.random() < 0.7: + rad = random.uniform(0, math.pi * 2) + r = random.uniform(100, POND_RADIUS - 150) + x = r * math.cos(rad) + y = r * math.sin(rad) + else: + y = random.choice([random.uniform(POND_RADIUS, STREAM_MAX_Y), random.uniform(STREAM_MIN_Y, -POND_RADIUS)]) + cx = get_river_center(y) + x = cx + random.uniform(-60, 60) + + if is_over_water(x, y): + z = random.uniform(5.0, 35.0) + return x, y, z + +# ========================================== +# CLASES DE INSECTOS +# ========================================== +class Insect: + def __init__(self, insect_type): + self.type = insect_type + self.x, self.y, self.z = get_random_water_position() + self.yaw = random.uniform(0, 360) + self.timer = random.uniform(0, 100) + + if self.type == "mosquito": + self.speed = 1.8 + self.size = 1.2 + else: + self.speed = 0.9 + self.size = 1.6 + + def update(self, player_pos): + self.timer += 0.05 + px, py, pz = player_pos + dist_to_player = math.hypot(self.x - px, self.y - py) + + if self.type == "mosquito": + can_detect = (pz <= self.z) + + if can_detect and dist_to_player < 100: + escape_angle = math.degrees(math.atan2(self.y - py, self.x - px)) + self.yaw += (escape_angle - self.yaw) * 0.05 + + self.yaw += math.sin(self.timer * 3) * 1.5 + self.z += math.sin(self.timer * 1.5) * 0.2 + + elif self.type == "fly": + if dist_to_player < 120: + escape_angle = math.degrees(math.atan2(self.y - py, self.x - px)) + self.yaw = escape_angle + random.uniform(-45, 45) + self.z += random.choice([-1.5, 1.5]) + else: + if random.random() < 0.08: + self.yaw += random.uniform(-60, 60) + + self.yaw += math.sin(self.timer * 6) * 4.0 + self.z += math.cos(self.timer * 4) * 0.6 + + self.z = max(3.0, min(50.0, self.z)) + + rad = math.radians(self.yaw) + next_x = self.x - math.sin(rad) * self.speed + next_y = self.y + math.cos(rad) * self.speed + + if is_over_water(next_x, next_y): + self.x, self.y = next_x, next_y + else: + self.yaw += 140.0 + random.uniform(-20, 20) + + if self.y > STREAM_MAX_Y: + self.y -= WORLD_LEN + elif self.y < STREAM_MIN_Y: + self.y += WORLD_LEN + + def draw(self, attached=False): + if self.type == "mosquito": + glColor3f(*COLOR_MOSQUITO) + else: + glColor3f(*COLOR_FLY) + + glPushMatrix() + if not attached: + glTranslatef(self.x, self.y, self.z) + glRotatef(self.yaw, 0, 0, 1) + + s = self.size + glBegin(GL_LINES) + glVertex3f(0, s, 0) + glVertex3f(0, -s, 0) + w_offset = 0.0 if attached else math.sin(self.timer * 30) * (s * 0.8) + glVertex3f(0, 0, 0) + glVertex3f(-s * 1.5, 0, w_offset) + glVertex3f(0, 0, 0) + glVertex3f(s * 1.5, 0, w_offset) + glEnd() + + glPopMatrix() + +# ========================================== +# RENDERIZADO DE ESCENARIO Y MODELOS +# ========================================== +def draw_water_surface(time_counter): + glColor3f(*COLOR_WATER) + + grid_size = 30 + for x in range(int(-POND_RADIUS), int(POND_RADIUS), grid_size): + glBegin(GL_LINE_STRIP) + for y in range(int(-POND_RADIUS), int(POND_RADIUS), grid_size): + dist = math.hypot(x, y) + if dist < POND_RADIUS - 30: + z = 1.0 * math.sin(dist * 0.015 - time_counter * 3.0) + 0.5 * math.cos(x * 0.01 + time_counter) + glVertex3f(x, y, z) + glEnd() + + for y in range(int(STREAM_MIN_Y), int(STREAM_MAX_Y), 20): + if abs(y) < POND_RADIUS: + continue + cx = get_river_center(y) + glBegin(GL_LINE_STRIP) + for dx in range(-100, 110, 15): + x = cx + dx + z = 0.5 * math.sin(y * 0.03 + dx * 0.08 + time_counter * 4.0) + glVertex3f(x, y, z) + glEnd() + +def draw_terrain(): + glColor3f(*COLOR_LAND) + + grid_step = 120 + for y in range(int(STREAM_MIN_Y - 300), int(STREAM_MAX_Y + 300), grid_step): + glBegin(GL_LINE_STRIP) + for x in range(-2500, 2500, grid_step): + z = 1.5 * math.sin(x * 0.005) + 1.5 * math.cos(y * 0.005) + glVertex3f(x, y, z) + glEnd() + + width_river = 100.0 + margin_land = 70.0 + glColor3f(*COLOR_LAND_RIVER) + + for y in range(int(STREAM_MIN_Y), int(STREAM_MAX_Y), 30): + if abs(y) < POND_RADIUS: + continue + cx = get_river_center(y) + + glBegin(GL_LINE_STRIP) + for dx in range(int(-width_river - margin_land), int(-width_river), 15): + x = cx + dx + z = 0.6 * math.sin(y * 0.02 + x * 0.02) + 0.3 + glVertex3f(x, y, z) + glEnd() + + glBegin(GL_LINE_STRIP) + for dx in range(int(width_river), int(width_river + margin_land), 15): + x = cx + dx + z = 0.6 * math.sin(y * 0.02 + x * 0.02) + 0.3 + glVertex3f(x, y, z) + glEnd() + + for ix, iy, ir in ISLANDS: + glColor3f(*COLOR_ISLAND) + for layer in range(1, 4): + rad_layer = ir * (1 - layer * 0.25) + height = layer * 1.2 + glBegin(GL_LINE_LOOP) + for i in range(16): + angle = 2.0 * math.pi * i / 16 + dx = rad_layer * math.cos(angle) + dy = rad_layer * math.sin(angle) + glVertex3f(ix + dx, iy + dy, height) + glEnd() + +def draw_map(time_counter): + draw_terrain() + draw_water_surface(time_counter) + + offsets = [(-1.5, 0.6), (1.3, -0.6), (-0.7, -1.4), (1.1, 1.2), (0.0, 1.6), (-1.2, 0.0)] + + for rx, ry, rz, rh in REEDS: + glColor3f(*COLOR_REED) + glBegin(GL_LINES) + glVertex3f(rx, ry, rz) + glVertex3f(rx, ry, rz + rh) + for ox, oy in offsets: + glVertex3f(rx, ry, rz) + glVertex3f(rx + ox, ry + oy, rz + rh * 0.85) + glEnd() + + glColor3f(*COLOR_CATTAIL) + glBegin(GL_LINES) + for d in [-0.2, 0.0, 0.2]: + glVertex3f(rx + d, ry, rz + rh - 4.5) + glVertex3f(rx + d, ry, rz + rh) + for ox, oy in offsets: + for d in [-0.15, 0.15]: + glVertex3f(rx + ox + d, ry + oy, (rz + rh * 0.85) - 3.8) + glVertex3f(rx + ox + d, ry + oy, rz + rh * 0.85) + glEnd() + +def draw_dragonfly(wing_angle, caught_insect=None): + glColor3f(*COLOR_DRAGONFLY) + + glBegin(GL_LINES) + glVertex3f(0, 1.5, 0) + glVertex3f(0, -3.0, 0) + glVertex3f(-0.4, 0.6, 0) + glVertex3f(0.4, 0.6, 0) + glVertex3f(-0.3, -0.6, 0) + glVertex3f(0.3, -0.6, 0) + + glVertex3f(-0.3, 0.5, 0) + glVertex3f(-0.2, 0.2, -0.6) + glVertex3f(0.3, 0.5, 0) + glVertex3f(0.2, 0.2, -0.6) + glVertex3f(-0.2, -0.2, 0) + glVertex3f(-0.1, -0.3, -0.6) + glVertex3f(0.2, -0.2, 0) + glVertex3f(0.1, -0.3, -0.6) + glEnd() + + glBegin(GL_LINE_LOOP) + for i in range(8): + angle = 2.0 * math.pi * i / 8 + glVertex3f(0.3 * math.cos(angle), 1.4 + 0.3 * math.sin(angle), 0) + glEnd() + + rad_wing = math.radians(wing_angle) + w_z = math.sin(rad_wing) * 1.2 if wing_angle != 0 else 0.0 + + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(-4.5, 1.0, w_z) + glVertex3f(-3.8, 0.2, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(4.5, 1.0, w_z) + glVertex3f(3.8, 0.2, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(-3.8, -0.2, w_z) + glVertex3f(-3.0, -0.8, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(3.8, -0.2, w_z) + glVertex3f(3.0, -0.8, w_z) + glEnd() + + if caught_insect: + glPushMatrix() + glTranslatef(0.0, 0.2, -0.6) + caught_insect.draw(attached=True) + glPopMatrix() + +# ========================================== +# INTERFAZ 2D / HUD +# ========================================== +def draw_text_2d(text_surface, x, y): + text_data = pygame.image.tostring(text_surface, "RGBA", True) + w, h = text_surface.get_size() + + glEnable(GL_BLEND) + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) + + glRasterPos2i(x, y) + glDrawPixels(w, h, GL_RGBA, GL_UNSIGNED_BYTE, text_data) + glDisable(GL_BLEND) + +def draw_hud(player_pos, yaw, insects, eaten_mosquitos, eaten_flies, font): + glDisable(GL_DEPTH_TEST) + + glMatrixMode(GL_PROJECTION) + glPushMatrix() + glLoadIdentity() + gluOrtho2D(0, WIDTH, 0, HEIGHT) + + glMatrixMode(GL_MODELVIEW) + glPushMatrix() + glLoadIdentity() + + px, py, pz = player_pos + + # RADAR REDUCIDO + cx, cy = WIDTH // 2, HEIGHT - 35 + radius = 22 + + glColor3f(*COLOR_HUD) + glBegin(GL_LINE_LOOP) + for i in range(24): + angle = 2.0 * math.pi * i / 24 + glVertex2f(cx + radius * math.cos(angle), cy + radius * math.sin(angle)) + glEnd() + + glBegin(GL_LINES) + glVertex2f(cx, cy + radius) + glVertex2f(cx, cy + radius - 5) + glVertex2f(cx, cy - radius) + glVertex2f(cx, cy - radius + 5) + glVertex2f(cx - radius, cy) + glVertex2f(cx - radius + 5, cy) + glVertex2f(cx + radius, cy) + glVertex2f(cx + radius - 5, cy) + glEnd() + + closest_insect = None + min_dist = 999999.0 + relative_angle = 0.0 + diff_z = 0.0 + + for insect in insects: + dx = insect.x - px + dy = insect.y - py + dist = math.hypot(dx, dy) + + if dist < min_dist and dist > 0.1: + angle_to_insect = math.degrees(math.atan2(dy, dx)) + heading = (angle_to_insect - (90 - yaw)) % 360 + if heading > 180: + heading -= 360 + + if abs(heading) < 60: + min_dist = dist + closest_insect = insect + relative_angle = heading + diff_z = insect.z - pz + + if closest_insect: + radar_x = cx - (relative_angle / 60.0) * (radius - 4) + radar_y = cy + (diff_z / 20.0) * (radius - 4) + + dist_from_center = math.hypot(radar_x - cx, radar_y - cy) + if dist_from_center > (radius - 3): + factor = (radius - 3) / dist_from_center + radar_x = cx + (radar_x - cx) * factor + radar_y = cy + (radar_y - cy) * factor + + glColor3f(1.0, 1.0, 1.0) + glBegin(GL_LINE_LOOP) + for i in range(8): + ang = 2.0 * math.pi * i / 8 + glVertex2f(radar_x + 3 * math.cos(ang), radar_y + 3 * math.sin(ang)) + glEnd() + + # ÍCONOS SUPERIORES + glColor3f(*COLOR_MOSQUITO) + glBegin(GL_LINES) + glVertex2f(25, HEIGHT - 30) + glVertex2f(12, HEIGHT - 30) + glVertex2f(25, HEIGHT - 30) + glVertex2f(38, HEIGHT - 30) + glEnd() + + glColor3f(*COLOR_FLY) + glBegin(GL_LINES) + glVertex2f(WIDTH - 85, HEIGHT - 30) + glVertex2f(WIDTH - 98, HEIGHT - 30) + glVertex2f(WIDTH - 85, HEIGHT - 30) + glVertex2f(WIDTH - 72, HEIGHT - 30) + glEnd() + + # TEXTOS + txt_mosq = font.render(f"x {eaten_mosquitos}", True, (255, 230, 0)) + txt_flies = font.render(f"x {eaten_flies}", True, (200, 65, 40)) + + draw_text_2d(txt_mosq, 45, HEIGHT - 40) + draw_text_2d(txt_flies, WIDTH - 65, HEIGHT - 40) + + glMatrixMode(GL_PROJECTION) + glPopMatrix() + glMatrixMode(GL_MODELVIEW) + glPopMatrix() + glEnable(GL_DEPTH_TEST) + +# ========================================== +# BUCLE PRINCIPAL +# ========================================== +def main(): + pygame.init() + pygame.font.init() + font = pygame.font.SysFont("Arial", 22, bold=True) + + pygame.display.set_mode((WIDTH, HEIGHT), DOUBLEBUF | OPENGL) + pygame.display.set_caption("Simulador de Vuelo de Libélula 3D") + + glEnable(GL_DEPTH_TEST) + glClearColor(*COLOR_BG, 1.0) + + glMatrixMode(GL_PROJECTION) + gluPerspective(50, (WIDTH / HEIGHT), 0.1, 5000.0) + glMatrixMode(GL_MODELVIEW) + + # Estado de la Libélula + px, py, pz = 0.0, 0.0, 15.0 + yaw = 0.0 + roll = 0.0 + pitch = 0.0 + max_roll = 30.0 + max_pitch = 20.0 + + speed = 0.0 + max_speed = 3.0 + acceleration = 0.06 + deceleration = 0.03 + + min_altitude = 1.0 + max_altitude = 120.0 + + wing_timer = 0.0 + water_timer = 0.0 + is_perched = False + + dive_phase = "none" + pre_dive_alt = 15.0 + pre_dive_speed = 0.0 + + auto_landing = False + target_perch = None + + caught_insect = None + eaten_mosquitos = 0 + eaten_flies = 0 + + insects = [] + num_mosquitos = random.randint(MOSQUITO_MIN, MOSQUITO_MAX) + num_flies = random.randint(FLY_MIN, FLY_MAX) + + for _ in range(num_mosquitos): + insects.append(Insect("mosquito")) + for _ in range(num_flies): + insects.append(Insect("fly")) + + clock = pygame.time.Clock() + + running = True + while running: + dt = clock.tick(60) / 1000.0 + water_timer += dt + + for event in pygame.event.get(): + if event.type == QUIT or (event.type == KEYDOWN and event.key == K_ESCAPE): + running = False + elif event.type == KEYDOWN and event.key == K_SPACE: + if not is_perched and dive_phase == "none": + dive_phase = "diving" + pre_dive_alt = pz + pre_dive_speed = speed + auto_landing = False + elif event.type == KEYDOWN and event.key == K_p: + if not is_perched and dive_phase == "none": + best_reed = None + min_d = 999.0 + for rx, ry, rz, rh in REEDS: + dist = math.hypot(px - rx, py - ry) + if dist < 35.0: + angle_to_reed = math.degrees(math.atan2(ry - py, rx - px)) + heading = (angle_to_reed - (90 - yaw)) % 360 + if heading > 180: + heading -= 360 + if abs(heading) < 45 and dist < min_d: + min_d = dist + best_reed = (rx, ry, rz + rh) + + if best_reed: + target_perch = best_reed + auto_landing = True + + keys = pygame.key.get_pressed() + + if is_perched: + if keys[K_LEFT]: + yaw += 3.0 + if keys[K_RIGHT]: + yaw -= 3.0 + + if keys[K_w]: + is_perched = False + speed = 0.5 + + wing_angle = 0.0 + pitch = 0.0 + roll = 0.0 + dive_phase = "none" + auto_landing = False + else: + if auto_landing and target_perch: + tx, ty, tz = target_perch + dx = tx - px + dy = ty - py + dist_to_target = math.hypot(dx, dy) + + speed = 1.2 + px += (dx / dist_to_target) * speed if dist_to_target > 0.1 else 0 + py += (dy / dist_to_target) * speed if dist_to_target > 0.1 else 0 + pz += (tz - pz) * 0.1 + + if dist_to_target < 2.5 and abs(pz - tz) < 2.5: + px, py, pz = target_perch + speed = 0.0 + roll = 0.0 + pitch = 0.0 + is_perched = True + auto_landing = False + target_perch = None + + if caught_insect: + if caught_insect.type == "mosquito": + eaten_mosquitos += 1 + else: + eaten_flies += 1 + caught_insect = None + + elif dive_phase == "diving": + speed = max_speed * 1.5 + pz -= 1.2 + target_pitch = -45.0 + + if pz <= min_altitude: + pz = min_altitude + dive_phase = "recovering" + + elif dive_phase == "recovering": + target_pitch = 30.0 + pz += 0.8 + speed += (pre_dive_speed - speed) * 0.1 + + if pz >= pre_dive_alt: + pz = pre_dive_alt + speed = pre_dive_speed + dive_phase = "none" + else: + if keys[K_w]: + speed = min(speed + acceleration, max_speed) + elif keys[K_s]: + speed = max(speed - deceleration, 0.0) + + if speed < 0.3: + for rx, ry, rz, rh in REEDS: + dist = math.hypot(px - rx, py - ry) + if dist < 10.0 and abs(pz - (rz + rh)) < 10.0: + px, py, pz = rx, ry, rz + rh + speed = 0.0 + roll = 0.0 + pitch = 0.0 + is_perched = True + + if caught_insect: + if caught_insect.type == "mosquito": + eaten_mosquitos += 1 + else: + eaten_flies += 1 + caught_insect = None + break + + target_pitch = 0.0 + if keys[K_UP]: + pz = min(pz + 0.8, max_altitude) + target_pitch = max_pitch + elif keys[K_DOWN]: + pz = max(pz - 0.8, min_altitude) + target_pitch = -max_pitch + + if not auto_landing: + pitch += (target_pitch - pitch) * 0.15 + + target_roll = 0.0 + if keys[K_LEFT]: + yaw += 2.8 + target_roll = -max_roll + elif keys[K_RIGHT]: + yaw -= 2.8 + target_roll = max_roll + + roll += (target_roll - roll) * 0.15 + + rad_yaw = math.radians(yaw) + px += -math.sin(rad_yaw) * speed + py += math.cos(rad_yaw) * speed + + if abs(py) < POND_RADIUS: + max_limit_x = POND_RADIUS + 200.0 + else: + cx = get_river_center(py) + max_limit_x = abs(cx) + 200.0 + + px = max(-max_limit_x, min(max_limit_x, px)) + + if py > STREAM_MAX_Y: + py -= WORLD_LEN + elif py < STREAM_MIN_Y: + py += WORLD_LEN + + wing_timer += 80.0 if (dive_phase != "none" or auto_landing) else (60.0 if speed > 0.1 else 30.0) + wing_angle = math.sin(wing_timer) * 35.0 + + current_mosquitos = 0 + current_flies = 0 + + for insect in insects[:]: + insect.update((px, py, pz)) + + if insect.type == "mosquito": + current_mosquitos += 1 + else: + current_flies += 1 + + if not caught_insect: + dist_to_dragonfly = math.sqrt((px - insect.x)**2 + (py - insect.y)**2 + (pz - insect.z)**2) + if dist_to_dragonfly < 6.0: + caught_insect = insect + insects.remove(insect) + + if dive_phase == "diving": + dive_phase = "recovering" + + if current_mosquitos < MOSQUITO_MIN: + insects.append(Insect("mosquito")) + if current_flies < FLY_MIN: + insects.append(Insect("fly")) + + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) + glLoadIdentity() + + cam_dist = 18.0 + cam_height = 8.0 + rad_yaw = math.radians(yaw) + + cam_x = px + math.sin(rad_yaw) * cam_dist + cam_y = py - math.cos(rad_yaw) * cam_dist + cam_z = pz + cam_height + + gluLookAt(cam_x, cam_y, cam_z, + px, py, pz + 0.5, + 0, 0, 1) + + draw_map(water_timer) + + for insect in insects: + insect.draw() + + glPushMatrix() + glTranslatef(px, py, pz) + glRotatef(yaw, 0, 0, 1) + glRotatef(pitch, 1, 0, 0) + glRotatef(roll, 0, 1, 0) + draw_dragonfly(wing_angle, caught_insect) + glPopMatrix() + + draw_hud((px, py, pz), yaw, insects, eaten_mosquitos, eaten_flies, font) + + pygame.display.flip() + + pygame.quit() + sys.exit() + +if __name__ == "__main__": + main() diff --git a/previos/dragonfly_game_v6.3.py b/previos/dragonfly_game_v6.3.py new file mode 100644 index 0000000..0b8c8b8 --- /dev/null +++ b/previos/dragonfly_game_v6.3.py @@ -0,0 +1,797 @@ +import sys +import math +import random +import pygame +from pygame.locals import * +from OpenGL.GL import * +from OpenGL.GLU import * + +# ========================================== +# CONFIGURACIÓN Y CONSTANTES +# ========================================== +WIDTH, HEIGHT = 800, 600 + +# Colores (R, G, B) +COLOR_BG = (0.12, 0.15, 0.12) # Fondo ambiente +COLOR_WATER = (0.0, 0.5, 0.85) # Agua animada (Cian/Azul) +COLOR_LAND = (0.45, 0.32, 0.15) # Tierra / Relieve +COLOR_LAND_RIVER = (0.40, 0.28, 0.12) # Tierra en riachuelos +COLOR_ISLAND = (0.5, 0.38, 0.18) # Islas +COLOR_REED = (0.15, 0.85, 0.25) # Juncos verdes +COLOR_CATTAIL = (0.35, 0.18, 0.08) # Espigas marrones densas +COLOR_DRAGONFLY = (0.0, 1.0, 1.0) # Libélula alámbrica (Cian) +COLOR_MOSQUITO = (1.0, 0.9, 0.0) # Mosquitos (Amarillo) +COLOR_FLY = (0.8, 0.25, 0.15) # Moscas (Rojo-Marrón) +COLOR_FROG = (0.1, 0.9, 0.3) # Rana verde decorativa +COLOR_HUD = (1.0, 1.0, 1.0) # Retícula HUD (Blanco) + +# Dimensiones del Mundo +POND_RADIUS = 1800.0 +STREAM_LEN = 900.0 +STREAM_MIN_Y = -(POND_RADIUS + STREAM_LEN) +STREAM_MAX_Y = (POND_RADIUS + STREAM_LEN) +WORLD_LEN = STREAM_MAX_Y - STREAM_MIN_Y + +# Cantidades de insectos +MOSQUITO_MIN, MOSQUITO_MAX = 10, 20 +FLY_MIN, FLY_MAX = 2, 7 + +# ========================================== +# ELEMENTOS DEL MAPA +# ========================================== +REEDS = [ + (-250, 350, 0, 20), (-160, -500, 0, 22), (500, 200, 0, 24), (-700, 200, 0, 21), (200, -800, 0, 23), + (-900, -300, 0, 25), (760, -600, 0, 22), (900, 500, 0, 26), (-500, 900, 0, 23), (600, -1000, 0, 22), + (-1200, 400, 0, 24), (1300, -300, 0, 20), (-1400, -700, 0, 25), (1100, 1200, 0, 23), (-360, -1300, 0, 22), + (-90, -1950, 0, 20), (-110, -2150, 0, 23), (-80, -2350, 0, 19), (-100, -2550, 0, 22), + (90, 1950, 0, 22), (110, 2150, 0, 20), (85, 2350, 0, 24), (105, 2550, 0, 21) +] + +ISLANDS = [ + (-250, 350, 90), (-160, -500, 85), (500, 200, 100), (-700, 200, 110), (200, -800, 95), + (-900, -300, 120), (760, -600, 90), (900, 500, 105), (-500, 900, 115), (600, -1000, 100) +] + +def get_river_center(y): + if y < -POND_RADIUS: + dy = y + POND_RADIUS + return -50 + 100 * math.sin(dy * 0.008) + elif y > POND_RADIUS: + dy = y - POND_RADIUS + return 50 + 100 * math.sin(dy * 0.008) + else: + return (y / POND_RADIUS) * 50.0 + +def is_over_water(x, y): + dist_center = math.hypot(x, y) + if dist_center < POND_RADIUS - 50: + return True + if abs(y) >= POND_RADIUS: + cx = get_river_center(y) + if abs(x - cx) < 80: + return True + return False + +def get_random_water_position(): + while True: + if random.random() < 0.7: + rad = random.uniform(0, math.pi * 2) + r = random.uniform(100, POND_RADIUS - 150) + x = r * math.cos(rad) + y = r * math.sin(rad) + else: + y = random.choice([random.uniform(POND_RADIUS, STREAM_MAX_Y), random.uniform(STREAM_MIN_Y, -POND_RADIUS)]) + cx = get_river_center(y) + x = cx + random.uniform(-60, 60) + + if is_over_water(x, y): + z = random.uniform(5.0, 35.0) + return x, y, z + +# ========================================== +# CLASES DE INSECTOS Y RANA DECORATIVA +# ========================================== +class Insect: + def __init__(self, insect_type): + self.type = insect_type + self.x, self.y, self.z = get_random_water_position() + self.yaw = random.uniform(0, 360) + self.timer = random.uniform(0, 100) + + if self.type == "mosquito": + self.speed = 1.8 + self.size = 1.2 + else: + self.speed = 0.9 + self.size = 1.6 + + def update(self, player_pos): + self.timer += 0.05 + px, py, pz = player_pos + dist_to_player = math.hypot(self.x - px, self.y - py) + + if self.type == "mosquito": + can_detect = (pz <= self.z) + if can_detect and dist_to_player < 100: + escape_angle = math.degrees(math.atan2(self.y - py, self.x - px)) + self.yaw += (escape_angle - self.yaw) * 0.05 + + self.yaw += math.sin(self.timer * 3) * 1.5 + self.z += math.sin(self.timer * 1.5) * 0.2 + + elif self.type == "fly": + if dist_to_player < 120: + escape_angle = math.degrees(math.atan2(self.y - py, self.x - px)) + self.yaw = escape_angle + random.uniform(-45, 45) + self.z += random.choice([-1.5, 1.5]) + else: + if random.random() < 0.08: + self.yaw += random.uniform(-60, 60) + + self.yaw += math.sin(self.timer * 6) * 4.0 + self.z += math.cos(self.timer * 4) * 0.6 + + self.z = max(3.0, min(50.0, self.z)) + + rad = math.radians(self.yaw) + next_x = self.x - math.sin(rad) * self.speed + next_y = self.y + math.cos(rad) * self.speed + + if is_over_water(next_x, next_y): + self.x, self.y = next_x, next_y + else: + self.yaw += 140.0 + random.uniform(-20, 20) + + if self.y > STREAM_MAX_Y: + self.y -= WORLD_LEN + elif self.y < STREAM_MIN_Y: + self.y += WORLD_LEN + + def draw(self, attached=False): + if self.type == "mosquito": + glColor3f(*COLOR_MOSQUITO) + else: + glColor3f(*COLOR_FLY) + + glPushMatrix() + if not attached: + glTranslatef(self.x, self.y, self.z) + glRotatef(self.yaw, 0, 0, 1) + + s = self.size + glBegin(GL_LINES) + glVertex3f(0, s, 0) + glVertex3f(0, -s, 0) + w_offset = 0.0 if attached else math.sin(self.timer * 30) * (s * 0.8) + glVertex3f(0, 0, 0) + glVertex3f(-s * 1.5, 0, w_offset) + glVertex3f(0, 0, 0) + glVertex3f(s * 1.5, 0, w_offset) + glEnd() + + glPopMatrix() + +class Frog: + def __init__(self): + # Seleccionar dos puntos aleatorios de inicio y destino entre islas y juncos + self.pick_new_destination() + self.x, self.y = self.target_x, self.target_y + self.z = 0.5 + self.speed = 0.4 + self.yaw = 0.0 + + def pick_new_destination(self): + # Destinos posibles: islas o posiciones de juncos + destinations = [(rx, ry) for rx, ry, _, _ in REEDS] + [(ix, iy) for ix, iy, _ in ISLANDS] + dest = random.choice(destinations) + self.target_x, self.target_y = dest[0] + random.uniform(-20, 20), dest[1] + random.uniform(-20, 20) + + def update(self): + dx = self.target_x - self.x + dy = self.target_y - self.y + dist = math.hypot(dx, dy) + + if dist < 5.0: + self.pick_new_destination() + else: + self.yaw = math.degrees(math.atan2(dy, dx)) + self.x += (dx / dist) * self.speed + self.y += (dy / dist) * self.speed + + def draw(self): + glColor3f(*COLOR_FROG) + glPushMatrix() + glTranslatef(self.x, self.y, self.z) + glRotatef(self.yaw, 0, 0, 1) + + # Pequeño modelo alámbrico de rana + glBegin(GL_LINES) + # Cuerpo + glVertex3f(0, 2.0, 0) + glVertex3f(0, -2.0, 0) + glVertex3f(-1.5, 1.0, 0) + glVertex3f(1.5, 1.0, 0) + glVertex3f(-1.5, -1.0, 0) + glVertex3f(1.5, -1.0, 0) + # Ojos saltones + glVertex3f(-0.8, 1.8, 0.5) + glVertex3f(-0.8, 1.8, 0) + glVertex3f(0.8, 1.8, 0.5) + glVertex3f(0.8, 1.8, 0) + glEnd() + + glPopMatrix() + +# ========================================== +# RENDERIZADO DE ESCENARIO Y MODELOS +# ========================================== +def draw_water_surface(time_counter): + """Malla de agua con mayor densidad de líneas para mayor realismo visual.""" + glColor3f(*COLOR_WATER) + + grid_size = 15 # Reducido de 30 a 15 para duplicar la densidad de líneas + for x in range(int(-POND_RADIUS), int(POND_RADIUS), grid_size * 2): + glBegin(GL_LINE_STRIP) + for y in range(int(-POND_RADIUS), int(POND_RADIUS), grid_size): + dist = math.hypot(x, y) + if dist < POND_RADIUS - 30: + z = 1.0 * math.sin(dist * 0.015 - time_counter * 3.0) + 0.5 * math.cos(x * 0.01 + time_counter) + glVertex3f(x, y, z) + glEnd() + + for y in range(int(STREAM_MIN_Y), int(STREAM_MAX_Y), 15): + if abs(y) < POND_RADIUS: + continue + cx = get_river_center(y) + glBegin(GL_LINE_STRIP) + for dx in range(-100, 110, 10): + x = cx + dx + z = 0.5 * math.sin(y * 0.03 + dx * 0.08 + time_counter * 4.0) + glVertex3f(x, y, z) + glEnd() + +def draw_terrain(): + glColor3f(*COLOR_LAND) + + grid_step = 120 + for y in range(int(STREAM_MIN_Y - 300), int(STREAM_MAX_Y + 300), grid_step): + glBegin(GL_LINE_STRIP) + for x in range(-2500, 2500, grid_step): + z = 1.5 * math.sin(x * 0.005) + 1.5 * math.cos(y * 0.005) + glVertex3f(x, y, z) + glEnd() + + width_river = 100.0 + margin_land = 70.0 + glColor3f(*COLOR_LAND_RIVER) + + for y in range(int(STREAM_MIN_Y), int(STREAM_MAX_Y), 30): + if abs(y) < POND_RADIUS: + continue + cx = get_river_center(y) + + glBegin(GL_LINE_STRIP) + for dx in range(int(-width_river - margin_land), int(-width_river), 15): + x = cx + dx + z = 0.6 * math.sin(y * 0.02 + x * 0.02) + 0.3 + glVertex3f(x, y, z) + glEnd() + + glBegin(GL_LINE_STRIP) + for dx in range(int(width_river), int(width_river + margin_land), 15): + x = cx + dx + z = 0.6 * math.sin(y * 0.02 + x * 0.02) + 0.3 + glVertex3f(x, y, z) + glEnd() + + for ix, iy, ir in ISLANDS: + glColor3f(*COLOR_ISLAND) + for layer in range(1, 4): + rad_layer = ir * (1 - layer * 0.25) + height = layer * 1.2 + glBegin(GL_LINE_LOOP) + for i in range(16): + angle = 2.0 * math.pi * i / 16 + dx = rad_layer * math.cos(angle) + dy = rad_layer * math.sin(angle) + glVertex3f(ix + dx, iy + dy, height) + glEnd() + +def draw_map(time_counter): + draw_terrain() + draw_water_surface(time_counter) + + offsets = [(-1.5, 0.6), (1.3, -0.6), (-0.7, -1.4), (1.1, 1.2), (0.0, 1.6), (-1.2, 0.0)] + + for rx, ry, rz, rh in REEDS: + glColor3f(*COLOR_REED) + glBegin(GL_LINES) + glVertex3f(rx, ry, rz) + glVertex3f(rx, ry, rz + rh) + for ox, oy in offsets: + glVertex3f(rx, ry, rz) + glVertex3f(rx + ox, ry + oy, rz + rh * 0.85) + glEnd() + + glColor3f(*COLOR_CATTAIL) + glBegin(GL_LINES) + for d in [-0.2, 0.0, 0.2]: + glVertex3f(rx + d, ry, rz + rh - 4.5) + glVertex3f(rx + d, ry, rz + rh) + for ox, oy in offsets: + for d in [-0.15, 0.15]: + glVertex3f(rx + ox + d, ry + oy, (rz + rh * 0.85) - 3.8) + glVertex3f(rx + ox + d, ry + oy, rz + rh * 0.85) + glEnd() + +def draw_dragonfly(wing_angle, caught_insect=None): + glColor3f(*COLOR_DRAGONFLY) + + glBegin(GL_LINES) + glVertex3f(0, 1.5, 0) + glVertex3f(0, -3.0, 0) + glVertex3f(-0.4, 0.6, 0) + glVertex3f(0.4, 0.6, 0) + glVertex3f(-0.3, -0.6, 0) + glVertex3f(0.3, -0.6, 0) + + glVertex3f(-0.3, 0.5, 0) + glVertex3f(-0.2, 0.2, -0.6) + glVertex3f(0.3, 0.5, 0) + glVertex3f(0.2, 0.2, -0.6) + glVertex3f(-0.2, -0.2, 0) + glVertex3f(-0.1, -0.3, -0.6) + glVertex3f(0.2, -0.2, 0) + glVertex3f(0.1, -0.3, -0.6) + glEnd() + + glBegin(GL_LINE_LOOP) + for i in range(8): + angle = 2.0 * math.pi * i / 8 + glVertex3f(0.3 * math.cos(angle), 1.4 + 0.3 * math.sin(angle), 0) + glEnd() + + rad_wing = math.radians(wing_angle) + w_z = math.sin(rad_wing) * 1.2 if wing_angle != 0 else 0.0 + + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(-4.5, 1.0, w_z) + glVertex3f(-3.8, 0.2, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(4.5, 1.0, w_z) + glVertex3f(3.8, 0.2, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(-3.8, -0.2, w_z) + glVertex3f(-3.0, -0.8, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(3.8, -0.2, w_z) + glVertex3f(3.0, -0.8, w_z) + glEnd() + + if caught_insect: + glPushMatrix() + glTranslatef(0.0, 0.2, -0.6) + caught_insect.draw(attached=True) + glPopMatrix() + +# ========================================== +# INTERFAZ 2D / HUD +# ========================================== +def draw_text_2d(text_surface, x, y): + text_data = pygame.image.tostring(text_surface, "RGBA", True) + w, h = text_surface.get_size() + + glEnable(GL_BLEND) + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) + + glRasterPos2i(x, y) + glDrawPixels(w, h, GL_RGBA, GL_UNSIGNED_BYTE, text_data) + glDisable(GL_BLEND) + +def draw_hud(player_pos, yaw, insects, eaten_mosquitos, eaten_flies, font): + glDisable(GL_DEPTH_TEST) + + glMatrixMode(GL_PROJECTION) + glPushMatrix() + glLoadIdentity() + gluOrtho2D(0, WIDTH, 0, HEIGHT) + + glMatrixMode(GL_MODELVIEW) + glPushMatrix() + glLoadIdentity() + + px, py, pz = player_pos + + # RADAR REDUCIDO + cx, cy = WIDTH // 2, HEIGHT - 35 + radius = 22 + + glColor3f(*COLOR_HUD) + glBegin(GL_LINE_LOOP) + for i in range(24): + angle = 2.0 * math.pi * i / 24 + glVertex2f(cx + radius * math.cos(angle), cy + radius * math.sin(angle)) + glEnd() + + glBegin(GL_LINES) + glVertex2f(cx, cy + radius) + glVertex2f(cx, cy + radius - 5) + glVertex2f(cx, cy - radius) + glVertex2f(cx, cy - radius + 5) + glVertex2f(cx - radius, cy) + glVertex2f(cx - radius + 5, cy) + glVertex2f(cx + radius, cy) + glVertex2f(cx + radius - 5, cy) + glEnd() + + closest_insect = None + min_dist = 999999.0 + relative_angle = 0.0 + diff_z = 0.0 + + for insect in insects: + dx = insect.x - px + dy = insect.y - py + dist = math.hypot(dx, dy) + + if dist < min_dist and dist > 0.1: + angle_to_insect = math.degrees(math.atan2(dy, dx)) + heading = (angle_to_insect - (90 - yaw)) % 360 + if heading > 180: + heading -= 360 + + if abs(heading) < 60: + min_dist = dist + closest_insect = insect + relative_angle = heading + diff_z = insect.z - pz + + if closest_insect: + radar_x = cx - (relative_angle / 60.0) * (radius - 4) + radar_y = cy + (diff_z / 20.0) * (radius - 4) + + dist_from_center = math.hypot(radar_x - cx, radar_y - cy) + if dist_from_center > (radius - 3): + factor = (radius - 3) / dist_from_center + radar_x = cx + (radar_x - cx) * factor + radar_y = cy + (radar_y - cy) * factor + + glColor3f(1.0, 1.0, 1.0) + glBegin(GL_LINE_LOOP) + for i in range(8): + ang = 2.0 * math.pi * i / 8 + glVertex2f(radar_x + 3 * math.cos(ang), radar_y + 3 * math.sin(ang)) + glEnd() + + # ÍCONOS SUPERIORES + glColor3f(*COLOR_MOSQUITO) + glBegin(GL_LINES) + glVertex2f(25, HEIGHT - 30) + glVertex2f(12, HEIGHT - 30) + glVertex2f(25, HEIGHT - 30) + glVertex2f(38, HEIGHT - 30) + glEnd() + + glColor3f(*COLOR_FLY) + glBegin(GL_LINES) + glVertex2f(WIDTH - 85, HEIGHT - 30) + glVertex2f(WIDTH - 98, HEIGHT - 30) + glVertex2f(WIDTH - 85, HEIGHT - 30) + glVertex2f(WIDTH - 72, HEIGHT - 30) + glEnd() + + # TEXTOS + txt_mosq = font.render(f"x {eaten_mosquitos}", True, (255, 230, 0)) + txt_flies = font.render(f"x {eaten_flies}", True, (200, 65, 40)) + + draw_text_2d(txt_mosq, 45, HEIGHT - 40) + draw_text_2d(txt_flies, WIDTH - 65, HEIGHT - 40) + + glMatrixMode(GL_PROJECTION) + glPopMatrix() + glMatrixMode(GL_MODELVIEW) + glPopMatrix() + glEnable(GL_DEPTH_TEST) + +# ========================================== +# BUCLE PRINCIPAL +# ========================================== +def main(): + pygame.init() + pygame.font.init() + font = pygame.font.SysFont("Arial", 22, bold=True) + + pygame.display.set_mode((WIDTH, HEIGHT), DOUBLEBUF | OPENGL) + pygame.display.set_caption("Simulador de Vuelo de Libélula 3D") + + glEnable(GL_DEPTH_TEST) + glClearColor(*COLOR_BG, 1.0) + + glMatrixMode(GL_PROJECTION) + gluPerspective(50, (WIDTH / HEIGHT), 0.1, 5000.0) + glMatrixMode(GL_MODELVIEW) + + # Estado de la Libélula + px, py, pz = 0.0, 0.0, 15.0 + yaw = 0.0 + roll = 0.0 + pitch = 0.0 + max_roll = 30.0 + max_pitch = 20.0 + + speed = 0.0 + max_speed = 3.0 + acceleration = 0.06 + deceleration = 0.03 + + min_altitude = 1.0 + max_altitude = 120.0 + + wing_timer = 0.0 + water_timer = 0.0 + is_perched = False + + dive_phase = "none" + pre_dive_alt = 15.0 + pre_dive_speed = 0.0 + + auto_landing = False + target_perch = None + + caught_insect = None + eaten_mosquitos = 0 + eaten_flies = 0 + + # Instanciar elementos y animales + frog = Frog() + + insects = [] + num_mosquitos = random.randint(MOSQUITO_MIN, MOSQUITO_MAX) + num_flies = random.randint(FLY_MIN, FLY_MAX) + + for _ in range(num_mosquitos): + insects.append(Insect("mosquito")) + for _ in range(num_flies): + insects.append(Insect("fly")) + + clock = pygame.time.Clock() + + running = True + while running: + dt = clock.tick(60) / 1000.0 + water_timer += dt + + for event in pygame.event.get(): + if event.type == QUIT or (event.type == KEYDOWN and event.key == K_ESCAPE): + running = False + elif event.type == KEYDOWN and event.key == K_SPACE: + if not is_perched and dive_phase == "none": + dive_phase = "diving" + pre_dive_alt = pz + pre_dive_speed = speed + auto_landing = False + elif event.type == KEYDOWN and event.key == K_p: + if not is_perched and dive_phase == "none": + best_reed = None + min_d = 999.0 + for rx, ry, rz, rh in REEDS: + dist = math.hypot(px - rx, py - ry) + if dist < 35.0: + angle_to_reed = math.degrees(math.atan2(ry - py, rx - px)) + heading = (angle_to_reed - (90 - yaw)) % 360 + if heading > 180: + heading -= 360 + if abs(heading) < 45 and dist < min_d: + min_d = dist + best_reed = (rx, ry, rz + rh) + + if best_reed: + target_perch = best_reed + auto_landing = True + + keys = pygame.key.get_pressed() + + if is_perched: + if keys[K_LEFT]: + yaw += 3.0 + if keys[K_RIGHT]: + yaw -= 3.0 + + if keys[K_w]: + is_perched = False + speed = 0.5 + + wing_angle = 0.0 + pitch = 0.0 + roll = 0.0 + dive_phase = "none" + auto_landing = False + else: + if auto_landing and target_perch: + tx, ty, tz = target_perch + dx = tx - px + dy = ty - py + dist_to_target = math.hypot(dx, dy) + + speed = 1.2 + px += (dx / dist_to_target) * speed if dist_to_target > 0.1 else 0 + py += (dy / dist_to_target) * speed if dist_to_target > 0.1 else 0 + pz += (tz - pz) * 0.1 + + if dist_to_target < 2.5 and abs(pz - tz) < 2.5: + px, py, pz = target_perch + speed = 0.0 + roll = 0.0 + pitch = 0.0 + is_perched = True + auto_landing = False + target_perch = None + + if caught_insect: + if caught_insect.type == "mosquito": + eaten_mosquitos += 1 + else: + eaten_flies += 1 + caught_insect = None + + elif dive_phase == "diving": + speed = max_speed * 1.5 + pz -= 1.2 + target_pitch = -45.0 + + if pz <= min_altitude: + pz = min_altitude + dive_phase = "recovering" + + elif dive_phase == "recovering": + target_pitch = 30.0 + pz += 0.8 + speed += (pre_dive_speed - speed) * 0.1 + + if pz >= pre_dive_alt: + pz = pre_dive_alt + speed = pre_dive_speed + dive_phase = "none" + else: + if keys[K_w]: + speed = min(speed + acceleration, max_speed) + elif keys[K_s]: + speed = max(speed - deceleration, 0.0) + + if speed < 0.3: + for rx, ry, rz, rh in REEDS: + dist = math.hypot(px - rx, py - ry) + if dist < 10.0 and abs(pz - (rz + rh)) < 10.0: + px, py, pz = rx, ry, rz + rh + speed = 0.0 + roll = 0.0 + pitch = 0.0 + is_perched = True + + if caught_insect: + if caught_insect.type == "mosquito": + eaten_mosquitos += 1 + else: + eaten_flies += 1 + caught_insect = None + break + + target_pitch = 0.0 + if keys[K_UP]: + pz = min(pz + 0.8, max_altitude) + target_pitch = max_pitch + elif keys[K_DOWN]: + pz = max(pz - 0.8, min_altitude) + target_pitch = -max_pitch + + if not auto_landing: + pitch += (target_pitch - pitch) * 0.15 + + target_roll = 0.0 + if keys[K_LEFT]: + yaw += 2.8 + target_roll = -max_roll + elif keys[K_RIGHT]: + yaw -= 2.8 + target_roll = max_roll + + roll += (target_roll - roll) * 0.15 + + rad_yaw = math.radians(yaw) + px += -math.sin(rad_yaw) * speed + py += math.cos(rad_yaw) * speed + + if abs(py) < POND_RADIUS: + max_limit_x = POND_RADIUS + 200.0 + else: + cx = get_river_center(py) + max_limit_x = abs(cx) + 200.0 + + px = max(-max_limit_x, min(max_limit_x, px)) + + if py > STREAM_MAX_Y: + py -= WORLD_LEN + elif py < STREAM_MIN_Y: + py += WORLD_LEN + + wing_timer += 80.0 if (dive_phase != "none" or auto_landing) else (60.0 if speed > 0.1 else 30.0) + wing_angle = math.sin(wing_timer) * 35.0 + + # Actualizar rana y ambiente + frog.update() + + current_mosquitos = 0 + current_flies = 0 + + for insect in insects[:]: + insect.update((px, py, pz)) + + if insect.type == "mosquito": + current_mosquitos += 1 + else: + current_flies += 1 + + if not caught_insect: + dist_to_dragonfly = math.sqrt((px - insect.x)**2 + (py - insect.y)**2 + (pz - insect.z)**2) + if dist_to_dragonfly < 6.0: + caught_insect = insect + insects.remove(insect) + + if dive_phase == "diving": + dive_phase = "recovering" + + if current_mosquitos < MOSQUITO_MIN: + insects.append(Insect("mosquito")) + if current_flies < FLY_MIN: + insects.append(Insect("fly")) + + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) + glLoadIdentity() + + cam_dist = 18.0 + cam_height = 8.0 + rad_yaw = math.radians(yaw) + + cam_x = px + math.sin(rad_yaw) * cam_dist + cam_y = py - math.cos(rad_yaw) * cam_dist + cam_z = pz + cam_height + + gluLookAt(cam_x, cam_y, cam_z, + px, py, pz + 0.5, + 0, 0, 1) + + draw_map(water_timer) + + # Dibujar rana sobre el agua + frog.draw() + + for insect in insects: + insect.draw() + + glPushMatrix() + glTranslatef(px, py, pz) + glRotatef(yaw, 0, 0, 1) + glRotatef(pitch, 1, 0, 0) + glRotatef(roll, 0, 1, 0) + draw_dragonfly(wing_angle, caught_insect) + glPopMatrix() + + draw_hud((px, py, pz), yaw, insects, eaten_mosquitos, eaten_flies, font) + + pygame.display.flip() + + pygame.quit() + sys.exit() + +if __name__ == "__main__": + main() diff --git a/previos/dragonfly_game_v6.4.py b/previos/dragonfly_game_v6.4.py new file mode 100644 index 0000000..1489d4c --- /dev/null +++ b/previos/dragonfly_game_v6.4.py @@ -0,0 +1,828 @@ +import sys +import math +import random +import pygame +from pygame.locals import * +from OpenGL.GL import * +from OpenGL.GLU import * + +# ========================================== +# CONFIGURACIÓN Y CONSTANTES +# ========================================== +WIDTH, HEIGHT = 800, 600 + +# Colores (R, G, B) +COLOR_BG = (0.12, 0.15, 0.12) # Fondo ambiente +COLOR_WATER = (0.0, 0.5, 0.85) # Agua animada (Cian/Azul) +COLOR_LAND = (0.45, 0.32, 0.15) # Tierra / Relieve +COLOR_LAND_RIVER = (0.40, 0.28, 0.12) # Tierra en riachuelos +COLOR_ISLAND = (0.5, 0.38, 0.18) # Islas +COLOR_REED = (0.15, 0.85, 0.25) # Juncos verdes +COLOR_CATTAIL = (0.35, 0.18, 0.08) # Espigas marrones densas +COLOR_DRAGONFLY = (0.0, 1.0, 1.0) # Libélula alámbrica (Cian) +COLOR_MOSQUITO = (1.0, 0.9, 0.0) # Mosquitos (Amarillo) +COLOR_FLY = (0.8, 0.25, 0.15) # Moscas (Rojo-Marrón) +COLOR_FROG = (0.15, 0.85, 0.35) # Rana verde detallada +COLOR_HUD = (1.0, 1.0, 1.0) # Retícula HUD (Blanco) + +# Dimensiones del Mundo +POND_RADIUS = 1800.0 +STREAM_LEN = 900.0 +STREAM_MIN_Y = -(POND_RADIUS + STREAM_LEN) +STREAM_MAX_Y = (POND_RADIUS + STREAM_LEN) +WORLD_LEN = STREAM_MAX_Y - STREAM_MIN_Y + +# Cantidades de insectos +MOSQUITO_MIN, MOSQUITO_MAX = 10, 20 +FLY_MIN, FLY_MAX = 2, 7 + +# ========================================== +# ELEMENTOS DEL MAPA +# ========================================== +REEDS = [ + (-250, 350, 0, 20), (-160, -500, 0, 22), (500, 200, 0, 24), (-700, 200, 0, 21), (200, -800, 0, 23), + (-900, -300, 0, 25), (760, -600, 0, 22), (900, 500, 0, 26), (-500, 900, 0, 23), (600, -1000, 0, 22), + (-1200, 400, 0, 24), (1300, -300, 0, 20), (-1400, -700, 0, 25), (1100, 1200, 0, 23), (-360, -1300, 0, 22), + (-90, -1950, 0, 20), (-110, -2150, 0, 23), (-80, -2350, 0, 19), (-100, -2550, 0, 22), + (90, 1950, 0, 22), (110, 2150, 0, 20), (85, 2350, 0, 24), (105, 2550, 0, 21) +] + +ISLANDS = [ + (-250, 350, 90), (-160, -500, 85), (500, 200, 100), (-700, 200, 110), (200, -800, 95), + (-900, -300, 120), (760, -600, 90), (900, 500, 105), (-500, 900, 115), (600, -1000, 100) +] + +def get_river_center(y): + if y < -POND_RADIUS: + dy = y + POND_RADIUS + return -50 + 100 * math.sin(dy * 0.008) + elif y > POND_RADIUS: + dy = y - POND_RADIUS + return 50 + 100 * math.sin(dy * 0.008) + else: + return (y / POND_RADIUS) * 50.0 + +def is_over_water(x, y): + dist_center = math.hypot(x, y) + if dist_center < POND_RADIUS - 50: + return True + if abs(y) >= POND_RADIUS: + cx = get_river_center(y) + if abs(x - cx) < 80: + return True + return False + +def get_random_water_position(): + while True: + if random.random() < 0.7: + rad = random.uniform(0, math.pi * 2) + r = random.uniform(100, POND_RADIUS - 150) + x = r * math.cos(rad) + y = r * math.sin(rad) + else: + y = random.choice([random.uniform(POND_RADIUS, STREAM_MAX_Y), random.uniform(STREAM_MIN_Y, -POND_RADIUS)]) + cx = get_river_center(y) + x = cx + random.uniform(-60, 60) + + if is_over_water(x, y): + z = random.uniform(5.0, 35.0) + return x, y, z + +# ========================================== +# CLASES DE INSECTOS Y RANA DECORATIVA +# ========================================== +class Insect: + def __init__(self, insect_type): + self.type = insect_type + self.x, self.y, self.z = get_random_water_position() + self.yaw = random.uniform(0, 360) + self.timer = random.uniform(0, 100) + + if self.type == "mosquito": + self.speed = 1.8 + self.size = 1.2 + else: + self.speed = 0.9 + self.size = 1.6 + + def update(self, player_pos): + self.timer += 0.05 + px, py, pz = player_pos + dist_to_player = math.hypot(self.x - px, self.y - py) + + if self.type == "mosquito": + can_detect = (pz <= self.z) + if can_detect and dist_to_player < 100: + escape_angle = math.degrees(math.atan2(self.y - py, self.x - px)) + self.yaw += (escape_angle - self.yaw) * 0.05 + + self.yaw += math.sin(self.timer * 3) * 1.5 + self.z += math.sin(self.timer * 1.5) * 0.2 + + elif self.type == "fly": + if dist_to_player < 120: + escape_angle = math.degrees(math.atan2(self.y - py, self.x - px)) + self.yaw = escape_angle + random.uniform(-45, 45) + self.z += random.choice([-1.5, 1.5]) + else: + if random.random() < 0.08: + self.yaw += random.uniform(-60, 60) + + self.yaw += math.sin(self.timer * 6) * 4.0 + self.z += math.cos(self.timer * 4) * 0.6 + + self.z = max(3.0, min(50.0, self.z)) + + rad = math.radians(self.yaw) + next_x = self.x - math.sin(rad) * self.speed + next_y = self.y + math.cos(rad) * self.speed + + if is_over_water(next_x, next_y): + self.x, self.y = next_x, next_y + else: + self.yaw += 140.0 + random.uniform(-20, 20) + + if self.y > STREAM_MAX_Y: + self.y -= WORLD_LEN + elif self.y < STREAM_MIN_Y: + self.y += WORLD_LEN + + def draw(self, attached=False): + if self.type == "mosquito": + glColor3f(*COLOR_MOSQUITO) + else: + glColor3f(*COLOR_FLY) + + glPushMatrix() + if not attached: + glTranslatef(self.x, self.y, self.z) + glRotatef(self.yaw, 0, 0, 1) + + s = self.size + glBegin(GL_LINES) + glVertex3f(0, s, 0) + glVertex3f(0, -s, 0) + w_offset = 0.0 if attached else math.sin(self.timer * 30) * (s * 0.8) + glVertex3f(0, 0, 0) + glVertex3f(-s * 1.5, 0, w_offset) + glVertex3f(0, 0, 0) + glVertex3f(s * 1.5, 0, w_offset) + glEnd() + + glPopMatrix() + +class Frog: + def __init__(self): + self.pick_new_destination() + self.x, self.y = self.target_x, self.target_y + self.z = 0.6 + self.speed = 0.35 + self.yaw = 0.0 + + def pick_new_destination(self): + destinations = [(rx, ry) for rx, ry, _, _ in REEDS] + [(ix, iy) for ix, iy, _ in ISLANDS] + dest = random.choice(destinations) + self.target_x, self.target_y = dest[0] + random.uniform(-30, 30), dest[1] + random.uniform(-30, 30) + + def update(self): + dx = self.target_x - self.x + dy = self.target_y - self.y + dist = math.hypot(dx, dy) + + if dist < 6.0: + self.pick_new_destination() + else: + self.yaw = math.degrees(math.atan2(dy, dx)) + self.x += (dx / dist) * self.speed + self.y += (dy / dist) * self.speed + + def draw(self): + glColor3f(*COLOR_FROG) + glPushMatrix() + glTranslatef(self.x, self.y, self.z) + glRotatef(self.yaw, 0, 0, 1) + + # Modelo 3D de rana ampliado y detallado (Anatomía reconocible) + glBegin(GL_LINES) + # Tronco / Cuerpo robusto y ancho + glVertex3f(0.0, 5.0, 0.5) + glVertex3f(0.0, -4.0, 0.5) + glVertex3f(-3.0, 2.0, 0.5) + glVertex3f(3.0, 2.0, 0.5) + glVertex3f(-3.5, -2.0, 0.5) + glVertex3f(3.5, -2.0, 0.5) + + # Conexión lomo + glVertex3f(-3.0, 2.0, 0.5) + glVertex3f(0.0, 5.0, 0.5) + glVertex3f(3.0, 2.0, 0.5) + glVertex3f(0.0, 5.0, 0.5) + + # Cabeza ancha + glVertex3f(-2.5, 5.0, 0.6) + glVertex3f(2.5, 5.0, 0.6) + glVertex3f(-2.5, 5.0, 0.6) + glVertex3f(0.0, 7.5, 0.6) + glVertex3f(2.5, 5.0, 0.6) + glVertex3f(0.0, 7.5, 0.6) + + # Ojos saltones prominentes + glVertex3f(-2.0, 6.5, 1.8) + glVertex3f(-2.0, 6.5, 0.8) + glVertex3f(2.0, 6.5, 1.8) + glVertex3f(2.0, 6.5, 0.8) + + # Patas delanteras flexionadas + glVertex3f(-3.0, 2.0, 0.5) + glVertex3f(-5.0, 3.5, 0.0) + glVertex3f(-5.0, 3.5, 0.0) + glVertex3f(-4.5, 4.5, 0.0) + + glVertex3f(3.0, 2.0, 0.5) + glVertex3f(5.0, 3.5, 0.0) + glVertex3f(5.0, 3.5, 0.0) + glVertex3f(4.5, 4.5, 0.0) + + # Patas traseras musculosas y plegadas + glVertex3f(-3.5, -2.0, 0.5) + glVertex3f(-6.5, -4.0, 0.0) + glVertex3f(-6.5, -4.0, 0.0) + glVertex3f(-4.0, -6.0, 0.0) + + glVertex3f(3.5, -2.0, 0.5) + glVertex3f(6.5, -4.0, 0.0) + glVertex3f(6.5, -4.0, 0.0) + glVertex3f(4.0, -6.0, 0.0) + glEnd() + + glPopMatrix() + +# ========================================== +# RENDERIZADO DE ESCENARIO Y MODELOS +# ========================================== +def draw_water_surface(time_counter): + glColor3f(*COLOR_WATER) + + grid_size = 15 + for x in range(int(-POND_RADIUS), int(POND_RADIUS), grid_size * 2): + glBegin(GL_LINE_STRIP) + for y in range(int(-POND_RADIUS), int(POND_RADIUS), grid_size): + dist = math.hypot(x, y) + if dist < POND_RADIUS - 30: + z = 1.0 * math.sin(dist * 0.015 - time_counter * 3.0) + 0.5 * math.cos(x * 0.01 + time_counter) + glVertex3f(x, y, z) + glEnd() + + for y in range(int(STREAM_MIN_Y), int(STREAM_MAX_Y), 15): + if abs(y) < POND_RADIUS: + continue + cx = get_river_center(y) + glBegin(GL_LINE_STRIP) + for dx in range(-100, 110, 10): + x = cx + dx + z = 0.5 * math.sin(y * 0.03 + dx * 0.08 + time_counter * 4.0) + glVertex3f(x, y, z) + glEnd() + +def draw_terrain(): + glColor3f(*COLOR_LAND) + + grid_step = 120 + for y in range(int(STREAM_MIN_Y - 300), int(STREAM_MAX_Y + 300), grid_step): + glBegin(GL_LINE_STRIP) + for x in range(-2500, 2500, grid_step): + z = 1.5 * math.sin(x * 0.005) + 1.5 * math.cos(y * 0.005) + glVertex3f(x, y, z) + glEnd() + + width_river = 100.0 + margin_land = 70.0 + glColor3f(*COLOR_LAND_RIVER) + + for y in range(int(STREAM_MIN_Y), int(STREAM_MAX_Y), 30): + if abs(y) < POND_RADIUS: + continue + cx = get_river_center(y) + + glBegin(GL_LINE_STRIP) + for dx in range(int(-width_river - margin_land), int(-width_river), 15): + x = cx + dx + z = 0.6 * math.sin(y * 0.02 + x * 0.02) + 0.3 + glVertex3f(x, y, z) + glEnd() + + glBegin(GL_LINE_STRIP) + for dx in range(int(width_river), int(width_river + margin_land), 15): + x = cx + dx + z = 0.6 * math.sin(y * 0.02 + x * 0.02) + 0.3 + glVertex3f(x, y, z) + glEnd() + + for ix, iy, ir in ISLANDS: + glColor3f(*COLOR_ISLAND) + for layer in range(1, 4): + rad_layer = ir * (1 - layer * 0.25) + height = layer * 1.2 + glBegin(GL_LINE_LOOP) + for i in range(16): + angle = 2.0 * math.pi * i / 16 + dx = rad_layer * math.cos(angle) + dy = rad_layer * math.sin(angle) + glVertex3f(ix + dx, iy + dy, height) + glEnd() + +def draw_map(time_counter): + draw_terrain() + draw_water_surface(time_counter) + + offsets = [(-1.5, 0.6), (1.3, -0.6), (-0.7, -1.4), (1.1, 1.2), (0.0, 1.6), (-1.2, 0.0)] + + for rx, ry, rz, rh in REEDS: + glColor3f(*COLOR_REED) + glBegin(GL_LINES) + glVertex3f(rx, ry, rz) + glVertex3f(rx, ry, rz + rh) + for ox, oy in offsets: + glVertex3f(rx, ry, rz) + glVertex3f(rx + ox, ry + oy, rz + rh * 0.85) + glEnd() + + glColor3f(*COLOR_CATTAIL) + glBegin(GL_LINES) + for d in [-0.2, 0.0, 0.2]: + glVertex3f(rx + d, ry, rz + rh - 4.5) + glVertex3f(rx + d, ry, rz + rh) + for ox, oy in offsets: + for d in [-0.15, 0.15]: + glVertex3f(rx + ox + d, ry + oy, (rz + rh * 0.85) - 3.8) + glVertex3f(rx + ox + d, ry + oy, rz + rh * 0.85) + glEnd() + +def draw_dragonfly(wing_angle, caught_insect=None): + glColor3f(*COLOR_DRAGONFLY) + + glBegin(GL_LINES) + glVertex3f(0, 1.5, 0) + glVertex3f(0, -3.0, 0) + glVertex3f(-0.4, 0.6, 0) + glVertex3f(0.4, 0.6, 0) + glVertex3f(-0.3, -0.6, 0) + glVertex3f(0.3, -0.6, 0) + + glVertex3f(-0.3, 0.5, 0) + glVertex3f(-0.2, 0.2, -0.6) + glVertex3f(0.3, 0.5, 0) + glVertex3f(0.2, 0.2, -0.6) + glVertex3f(-0.2, -0.2, 0) + glVertex3f(-0.1, -0.3, -0.6) + glVertex3f(0.2, -0.2, 0) + glVertex3f(0.1, -0.3, -0.6) + glEnd() + + glBegin(GL_LINE_LOOP) + for i in range(8): + angle = 2.0 * math.pi * i / 8 + glVertex3f(0.3 * math.cos(angle), 1.4 + 0.3 * math.sin(angle), 0) + glEnd() + + rad_wing = math.radians(wing_angle) + w_z = math.sin(rad_wing) * 1.2 if wing_angle != 0 else 0.0 + + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(-4.5, 1.0, w_z) + glVertex3f(-3.8, 0.2, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(4.5, 1.0, w_z) + glVertex3f(3.8, 0.2, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(-3.8, -0.2, w_z) + glVertex3f(-3.0, -0.8, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(3.8, -0.2, w_z) + glVertex3f(3.0, -0.8, w_z) + glEnd() + + if caught_insect: + glPushMatrix() + glTranslatef(0.0, 0.2, -0.6) + caught_insect.draw(attached=True) + glPopMatrix() + +# ========================================== +# INTERFAZ 2D / HUD +# ========================================== +def draw_text_2d(text_surface, x, y): + text_data = pygame.image.tostring(text_surface, "RGBA", True) + w, h = text_surface.get_size() + + glEnable(GL_BLEND) + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) + + glRasterPos2i(x, y) + glDrawPixels(w, h, GL_RGBA, GL_UNSIGNED_BYTE, text_data) + glDisable(GL_BLEND) + +def draw_hud(player_pos, yaw, insects, eaten_mosquitos, eaten_flies, font): + glDisable(GL_DEPTH_TEST) + + glMatrixMode(GL_PROJECTION) + glPushMatrix() + glLoadIdentity() + gluOrtho2D(0, WIDTH, 0, HEIGHT) + + glMatrixMode(GL_MODELVIEW) + glPushMatrix() + glLoadIdentity() + + px, py, pz = player_pos + + # RADAR REDUCIDO + cx, cy = WIDTH // 2, HEIGHT - 35 + radius = 22 + + glColor3f(*COLOR_HUD) + glBegin(GL_LINE_LOOP) + for i in range(24): + angle = 2.0 * math.pi * i / 24 + glVertex2f(cx + radius * math.cos(angle), cy + radius * math.sin(angle)) + glEnd() + + glBegin(GL_LINES) + glVertex2f(cx, cy + radius) + glVertex2f(cx, cy + radius - 5) + glVertex2f(cx, cy - radius) + glVertex2f(cx, cy - radius + 5) + glVertex2f(cx - radius, cy) + glVertex2f(cx - radius + 5, cy) + glVertex2f(cx + radius, cy) + glVertex2f(cx + radius - 5, cy) + glEnd() + + closest_insect = None + min_dist = 999999.0 + relative_angle = 0.0 + diff_z = 0.0 + + for insect in insects: + dx = insect.x - px + dy = insect.y - py + dist = math.hypot(dx, dy) + + if dist < min_dist and dist > 0.1: + angle_to_insect = math.degrees(math.atan2(dy, dx)) + heading = (angle_to_insect - (90 - yaw)) % 360 + if heading > 180: + heading -= 360 + + if abs(heading) < 60: + min_dist = dist + closest_insect = insect + relative_angle = heading + diff_z = insect.z - pz + + if closest_insect: + radar_x = cx - (relative_angle / 60.0) * (radius - 4) + radar_y = cy + (diff_z / 20.0) * (radius - 4) + + dist_from_center = math.hypot(radar_x - cx, radar_y - cy) + if dist_from_center > (radius - 3): + factor = (radius - 3) / dist_from_center + radar_x = cx + (radar_x - cx) * factor + radar_y = cy + (radar_y - cy) * factor + + glColor3f(1.0, 1.0, 1.0) + glBegin(GL_LINE_LOOP) + for i in range(8): + ang = 2.0 * math.pi * i / 8 + glVertex2f(radar_x + 3 * math.cos(ang), radar_y + 3 * math.sin(ang)) + glEnd() + + # ÍCONOS SUPERIORES + glColor3f(*COLOR_MOSQUITO) + glBegin(GL_LINES) + glVertex2f(25, HEIGHT - 30) + glVertex2f(12, HEIGHT - 30) + glVertex2f(25, HEIGHT - 30) + glVertex2f(38, HEIGHT - 30) + glEnd() + + glColor3f(*COLOR_FLY) + glBegin(GL_LINES) + glVertex2f(WIDTH - 85, HEIGHT - 30) + glVertex2f(WIDTH - 98, HEIGHT - 30) + glVertex2f(WIDTH - 85, HEIGHT - 30) + glVertex2f(WIDTH - 72, HEIGHT - 30) + glEnd() + + # TEXTOS + txt_mosq = font.render(f"x {eaten_mosquitos}", True, (255, 230, 0)) + txt_flies = font.render(f"x {eaten_flies}", True, (200, 65, 40)) + + draw_text_2d(txt_mosq, 45, HEIGHT - 40) + draw_text_2d(txt_flies, WIDTH - 65, HEIGHT - 40) + + glMatrixMode(GL_PROJECTION) + glPopMatrix() + glMatrixMode(GL_MODELVIEW) + glPopMatrix() + glEnable(GL_DEPTH_TEST) + +# ========================================== +# BUCLE PRINCIPAL +# ========================================== +def main(): + pygame.init() + pygame.font.init() + font = pygame.font.SysFont("Arial", 22, bold=True) + + pygame.display.set_mode((WIDTH, HEIGHT), DOUBLEBUF | OPENGL) + pygame.display.set_caption("Simulador de Vuelo de Libélula 3D") + + glEnable(GL_DEPTH_TEST) + glClearColor(*COLOR_BG, 1.0) + + glMatrixMode(GL_PROJECTION) + gluPerspective(50, (WIDTH / HEIGHT), 0.1, 5000.0) + glMatrixMode(GL_MODELVIEW) + + # Estado de la Libélula + px, py, pz = 0.0, 0.0, 15.0 + yaw = 0.0 + roll = 0.0 + pitch = 0.0 + max_roll = 30.0 + max_pitch = 20.0 + + speed = 0.0 + max_speed = 3.0 + acceleration = 0.06 + deceleration = 0.03 + + min_altitude = 1.0 + max_altitude = 120.0 + + wing_timer = 0.0 + water_timer = 0.0 + is_perched = False + + dive_phase = "none" + pre_dive_alt = 15.0 + pre_dive_speed = 0.0 + + auto_landing = False + target_perch = None + + caught_insect = None + eaten_mosquitos = 0 + eaten_flies = 0 + + frog = Frog() + + insects = [] + num_mosquitos = random.randint(MOSQUITO_MIN, MOSQUITO_MAX) + num_flies = random.randint(FLY_MIN, FLY_MAX) + + for _ in range(num_mosquitos): + insects.append(Insect("mosquito")) + for _ in range(num_flies): + insects.append(Insect("fly")) + + clock = pygame.time.Clock() + + running = True + while running: + dt = clock.tick(60) / 1000.0 + water_timer += dt + + for event in pygame.event.get(): + if event.type == QUIT or (event.type == KEYDOWN and event.key == K_ESCAPE): + running = False + elif event.type == KEYDOWN and event.key == K_SPACE: + if not is_perched and dive_phase == "none": + dive_phase = "diving" + pre_dive_alt = pz + pre_dive_speed = speed + auto_landing = False + elif event.type == KEYDOWN and event.key == K_p: + if not is_perched and dive_phase == "none": + best_reed = None + min_d = 999.0 + for rx, ry, rz, rh in REEDS: + dist = math.hypot(px - rx, py - ry) + if dist < 35.0: + angle_to_reed = math.degrees(math.atan2(ry - py, rx - px)) + heading = (angle_to_reed - (90 - yaw)) % 360 + if heading > 180: + heading -= 360 + if abs(heading) < 45 and dist < min_d: + min_d = dist + best_reed = (rx, ry, rz + rh) + + if best_reed: + target_perch = best_reed + auto_landing = True + + keys = pygame.key.get_pressed() + + if is_perched: + if keys[K_LEFT]: + yaw += 3.0 + if keys[K_RIGHT]: + yaw -= 3.0 + + if keys[K_w]: + is_perched = False + speed = 0.5 + + wing_angle = 0.0 + pitch = 0.0 + roll = 0.0 + dive_phase = "none" + auto_landing = False + else: + if auto_landing and target_perch: + tx, ty, tz = target_perch + dx = tx - px + dy = ty - py + dist_to_target = math.hypot(dx, dy) + + speed = 1.2 + px += (dx / dist_to_target) * speed if dist_to_target > 0.1 else 0 + py += (dy / dist_to_target) * speed if dist_to_target > 0.1 else 0 + pz += (tz - pz) * 0.1 + + if dist_to_target < 2.5 and abs(pz - tz) < 2.5: + px, py, pz = target_perch + speed = 0.0 + roll = 0.0 + pitch = 0.0 + is_perched = True + auto_landing = False + target_perch = None + + if caught_insect: + if caught_insect.type == "mosquito": + eaten_mosquitos += 1 + else: + eaten_flies += 1 + caught_insect = None + + elif dive_phase == "diving": + speed = max_speed * 1.5 + pz -= 1.2 + target_pitch = -45.0 + + if pz <= min_altitude: + pz = min_altitude + dive_phase = "recovering" + + elif dive_phase == "recovering": + target_pitch = 30.0 + pz += 0.8 + speed += (pre_dive_speed - speed) * 0.1 + + if pz >= pre_dive_alt: + pz = pre_dive_alt + speed = pre_dive_speed + dive_phase = "none" + else: + if keys[K_w]: + speed = min(speed + acceleration, max_speed) + elif keys[K_s]: + speed = max(speed - deceleration, 0.0) + + if speed < 0.3: + for rx, ry, rz, rh in REEDS: + dist = math.hypot(px - rx, py - ry) + if dist < 10.0 and abs(pz - (rz + rh)) < 10.0: + px, py, pz = rx, ry, rz + rh + speed = 0.0 + roll = 0.0 + pitch = 0.0 + is_perched = True + + if caught_insect: + if caught_insect.type == "mosquito": + eaten_mosquitos += 1 + else: + eaten_flies += 1 + caught_insect = None + break + + target_pitch = 0.0 + if keys[K_UP]: + pz = min(pz + 0.8, max_altitude) + target_pitch = max_pitch + elif keys[K_DOWN]: + pz = max(pz - 0.8, min_altitude) + target_pitch = -max_pitch + + if not auto_landing: + pitch += (target_pitch - pitch) * 0.15 + + target_roll = 0.0 + if keys[K_LEFT]: + yaw += 2.8 + target_roll = -max_roll + elif keys[K_RIGHT]: + yaw -= 2.8 + target_roll = max_roll + + roll += (target_roll - roll) * 0.15 + + rad_yaw = math.radians(yaw) + px += -math.sin(rad_yaw) * speed + py += math.cos(rad_yaw) * speed + + if abs(py) < POND_RADIUS: + max_limit_x = POND_RADIUS + 200.0 + else: + cx = get_river_center(py) + max_limit_x = abs(cx) + 200.0 + + px = max(-max_limit_x, min(max_limit_x, px)) + + if py > STREAM_MAX_Y: + py -= WORLD_LEN + elif py < STREAM_MIN_Y: + py += WORLD_LEN + + wing_timer += 80.0 if (dive_phase != "none" or auto_landing) else (60.0 if speed > 0.1 else 30.0) + wing_angle = math.sin(wing_timer) * 35.0 + + frog.update() + + current_mosquitos = 0 + current_flies = 0 + + for insect in insects[:]: + insect.update((px, py, pz)) + + if insect.type == "mosquito": + current_mosquitos += 1 + else: + current_flies += 1 + + if not caught_insect: + dist_to_dragonfly = math.sqrt((px - insect.x)**2 + (py - insect.y)**2 + (pz - insect.z)**2) + if dist_to_dragonfly < 6.0: + caught_insect = insect + insects.remove(insect) + + if dive_phase == "diving": + dive_phase = "recovering" + + if current_mosquitos < MOSQUITO_MIN: + insects.append(Insect("mosquito")) + if current_flies < FLY_MIN: + insects.append(Insect("fly")) + + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) + glLoadIdentity() + + cam_dist = 18.0 + cam_height = 8.0 + rad_yaw = math.radians(yaw) + + cam_x = px + math.sin(rad_yaw) * cam_dist + cam_y = py - math.cos(rad_yaw) * cam_dist + cam_z = pz + cam_height + + gluLookAt(cam_x, cam_y, cam_z, + px, py, pz + 0.5, + 0, 0, 1) + + draw_map(water_timer) + + frog.draw() + + for insect in insects: + insect.draw() + + glPushMatrix() + glTranslatef(px, py, pz) + glRotatef(yaw, 0, 0, 1) + glRotatef(pitch, 1, 0, 0) + glRotatef(roll, 0, 1, 0) + draw_dragonfly(wing_angle, caught_insect) + glPopMatrix() + + draw_hud((px, py, pz), yaw, insects, eaten_mosquitos, eaten_flies, font) + + pygame.display.flip() + + pygame.quit() + sys.exit() + +if __name__ == "__main__": + main() diff --git a/previos/dragonfly_game_v6.5.py b/previos/dragonfly_game_v6.5.py new file mode 100644 index 0000000..96fe80b --- /dev/null +++ b/previos/dragonfly_game_v6.5.py @@ -0,0 +1,834 @@ +import sys +import math +import random +import pygame +from pygame.locals import * +from OpenGL.GL import * +from OpenGL.GLU import * + +# ========================================== +# CONFIGURACIÓN Y CONSTANTES +# ========================================== +WIDTH, HEIGHT = 800, 600 + +# Colores (R, G, B) +COLOR_BG = (0.12, 0.15, 0.12) # Fondo ambiente +COLOR_WATER = (0.0, 0.5, 0.85) # Agua animada (Cian/Azul) +COLOR_LAND = (0.45, 0.32, 0.15) # Tierra / Relieve +COLOR_LAND_RIVER = (0.40, 0.28, 0.12) # Tierra en riachuelos +COLOR_ISLAND = (0.5, 0.38, 0.18) # Islas +COLOR_REED = (0.15, 0.85, 0.25) # Juncos verdes +COLOR_CATTAIL = (0.35, 0.18, 0.08) # Espigas marrones densas +COLOR_DRAGONFLY = (0.0, 1.0, 1.0) # Libélula alámbrica (Cian) +COLOR_MOSQUITO = (1.0, 0.9, 0.0) # Mosquitos (Amarillo) +COLOR_FLY = (0.8, 0.25, 0.15) # Moscas (Rojo-Marrón) +COLOR_FROG = (0.15, 0.85, 0.35) # Rana verde detallada +COLOR_HUD = (1.0, 1.0, 1.0) # Retícula HUD (Blanco) + +# Dimensiones del Mundo +POND_RADIUS = 1800.0 +STREAM_LEN = 900.0 +STREAM_MIN_Y = -(POND_RADIUS + STREAM_LEN) +STREAM_MAX_Y = (POND_RADIUS + STREAM_LEN) +WORLD_LEN = STREAM_MAX_Y - STREAM_MIN_Y + +# Cantidades de insectos +MOSQUITO_MIN, MOSQUITO_MAX = 10, 20 +FLY_MIN, FLY_MAX = 2, 7 + +# ========================================== +# ELEMENTOS DEL MAPA +# ========================================== +REEDS = [ + (-250, 350, 0, 20), (-160, -500, 0, 22), (500, 200, 0, 24), (-700, 200, 0, 21), (200, -800, 0, 23), + (-900, -300, 0, 25), (760, -600, 0, 22), (900, 500, 0, 26), (-500, 900, 0, 23), (600, -1000, 0, 22), + (-1200, 400, 0, 24), (1300, -300, 0, 20), (-1400, -700, 0, 25), (1100, 1200, 0, 23), (-360, -1300, 0, 22), + (-90, -1950, 0, 20), (-110, -2150, 0, 23), (-80, -2350, 0, 19), (-100, -2550, 0, 22), + (90, 1950, 0, 22), (110, 2150, 0, 20), (85, 2350, 0, 24), (105, 2550, 0, 21) +] + +ISLANDS = [ + (-250, 350, 90), (-160, -500, 85), (500, 200, 100), (-700, 200, 110), (200, -800, 95), + (-900, -300, 120), (760, -600, 90), (900, 500, 105), (-500, 900, 115), (600, -1000, 100) +] + +def get_river_center(y): + if y < -POND_RADIUS: + dy = y + POND_RADIUS + return -50 + 100 * math.sin(dy * 0.008) + elif y > POND_RADIUS: + dy = y - POND_RADIUS + return 50 + 100 * math.sin(dy * 0.008) + else: + return (y / POND_RADIUS) * 50.0 + +def is_over_water(x, y): + dist_center = math.hypot(x, y) + if dist_center < POND_RADIUS - 50: + return True + if abs(y) >= POND_RADIUS: + cx = get_river_center(y) + if abs(x - cx) < 80: + return True + return False + +def get_random_water_position(): + while True: + if random.random() < 0.7: + rad = random.uniform(0, math.pi * 2) + r = random.uniform(100, POND_RADIUS - 150) + x = r * math.cos(rad) + y = r * math.sin(rad) + else: + y = random.choice([random.uniform(POND_RADIUS, STREAM_MAX_Y), random.uniform(STREAM_MIN_Y, -POND_RADIUS)]) + cx = get_river_center(y) + x = cx + random.uniform(-60, 60) + + if is_over_water(x, y): + z = random.uniform(5.0, 35.0) + return x, y, z + +# ========================================== +# CLASES DE INSECTOS Y RANA DECORATIVA +# ========================================== +class Insect: + def __init__(self, insect_type): + self.type = insect_type + self.x, self.y, self.z = get_random_water_position() + self.yaw = random.uniform(0, 360) + self.timer = random.uniform(0, 100) + + if self.type == "mosquito": + self.speed = 1.8 + self.size = 1.2 + else: + self.speed = 0.9 + self.size = 1.6 + + def update(self, player_pos): + self.timer += 0.05 + px, py, pz = player_pos + dist_to_player = math.hypot(self.x - px, self.y - py) + + if self.type == "mosquito": + can_detect = (pz <= self.z) + if can_detect and dist_to_player < 100: + escape_angle = math.degrees(math.atan2(self.y - py, self.x - px)) + self.yaw += (escape_angle - self.yaw) * 0.05 + + self.yaw += math.sin(self.timer * 3) * 1.5 + self.z += math.sin(self.timer * 1.5) * 0.2 + + elif self.type == "fly": + if dist_to_player < 120: + escape_angle = math.degrees(math.atan2(self.y - py, self.x - px)) + self.yaw = escape_angle + random.uniform(-45, 45) + self.z += random.choice([-1.5, 1.5]) + else: + if random.random() < 0.08: + self.yaw += random.uniform(-60, 60) + + self.yaw += math.sin(self.timer * 6) * 4.0 + self.z += math.cos(self.timer * 4) * 0.6 + + self.z = max(3.0, min(50.0, self.z)) + + rad = math.radians(self.yaw) + next_x = self.x - math.sin(rad) * self.speed + next_y = self.y + math.cos(rad) * self.speed + + if is_over_water(next_x, next_y): + self.x, self.y = next_x, next_y + else: + self.yaw += 140.0 + random.uniform(-20, 20) + + if self.y > STREAM_MAX_Y: + self.y -= WORLD_LEN + elif self.y < STREAM_MIN_Y: + self.y += WORLD_LEN + + def draw(self, attached=False): + if self.type == "mosquito": + glColor3f(*COLOR_MOSQUITO) + else: + glColor3f(*COLOR_FLY) + + glPushMatrix() + if not attached: + glTranslatef(self.x, self.y, self.z) + glRotatef(self.yaw, 0, 0, 1) + + s = self.size + glBegin(GL_LINES) + glVertex3f(0, s, 0) + glVertex3f(0, -s, 0) + w_offset = 0.0 if attached else math.sin(self.timer * 30) * (s * 0.8) + glVertex3f(0, 0, 0) + glVertex3f(-s * 1.5, 0, w_offset) + glVertex3f(0, 0, 0) + glVertex3f(s * 1.5, 0, w_offset) + glEnd() + + glPopMatrix() + +class Frog: + def __init__(self): + self.pick_new_destination() + self.x, self.y = self.target_x, self.target_y + self.z = 0.6 + self.speed = 0.35 + self.yaw = 0.0 + self.swim_timer = 0.0 + + def pick_new_destination(self): + destinations = [(rx, ry) for rx, ry, _, _ in REEDS] + [(ix, iy) for ix, iy, _ in ISLANDS] + dest = random.choice(destinations) + self.target_x, self.target_y = dest[0] + random.uniform(-30, 30), dest[1] + random.uniform(-30, 30) + + def update(self): + self.swim_timer += 0.15 + dx = self.target_x - self.x + dy = self.target_y - self.y + dist = math.hypot(dx, dy) + + if dist < 6.0: + self.pick_new_destination() + else: + # Apuntar la cabeza hacia la dirección del movimiento (ajustado 90 grados para orientar el frente del modelo) + self.yaw = math.degrees(math.atan2(dy, dx)) - 90.0 + self.x += (dx / dist) * self.speed + self.y += (dy / dist) * self.speed + + def draw(self): + glColor3f(*COLOR_FROG) + glPushMatrix() + glTranslatef(self.x, self.y, self.z) + glRotatef(self.yaw, 0, 0, 1) + + # Offset de animación de las patas traseras para simular el nado + leg_offset = math.sin(self.swim_timer) * 1.5 + + # Modelo 3D de rana orientado al frente y con patas traseras móviles + glBegin(GL_LINES) + # Tronco / Cuerpo robusto + glVertex3f(0.0, 0.0, 0.5) + glVertex3f(0.0, -5.0, 0.5) + glVertex3f(-2.0, 2.0, 0.5) + glVertex3f(2.0, 2.0, 0.5) + glVertex3f(-2.5, -2.0, 0.5) + glVertex3f(2.5, -2.0, 0.5) + + # Conexión lomo + glVertex3f(-2.0, 2.0, 0.5) + glVertex3f(0.0, 0.0, 0.5) + glVertex3f(2.0, 2.0, 0.5) + glVertex3f(0.0, 0.0, 0.5) + + # Cabeza (apuntando hacia adelante / eje Y positivo) + glVertex3f(-2.0, 2.0, 0.6) + glVertex3f(2.0, 2.0, 0.6) + glVertex3f(-2.0, 2.0, 0.6) + glVertex3f(0.0, 4.5, 0.6) + glVertex3f(2.0, 2.0, 0.6) + glVertex3f(0.0, 4.5, 0.6) + + # Ojos saltones prominentes + glVertex3f(-1.5, 3.5, 1.5) + glVertex3f(-1.5, 3.5, 0.6) + glVertex3f(1.5, 3.5, 1.5) + glVertex3f(1.5, 3.5, 0.6) + + # Patas delanteras estáticas + glVertex3f(-2.0, 2.0, 0.5) + glVertex3f(-4.0, 3.0, 0.0) + glVertex3f(-4.0, 3.0, 0.0) + glVertex3f(-3.5, 4.0, 0.0) + + glVertex3f(2.0, 2.0, 0.5) + glVertex3f(4.0, 3.0, 0.0) + glVertex3f(4.0, 3.0, 0.0) + glVertex3f(3.5, 4.0, 0.0) + + # Patas traseras móviles simulando brazada/patada de natación + glVertex3f(-2.5, -2.0, 0.5) + glVertex3f(-5.0 - leg_offset, -4.0, 0.0) + glVertex3f(-5.0 - leg_offset, -4.0, 0.0) + glVertex3f(-3.0 - leg_offset, -6.0, 0.0) + + glVertex3f(2.5, -2.0, 0.5) + glVertex3f(5.0 + leg_offset, -4.0, 0.0) + glVertex3f(5.0 + leg_offset, -4.0, 0.0) + glVertex3f(3.0 + leg_offset, -6.0, 0.0) + glEnd() + + glPopMatrix() + +# ========================================== +# RENDERIZADO DE ESCENARIO Y MODELOS +# ========================================== +def draw_water_surface(time_counter): + glColor3f(*COLOR_WATER) + + grid_size = 15 + for x in range(int(-POND_RADIUS), int(POND_RADIUS), grid_size * 2): + glBegin(GL_LINE_STRIP) + for y in range(int(-POND_RADIUS), int(POND_RADIUS), grid_size): + dist = math.hypot(x, y) + if dist < POND_RADIUS - 30: + z = 1.0 * math.sin(dist * 0.015 - time_counter * 3.0) + 0.5 * math.cos(x * 0.01 + time_counter) + glVertex3f(x, y, z) + glEnd() + + for y in range(int(STREAM_MIN_Y), int(STREAM_MAX_Y), 15): + if abs(y) < POND_RADIUS: + continue + cx = get_river_center(y) + glBegin(GL_LINE_STRIP) + for dx in range(-100, 110, 10): + x = cx + dx + z = 0.5 * math.sin(y * 0.03 + dx * 0.08 + time_counter * 4.0) + glVertex3f(x, y, z) + glEnd() + +def draw_terrain(): + glColor3f(*COLOR_LAND) + + grid_step = 120 + for y in range(int(STREAM_MIN_Y - 300), int(STREAM_MAX_Y + 300), grid_step): + glBegin(GL_LINE_STRIP) + for x in range(-2500, 2500, grid_step): + z = 1.5 * math.sin(x * 0.005) + 1.5 * math.cos(y * 0.005) + glVertex3f(x, y, z) + glEnd() + + width_river = 100.0 + margin_land = 70.0 + glColor3f(*COLOR_LAND_RIVER) + + for y in range(int(STREAM_MIN_Y), int(STREAM_MAX_Y), 30): + if abs(y) < POND_RADIUS: + continue + cx = get_river_center(y) + + glBegin(GL_LINE_STRIP) + for dx in range(int(-width_river - margin_land), int(-width_river), 15): + x = cx + dx + z = 0.6 * math.sin(y * 0.02 + x * 0.02) + 0.3 + glVertex3f(x, y, z) + glEnd() + + glBegin(GL_LINE_STRIP) + for dx in range(int(width_river), int(width_river + margin_land), 15): + x = cx + dx + z = 0.6 * math.sin(y * 0.02 + x * 0.02) + 0.3 + glVertex3f(x, y, z) + glEnd() + + for ix, iy, ir in ISLANDS: + glColor3f(*COLOR_ISLAND) + for layer in range(1, 4): + rad_layer = ir * (1 - layer * 0.25) + height = layer * 1.2 + glBegin(GL_LINE_LOOP) + for i in range(16): + angle = 2.0 * math.pi * i / 16 + dx = rad_layer * math.cos(angle) + dy = rad_layer * math.sin(angle) + glVertex3f(ix + dx, iy + dy, height) + glEnd() + +def draw_map(time_counter): + draw_terrain() + draw_water_surface(time_counter) + + offsets = [(-1.5, 0.6), (1.3, -0.6), (-0.7, -1.4), (1.1, 1.2), (0.0, 1.6), (-1.2, 0.0)] + + for rx, ry, rz, rh in REEDS: + glColor3f(*COLOR_REED) + glBegin(GL_LINES) + glVertex3f(rx, ry, rz) + glVertex3f(rx, ry, rz + rh) + for ox, oy in offsets: + glVertex3f(rx, ry, rz) + glVertex3f(rx + ox, ry + oy, rz + rh * 0.85) + glEnd() + + glColor3f(*COLOR_CATTAIL) + glBegin(GL_LINES) + for d in [-0.2, 0.0, 0.2]: + glVertex3f(rx + d, ry, rz + rh - 4.5) + glVertex3f(rx + d, ry, rz + rh) + for ox, oy in offsets: + for d in [-0.15, 0.15]: + glVertex3f(rx + ox + d, ry + oy, (rz + rh * 0.85) - 3.8) + glVertex3f(rx + ox + d, ry + oy, rz + rh * 0.85) + glEnd() + +def draw_dragonfly(wing_angle, caught_insect=None): + glColor3f(*COLOR_DRAGONFLY) + + glBegin(GL_LINES) + glVertex3f(0, 1.5, 0) + glVertex3f(0, -3.0, 0) + glVertex3f(-0.4, 0.6, 0) + glVertex3f(0.4, 0.6, 0) + glVertex3f(-0.3, -0.6, 0) + glVertex3f(0.3, -0.6, 0) + + glVertex3f(-0.3, 0.5, 0) + glVertex3f(-0.2, 0.2, -0.6) + glVertex3f(0.3, 0.5, 0) + glVertex3f(0.2, 0.2, -0.6) + glVertex3f(-0.2, -0.2, 0) + glVertex3f(-0.1, -0.3, -0.6) + glVertex3f(0.2, -0.2, 0) + glVertex3f(0.1, -0.3, -0.6) + glEnd() + + glBegin(GL_LINE_LOOP) + for i in range(8): + angle = 2.0 * math.pi * i / 8 + glVertex3f(0.3 * math.cos(angle), 1.4 + 0.3 * math.sin(angle), 0) + glEnd() + + rad_wing = math.radians(wing_angle) + w_z = math.sin(rad_wing) * 1.2 if wing_angle != 0 else 0.0 + + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(-4.5, 1.0, w_z) + glVertex3f(-3.8, 0.2, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(4.5, 1.0, w_z) + glVertex3f(3.8, 0.2, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(-3.8, -0.2, w_z) + glVertex3f(-3.0, -0.8, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(3.8, -0.2, w_z) + glVertex3f(3.0, -0.8, w_z) + glEnd() + + if caught_insect: + glPushMatrix() + glTranslatef(0.0, 0.2, -0.6) + caught_insect.draw(attached=True) + glPopMatrix() + +# ========================================== +# INTERFAZ 2D / HUD +# ========================================== +def draw_text_2d(text_surface, x, y): + text_data = pygame.image.tostring(text_surface, "RGBA", True) + w, h = text_surface.get_size() + + glEnable(GL_BLEND) + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) + + glRasterPos2i(x, y) + glDrawPixels(w, h, GL_RGBA, GL_UNSIGNED_BYTE, text_data) + glDisable(GL_BLEND) + +def draw_hud(player_pos, yaw, insects, eaten_mosquitos, eaten_flies, font): + glDisable(GL_DEPTH_TEST) + + glMatrixMode(GL_PROJECTION) + glPushMatrix() + glLoadIdentity() + gluOrtho2D(0, WIDTH, 0, HEIGHT) + + glMatrixMode(GL_MODELVIEW) + glPushMatrix() + glLoadIdentity() + + px, py, pz = player_pos + + # RADAR REDUCIDO + cx, cy = WIDTH // 2, HEIGHT - 35 + radius = 22 + + glColor3f(*COLOR_HUD) + glBegin(GL_LINE_LOOP) + for i in range(24): + angle = 2.0 * math.pi * i / 24 + glVertex2f(cx + radius * math.cos(angle), cy + radius * math.sin(angle)) + glEnd() + + glBegin(GL_LINES) + glVertex2f(cx, cy + radius) + glVertex2f(cx, cy + radius - 5) + glVertex2f(cx, cy - radius) + glVertex2f(cx, cy - radius + 5) + glVertex2f(cx - radius, cy) + glVertex2f(cx - radius + 5, cy) + glVertex2f(cx + radius, cy) + glVertex2f(cx + radius - 5, cy) + glEnd() + + closest_insect = None + min_dist = 999999.0 + relative_angle = 0.0 + diff_z = 0.0 + + for insect in insects: + dx = insect.x - px + dy = insect.y - py + dist = math.hypot(dx, dy) + + if dist < min_dist and dist > 0.1: + angle_to_insect = math.degrees(math.atan2(dy, dx)) + heading = (angle_to_insect - (90 - yaw)) % 360 + if heading > 180: + heading -= 360 + + if abs(heading) < 60: + min_dist = dist + closest_insect = insect + relative_angle = heading + diff_z = insect.z - pz + + if closest_insect: + radar_x = cx - (relative_angle / 60.0) * (radius - 4) + radar_y = cy + (diff_z / 20.0) * (radius - 4) + + dist_from_center = math.hypot(radar_x - cx, radar_y - cy) + if dist_from_center > (radius - 3): + factor = (radius - 3) / dist_from_center + radar_x = cx + (radar_x - cx) * factor + radar_y = cy + (radar_y - cy) * factor + + glColor3f(1.0, 1.0, 1.0) + glBegin(GL_LINE_LOOP) + for i in range(8): + ang = 2.0 * math.pi * i / 8 + glVertex2f(radar_x + 3 * math.cos(ang), radar_y + 3 * math.sin(ang)) + glEnd() + + # ÍCONOS SUPERIORES + glColor3f(*COLOR_MOSQUITO) + glBegin(GL_LINES) + glVertex2f(25, HEIGHT - 30) + glVertex2f(12, HEIGHT - 30) + glVertex2f(25, HEIGHT - 30) + glVertex2f(38, HEIGHT - 30) + glEnd() + + glColor3f(*COLOR_FLY) + glBegin(GL_LINES) + glVertex2f(WIDTH - 85, HEIGHT - 30) + glVertex2f(WIDTH - 98, HEIGHT - 30) + glVertex2f(WIDTH - 85, HEIGHT - 30) + glVertex2f(WIDTH - 72, HEIGHT - 30) + glEnd() + + # TEXTOS + txt_mosq = font.render(f"x {eaten_mosquitos}", True, (255, 230, 0)) + txt_flies = font.render(f"x {eaten_flies}", True, (200, 65, 40)) + + draw_text_2d(txt_mosq, 45, HEIGHT - 40) + draw_text_2d(txt_flies, WIDTH - 65, HEIGHT - 40) + + glMatrixMode(GL_PROJECTION) + glPopMatrix() + glMatrixMode(GL_MODELVIEW) + glPopMatrix() + glEnable(GL_DEPTH_TEST) + +# ========================================== +# BUCLE PRINCIPAL +# ========================================== +def main(): + pygame.init() + pygame.font.init() + font = pygame.font.SysFont("Arial", 22, bold=True) + + pygame.display.set_mode((WIDTH, HEIGHT), DOUBLEBUF | OPENGL) + pygame.display.set_caption("Simulador de Vuelo de Libélula 3D") + + glEnable(GL_DEPTH_TEST) + glClearColor(*COLOR_BG, 1.0) + + glMatrixMode(GL_PROJECTION) + gluPerspective(50, (WIDTH / HEIGHT), 0.1, 5000.0) + glMatrixMode(GL_MODELVIEW) + + # Estado de la Libélula + px, py, pz = 0.0, 0.0, 15.0 + yaw = 0.0 + roll = 0.0 + pitch = 0.0 + max_roll = 30.0 + max_pitch = 20.0 + + speed = 0.0 + max_speed = 3.0 + acceleration = 0.06 + deceleration = 0.03 + + min_altitude = 1.0 + max_altitude = 120.0 + + wing_timer = 0.0 + water_timer = 0.0 + is_perched = False + + dive_phase = "none" + pre_dive_alt = 15.0 + pre_dive_speed = 0.0 + + auto_landing = False + target_perch = None + + caught_insect = None + eaten_mosquitos = 0 + eaten_flies = 0 + + frog = Frog() + + insects = [] + num_mosquitos = random.randint(MOSQUITO_MIN, MOSQUITO_MAX) + num_flies = random.randint(FLY_MIN, FLY_MAX) + + for _ in range(num_mosquitos): + insects.append(Insect("mosquito")) + for _ in range(num_flies): + insects.append(Insect("fly")) + + clock = pygame.time.Clock() + + running = True + while running: + dt = clock.tick(60) / 1000.0 + water_timer += dt + + for event in pygame.event.get(): + if event.type == QUIT or (event.type == KEYDOWN and event.key == K_ESCAPE): + running = False + elif event.type == KEYDOWN and event.key == K_SPACE: + if not is_perched and dive_phase == "none": + dive_phase = "diving" + pre_dive_alt = pz + pre_dive_speed = speed + auto_landing = False + elif event.type == KEYDOWN and event.key == K_p: + if not is_perched and dive_phase == "none": + best_reed = None + min_d = 999.0 + for rx, ry, rz, rh in REEDS: + dist = math.hypot(px - rx, py - ry) + if dist < 35.0: + angle_to_reed = math.degrees(math.atan2(ry - py, rx - px)) + heading = (angle_to_reed - (90 - yaw)) % 360 + if heading > 180: + heading -= 360 + if abs(heading) < 45 and dist < min_d: + min_d = dist + best_reed = (rx, ry, rz + rh) + + if best_reed: + target_perch = best_reed + auto_landing = True + + keys = pygame.key.get_pressed() + + if is_perched: + if keys[K_LEFT]: + yaw += 3.0 + if keys[K_RIGHT]: + yaw -= 3.0 + + if keys[K_w]: + is_perched = False + speed = 0.5 + + wing_angle = 0.0 + pitch = 0.0 + roll = 0.0 + dive_phase = "none" + auto_landing = False + else: + if auto_landing and target_perch: + tx, ty, tz = target_perch + dx = tx - px + dy = ty - py + dist_to_target = math.hypot(dx, dy) + + speed = 1.2 + px += (dx / dist_to_target) * speed if dist_to_target > 0.1 else 0 + py += (dy / dist_to_target) * speed if dist_to_target > 0.1 else 0 + pz += (tz - pz) * 0.1 + + if dist_to_target < 2.5 and abs(pz - tz) < 2.5: + px, py, pz = target_perch + speed = 0.0 + roll = 0.0 + pitch = 0.0 + is_perched = True + auto_landing = False + target_perch = None + + if caught_insect: + if caught_insect.type == "mosquito": + eaten_mosquitos += 1 + else: + eaten_flies += 1 + caught_insect = None + + elif dive_phase == "diving": + speed = max_speed * 1.5 + pz -= 1.2 + target_pitch = -45.0 + + if pz <= min_altitude: + pz = min_altitude + dive_phase = "recovering" + + elif dive_phase == "recovering": + target_pitch = 30.0 + pz += 0.8 + speed += (pre_dive_speed - speed) * 0.1 + + if pz >= pre_dive_alt: + pz = pre_dive_alt + speed = pre_dive_speed + dive_phase = "none" + else: + if keys[K_w]: + speed = min(speed + acceleration, max_speed) + elif keys[K_s]: + speed = max(speed - deceleration, 0.0) + + if speed < 0.3: + for rx, ry, rz, rh in REEDS: + dist = math.hypot(px - rx, py - ry) + if dist < 10.0 and abs(pz - (rz + rh)) < 10.0: + px, py, pz = rx, ry, rz + rh + speed = 0.0 + roll = 0.0 + pitch = 0.0 + is_perched = True + + if caught_insect: + if caught_insect.type == "mosquito": + eaten_mosquitos += 1 + else: + eaten_flies += 1 + caught_insect = None + break + + target_pitch = 0.0 + if keys[K_UP]: + pz = min(pz + 0.8, max_altitude) + target_pitch = max_pitch + elif keys[K_DOWN]: + pz = max(pz - 0.8, min_altitude) + target_pitch = -max_pitch + + if not auto_landing: + pitch += (target_pitch - pitch) * 0.15 + + target_roll = 0.0 + if keys[K_LEFT]: + yaw += 2.8 + target_roll = -max_roll + elif keys[K_RIGHT]: + yaw -= 2.8 + target_roll = max_roll + + roll += (target_roll - roll) * 0.15 + + rad_yaw = math.radians(yaw) + px += -math.sin(rad_yaw) * speed + py += math.cos(rad_yaw) * speed + + if abs(py) < POND_RADIUS: + max_limit_x = POND_RADIUS + 200.0 + else: + cx = get_river_center(py) + max_limit_x = abs(cx) + 200.0 + + px = max(-max_limit_x, min(max_limit_x, px)) + + if py > STREAM_MAX_Y: + py -= WORLD_LEN + elif py < STREAM_MIN_Y: + py += WORLD_LEN + + wing_timer += 80.0 if (dive_phase != "none" or auto_landing) else (60.0 if speed > 0.1 else 30.0) + wing_angle = math.sin(wing_timer) * 35.0 + + frog.update() + + current_mosquitos = 0 + current_flies = 0 + + for insect in insects[:]: + insect.update((px, py, pz)) + + if insect.type == "mosquito": + current_mosquitos += 1 + else: + current_flies += 1 + + if not caught_insect: + dist_to_dragonfly = math.sqrt((px - insect.x)**2 + (py - insect.y)**2 + (pz - insect.z)**2) + if dist_to_dragonfly < 6.0: + caught_insect = insect + insects.remove(insect) + + if dive_phase == "diving": + dive_phase = "recovering" + + if current_mosquitos < MOSQUITO_MIN: + insects.append(Insect("mosquito")) + if current_flies < FLY_MIN: + insects.append(Insect("fly")) + + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) + glLoadIdentity() + + cam_dist = 18.0 + cam_height = 8.0 + rad_yaw = math.radians(yaw) + + cam_x = px + math.sin(rad_yaw) * cam_dist + cam_y = py - math.cos(rad_yaw) * cam_dist + cam_z = pz + cam_height + + gluLookAt(cam_x, cam_y, cam_z, + px, py, pz + 0.5, + 0, 0, 1) + + draw_map(water_timer) + + frog.draw() + + for insect in insects: + insect.draw() + + glPushMatrix() + glTranslatef(px, py, pz) + glRotatef(yaw, 0, 0, 1) + glRotatef(pitch, 1, 0, 0) + glRotatef(roll, 0, 1, 0) + draw_dragonfly(wing_angle, caught_insect) + glPopMatrix() + + draw_hud((px, py, pz), yaw, insects, eaten_mosquitos, eaten_flies, font) + + pygame.display.flip() + + pygame.quit() + sys.exit() + +if __name__ == "__main__": + main() diff --git a/previos/dragonfly_game_v7.0.py b/previos/dragonfly_game_v7.0.py new file mode 100644 index 0000000..fd28e52 --- /dev/null +++ b/previos/dragonfly_game_v7.0.py @@ -0,0 +1,830 @@ +import sys +import math +import random +import pygame +from pygame.locals import * +from OpenGL.GL import * +from OpenGL.GLU import * + +# ========================================== +# CONFIGURACIÓN Y CONSTANTES +# ========================================== +WIDTH, HEIGHT = 800, 600 + +# Colores (R, G, B) +COLOR_BG = (0.12, 0.15, 0.12) # Fondo ambiente +COLOR_WATER = (0.0, 0.5, 0.85) # Agua animada (Cian/Azul) +COLOR_LAND = (0.45, 0.32, 0.15) # Tierra / Relieve +COLOR_LAND_RIVER = (0.40, 0.28, 0.12) # Tierra en riachuelos +COLOR_ISLAND = (0.5, 0.38, 0.18) # Islas +COLOR_REED = (0.15, 0.85, 0.25) # Juncos verdes +COLOR_CATTAIL = (0.35, 0.18, 0.08) # Espigas marrones densas +COLOR_DRAGONFLY = (0.0, 1.0, 1.0) # Libélula alámbrica (Cian) +COLOR_MOSQUITO = (1.0, 0.9, 0.0) # Mosquitos (Amarillo) +COLOR_FLY = (0.8, 0.25, 0.15) # Moscas (Rojo-Marrón) +COLOR_FROG = (0.15, 0.85, 0.35) # Rana verde detallada +COLOR_HUD = (1.0, 1.0, 1.0) # Retícula HUD (Blanco) + +# Dimensiones del Mundo +POND_RADIUS = 1800.0 +STREAM_LEN = 900.0 +STREAM_MIN_Y = -(POND_RADIUS + STREAM_LEN) +STREAM_MAX_Y = (POND_RADIUS + STREAM_LEN) +WORLD_LEN = STREAM_MAX_Y - STREAM_MIN_Y + +# Cantidades de insectos +MOSQUITO_MIN, MOSQUITO_MAX = 10, 20 +FLY_MIN, FLY_MAX = 2, 7 + +# ========================================== +# ELEMENTOS DEL MAPA +# ========================================== +REEDS = [ + (-250, 350, 0, 20), (-160, -500, 0, 22), (500, 200, 0, 24), (-700, 200, 0, 21), (200, -800, 0, 23), + (-900, -300, 0, 25), (760, -600, 0, 22), (900, 500, 0, 26), (-500, 900, 0, 23), (600, -1000, 0, 22), + (-1200, 400, 0, 24), (1300, -300, 0, 20), (-1400, -700, 0, 25), (1100, 1200, 0, 23), (-360, -1300, 0, 22), + (-90, -1950, 0, 20), (-110, -2150, 0, 23), (-80, -2350, 0, 19), (-100, -2550, 0, 22), + (90, 1950, 0, 22), (110, 2150, 0, 20), (85, 2350, 0, 24), (105, 2550, 0, 21) +] + +ISLANDS = [ + (-250, 350, 90), (-160, -500, 85), (500, 200, 100), (-700, 200, 110), (200, -800, 95), + (-900, -300, 120), (760, -600, 90), (900, 500, 105), (-500, 900, 115), (600, -1000, 100) +] + +def get_river_center(y): + if y < -POND_RADIUS: + dy = y + POND_RADIUS + return -50 + 100 * math.sin(dy * 0.008) + elif y > POND_RADIUS: + dy = y - POND_RADIUS + return 50 + 100 * math.sin(dy * 0.008) + else: + return (y / POND_RADIUS) * 50.0 + +def is_over_water(x, y): + dist_center = math.hypot(x, y) + if dist_center < POND_RADIUS - 50: + return True + if abs(y) >= POND_RADIUS: + cx = get_river_center(y) + if abs(x - cx) < 80: + return True + return False + +def get_random_water_position(): + while True: + if random.random() < 0.7: + rad = random.uniform(0, math.pi * 2) + r = random.uniform(100, POND_RADIUS - 150) + x = r * math.cos(rad) + y = r * math.sin(rad) + else: + y = random.choice([random.uniform(POND_RADIUS, STREAM_MAX_Y), random.uniform(STREAM_MIN_Y, -POND_RADIUS)]) + cx = get_river_center(y) + x = cx + random.uniform(-60, 60) + + if is_over_water(x, y): + z = random.uniform(5.0, 35.0) + return x, y, z + +# ========================================== +# CLASES DE INSECTOS Y RANA DECORATIVA +# ========================================== +class Insect: + def __init__(self, insect_type): + self.type = insect_type + self.x, self.y, self.z = get_random_water_position() + self.yaw = random.uniform(0, 360) + self.timer = random.uniform(0, 100) + + if self.type == "mosquito": + self.speed = 1.8 + self.size = 1.2 + else: + self.speed = 0.9 + self.size = 1.6 + + def update(self, player_pos): + self.timer += 0.05 + px, py, pz = player_pos + dist_to_player = math.hypot(self.x - px, self.y - py) + + if self.type == "mosquito": + can_detect = (pz <= self.z) + if can_detect and dist_to_player < 100: + escape_angle = math.degrees(math.atan2(self.y - py, self.x - px)) + self.yaw += (escape_angle - self.yaw) * 0.05 + + self.yaw += math.sin(self.timer * 3) * 1.5 + self.z += math.sin(self.timer * 1.5) * 0.2 + + elif self.type == "fly": + if dist_to_player < 120: + escape_angle = math.degrees(math.atan2(self.y - py, self.x - px)) + self.yaw = escape_angle + random.uniform(-45, 45) + self.z += random.choice([-1.5, 1.5]) + else: + if random.random() < 0.08: + self.yaw += random.uniform(-60, 60) + + self.yaw += math.sin(self.timer * 6) * 4.0 + self.z += math.cos(self.timer * 4) * 0.6 + + self.z = max(3.0, min(50.0, self.z)) + + rad = math.radians(self.yaw) + next_x = self.x - math.sin(rad) * self.speed + next_y = self.y + math.cos(rad) * self.speed + + if is_over_water(next_x, next_y): + self.x, self.y = next_x, next_y + else: + self.yaw += 140.0 + random.uniform(-20, 20) + + if self.y > STREAM_MAX_Y: + self.y -= WORLD_LEN + elif self.y < STREAM_MIN_Y: + self.y += WORLD_LEN + + def draw(self, attached=False): + if self.type == "mosquito": + glColor3f(*COLOR_MOSQUITO) + else: + glColor3f(*COLOR_FLY) + + glPushMatrix() + if not attached: + glTranslatef(self.x, self.y, self.z) + glRotatef(self.yaw, 0, 0, 1) + + s = self.size + glBegin(GL_LINES) + glVertex3f(0, s, 0) + glVertex3f(0, -s, 0) + w_offset = 0.0 if attached else math.sin(self.timer * 30) * (s * 0.8) + glVertex3f(0, 0, 0) + glVertex3f(-s * 1.5, 0, w_offset) + glVertex3f(0, 0, 0) + glVertex3f(s * 1.5, 0, w_offset) + glEnd() + + glPopMatrix() + +class Frog: + def __init__(self): + self.pick_new_destination() + self.x, self.y = self.target_x, self.target_y + self.z = 0.6 + self.speed = 0.35 + self.yaw = 0.0 + self.swim_timer = 0.0 + + def pick_new_destination(self): + destinations = [(rx, ry) for rx, ry, _, _ in REEDS] + [(ix, iy) for ix, iy, _ in ISLANDS] + dest = random.choice(destinations) + self.target_x, self.target_y = dest[0] + random.uniform(-30, 30), dest[1] + random.uniform(-30, 30) + + def update(self): + self.swim_timer += 0.15 + dx = self.target_x - self.x + dy = self.target_y - self.y + dist = math.hypot(dx, dy) + + if dist < 6.0: + self.pick_new_destination() + else: + self.yaw = math.degrees(math.atan2(dy, dx)) - 90.0 + self.x += (dx / dist) * self.speed + self.y += (dy / dist) * self.speed + + def draw(self): + glColor3f(*COLOR_FROG) + glPushMatrix() + glTranslatef(self.x, self.y, self.z) + glRotatef(self.yaw, 0, 0, 1) + + leg_offset = math.sin(self.swim_timer) * 1.5 + + glBegin(GL_LINES) + glVertex3f(0.0, 0.0, 0.5) + glVertex3f(0.0, -5.0, 0.5) + glVertex3f(-2.0, 2.0, 0.5) + glVertex3f(2.0, 2.0, 0.5) + glVertex3f(-2.5, -2.0, 0.5) + glVertex3f(2.5, -2.0, 0.5) + + glVertex3f(-2.0, 2.0, 0.5) + glVertex3f(0.0, 0.0, 0.5) + glVertex3f(2.0, 2.0, 0.5) + glVertex3f(0.0, 0.0, 0.5) + + glVertex3f(-2.0, 2.0, 0.6) + glVertex3f(2.0, 2.0, 0.6) + glVertex3f(-2.0, 2.0, 0.6) + glVertex3f(0.0, 4.5, 0.6) + glVertex3f(2.0, 2.0, 0.6) + glVertex3f(0.0, 4.5, 0.6) + + glVertex3f(-1.5, 3.5, 1.5) + glVertex3f(-1.5, 3.5, 0.6) + glVertex3f(1.5, 3.5, 1.5) + glVertex3f(1.5, 3.5, 0.6) + + glVertex3f(-2.0, 2.0, 0.5) + glVertex3f(-4.0, 3.0, 0.0) + glVertex3f(-4.0, 3.0, 0.0) + glVertex3f(-3.5, 4.0, 0.0) + + glVertex3f(2.0, 2.0, 0.5) + glVertex3f(4.0, 3.0, 0.0) + glVertex3f(4.0, 3.0, 0.0) + glVertex3f(3.5, 4.0, 0.0) + + glVertex3f(-2.5, -2.0, 0.5) + glVertex3f(-5.0 - leg_offset, -4.0, 0.0) + glVertex3f(-5.0 - leg_offset, -4.0, 0.0) + glVertex3f(-3.0 - leg_offset, -6.0, 0.0) + + glVertex3f(2.5, -2.0, 0.5) + glVertex3f(5.0 + leg_offset, -4.0, 0.0) + glVertex3f(5.0 + leg_offset, -4.0, 0.0) + glVertex3f(3.0 + leg_offset, -6.0, 0.0) + glEnd() + + glPopMatrix() + +# ========================================== +# RENDERIZADO DE ESCENARIO Y MODELOS +# ========================================== +def draw_water_surface(time_counter): + glColor3f(*COLOR_WATER) + + grid_size = 15 + for x in range(int(-POND_RADIUS), int(POND_RADIUS), grid_size * 2): + glBegin(GL_LINE_STRIP) + for y in range(int(-POND_RADIUS), int(POND_RADIUS), grid_size): + dist = math.hypot(x, y) + if dist < POND_RADIUS - 30: + z = 1.0 * math.sin(dist * 0.015 - time_counter * 3.0) + 0.5 * math.cos(x * 0.01 + time_counter) + glVertex3f(x, y, z) + glEnd() + + for y in range(int(STREAM_MIN_Y), int(STREAM_MAX_Y), 15): + if abs(y) < POND_RADIUS: + continue + cx = get_river_center(y) + glBegin(GL_LINE_STRIP) + for dx in range(-100, 110, 10): + x = cx + dx + z = 0.5 * math.sin(y * 0.03 + dx * 0.08 + time_counter * 4.0) + glVertex3f(x, y, z) + glEnd() + +def draw_terrain(): + glColor3f(*COLOR_LAND) + + grid_step = 120 + for y in range(int(STREAM_MIN_Y - 300), int(STREAM_MAX_Y + 300), grid_step): + glBegin(GL_LINE_STRIP) + for x in range(-2500, 2500, grid_step): + z = 1.5 * math.sin(x * 0.005) + 1.5 * math.cos(y * 0.005) + glVertex3f(x, y, z) + glEnd() + + width_river = 100.0 + margin_land = 70.0 + glColor3f(*COLOR_LAND_RIVER) + + for y in range(int(STREAM_MIN_Y), int(STREAM_MAX_Y), 30): + if abs(y) < POND_RADIUS: + continue + cx = get_river_center(y) + + glBegin(GL_LINE_STRIP) + for dx in range(int(-width_river - margin_land), int(-width_river), 15): + x = cx + dx + z = 0.6 * math.sin(y * 0.02 + x * 0.02) + 0.3 + glVertex3f(x, y, z) + glEnd() + + glBegin(GL_LINE_STRIP) + for dx in range(int(width_river), int(width_river + margin_land), 15): + x = cx + dx + z = 0.6 * math.sin(y * 0.02 + x * 0.02) + 0.3 + glVertex3f(x, y, z) + glEnd() + + for ix, iy, ir in ISLANDS: + glColor3f(*COLOR_ISLAND) + for layer in range(1, 4): + rad_layer = ir * (1 - layer * 0.25) + height = layer * 1.2 + glBegin(GL_LINE_LOOP) + for i in range(16): + angle = 2.0 * math.pi * i / 16 + dx = rad_layer * math.cos(angle) + dy = rad_layer * math.sin(angle) + glVertex3f(ix + dx, iy + dy, height) + glEnd() + +def draw_map(time_counter): + draw_terrain() + draw_water_surface(time_counter) + + offsets = [(-1.5, 0.6), (1.3, -0.6), (-0.7, -1.4), (1.1, 1.2), (0.0, 1.6), (-1.2, 0.0)] + + for rx, ry, rz, rh in REEDS: + glColor3f(*COLOR_REED) + glBegin(GL_LINES) + glVertex3f(rx, ry, rz) + glVertex3f(rx, ry, rz + rh) + for ox, oy in offsets: + glVertex3f(rx, ry, rz) + glVertex3f(rx + ox, ry + oy, rz + rh * 0.85) + glEnd() + + glColor3f(*COLOR_CATTAIL) + glBegin(GL_LINES) + for d in [-0.2, 0.0, 0.2]: + glVertex3f(rx + d, ry, rz + rh - 4.5) + glVertex3f(rx + d, ry, rz + rh) + for ox, oy in offsets: + for d in [-0.15, 0.15]: + glVertex3f(rx + ox + d, ry + oy, (rz + rh * 0.85) - 3.8) + glVertex3f(rx + ox + d, ry + oy, rz + rh * 0.85) + glEnd() + +def draw_dragonfly(wing_angle, caught_insect=None): + glColor3f(*COLOR_DRAGONFLY) + + glBegin(GL_LINES) + glVertex3f(0, 1.5, 0) + glVertex3f(0, -3.0, 0) + glVertex3f(-0.4, 0.6, 0) + glVertex3f(0.4, 0.6, 0) + glVertex3f(-0.3, -0.6, 0) + glVertex3f(0.3, -0.6, 0) + + glVertex3f(-0.3, 0.5, 0) + glVertex3f(-0.2, 0.2, -0.6) + glVertex3f(0.3, 0.5, 0) + glVertex3f(0.2, 0.2, -0.6) + glVertex3f(-0.2, -0.2, 0) + glVertex3f(-0.1, -0.3, -0.6) + glVertex3f(0.2, -0.2, 0) + glVertex3f(0.1, -0.3, -0.6) + glEnd() + + glBegin(GL_LINE_LOOP) + for i in range(8): + angle = 2.0 * math.pi * i / 8 + glVertex3f(0.3 * math.cos(angle), 1.4 + 0.3 * math.sin(angle), 0) + glEnd() + + rad_wing = math.radians(wing_angle) + w_z = math.sin(rad_wing) * 1.2 if wing_angle != 0 else 0.0 + + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(-4.5, 1.0, w_z) + glVertex3f(-3.8, 0.2, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(4.5, 1.0, w_z) + glVertex3f(3.8, 0.2, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(-3.8, -0.2, w_z) + glVertex3f(-3.0, -0.8, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(3.8, -0.2, w_z) + glVertex3f(3.0, -0.8, w_z) + glEnd() + + if caught_insect: + glPushMatrix() + glTranslatef(0.0, 0.2, -0.6) + caught_insect.draw(attached=True) + glPopMatrix() + +# ========================================== +# INTERFAZ 2D / HUD +# ========================================== +def draw_text_2d(text_surface, x, y): + text_data = pygame.image.tostring(text_surface, "RGBA", True) + w, h = text_surface.get_size() + + glEnable(GL_BLEND) + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) + + glRasterPos2i(x, y) + glDrawPixels(w, h, GL_RGBA, GL_UNSIGNED_BYTE, text_data) + glDisable(GL_BLEND) + +def draw_hud(player_pos, yaw, insects, eaten_mosquitos, eaten_flies, font): + glDisable(GL_DEPTH_TEST) + + glMatrixMode(GL_PROJECTION) + glPushMatrix() + glLoadIdentity() + gluOrtho2D(0, WIDTH, 0, HEIGHT) + + glMatrixMode(GL_MODELVIEW) + glPushMatrix() + glLoadIdentity() + + px, py, pz = player_pos + + # RADAR REDUCIDO + cx, cy = WIDTH // 2, HEIGHT - 35 + radius = 22 + + glColor3f(*COLOR_HUD) + glBegin(GL_LINE_LOOP) + for i in range(24): + angle = 2.0 * math.pi * i / 24 + glVertex2f(cx + radius * math.cos(angle), cy + radius * math.sin(angle)) + glEnd() + + glBegin(GL_LINES) + glVertex2f(cx, cy + radius) + glVertex2f(cx, cy + radius - 5) + glVertex2f(cx, cy - radius) + glVertex2f(cx, cy - radius + 5) + glVertex2f(cx - radius, cy) + glVertex2f(cx - radius + 5, cy) + glVertex2f(cx + radius, cy) + glVertex2f(cx + radius - 5, cy) + glEnd() + + closest_insect = None + min_dist = 999999.0 + relative_angle = 0.0 + diff_z = 0.0 + + for insect in insects: + dx = insect.x - px + dy = insect.y - py + dist = math.hypot(dx, dy) + + if dist < min_dist and dist > 0.1: + angle_to_insect = math.degrees(math.atan2(dy, dx)) + heading = (angle_to_insect - (90 - yaw)) % 360 + if heading > 180: + heading -= 360 + + if abs(heading) < 60: + min_dist = dist + closest_insect = insect + relative_angle = heading + diff_z = insect.z - pz + + if closest_insect: + radar_x = cx - (relative_angle / 60.0) * (radius - 4) + radar_y = cy + (diff_z / 20.0) * (radius - 4) + + dist_from_center = math.hypot(radar_x - cx, radar_y - cy) + if dist_from_center > (radius - 3): + factor = (radius - 3) / dist_from_center + radar_x = cx + (radar_x - cx) * factor + radar_y = cy + (radar_y - cy) * factor + + glColor3f(1.0, 1.0, 1.0) + glBegin(GL_LINE_LOOP) + for i in range(8): + ang = 2.0 * math.pi * i / 8 + glVertex2f(radar_x + 3 * math.cos(ang), radar_y + 3 * math.sin(ang)) + glEnd() + + # DIBUJOS ESTÁTICOS 2D DE LOS INSECTOS EN EL HUD + # 1. Icono de Mosquito (Izquierda - Amarillo) + glColor3f(*COLOR_MOSQUITO) + glBegin(GL_LINES) + glVertex2f(25, HEIGHT - 33) + glVertex2f(25, HEIGHT - 27) + glVertex2f(21, HEIGHT - 30) + glVertex2f(29, HEIGHT - 30) + glEnd() + + # 2. Icono de Mosca (Derecha - Rojo/Marrón) + glColor3f(*COLOR_FLY) + glBegin(GL_LINES) + # Cuerpo y alas estáticas de la mosca + glVertex2f(WIDTH - 82, HEIGHT - 33) + glVertex2f(WIDTH - 82, HEIGHT - 27) + glVertex2f(WIDTH - 86, HEIGHT - 28) + glVertex2f(WIDTH - 78, HEIGHT - 32) + glVertex2f(WIDTH - 86, HEIGHT - 32) + glVertex2f(WIDTH - 78, HEIGHT - 28) + glEnd() + + # TEXTOS DE CONTADORES + txt_mosq = font.render(f"x {eaten_mosquitos}", True, (255, 230, 0)) + txt_flies = font.render(f"x {eaten_flies}", True, (200, 65, 40)) + + draw_text_2d(txt_mosq, 38, HEIGHT - 40) + draw_text_2d(txt_flies, WIDTH - 68, HEIGHT - 40) + + glMatrixMode(GL_PROJECTION) + glPopMatrix() + glMatrixMode(GL_MODELVIEW) + glPopMatrix() + glEnable(GL_DEPTH_TEST) + +# ========================================== +# BUCLE PRINCIPAL +# ========================================== +def main(): + pygame.init() + pygame.font.init() + font = pygame.font.SysFont("Arial", 22, bold=True) + + pygame.display.set_mode((WIDTH, HEIGHT), DOUBLEBUF | OPENGL) + pygame.display.set_caption("Simulador de Vuelo de Libélula 3D") + + glEnable(GL_DEPTH_TEST) + glClearColor(*COLOR_BG, 1.0) + + glMatrixMode(GL_PROJECTION) + gluPerspective(50, (WIDTH / HEIGHT), 0.1, 5000.0) + glMatrixMode(GL_MODELVIEW) + + # Estado de la Libélula + px, py, pz = 0.0, 0.0, 15.0 + yaw = 0.0 + roll = 0.0 + pitch = 0.0 + max_roll = 30.0 + max_pitch = 20.0 + + speed = 0.0 + max_speed = 3.0 + acceleration = 0.06 + deceleration = 0.03 + + min_altitude = 1.0 + max_altitude = 120.0 + + wing_timer = 0.0 + water_timer = 0.0 + is_perched = False + + dive_phase = "none" + pre_dive_alt = 15.0 + pre_dive_speed = 0.0 + + auto_landing = False + target_perch = None + + caught_insect = None + eaten_mosquitos = 0 + eaten_flies = 0 + + frog = Frog() + + insects = [] + num_mosquitos = random.randint(MOSQUITO_MIN, MOSQUITO_MAX) + num_flies = random.randint(FLY_MIN, FLY_MAX) + + for _ in range(num_mosquitos): + insects.append(Insect("mosquito")) + for _ in range(num_flies): + insects.append(Insect("fly")) + + clock = pygame.time.Clock() + + running = True + while running: + dt = clock.tick(60) / 1000.0 + water_timer += dt + + for event in pygame.event.get(): + if event.type == QUIT or (event.type == KEYDOWN and event.key == K_ESCAPE): + running = False + elif event.type == KEYDOWN and event.key == K_SPACE: + if not is_perched and dive_phase == "none": + dive_phase = "diving" + pre_dive_alt = pz + pre_dive_speed = speed + auto_landing = False + elif event.type == KEYDOWN and event.key == K_p: + if not is_perched and dive_phase == "none": + best_reed = None + min_d = 999.0 + for rx, ry, rz, rh in REEDS: + dist = math.hypot(px - rx, py - ry) + if dist < 35.0: + angle_to_reed = math.degrees(math.atan2(ry - py, rx - px)) + heading = (angle_to_reed - (90 - yaw)) % 360 + if heading > 180: + heading -= 360 + if abs(heading) < 45 and dist < min_d: + min_d = dist + best_reed = (rx, ry, rz + rh) + + if best_reed: + target_perch = best_reed + auto_landing = True + + keys = pygame.key.get_pressed() + + if is_perched: + if keys[K_LEFT]: + yaw += 3.0 + if keys[K_RIGHT]: + yaw -= 3.0 + + if keys[K_w]: + is_perched = False + speed = 0.5 + + wing_angle = 0.0 + pitch = 0.0 + roll = 0.0 + dive_phase = "none" + auto_landing = False + else: + if auto_landing and target_perch: + tx, ty, tz = target_perch + dx = tx - px + dy = ty - py + dist_to_target = math.hypot(dx, dy) + + speed = 1.2 + px += (dx / dist_to_target) * speed if dist_to_target > 0.1 else 0 + py += (dy / dist_to_target) * speed if dist_to_target > 0.1 else 0 + pz += (tz - pz) * 0.1 + + if dist_to_target < 2.5 and abs(pz - tz) < 2.5: + px, py, pz = target_perch + speed = 0.0 + roll = 0.0 + pitch = 0.0 + is_perched = True + auto_landing = False + target_perch = None + + if caught_insect: + if caught_insect.type == "mosquito": + eaten_mosquitos += 1 + else: + eaten_flies += 1 + caught_insect = None + + elif dive_phase == "diving": + speed = max_speed * 1.5 + pz -= 1.2 + target_pitch = -45.0 + + if pz <= min_altitude: + pz = min_altitude + dive_phase = "recovering" + + elif dive_phase == "recovering": + target_pitch = 30.0 + pz += 0.8 + speed += (pre_dive_speed - speed) * 0.1 + + if pz >= pre_dive_alt: + pz = pre_dive_alt + speed = pre_dive_speed + dive_phase = "none" + else: + if keys[K_w]: + speed = min(speed + acceleration, max_speed) + elif keys[K_s]: + speed = max(speed - deceleration, 0.0) + + if speed < 0.3: + for rx, ry, rz, rh in REEDS: + dist = math.hypot(px - rx, py - ry) + if dist < 10.0 and abs(pz - (rz + rh)) < 10.0: + px, py, pz = rx, ry, rz + rh + speed = 0.0 + roll = 0.0 + pitch = 0.0 + is_perched = True + + if caught_insect: + if caught_insect.type == "mosquito": + eaten_mosquitos += 1 + else: + eaten_flies += 1 + caught_insect = None + break + + target_pitch = 0.0 + if keys[K_UP]: + pz = min(pz + 0.8, max_altitude) + target_pitch = max_pitch + elif keys[K_DOWN]: + pz = max(pz - 0.8, min_altitude) + target_pitch = -max_pitch + + if not auto_landing: + pitch += (target_pitch - pitch) * 0.15 + + target_roll = 0.0 + if keys[K_LEFT]: + yaw += 2.8 + target_roll = -max_roll + elif keys[K_RIGHT]: + yaw -= 2.8 + target_roll = max_roll + + roll += (target_roll - roll) * 0.15 + + rad_yaw = math.radians(yaw) + px += -math.sin(rad_yaw) * speed + py += math.cos(rad_yaw) * speed + + if abs(py) < POND_RADIUS: + max_limit_x = POND_RADIUS + 200.0 + else: + cx = get_river_center(py) + max_limit_x = abs(cx) + 200.0 + + px = max(-max_limit_x, min(max_limit_x, px)) + + if py > STREAM_MAX_Y: + py -= WORLD_LEN + elif py < STREAM_MIN_Y: + py += WORLD_LEN + + wing_timer += 80.0 if (dive_phase != "none" or auto_landing) else (60.0 if speed > 0.1 else 30.0) + wing_angle = math.sin(wing_timer) * 35.0 + + frog.update() + + current_mosquitos = 0 + current_flies = 0 + + for insect in insects[:]: + insect.update((px, py, pz)) + + if insect.type == "mosquito": + current_mosquitos += 1 + else: + current_flies += 1 + + if not caught_insect: + dist_to_dragonfly = math.sqrt((px - insect.x)**2 + (py - insect.y)**2 + (pz - insect.z)**2) + if dist_to_dragonfly < 6.0: + caught_insect = insect + insects.remove(insect) + + if dive_phase == "diving": + dive_phase = "recovering" + + if current_mosquitos < MOSQUITO_MIN: + insects.append(Insect("mosquito")) + if current_flies < FLY_MIN: + insects.append(Insect("fly")) + + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) + glLoadIdentity() + + cam_dist = 18.0 + cam_height = 8.0 + rad_yaw = math.radians(yaw) + + cam_x = px + math.sin(rad_yaw) * cam_dist + cam_y = py - math.cos(rad_yaw) * cam_dist + cam_z = pz + cam_height + + gluLookAt(cam_x, cam_y, cam_z, + px, py, pz + 0.5, + 0, 0, 1) + + draw_map(water_timer) + + frog.draw() + + for insect in insects: + insect.draw() + + glPushMatrix() + glTranslatef(px, py, pz) + glRotatef(yaw, 0, 0, 1) + glRotatef(pitch, 1, 0, 0) + glRotatef(roll, 0, 1, 0) + draw_dragonfly(wing_angle, caught_insect) + glPopMatrix() + + draw_hud((px, py, pz), yaw, insects, eaten_mosquitos, eaten_flies, font) + + pygame.display.flip() + + pygame.quit() + sys.exit() + +if __name__ == "__main__": + main() diff --git a/previos/dragonfly_game_v7.2.py b/previos/dragonfly_game_v7.2.py new file mode 100644 index 0000000..fa3d899 --- /dev/null +++ b/previos/dragonfly_game_v7.2.py @@ -0,0 +1,827 @@ +import sys +import math +import random +import pygame +from pygame.locals import * +from OpenGL.GL import * +from OpenGL.GLU import * + +# ========================================== +# CONFIGURACIÓN Y CONSTANTES +# ========================================== +WIDTH, HEIGHT = 800, 600 + +# Colores (R, G, B) +COLOR_BG = (0.12, 0.15, 0.12) # Fondo ambiente +COLOR_WATER = (0.0, 0.5, 0.85) # Agua animada (Cian/Azul) +COLOR_LAND = (0.45, 0.32, 0.15) # Tierra / Relieve +COLOR_LAND_RIVER = (0.40, 0.28, 0.12) # Tierra en riachuelos +COLOR_ISLAND = (0.5, 0.38, 0.18) # Islas +COLOR_REED = (0.15, 0.85, 0.25) # Juncos verdes +COLOR_CATTAIL = (0.35, 0.18, 0.08) # Espigas marrones densas +COLOR_DRAGONFLY = (0.0, 1.0, 1.0) # Libélula alámbrica (Cian) +COLOR_MOSQUITO = (1.0, 0.9, 0.0) # Mosquitos (Amarillo) +COLOR_FLY = (0.8, 0.25, 0.15) # Moscas (Rojo-Marrón) +COLOR_FROG = (0.15, 0.85, 0.35) # Rana verde detallada +COLOR_HUD = (1.0, 1.0, 1.0) # Retícula HUD (Blanco) + +# Dimensiones del Mundo +POND_RADIUS = 1800.0 +STREAM_LEN = 900.0 +STREAM_MIN_Y = -(POND_RADIUS + STREAM_LEN) +STREAM_MAX_Y = (POND_RADIUS + STREAM_LEN) +WORLD_LEN = STREAM_MAX_Y - STREAM_MIN_Y + +# Cantidades de insectos (Aumentadas en +7 el mínimo y máximo) +MOSQUITO_MIN, MOSQUITO_MAX = 17, 27 +FLY_MIN, FLY_MAX = 9, 14 + +# ========================================== +# ELEMENTOS DEL MAPA +# ========================================== +REEDS = [ + (-250, 350, 0, 20), (-160, -500, 0, 22), (500, 200, 0, 24), (-700, 200, 0, 21), (200, -800, 0, 23), + (-900, -300, 0, 25), (760, -600, 0, 22), (900, 500, 0, 26), (-500, 900, 0, 23), (600, -1000, 0, 22), + (-1200, 400, 0, 24), (1300, -300, 0, 20), (-1400, -700, 0, 25), (1100, 1200, 0, 23), (-360, -1300, 0, 22), + (-90, -1950, 0, 20), (-110, -2150, 0, 23), (-80, -2350, 0, 19), (-100, -2550, 0, 22), + (90, 1950, 0, 22), (110, 2150, 0, 20), (85, 2350, 0, 24), (105, 2550, 0, 21) +] + +ISLANDS = [ + (-250, 350, 90), (-160, -500, 85), (500, 200, 100), (-700, 200, 110), (200, -800, 95), + (-900, -300, 120), (760, -600, 90), (900, 500, 105), (-500, 900, 115), (600, -1000, 100) +] + +def get_river_center(y): + if y < -POND_RADIUS: + dy = y + POND_RADIUS + return -50 + 100 * math.sin(dy * 0.008) + elif y > POND_RADIUS: + dy = y - POND_RADIUS + return 50 + 100 * math.sin(dy * 0.008) + else: + return (y / POND_RADIUS) * 50.0 + +def is_over_water(x, y): + dist_center = math.hypot(x, y) + if dist_center < POND_RADIUS - 50: + return True + if abs(y) >= POND_RADIUS: + cx = get_river_center(y) + if abs(x - cx) < 80: + return True + return False + +def get_random_water_position(): + while True: + if random.random() < 0.7: + rad = random.uniform(0, math.pi * 2) + r = random.uniform(100, POND_RADIUS - 150) + x = r * math.cos(rad) + y = r * math.sin(rad) + else: + y = random.choice([random.uniform(POND_RADIUS, STREAM_MAX_Y), random.uniform(STREAM_MIN_Y, -POND_RADIUS)]) + cx = get_river_center(y) + x = cx + random.uniform(-60, 60) + + if is_over_water(x, y): + z = random.uniform(5.0, 35.0) + return x, y, z + +# ========================================== +# CLASES DE INSECTOS Y RANA DECORATIVA +# ========================================== +class Insect: + def __init__(self, insect_type): + self.type = insect_type + self.x, self.y, self.z = get_random_water_position() + self.yaw = random.uniform(0, 360) + self.timer = random.uniform(0, 100) + + if self.type == "mosquito": + self.speed = 1.8 + self.size = 1.2 + else: + self.speed = 0.9 + self.size = 1.6 + + def update(self, player_pos): + self.timer += 0.05 + px, py, pz = player_pos + dist_to_player = math.hypot(self.x - px, self.y - py) + + if self.type == "mosquito": + can_detect = (pz <= self.z) + if can_detect and dist_to_player < 100: + escape_angle = math.degrees(math.atan2(self.y - py, self.x - px)) + self.yaw += (escape_angle - self.yaw) * 0.05 + + self.yaw += math.sin(self.timer * 3) * 1.5 + self.z += math.sin(self.timer * 1.5) * 0.2 + + elif self.type == "fly": + if dist_to_player < 120: + escape_angle = math.degrees(math.atan2(self.y - py, self.x - px)) + self.yaw = escape_angle + random.uniform(-45, 45) + self.z += random.choice([-1.5, 1.5]) + else: + if random.random() < 0.08: + self.yaw += random.uniform(-60, 60) + + self.yaw += math.sin(self.timer * 6) * 4.0 + self.z += math.cos(self.timer * 4) * 0.6 + + self.z = max(3.0, min(50.0, self.z)) + + rad = math.radians(self.yaw) + next_x = self.x - math.sin(rad) * self.speed + next_y = self.y + math.cos(rad) * self.speed + + if is_over_water(next_x, next_y): + self.x, self.y = next_x, next_y + else: + self.yaw += 140.0 + random.uniform(-20, 20) + + if self.y > STREAM_MAX_Y: + self.y -= WORLD_LEN + elif self.y < STREAM_MIN_Y: + self.y += WORLD_LEN + + def draw(self, attached=False): + if self.type == "mosquito": + glColor3f(*COLOR_MOSQUITO) + else: + glColor3f(*COLOR_FLY) + + glPushMatrix() + if not attached: + glTranslatef(self.x, self.y, self.z) + glRotatef(self.yaw, 0, 0, 1) + + s = self.size + glBegin(GL_LINES) + glVertex3f(0, s, 0) + glVertex3f(0, -s, 0) + w_offset = 0.0 if attached else math.sin(self.timer * 30) * (s * 0.8) + glVertex3f(0, 0, 0) + glVertex3f(-s * 1.5, 0, w_offset) + glVertex3f(0, 0, 0) + glVertex3f(s * 1.5, 0, w_offset) + glEnd() + + glPopMatrix() + +class Frog: + def __init__(self): + self.pick_new_destination() + self.x, self.y = self.target_x, self.target_y + self.z = 0.6 + self.speed = 0.35 + self.yaw = 0.0 + self.swim_timer = 0.0 + + def pick_new_destination(self): + destinations = [(rx, ry) for rx, ry, _, _ in REEDS] + [(ix, iy) for ix, iy, _ in ISLANDS] + dest = random.choice(destinations) + self.target_x, self.target_y = dest[0] + random.uniform(-30, 30), dest[1] + random.uniform(-30, 30) + + def update(self): + self.swim_timer += 0.15 + dx = self.target_x - self.x + dy = self.target_y - self.y + dist = math.hypot(dx, dy) + + if dist < 6.0: + self.pick_new_destination() + else: + self.yaw = math.degrees(math.atan2(dy, dx)) - 90.0 + self.x += (dx / dist) * self.speed + self.y += (dy / dist) * self.speed + + def draw(self): + glColor3f(*COLOR_FROG) + glPushMatrix() + glTranslatef(self.x, self.y, self.z) + glRotatef(self.yaw, 0, 0, 1) + + leg_offset = math.sin(self.swim_timer) * 1.5 + + glBegin(GL_LINES) + glVertex3f(0.0, 0.0, 0.5) + glVertex3f(0.0, -5.0, 0.5) + glVertex3f(-2.0, 2.0, 0.5) + glVertex3f(2.0, 2.0, 0.5) + glVertex3f(-2.5, -2.0, 0.5) + glVertex3f(2.5, -2.0, 0.5) + + glVertex3f(-2.0, 2.0, 0.5) + glVertex3f(0.0, 0.0, 0.5) + glVertex3f(2.0, 2.0, 0.5) + glVertex3f(0.0, 0.0, 0.5) + + glVertex3f(-2.0, 2.0, 0.6) + glVertex3f(2.0, 2.0, 0.6) + glVertex3f(-2.0, 2.0, 0.6) + glVertex3f(0.0, 4.5, 0.6) + glVertex3f(2.0, 2.0, 0.6) + glVertex3f(0.0, 4.5, 0.6) + + glVertex3f(-1.5, 3.5, 1.5) + glVertex3f(-1.5, 3.5, 0.6) + glVertex3f(1.5, 3.5, 1.5) + glVertex3f(1.5, 3.5, 0.6) + + glVertex3f(-2.0, 2.0, 0.5) + glVertex3f(-4.0, 3.0, 0.0) + glVertex3f(-4.0, 3.0, 0.0) + glVertex3f(-3.5, 4.0, 0.0) + + glVertex3f(2.0, 2.0, 0.5) + glVertex3f(4.0, 3.0, 0.0) + glVertex3f(4.0, 3.0, 0.0) + glVertex3f(3.5, 4.0, 0.0) + + glVertex3f(-2.5, -2.0, 0.5) + glVertex3f(-5.0 - leg_offset, -4.0, 0.0) + glVertex3f(-5.0 - leg_offset, -4.0, 0.0) + glVertex3f(-3.0 - leg_offset, -6.0, 0.0) + + glVertex3f(2.5, -2.0, 0.5) + glVertex3f(5.0 + leg_offset, -4.0, 0.0) + glVertex3f(5.0 + leg_offset, -4.0, 0.0) + glVertex3f(3.0 + leg_offset, -6.0, 0.0) + glEnd() + + glPopMatrix() + +# ========================================== +# RENDERIZADO DE ESCENARIO Y MODELOS +# ========================================== +def draw_water_surface(time_counter): + glColor3f(*COLOR_WATER) + + grid_size = 15 + for x in range(int(-POND_RADIUS), int(POND_RADIUS), grid_size * 2): + glBegin(GL_LINE_STRIP) + for y in range(int(-POND_RADIUS), int(POND_RADIUS), grid_size): + dist = math.hypot(x, y) + if dist < POND_RADIUS - 30: + z = 1.0 * math.sin(dist * 0.015 - time_counter * 3.0) + 0.5 * math.cos(x * 0.01 + time_counter) + glVertex3f(x, y, z) + glEnd() + + for y in range(int(STREAM_MIN_Y), int(STREAM_MAX_Y), 15): + if abs(y) < POND_RADIUS: + continue + cx = get_river_center(y) + glBegin(GL_LINE_STRIP) + for dx in range(-100, 110, 10): + x = cx + dx + z = 0.5 * math.sin(y * 0.03 + dx * 0.08 + time_counter * 4.0) + glVertex3f(x, y, z) + glEnd() + +def draw_terrain(): + glColor3f(*COLOR_LAND) + + grid_step = 120 + for y in range(int(STREAM_MIN_Y - 300), int(STREAM_MAX_Y + 300), grid_step): + glBegin(GL_LINE_STRIP) + for x in range(-2500, 2500, grid_step): + z = 1.5 * math.sin(x * 0.005) + 1.5 * math.cos(y * 0.005) + glVertex3f(x, y, z) + glEnd() + + width_river = 100.0 + margin_land = 70.0 + glColor3f(*COLOR_LAND_RIVER) + + for y in range(int(STREAM_MIN_Y), int(STREAM_MAX_Y), 30): + if abs(y) < POND_RADIUS: + continue + cx = get_river_center(y) + + glBegin(GL_LINE_STRIP) + for dx in range(int(-width_river - margin_land), int(-width_river), 15): + x = cx + dx + z = 0.6 * math.sin(y * 0.02 + x * 0.02) + 0.3 + glVertex3f(x, y, z) + glEnd() + + glBegin(GL_LINE_STRIP) + for dx in range(int(width_river), int(width_river + margin_land), 15): + x = cx + dx + z = 0.6 * math.sin(y * 0.02 + x * 0.02) + 0.3 + glVertex3f(x, y, z) + glEnd() + + for ix, iy, ir in ISLANDS: + glColor3f(*COLOR_ISLAND) + for layer in range(1, 4): + rad_layer = ir * (1 - layer * 0.25) + height = layer * 1.2 + glBegin(GL_LINE_LOOP) + for i in range(16): + angle = 2.0 * math.pi * i / 16 + dx = rad_layer * math.cos(angle) + dy = rad_layer * math.sin(angle) + glVertex3f(ix + dx, iy + dy, height) + glEnd() + +def draw_map(time_counter): + draw_terrain() + draw_water_surface(time_counter) + + offsets = [(-1.5, 0.6), (1.3, -0.6), (-0.7, -1.4), (1.1, 1.2), (0.0, 1.6), (-1.2, 0.0)] + + for rx, ry, rz, rh in REEDS: + glColor3f(*COLOR_REED) + glBegin(GL_LINES) + glVertex3f(rx, ry, rz) + glVertex3f(rx, ry, rz + rh) + for ox, oy in offsets: + glVertex3f(rx, ry, rz) + glVertex3f(rx + ox, ry + oy, rz + rh * 0.85) + glEnd() + + glColor3f(*COLOR_CATTAIL) + glBegin(GL_LINES) + for d in [-0.2, 0.0, 0.2]: + glVertex3f(rx + d, ry, rz + rh - 4.5) + glVertex3f(rx + d, ry, rz + rh) + for ox, oy in offsets: + for d in [-0.15, 0.15]: + glVertex3f(rx + ox + d, ry + oy, (rz + rh * 0.85) - 3.8) + glVertex3f(rx + ox + d, ry + oy, rz + rh * 0.85) + glEnd() + +def draw_dragonfly(wing_angle, caught_insect=None): + glColor3f(*COLOR_DRAGONFLY) + + glBegin(GL_LINES) + glVertex3f(0, 1.5, 0) + glVertex3f(0, -3.0, 0) + glVertex3f(-0.4, 0.6, 0) + glVertex3f(0.4, 0.6, 0) + glVertex3f(-0.3, -0.6, 0) + glVertex3f(0.3, -0.6, 0) + + glVertex3f(-0.3, 0.5, 0) + glVertex3f(-0.2, 0.2, -0.6) + glVertex3f(0.3, 0.5, 0) + glVertex3f(0.2, 0.2, -0.6) + glVertex3f(-0.2, -0.2, 0) + glVertex3f(-0.1, -0.3, -0.6) + glVertex3f(0.2, -0.2, 0) + glVertex3f(0.1, -0.3, -0.6) + glEnd() + + glBegin(GL_LINE_LOOP) + for i in range(8): + angle = 2.0 * math.pi * i / 8 + glVertex3f(0.3 * math.cos(angle), 1.4 + 0.3 * math.sin(angle), 0) + glEnd() + + rad_wing = math.radians(wing_angle) + w_z = math.sin(rad_wing) * 1.2 if wing_angle != 0 else 0.0 + + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(-4.5, 1.0, w_z) + glVertex3f(-3.8, 0.2, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(4.5, 1.0, w_z) + glVertex3f(3.8, 0.2, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(-3.8, -0.2, w_z) + glVertex3f(-3.0, -0.8, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(3.8, -0.2, w_z) + glVertex3f(3.0, -0.8, w_z) + glEnd() + + if caught_insect: + glPushMatrix() + glTranslatef(0.0, 0.2, -0.6) + caught_insect.draw(attached=True) + glPopMatrix() + +# ========================================== +# INTERFAZ 2D / HUD +# ========================================== +def draw_text_2d(text_surface, x, y): + text_data = pygame.image.tostring(text_surface, "RGBA", True) + w, h = text_surface.get_size() + + glEnable(GL_BLEND) + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) + + glRasterPos2i(x, y) + glDrawPixels(w, h, GL_RGBA, GL_UNSIGNED_BYTE, text_data) + glDisable(GL_BLEND) + +def draw_hud(player_pos, yaw, insects, eaten_mosquitos, eaten_flies, font): + glDisable(GL_DEPTH_TEST) + + glMatrixMode(GL_PROJECTION) + glPushMatrix() + glLoadIdentity() + gluOrtho2D(0, WIDTH, 0, HEIGHT) + + glMatrixMode(GL_MODELVIEW) + glPushMatrix() + glLoadIdentity() + + px, py, pz = player_pos + + # RADAR REDUCIDO + cx, cy = WIDTH // 2, HEIGHT - 35 + radius = 22 + + glColor3f(*COLOR_HUD) + glBegin(GL_LINE_LOOP) + for i in range(24): + angle = 2.0 * math.pi * i / 24 + glVertex2f(cx + radius * math.cos(angle), cy + radius * math.sin(angle)) + glEnd() + + glBegin(GL_LINES) + glVertex2f(cx, cy + radius) + glVertex2f(cx, cy + radius - 5) + glVertex2f(cx, cy - radius) + glVertex2f(cx, cy - radius + 5) + glVertex2f(cx - radius, cy) + glVertex2f(cx - radius + 5, cy) + glVertex2f(cx + radius, cy) + glVertex2f(cx + radius - 5, cy) + glEnd() + + closest_insect = None + min_dist = 999999.0 + relative_angle = 0.0 + diff_z = 0.0 + + for insect in insects: + dx = insect.x - px + dy = insect.y - py + dist = math.hypot(dx, dy) + + if dist < min_dist and dist > 0.1: + angle_to_insect = math.degrees(math.atan2(dy, dx)) + heading = (angle_to_insect - (90 - yaw)) % 360 + if heading > 180: + heading -= 360 + + if abs(heading) < 60: + min_dist = dist + closest_insect = insect + relative_angle = heading + diff_z = insect.z - pz + + if closest_insect: + radar_x = cx - (relative_angle / 60.0) * (radius - 4) + radar_y = cy + (diff_z / 20.0) * (radius - 4) + + dist_from_center = math.hypot(radar_x - cx, radar_y - cy) + if dist_from_center > (radius - 3): + factor = (radius - 3) / dist_from_center + radar_x = cx + (radar_x - cx) * factor + radar_y = cy + (radar_y - cy) * factor + + glColor3f(1.0, 1.0, 1.0) + glBegin(GL_LINE_LOOP) + for i in range(8): + ang = 2.0 * math.pi * i / 8 + glVertex2f(radar_x + 3 * math.cos(ang), radar_y + 3 * math.sin(ang)) + glEnd() + + # DIBUJOS ESTÁTICOS 2D DE LOS INSECTOS EN EL HUD + glColor3f(*COLOR_MOSQUITO) + glBegin(GL_LINES) + glVertex2f(25, HEIGHT - 33) + glVertex2f(25, HEIGHT - 27) + glVertex2f(21, HEIGHT - 30) + glVertex2f(29, HEIGHT - 30) + glEnd() + + glColor3f(*COLOR_FLY) + glBegin(GL_LINES) + glVertex2f(WIDTH - 82, HEIGHT - 33) + glVertex2f(WIDTH - 82, HEIGHT - 27) + glVertex2f(WIDTH - 86, HEIGHT - 28) + glVertex2f(WIDTH - 78, HEIGHT - 32) + glVertex2f(WIDTH - 86, HEIGHT - 32) + glVertex2f(WIDTH - 78, HEIGHT - 28) + glEnd() + + # TEXTOS DE CONTADORES + txt_mosq = font.render(f"x {eaten_mosquitos}", True, (255, 230, 0)) + txt_flies = font.render(f"x {eaten_flies}", True, (200, 65, 40)) + + draw_text_2d(txt_mosq, 38, HEIGHT - 40) + draw_text_2d(txt_flies, WIDTH - 68, HEIGHT - 40) + + glMatrixMode(GL_PROJECTION) + glPopMatrix() + glMatrixMode(GL_MODELVIEW) + glPopMatrix() + glEnable(GL_DEPTH_TEST) + +# ========================================== +# BUCLE PRINCIPAL +# ========================================== +def main(): + pygame.init() + pygame.font.init() + font = pygame.font.SysFont("Arial", 22, bold=True) + + pygame.display.set_mode((WIDTH, HEIGHT), DOUBLEBUF | OPENGL) + pygame.display.set_caption("Simulador de Vuelo de Libélula 3D") + + glEnable(GL_DEPTH_TEST) + glClearColor(*COLOR_BG, 1.0) + + glMatrixMode(GL_PROJECTION) + gluPerspective(50, (WIDTH / HEIGHT), 0.1, 5000.0) + glMatrixMode(GL_MODELVIEW) + + # Estado de la Libélula + px, py, pz = 0.0, 0.0, 15.0 + yaw = 0.0 + roll = 0.0 + pitch = 0.0 + max_roll = 30.0 + max_pitch = 20.0 + + speed = 0.0 + max_speed = 3.0 + acceleration = 0.06 + deceleration = 0.03 + + min_altitude = 1.0 + max_altitude = 120.0 + + wing_timer = 0.0 + water_timer = 0.0 + is_perched = False + + dive_phase = "none" + pre_dive_alt = 15.0 + pre_dive_speed = 0.0 + + auto_landing = False + target_perch = None + + caught_insect = None + eaten_mosquitos = 0 + eaten_flies = 0 + + frog = Frog() + + insects = [] + num_mosquitos = random.randint(MOSQUITO_MIN, MOSQUITO_MAX) + num_flies = random.randint(FLY_MIN, FLY_MAX) + + for _ in range(num_mosquitos): + insects.append(Insect("mosquito")) + for _ in range(num_flies): + insects.append(Insect("fly")) + + clock = pygame.time.Clock() + + running = True + while running: + dt = clock.tick(60) / 1000.0 + water_timer += dt + + for event in pygame.event.get(): + if event.type == QUIT or (event.type == KEYDOWN and event.key == K_ESCAPE): + running = False + elif event.type == KEYDOWN and event.key == K_SPACE: + if not is_perched and dive_phase == "none": + dive_phase = "diving" + pre_dive_alt = pz + pre_dive_speed = speed + auto_landing = False + elif event.type == KEYDOWN and event.key == K_p: + if not is_perched and dive_phase == "none": + best_reed = None + min_d = 999.0 + for rx, ry, rz, rh in REEDS: + dist = math.hypot(px - rx, py - ry) + if dist < 35.0: + angle_to_reed = math.degrees(math.atan2(ry - py, rx - px)) + heading = (angle_to_reed - (90 - yaw)) % 360 + if heading > 180: + heading -= 360 + if abs(heading) < 45 and dist < min_d: + min_d = dist + best_reed = (rx, ry, rz + rh) + + if best_reed: + target_perch = best_reed + auto_landing = True + + keys = pygame.key.get_pressed() + + if is_perched: + if keys[K_LEFT]: + yaw += 3.0 + if keys[K_RIGHT]: + yaw -= 3.0 + + if keys[K_w]: + is_perched = False + speed = 0.5 + + wing_angle = 0.0 + pitch = 0.0 + roll = 0.0 + dive_phase = "none" + auto_landing = False + else: + if auto_landing and target_perch: + tx, ty, tz = target_perch + dx = tx - px + dy = ty - py + dist_to_target = math.hypot(dx, dy) + + speed = 1.2 + px += (dx / dist_to_target) * speed if dist_to_target > 0.1 else 0 + py += (dy / dist_to_target) * speed if dist_to_target > 0.1 else 0 + pz += (tz - pz) * 0.1 + + if dist_to_target < 2.5 and abs(pz - tz) < 2.5: + px, py, pz = target_perch + speed = 0.0 + roll = 0.0 + pitch = 0.0 + is_perched = True + auto_landing = False + target_perch = None + + if caught_insect: + if caught_insect.type == "mosquito": + eaten_mosquitos += 1 + else: + eaten_flies += 1 + caught_insect = None + + elif dive_phase == "diving": + speed = max_speed * 1.5 + pz -= 1.2 + target_pitch = -45.0 + + if pz <= min_altitude: + pz = min_altitude + dive_phase = "recovering" + + elif dive_phase == "recovering": + target_pitch = 30.0 + pz += 0.8 + speed += (pre_dive_speed - speed) * 0.1 + + if pz >= pre_dive_alt: + pz = pre_dive_alt + speed = pre_dive_speed + dive_phase = "none" + else: + if keys[K_w]: + speed = min(speed + acceleration, max_speed) + elif keys[K_s]: + speed = max(speed - deceleration, 0.0) + + if speed < 0.3: + for rx, ry, rz, rh in REEDS: + dist = math.hypot(px - rx, py - ry) + if dist < 10.0 and abs(pz - (rz + rh)) < 10.0: + px, py, pz = rx, ry, rz + rh + speed = 0.0 + roll = 0.0 + pitch = 0.0 + is_perched = True + + if caught_insect: + if caught_insect.type == "mosquito": + eaten_mosquitos += 1 + else: + eaten_flies += 1 + caught_insect = None + break + + target_pitch = 0.0 + if keys[K_UP]: + pz = min(pz + 0.8, max_altitude) + target_pitch = max_pitch + elif keys[K_DOWN]: + pz = max(pz - 0.8, min_altitude) + target_pitch = -max_pitch + + if not auto_landing: + pitch += (target_pitch - pitch) * 0.15 + + target_roll = 0.0 + if keys[K_LEFT]: + yaw += 2.8 + target_roll = -max_roll + elif keys[K_RIGHT]: + yaw -= 2.8 + target_roll = max_roll + + roll += (target_roll - roll) * 0.15 + + rad_yaw = math.radians(yaw) + px += -math.sin(rad_yaw) * speed + py += math.cos(rad_yaw) * speed + + if abs(py) < POND_RADIUS: + max_limit_x = POND_RADIUS + 200.0 + else: + cx = get_river_center(py) + max_limit_x = abs(cx) + 200.0 + + px = max(-max_limit_x, min(max_limit_x, px)) + + if py > STREAM_MAX_Y: + py -= WORLD_LEN + elif py < STREAM_MIN_Y: + py += WORLD_LEN + + wing_timer += 80.0 if (dive_phase != "none" or auto_landing) else (60.0 if speed > 0.1 else 30.0) + wing_angle = math.sin(wing_timer) * 35.0 + + frog.update() + + current_mosquitos = 0 + current_flies = 0 + + for insect in insects[:]: + insect.update((px, py, pz)) + + if insect.type == "mosquito": + current_mosquitos += 1 + else: + current_flies += 1 + + if not caught_insect: + dist_to_dragonfly = math.sqrt((px - insect.x)**2 + (py - insect.y)**2 + (pz - insect.z)**2) + if dist_to_dragonfly < 6.0: + caught_insect = insect + insects.remove(insect) + + if dive_phase == "diving": + dive_phase = "recovering" + + if current_mosquitos < MOSQUITO_MIN: + insects.append(Insect("mosquito")) + if current_flies < FLY_MIN: + insects.append(Insect("fly")) + + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) + glLoadIdentity() + + cam_dist = 18.0 + cam_height = 8.0 + rad_yaw = math.radians(yaw) + + cam_x = px + math.sin(rad_yaw) * cam_dist + cam_y = py - math.cos(rad_yaw) * cam_dist + cam_z = pz + cam_height + + gluLookAt(cam_x, cam_y, cam_z, + px, py, pz + 0.5, + 0, 0, 1) + + draw_map(water_timer) + + frog.draw() + + for insect in insects: + insect.draw() + + glPushMatrix() + glTranslatef(px, py, pz) + glRotatef(yaw, 0, 0, 1) + glRotatef(pitch, 1, 0, 0) + glRotatef(roll, 0, 1, 0) + draw_dragonfly(wing_angle, caught_insect) + glPopMatrix() + + draw_hud((px, py, pz), yaw, insects, eaten_mosquitos, eaten_flies, font) + + pygame.display.flip() + + pygame.quit() + sys.exit() + +if __name__ == "__main__": + main() diff --git a/previos/dragonfly_game_v8.0.py b/previos/dragonfly_game_v8.0.py new file mode 100644 index 0000000..fa3d899 --- /dev/null +++ b/previos/dragonfly_game_v8.0.py @@ -0,0 +1,827 @@ +import sys +import math +import random +import pygame +from pygame.locals import * +from OpenGL.GL import * +from OpenGL.GLU import * + +# ========================================== +# CONFIGURACIÓN Y CONSTANTES +# ========================================== +WIDTH, HEIGHT = 800, 600 + +# Colores (R, G, B) +COLOR_BG = (0.12, 0.15, 0.12) # Fondo ambiente +COLOR_WATER = (0.0, 0.5, 0.85) # Agua animada (Cian/Azul) +COLOR_LAND = (0.45, 0.32, 0.15) # Tierra / Relieve +COLOR_LAND_RIVER = (0.40, 0.28, 0.12) # Tierra en riachuelos +COLOR_ISLAND = (0.5, 0.38, 0.18) # Islas +COLOR_REED = (0.15, 0.85, 0.25) # Juncos verdes +COLOR_CATTAIL = (0.35, 0.18, 0.08) # Espigas marrones densas +COLOR_DRAGONFLY = (0.0, 1.0, 1.0) # Libélula alámbrica (Cian) +COLOR_MOSQUITO = (1.0, 0.9, 0.0) # Mosquitos (Amarillo) +COLOR_FLY = (0.8, 0.25, 0.15) # Moscas (Rojo-Marrón) +COLOR_FROG = (0.15, 0.85, 0.35) # Rana verde detallada +COLOR_HUD = (1.0, 1.0, 1.0) # Retícula HUD (Blanco) + +# Dimensiones del Mundo +POND_RADIUS = 1800.0 +STREAM_LEN = 900.0 +STREAM_MIN_Y = -(POND_RADIUS + STREAM_LEN) +STREAM_MAX_Y = (POND_RADIUS + STREAM_LEN) +WORLD_LEN = STREAM_MAX_Y - STREAM_MIN_Y + +# Cantidades de insectos (Aumentadas en +7 el mínimo y máximo) +MOSQUITO_MIN, MOSQUITO_MAX = 17, 27 +FLY_MIN, FLY_MAX = 9, 14 + +# ========================================== +# ELEMENTOS DEL MAPA +# ========================================== +REEDS = [ + (-250, 350, 0, 20), (-160, -500, 0, 22), (500, 200, 0, 24), (-700, 200, 0, 21), (200, -800, 0, 23), + (-900, -300, 0, 25), (760, -600, 0, 22), (900, 500, 0, 26), (-500, 900, 0, 23), (600, -1000, 0, 22), + (-1200, 400, 0, 24), (1300, -300, 0, 20), (-1400, -700, 0, 25), (1100, 1200, 0, 23), (-360, -1300, 0, 22), + (-90, -1950, 0, 20), (-110, -2150, 0, 23), (-80, -2350, 0, 19), (-100, -2550, 0, 22), + (90, 1950, 0, 22), (110, 2150, 0, 20), (85, 2350, 0, 24), (105, 2550, 0, 21) +] + +ISLANDS = [ + (-250, 350, 90), (-160, -500, 85), (500, 200, 100), (-700, 200, 110), (200, -800, 95), + (-900, -300, 120), (760, -600, 90), (900, 500, 105), (-500, 900, 115), (600, -1000, 100) +] + +def get_river_center(y): + if y < -POND_RADIUS: + dy = y + POND_RADIUS + return -50 + 100 * math.sin(dy * 0.008) + elif y > POND_RADIUS: + dy = y - POND_RADIUS + return 50 + 100 * math.sin(dy * 0.008) + else: + return (y / POND_RADIUS) * 50.0 + +def is_over_water(x, y): + dist_center = math.hypot(x, y) + if dist_center < POND_RADIUS - 50: + return True + if abs(y) >= POND_RADIUS: + cx = get_river_center(y) + if abs(x - cx) < 80: + return True + return False + +def get_random_water_position(): + while True: + if random.random() < 0.7: + rad = random.uniform(0, math.pi * 2) + r = random.uniform(100, POND_RADIUS - 150) + x = r * math.cos(rad) + y = r * math.sin(rad) + else: + y = random.choice([random.uniform(POND_RADIUS, STREAM_MAX_Y), random.uniform(STREAM_MIN_Y, -POND_RADIUS)]) + cx = get_river_center(y) + x = cx + random.uniform(-60, 60) + + if is_over_water(x, y): + z = random.uniform(5.0, 35.0) + return x, y, z + +# ========================================== +# CLASES DE INSECTOS Y RANA DECORATIVA +# ========================================== +class Insect: + def __init__(self, insect_type): + self.type = insect_type + self.x, self.y, self.z = get_random_water_position() + self.yaw = random.uniform(0, 360) + self.timer = random.uniform(0, 100) + + if self.type == "mosquito": + self.speed = 1.8 + self.size = 1.2 + else: + self.speed = 0.9 + self.size = 1.6 + + def update(self, player_pos): + self.timer += 0.05 + px, py, pz = player_pos + dist_to_player = math.hypot(self.x - px, self.y - py) + + if self.type == "mosquito": + can_detect = (pz <= self.z) + if can_detect and dist_to_player < 100: + escape_angle = math.degrees(math.atan2(self.y - py, self.x - px)) + self.yaw += (escape_angle - self.yaw) * 0.05 + + self.yaw += math.sin(self.timer * 3) * 1.5 + self.z += math.sin(self.timer * 1.5) * 0.2 + + elif self.type == "fly": + if dist_to_player < 120: + escape_angle = math.degrees(math.atan2(self.y - py, self.x - px)) + self.yaw = escape_angle + random.uniform(-45, 45) + self.z += random.choice([-1.5, 1.5]) + else: + if random.random() < 0.08: + self.yaw += random.uniform(-60, 60) + + self.yaw += math.sin(self.timer * 6) * 4.0 + self.z += math.cos(self.timer * 4) * 0.6 + + self.z = max(3.0, min(50.0, self.z)) + + rad = math.radians(self.yaw) + next_x = self.x - math.sin(rad) * self.speed + next_y = self.y + math.cos(rad) * self.speed + + if is_over_water(next_x, next_y): + self.x, self.y = next_x, next_y + else: + self.yaw += 140.0 + random.uniform(-20, 20) + + if self.y > STREAM_MAX_Y: + self.y -= WORLD_LEN + elif self.y < STREAM_MIN_Y: + self.y += WORLD_LEN + + def draw(self, attached=False): + if self.type == "mosquito": + glColor3f(*COLOR_MOSQUITO) + else: + glColor3f(*COLOR_FLY) + + glPushMatrix() + if not attached: + glTranslatef(self.x, self.y, self.z) + glRotatef(self.yaw, 0, 0, 1) + + s = self.size + glBegin(GL_LINES) + glVertex3f(0, s, 0) + glVertex3f(0, -s, 0) + w_offset = 0.0 if attached else math.sin(self.timer * 30) * (s * 0.8) + glVertex3f(0, 0, 0) + glVertex3f(-s * 1.5, 0, w_offset) + glVertex3f(0, 0, 0) + glVertex3f(s * 1.5, 0, w_offset) + glEnd() + + glPopMatrix() + +class Frog: + def __init__(self): + self.pick_new_destination() + self.x, self.y = self.target_x, self.target_y + self.z = 0.6 + self.speed = 0.35 + self.yaw = 0.0 + self.swim_timer = 0.0 + + def pick_new_destination(self): + destinations = [(rx, ry) for rx, ry, _, _ in REEDS] + [(ix, iy) for ix, iy, _ in ISLANDS] + dest = random.choice(destinations) + self.target_x, self.target_y = dest[0] + random.uniform(-30, 30), dest[1] + random.uniform(-30, 30) + + def update(self): + self.swim_timer += 0.15 + dx = self.target_x - self.x + dy = self.target_y - self.y + dist = math.hypot(dx, dy) + + if dist < 6.0: + self.pick_new_destination() + else: + self.yaw = math.degrees(math.atan2(dy, dx)) - 90.0 + self.x += (dx / dist) * self.speed + self.y += (dy / dist) * self.speed + + def draw(self): + glColor3f(*COLOR_FROG) + glPushMatrix() + glTranslatef(self.x, self.y, self.z) + glRotatef(self.yaw, 0, 0, 1) + + leg_offset = math.sin(self.swim_timer) * 1.5 + + glBegin(GL_LINES) + glVertex3f(0.0, 0.0, 0.5) + glVertex3f(0.0, -5.0, 0.5) + glVertex3f(-2.0, 2.0, 0.5) + glVertex3f(2.0, 2.0, 0.5) + glVertex3f(-2.5, -2.0, 0.5) + glVertex3f(2.5, -2.0, 0.5) + + glVertex3f(-2.0, 2.0, 0.5) + glVertex3f(0.0, 0.0, 0.5) + glVertex3f(2.0, 2.0, 0.5) + glVertex3f(0.0, 0.0, 0.5) + + glVertex3f(-2.0, 2.0, 0.6) + glVertex3f(2.0, 2.0, 0.6) + glVertex3f(-2.0, 2.0, 0.6) + glVertex3f(0.0, 4.5, 0.6) + glVertex3f(2.0, 2.0, 0.6) + glVertex3f(0.0, 4.5, 0.6) + + glVertex3f(-1.5, 3.5, 1.5) + glVertex3f(-1.5, 3.5, 0.6) + glVertex3f(1.5, 3.5, 1.5) + glVertex3f(1.5, 3.5, 0.6) + + glVertex3f(-2.0, 2.0, 0.5) + glVertex3f(-4.0, 3.0, 0.0) + glVertex3f(-4.0, 3.0, 0.0) + glVertex3f(-3.5, 4.0, 0.0) + + glVertex3f(2.0, 2.0, 0.5) + glVertex3f(4.0, 3.0, 0.0) + glVertex3f(4.0, 3.0, 0.0) + glVertex3f(3.5, 4.0, 0.0) + + glVertex3f(-2.5, -2.0, 0.5) + glVertex3f(-5.0 - leg_offset, -4.0, 0.0) + glVertex3f(-5.0 - leg_offset, -4.0, 0.0) + glVertex3f(-3.0 - leg_offset, -6.0, 0.0) + + glVertex3f(2.5, -2.0, 0.5) + glVertex3f(5.0 + leg_offset, -4.0, 0.0) + glVertex3f(5.0 + leg_offset, -4.0, 0.0) + glVertex3f(3.0 + leg_offset, -6.0, 0.0) + glEnd() + + glPopMatrix() + +# ========================================== +# RENDERIZADO DE ESCENARIO Y MODELOS +# ========================================== +def draw_water_surface(time_counter): + glColor3f(*COLOR_WATER) + + grid_size = 15 + for x in range(int(-POND_RADIUS), int(POND_RADIUS), grid_size * 2): + glBegin(GL_LINE_STRIP) + for y in range(int(-POND_RADIUS), int(POND_RADIUS), grid_size): + dist = math.hypot(x, y) + if dist < POND_RADIUS - 30: + z = 1.0 * math.sin(dist * 0.015 - time_counter * 3.0) + 0.5 * math.cos(x * 0.01 + time_counter) + glVertex3f(x, y, z) + glEnd() + + for y in range(int(STREAM_MIN_Y), int(STREAM_MAX_Y), 15): + if abs(y) < POND_RADIUS: + continue + cx = get_river_center(y) + glBegin(GL_LINE_STRIP) + for dx in range(-100, 110, 10): + x = cx + dx + z = 0.5 * math.sin(y * 0.03 + dx * 0.08 + time_counter * 4.0) + glVertex3f(x, y, z) + glEnd() + +def draw_terrain(): + glColor3f(*COLOR_LAND) + + grid_step = 120 + for y in range(int(STREAM_MIN_Y - 300), int(STREAM_MAX_Y + 300), grid_step): + glBegin(GL_LINE_STRIP) + for x in range(-2500, 2500, grid_step): + z = 1.5 * math.sin(x * 0.005) + 1.5 * math.cos(y * 0.005) + glVertex3f(x, y, z) + glEnd() + + width_river = 100.0 + margin_land = 70.0 + glColor3f(*COLOR_LAND_RIVER) + + for y in range(int(STREAM_MIN_Y), int(STREAM_MAX_Y), 30): + if abs(y) < POND_RADIUS: + continue + cx = get_river_center(y) + + glBegin(GL_LINE_STRIP) + for dx in range(int(-width_river - margin_land), int(-width_river), 15): + x = cx + dx + z = 0.6 * math.sin(y * 0.02 + x * 0.02) + 0.3 + glVertex3f(x, y, z) + glEnd() + + glBegin(GL_LINE_STRIP) + for dx in range(int(width_river), int(width_river + margin_land), 15): + x = cx + dx + z = 0.6 * math.sin(y * 0.02 + x * 0.02) + 0.3 + glVertex3f(x, y, z) + glEnd() + + for ix, iy, ir in ISLANDS: + glColor3f(*COLOR_ISLAND) + for layer in range(1, 4): + rad_layer = ir * (1 - layer * 0.25) + height = layer * 1.2 + glBegin(GL_LINE_LOOP) + for i in range(16): + angle = 2.0 * math.pi * i / 16 + dx = rad_layer * math.cos(angle) + dy = rad_layer * math.sin(angle) + glVertex3f(ix + dx, iy + dy, height) + glEnd() + +def draw_map(time_counter): + draw_terrain() + draw_water_surface(time_counter) + + offsets = [(-1.5, 0.6), (1.3, -0.6), (-0.7, -1.4), (1.1, 1.2), (0.0, 1.6), (-1.2, 0.0)] + + for rx, ry, rz, rh in REEDS: + glColor3f(*COLOR_REED) + glBegin(GL_LINES) + glVertex3f(rx, ry, rz) + glVertex3f(rx, ry, rz + rh) + for ox, oy in offsets: + glVertex3f(rx, ry, rz) + glVertex3f(rx + ox, ry + oy, rz + rh * 0.85) + glEnd() + + glColor3f(*COLOR_CATTAIL) + glBegin(GL_LINES) + for d in [-0.2, 0.0, 0.2]: + glVertex3f(rx + d, ry, rz + rh - 4.5) + glVertex3f(rx + d, ry, rz + rh) + for ox, oy in offsets: + for d in [-0.15, 0.15]: + glVertex3f(rx + ox + d, ry + oy, (rz + rh * 0.85) - 3.8) + glVertex3f(rx + ox + d, ry + oy, rz + rh * 0.85) + glEnd() + +def draw_dragonfly(wing_angle, caught_insect=None): + glColor3f(*COLOR_DRAGONFLY) + + glBegin(GL_LINES) + glVertex3f(0, 1.5, 0) + glVertex3f(0, -3.0, 0) + glVertex3f(-0.4, 0.6, 0) + glVertex3f(0.4, 0.6, 0) + glVertex3f(-0.3, -0.6, 0) + glVertex3f(0.3, -0.6, 0) + + glVertex3f(-0.3, 0.5, 0) + glVertex3f(-0.2, 0.2, -0.6) + glVertex3f(0.3, 0.5, 0) + glVertex3f(0.2, 0.2, -0.6) + glVertex3f(-0.2, -0.2, 0) + glVertex3f(-0.1, -0.3, -0.6) + glVertex3f(0.2, -0.2, 0) + glVertex3f(0.1, -0.3, -0.6) + glEnd() + + glBegin(GL_LINE_LOOP) + for i in range(8): + angle = 2.0 * math.pi * i / 8 + glVertex3f(0.3 * math.cos(angle), 1.4 + 0.3 * math.sin(angle), 0) + glEnd() + + rad_wing = math.radians(wing_angle) + w_z = math.sin(rad_wing) * 1.2 if wing_angle != 0 else 0.0 + + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(-4.5, 1.0, w_z) + glVertex3f(-3.8, 0.2, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(4.5, 1.0, w_z) + glVertex3f(3.8, 0.2, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(-3.8, -0.2, w_z) + glVertex3f(-3.0, -0.8, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(3.8, -0.2, w_z) + glVertex3f(3.0, -0.8, w_z) + glEnd() + + if caught_insect: + glPushMatrix() + glTranslatef(0.0, 0.2, -0.6) + caught_insect.draw(attached=True) + glPopMatrix() + +# ========================================== +# INTERFAZ 2D / HUD +# ========================================== +def draw_text_2d(text_surface, x, y): + text_data = pygame.image.tostring(text_surface, "RGBA", True) + w, h = text_surface.get_size() + + glEnable(GL_BLEND) + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) + + glRasterPos2i(x, y) + glDrawPixels(w, h, GL_RGBA, GL_UNSIGNED_BYTE, text_data) + glDisable(GL_BLEND) + +def draw_hud(player_pos, yaw, insects, eaten_mosquitos, eaten_flies, font): + glDisable(GL_DEPTH_TEST) + + glMatrixMode(GL_PROJECTION) + glPushMatrix() + glLoadIdentity() + gluOrtho2D(0, WIDTH, 0, HEIGHT) + + glMatrixMode(GL_MODELVIEW) + glPushMatrix() + glLoadIdentity() + + px, py, pz = player_pos + + # RADAR REDUCIDO + cx, cy = WIDTH // 2, HEIGHT - 35 + radius = 22 + + glColor3f(*COLOR_HUD) + glBegin(GL_LINE_LOOP) + for i in range(24): + angle = 2.0 * math.pi * i / 24 + glVertex2f(cx + radius * math.cos(angle), cy + radius * math.sin(angle)) + glEnd() + + glBegin(GL_LINES) + glVertex2f(cx, cy + radius) + glVertex2f(cx, cy + radius - 5) + glVertex2f(cx, cy - radius) + glVertex2f(cx, cy - radius + 5) + glVertex2f(cx - radius, cy) + glVertex2f(cx - radius + 5, cy) + glVertex2f(cx + radius, cy) + glVertex2f(cx + radius - 5, cy) + glEnd() + + closest_insect = None + min_dist = 999999.0 + relative_angle = 0.0 + diff_z = 0.0 + + for insect in insects: + dx = insect.x - px + dy = insect.y - py + dist = math.hypot(dx, dy) + + if dist < min_dist and dist > 0.1: + angle_to_insect = math.degrees(math.atan2(dy, dx)) + heading = (angle_to_insect - (90 - yaw)) % 360 + if heading > 180: + heading -= 360 + + if abs(heading) < 60: + min_dist = dist + closest_insect = insect + relative_angle = heading + diff_z = insect.z - pz + + if closest_insect: + radar_x = cx - (relative_angle / 60.0) * (radius - 4) + radar_y = cy + (diff_z / 20.0) * (radius - 4) + + dist_from_center = math.hypot(radar_x - cx, radar_y - cy) + if dist_from_center > (radius - 3): + factor = (radius - 3) / dist_from_center + radar_x = cx + (radar_x - cx) * factor + radar_y = cy + (radar_y - cy) * factor + + glColor3f(1.0, 1.0, 1.0) + glBegin(GL_LINE_LOOP) + for i in range(8): + ang = 2.0 * math.pi * i / 8 + glVertex2f(radar_x + 3 * math.cos(ang), radar_y + 3 * math.sin(ang)) + glEnd() + + # DIBUJOS ESTÁTICOS 2D DE LOS INSECTOS EN EL HUD + glColor3f(*COLOR_MOSQUITO) + glBegin(GL_LINES) + glVertex2f(25, HEIGHT - 33) + glVertex2f(25, HEIGHT - 27) + glVertex2f(21, HEIGHT - 30) + glVertex2f(29, HEIGHT - 30) + glEnd() + + glColor3f(*COLOR_FLY) + glBegin(GL_LINES) + glVertex2f(WIDTH - 82, HEIGHT - 33) + glVertex2f(WIDTH - 82, HEIGHT - 27) + glVertex2f(WIDTH - 86, HEIGHT - 28) + glVertex2f(WIDTH - 78, HEIGHT - 32) + glVertex2f(WIDTH - 86, HEIGHT - 32) + glVertex2f(WIDTH - 78, HEIGHT - 28) + glEnd() + + # TEXTOS DE CONTADORES + txt_mosq = font.render(f"x {eaten_mosquitos}", True, (255, 230, 0)) + txt_flies = font.render(f"x {eaten_flies}", True, (200, 65, 40)) + + draw_text_2d(txt_mosq, 38, HEIGHT - 40) + draw_text_2d(txt_flies, WIDTH - 68, HEIGHT - 40) + + glMatrixMode(GL_PROJECTION) + glPopMatrix() + glMatrixMode(GL_MODELVIEW) + glPopMatrix() + glEnable(GL_DEPTH_TEST) + +# ========================================== +# BUCLE PRINCIPAL +# ========================================== +def main(): + pygame.init() + pygame.font.init() + font = pygame.font.SysFont("Arial", 22, bold=True) + + pygame.display.set_mode((WIDTH, HEIGHT), DOUBLEBUF | OPENGL) + pygame.display.set_caption("Simulador de Vuelo de Libélula 3D") + + glEnable(GL_DEPTH_TEST) + glClearColor(*COLOR_BG, 1.0) + + glMatrixMode(GL_PROJECTION) + gluPerspective(50, (WIDTH / HEIGHT), 0.1, 5000.0) + glMatrixMode(GL_MODELVIEW) + + # Estado de la Libélula + px, py, pz = 0.0, 0.0, 15.0 + yaw = 0.0 + roll = 0.0 + pitch = 0.0 + max_roll = 30.0 + max_pitch = 20.0 + + speed = 0.0 + max_speed = 3.0 + acceleration = 0.06 + deceleration = 0.03 + + min_altitude = 1.0 + max_altitude = 120.0 + + wing_timer = 0.0 + water_timer = 0.0 + is_perched = False + + dive_phase = "none" + pre_dive_alt = 15.0 + pre_dive_speed = 0.0 + + auto_landing = False + target_perch = None + + caught_insect = None + eaten_mosquitos = 0 + eaten_flies = 0 + + frog = Frog() + + insects = [] + num_mosquitos = random.randint(MOSQUITO_MIN, MOSQUITO_MAX) + num_flies = random.randint(FLY_MIN, FLY_MAX) + + for _ in range(num_mosquitos): + insects.append(Insect("mosquito")) + for _ in range(num_flies): + insects.append(Insect("fly")) + + clock = pygame.time.Clock() + + running = True + while running: + dt = clock.tick(60) / 1000.0 + water_timer += dt + + for event in pygame.event.get(): + if event.type == QUIT or (event.type == KEYDOWN and event.key == K_ESCAPE): + running = False + elif event.type == KEYDOWN and event.key == K_SPACE: + if not is_perched and dive_phase == "none": + dive_phase = "diving" + pre_dive_alt = pz + pre_dive_speed = speed + auto_landing = False + elif event.type == KEYDOWN and event.key == K_p: + if not is_perched and dive_phase == "none": + best_reed = None + min_d = 999.0 + for rx, ry, rz, rh in REEDS: + dist = math.hypot(px - rx, py - ry) + if dist < 35.0: + angle_to_reed = math.degrees(math.atan2(ry - py, rx - px)) + heading = (angle_to_reed - (90 - yaw)) % 360 + if heading > 180: + heading -= 360 + if abs(heading) < 45 and dist < min_d: + min_d = dist + best_reed = (rx, ry, rz + rh) + + if best_reed: + target_perch = best_reed + auto_landing = True + + keys = pygame.key.get_pressed() + + if is_perched: + if keys[K_LEFT]: + yaw += 3.0 + if keys[K_RIGHT]: + yaw -= 3.0 + + if keys[K_w]: + is_perched = False + speed = 0.5 + + wing_angle = 0.0 + pitch = 0.0 + roll = 0.0 + dive_phase = "none" + auto_landing = False + else: + if auto_landing and target_perch: + tx, ty, tz = target_perch + dx = tx - px + dy = ty - py + dist_to_target = math.hypot(dx, dy) + + speed = 1.2 + px += (dx / dist_to_target) * speed if dist_to_target > 0.1 else 0 + py += (dy / dist_to_target) * speed if dist_to_target > 0.1 else 0 + pz += (tz - pz) * 0.1 + + if dist_to_target < 2.5 and abs(pz - tz) < 2.5: + px, py, pz = target_perch + speed = 0.0 + roll = 0.0 + pitch = 0.0 + is_perched = True + auto_landing = False + target_perch = None + + if caught_insect: + if caught_insect.type == "mosquito": + eaten_mosquitos += 1 + else: + eaten_flies += 1 + caught_insect = None + + elif dive_phase == "diving": + speed = max_speed * 1.5 + pz -= 1.2 + target_pitch = -45.0 + + if pz <= min_altitude: + pz = min_altitude + dive_phase = "recovering" + + elif dive_phase == "recovering": + target_pitch = 30.0 + pz += 0.8 + speed += (pre_dive_speed - speed) * 0.1 + + if pz >= pre_dive_alt: + pz = pre_dive_alt + speed = pre_dive_speed + dive_phase = "none" + else: + if keys[K_w]: + speed = min(speed + acceleration, max_speed) + elif keys[K_s]: + speed = max(speed - deceleration, 0.0) + + if speed < 0.3: + for rx, ry, rz, rh in REEDS: + dist = math.hypot(px - rx, py - ry) + if dist < 10.0 and abs(pz - (rz + rh)) < 10.0: + px, py, pz = rx, ry, rz + rh + speed = 0.0 + roll = 0.0 + pitch = 0.0 + is_perched = True + + if caught_insect: + if caught_insect.type == "mosquito": + eaten_mosquitos += 1 + else: + eaten_flies += 1 + caught_insect = None + break + + target_pitch = 0.0 + if keys[K_UP]: + pz = min(pz + 0.8, max_altitude) + target_pitch = max_pitch + elif keys[K_DOWN]: + pz = max(pz - 0.8, min_altitude) + target_pitch = -max_pitch + + if not auto_landing: + pitch += (target_pitch - pitch) * 0.15 + + target_roll = 0.0 + if keys[K_LEFT]: + yaw += 2.8 + target_roll = -max_roll + elif keys[K_RIGHT]: + yaw -= 2.8 + target_roll = max_roll + + roll += (target_roll - roll) * 0.15 + + rad_yaw = math.radians(yaw) + px += -math.sin(rad_yaw) * speed + py += math.cos(rad_yaw) * speed + + if abs(py) < POND_RADIUS: + max_limit_x = POND_RADIUS + 200.0 + else: + cx = get_river_center(py) + max_limit_x = abs(cx) + 200.0 + + px = max(-max_limit_x, min(max_limit_x, px)) + + if py > STREAM_MAX_Y: + py -= WORLD_LEN + elif py < STREAM_MIN_Y: + py += WORLD_LEN + + wing_timer += 80.0 if (dive_phase != "none" or auto_landing) else (60.0 if speed > 0.1 else 30.0) + wing_angle = math.sin(wing_timer) * 35.0 + + frog.update() + + current_mosquitos = 0 + current_flies = 0 + + for insect in insects[:]: + insect.update((px, py, pz)) + + if insect.type == "mosquito": + current_mosquitos += 1 + else: + current_flies += 1 + + if not caught_insect: + dist_to_dragonfly = math.sqrt((px - insect.x)**2 + (py - insect.y)**2 + (pz - insect.z)**2) + if dist_to_dragonfly < 6.0: + caught_insect = insect + insects.remove(insect) + + if dive_phase == "diving": + dive_phase = "recovering" + + if current_mosquitos < MOSQUITO_MIN: + insects.append(Insect("mosquito")) + if current_flies < FLY_MIN: + insects.append(Insect("fly")) + + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) + glLoadIdentity() + + cam_dist = 18.0 + cam_height = 8.0 + rad_yaw = math.radians(yaw) + + cam_x = px + math.sin(rad_yaw) * cam_dist + cam_y = py - math.cos(rad_yaw) * cam_dist + cam_z = pz + cam_height + + gluLookAt(cam_x, cam_y, cam_z, + px, py, pz + 0.5, + 0, 0, 1) + + draw_map(water_timer) + + frog.draw() + + for insect in insects: + insect.draw() + + glPushMatrix() + glTranslatef(px, py, pz) + glRotatef(yaw, 0, 0, 1) + glRotatef(pitch, 1, 0, 0) + glRotatef(roll, 0, 1, 0) + draw_dragonfly(wing_angle, caught_insect) + glPopMatrix() + + draw_hud((px, py, pz), yaw, insects, eaten_mosquitos, eaten_flies, font) + + pygame.display.flip() + + pygame.quit() + sys.exit() + +if __name__ == "__main__": + main() diff --git a/previos/libelula_v8.2.py b/previos/libelula_v8.2.py new file mode 100644 index 0000000..f4c9cfa --- /dev/null +++ b/previos/libelula_v8.2.py @@ -0,0 +1,968 @@ +import sys +import math +import random +import pygame +from pygame.locals import * +from OpenGL.GL import * +from OpenGL.GLU import * + +# ========================================== +# CONFIGURACIÓN Y CONSTANTES +# ========================================== +WIDTH, HEIGHT = 800, 600 + +# Colores (R, G, B) +COLOR_BG = (0.12, 0.15, 0.12) # Fondo ambiente +COLOR_WATER = (0.0, 0.5, 0.85) # Agua animada (Cian/Azul) +COLOR_LAND = (0.45, 0.32, 0.15) # Tierra / Relieve +COLOR_LAND_RIVER = (0.40, 0.28, 0.12) # Tierra en riachuelos +COLOR_ISLAND = (0.5, 0.38, 0.18) # Islas +COLOR_REED = (0.15, 0.85, 0.25) # Juncos verdes +COLOR_CATTAIL = (0.35, 0.18, 0.08) # Espigas marrones densas +COLOR_DRAGONFLY = (0.0, 1.0, 1.0) # Libélula alámbrica (Cian) +COLOR_MOSQUITO = (1.0, 0.9, 0.0) # Mosquitos (Amarillo) +COLOR_FLY = (0.8, 0.25, 0.15) # Moscas (Rojo-Marrón) +COLOR_FROG = (0.15, 0.85, 0.35) # Rana verde detallada +COLOR_HUD = (1.0, 1.0, 1.0) # Retícula HUD (Blanco) + +# Dimensiones del Mundo +POND_RADIUS = 1800.0 +STREAM_LEN = 900.0 +STREAM_MIN_Y = -(POND_RADIUS + STREAM_LEN) +STREAM_MAX_Y = (POND_RADIUS + STREAM_LEN) +WORLD_LEN = STREAM_MAX_Y - STREAM_MIN_Y + +# Cantidades de insectos +MOSQUITO_MIN, MOSQUITO_MAX = 17, 27 +FLY_MIN, FLY_MAX = 9, 14 + +# ========================================== +# ELEMENTOS DEL MAPA +# ========================================== +REEDS = [ + (-250, 350, 0, 20), (-160, -500, 0, 22), (500, 200, 0, 24), (-700, 200, 0, 21), (200, -800, 0, 23), + (-900, -300, 0, 25), (760, -600, 0, 22), (900, 500, 0, 26), (-500, 900, 0, 23), (600, -1000, 0, 22), + (-1200, 400, 0, 24), (1300, -300, 0, 20), (-1400, -700, 0, 25), (1100, 1200, 0, 23), (-360, -1300, 0, 22), + (-90, -1950, 0, 20), (-110, -2150, 0, 23), (-80, -2350, 0, 19), (-100, -2550, 0, 22), + (90, 1950, 0, 22), (110, 2150, 0, 20), (85, 2350, 0, 24), (105, 2550, 0, 21) +] + +ISLANDS = [ + (-250, 350, 90), (-160, -500, 85), (500, 200, 100), (-700, 200, 110), (200, -800, 95), + (-900, -300, 120), (760, -600, 90), (900, 500, 105), (-500, 900, 115), (600, -1000, 100) +] + +def get_river_center(y): + if y < -POND_RADIUS: + dy = y + POND_RADIUS + return -50 + 100 * math.sin(dy * 0.008) + elif y > POND_RADIUS: + dy = y - POND_RADIUS + return 50 + 100 * math.sin(dy * 0.008) + else: + return (y / POND_RADIUS) * 50.0 + +def is_over_water(x, y): + dist_center = math.hypot(x, y) + if dist_center < POND_RADIUS - 50: + return True + if abs(y) >= POND_RADIUS: + cx = get_river_center(y) + if abs(x - cx) < 80: + return True + return False + +def get_random_water_position(): + while True: + if random.random() < 0.7: + rad = random.uniform(0, math.pi * 2) + r = random.uniform(100, POND_RADIUS - 150) + x = r * math.cos(rad) + y = r * math.sin(rad) + else: + y = random.choice([random.uniform(POND_RADIUS, STREAM_MAX_Y), random.uniform(STREAM_MIN_Y, -POND_RADIUS)]) + cx = get_river_center(y) + x = cx + random.uniform(-60, 60) + + if is_over_water(x, y): + z = random.uniform(5.0, 35.0) + return x, y, z + +# ========================================== +# CLASES DE INSECTOS Y RANA DECORATIVA +# ========================================== +class Insect: + def __init__(self, insect_type): + self.type = insect_type + self.x, self.y, self.z = get_random_water_position() + self.yaw = random.uniform(0, 360) + self.timer = random.uniform(0, 100) + + if self.type == "mosquito": + self.speed = 1.8 + self.size = 1.2 + else: + self.speed = 0.9 + self.size = 1.6 + + def update(self, player_pos): + self.timer += 0.05 + px, py, pz = player_pos + dist_to_player = math.hypot(self.x - px, self.y - py) + + if self.type == "mosquito": + can_detect = (pz <= self.z) + if can_detect and dist_to_player < 100: + escape_angle = math.degrees(math.atan2(self.y - py, self.x - px)) + self.yaw += (escape_angle - self.yaw) * 0.05 + + self.yaw += math.sin(self.timer * 3) * 1.5 + self.z += math.sin(self.timer * 1.5) * 0.2 + + elif self.type == "fly": + if dist_to_player < 120: + escape_angle = math.degrees(math.atan2(self.y - py, self.x - px)) + self.yaw = escape_angle + random.uniform(-45, 45) + self.z += random.choice([-1.5, 1.5]) + else: + if random.random() < 0.08: + self.yaw += random.uniform(-60, 60) + + self.yaw += math.sin(self.timer * 6) * 4.0 + self.z += math.cos(self.timer * 4) * 0.6 + + self.z = max(3.0, min(50.0, self.z)) + + rad = math.radians(self.yaw) + next_x = self.x - math.sin(rad) * self.speed + next_y = self.y + math.cos(rad) * self.speed + + if is_over_water(next_x, next_y): + self.x, self.y = next_x, next_y + else: + self.yaw += 140.0 + random.uniform(-20, 20) + + if self.y > STREAM_MAX_Y: + self.y -= WORLD_LEN + elif self.y < STREAM_MIN_Y: + self.y += WORLD_LEN + + def draw(self, attached=False): + if self.type == "mosquito": + glColor3f(*COLOR_MOSQUITO) + else: + glColor3f(*COLOR_FLY) + + glPushMatrix() + if not attached: + glTranslatef(self.x, self.y, self.z) + glRotatef(self.yaw, 0, 0, 1) + + s = self.size + glBegin(GL_LINES) + glVertex3f(0, s, 0) + glVertex3f(0, -s, 0) + w_offset = 0.0 if attached else math.sin(self.timer * 30) * (s * 0.8) + glVertex3f(0, 0, 0) + glVertex3f(-s * 1.5, 0, w_offset) + glVertex3f(0, 0, 0) + glVertex3f(s * 1.5, 0, w_offset) + glEnd() + + glPopMatrix() + +class Frog: + def __init__(self): + self.pick_new_destination() + self.x, self.y = self.target_x, self.target_y + self.z = 0.6 + self.speed = 0.35 + self.yaw = 0.0 + self.swim_timer = 0.0 + + def pick_new_destination(self): + destinations = [(rx, ry) for rx, ry, _, _ in REEDS] + [(ix, iy) for ix, iy, _ in ISLANDS] + dest = random.choice(destinations) + self.target_x, self.target_y = dest[0] + random.uniform(-30, 30), dest[1] + random.uniform(-30, 30) + + def update(self): + self.swim_timer += 0.15 + dx = self.target_x - self.x + dy = self.target_y - self.y + dist = math.hypot(dx, dy) + + if dist < 6.0: + self.pick_new_destination() + else: + self.yaw = math.degrees(math.atan2(dy, dx)) - 90.0 + self.x += (dx / dist) * self.speed + self.y += (dy / dist) * self.speed + + def draw(self): + glColor3f(*COLOR_FROG) + glPushMatrix() + glTranslatef(self.x, self.y, self.z) + glRotatef(self.yaw, 0, 0, 1) + + leg_offset = math.sin(self.swim_timer) * 1.5 + + glBegin(GL_LINES) + glVertex3f(0.0, 0.0, 0.5) + glVertex3f(0.0, -5.0, 0.5) + glVertex3f(-2.0, 2.0, 0.5) + glVertex3f(2.0, 2.0, 0.5) + glVertex3f(-2.5, -2.0, 0.5) + glVertex3f(2.5, -2.0, 0.5) + + glVertex3f(-2.0, 2.0, 0.5) + glVertex3f(0.0, 0.0, 0.5) + glVertex3f(2.0, 2.0, 0.5) + glVertex3f(0.0, 0.0, 0.5) + + glVertex3f(-2.0, 2.0, 0.6) + glVertex3f(2.0, 2.0, 0.6) + glVertex3f(-2.0, 2.0, 0.6) + glVertex3f(0.0, 4.5, 0.6) + glVertex3f(2.0, 2.0, 0.6) + glVertex3f(0.0, 4.5, 0.6) + + glVertex3f(-1.5, 3.5, 1.5) + glVertex3f(-1.5, 3.5, 0.6) + glVertex3f(1.5, 3.5, 1.5) + glVertex3f(1.5, 3.5, 0.6) + + glVertex3f(-2.0, 2.0, 0.5) + glVertex3f(-4.0, 3.0, 0.0) + glVertex3f(-4.0, 3.0, 0.0) + glVertex3f(-3.5, 4.0, 0.0) + + glVertex3f(2.0, 2.0, 0.5) + glVertex3f(4.0, 3.0, 0.0) + glVertex3f(4.0, 3.0, 0.0) + glVertex3f(3.5, 4.0, 0.0) + + glVertex3f(-2.5, -2.0, 0.5) + glVertex3f(-5.0 - leg_offset, -4.0, 0.0) + glVertex3f(-5.0 - leg_offset, -4.0, 0.0) + glVertex3f(-3.0 - leg_offset, -6.0, 0.0) + + glVertex3f(2.5, -2.0, 0.5) + glVertex3f(5.0 + leg_offset, -4.0, 0.0) + glVertex3f(5.0 + leg_offset, -4.0, 0.0) + glVertex3f(3.0 + leg_offset, -6.0, 0.0) + glEnd() + + glPopMatrix() + +# ========================================== +# RENDERIZADO DE ESCENARIO Y MODELOS +# ========================================== +def draw_water_surface(time_counter): + glColor3f(*COLOR_WATER) + + grid_size = 15 + for x in range(int(-POND_RADIUS), int(POND_RADIUS), grid_size * 2): + glBegin(GL_LINE_STRIP) + for y in range(int(-POND_RADIUS), int(POND_RADIUS), grid_size): + dist = math.hypot(x, y) + if dist < POND_RADIUS - 30: + z = 1.0 * math.sin(dist * 0.015 - time_counter * 3.0) + 0.5 * math.cos(x * 0.01 + time_counter) + glVertex3f(x, y, z) + glEnd() + + for y in range(int(STREAM_MIN_Y), int(STREAM_MAX_Y), 15): + if abs(y) < POND_RADIUS: + continue + cx = get_river_center(y) + glBegin(GL_LINE_STRIP) + for dx in range(-100, 110, 10): + x = cx + dx + z = 0.5 * math.sin(y * 0.03 + dx * 0.08 + time_counter * 4.0) + glVertex3f(x, y, z) + glEnd() + +def draw_terrain(): + glColor3f(*COLOR_LAND) + + grid_step = 120 + for y in range(int(STREAM_MIN_Y - 300), int(STREAM_MAX_Y + 300), grid_step): + glBegin(GL_LINE_STRIP) + for x in range(-2500, 2500, grid_step): + z = 1.5 * math.sin(x * 0.005) + 1.5 * math.cos(y * 0.005) + glVertex3f(x, y, z) + glEnd() + + width_river = 100.0 + margin_land = 70.0 + glColor3f(*COLOR_LAND_RIVER) + + for y in range(int(STREAM_MIN_Y), int(STREAM_MAX_Y), 30): + if abs(y) < POND_RADIUS: + continue + cx = get_river_center(y) + + glBegin(GL_LINE_STRIP) + for dx in range(int(-width_river - margin_land), int(-width_river), 15): + x = cx + dx + z = 0.6 * math.sin(y * 0.02 + x * 0.02) + 0.3 + glVertex3f(x, y, z) + glEnd() + + glBegin(GL_LINE_STRIP) + for dx in range(int(width_river), int(width_river + margin_land), 15): + x = cx + dx + z = 0.6 * math.sin(y * 0.02 + x * 0.02) + 0.3 + glVertex3f(x, y, z) + glEnd() + + for ix, iy, ir in ISLANDS: + glColor3f(*COLOR_ISLAND) + for layer in range(1, 4): + rad_layer = ir * (1 - layer * 0.25) + height = layer * 1.2 + glBegin(GL_LINE_LOOP) + for i in range(16): + angle = 2.0 * math.pi * i / 16 + dx = rad_layer * math.cos(angle) + dy = rad_layer * math.sin(angle) + glVertex3f(ix + dx, iy + dy, height) + glEnd() + +def draw_map(time_counter): + draw_terrain() + draw_water_surface(time_counter) + + offsets = [(-1.5, 0.6), (1.3, -0.6), (-0.7, -1.4), (1.1, 1.2), (0.0, 1.6), (-1.2, 0.0)] + + for rx, ry, rz, rh in REEDS: + glColor3f(*COLOR_REED) + glBegin(GL_LINES) + glVertex3f(rx, ry, rz) + glVertex3f(rx, ry, rz + rh) + for ox, oy in offsets: + glVertex3f(rx, ry, rz) + glVertex3f(rx + ox, ry + oy, rz + rh * 0.85) + glEnd() + + glColor3f(*COLOR_CATTAIL) + glBegin(GL_LINES) + for d in [-0.2, 0.0, 0.2]: + glVertex3f(rx + d, ry, rz + rh - 4.5) + glVertex3f(rx + d, ry, rz + rh) + for ox, oy in offsets: + for d in [-0.15, 0.15]: + glVertex3f(rx + ox + d, ry + oy, (rz + rh * 0.85) - 3.8) + glVertex3f(rx + ox + d, ry + oy, rz + rh * 0.85) + glEnd() + +def draw_dragonfly(wing_angle, caught_insect=None): + glColor3f(*COLOR_DRAGONFLY) + + glBegin(GL_LINES) + glVertex3f(0, 1.5, 0) + glVertex3f(0, -3.0, 0) + glVertex3f(-0.4, 0.6, 0) + glVertex3f(0.4, 0.6, 0) + glVertex3f(-0.3, -0.6, 0) + glVertex3f(0.3, -0.6, 0) + + glVertex3f(-0.3, 0.5, 0) + glVertex3f(-0.2, 0.2, -0.6) + glVertex3f(0.3, 0.5, 0) + glVertex3f(0.2, 0.2, -0.6) + glVertex3f(-0.2, -0.2, 0) + glVertex3f(-0.1, -0.3, -0.6) + glVertex3f(0.2, -0.2, 0) + glVertex3f(0.1, -0.3, -0.6) + glEnd() + + glBegin(GL_LINE_LOOP) + for i in range(8): + angle = 2.0 * math.pi * i / 8 + glVertex3f(0.3 * math.cos(angle), 1.4 + 0.3 * math.sin(angle), 0) + glEnd() + + rad_wing = math.radians(wing_angle) + w_z = math.sin(rad_wing) * 1.2 if wing_angle != 0 else 0.0 + + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(-4.5, 1.0, w_z) + glVertex3f(-3.8, 0.2, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(4.5, 1.0, w_z) + glVertex3f(3.8, 0.2, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(-3.8, -0.2, w_z) + glVertex3f(-3.0, -0.8, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(3.8, -0.2, w_z) + glVertex3f(3.0, -0.8, w_z) + glEnd() + + if caught_insect: + glPushMatrix() + glTranslatef(0.0, 0.2, -0.6) + caught_insect.draw(attached=True) + glPopMatrix() + +# ========================================== +# INTERFAZ 2D / HUD Y PANTALLA INICIAL +# ========================================== +def draw_text_2d(text_surface, x, y): + text_data = pygame.image.tostring(text_surface, "RGBA", True) + w, h = text_surface.get_size() + + glEnable(GL_BLEND) + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) + + glRasterPos2i(x, y) + glDrawPixels(w, h, GL_RGBA, GL_UNSIGNED_BYTE, text_data) + glDisable(GL_BLEND) + +def draw_start_screen(font_title, font_subtitle, font_text, wing_angle): + glDisable(GL_DEPTH_TEST) + + glMatrixMode(GL_PROJECTION) + glPushMatrix() + glLoadIdentity() + gluOrtho2D(0, WIDTH, 0, HEIGHT) + + glMatrixMode(GL_MODELVIEW) + glPushMatrix() + glLoadIdentity() + + # TÍTULO PRINCIPAL + txt_title = font_title.render("LIBÉLULA", True, (0, 220, 255)) + draw_text_2d(txt_title, WIDTH // 2 - 130, HEIGHT - 100) + + # DIBUJO ANIMADO DE LA LIBÉLULA EN LA PANTALLA INICIAL + glPushMatrix() + glTranslatef(WIDTH // 2 + 150, HEIGHT - 75, 0.0) + glScalef(12.0, 12.0, 1.0) + + # Dibujar libélula estática en 2D para el menú + glColor3f(*COLOR_DRAGONFLY) + glBegin(GL_LINES) + glVertex3f(0, 1.5, 0) + glVertex3f(0, -3.0, 0) + glVertex3f(-0.4, 0.6, 0) + glVertex3f(0.4, 0.6, 0) + glEnd() + + # Alas aleteando + rad_w = math.radians(wing_angle) + w_off = math.sin(rad_w) * 1.5 + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(-4.5, 1.0, w_off) + glVertex3f(-3.8, 0.2, w_off) + glEnd() + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(4.5, 1.0, w_off) + glVertex3f(3.8, 0.2, w_off) + glEnd() + glPopMatrix() + + # OBJETIVO DEL JUEGO + obj_line1 = font_text.render("Objetivo: Vuela libre por el estanque y sus arroyos capturando moscas", True, (255, 255, 255)) + obj_line2 = font_text.render("y mosquitos. Una vez cazados debes devorarlos posado en un junco.", True, (255, 255, 255)) + draw_text_2d(obj_line1, 70, HEIGHT - 170) + draw_text_2d(obj_line2, 70, HEIGHT - 195) + + # CONTROLES Y TECLAS + controls = [ + "CONTROLES:", + "W / S : Acelerar / Frenar", + "Flecha Arriba / Abajo : Cambiar altitud", + "Flecha Izquierda / Derecha : Girar / Virar", + "ESPACIO : Picado rápido para caza", + "P : Aterrizaje automático en junco cercano", + "ESC : Salir del juego" + ] + + y_pos = HEIGHT - 250 + for ctrl in controls: + is_title = ctrl.endswith(":") + col = (255, 220, 50) if is_title else (200, 200, 200) + t_surf = font_subtitle.render(ctrl, True, col) if is_title else font_text.render(ctrl, True, col) + draw_text_2d(t_surf, 70, y_pos) + y_pos -= 25 + + # LEYENDA VISUAL INFERIOR (Mosquito, Mosca y Junco) + y_icon = 90 + + # Icono Mosquito + glColor3f(*COLOR_MOSQUITO) + glBegin(GL_LINES) + glVertex2f(100, y_icon + 5) + glVertex2f(100, y_icon - 5) + glVertex2f(95, y_icon) + glVertex2f(105, y_icon) + glEnd() + txt_m = font_text.render("Mosquito", True, (255, 230, 0)) + draw_text_2d(txt_m, 120, y_icon - 8) + + # Icono Mosca + glColor3f(*COLOR_FLY) + glBegin(GL_LINES) + glVertex2f(280, y_icon + 5) + glVertex2f(280, y_icon - 5) + glVertex2f(275, y_icon - 3) + glVertex2f(285, y_icon + 3) + glVertex2f(275, y_icon + 3) + glVertex2f(285, y_icon - 3) + glEnd() + txt_f = font_text.render("Mosca", True, (200, 65, 40)) + draw_text_2d(txt_f, 300, y_icon - 8) + + # Icono Junco + glColor3f(*COLOR_REED) + glBegin(GL_LINES) + glVertex2f(450, y_icon - 10) + glVertex2f(450, y_icon + 15) + glEnd() + glColor3f(*COLOR_CATTAIL) + glBegin(GL_LINES) + glVertex2f(450, y_icon + 8) + glVertex2f(450, y_icon + 15) + glEnd() + txt_r = font_text.render("Junco", True, (100, 220, 100)) + draw_text_2d(txt_r, 470, y_icon - 8) + + # MENSAJE PARA COMENZAR + start_msg = font_subtitle.render("Pulsa INTRO o cualquier tecla para comenzar...", True, (100, 255, 100)) + draw_text_2d(start_msg, WIDTH // 2 - 200, 30) + + glMatrixMode(GL_PROJECTION) + glPopMatrix() + glMatrixMode(GL_MODELVIEW) + glPopMatrix() + glEnable(GL_DEPTH_TEST) + +def draw_hud(player_pos, yaw, insects, eaten_mosquitos, eaten_flies, font): + glDisable(GL_DEPTH_TEST) + + glMatrixMode(GL_PROJECTION) + glPushMatrix() + glLoadIdentity() + gluOrtho2D(0, WIDTH, 0, HEIGHT) + + glMatrixMode(GL_MODELVIEW) + glPushMatrix() + glLoadIdentity() + + px, py, pz = player_pos + + # RADAR REDUCIDO + cx, cy = WIDTH // 2, HEIGHT - 35 + radius = 22 + + glColor3f(*COLOR_HUD) + glBegin(GL_LINE_LOOP) + for i in range(24): + angle = 2.0 * math.pi * i / 24 + glVertex2f(cx + radius * math.cos(angle), cy + radius * math.sin(angle)) + glEnd() + + glBegin(GL_LINES) + glVertex2f(cx, cy + radius) + glVertex2f(cx, cy + radius - 5) + glVertex2f(cx, cy - radius) + glVertex2f(cx, cy - radius + 5) + glVertex2f(cx - radius, cy) + glVertex2f(cx - radius + 5, cy) + glVertex2f(cx + radius, cy) + glVertex2f(cx + radius - 5, cy) + glEnd() + + closest_insect = None + min_dist = 999999.0 + relative_angle = 0.0 + diff_z = 0.0 + + for insect in insects: + dx = insect.x - px + dy = insect.y - py + dist = math.hypot(dx, dy) + + if dist < min_dist and dist > 0.1: + angle_to_insect = math.degrees(math.atan2(dy, dx)) + heading = (angle_to_insect - (90 - yaw)) % 360 + if heading > 180: + heading -= 360 + + if abs(heading) < 60: + min_dist = dist + closest_insect = insect + relative_angle = heading + diff_z = insect.z - pz + + if closest_insect: + radar_x = cx - (relative_angle / 60.0) * (radius - 4) + radar_y = cy + (diff_z / 20.0) * (radius - 4) + + dist_from_center = math.hypot(radar_x - cx, radar_y - cy) + if dist_from_center > (radius - 3): + factor = (radius - 3) / dist_from_center + radar_x = cx + (radar_x - cx) * factor + radar_y = cy + (radar_y - cy) * factor + + glColor3f(1.0, 1.0, 1.0) + glBegin(GL_LINE_LOOP) + for i in range(8): + ang = 2.0 * math.pi * i / 8 + glVertex2f(radar_x + 3 * math.cos(ang), radar_y + 3 * math.sin(ang)) + glEnd() + + # DIBUJOS ESTÁTICOS 2D DE LOS INSECTOS EN EL HUD[cite: 4] + glColor3f(*COLOR_MOSQUITO) + glBegin(GL_LINES) + glVertex2f(25, HEIGHT - 33) + glVertex2f(25, HEIGHT - 27) + glVertex2f(21, HEIGHT - 30) + glVertex2f(29, HEIGHT - 30) + glEnd() + + glColor3f(*COLOR_FLY) + glBegin(GL_LINES) + glVertex2f(WIDTH - 82, HEIGHT - 33) + glVertex2f(WIDTH - 82, HEIGHT - 27) + glVertex2f(WIDTH - 86, HEIGHT - 28) + glVertex2f(WIDTH - 78, HEIGHT - 32) + glVertex2f(WIDTH - 86, HEIGHT - 32) + glVertex2f(WIDTH - 78, HEIGHT - 28) + glEnd() + + # TEXTOS DE CONTADORES[cite: 4] + txt_mosq = font.render(f"x {eaten_mosquitos}", True, (255, 230, 0)) + txt_flies = font.render(f"x {eaten_flies}", True, (200, 65, 40)) + + draw_text_2d(txt_mosq, 38, HEIGHT - 40) + draw_text_2d(txt_flies, WIDTH - 68, HEIGHT - 40) + + glMatrixMode(GL_PROJECTION) + glPopMatrix() + glMatrixMode(GL_MODELVIEW) + glPopMatrix() + glEnable(GL_DEPTH_TEST) + +# ========================================== +# BUCLE PRINCIPAL +# ========================================== +def main(): + pygame.init() + pygame.font.init() + font = pygame.font.SysFont("Arial", 22, bold=True) + font_title = pygame.font.SysFont("Arial", 48, bold=True) + font_subtitle = pygame.font.SysFont("Arial", 20, bold=True) + font_text = pygame.font.SysFont("Arial", 16, bold=False) + + pygame.display.set_mode((WIDTH, HEIGHT), DOUBLEBUF | OPENGL) + pygame.display.set_caption("Libélula") + + glEnable(GL_DEPTH_TEST) + glClearColor(*COLOR_BG, 1.0) + + glMatrixMode(GL_PROJECTION) + gluPerspective(50, (WIDTH / HEIGHT), 0.1, 5000.0) + glMatrixMode(GL_MODELVIEW) + + # Estado de la Libélula + px, py, pz = 0.0, 0.0, 15.0 + yaw = 0.0 + roll = 0.0 + pitch = 0.0 + max_roll = 30.0 + max_pitch = 20.0 + + speed = 0.0 + max_speed = 3.0 + acceleration = 0.06 + deceleration = 0.03 + + min_altitude = 1.0 + max_altitude = 120.0 + + wing_timer = 0.0 + water_timer = 0.0 + is_perched = False + + dive_phase = "none" + pre_dive_alt = 15.0 + pre_dive_speed = 0.0 + + auto_landing = False + target_perch = None + + caught_insect = None + eaten_mosquitos = 0 + eaten_flies = 0 + + frog = Frog() + + insects = [] + num_mosquitos = random.randint(MOSQUITO_MIN, MOSQUITO_MAX) + num_flies = random.randint(FLY_MIN, FLY_MAX) + + for _ in range(num_mosquitos): + insects.append(Insect("mosquito")) + for _ in range(num_flies): + insects.append(Insect("fly")) + + clock = pygame.time.Clock() + + in_start_screen = True + running = True + + while running: + dt = clock.tick(60) / 1000.0 + water_timer += dt + wing_timer += 60.0 + + for event in pygame.event.get(): + if event.type == QUIT or (event.type == KEYDOWN and event.key == K_ESCAPE): + running = False + elif event.type == KEYDOWN: + if in_start_screen: + in_start_screen = False # Salir de la pantalla de inicio con cualquier tecla + else: + if event.key == K_SPACE: + if not is_perched and dive_phase == "none": + dive_phase = "diving" + pre_dive_alt = pz + pre_dive_speed = speed + auto_landing = False + elif event.key == K_p: + if not is_perched and dive_phase == "none": + best_reed = None + min_d = 999.0 + for rx, ry, rz, rh in REEDS: + dist = math.hypot(px - rx, py - ry) + if dist < 35.0: + angle_to_reed = math.degrees(math.atan2(ry - py, rx - px)) + heading = (angle_to_reed - (90 - yaw)) % 360 + if heading > 180: + heading -= 360 + if abs(heading) < 45 and dist < min_d: + min_d = dist + best_reed = (rx, ry, rz + rh) + + if best_reed: + target_perch = best_reed + auto_landing = True + + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) + glLoadIdentity() + + if in_start_screen: + # Renderizar fondo del mundo de forma estática para ambientar el menú + cam_x, cam_y, cam_z = 0.0, -100.0, 30.0 + gluLookAt(cam_x, cam_y, cam_z, 0, 0, 15, 0, 0, 1) + draw_map(water_timer) + frog.draw() + + # Dibujar la pantalla de inicio 2D encima + wing_angle = math.sin(wing_timer * 0.5) * 35.0 + draw_start_screen(font_title, font_subtitle, font_text, wing_angle) + else: + keys = pygame.key.get_pressed() + + if is_perched: + if keys[K_LEFT]: + yaw += 3.0 + if keys[K_RIGHT]: + yaw -= 3.0 + + if keys[K_w]: + is_perched = False + speed = 0.5 + + wing_angle = 0.0 + pitch = 0.0 + roll = 0.0 + dive_phase = "none" + auto_landing = False + else: + if auto_landing and target_perch: + tx, ty, tz = target_perch + dx = tx - px + dy = ty - py + dist_to_target = math.hypot(dx, dy) + + speed = 1.2 + px += (dx / dist_to_target) * speed if dist_to_target > 0.1 else 0 + py += (dy / dist_to_target) * speed if dist_to_target > 0.1 else 0 + pz += (tz - pz) * 0.1 + + if dist_to_target < 2.5 and abs(pz - tz) < 2.5: + px, py, pz = target_perch + speed = 0.0 + roll = 0.0 + pitch = 0.0 + is_perched = True + auto_landing = False + target_perch = None + + if caught_insect: + if caught_insect.type == "mosquito": + eaten_mosquitos += 1 + else: + eaten_flies += 1 + caught_insect = None + + elif dive_phase == "diving": + speed = max_speed * 1.5 + pz -= 1.2 + target_pitch = -45.0 + + if pz <= min_altitude: + pz = min_altitude + dive_phase = "recovering" + + elif dive_phase == "recovering": + target_pitch = 30.0 + pz += 0.8 + speed += (pre_dive_speed - speed) * 0.1 + + if pz >= pre_dive_alt: + pz = pre_dive_alt + speed = pre_dive_speed + dive_phase = "none" + else: + if keys[K_w]: + speed = min(speed + acceleration, max_speed) + elif keys[K_s]: + speed = max(speed - deceleration, 0.0) + + if speed < 0.3: + for rx, ry, rz, rh in REEDS: + dist = math.hypot(px - rx, py - ry) + if dist < 10.0 and abs(pz - (rz + rh)) < 10.0: + px, py, pz = rx, ry, rz + rh + speed = 0.0 + roll = 0.0 + pitch = 0.0 + is_perched = True + + if caught_insect: + if caught_insect.type == "mosquito": + eaten_mosquitos += 1 + else: + eaten_flies += 1 + caught_insect = None + break + + target_pitch = 0.0 + if keys[K_UP]: + pz = min(pz + 0.8, max_altitude) + target_pitch = max_pitch + elif keys[K_DOWN]: + pz = max(pz - 0.8, min_altitude) + target_pitch = -max_pitch + + if not auto_landing: + pitch += (target_pitch - pitch) * 0.15 + + target_roll = 0.0 + if keys[K_LEFT]: + yaw += 2.8 + target_roll = -max_roll + elif keys[K_RIGHT]: + yaw -= 2.8 + target_roll = max_roll + + roll += (target_roll - roll) * 0.15 + + rad_yaw = math.radians(yaw) + px += -math.sin(rad_yaw) * speed + py += math.cos(rad_yaw) * speed + + if abs(py) < POND_RADIUS: + max_limit_x = POND_RADIUS + 200.0 + else: + cx = get_river_center(py) + max_limit_x = abs(cx) + 200.0 + + px = max(-max_limit_x, min(max_limit_x, px)) + + if py > STREAM_MAX_Y: + py -= WORLD_LEN + elif py < STREAM_MIN_Y: + py += WORLD_LEN + + wing_timer += 80.0 if (dive_phase != "none" or auto_landing) else (60.0 if speed > 0.1 else 30.0) + wing_angle = math.sin(wing_timer) * 35.0 + + frog.update() + + current_mosquitos = 0 + current_flies = 0 + + for insect in insects[:]: + insect.update((px, py, pz)) + + if insect.type == "mosquito": + current_mosquitos += 1 + else: + current_flies += 1 + + if not caught_insect: + dist_to_dragonfly = math.sqrt((px - insect.x)**2 + (py - insect.y)**2 + (pz - insect.z)**2) + if dist_to_dragonfly < 6.0: + caught_insect = insect + insects.remove(insect) + + if dive_phase == "diving": + dive_phase = "recovering" + + if current_mosquitos < MOSQUITO_MIN: + insects.append(Insect("mosquito")) + if current_flies < FLY_MIN: + insects.append(Insect("fly")) + + cam_dist = 18.0 + cam_height = 8.0 + rad_yaw = math.radians(yaw) + + cam_x = px + math.sin(rad_yaw) * cam_dist + cam_y = py - math.cos(rad_yaw) * cam_dist + cam_z = pz + cam_height + + gluLookAt(cam_x, cam_y, cam_z, + px, py, pz + 0.5, + 0, 0, 1) + + draw_map(water_timer) + frog.draw() + + for insect in insects: + insect.draw() + + glPushMatrix() + glTranslatef(px, py, pz) + glRotatef(yaw, 0, 0, 1) + glRotatef(pitch, 1, 0, 0) + glRotatef(roll, 0, 1, 0) + draw_dragonfly(wing_angle, caught_insect) + glPopMatrix() + + draw_hud((px, py, pz), yaw, insects, eaten_mosquitos, eaten_flies, font) + + pygame.display.flip() + + pygame.quit() + sys.exit() + +if __name__ == "__main__": + main() diff --git a/previos/libelula_v8.3.py b/previos/libelula_v8.3.py new file mode 100644 index 0000000..631455b --- /dev/null +++ b/previos/libelula_v8.3.py @@ -0,0 +1,982 @@ +import sys +import math +import random +import pygame +from pygame.locals import * +from OpenGL.GL import * +from OpenGL.GLU import * + +# ========================================== +# CONFIGURACIÓN Y CONSTANTES +# ========================================== +WIDTH, HEIGHT = 800, 600 + +# Colores (R, G, B) +COLOR_BG = (0.12, 0.15, 0.12) # Fondo ambiente +COLOR_WATER = (0.0, 0.5, 0.85) # Agua animada (Cian/Azul) +COLOR_LAND = (0.45, 0.32, 0.15) # Tierra / Relieve +COLOR_LAND_RIVER = (0.40, 0.28, 0.12) # Tierra en riachuelos +COLOR_ISLAND = (0.5, 0.38, 0.18) # Islas +COLOR_REED = (0.15, 0.85, 0.25) # Juncos verdes +COLOR_CATTAIL = (0.35, 0.18, 0.08) # Espigas marrones densas +COLOR_DRAGONFLY = (0.0, 1.0, 1.0) # Libélula alámbrica (Cian) +COLOR_MOSQUITO = (1.0, 0.9, 0.0) # Mosquitos (Amarillo) +COLOR_FLY = (0.8, 0.25, 0.15) # Moscas (Rojo-Marrón) +COLOR_FROG = (0.15, 0.85, 0.35) # Rana verde detallada +COLOR_HUD = (1.0, 1.0, 1.0) # Retícula HUD (Blanco) + +# Dimensiones del Mundo +POND_RADIUS = 1800.0 +STREAM_LEN = 900.0 +STREAM_MIN_Y = -(POND_RADIUS + STREAM_LEN) +STREAM_MAX_Y = (POND_RADIUS + STREAM_LEN) +WORLD_LEN = STREAM_MAX_Y - STREAM_MIN_Y + +# Cantidades de insectos +MOSQUITO_MIN, MOSQUITO_MAX = 17, 27 +FLY_MIN, FLY_MAX = 9, 14 + +# ========================================== +# ELEMENTOS DEL MAPA +# ========================================== +REEDS = [ + (-250, 350, 0, 20), (-160, -500, 0, 22), (500, 200, 0, 24), (-700, 200, 0, 21), (200, -800, 0, 23), + (-900, -300, 0, 25), (760, -600, 0, 22), (900, 500, 0, 26), (-500, 900, 0, 23), (600, -1000, 0, 22), + (-1200, 400, 0, 24), (1300, -300, 0, 20), (-1400, -700, 0, 25), (1100, 1200, 0, 23), (-360, -1300, 0, 22), + (-90, -1950, 0, 20), (-110, -2150, 0, 23), (-80, -2350, 0, 19), (-100, -2550, 0, 22), + (90, 1950, 0, 22), (110, 2150, 0, 20), (85, 2350, 0, 24), (105, 2550, 0, 21) +] + +ISLANDS = [ + (-250, 350, 90), (-160, -500, 85), (500, 200, 100), (-700, 200, 110), (200, -800, 95), + (-900, -300, 120), (760, -600, 90), (900, 500, 105), (-500, 900, 115), (600, -1000, 100) +] + +def get_river_center(y): + if y < -POND_RADIUS: + dy = y + POND_RADIUS + return -50 + 100 * math.sin(dy * 0.008) + elif y > POND_RADIUS: + dy = y - POND_RADIUS + return 50 + 100 * math.sin(dy * 0.008) + else: + return (y / POND_RADIUS) * 50.0 + +def is_over_water(x, y): + dist_center = math.hypot(x, y) + if dist_center < POND_RADIUS - 50: + return True + if abs(y) >= POND_RADIUS: + cx = get_river_center(y) + if abs(x - cx) < 80: + return True + return False + +def get_random_water_position(): + while True: + if random.random() < 0.7: + rad = random.uniform(0, math.pi * 2) + r = random.uniform(100, POND_RADIUS - 150) + x = r * math.cos(rad) + y = r * math.sin(rad) + else: + y = random.choice([random.uniform(POND_RADIUS, STREAM_MAX_Y), random.uniform(STREAM_MIN_Y, -POND_RADIUS)]) + cx = get_river_center(y) + x = cx + random.uniform(-60, 60) + + if is_over_water(x, y): + z = random.uniform(5.0, 35.0) + return x, y, z + +# ========================================== +# CLASES DE INSECTOS Y RANA DECORATIVA +# ========================================== +class Insect: + def __init__(self, insect_type): + self.type = insect_type + self.x, self.y, self.z = get_random_water_position() + self.yaw = random.uniform(0, 360) + self.timer = random.uniform(0, 100) + + if self.type == "mosquito": + self.speed = 1.8 + self.size = 1.2 + else: + self.speed = 0.9 + self.size = 1.6 + + def update(self, player_pos): + self.timer += 0.05 + px, py, pz = player_pos + dist_to_player = math.hypot(self.x - px, self.y - py) + + if self.type == "mosquito": + can_detect = (pz <= self.z) + if can_detect and dist_to_player < 100: + escape_angle = math.degrees(math.atan2(self.y - py, self.x - px)) + self.yaw += (escape_angle - self.yaw) * 0.05 + + self.yaw += math.sin(self.timer * 3) * 1.5 + self.z += math.sin(self.timer * 1.5) * 0.2 + + elif self.type == "fly": + if dist_to_player < 120: + escape_angle = math.degrees(math.atan2(self.y - py, self.x - px)) + self.yaw = escape_angle + random.uniform(-45, 45) + self.z += random.choice([-1.5, 1.5]) + else: + if random.random() < 0.08: + self.yaw += random.uniform(-60, 60) + + self.yaw += math.sin(self.timer * 6) * 4.0 + self.z += math.cos(self.timer * 4) * 0.6 + + self.z = max(3.0, min(50.0, self.z)) + + rad = math.radians(self.yaw) + next_x = self.x - math.sin(rad) * self.speed + next_y = self.y + math.cos(rad) * self.speed + + if is_over_water(next_x, next_y): + self.x, self.y = next_x, next_y + else: + self.yaw += 140.0 + random.uniform(-20, 20) + + if self.y > STREAM_MAX_Y: + self.y -= WORLD_LEN + elif self.y < STREAM_MIN_Y: + self.y += WORLD_LEN + + def draw(self, attached=False): + if self.type == "mosquito": + glColor3f(*COLOR_MOSQUITO) + else: + glColor3f(*COLOR_FLY) + + glPushMatrix() + if not attached: + glTranslatef(self.x, self.y, self.z) + glRotatef(self.yaw, 0, 0, 1) + + s = self.size + glBegin(GL_LINES) + glVertex3f(0, s, 0) + glVertex3f(0, -s, 0) + w_offset = 0.0 if attached else math.sin(self.timer * 30) * (s * 0.8) + glVertex3f(0, 0, 0) + glVertex3f(-s * 1.5, 0, w_offset) + glVertex3f(0, 0, 0) + glVertex3f(s * 1.5, 0, w_offset) + glEnd() + + glPopMatrix() + +class Frog: + def __init__(self): + self.pick_new_destination() + self.x, self.y = self.target_x, self.target_y + self.z = 0.6 + self.speed = 0.35 + self.yaw = 0.0 + self.swim_timer = 0.0 + + def pick_new_destination(self): + destinations = [(rx, ry) for rx, ry, _, _ in REEDS] + [(ix, iy) for ix, iy, _ in ISLANDS] + dest = random.choice(destinations) + self.target_x, self.target_y = dest[0] + random.uniform(-30, 30), dest[1] + random.uniform(-30, 30) + + def update(self): + self.swim_timer += 0.15 + dx = self.target_x - self.x + dy = self.target_y - self.y + dist = math.hypot(dx, dy) + + if dist < 6.0: + self.pick_new_destination() + else: + self.yaw = math.degrees(math.atan2(dy, dx)) - 90.0 + self.x += (dx / dist) * self.speed + self.y += (dy / dist) * self.speed + + def draw(self): + glColor3f(*COLOR_FROG) + glPushMatrix() + glTranslatef(self.x, self.y, self.z) + glRotatef(self.yaw, 0, 0, 1) + + leg_offset = math.sin(self.swim_timer) * 1.5 + + glBegin(GL_LINES) + glVertex3f(0.0, 0.0, 0.5) + glVertex3f(0.0, -5.0, 0.5) + glVertex3f(-2.0, 2.0, 0.5) + glVertex3f(2.0, 2.0, 0.5) + glVertex3f(-2.5, -2.0, 0.5) + glVertex3f(2.5, -2.0, 0.5) + + glVertex3f(-2.0, 2.0, 0.5) + glVertex3f(0.0, 0.0, 0.5) + glVertex3f(2.0, 2.0, 0.5) + glVertex3f(0.0, 0.0, 0.5) + + glVertex3f(-2.0, 2.0, 0.6) + glVertex3f(2.0, 2.0, 0.6) + glVertex3f(-2.0, 2.0, 0.6) + glVertex3f(0.0, 4.5, 0.6) + glVertex3f(2.0, 2.0, 0.6) + glVertex3f(0.0, 4.5, 0.6) + + glVertex3f(-1.5, 3.5, 1.5) + glVertex3f(-1.5, 3.5, 0.6) + glVertex3f(1.5, 3.5, 1.5) + glVertex3f(1.5, 3.5, 0.6) + + glVertex3f(-2.0, 2.0, 0.5) + glVertex3f(-4.0, 3.0, 0.0) + glVertex3f(-4.0, 3.0, 0.0) + glVertex3f(-3.5, 4.0, 0.0) + + glVertex3f(2.0, 2.0, 0.5) + glVertex3f(4.0, 3.0, 0.0) + glVertex3f(4.0, 3.0, 0.0) + glVertex3f(3.5, 4.0, 0.0) + + glVertex3f(-2.5, -2.0, 0.5) + glVertex3f(-5.0 - leg_offset, -4.0, 0.0) + glVertex3f(-5.0 - leg_offset, -4.0, 0.0) + glVertex3f(-3.0 - leg_offset, -6.0, 0.0) + + glVertex3f(2.5, -2.0, 0.5) + glVertex3f(5.0 + leg_offset, -4.0, 0.0) + glVertex3f(5.0 + leg_offset, -4.0, 0.0) + glVertex3f(3.0 + leg_offset, -6.0, 0.0) + glEnd() + + glPopMatrix() + +# ========================================== +# RENDERIZADO DE ESCENARIO Y MODELOS +# ========================================== +def draw_water_surface(time_counter): + glColor3f(*COLOR_WATER) + + grid_size = 15 + for x in range(int(-POND_RADIUS), int(POND_RADIUS), grid_size * 2): + glBegin(GL_LINE_STRIP) + for y in range(int(-POND_RADIUS), int(POND_RADIUS), grid_size): + dist = math.hypot(x, y) + if dist < POND_RADIUS - 30: + z = 1.0 * math.sin(dist * 0.015 - time_counter * 3.0) + 0.5 * math.cos(x * 0.01 + time_counter) + glVertex3f(x, y, z) + glEnd() + + for y in range(int(STREAM_MIN_Y), int(STREAM_MAX_Y), 15): + if abs(y) < POND_RADIUS: + continue + cx = get_river_center(y) + glBegin(GL_LINE_STRIP) + for dx in range(-100, 110, 10): + x = cx + dx + z = 0.5 * math.sin(y * 0.03 + dx * 0.08 + time_counter * 4.0) + glVertex3f(x, y, z) + glEnd() + +def draw_terrain(): + glColor3f(*COLOR_LAND) + + grid_step = 120 + for y in range(int(STREAM_MIN_Y - 300), int(STREAM_MAX_Y + 300), grid_step): + glBegin(GL_LINE_STRIP) + for x in range(-2500, 2500, grid_step): + z = 1.5 * math.sin(x * 0.005) + 1.5 * math.cos(y * 0.005) + glVertex3f(x, y, z) + glEnd() + + width_river = 100.0 + margin_land = 70.0 + glColor3f(*COLOR_LAND_RIVER) + + for y in range(int(STREAM_MIN_Y), int(STREAM_MAX_Y), 30): + if abs(y) < POND_RADIUS: + continue + cx = get_river_center(y) + + glBegin(GL_LINE_STRIP) + for dx in range(int(-width_river - margin_land), int(-width_river), 15): + x = cx + dx + z = 0.6 * math.sin(y * 0.02 + x * 0.02) + 0.3 + glVertex3f(x, y, z) + glEnd() + + glBegin(GL_LINE_STRIP) + for dx in range(int(width_river), int(width_river + margin_land), 15): + x = cx + dx + z = 0.6 * math.sin(y * 0.02 + x * 0.02) + 0.3 + glVertex3f(x, y, z) + glEnd() + + for ix, iy, ir in ISLANDS: + glColor3f(*COLOR_ISLAND) + for layer in range(1, 4): + rad_layer = ir * (1 - layer * 0.25) + height = layer * 1.2 + glBegin(GL_LINE_LOOP) + for i in range(16): + angle = 2.0 * math.pi * i / 16 + dx = rad_layer * math.cos(angle) + dy = rad_layer * math.sin(angle) + glVertex3f(ix + dx, iy + dy, height) + glEnd() + +def draw_map(time_counter): + draw_terrain() + draw_water_surface(time_counter) + + offsets = [(-1.5, 0.6), (1.3, -0.6), (-0.7, -1.4), (1.1, 1.2), (0.0, 1.6), (-1.2, 0.0)] + + for rx, ry, rz, rh in REEDS: + glColor3f(*COLOR_REED) + glBegin(GL_LINES) + glVertex3f(rx, ry, rz) + glVertex3f(rx, ry, rz + rh) + for ox, oy in offsets: + glVertex3f(rx, ry, rz) + glVertex3f(rx + ox, ry + oy, rz + rh * 0.85) + glEnd() + + glColor3f(*COLOR_CATTAIL) + glBegin(GL_LINES) + for d in [-0.2, 0.0, 0.2]: + glVertex3f(rx + d, ry, rz + rh - 4.5) + glVertex3f(rx + d, ry, rz + rh) + for ox, oy in offsets: + for d in [-0.15, 0.15]: + glVertex3f(rx + ox + d, ry + oy, (rz + rh * 0.85) - 3.8) + glVertex3f(rx + ox + d, ry + oy, rz + rh * 0.85) + glEnd() + +def draw_dragonfly(wing_angle, caught_insect=None): + glColor3f(*COLOR_DRAGONFLY) + + glBegin(GL_LINES) + glVertex3f(0, 1.5, 0) + glVertex3f(0, -3.0, 0) + glVertex3f(-0.4, 0.6, 0) + glVertex3f(0.4, 0.6, 0) + glVertex3f(-0.3, -0.6, 0) + glVertex3f(0.3, -0.6, 0) + + glVertex3f(-0.3, 0.5, 0) + glVertex3f(-0.2, 0.2, -0.6) + glVertex3f(0.3, 0.5, 0) + glVertex3f(0.2, 0.2, -0.6) + glVertex3f(-0.2, -0.2, 0) + glVertex3f(-0.1, -0.3, -0.6) + glVertex3f(0.2, -0.2, 0) + glVertex3f(0.1, -0.3, -0.6) + glEnd() + + glBegin(GL_LINE_LOOP) + for i in range(8): + angle = 2.0 * math.pi * i / 8 + glVertex3f(0.3 * math.cos(angle), 1.4 + 0.3 * math.sin(angle), 0) + glEnd() + + rad_wing = math.radians(wing_angle) + w_z = math.sin(rad_wing) * 1.2 if wing_angle != 0 else 0.0 + + # Par de alas delanteras + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(-4.5, 1.0, w_z) + glVertex3f(-3.8, 0.2, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(4.5, 1.0, w_z) + glVertex3f(3.8, 0.2, w_z) + glEnd() + + # Par de alas traseras (añadidas para completar las 4 alas) + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(-3.8, -0.2, w_z) + glVertex3f(-3.0, -0.8, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(3.8, -0.2, w_z) + glVertex3f(3.0, -0.8, w_z) + glEnd() + + if caught_insect: + glPushMatrix() + glTranslatef(0.0, 0.2, -0.6) + caught_insect.draw(attached=True) + glPopMatrix() + +# ========================================== +# INTERFAZ 2D / HUD Y PANTALLA INICIAL +# ========================================== +def draw_text_2d(text_surface, x, y): + text_data = pygame.image.tostring(text_surface, "RGBA", True) + w, h = text_surface.get_size() + + glEnable(GL_BLEND) + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) + + glRasterPos2i(x, y) + glDrawPixels(w, h, GL_RGBA, GL_UNSIGNED_BYTE, text_data) + glDisable(GL_BLEND) + +def draw_start_screen(font_title, font_subtitle, font_text, wing_angle): + glDisable(GL_DEPTH_TEST) + + glMatrixMode(GL_PROJECTION) + glPushMatrix() + glLoadIdentity() + gluOrtho2D(0, WIDTH, 0, HEIGHT) + + glMatrixMode(GL_MODELVIEW) + glPushMatrix() + glLoadIdentity() + + # TÍTULO PRINCIPAL + txt_title = font_title.render("LIBÉLULA", True, (0, 220, 255)) + draw_text_2d(txt_title, WIDTH // 2 - 130, HEIGHT - 100) + + # DIBUJO ANIMADO DE LA LIBÉLULA DE 4 ALAS EN LA PANTALLA INICIAL + glPushMatrix() + glTranslatef(WIDTH // 2 + 150, HEIGHT - 75, 0.0) + glScalef(12.0, 12.0, 1.0) + + glColor3f(*COLOR_DRAGONFLY) + glBegin(GL_LINES) + glVertex3f(0, 1.5, 0) + glVertex3f(0, -3.0, 0) + glVertex3f(-0.4, 0.6, 0) + glVertex3f(0.4, 0.6, 0) + glEnd() + + # 4 alas aleteando en el menú + rad_w = math.radians(wing_angle) + w_off = math.sin(rad_w) * 1.5 + + # Alas delanteras + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(-4.5, 1.0, w_off) + glVertex3f(-3.8, 0.2, w_off) + glEnd() + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(4.5, 1.0, w_off) + glVertex3f(3.8, 0.2, w_off) + glEnd() + + # Alas traseras + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(-3.8, -0.2, w_off) + glVertex3f(-3.0, -0.8, w_off) + glEnd() + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(3.8, -0.2, w_off) + glVertex3f(3.0, -0.8, w_off) + glEnd() + + glPopMatrix() + + # OBJETIVO DEL JUEGO + obj_line1 = font_text.render("Objetivo: Vuela libre por el estanque y sus arroyos capturando moscas", True, (255, 255, 255)) + obj_line2 = font_text.render("y mosquitos. Una vez cazados debes devorarlos posado en un junco.", True, (255, 255, 255)) + draw_text_2d(obj_line1, 70, HEIGHT - 170) + draw_text_2d(obj_line2, 70, HEIGHT - 195) + + # CONTROLES Y TECLAS + controls = [ + "CONTROLES:", + "W / S : Acelerar / Frenar", + "Flecha Arriba / Abajo : Cambiar altitud", + "Flecha Izquierda / Derecha : Girar / Virar", + "ESPACIO : Picado rápido para caza", + "P : Aterrizaje automático en junco cercano", + "ESC : Salir del juego" + ] + + y_pos = HEIGHT - 250 + for ctrl in controls: + is_title = ctrl.endswith(":") + col = (255, 220, 50) if is_title else (200, 200, 200) + t_surf = font_subtitle.render(ctrl, True, col) if is_title else font_text.render(ctrl, True, col) + draw_text_2d(t_surf, 70, y_pos) + y_pos -= 25 + + # LEYENDA VISUAL INFERIOR (Mosquito, Mosca y Junco) + y_icon = 90 + + # Icono Mosquito + glColor3f(*COLOR_MOSQUITO) + glBegin(GL_LINES) + glVertex2f(100, y_icon + 5) + glVertex2f(100, y_icon - 5) + glVertex2f(95, y_icon) + glVertex2f(105, y_icon) + glEnd() + txt_m = font_text.render("Mosquito", True, (255, 230, 0)) + draw_text_2d(txt_m, 120, y_icon - 8) + + # Icono Mosca + glColor3f(*COLOR_FLY) + glBegin(GL_LINES) + glVertex2f(280, y_icon + 5) + glVertex2f(280, y_icon - 5) + glVertex2f(275, y_icon - 3) + glVertex2f(285, y_icon + 3) + glVertex2f(275, y_icon + 3) + glVertex2f(285, y_icon - 3) + glEnd() + txt_f = font_text.render("Mosca", True, (200, 65, 40)) + draw_text_2d(txt_f, 300, y_icon - 8) + + # Icono Junco + glColor3f(*COLOR_REED) + glBegin(GL_LINES) + glVertex2f(450, y_icon - 10) + glVertex2f(450, y_icon + 15) + glEnd() + glColor3f(*COLOR_CATTAIL) + glBegin(GL_LINES) + glVertex2f(450, y_icon + 8) + glVertex2f(450, y_icon + 15) + glEnd() + txt_r = font_text.render("Junco", True, (100, 220, 100)) + draw_text_2d(txt_r, 470, y_icon - 8) + + # MENSAJE PARA COMENZAR + start_msg = font_subtitle.render("Pulsa INTRO o cualquier tecla para comenzar...", True, (100, 255, 100)) + draw_text_2d(start_msg, WIDTH // 2 - 200, 30) + + glMatrixMode(GL_PROJECTION) + glPopMatrix() + glMatrixMode(GL_MODELVIEW) + glPopMatrix() + glEnable(GL_DEPTH_TEST) + +def draw_hud(player_pos, yaw, insects, eaten_mosquitos, eaten_flies, font): + glDisable(GL_DEPTH_TEST) + + glMatrixMode(GL_PROJECTION) + glPushMatrix() + glLoadIdentity() + gluOrtho2D(0, WIDTH, 0, HEIGHT) + + glMatrixMode(GL_MODELVIEW) + glPushMatrix() + glLoadIdentity() + + px, py, pz = player_pos + + # RADAR REDUCIDO + cx, cy = WIDTH // 2, HEIGHT - 35 + radius = 22 + + glColor3f(*COLOR_HUD) + glBegin(GL_LINE_LOOP) + for i in range(24): + angle = 2.0 * math.pi * i / 24 + glVertex2f(cx + radius * math.cos(angle), cy + radius * math.sin(angle)) + glEnd() + + glBegin(GL_LINES) + glVertex2f(cx, cy + radius) + glVertex2f(cx, cy + radius - 5) + glVertex2f(cx, cy - radius) + glVertex2f(cx, cy - radius + 5) + glVertex2f(cx - radius, cy) + glVertex2f(cx - radius + 5, cy) + glVertex2f(cx + radius, cy) + glVertex2f(cx + radius - 5, cy) + glEnd() + + closest_insect = None + min_dist = 999999.0 + relative_angle = 0.0 + diff_z = 0.0 + + for insect in insects: + dx = insect.x - px + dy = insect.y - py + dist = math.hypot(dx, dy) + + if dist < min_dist and dist > 0.1: + angle_to_insect = math.degrees(math.atan2(dy, dx)) + heading = (angle_to_insect - (90 - yaw)) % 360 + if heading > 180: + heading -= 360 + + if abs(heading) < 60: + min_dist = dist + closest_insect = insect + relative_angle = heading + diff_z = insect.z - pz + + if closest_insect: + radar_x = cx - (relative_angle / 60.0) * (radius - 4) + radar_y = cy + (diff_z / 20.0) * (radius - 4) + + dist_from_center = math.hypot(radar_x - cx, radar_y - cy) + if dist_from_center > (radius - 3): + factor = (radius - 3) / dist_from_center + radar_x = cx + (radar_x - cx) * factor + radar_y = cy + (radar_y - cy) * factor + + glColor3f(1.0, 1.0, 1.0) + glBegin(GL_LINE_LOOP) + for i in range(8): + ang = 2.0 * math.pi * i / 8 + glVertex2f(radar_x + 3 * math.cos(ang), radar_y + 3 * math.sin(ang)) + glEnd() + + # DIBUJOS ESTÁTICOS 2D DE LOS INSECTOS EN EL HUD + glColor3f(*COLOR_MOSQUITO) + glBegin(GL_LINES) + glVertex2f(25, HEIGHT - 33) + glVertex2f(25, HEIGHT - 27) + glVertex2f(21, HEIGHT - 30) + glVertex2f(29, HEIGHT - 30) + glEnd() + + glColor3f(*COLOR_FLY) + glBegin(GL_LINES) + glVertex2f(WIDTH - 82, HEIGHT - 33) + glVertex2f(WIDTH - 82, HEIGHT - 27) + glVertex2f(WIDTH - 86, HEIGHT - 28) + glVertex2f(WIDTH - 78, HEIGHT - 32) + glVertex2f(WIDTH - 86, HEIGHT - 32) + glVertex2f(WIDTH - 78, HEIGHT - 28) + glEnd() + + # TEXTOS DE CONTADORES + txt_mosq = font.render(f"x {eaten_mosquitos}", True, (255, 230, 0)) + txt_flies = font.render(f"x {eaten_flies}", True, (200, 65, 40)) + + draw_text_2d(txt_mosq, 38, HEIGHT - 40) + draw_text_2d(txt_flies, WIDTH - 68, HEIGHT - 40) + + glMatrixMode(GL_PROJECTION) + glPopMatrix() + glMatrixMode(GL_MODELVIEW) + glPopMatrix() + glEnable(GL_DEPTH_TEST) + +# ========================================== +# BUCLE PRINCIPAL +# ========================================== +def main(): + pygame.init() + pygame.font.init() + font = pygame.font.SysFont("Arial", 22, bold=True) + font_title = pygame.font.SysFont("Arial", 48, bold=True) + font_subtitle = pygame.font.SysFont("Arial", 20, bold=True) + font_text = pygame.font.SysFont("Arial", 16, bold=False) + + pygame.display.set_mode((WIDTH, HEIGHT), DOUBLEBUF | OPENGL) + pygame.display.set_caption("Libélula") + + glEnable(GL_DEPTH_TEST) + glClearColor(*COLOR_BG, 1.0) + + glMatrixMode(GL_PROJECTION) + gluPerspective(50, (WIDTH / HEIGHT), 0.1, 5000.0) + glMatrixMode(GL_MODELVIEW) + + # Estado de la Libélula + px, py, pz = 0.0, 0.0, 15.0 + yaw = 0.0 + roll = 0.0 + pitch = 0.0 + max_roll = 30.0 + max_pitch = 20.0 + + speed = 0.0 + max_speed = 3.0 + acceleration = 0.06 + deceleration = 0.03 + + min_altitude = 1.0 + max_altitude = 120.0 + + wing_timer = 0.0 + water_timer = 0.0 + is_perched = False + + dive_phase = "none" + pre_dive_alt = 15.0 + pre_dive_speed = 0.0 + + auto_landing = False + target_perch = None + + caught_insect = None + eaten_mosquitos = 0 + eaten_flies = 0 + + frog = Frog() + + insects = [] + num_mosquitos = random.randint(MOSQUITO_MIN, MOSQUITO_MAX) + num_flies = random.randint(FLY_MIN, FLY_MAX) + + for _ in range(num_mosquitos): + insects.append(Insect("mosquito")) + for _ in range(num_flies): + insects.append(Insect("fly")) + + clock = pygame.time.Clock() + + in_start_screen = True + running = True + + while running: + dt = clock.tick(60) / 1000.0 + water_timer += dt + wing_timer += 60.0 + + for event in pygame.event.get(): + if event.type == QUIT or (event.type == KEYDOWN and event.key == K_ESCAPE): + running = False + elif event.type == KEYDOWN: + if in_start_screen: + in_start_screen = False # Salir de la pantalla de inicio con cualquier tecla + else: + if event.key == K_SPACE: + if not is_perched and dive_phase == "none": + dive_phase = "diving" + pre_dive_alt = pz + pre_dive_speed = speed + auto_landing = False + elif event.key == K_p: + if not is_perched and dive_phase == "none": + best_reed = None + min_d = 999.0 + for rx, ry, rz, rh in REEDS: + dist = math.hypot(px - rx, py - ry) + if dist < 35.0: + angle_to_reed = math.degrees(math.atan2(ry - py, rx - px)) + heading = (angle_to_reed - (90 - yaw)) % 360 + if heading > 180: + heading -= 360 + if abs(heading) < 45 and dist < min_d: + min_d = dist + best_reed = (rx, ry, rz + rh) + + if best_reed: + target_perch = best_reed + auto_landing = True + + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) + glLoadIdentity() + + if in_start_screen: + cam_x, cam_y, cam_z = 0.0, -100.0, 30.0 + gluLookAt(cam_x, cam_y, cam_z, 0, 0, 15, 0, 0, 1) + draw_map(water_timer) + frog.draw() + + wing_angle = math.sin(wing_timer * 0.5) * 35.0 + draw_start_screen(font_title, font_subtitle, font_text, wing_angle) + else: + keys = pygame.key.get_pressed() + + if is_perched: + if keys[K_LEFT]: + yaw += 3.0 + if keys[K_RIGHT]: + yaw -= 3.0 + + if keys[K_w]: + is_perched = False + speed = 0.5 + + wing_angle = 0.0 + pitch = 0.0 + roll = 0.0 + dive_phase = "none" + auto_landing = False + else: + if auto_landing and target_perch: + tx, ty, tz = target_perch + dx = tx - px + dy = ty - py + dist_to_target = math.hypot(dx, dy) + + speed = 1.2 + px += (dx / dist_to_target) * speed if dist_to_target > 0.1 else 0 + py += (dy / dist_to_target) * speed if dist_to_target > 0.1 else 0 + pz += (tz - pz) * 0.1 + + if dist_to_target < 2.5 and abs(pz - tz) < 2.5: + px, py, pz = target_perch + speed = 0.0 + roll = 0.0 + pitch = 0.0 + is_perched = True + auto_landing = False + target_perch = None + + if caught_insect: + if caught_insect.type == "mosquito": + eaten_mosquitos += 1 + else: + eaten_flies += 1 + caught_insect = None + + elif dive_phase == "diving": + speed = max_speed * 1.5 + pz -= 1.2 + target_pitch = -45.0 + + if pz <= min_altitude: + pz = min_altitude + dive_phase = "recovering" + + elif dive_phase == "recovering": + target_pitch = 30.0 + pz += 0.8 + speed += (pre_dive_speed - speed) * 0.1 + + if pz >= pre_dive_alt: + pz = pre_dive_alt + speed = pre_dive_speed + dive_phase = "none" + else: + if keys[K_w]: + speed = min(speed + acceleration, max_speed) + elif keys[K_s]: + speed = max(speed - deceleration, 0.0) + + if speed < 0.3: + for rx, ry, rz, rh in REEDS: + dist = math.hypot(px - rx, py - ry) + if dist < 10.0 and abs(pz - (rz + rh)) < 10.0: + px, py, pz = rx, ry, rz + rh + speed = 0.0 + roll = 0.0 + pitch = 0.0 + is_perched = True + + if caught_insect: + if caught_insect.type == "mosquito": + eaten_mosquitos += 1 + else: + eaten_flies += 1 + caught_insect = None + break + + target_pitch = 0.0 + if keys[K_UP]: + pz = min(pz + 0.8, max_altitude) + target_pitch = max_pitch + elif keys[K_DOWN]: + pz = max(pz - 0.8, min_altitude) + target_pitch = -max_pitch + + if not auto_landing: + pitch += (target_pitch - pitch) * 0.15 + + target_roll = 0.0 + if keys[K_LEFT]: + yaw += 2.8 + target_roll = -max_roll + elif keys[K_RIGHT]: + yaw -= 2.8 + target_roll = max_roll + + roll += (target_roll - roll) * 0.15 + + rad_yaw = math.radians(yaw) + px += -math.sin(rad_yaw) * speed + py += math.cos(rad_yaw) * speed + + if abs(py) < POND_RADIUS: + max_limit_x = POND_RADIUS + 200.0 + else: + cx = get_river_center(py) + max_limit_x = abs(cx) + 200.0 + + px = max(-max_limit_x, min(max_limit_x, px)) + + if py > STREAM_MAX_Y: + py -= WORLD_LEN + elif py < STREAM_MIN_Y: + py += WORLD_LEN + + wing_timer += 80.0 if (dive_phase != "none" or auto_landing) else (60.0 if speed > 0.1 else 30.0) + wing_angle = math.sin(wing_timer) * 35.0 + + frog.update() + + current_mosquitos = 0 + current_flies = 0 + + for insect in insects[:]: + insect.update((px, py, pz)) + + if insect.type == "mosquito": + current_mosquitos += 1 + else: + current_flies += 1 + + if not caught_insect: + dist_to_dragonfly = math.sqrt((px - insect.x)**2 + (py - insect.y)**2 + (pz - insect.z)**2) + if dist_to_dragonfly < 6.0: + caught_insect = insect + insects.remove(insect) + + if dive_phase == "diving": + dive_phase = "recovering" + + if current_mosquitos < MOSQUITO_MIN: + insects.append(Insect("mosquito")) + if current_flies < FLY_MIN: + insects.append(Insect("fly")) + + cam_dist = 18.0 + cam_height = 8.0 + rad_yaw = math.radians(yaw) + + cam_x = px + math.sin(rad_yaw) * cam_dist + cam_y = py - math.cos(rad_yaw) * cam_dist + cam_z = pz + cam_height + + gluLookAt(cam_x, cam_y, cam_z, + px, py, pz + 0.5, + 0, 0, 1) + + draw_map(water_timer) + frog.draw() + + for insect in insects: + insect.draw() + + glPushMatrix() + glTranslatef(px, py, pz) + glRotatef(yaw, 0, 0, 1) + glRotatef(pitch, 1, 0, 0) + glRotatef(roll, 0, 1, 0) + draw_dragonfly(wing_angle, caught_insect) + glPopMatrix() + + draw_hud((px, py, pz), yaw, insects, eaten_mosquitos, eaten_flies, font) + + pygame.display.flip() + + pygame.quit() + sys.exit() + +if __name__ == "__main__": + main() diff --git a/previos/libelula_v8.4.py b/previos/libelula_v8.4.py new file mode 100644 index 0000000..66793d8 --- /dev/null +++ b/previos/libelula_v8.4.py @@ -0,0 +1,972 @@ +import sys +import math +import random +import pygame +from pygame.locals import * +from OpenGL.GL import * +from OpenGL.GLU import * + +# ========================================== +# CONFIGURACIÓN Y CONSTANTES +# ========================================== +WIDTH, HEIGHT = 800, 600 + +# Colores (R, G, B) +COLOR_BG = (0.12, 0.15, 0.12) # Fondo ambiente +COLOR_WATER = (0.0, 0.5, 0.85) # Agua animada (Cian/Azul) +COLOR_LAND = (0.45, 0.32, 0.15) # Tierra / Relieve +COLOR_LAND_RIVER = (0.40, 0.28, 0.12) # Tierra en riachuelos +COLOR_ISLAND = (0.5, 0.38, 0.18) # Islas +COLOR_REED = (0.15, 0.85, 0.25) # Juncos verdes +COLOR_CATTAIL = (0.35, 0.18, 0.08) # Espigas marrones densas +COLOR_DRAGONFLY = (0.0, 1.0, 1.0) # Libélula alámbrica (Cian) +COLOR_MOSQUITO = (1.0, 0.9, 0.0) # Mosquitos (Amarillo) +COLOR_FLY = (0.8, 0.25, 0.15) # Moscas (Rojo-Marrón) +COLOR_FROG = (0.15, 0.85, 0.35) # Rana verde detallada +COLOR_HUD = (1.0, 1.0, 1.0) # Retícula HUD (Blanco) + +# Dimensiones del Mundo +POND_RADIUS = 1800.0 +STREAM_LEN = 900.0 +STREAM_MIN_Y = -(POND_RADIUS + STREAM_LEN) +STREAM_MAX_Y = (POND_RADIUS + STREAM_LEN) +WORLD_LEN = STREAM_MAX_Y - STREAM_MIN_Y + +# Cantidades de insectos +MOSQUITO_MIN, MOSQUITO_MAX = 17, 27 +FLY_MIN, FLY_MAX = 9, 14 + +# ========================================== +# ELEMENTOS DEL MAPA +# ========================================== +REEDS = [ + (-250, 350, 0, 20), (-160, -500, 0, 22), (500, 200, 0, 24), (-700, 200, 0, 21), (200, -800, 0, 23), + (-900, -300, 0, 25), (760, -600, 0, 22), (900, 500, 0, 26), (-500, 900, 0, 23), (600, -1000, 0, 22), + (-1200, 400, 0, 24), (1300, -300, 0, 20), (-1400, -700, 0, 25), (1100, 1200, 0, 23), (-360, -1300, 0, 22), + (-90, -1950, 0, 20), (-110, -2150, 0, 23), (-80, -2350, 0, 19), (-100, -2550, 0, 22), + (90, 1950, 0, 22), (110, 2150, 0, 20), (85, 2350, 0, 24), (105, 2550, 0, 21) +] + +ISLANDS = [ + (-250, 350, 90), (-160, -500, 85), (500, 200, 100), (-700, 200, 110), (200, -800, 95), + (-900, -300, 120), (760, -600, 90), (900, 500, 105), (-500, 900, 115), (600, -1000, 100) +] + +def get_river_center(y): + if y < -POND_RADIUS: + dy = y + POND_RADIUS + return -50 + 100 * math.sin(dy * 0.008) + elif y > POND_RADIUS: + dy = y - POND_RADIUS + return 50 + 100 * math.sin(dy * 0.008) + else: + return (y / POND_RADIUS) * 50.0 + +def is_over_water(x, y): + dist_center = math.hypot(x, y) + if dist_center < POND_RADIUS - 50: + return True + if abs(y) >= POND_RADIUS: + cx = get_river_center(y) + if abs(x - cx) < 80: + return True + return False + +def get_random_water_position(): + while True: + if random.random() < 0.7: + rad = random.uniform(0, math.pi * 2) + r = random.uniform(100, POND_RADIUS - 150) + x = r * math.cos(rad) + y = r * math.sin(rad) + else: + y = random.choice([random.uniform(POND_RADIUS, STREAM_MAX_Y), random.uniform(STREAM_MIN_Y, -POND_RADIUS)]) + cx = get_river_center(y) + x = cx + random.uniform(-60, 60) + + if is_over_water(x, y): + z = random.uniform(5.0, 35.0) + return x, y, z + +# ========================================== +# CLASES DE INSECTOS Y RANA DECORATIVA +# ========================================== +class Insect: + def __init__(self, insect_type): + self.type = insect_type + self.x, self.y, self.z = get_random_water_position() + self.yaw = random.uniform(0, 360) + self.timer = random.uniform(0, 100) + + if self.type == "mosquito": + self.speed = 1.8 + self.size = 1.2 + else: + self.speed = 0.9 + self.size = 1.6 + + def update(self, player_pos): + self.timer += 0.05 + px, py, pz = player_pos + dist_to_player = math.hypot(self.x - px, self.y - py) + + if self.type == "mosquito": + can_detect = (pz <= self.z) + if can_detect and dist_to_player < 100: + escape_angle = math.degrees(math.atan2(self.y - py, self.x - px)) + self.yaw += (escape_angle - self.yaw) * 0.05 + + self.yaw += math.sin(self.timer * 3) * 1.5 + self.z += math.sin(self.timer * 1.5) * 0.2 + + elif self.type == "fly": + if dist_to_player < 120: + escape_angle = math.degrees(math.atan2(self.y - py, self.x - px)) + self.yaw = escape_angle + random.uniform(-45, 45) + self.z += random.choice([-1.5, 1.5]) + else: + if random.random() < 0.08: + self.yaw += random.uniform(-60, 60) + + self.yaw += math.sin(self.timer * 6) * 4.0 + self.z += math.cos(self.timer * 4) * 0.6 + + self.z = max(3.0, min(50.0, self.z)) + + rad = math.radians(self.yaw) + next_x = self.x - math.sin(rad) * self.speed + next_y = self.y + math.cos(rad) * self.speed + + if is_over_water(next_x, next_y): + self.x, self.y = next_x, next_y + else: + self.yaw += 140.0 + random.uniform(-20, 20) + + if self.y > STREAM_MAX_Y: + self.y -= WORLD_LEN + elif self.y < STREAM_MIN_Y: + self.y += WORLD_LEN + + def draw(self, attached=False): + if self.type == "mosquito": + glColor3f(*COLOR_MOSQUITO) + else: + glColor3f(*COLOR_FLY) + + glPushMatrix() + if not attached: + glTranslatef(self.x, self.y, self.z) + glRotatef(self.yaw, 0, 0, 1) + + s = self.size + glBegin(GL_LINES) + glVertex3f(0, s, 0) + glVertex3f(0, -s, 0) + w_offset = 0.0 if attached else math.sin(self.timer * 30) * (s * 0.8) + glVertex3f(0, 0, 0) + glVertex3f(-s * 1.5, 0, w_offset) + glVertex3f(0, 0, 0) + glVertex3f(s * 1.5, 0, w_offset) + glEnd() + + glPopMatrix() + +class Frog: + def __init__(self): + self.pick_new_destination() + self.x, self.y = self.target_x, self.target_y + self.z = 0.6 + self.speed = 0.35 + self.yaw = 0.0 + self.swim_timer = 0.0 + + def pick_new_destination(self): + destinations = [(rx, ry) for rx, ry, _, _ in REEDS] + [(ix, iy) for ix, iy, _ in ISLANDS] + dest = random.choice(destinations) + self.target_x, self.target_y = dest[0] + random.uniform(-30, 30), dest[1] + random.uniform(-30, 30) + + def update(self): + self.swim_timer += 0.15 + dx = self.target_x - self.x + dy = self.target_y - self.y + dist = math.hypot(dx, dy) + + if dist < 6.0: + self.pick_new_destination() + else: + self.yaw = math.degrees(math.atan2(dy, dx)) - 90.0 + self.x += (dx / dist) * self.speed + self.y += (dy / dist) * self.speed + + def draw(self): + glColor3f(*COLOR_FROG) + glPushMatrix() + glTranslatef(self.x, self.y, self.z) + glRotatef(self.yaw, 0, 0, 1) + + leg_offset = math.sin(self.swim_timer) * 1.5 + + glBegin(GL_LINES) + glVertex3f(0.0, 0.0, 0.5) + glVertex3f(0.0, -5.0, 0.5) + glVertex3f(-2.0, 2.0, 0.5) + glVertex3f(2.0, 2.0, 0.5) + glVertex3f(-2.5, -2.0, 0.5) + glVertex3f(2.5, -2.0, 0.5) + + glVertex3f(-2.0, 2.0, 0.5) + glVertex3f(0.0, 0.0, 0.5) + glVertex3f(2.0, 2.0, 0.5) + glVertex3f(0.0, 0.0, 0.5) + + glVertex3f(-2.0, 2.0, 0.6) + glVertex3f(2.0, 2.0, 0.6) + glVertex3f(-2.0, 2.0, 0.6) + glVertex3f(0.0, 4.5, 0.6) + glVertex3f(2.0, 2.0, 0.6) + glVertex3f(0.0, 4.5, 0.6) + + glVertex3f(-1.5, 3.5, 1.5) + glVertex3f(-1.5, 3.5, 0.6) + glVertex3f(1.5, 3.5, 1.5) + glVertex3f(1.5, 3.5, 0.6) + + glVertex3f(-2.0, 2.0, 0.5) + glVertex3f(-4.0, 3.0, 0.0) + glVertex3f(-4.0, 3.0, 0.0) + glVertex3f(-3.5, 4.0, 0.0) + + glVertex3f(2.0, 2.0, 0.5) + glVertex3f(4.0, 3.0, 0.0) + glVertex3f(4.0, 3.0, 0.0) + glVertex3f(3.5, 4.0, 0.0) + + # Patas traseras con los pies mirando hacia el exterior (más realismo)[cite: 4] + glVertex3f(-2.5, -2.0, 0.5) + glVertex3f(-5.0 - leg_offset, -4.0, 0.0) + glVertex3f(-5.0 - leg_offset, -4.0, 0.0) + glVertex3f(-6.5 - leg_offset, -3.2, 0.0) + + glVertex3f(2.5, -2.0, 0.5) + glVertex3f(5.0 + leg_offset, -4.0, 0.0) + glVertex3f(5.0 + leg_offset, -4.0, 0.0) + glVertex3f(6.5 + leg_offset, -3.2, 0.0) + glEnd() + + glPopMatrix() + +# ========================================== +# RENDERIZADO DE ESCENARIO Y MODELOS +# ========================================== +def draw_water_surface(time_counter): + glColor3f(*COLOR_WATER) + + grid_size = 15 + for x in range(int(-POND_RADIUS), int(POND_RADIUS), grid_size * 2): + glBegin(GL_LINE_STRIP) + for y in range(int(-POND_RADIUS), int(POND_RADIUS), grid_size): + dist = math.hypot(x, y) + if dist < POND_RADIUS - 30: + z = 1.0 * math.sin(dist * 0.015 - time_counter * 3.0) + 0.5 * math.cos(x * 0.01 + time_counter) + glVertex3f(x, y, z) + glEnd() + + for y in range(int(STREAM_MIN_Y), int(STREAM_MAX_Y), 15): + if abs(y) < POND_RADIUS: + continue + cx = get_river_center(y) + glBegin(GL_LINE_STRIP) + for dx in range(-100, 110, 10): + x = cx + dx + z = 0.5 * math.sin(y * 0.03 + dx * 0.08 + time_counter * 4.0) + glVertex3f(x, y, z) + glEnd() + +def draw_terrain(): + glColor3f(*COLOR_LAND) + + grid_step = 120 + for y in range(int(STREAM_MIN_Y - 300), int(STREAM_MAX_Y + 300), grid_step): + glBegin(GL_LINE_STRIP) + for x in range(-2500, 2500, grid_step): + z = 1.5 * math.sin(x * 0.005) + 1.5 * math.cos(y * 0.005) + glVertex3f(x, y, z) + glEnd() + + width_river = 100.0 + margin_land = 70.0 + glColor3f(*COLOR_LAND_RIVER) + + for y in range(int(STREAM_MIN_Y), int(STREAM_MAX_Y), 30): + if abs(y) < POND_RADIUS: + continue + cx = get_river_center(y) + + glBegin(GL_LINE_STRIP) + for dx in range(int(-width_river - margin_land), int(-width_river), 15): + x = cx + dx + z = 0.6 * math.sin(y * 0.02 + x * 0.02) + 0.3 + glVertex3f(x, y, z) + glEnd() + + glBegin(GL_LINE_STRIP) + for dx in range(int(width_river), int(width_river + margin_land), 15): + x = cx + dx + z = 0.6 * math.sin(y * 0.02 + x * 0.02) + 0.3 + glVertex3f(x, y, z) + glEnd() + + for ix, iy, ir in ISLANDS: + glColor3f(*COLOR_ISLAND) + for layer in range(1, 4): + rad_layer = ir * (1 - layer * 0.25) + height = layer * 1.2 + glBegin(GL_LINE_LOOP) + for i in range(16): + angle = 2.0 * math.pi * i / 16 + dx = rad_layer * math.cos(angle) + dy = rad_layer * math.sin(angle) + glVertex3f(ix + dx, iy + dy, height) + glEnd() + +def draw_map(time_counter): + draw_terrain() + draw_water_surface(time_counter) + + offsets = [(-1.5, 0.6), (1.3, -0.6), (-0.7, -1.4), (1.1, 1.2), (0.0, 1.6), (-1.2, 0.0)] + + for rx, ry, rz, rh in REEDS: + glColor3f(*COLOR_REED) + glBegin(GL_LINES) + glVertex3f(rx, ry, rz) + glVertex3f(rx, ry, rz + rh) + for ox, oy in offsets: + glVertex3f(rx, ry, rz) + glVertex3f(rx + ox, ry + oy, rz + rh * 0.85) + glEnd() + + glColor3f(*COLOR_CATTAIL) + glBegin(GL_LINES) + for d in [-0.2, 0.0, 0.2]: + glVertex3f(rx + d, ry, rz + rh - 4.5) + glVertex3f(rx + d, ry, rz + rh) + for ox, oy in offsets: + for d in [-0.15, 0.15]: + glVertex3f(rx + ox + d, ry + oy, (rz + rh * 0.85) - 3.8) + glVertex3f(rx + ox + d, ry + oy, rz + rh * 0.85) + glEnd() + +def draw_dragonfly(wing_angle, caught_insect=None): + glColor3f(*COLOR_DRAGONFLY) + + glBegin(GL_LINES) + glVertex3f(0, 1.5, 0) + glVertex3f(0, -3.0, 0) + glVertex3f(-0.4, 0.6, 0) + glVertex3f(0.4, 0.6, 0) + glVertex3f(-0.3, -0.6, 0) + glVertex3f(0.3, -0.6, 0) + + glVertex3f(-0.3, 0.5, 0) + glVertex3f(-0.2, 0.2, -0.6) + glVertex3f(0.3, 0.5, 0) + glVertex3f(0.2, 0.2, -0.6) + glVertex3f(-0.2, -0.2, 0) + glVertex3f(-0.1, -0.3, -0.6) + glVertex3f(0.2, -0.2, 0) + glVertex3f(0.1, -0.3, -0.6) + glEnd() + + glBegin(GL_LINE_LOOP) + for i in range(8): + angle = 2.0 * math.pi * i / 8 + glVertex3f(0.3 * math.cos(angle), 1.4 + 0.3 * math.sin(angle), 0) + glEnd() + + rad_wing = math.radians(wing_angle) + w_z = math.sin(rad_wing) * 1.2 if wing_angle != 0 else 0.0 + + # Par de alas delanteras + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(-4.5, 1.0, w_z) + glVertex3f(-3.8, 0.2, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(4.5, 1.0, w_z) + glVertex3f(3.8, 0.2, w_z) + glEnd() + + # Par de alas traseras + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(-3.8, -0.2, w_z) + glVertex3f(-3.0, -0.8, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(3.8, -0.2, w_z) + glVertex3f(3.0, -0.8, w_z) + glEnd() + + if caught_insect: + glPushMatrix() + glTranslatef(0.0, 0.2, -0.6) + caught_insect.draw(attached=True) + glPopMatrix() + +# ========================================== +# INTERFAZ 2D / HUD Y PANTALLA INICIAL +# ========================================== +def draw_text_2d(text_surface, x, y): + text_data = pygame.image.tostring(text_surface, "RGBA", True) + w, h = text_surface.get_size() + + glEnable(GL_BLEND) + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) + + glRasterPos2i(x, y) + glDrawPixels(w, h, GL_RGBA, GL_UNSIGNED_BYTE, text_data) + glDisable(GL_BLEND) + +def draw_start_screen(font_title, font_subtitle, font_text, wing_angle): + glDisable(GL_DEPTH_TEST) + + glMatrixMode(GL_PROJECTION) + glPushMatrix() + glLoadIdentity() + gluOrtho2D(0, WIDTH, 0, HEIGHT) + + glMatrixMode(GL_MODELVIEW) + glPushMatrix() + glLoadIdentity() + + # TÍTULO PRINCIPAL + txt_title = font_title.render("LIBÉLULA", True, (0, 220, 255)) + draw_text_2d(txt_title, WIDTH // 2 - 130, HEIGHT - 100) + + # DIBUJO ANIMADO DE LA LIBÉLULA DE 4 ALAS EN LA PANTALLA INICIAL + glPushMatrix() + glTranslatef(WIDTH // 2 + 150, HEIGHT - 75, 0.0) + glScalef(12.0, 12.0, 1.0) + + glColor3f(*COLOR_DRAGONFLY) + glBegin(GL_LINES) + glVertex3f(0, 1.5, 0) + glVertex3f(0, -3.0, 0) + glVertex3f(-0.4, 0.6, 0) + glVertex3f(0.4, 0.6, 0) + glEnd() + + rad_w = math.radians(wing_angle) + w_off = math.sin(rad_w) * 1.5 + + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(-4.5, 1.0, w_off) + glVertex3f(-3.8, 0.2, w_off) + glEnd() + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(4.5, 1.0, w_off) + glVertex3f(3.8, 0.2, w_off) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(-3.8, -0.2, w_off) + glVertex3f(-3.0, -0.8, w_off) + glEnd() + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(3.8, -0.2, w_off) + glVertex3f(3.0, -0.8, w_off) + glEnd() + + glPopMatrix() + + # OBJETIVO DEL JUEGO + obj_line1 = font_text.render("Objetivo: Vuela libre por el estanque y sus arroyos capturando moscas", True, (255, 255, 255)) + obj_line2 = font_text.render("y mosquitos. Una vez cazados debes devorarlos posado en un junco.", True, (255, 255, 255)) + draw_text_2d(obj_line1, 70, HEIGHT - 170) + draw_text_2d(obj_line2, 70, HEIGHT - 195) + + # CONTROLES Y TECLAS + controls = [ + "CONTROLES:", + "W / S : Acelerar / Frenar", + "Flecha Arriba / Abajo : Cambiar altitud", + "Flecha Izquierda / Derecha : Girar / Virar", + "ESPACIO : Picado rápido para caza", + "P : Aterrizaje automático en junco cercano", + "ESC : Salir del juego" + ] + + y_pos = HEIGHT - 250 + for ctrl in controls: + is_title = ctrl.endswith(":") + col = (255, 220, 50) if is_title else (200, 200, 200) + t_surf = font_subtitle.render(ctrl, True, col) if is_title else font_text.render(ctrl, True, col) + draw_text_2d(t_surf, 70, y_pos) + y_pos -= 25 + + # LEYENDA VISUAL INFERIOR + y_icon = 90 + + glColor3f(*COLOR_MOSQUITO) + glBegin(GL_LINES) + glVertex2f(100, y_icon + 5) + glVertex2f(100, y_icon - 5) + glVertex2f(95, y_icon) + glVertex2f(105, y_icon) + glEnd() + txt_m = font_text.render("Mosquito", True, (255, 230, 0)) + draw_text_2d(txt_m, 120, y_icon - 8) + + glColor3f(*COLOR_FLY) + glBegin(GL_LINES) + glVertex2f(280, y_icon + 5) + glVertex2f(280, y_icon - 5) + glVertex2f(275, y_icon - 3) + glVertex2f(285, y_icon + 3) + glVertex2f(275, y_icon + 3) + glVertex2f(285, y_icon - 3) + glEnd() + txt_f = font_text.render("Mosca", True, (200, 65, 40)) + draw_text_2d(txt_f, 300, y_icon - 8) + + glColor3f(*COLOR_REED) + glBegin(GL_LINES) + glVertex2f(450, y_icon - 10) + glVertex2f(450, y_icon + 15) + glEnd() + glColor3f(*COLOR_CATTAIL) + glBegin(GL_LINES) + glVertex2f(450, y_icon + 8) + glVertex2f(450, y_icon + 15) + glEnd() + txt_r = font_text.render("Junco", True, (100, 220, 100)) + draw_text_2d(txt_r, 470, y_icon - 8) + + start_msg = font_subtitle.render("Pulsa INTRO o cualquier tecla para comenzar...", True, (100, 255, 100)) + draw_text_2d(start_msg, WIDTH // 2 - 200, 30) + + glMatrixMode(GL_PROJECTION) + glPopMatrix() + glMatrixMode(GL_MODELVIEW) + glPopMatrix() + glEnable(GL_DEPTH_TEST) + +def draw_hud(player_pos, yaw, insects, eaten_mosquitos, eaten_flies, font): + glDisable(GL_DEPTH_TEST) + + glMatrixMode(GL_PROJECTION) + glPushMatrix() + glLoadIdentity() + gluOrtho2D(0, WIDTH, 0, HEIGHT) + + glMatrixMode(GL_MODELVIEW) + glPushMatrix() + glLoadIdentity() + + px, py, pz = player_pos + + cx, cy = WIDTH // 2, HEIGHT - 35 + radius = 22 + + glColor3f(*COLOR_HUD) + glBegin(GL_LINE_LOOP) + for i in range(24): + angle = 2.0 * math.pi * i / 24 + glVertex2f(cx + radius * math.cos(angle), cy + radius * math.sin(angle)) + glEnd() + + glBegin(GL_LINES) + glVertex2f(cx, cy + radius) + glVertex2f(cx, cy + radius - 5) + glVertex2f(cx, cy - radius) + glVertex2f(cx, cy - radius + 5) + glVertex2f(cx - radius, cy) + glVertex2f(cx - radius + 5, cy) + glVertex2f(cx + radius, cy) + glVertex2f(cx + radius - 5, cy) + glEnd() + + closest_insect = None + min_dist = 999999.0 + relative_angle = 0.0 + diff_z = 0.0 + + for insect in insects: + dx = insect.x - px + dy = insect.y - py + dist = math.hypot(dx, dy) + + if dist < min_dist and dist > 0.1: + angle_to_insect = math.degrees(math.atan2(dy, dx)) + heading = (angle_to_insect - (90 - yaw)) % 360 + if heading > 180: + heading -= 360 + + if abs(heading) < 60: + min_dist = dist + closest_insect = insect + relative_angle = heading + diff_z = insect.z - pz + + if closest_insect: + radar_x = cx - (relative_angle / 60.0) * (radius - 4) + radar_y = cy + (diff_z / 20.0) * (radius - 4) + + dist_from_center = math.hypot(radar_x - cx, radar_y - cy) + if dist_from_center > (radius - 3): + factor = (radius - 3) / dist_from_center + radar_x = cx + (radar_x - cx) * factor + radar_y = cy + (radar_y - cy) * factor + + glColor3f(1.0, 1.0, 1.0) + glBegin(GL_LINE_LOOP) + for i in range(8): + ang = 2.0 * math.pi * i / 8 + glVertex2f(radar_x + 3 * math.cos(ang), radar_y + 3 * math.sin(ang)) + glEnd() + + glColor3f(*COLOR_MOSQUITO) + glBegin(GL_LINES) + glVertex2f(25, HEIGHT - 33) + glVertex2f(25, HEIGHT - 27) + glVertex2f(21, HEIGHT - 30) + glVertex2f(29, HEIGHT - 30) + glEnd() + + glColor3f(*COLOR_FLY) + glBegin(GL_LINES) + glVertex2f(WIDTH - 82, HEIGHT - 33) + glVertex2f(WIDTH - 82, HEIGHT - 27) + glVertex2f(WIDTH - 86, HEIGHT - 28) + glVertex2f(WIDTH - 78, HEIGHT - 32) + glVertex2f(WIDTH - 86, HEIGHT - 32) + glVertex2f(WIDTH - 78, HEIGHT - 28) + glEnd() + + txt_mosq = font.render(f"x {eaten_mosquitos}", True, (255, 230, 0)) + txt_flies = font.render(f"x {eaten_flies}", True, (200, 65, 40)) + + draw_text_2d(txt_mosq, 38, HEIGHT - 40) + draw_text_2d(txt_flies, WIDTH - 68, HEIGHT - 40) + + glMatrixMode(GL_PROJECTION) + glPopMatrix() + glMatrixMode(GL_MODELVIEW) + glPopMatrix() + glEnable(GL_DEPTH_TEST) + +# ========================================== +# BUCLE PRINCIPAL +# ========================================== +def main(): + pygame.init() + pygame.font.init() + font = pygame.font.SysFont("Arial", 22, bold=True) + font_title = pygame.font.SysFont("Arial", 48, bold=True) + font_subtitle = pygame.font.SysFont("Arial", 20, bold=True) + font_text = pygame.font.SysFont("Arial", 16, bold=False) + + pygame.display.set_mode((WIDTH, HEIGHT), DOUBLEBUF | OPENGL) + pygame.display.set_caption("Libélula") + + glEnable(GL_DEPTH_TEST) + glClearColor(*COLOR_BG, 1.0) + + glMatrixMode(GL_PROJECTION) + gluPerspective(50, (WIDTH / HEIGHT), 0.1, 5000.0) + glMatrixMode(GL_MODELVIEW) + + px, py, pz = 0.0, 0.0, 15.0 + yaw = 0.0 + roll = 0.0 + pitch = 0.0 + max_roll = 30.0 + max_pitch = 20.0 + + speed = 0.0 + max_speed = 3.0 + acceleration = 0.06 + deceleration = 0.03 + + min_altitude = 1.0 + max_altitude = 120.0 + + wing_timer = 0.0 + water_timer = 0.0 + is_perched = False + + dive_phase = "none" + pre_dive_alt = 15.0 + pre_dive_speed = 0.0 + + auto_landing = False + target_perch = None + + caught_insect = None + eaten_mosquitos = 0 + eaten_flies = 0 + + frog = Frog() + + insects = [] + num_mosquitos = random.randint(MOSQUITO_MIN, MOSQUITO_MAX) + num_flies = random.randint(FLY_MIN, FLY_MAX) + + for _ in range(num_mosquitos): + insects.append(Insect("mosquito")) + for _ in range(num_flies): + insects.append(Insect("fly")) + + clock = pygame.time.Clock() + + in_start_screen = True + running = True + + while running: + dt = clock.tick(60) / 1000.0 + water_timer += dt + wing_timer += 60.0 + + for event in pygame.event.get(): + if event.type == QUIT or (event.type == KEYDOWN and event.key == K_ESCAPE): + running = False + elif event.type == KEYDOWN: + if in_start_screen: + in_start_screen = False + else: + if event.key == K_SPACE: + if not is_perched and dive_phase == "none": + dive_phase = "diving" + pre_dive_alt = pz + pre_dive_speed = speed + auto_landing = False + elif event.key == K_p: + if not is_perched and dive_phase == "none": + best_reed = None + min_d = 999.0 + for rx, ry, rz, rh in REEDS: + dist = math.hypot(px - rx, py - ry) + if dist < 35.0: + angle_to_reed = math.degrees(math.atan2(ry - py, rx - px)) + heading = (angle_to_reed - (90 - yaw)) % 360 + if heading > 180: + heading -= 360 + if abs(heading) < 45 and dist < min_d: + min_d = dist + best_reed = (rx, ry, rz + rh) + + if best_reed: + target_perch = best_reed + auto_landing = True + + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) + glLoadIdentity() + + if in_start_screen: + cam_x, cam_y, cam_z = 0.0, -100.0, 30.0 + gluLookAt(cam_x, cam_y, cam_z, 0, 0, 15, 0, 0, 1) + draw_map(water_timer) + frog.draw() + + wing_angle = math.sin(wing_timer * 0.5) * 35.0 + draw_start_screen(font_title, font_subtitle, font_text, wing_angle) + else: + keys = pygame.key.get_pressed() + + if is_perched: + if keys[K_LEFT]: + yaw += 3.0 + if keys[K_RIGHT]: + yaw -= 3.0 + + if keys[K_w]: + is_perched = False + speed = 0.5 + + wing_angle = 0.0 + pitch = 0.0 + roll = 0.0 + dive_phase = "none" + auto_landing = False + else: + if auto_landing and target_perch: + tx, ty, tz = target_perch + dx = tx - px + dy = ty - py + dist_to_target = math.hypot(dx, dy) + + speed = 1.2 + px += (dx / dist_to_target) * speed if dist_to_target > 0.1 else 0 + py += (dy / dist_to_target) * speed if dist_to_target > 0.1 else 0 + pz += (tz - pz) * 0.1 + + if dist_to_target < 2.5 and abs(pz - tz) < 2.5: + px, py, pz = target_perch + speed = 0.0 + roll = 0.0 + pitch = 0.0 + is_perched = True + auto_landing = False + target_perch = None + + if caught_insect: + if caught_insect.type == "mosquito": + eaten_mosquitos += 1 + else: + eaten_flies += 1 + caught_insect = None + + elif dive_phase == "diving": + speed = max_speed * 1.5 + pz -= 1.2 + target_pitch = -45.0 + + if pz <= min_altitude: + pz = min_altitude + dive_phase = "recovering" + + elif dive_phase == "recovering": + target_pitch = 30.0 + pz += 0.8 + speed += (pre_dive_speed - speed) * 0.1 + + if pz >= pre_dive_alt: + pz = pre_dive_alt + speed = pre_dive_speed + dive_phase = "none" + else: + if keys[K_w]: + speed = min(speed + acceleration, max_speed) + elif keys[K_s]: + speed = max(speed - deceleration, 0.0) + + if speed < 0.3: + for rx, ry, rz, rh in REEDS: + dist = math.hypot(px - rx, py - ry) + if dist < 10.0 and abs(pz - (rz + rh)) < 10.0: + px, py, pz = rx, ry, rz + rh + speed = 0.0 + roll = 0.0 + pitch = 0.0 + is_perched = True + + if caught_insect: + if caught_insect.type == "mosquito": + eaten_mosquitos += 1 + else: + eaten_flies += 1 + caught_insect = None + break + + target_pitch = 0.0 + if keys[K_UP]: + pz = min(pz + 0.8, max_altitude) + target_pitch = max_pitch + elif keys[K_DOWN]: + pz = max(pz - 0.8, min_altitude) + target_pitch = -max_pitch + + if not auto_landing: + pitch += (target_pitch - pitch) * 0.15 + + target_roll = 0.0 + if keys[K_LEFT]: + yaw += 2.8 + target_roll = -max_roll + elif keys[K_RIGHT]: + yaw -= 2.8 + target_roll = max_roll + + roll += (target_roll - roll) * 0.15 + + rad_yaw = math.radians(yaw) + px += -math.sin(rad_yaw) * speed + py += math.cos(rad_yaw) * speed + + if abs(py) < POND_RADIUS: + max_limit_x = POND_RADIUS + 200.0 + else: + cx = get_river_center(py) + max_limit_x = abs(cx) + 200.0 + + px = max(-max_limit_x, min(max_limit_x, px)) + + if py > STREAM_MAX_Y: + py -= WORLD_LEN + elif py < STREAM_MIN_Y: + py += WORLD_LEN + + wing_timer += 80.0 if (dive_phase != "none" or auto_landing) else (60.0 if speed > 0.1 else 30.0) + wing_angle = math.sin(wing_timer) * 35.0 + + frog.update() + + current_mosquitos = 0 + current_flies = 0 + + for insect in insects[:]: + insect.update((px, py, pz)) + + if insect.type == "mosquito": + current_mosquitos += 1 + else: + current_flies += 1 + + if not caught_insect: + dist_to_dragonfly = math.sqrt((px - insect.x)**2 + (py - insect.y)**2 + (pz - insect.z)**2) + if dist_to_dragonfly < 6.0: + caught_insect = insect + insects.remove(insect) + + if dive_phase == "diving": + dive_phase = "recovering" + + if current_mosquitos < MOSQUITO_MIN: + insects.append(Insect("mosquito")) + if current_flies < FLY_MIN: + insects.append(Insect("fly")) + + cam_dist = 18.0 + cam_height = 8.0 + rad_yaw = math.radians(yaw) + + cam_x = px + math.sin(rad_yaw) * cam_dist + cam_y = py - math.cos(rad_yaw) * cam_dist + cam_z = pz + cam_height + + gluLookAt(cam_x, cam_y, cam_z, + px, py, pz + 0.5, + 0, 0, 1) + + draw_map(water_timer) + frog.draw() + + for insect in insects: + insect.draw() + + glPushMatrix() + glTranslatef(px, py, pz) + glRotatef(yaw, 0, 0, 1) + glRotatef(pitch, 1, 0, 0) + glRotatef(roll, 0, 1, 0) + draw_dragonfly(wing_angle, caught_insect) + glPopMatrix() + + draw_hud((px, py, pz), yaw, insects, eaten_mosquitos, eaten_flies, font) + + pygame.display.flip() + + pygame.quit() + sys.exit() + +if __name__ == "__main__": + main() diff --git a/previos/libelula_v9.0.py b/previos/libelula_v9.0.py new file mode 100644 index 0000000..0073f8a --- /dev/null +++ b/previos/libelula_v9.0.py @@ -0,0 +1,1139 @@ +import sys +import math +import random +import struct +import pygame +from pygame.locals import * +from OpenGL.GL import * +from OpenGL.GLU import * + +# ========================================== +# CONFIGURACIÓN Y CONSTANTES +# ========================================== +WIDTH, HEIGHT = 800, 600 + +# Colores (R, G, B) +COLOR_BG = (0.12, 0.15, 0.12) # Fondo ambiente +COLOR_WATER = (0.0, 0.5, 0.85) # Agua animada (Cian/Azul) +COLOR_LAND = (0.45, 0.32, 0.15) # Tierra / Relieve +COLOR_LAND_RIVER = (0.40, 0.28, 0.12) # Tierra en riachuelos +COLOR_ISLAND = (0.5, 0.38, 0.18) # Islas +COLOR_REED = (0.15, 0.85, 0.25) # Juncos verdes +COLOR_CATTAIL = (0.35, 0.18, 0.08) # Espigas marrones densas +COLOR_DRAGONFLY = (0.0, 1.0, 1.0) # Libélula del jugador (Cian) +COLOR_PURPLE_DRAGONFLY = (0.7, 0.2, 0.8) # Libélulas decorativas (Morado) +COLOR_MOSQUITO = (1.0, 0.9, 0.0) # Mosquitos (Amarillo) +COLOR_FLY = (0.8, 0.25, 0.15) # Moscas (Rojo-Marrón) +COLOR_FROG = (0.15, 0.85, 0.35) # Rana verde detallada +COLOR_HUD = (1.0, 1.0, 1.0) # Retícula HUD (Blanco) + +# Dimensiones del Mundo +POND_RADIUS = 1800.0 +STREAM_LEN = 900.0 +STREAM_MIN_Y = -(POND_RADIUS + STREAM_LEN) +STREAM_MAX_Y = (POND_RADIUS + STREAM_LEN) +WORLD_LEN = STREAM_MAX_Y - STREAM_MIN_Y + +# Cantidades de insectos +MOSQUITO_MIN, MOSQUITO_MAX = 17, 27 +FLY_MIN, FLY_MAX = 9, 14 + +# ========================================== +# ELEMENTOS DEL MAPA +# ========================================== +REEDS = [ + (-250, 350, 0, 20), (-160, -500, 0, 22), (500, 200, 0, 24), (-700, 200, 0, 21), (200, -800, 0, 23), + (-900, -300, 0, 25), (760, -600, 0, 22), (900, 500, 0, 26), (-500, 900, 0, 23), (600, -1000, 0, 22), + (-1200, 400, 0, 24), (1300, -300, 0, 20), (-1400, -700, 0, 25), (1100, 1200, 0, 23), (-360, -1300, 0, 22), + (-90, -1950, 0, 20), (-110, -2150, 0, 23), (-80, -2350, 0, 19), (-100, -2550, 0, 22), + (90, 1950, 0, 22), (110, 2150, 0, 20), (85, 2350, 0, 24), (105, 2550, 0, 21) +] + +ISLANDS = [ + (-250, 350, 90), (-160, -500, 85), (500, 200, 100), (-700, 200, 110), (200, -800, 95), + (-900, -300, 120), (760, -600, 90), (900, 500, 105), (-500, 900, 115), (600, -1000, 100) +] + +def get_river_center(y): + if y < -POND_RADIUS: + dy = y + POND_RADIUS + return -50 + 100 * math.sin(dy * 0.008) + elif y > POND_RADIUS: + dy = y - POND_RADIUS + return 50 + 100 * math.sin(dy * 0.008) + else: + return (y / POND_RADIUS) * 50.0 + +def is_over_water(x, y): + dist_center = math.hypot(x, y) + if dist_center < POND_RADIUS - 50: + return True + if abs(y) >= POND_RADIUS: + cx = get_river_center(y) + if abs(x - cx) < 80: + return True + return False + +def get_random_water_position(): + while True: + if random.random() < 0.7: + rad = random.uniform(0, math.pi * 2) + r = random.uniform(100, POND_RADIUS - 150) + x = r * math.cos(rad) + y = r * math.sin(rad) + else: + y = random.choice([random.uniform(POND_RADIUS, STREAM_MAX_Y), random.uniform(STREAM_MIN_Y, -POND_RADIUS)]) + cx = get_river_center(y) + x = cx + random.uniform(-60, 60) + + if is_over_water(x, y): + z = random.uniform(5.0, 35.0) + return x, y, z + +# ========================================== +# GENERADOR DE AUDIO PROCEDURAL (SIN ARCHIVOS) +# ========================================== +def generate_buzz_sound(): + try: + sample_rate = 22050 + duration = 0.3 + num_samples = int(sample_rate * duration) + buffer = bytearray() + for i in range(num_samples): + t = i / sample_rate + val = int(32767 * 0.25 * math.sin(2 * math.pi * 180 * t + math.sin(2 * math.pi * 25 * t) * 3)) + buffer.extend(struct.pack(' STREAM_MAX_Y: + self.y -= WORLD_LEN + elif self.y < STREAM_MIN_Y: + self.y += WORLD_LEN + + def draw(self, attached=False): + if self.type == "mosquito": + glColor3f(*COLOR_MOSQUITO) + else: + glColor3f(*COLOR_FLY) + + glPushMatrix() + if not attached: + glTranslatef(self.x, self.y, self.z) + glRotatef(self.yaw, 0, 0, 1) + + s = self.size + glBegin(GL_LINES) + glVertex3f(0, s, 0) + glVertex3f(0, -s, 0) + w_offset = 0.0 if attached else math.sin(self.timer * 30) * (s * 0.8) + glVertex3f(0, 0, 0) + glVertex3f(-s * 1.5, 0, w_offset) + glVertex3f(0, 0, 0) + glVertex3f(s * 1.5, 0, w_offset) + glEnd() + + glPopMatrix() + +class PurpleDragonfly: + def __init__(self, behavior_type): + self.behavior = behavior_type # 1: Círculos, 2: Patrulla lineal, 3: Zig-zag errático + self.x, self.y, self.z = get_random_water_position() + self.yaw = random.uniform(0, 360) + self.timer = random.uniform(0, 100) + self.wing_timer = random.uniform(0, 100) + self.is_perched = False + self.perch_timer = 0 + + def update(self): + self.wing_timer += 60.0 + if self.is_perched: + self.perch_timer -= 1 + if self.perch_timer <= 0: + self.is_perched = False + return + + self.timer += 0.05 + if self.behavior == 1: + self.yaw += 1.2 + rad = math.radians(self.yaw) + self.x += -math.sin(rad) * 1.1 + self.y += math.cos(rad) * 1.1 + elif self.behavior == 2: + if random.random() < 0.02: + self.yaw += random.uniform(-40, 40) + rad = math.radians(self.yaw) + self.x += -math.sin(rad) * 1.3 + self.y += math.cos(rad) * 1.3 + else: + self.yaw += math.sin(self.timer * 3) * 3.5 + rad = math.radians(self.yaw) + self.x += -math.sin(rad) * 1.4 + self.y += math.cos(rad) * 1.4 + + self.z = 15.0 + math.sin(self.timer * 2) * 6.0 + + # Posibilidad de posarse en un junco de vez en cuando + if random.random() < 0.0015 and REEDS: + reed = random.choice(REEDS) + self.x, self.y, self.z = reed[0], reed[1], reed[2] + reed[3] + self.is_perched = True + self.perch_timer = random.randint(300, 600) + + if not is_over_water(self.x, self.y): + self.yaw += 150.0 + + if self.y > STREAM_MAX_Y: + self.y -= WORLD_LEN + elif self.y < STREAM_MIN_Y: + self.y += WORLD_LEN + + def draw(self): + glColor3f(*COLOR_PURPLE_DRAGONFLY) + glPushMatrix() + glTranslatef(self.x, self.y, self.z) + glRotatef(self.yaw, 0, 0, 1) + + glBegin(GL_LINES) + glVertex3f(0, 1.5, 0) + glVertex3f(0, -3.0, 0) + glVertex3f(-0.4, 0.6, 0) + glVertex3f(0.4, 0.6, 0) + glVertex3f(-0.3, -0.6, 0) + glVertex3f(0.3, -0.6, 0) + glEnd() + + glBegin(GL_LINE_LOOP) + for i in range(8): + angle = 2.0 * math.pi * i / 8 + glVertex3f(0.3 * math.cos(angle), 1.4 + 0.3 * math.sin(angle), 0) + glEnd() + + wing_angle = 0.0 if self.is_perched else math.sin(self.wing_timer) * 35.0 + rad_wing = math.radians(wing_angle) + w_z = math.sin(rad_wing) * 1.2 if wing_angle != 0 else 0.0 + + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(-4.5, 1.0, w_z) + glVertex3f(-3.8, 0.2, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(4.5, 1.0, w_z) + glVertex3f(3.8, 0.2, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(-3.8, -0.2, w_z) + glVertex3f(-3.0, -0.8, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(3.8, -0.2, w_z) + glVertex3f(3.0, -0.8, w_z) + glEnd() + + glPopMatrix() + +class Frog: + def __init__(self): + self.pick_new_destination() + self.x, self.y = self.target_x, self.target_y + self.z = 0.6 + self.speed = 0.35 + self.yaw = 0.0 + self.swim_timer = 0.0 + + def pick_new_destination(self): + destinations = [(rx, ry) for rx, ry, _, _ in REEDS] + [(ix, iy) for ix, iy, _ in ISLANDS] + dest = random.choice(destinations) + self.target_x, self.target_y = dest[0] + random.uniform(-30, 30), dest[1] + random.uniform(-30, 30) + + def update(self): + self.swim_timer += 0.15 + dx = self.target_x - self.x + dy = self.target_y - self.y + dist = math.hypot(dx, dy) + + if dist < 6.0: + self.pick_new_destination() + else: + self.yaw = math.degrees(math.atan2(dy, dx)) - 90.0 + self.x += (dx / dist) * self.speed + self.y += (dy / dist) * self.speed + + def draw(self): + glColor3f(*COLOR_FROG) + glPushMatrix() + glTranslatef(self.x, self.y, self.z) + glRotatef(self.yaw, 0, 0, 1) + + leg_offset = math.sin(self.swim_timer) * 1.5 + + glBegin(GL_LINES) + glVertex3f(0.0, 0.0, 0.5) + glVertex3f(0.0, -5.0, 0.5) + glVertex3f(-2.0, 2.0, 0.5) + glVertex3f(2.0, 2.0, 0.5) + glVertex3f(-2.5, -2.0, 0.5) + glVertex3f(2.5, -2.0, 0.5) + + glVertex3f(-2.0, 2.0, 0.5) + glVertex3f(0.0, 0.0, 0.5) + glVertex3f(2.0, 2.0, 0.5) + glVertex3f(0.0, 0.0, 0.5) + + glVertex3f(-2.0, 2.0, 0.6) + glVertex3f(2.0, 2.0, 0.6) + glVertex3f(-2.0, 2.0, 0.6) + glVertex3f(0.0, 4.5, 0.6) + glVertex3f(2.0, 2.0, 0.6) + glVertex3f(0.0, 4.5, 0.6) + + glVertex3f(-1.5, 3.5, 1.5) + glVertex3f(-1.5, 3.5, 0.6) + glVertex3f(1.5, 3.5, 1.5) + glVertex3f(1.5, 3.5, 0.6) + + glVertex3f(-2.0, 2.0, 0.5) + glVertex3f(-4.0, 3.0, 0.0) + glVertex3f(-4.0, 3.0, 0.0) + glVertex3f(-3.5, 4.0, 0.0) + + glVertex3f(2.0, 2.0, 0.5) + glVertex3f(4.0, 3.0, 0.0) + glVertex3f(4.0, 3.0, 0.0) + glVertex3f(3.5, 4.0, 0.0) + + # Pies hacia el exterior + glVertex3f(-2.5, -2.0, 0.5) + glVertex3f(-5.0 - leg_offset, -4.0, 0.0) + glVertex3f(-5.0 - leg_offset, -4.0, 0.0) + glVertex3f(-6.5 - leg_offset, -3.2, 0.0) + + glVertex3f(2.5, -2.0, 0.5) + glVertex3f(5.0 + leg_offset, -4.0, 0.0) + glVertex3f(5.0 + leg_offset, -4.0, 0.0) + glVertex3f(6.5 + leg_offset, -3.2, 0.0) + glEnd() + + glPopMatrix() + +# ========================================== +# RENDERIZADO DE ESCENARIO Y MODELOS +# ========================================== +def draw_water_surface(time_counter): + glColor3f(*COLOR_WATER) + + grid_size = 15 + for x in range(int(-POND_RADIUS), int(POND_RADIUS), grid_size * 2): + glBegin(GL_LINE_STRIP) + for y in range(int(-POND_RADIUS), int(POND_RADIUS), grid_size): + dist = math.hypot(x, y) + if dist < POND_RADIUS - 30: + z = 1.0 * math.sin(dist * 0.015 - time_counter * 3.0) + 0.5 * math.cos(x * 0.01 + time_counter) + glVertex3f(x, y, z) + glEnd() + + for y in range(int(STREAM_MIN_Y), int(STREAM_MAX_Y), 15): + if abs(y) < POND_RADIUS: + continue + cx = get_river_center(y) + glBegin(GL_LINE_STRIP) + for dx in range(-100, 110, 10): + x = cx + dx + z = 0.5 * math.sin(y * 0.03 + dx * 0.08 + time_counter * 4.0) + glVertex3f(x, y, z) + glEnd() + +def draw_terrain(): + glColor3f(*COLOR_LAND) + + grid_step = 120 + for y in range(int(STREAM_MIN_Y - 300), int(STREAM_MAX_Y + 300), grid_step): + glBegin(GL_LINE_STRIP) + for x in range(-2500, 2500, grid_step): + z = 1.5 * math.sin(x * 0.005) + 1.5 * math.cos(y * 0.005) + glVertex3f(x, y, z) + glEnd() + + width_river = 100.0 + margin_land = 70.0 + glColor3f(*COLOR_LAND_RIVER) + + for y in range(int(STREAM_MIN_Y), int(STREAM_MAX_Y), 30): + if abs(y) < POND_RADIUS: + continue + cx = get_river_center(y) + + glBegin(GL_LINE_STRIP) + for dx in range(int(-width_river - margin_land), int(-width_river), 15): + x = cx + dx + z = 0.6 * math.sin(y * 0.02 + x * 0.02) + 0.3 + glVertex3f(x, y, z) + glEnd() + + glBegin(GL_LINE_STRIP) + for dx in range(int(width_river), int(width_river + margin_land), 15): + x = cx + dx + z = 0.6 * math.sin(y * 0.02 + x * 0.02) + 0.3 + glVertex3f(x, y, z) + glEnd() + + for ix, iy, ir in ISLANDS: + glColor3f(*COLOR_ISLAND) + for layer in range(1, 4): + rad_layer = ir * (1 - layer * 0.25) + height = layer * 1.2 + glBegin(GL_LINE_LOOP) + for i in range(16): + angle = 2.0 * math.pi * i / 16 + dx = rad_layer * math.cos(angle) + dy = rad_layer * math.sin(angle) + glVertex3f(ix + dx, iy + dy, height) + glEnd() + +def draw_map(time_counter): + draw_terrain() + draw_water_surface(time_counter) + + offsets = [(-1.5, 0.6), (1.3, -0.6), (-0.7, -1.4), (1.1, 1.2), (0.0, 1.6), (-1.2, 0.0)] + + for rx, ry, rz, rh in REEDS: + glColor3f(*COLOR_REED) + glBegin(GL_LINES) + glVertex3f(rx, ry, rz) + glVertex3f(rx, ry, rz + rh) + for ox, oy in offsets: + glVertex3f(rx, ry, rz) + glVertex3f(rx + ox, ry + oy, rz + rh * 0.85) + glEnd() + + glColor3f(*COLOR_CATTAIL) + glBegin(GL_LINES) + for d in [-0.2, 0.0, 0.2]: + glVertex3f(rx + d, ry, rz + rh - 4.5) + glVertex3f(rx + d, ry, rz + rh) + for ox, oy in offsets: + for d in [-0.15, 0.15]: + glVertex3f(rx + ox + d, ry + oy, (rz + rh * 0.85) - 3.8) + glVertex3f(rx + ox + d, ry + oy, rz + rh * 0.85) + glEnd() + +def draw_dragonfly(wing_angle, caught_insect=None): + glColor3f(*COLOR_DRAGONFLY) + + glBegin(GL_LINES) + glVertex3f(0, 1.5, 0) + glVertex3f(0, -3.0, 0) + glVertex3f(-0.4, 0.6, 0) + glVertex3f(0.4, 0.6, 0) + glVertex3f(-0.3, -0.6, 0) + glVertex3f(0.3, -0.6, 0) + + glVertex3f(-0.3, 0.5, 0) + glVertex3f(-0.2, 0.2, -0.6) + glVertex3f(0.3, 0.5, 0) + glVertex3f(0.2, 0.2, -0.6) + glVertex3f(-0.2, -0.2, 0) + glVertex3f(-0.1, -0.3, -0.6) + glVertex3f(0.2, -0.2, 0) + glVertex3f(0.1, -0.3, -0.6) + glEnd() + + glBegin(GL_LINE_LOOP) + for i in range(8): + angle = 2.0 * math.pi * i / 8 + glVertex3f(0.3 * math.cos(angle), 1.4 + 0.3 * math.sin(angle), 0) + glEnd() + + rad_wing = math.radians(wing_angle) + w_z = math.sin(rad_wing) * 1.2 if wing_angle != 0 else 0.0 + + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(-4.5, 1.0, w_z) + glVertex3f(-3.8, 0.2, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(4.5, 1.0, w_z) + glVertex3f(3.8, 0.2, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(-3.8, -0.2, w_z) + glVertex3f(-3.0, -0.8, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(3.8, -0.2, w_z) + glVertex3f(3.0, -0.8, w_z) + glEnd() + + if caught_insect: + glPushMatrix() + glTranslatef(0.0, 0.2, -0.6) + caught_insect.draw(attached=True) + glPopMatrix() + +# ========================================== +# INTERFAZ 2D / HUD Y PANTALLA INICIAL +# ========================================== +def draw_text_2d(text_surface, x, y): + text_data = pygame.image.tostring(text_surface, "RGBA", True) + w, h = text_surface.get_size() + + glEnable(GL_BLEND) + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) + + glRasterPos2i(x, y) + glDrawPixels(w, h, GL_RGBA, GL_UNSIGNED_BYTE, text_data) + glDisable(GL_BLEND) + +def draw_start_screen(font_title, font_subtitle, font_text, wing_angle): + glDisable(GL_DEPTH_TEST) + + glMatrixMode(GL_PROJECTION) + glPushMatrix() + glLoadIdentity() + gluOrtho2D(0, WIDTH, 0, HEIGHT) + + glMatrixMode(GL_MODELVIEW) + glPushMatrix() + glLoadIdentity() + + txt_title = font_title.render("LIBÉLULA", True, (0, 220, 255)) + draw_text_2d(txt_title, WIDTH // 2 - 130, HEIGHT - 100) + + glPushMatrix() + glTranslatef(WIDTH // 2 + 150, HEIGHT - 75, 0.0) + glScalef(12.0, 12.0, 1.0) + + glColor3f(*COLOR_DRAGONFLY) + glBegin(GL_LINES) + glVertex3f(0, 1.5, 0) + glVertex3f(0, -3.0, 0) + glVertex3f(-0.4, 0.6, 0) + glVertex3f(0.4, 0.6, 0) + glEnd() + + rad_w = math.radians(wing_angle) + w_off = math.sin(rad_w) * 1.5 + + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(-4.5, 1.0, w_off) + glVertex3f(-3.8, 0.2, w_off) + glEnd() + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(4.5, 1.0, w_off) + glVertex3f(3.8, 0.2, w_off) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(-3.8, -0.2, w_off) + glVertex3f(-3.0, -0.8, w_off) + glEnd() + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(3.8, -0.2, w_off) + glVertex3f(3.0, -0.8, w_off) + glEnd() + + glPopMatrix() + + obj_line1 = font_text.render("Objetivo: Vuela libre por el estanque y sus arroyos capturando moscas", True, (255, 255, 255)) + obj_line2 = font_text.render("y mosquitos. Una vez cazados debes devorarlos posado en un junco.", True, (255, 255, 255)) + draw_text_2d(obj_line1, 70, HEIGHT - 170) + draw_text_2d(obj_line2, 70, HEIGHT - 195) + + controls = [ + "CONTROLES:", + "W / S : Acelerar / Frenar", + "Flecha Arriba / Abajo : Cambiar altitud", + "Flecha Izquierda / Derecha : Girar / Virar", + "ESPACIO : Picado rápido para caza", + "P : Aterrizaje automático en junco cercano", + "ESC : Salir del juego" + ] + + y_pos = HEIGHT - 250 + for ctrl in controls: + is_title = ctrl.endswith(":") + col = (255, 220, 50) if is_title else (200, 200, 200) + t_surf = font_subtitle.render(ctrl, True, col) if is_title else font_text.render(ctrl, True, col) + draw_text_2d(t_surf, 70, y_pos) + y_pos -= 25 + + y_icon = 90 + + glColor3f(*COLOR_MOSQUITO) + glBegin(GL_LINES) + glVertex2f(100, y_icon + 5) + glVertex2f(100, y_icon - 5) + glVertex2f(95, y_icon) + glVertex2f(105, y_icon) + glEnd() + txt_m = font_text.render("Mosquito", True, (255, 230, 0)) + draw_text_2d(txt_m, 120, y_icon - 8) + + glColor3f(*COLOR_FLY) + glBegin(GL_LINES) + glVertex2f(280, y_icon + 5) + glVertex2f(280, y_icon - 5) + glVertex2f(275, y_icon - 3) + glVertex2f(285, y_icon + 3) + glVertex2f(275, y_icon + 3) + glVertex2f(285, y_icon - 3) + glEnd() + txt_f = font_text.render("Mosca", True, (200, 65, 40)) + draw_text_2d(txt_f, 300, y_icon - 8) + + glColor3f(*COLOR_REED) + glBegin(GL_LINES) + glVertex2f(450, y_icon - 10) + glVertex2f(450, y_icon + 15) + glEnd() + glColor3f(*COLOR_CATTAIL) + glBegin(GL_LINES) + glVertex2f(450, y_icon + 8) + glVertex2f(450, y_icon + 15) + glEnd() + txt_r = font_text.render("Junco", True, (100, 220, 100)) + draw_text_2d(txt_r, 470, y_icon - 8) + + start_msg = font_subtitle.render("Pulsa INTRO o cualquier tecla para comenzar...", True, (100, 255, 100)) + draw_text_2d(start_msg, WIDTH // 2 - 200, 30) + + glMatrixMode(GL_PROJECTION) + glPopMatrix() + glMatrixMode(GL_MODELVIEW) + glPopMatrix() + glEnable(GL_DEPTH_TEST) + +def draw_hud(player_pos, yaw, insects, eaten_mosquitos, eaten_flies, font): + glDisable(GL_DEPTH_TEST) + + glMatrixMode(GL_PROJECTION) + glPushMatrix() + glLoadIdentity() + gluOrtho2D(0, WIDTH, 0, HEIGHT) + + glMatrixMode(GL_MODELVIEW) + glPushMatrix() + glLoadIdentity() + + px, py, pz = player_pos + + cx, cy = WIDTH // 2, HEIGHT - 35 + radius = 22 + + glColor3f(*COLOR_HUD) + glBegin(GL_LINE_LOOP) + for i in range(24): + angle = 2.0 * math.pi * i / 24 + glVertex2f(cx + radius * math.cos(angle), cy + radius * math.sin(angle)) + glEnd() + + glBegin(GL_LINES) + glVertex2f(cx, cy + radius) + glVertex2f(cx, cy + radius - 5) + glVertex2f(cx, cy - radius) + glVertex2f(cx, cy - radius + 5) + glVertex2f(cx - radius, cy) + glVertex2f(cx - radius + 5, cy) + glVertex2f(cx + radius, cy) + glVertex2f(cx + radius - 5, cy) + glEnd() + + closest_insect = None + min_dist = 999999.0 + relative_angle = 0.0 + diff_z = 0.0 + + for insect in insects: + dx = insect.x - px + dy = insect.y - py + dist = math.hypot(dx, dy) + + if dist < min_dist and dist > 0.1: + angle_to_insect = math.degrees(math.atan2(dy, dx)) + heading = (angle_to_insect - (90 - yaw)) % 360 + if heading > 180: + heading -= 360 + + if abs(heading) < 60: + min_dist = dist + closest_insect = insect + relative_angle = heading + diff_z = insect.z - pz + + if closest_insect: + radar_x = cx - (relative_angle / 60.0) * (radius - 4) + radar_y = cy + (diff_z / 20.0) * (radius - 4) + + dist_from_center = math.hypot(radar_x - cx, radar_y - cy) + if dist_from_center > (radius - 3): + factor = (radius - 3) / dist_from_center + radar_x = cx + (radar_x - cx) * factor + radar_y = cy + (radar_y - cy) * factor + + glColor3f(1.0, 1.0, 1.0) + glBegin(GL_LINE_LOOP) + for i in range(8): + ang = 2.0 * math.pi * i / 8 + glVertex2f(radar_x + 3 * math.cos(ang), radar_y + 3 * math.sin(ang)) + glEnd() + + glColor3f(*COLOR_MOSQUITO) + glBegin(GL_LINES) + glVertex2f(25, HEIGHT - 33) + glVertex2f(25, HEIGHT - 27) + glVertex2f(21, HEIGHT - 30) + glVertex2f(29, HEIGHT - 30) + glEnd() + + glColor3f(*COLOR_FLY) + glBegin(GL_LINES) + glVertex2f(WIDTH - 82, HEIGHT - 33) + glVertex2f(WIDTH - 82, HEIGHT - 27) + glVertex2f(WIDTH - 86, HEIGHT - 28) + glVertex2f(WIDTH - 78, HEIGHT - 32) + glVertex2f(WIDTH - 86, HEIGHT - 32) + glVertex2f(WIDTH - 78, HEIGHT - 28) + glEnd() + + txt_mosq = font.render(f"x {eaten_mosquitos}", True, (255, 230, 0)) + txt_flies = font.render(f"x {eaten_flies}", True, (200, 65, 40)) + + draw_text_2d(txt_mosq, 38, HEIGHT - 40) + draw_text_2d(txt_flies, WIDTH - 68, HEIGHT - 40) + + glMatrixMode(GL_PROJECTION) + glPopMatrix() + glMatrixMode(GL_MODELVIEW) + glPopMatrix() + glEnable(GL_DEPTH_TEST) + +# ========================================== +# BUCLE PRINCIPAL +# ========================================== +def main(): + pygame.init() + pygame.mixer.init(frequency=22050, size=-16, channels=1) + pygame.font.init() + + font = pygame.font.SysFont("Arial", 22, bold=True) + font_title = pygame.font.SysFont("Arial", 48, bold=True) + font_subtitle = pygame.font.SysFont("Arial", 20, bold=True) + font_text = pygame.font.SysFont("Arial", 16, bold=False) + + pygame.display.set_mode((WIDTH, HEIGHT), DOUBLEBUF | OPENGL) + pygame.display.set_caption("Libélula") + + glEnable(GL_DEPTH_TEST) + glClearColor(*COLOR_BG, 1.0) + + glMatrixMode(GL_PROJECTION) + gluPerspective(50, (WIDTH / HEIGHT), 0.1, 5000.0) + glMatrixMode(GL_MODELVIEW) + + # Generar efectos de sonido procedurales + buzz_sound = generate_buzz_sound() + wind_sound = generate_wind_sound() + + px, py, pz = 0.0, 0.0, 15.0 + yaw = 0.0 + roll = 0.0 + pitch = 0.0 + max_roll = 30.0 + max_pitch = 20.0 + + speed = 0.0 + max_speed = 3.0 + acceleration = 0.06 + deceleration = 0.03 + + min_altitude = 1.0 + max_altitude = 120.0 + + wing_timer = 0.0 + water_timer = 0.0 + is_perched = False + + dive_phase = "none" + pre_dive_alt = 15.0 + pre_dive_speed = 0.0 + + auto_landing = False + target_perch = None + + caught_insect = None + eaten_mosquitos = 0 + eaten_flies = 0 + + frog = Frog() + + # Instanciar las 3 libélulas moradas decorativas con comportamientos distintos + purple_dragonflies = [ + PurpleDragonfly(1), + PurpleDragonfly(2), + PurpleDragonfly(3) + ] + + insects = [] + num_mosquitos = random.randint(MOSQUITO_MIN, MOSQUITO_MAX) + num_flies = random.randint(FLY_MIN, FLY_MAX) + + for _ in range(num_mosquitos): + insects.append(Insect("mosquito")) + for _ in range(num_flies): + insects.append(Insect("fly")) + + clock = pygame.time.Clock() + + in_start_screen = True + running = True + + buzz_timer = 0 + + while running: + dt = clock.tick(60) / 1000.0 + water_timer += dt + wing_timer += 60.0 + + for event in pygame.event.get(): + if event.type == QUIT or (event.type == KEYDOWN and event.key == K_ESCAPE): + running = False + elif event.type == KEYDOWN: + if in_start_screen: + in_start_screen = False + else: + if event.key == K_SPACE: + if not is_perched and dive_phase == "none": + dive_phase = "diving" + pre_dive_alt = pz + pre_dive_speed = speed + auto_landing = False + elif event.key == K_p: + if not is_perched and dive_phase == "none": + best_reed = None + min_d = 999.0 + for rx, ry, rz, rh in REEDS: + dist = math.hypot(px - rx, py - ry) + if dist < 35.0: + angle_to_reed = math.degrees(math.atan2(ry - py, rx - px)) + heading = (angle_to_reed - (90 - yaw)) % 360 + if heading > 180: + heading -= 360 + if abs(heading) < 45 and dist < min_d: + min_d = dist + best_reed = (rx, ry, rz + rh) + + if best_reed: + target_perch = best_reed + auto_landing = True + + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) + glLoadIdentity() + + if in_start_screen: + cam_x, cam_y, cam_z = 0.0, -100.0, 30.0 + gluLookAt(cam_x, cam_y, cam_z, 0, 0, 15, 0, 0, 1) + draw_map(water_timer) + frog.draw() + for p_df in purple_dragonflies: + p_df.update() + p_df.draw() + + wing_angle = math.sin(wing_timer * 0.5) * 35.0 + draw_start_screen(font_title, font_subtitle, font_text, wing_angle) + else: + keys = pygame.key.get_pressed() + + if is_perched: + if keys[K_LEFT]: + yaw += 3.0 + if keys[K_RIGHT]: + yaw -= 3.0 + + if keys[K_w]: + is_perched = False + speed = 0.5 + + wing_angle = 0.0 + pitch = 0.0 + roll = 0.0 + dive_phase = "none" + auto_landing = False + else: + if auto_landing and target_perch: + tx, ty, tz = target_perch + dx = tx - px + dy = ty - py + dist_to_target = math.hypot(dx, dy) + + speed = 1.2 + px += (dx / dist_to_target) * speed if dist_to_target > 0.1 else 0 + py += (dy / dist_to_target) * speed if dist_to_target > 0.1 else 0 + pz += (tz - pz) * 0.1 + + if dist_to_target < 2.5 and abs(pz - tz) < 2.5: + px, py, pz = target_perch + speed = 0.0 + roll = 0.0 + pitch = 0.0 + is_perched = True + auto_landing = False + target_perch = None + + if caught_insect: + if caught_insect.type == "mosquito": + eaten_mosquitos += 1 + else: + eaten_flies += 1 + caught_insect = None + + elif dive_phase == "diving": + speed = max_speed * 1.5 + pz -= 1.2 + target_pitch = -45.0 + if wind_sound and not pygame.mixer.get_busy(): + wind_sound.play() + + if pz <= min_altitude: + pz = min_altitude + dive_phase = "recovering" + + elif dive_phase == "recovering": + target_pitch = 30.0 + pz += 0.8 + speed += (pre_dive_speed - speed) * 0.1 + + if pz >= pre_dive_alt: + pz = pre_dive_alt + speed = pre_dive_speed + dive_phase = "none" + else: + if keys[K_w]: + speed = min(speed + acceleration, max_speed) + if speed > 2.0 and wind_sound and random.random() < 0.1 and not pygame.mixer.get_busy(): + wind_sound.play() + elif keys[K_s]: + speed = max(speed - deceleration, 0.0) + + if speed < 0.3: + for rx, ry, rz, rh in REEDS: + dist = math.hypot(px - rx, py - ry) + if dist < 10.0 and abs(pz - (rz + rh)) < 10.0: + px, py, pz = rx, ry, rz + rh + speed = 0.0 + roll = 0.0 + pitch = 0.0 + is_perched = True + + if caught_insect: + if caught_insect.type == "mosquito": + eaten_mosquitos += 1 + else: + eaten_flies += 1 + caught_insect = None + break + + target_pitch = 0.0 + if keys[K_UP]: + pz = min(pz + 0.8, max_altitude) + target_pitch = max_pitch + elif keys[K_DOWN]: + pz = max(pz - 0.8, min_altitude) + target_pitch = -max_pitch + + if not auto_landing: + pitch += (target_pitch - pitch) * 0.15 + + target_roll = 0.0 + if keys[K_LEFT]: + yaw += 2.8 + target_roll = -max_roll + elif keys[K_RIGHT]: + yaw -= 2.8 + target_roll = max_roll + + roll += (target_roll - roll) * 0.15 + + rad_yaw = math.radians(yaw) + px += -math.sin(rad_yaw) * speed + py += math.cos(rad_yaw) * speed + + if abs(py) < POND_RADIUS: + max_limit_x = POND_RADIUS + 200.0 + else: + cx = get_river_center(py) + max_limit_x = abs(cx) + 200.0 + + px = max(-max_limit_x, min(max_limit_x, px)) + + if py > STREAM_MAX_Y: + py -= WORLD_LEN + elif py < STREAM_MIN_Y: + py += WORLD_LEN + + wing_timer += 80.0 if (dive_phase != "none" or auto_landing) else (60.0 if speed > 0.1 else 30.0) + wing_angle = math.sin(wing_timer) * 35.0 + + frog.update() + + for p_df in purple_dragonflies: + p_df.update() + + current_mosquitos = 0 + current_flies = 0 + near_insect = False + + for insect in insects[:]: + insect.update((px, py, pz)) + + if insect.type == "mosquito": + current_mosquitos += 1 + else: + current_flies += 1 + + dist_to_df = math.sqrt((px - insect.x)**2 + (py - insect.y)**2 + (pz - insect.z)**2) + if dist_to_df < 40.0: + near_insect = True + + if not caught_insect: + if dist_to_df < 6.0: + caught_insect = insect + insects.remove(insect) + if dive_phase == "diving": + dive_phase = "recovering" + + # Reproducir zumbido si estamos cerca de insectos + if near_insect and buzz_sound: + buzz_timer += 1 + if buzz_timer > 25 and not pygame.mixer.get_busy(): + buzz_sound.play() + buzz_timer = 0 + + if current_mosquitos < MOSQUITO_MIN: + insects.append(Insect("mosquito")) + if current_flies < FLY_MIN: + insects.append(Insect("fly")) + + cam_dist = 18.0 + cam_height = 8.0 + rad_yaw = math.radians(yaw) + + cam_x = px + math.sin(rad_yaw) * cam_dist + cam_y = py - math.cos(rad_yaw) * cam_dist + cam_z = pz + cam_height + + gluLookAt(cam_x, cam_y, cam_z, + px, py, pz + 0.5, + 0, 0, 1) + + draw_map(water_timer) + frog.draw() + + for p_df in purple_dragonflies: + p_df.draw() + + for insect in insects: + insect.draw() + + glPushMatrix() + glTranslatef(px, py, pz) + glRotatef(yaw, 0, 0, 1) + glRotatef(pitch, 1, 0, 0) + glRotatef(roll, 0, 1, 0) + draw_dragonfly(wing_angle, caught_insect) + glPopMatrix() + + draw_hud((px, py, pz), yaw, insects, eaten_mosquitos, eaten_flies, font) + + pygame.display.flip() + + pygame.quit() + sys.exit() + +if __name__ == "__main__": + main() diff --git a/previos/libelula_v9.2.py b/previos/libelula_v9.2.py new file mode 100644 index 0000000..b3aab8e --- /dev/null +++ b/previos/libelula_v9.2.py @@ -0,0 +1,1199 @@ +import sys +import math +import random +import struct +import pygame +from pygame.locals import * +from OpenGL.GL import * +from OpenGL.GLU import * + +# ========================================== +# CONFIGURACIÓN Y CONSTANTES +# ========================================== +WIDTH, HEIGHT = 800, 600 + +# Colores (R, G, B) +COLOR_BG = (0.12, 0.15, 0.12) # Fondo ambiente +COLOR_WATER = (0.0, 0.5, 0.85) # Agua animada (Cian/Azul) +COLOR_LAND = (0.45, 0.32, 0.15) # Tierra / Relieve +COLOR_LAND_RIVER = (0.40, 0.28, 0.12) # Tierra en riachuelos +COLOR_ISLAND = (0.5, 0.38, 0.18) # Islas +COLOR_REED = (0.15, 0.85, 0.25) # Juncos verdes +COLOR_CATTAIL = (0.35, 0.18, 0.08) # Espigas marrones densas +COLOR_DRAGONFLY = (0.0, 1.0, 1.0) # Libélula del jugador (Cian) +COLOR_PURPLE_DRAGONFLY = (0.7, 0.2, 0.8) # Libélulas decorativas (Morado) +COLOR_MOSQUITO = (1.0, 0.9, 0.0) # Mosquitos (Amarillo) +COLOR_FLY = (0.8, 0.25, 0.15) # Moscas (Rojo-Marrón) +COLOR_FROG = (0.15, 0.85, 0.35) # Rana verde detallada +COLOR_HUD = (1.0, 1.0, 1.0) # Retícula HUD (Blanco) + +# Dimensiones del Mundo +POND_RADIUS = 1800.0 +STREAM_LEN = 900.0 +STREAM_MIN_Y = -(POND_RADIUS + STREAM_LEN) +STREAM_MAX_Y = (POND_RADIUS + STREAM_LEN) +WORLD_LEN = STREAM_MAX_Y - STREAM_MIN_Y + +# Cantidades de insectos +MOSQUITO_MIN, MOSQUITO_MAX = 17, 27 +FLY_MIN, FLY_MAX = 9, 14 + +# ========================================== +# ELEMENTOS DEL MAPA +# ========================================== +REEDS = [ + (-250, 350, 0, 20), (-160, -500, 0, 22), (500, 200, 0, 24), (-700, 200, 0, 21), (200, -800, 0, 23), + (-900, -300, 0, 25), (760, -600, 0, 22), (900, 500, 0, 26), (-500, 900, 0, 23), (600, -1000, 0, 22), + (-1200, 400, 0, 24), (1300, -300, 0, 20), (-1400, -700, 0, 25), (1100, 1200, 0, 23), (-360, -1300, 0, 22), + (-90, -1950, 0, 20), (-110, -2150, 0, 23), (-80, -2350, 0, 19), (-100, -2550, 0, 22), + (90, 1950, 0, 22), (110, 2150, 0, 20), (85, 2350, 0, 24), (105, 2550, 0, 21) +] + +ISLANDS = [ + (-250, 350, 90), (-160, -500, 85), (500, 200, 100), (-700, 200, 110), (200, -800, 95), + (-900, -300, 120), (760, -600, 90), (900, 500, 105), (-500, 900, 115), (600, -1000, 100) +] + +def get_river_center(y): + if y < -POND_RADIUS: + dy = y + POND_RADIUS + return -50 + 100 * math.sin(dy * 0.008) + elif y > POND_RADIUS: + dy = y - POND_RADIUS + return 50 + 100 * math.sin(dy * 0.008) + else: + return (y / POND_RADIUS) * 50.0 + +def is_over_water(x, y): + dist_center = math.hypot(x, y) + if dist_center < POND_RADIUS - 50: + return True + if abs(y) >= POND_RADIUS: + cx = get_river_center(y) + if abs(x - cx) < 80: + return True + return False + +def get_random_water_position(): + while True: + if random.random() < 0.7: + rad = random.uniform(0, math.pi * 2) + r = random.uniform(100, POND_RADIUS - 150) + x = r * math.cos(rad) + y = r * math.sin(rad) + else: + y = random.choice([random.uniform(POND_RADIUS, STREAM_MAX_Y), random.uniform(STREAM_MIN_Y, -POND_RADIUS)]) + cx = get_river_center(y) + x = cx + random.uniform(-60, 60) + + if is_over_water(x, y): + z = random.uniform(5.0, 35.0) + return x, y, z + +# ========================================== +# GENERADOR DE AUDIO PROCEDURAL (SIN ARCHIVOS) +# ========================================== +def generate_buzz_sound(): + try: + sample_rate = 22050 + duration = 0.3 + num_samples = int(sample_rate * duration) + buffer = bytearray() + for i in range(num_samples): + t = i / sample_rate + val = int(32767 * 0.25 * math.sin(2 * math.pi * 180 * t + math.sin(2 * math.pi * 25 * t) * 3)) + buffer.extend(struct.pack(' STREAM_MAX_Y: + self.y -= WORLD_LEN + elif self.y < STREAM_MIN_Y: + self.y += WORLD_LEN + + def draw(self, attached=False): + if self.type == "mosquito": + glColor3f(*COLOR_MOSQUITO) + else: + glColor3f(*COLOR_FLY) + + glPushMatrix() + if not attached: + glTranslatef(self.x, self.y, self.z) + glRotatef(self.yaw, 0, 0, 1) + + s = self.size + glBegin(GL_LINES) + glVertex3f(0, s, 0) + glVertex3f(0, -s, 0) + w_offset = 0.0 if attached else math.sin(self.timer * 30) * (s * 0.8) + glVertex3f(0, 0, 0) + glVertex3f(-s * 1.5, 0, w_offset) + glVertex3f(0, 0, 0) + glVertex3f(s * 1.5, 0, w_offset) + glEnd() + + glPopMatrix() + +class PurpleDragonfly: + def __init__(self, behavior_type): + self.behavior = behavior_type + self.x, self.y, self.z = get_random_water_position() + self.yaw = random.uniform(0, 360) + self.timer = random.uniform(0, 100) + self.wing_timer = random.uniform(0, 100) + self.is_perched = False + self.perch_timer = 0 + + def update(self): + self.wing_timer += 60.0 + if self.is_perched: + self.perch_timer -= 1 + if self.perch_timer <= 0: + self.is_perched = False + return + + self.timer += 0.05 + if self.behavior == 1: + self.yaw += 1.2 + rad = math.radians(self.yaw) + self.x += -math.sin(rad) * 1.1 + self.y += math.cos(rad) * 1.1 + elif self.behavior == 2: + if random.random() < 0.02: + self.yaw += random.uniform(-40, 40) + rad = math.radians(self.yaw) + self.x += -math.sin(rad) * 1.3 + self.y += math.cos(rad) * 1.3 + else: + self.yaw += math.sin(self.timer * 3) * 3.5 + rad = math.radians(self.yaw) + self.x += -math.sin(rad) * 1.4 + self.y += math.cos(rad) * 1.4 + + self.z = 15.0 + math.sin(self.timer * 2) * 6.0 + + if random.random() < 0.0015 and REEDS: + reed = random.choice(REEDS) + self.x, self.y, self.z = reed[0], reed[1], reed[2] + reed[3] + self.is_perched = True + self.perch_timer = random.randint(300, 600) + + if not is_over_water(self.x, self.y): + self.yaw += 150.0 + + if self.y > STREAM_MAX_Y: + self.y -= WORLD_LEN + elif self.y < STREAM_MIN_Y: + self.y += WORLD_LEN + + def draw(self): + glColor3f(*COLOR_PURPLE_DRAGONFLY) + glPushMatrix() + glTranslatef(self.x, self.y, self.z) + glRotatef(self.yaw, 0, 0, 1) + + glBegin(GL_LINES) + glVertex3f(0, 1.5, 0) + glVertex3f(0, -3.0, 0) + glVertex3f(-0.4, 0.6, 0) + glVertex3f(0.4, 0.6, 0) + glVertex3f(-0.3, -0.6, 0) + glVertex3f(0.3, -0.6, 0) + glEnd() + + glBegin(GL_LINE_LOOP) + for i in range(8): + angle = 2.0 * math.pi * i / 8 + glVertex3f(0.3 * math.cos(angle), 1.4 + 0.3 * math.sin(angle), 0) + glEnd() + + wing_angle = 0.0 if self.is_perched else math.sin(self.wing_timer) * 35.0 + rad_wing = math.radians(wing_angle) + w_z = math.sin(rad_wing) * 1.2 if wing_angle != 0 else 0.0 + + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(-4.5, 1.0, w_z) + glVertex3f(-3.8, 0.2, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(4.5, 1.0, w_z) + glVertex3f(3.8, 0.2, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(-3.8, -0.2, w_z) + glVertex3f(-3.0, -0.8, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(3.8, -0.2, w_z) + glVertex3f(3.0, -0.8, w_z) + glEnd() + + glPopMatrix() + +class Frog: + def __init__(self): + self.pick_new_destination() + self.x, self.y = self.target_x, self.target_y + self.z = 0.6 + self.speed = 0.35 + self.yaw = 0.0 + self.swim_timer = 0.0 + + def pick_new_destination(self): + destinations = [(rx, ry) for rx, ry, _, _ in REEDS] + [(ix, iy) for ix, iy, _ in ISLANDS] + dest = random.choice(destinations) + self.target_x, self.target_y = dest[0] + random.uniform(-30, 30), dest[1] + random.uniform(-30, 30) + + def update(self): + self.swim_timer += 0.15 + dx = self.target_x - self.x + dy = self.target_y - self.y + dist = math.hypot(dx, dy) + + if dist < 6.0: + self.pick_new_destination() + else: + self.yaw = math.degrees(math.atan2(dy, dx)) - 90.0 + self.x += (dx / dist) * self.speed + self.y += (dy / dist) * self.speed + + def draw(self, custom_x=None, custom_y=None, custom_z=None, scale=1.0, menu_icon=False): + glColor3f(*COLOR_FROG) + glPushMatrix() + if custom_x is not None and custom_y is not None and custom_z is not None: + glTranslatef(custom_x, custom_y, custom_z) + glScalef(scale, scale, scale) + else: + glTranslatef(self.x, self.y, self.z) + glRotatef(self.yaw, 0, 0, 1) + + leg_offset = 0.0 if menu_icon else math.sin(self.swim_timer) * 1.5 + + glBegin(GL_LINES) + glVertex3f(0.0, 0.0, 0.5) + glVertex3f(0.0, -5.0, 0.5) + glVertex3f(-2.0, 2.0, 0.5) + glVertex3f(2.0, 2.0, 0.5) + glVertex3f(-2.5, -2.0, 0.5) + glVertex3f(2.5, -2.0, 0.5) + + glVertex3f(-2.0, 2.0, 0.5) + glVertex3f(0.0, 0.0, 0.5) + glVertex3f(2.0, 2.0, 0.5) + glVertex3f(0.0, 0.0, 0.5) + + glVertex3f(-2.0, 2.0, 0.6) + glVertex3f(2.0, 2.0, 0.6) + glVertex3f(-2.0, 2.0, 0.6) + glVertex3f(0.0, 4.5, 0.6) + glVertex3f(2.0, 2.0, 0.6) + glVertex3f(0.0, 4.5, 0.6) + + glVertex3f(-1.5, 3.5, 1.5) + glVertex3f(-1.5, 3.5, 0.6) + glVertex3f(1.5, 3.5, 1.5) + glVertex3f(1.5, 3.5, 0.6) + + glVertex3f(-2.0, 2.0, 0.5) + glVertex3f(-4.0, 3.0, 0.0) + glVertex3f(-4.0, 3.0, 0.0) + glVertex3f(-3.5, 4.0, 0.0) + + glVertex3f(2.0, 2.0, 0.5) + glVertex3f(4.0, 3.0, 0.0) + glVertex3f(4.0, 3.0, 0.0) + glVertex3f(3.5, 4.0, 0.0) + + # Patas traseras con los pies hacia el exterior + glVertex3f(-2.5, -2.0, 0.5) + glVertex3f(-5.0 - leg_offset, -4.0, 0.0) + glVertex3f(-5.0 - leg_offset, -4.0, 0.0) + glVertex3f(-6.5 - leg_offset, -3.2, 0.0) + + glVertex3f(2.5, -2.0, 0.5) + glVertex3f(5.0 + leg_offset, -4.0, 0.0) + glVertex3f(5.0 + leg_offset, -4.0, 0.0) + glVertex3f(6.5 + leg_offset, -3.2, 0.0) + glEnd() + + # Pequeño cuadrado/rectángulo central para representar el cuerpo sólido en el icono del menú + if menu_icon: + glBegin(GL_LINE_LOOP) + glVertex3f(-1.5, -1.5, 0.4) + glVertex3f(1.5, -1.5, 0.4) + glVertex3f(1.5, 1.5, 0.4) + glVertex3f(-1.5, 1.5, 0.4) + glEnd() + + glPopMatrix() + +# ========================================== +# RENDERIZADO DE ESCENARIO Y MODELOS +# ========================================== +def draw_water_surface(time_counter): + glColor3f(*COLOR_WATER) + + grid_size = 15 + for x in range(int(-POND_RADIUS), int(POND_RADIUS), grid_size * 2): + glBegin(GL_LINE_STRIP) + for y in range(int(-POND_RADIUS), int(-POND_RADIUS) + int(POND_RADIUS)*2, grid_size): + dist = math.hypot(x, y) + if dist < POND_RADIUS - 30: + z = 1.0 * math.sin(dist * 0.015 - time_counter * 3.0) + 0.5 * math.cos(x * 0.01 + time_counter) + glVertex3f(x, y, z) + glEnd() + + for y in range(int(STREAM_MIN_Y), int(STREAM_MAX_Y), 15): + if abs(y) < POND_RADIUS: + continue + cx = get_river_center(y) + glBegin(GL_LINE_STRIP) + for dx in range(-100, 110, 10): + x = cx + dx + z = 0.5 * math.sin(y * 0.03 + dx * 0.08 + time_counter * 4.0) + glVertex3f(x, y, z) + glEnd() + +def draw_terrain(): + glColor3f(*COLOR_LAND) + + grid_step = 120 + for y in range(int(STREAM_MIN_Y - 300), int(STREAM_MAX_Y + 300), grid_step): + glBegin(GL_LINE_STRIP) + for x in range(-2500, 2500, grid_step): + z = 1.5 * math.sin(x * 0.005) + 1.5 * math.cos(y * 0.005) + glVertex3f(x, y, z) + glEnd() + + width_river = 100.0 + margin_land = 70.0 + glColor3f(*COLOR_LAND_RIVER) + + for y in range(int(STREAM_MIN_Y), int(STREAM_MAX_Y), 30): + if abs(y) < POND_RADIUS: + continue + cx = get_river_center(y) + + glBegin(GL_LINE_STRIP) + for dx in range(int(-width_river - margin_land), int(-width_river), 15): + x = cx + dx + z = 0.6 * math.sin(y * 0.02 + x * 0.02) + 0.3 + glVertex3f(x, y, z) + glEnd() + + glBegin(GL_LINE_STRIP) + for dx in range(int(width_river), int(width_river + margin_land), 15): + x = cx + dx + z = 0.6 * math.sin(y * 0.02 + x * 0.02) + 0.3 + glVertex3f(x, y, z) + glEnd() + + for ix, iy, ir in ISLANDS: + glColor3f(*COLOR_ISLAND) + for layer in range(1, 4): + rad_layer = ir * (1 - layer * 0.25) + height = layer * 1.2 + glBegin(GL_LINE_LOOP) + for i in range(16): + angle = 2.0 * math.pi * i / 16 + dx = rad_layer * math.cos(angle) + dy = rad_layer * math.sin(angle) + glVertex3f(ix + dx, iy + dy, height) + glEnd() + +def draw_map(time_counter): + draw_terrain() + draw_water_surface(time_counter) + + offsets = [(-1.5, 0.6), (1.3, -0.6), (-0.7, -1.4), (1.1, 1.2), (0.0, 1.6), (-1.2, 0.0)] + + for rx, ry, rz, rh in REEDS: + glColor3f(*COLOR_REED) + glBegin(GL_LINES) + glVertex3f(rx, ry, rz) + glVertex3f(rx, ry, rz + rh) + for ox, oy in offsets: + glVertex3f(rx, ry, rz) + glVertex3f(rx + ox, ry + oy, rz + rh * 0.85) + glEnd() + + glColor3f(*COLOR_CATTAIL) + glBegin(GL_LINES) + for d in [-0.2, 0.0, 0.2]: + glVertex3f(rx + d, ry, rz + rh - 4.5) + glVertex3f(rx + d, ry, rz + rh) + for ox, oy in offsets: + for d in [-0.15, 0.15]: + glVertex3f(rx + ox + d, ry + oy, (rz + rh * 0.85) - 3.8) + glVertex3f(rx + ox + d, ry + oy, rz + rh * 0.85) + glEnd() + +def draw_dragonfly(wing_angle, caught_insect=None): + glColor3f(*COLOR_DRAGONFLY) + + glBegin(GL_LINES) + glVertex3f(0, 1.5, 0) + glVertex3f(0, -3.0, 0) + glVertex3f(-0.4, 0.6, 0) + glVertex3f(0.4, 0.6, 0) + glVertex3f(-0.3, -0.6, 0) + glVertex3f(0.3, -0.6, 0) + + glVertex3f(-0.3, 0.5, 0) + glVertex3f(-0.2, 0.2, -0.6) + glVertex3f(0.3, 0.5, 0) + glVertex3f(0.2, 0.2, -0.6) + glVertex3f(-0.2, -0.2, 0) + glVertex3f(-0.1, -0.3, -0.6) + glVertex3f(0.2, -0.2, 0) + glVertex3f(0.1, -0.3, -0.6) + glEnd() + + glBegin(GL_LINE_LOOP) + for i in range(8): + angle = 2.0 * math.pi * i / 8 + glVertex3f(0.3 * math.cos(angle), 1.4 + 0.3 * math.sin(angle), 0) + glEnd() + + rad_wing = math.radians(wing_angle) + w_z = math.sin(rad_wing) * 1.2 if wing_angle != 0 else 0.0 + + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(-4.5, 1.0, w_z) + glVertex3f(-3.8, 0.2, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(4.5, 1.0, w_z) + glVertex3f(3.8, 0.2, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(-3.8, -0.2, w_z) + glVertex3f(-3.0, -0.8, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(3.8, -0.2, w_z) + glVertex3f(3.0, -0.8, w_z) + glEnd() + + if caught_insect: + glPushMatrix() + glTranslatef(0.0, 0.2, -0.6) + caught_insect.draw(attached=True) + glPopMatrix() + +# ========================================== +# INTERFAZ 2D / HUD Y PANTALLA INICIAL +# ========================================== +def draw_text_2d(text_surface, x, y): + text_data = pygame.image.tostring(text_surface, "RGBA", True) + w, h = text_surface.get_size() + + glEnable(GL_BLEND) + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) + + glRasterPos2i(x, y) + glDrawPixels(w, h, GL_RGBA, GL_UNSIGNED_BYTE, text_data) + glDisable(GL_BLEND) + +def draw_start_screen(font_title, font_subtitle, font_text, wing_angle, frog_instance): + glDisable(GL_DEPTH_TEST) + + glMatrixMode(GL_PROJECTION) + glPushMatrix() + glLoadIdentity() + gluOrtho2D(0, WIDTH, 0, HEIGHT) + + glMatrixMode(GL_MODELVIEW) + glPushMatrix() + glLoadIdentity() + + # TÍTULO PRINCIPAL Y LIBÉLULA SUPERIOR + txt_title = font_title.render("LIBÉLULA", True, (0, 220, 255)) + draw_text_2d(txt_title, WIDTH // 2 - 130, HEIGHT - 100) + + glPushMatrix() + glTranslatef(WIDTH // 2 + 150, HEIGHT - 75, 0.0) + glScalef(12.0, 12.0, 1.0) + + glColor3f(*COLOR_DRAGONFLY) + glBegin(GL_LINES) + glVertex3f(0, 1.5, 0) + glVertex3f(0, -3.0, 0) + glVertex3f(-0.4, 0.6, 0) + glVertex3f(0.4, 0.6, 0) + glEnd() + + rad_w = math.radians(wing_angle) + w_off = math.sin(rad_w) * 1.5 + + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(-4.5, 1.0, w_off) + glVertex3f(-3.8, 0.2, w_off) + glEnd() + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(4.5, 1.0, w_off) + glVertex3f(3.8, 0.2, w_off) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(-3.8, -0.2, w_off) + glVertex3f(-3.0, -0.8, w_off) + glEnd() + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(3.8, -0.2, w_off) + glVertex3f(3.0, -0.8, w_off) + glEnd() + + glPopMatrix() + + # OBJETIVO DEL JUEGO + obj_line1 = font_text.render("Objetivo: Vuela libre por el estanque y sus arroyos capturando moscas", True, (255, 255, 255)) + obj_line2 = font_text.render("y mosquitos. Una vez cazados debes devorarlos posado en un junco.", True, (255, 255, 255)) + draw_text_2d(obj_line1, 70, HEIGHT - 170) + draw_text_2d(obj_line2, 70, HEIGHT - 195) + + # CONTROLES Y TECLAS + controls = [ + "CONTROLES:", + "W / S : Acelerar / Frenar", + "Flecha Arriba / Abajo : Cambiar altitud", + "Flecha Izquierda / Derecha : Girar / Virar", + "ESPACIO : Picado rápido para cazar", + "P : Aterrizaje automático en junco cercano", + "ESC : Salir del juego" + ] + + y_pos = HEIGHT - 250 + for ctrl in controls: + is_title = ctrl.endswith(":") + col = (255, 220, 50) if is_title else (200, 200, 200) + t_surf = font_subtitle.render(ctrl, True, col) if is_title else font_text.render(ctrl, True, col) + draw_text_2d(t_surf, 70, y_pos) + y_pos -= 25 + + # LEYENDA VISUAL INFERIOR (Mosquito, Mosca, Junco, Rana y Libélula) + y_icon = 90 + + # 1. Mosquito + glColor3f(*COLOR_MOSQUITO) + glBegin(GL_LINES) + glVertex2f(60, y_icon + 5) + glVertex2f(60, y_icon - 5) + glVertex2f(55, y_icon) + glVertex2f(65, y_icon) + glEnd() + txt_m = font_text.render("Mosquito", True, (255, 230, 0)) + draw_text_2d(txt_m, 75, y_icon - 8) + + # 2. Mosca + glColor3f(*COLOR_FLY) + glBegin(GL_LINES) + glVertex2f(210, y_icon + 5) + glVertex2f(210, y_icon - 5) + glVertex2f(205, y_icon - 3) + glVertex2f(215, y_icon + 3) + glVertex2f(205, y_icon + 3) + glVertex2f(215, y_icon - 3) + glEnd() + txt_f = font_text.render("Mosca", True, (200, 65, 40)) + draw_text_2d(txt_f, 225, y_icon - 8) + + # 3. Junco + glColor3f(*COLOR_REED) + glBegin(GL_LINES) + glVertex2f(340, y_icon - 10) + glVertex2f(340, y_icon + 15) + glEnd() + glColor3f(*COLOR_CATTAIL) + glBegin(GL_LINES) + glVertex2f(340, y_icon + 8) + glVertex2f(340, y_icon + 15) + glEnd() + txt_r = font_text.render("Junco", True, (100, 220, 100)) + draw_text_2d(txt_r, 355, y_icon - 8) + + # 4. Rana (Con su cuadradito de cuerpo sólido incorporado) + frog_instance.draw(custom_x=475, custom_y=y_icon, custom_z=0.0, scale=3.5, menu_icon=True) + txt_frog = font_text.render("Rana", True, (100, 255, 100)) + draw_text_2d(txt_frog, 505, y_icon - 8) + + # 5. Libélula (Con alas animadas) + glPushMatrix() + glTranslatef(625, y_icon, 0.0) + glScalef(6.0, 6.0, 1.0) + glColor3f(*COLOR_PURPLE_DRAGONFLY) + glBegin(GL_LINES) + glVertex3f(0, 1.5, 0) + glVertex3f(0, -3.0, 0) + glVertex3f(-0.4, 0.6, 0) + glVertex3f(0.4, 0.6, 0) + glEnd() + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(-4.5, 1.0, w_off) + glVertex3f(-3.8, 0.2, w_off) + glEnd() + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(4.5, 1.0, w_off) + glVertex3f(3.8, 0.2, w_off) + glEnd() + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(-3.8, -0.2, w_off) + glVertex3f(-3.0, -0.8, w_off) + glEnd() + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(3.8, -0.2, w_off) + glVertex3f(3.0, -0.8, w_off) + glEnd() + glPopMatrix() + + txt_df = font_text.render("Libélula", True, (0, 220, 255)) + draw_text_2d(txt_df, 654, y_icon - 8) + + # MENSAJE PARA COMENZAR + start_msg = font_subtitle.render("Pulsa INTRO o cualquier tecla para comenzar...", True, (100, 255, 100)) + draw_text_2d(start_msg, WIDTH // 2 - 200, 30) + + glMatrixMode(GL_PROJECTION) + glPopMatrix() + glMatrixMode(GL_MODELVIEW) + glPopMatrix() + glEnable(GL_DEPTH_TEST) + +def draw_hud(player_pos, yaw, insects, eaten_mosquitos, eaten_flies, font): + glDisable(GL_DEPTH_TEST) + + glMatrixMode(GL_PROJECTION) + glPushMatrix() + glLoadIdentity() + gluOrtho2D(0, WIDTH, 0, HEIGHT) + + glMatrixMode(GL_MODELVIEW) + glPushMatrix() + glLoadIdentity() + + px, py, pz = player_pos + + cx, cy = WIDTH // 2, HEIGHT - 35 + radius = 22 + + glColor3f(*COLOR_HUD) + glBegin(GL_LINE_LOOP) + for i in range(24): + angle = 2.0 * math.pi * i / 24 + glVertex2f(cx + radius * math.cos(angle), cy + radius * math.sin(angle)) + glEnd() + + glBegin(GL_LINES) + glVertex2f(cx, cy + radius) + glVertex2f(cx, cy + radius - 5) + glVertex2f(cx, cy - radius) + glVertex2f(cx, cy - radius + 5) + glVertex2f(cx - radius, cy) + glVertex2f(cx - radius + 5, cy) + glVertex2f(cx + radius, cy) + glVertex2f(cx + radius - 5, cy) + glEnd() + + closest_insect = None + min_dist = 999999.0 + relative_angle = 0.0 + diff_z = 0.0 + + for insect in insects: + dx = insect.x - px + dy = insect.y - py + dist = math.hypot(dx, dy) + + if dist < min_dist and dist > 0.1: + angle_to_insect = math.degrees(math.atan2(dy, dx)) + heading = (angle_to_insect - (90 - yaw)) % 360 + if heading > 180: + heading -= 360 + + if abs(heading) < 60: + min_dist = dist + closest_insect = insect + relative_angle = heading + diff_z = insect.z - pz + + if closest_insect: + radar_x = cx - (relative_angle / 60.0) * (radius - 4) + radar_y = cy + (diff_z / 20.0) * (radius - 4) + + dist_from_center = math.hypot(radar_x - cx, radar_y - cy) + if dist_from_center > (radius - 3): + factor = (radius - 3) / dist_from_center + radar_x = cx + (radar_x - cx) * factor + radar_y = cy + (radar_y - cy) * factor + + glColor3f(1.0, 1.0, 1.0) + glBegin(GL_LINE_LOOP) + for i in range(8): + ang = 2.0 * math.pi * i / 8 + glVertex2f(radar_x + 3 * math.cos(ang), radar_y + 3 * math.sin(ang)) + glEnd() + + glColor3f(*COLOR_MOSQUITO) + glBegin(GL_LINES) + glVertex2f(25, HEIGHT - 33) + glVertex2f(25, HEIGHT - 27) + glVertex2f(21, HEIGHT - 30) + glVertex2f(29, HEIGHT - 30) + glEnd() + + glColor3f(*COLOR_FLY) + glBegin(GL_LINES) + glVertex2f(WIDTH - 82, HEIGHT - 33) + glVertex2f(WIDTH - 82, HEIGHT - 27) + glVertex2f(WIDTH - 86, HEIGHT - 28) + glVertex2f(WIDTH - 78, HEIGHT - 32) + glVertex2f(WIDTH - 86, HEIGHT - 32) + glVertex2f(WIDTH - 78, HEIGHT - 28) + glEnd() + + txt_mosq = font.render(f"x {eaten_mosquitos}", True, (255, 230, 0)) + txt_flies = font.render(f"x {eaten_flies}", True, (200, 65, 40)) + + draw_text_2d(txt_mosq, 38, HEIGHT - 40) + draw_text_2d(txt_flies, WIDTH - 68, HEIGHT - 40) + + glMatrixMode(GL_PROJECTION) + glPopMatrix() + glMatrixMode(GL_MODELVIEW) + glPopMatrix() + glEnable(GL_DEPTH_TEST) + +# ========================================== +# BUCLE PRINCIPAL +# ========================================== +def main(): + pygame.init() + pygame.mixer.init(frequency=22050, size=-16, channels=1) + pygame.font.init() + + font = pygame.font.SysFont("Arial", 22, bold=True) + font_title = pygame.font.SysFont("Arial", 48, bold=True) + font_subtitle = pygame.font.SysFont("Arial", 20, bold=True) + font_text = pygame.font.SysFont("Arial", 16, bold=False) + + pygame.display.set_mode((WIDTH, HEIGHT), DOUBLEBUF | OPENGL) + pygame.display.set_caption("Libélula") + + glEnable(GL_DEPTH_TEST) + glClearColor(*COLOR_BG, 1.0) + + glMatrixMode(GL_PROJECTION) + gluPerspective(50, (WIDTH / HEIGHT), 0.1, 5000.0) + glMatrixMode(GL_MODELVIEW) + + buzz_sound = generate_buzz_sound() + wind_sound = generate_wind_sound() + + px, py, pz = 0.0, 0.0, 15.0 + yaw = 0.0 + roll = 0.0 + pitch = 0.0 + max_roll = 30.0 + max_pitch = 20.0 + + speed = 0.0 + max_speed = 3.0 + acceleration = 0.06 + deceleration = 0.03 + + min_altitude = 1.0 + max_altitude = 120.0 + + wing_timer = 0.0 + water_timer = 0.0 + is_perched = False + + dive_phase = "none" + pre_dive_alt = 15.0 + pre_dive_speed = 0.0 + + auto_landing = False + target_perch = None + + caught_insect = None + eaten_mosquitos = 0 + eaten_flies = 0 + + frog = Frog() + + purple_dragonflies = [ + PurpleDragonfly(1), + PurpleDragonfly(2), + PurpleDragonfly(3) + ] + + insects = [] + num_mosquitos = random.randint(MOSQUITO_MIN, MOSQUITO_MAX) + num_flies = random.randint(FLY_MIN, FLY_MAX) + + for _ in range(num_mosquitos): + insects.append(Insect("mosquito")) + for _ in range(num_flies): + insects.append(Insect("fly")) + + clock = pygame.time.Clock() + + in_start_screen = True + running = True + + buzz_timer = 0 + + while running: + dt = clock.tick(60) / 1000.0 + water_timer += dt + wing_timer += 60.0 + + for event in pygame.event.get(): + if event.type == QUIT or (event.type == KEYDOWN and event.key == K_ESCAPE): + running = False + elif event.type == KEYDOWN: + if in_start_screen: + in_start_screen = False + else: + if event.key == K_SPACE: + if not is_perched and dive_phase == "none": + dive_phase = "diving" + pre_dive_alt = pz + pre_dive_speed = speed + auto_landing = False + elif event.key == K_p: + if not is_perched and dive_phase == "none": + best_reed = None + min_d = 999.0 + for rx, ry, rz, rh in REEDS: + dist = math.hypot(px - rx, py - ry) + if dist < 35.0: + angle_to_reed = math.degrees(math.atan2(ry - py, rx - px)) + heading = (angle_to_reed - (90 - yaw)) % 360 + if heading > 180: + heading -= 360 + if abs(heading) < 45 and dist < min_d: + min_d = dist + best_reed = (rx, ry, rz + rh) + + if best_reed: + target_perch = best_reed + auto_landing = True + + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) + glLoadIdentity() + + if in_start_screen: + cam_x, cam_y, cam_z = 0.0, -200.0, 50.0 + gluLookAt(cam_x, cam_y, cam_z, 0, 0, 15, 0, 0, 1) + + draw_map(water_timer) + frog.update() + frog.draw() + for p_df in purple_dragonflies: + p_df.update() + p_df.draw() + + wing_angle = math.sin(wing_timer * 0.5) * 35.0 + draw_start_screen(font_title, font_subtitle, font_text, wing_angle, frog) + else: + keys = pygame.key.get_pressed() + + if is_perched: + if keys[K_LEFT]: + yaw += 3.0 + if keys[K_RIGHT]: + yaw -= 3.0 + + if keys[K_w]: + is_perched = False + speed = 0.5 + + wing_angle = 0.0 + pitch = 0.0 + roll = 0.0 + dive_phase = "none" + auto_landing = False + else: + if auto_landing and target_perch: + tx, ty, tz = target_perch + dx = tx - px + dy = ty - py + dist_to_target = math.hypot(dx, dy) + + speed = 1.2 + px += (dx / dist_to_target) * speed if dist_to_target > 0.1 else 0 + py += (dy / dist_to_target) * speed if dist_to_target > 0.1 else 0 + pz += (tz - pz) * 0.1 + + if dist_to_target < 2.5 and abs(pz - tz) < 2.5: + px, py, pz = target_perch + speed = 0.0 + roll = 0.0 + pitch = 0.0 + is_perched = True + auto_landing = False + target_perch = None + + if caught_insect: + if caught_insect.type == "mosquito": + eaten_mosquitos += 1 + else: + eaten_flies += 1 + caught_insect = None + + elif dive_phase == "diving": + speed = max_speed * 1.5 + pz -= 1.2 + target_pitch = -45.0 + if wind_sound and not pygame.mixer.get_busy(): + wind_sound.play() + + if pz <= min_altitude: + pz = min_altitude + dive_phase = "recovering" + + elif dive_phase == "recovering": + target_pitch = 30.0 + pz += 0.8 + speed += (pre_dive_speed - speed) * 0.1 + + if pz >= pre_dive_alt: + pz = pre_dive_alt + speed = pre_dive_speed + dive_phase = "none" + else: + if keys[K_w]: + speed = min(speed + acceleration, max_speed) + if speed > 2.0 and wind_sound and random.random() < 0.1 and not pygame.mixer.get_busy(): + wind_sound.play() + elif keys[K_s]: + speed = max(speed - deceleration, 0.0) + + if speed < 0.3: + for rx, ry, rz, rh in REEDS: + dist = math.hypot(px - rx, py - ry) + if dist < 10.0 and abs(pz - (rz + rh)) < 10.0: + px, py, pz = rx, ry, rz + rh + speed = 0.0 + roll = 0.0 + pitch = 0.0 + is_perched = True + + if caught_insect: + if caught_insect.type == "mosquito": + eaten_mosquitos += 1 + else: + eaten_flies += 1 + caught_insect = None + break + + target_pitch = 0.0 + if keys[K_UP]: + pz = min(pz + 0.8, max_altitude) + target_pitch = max_pitch + elif keys[K_DOWN]: + pz = max(pz - 0.8, min_altitude) + target_pitch = -max_pitch + + if not auto_landing: + pitch += (target_pitch - pitch) * 0.15 + + target_roll = 0.0 + if keys[K_LEFT]: + yaw += 2.8 + target_roll = -max_roll + elif keys[K_RIGHT]: + yaw -= 2.8 + target_roll = max_roll + + roll += (target_roll - roll) * 0.15 + + rad_yaw = math.radians(yaw) + px += -math.sin(rad_yaw) * speed + py += math.cos(rad_yaw) * speed + + if abs(py) < POND_RADIUS: + max_limit_x = POND_RADIUS + 200.0 + else: + cx = get_river_center(py) + max_limit_x = abs(cx) + 200.0 + + px = max(-max_limit_x, min(max_limit_x, px)) + + if py > STREAM_MAX_Y: + py -= WORLD_LEN + elif py < STREAM_MIN_Y: + py += WORLD_LEN + + wing_timer += 80.0 if (dive_phase != "none" or auto_landing) else (60.0 if speed > 0.1 else 30.0) + wing_angle = math.sin(wing_timer) * 35.0 + + frog.update() + + for p_df in purple_dragonflies: + p_df.update() + + current_mosquitos = 0 + current_flies = 0 + near_insect = False + + for insect in insects[:]: + insect.update((px, py, pz)) + + if insect.type == "mosquito": + current_mosquitos += 1 + else: + current_flies += 1 + + dist_to_df = math.sqrt((px - insect.x)**2 + (py - insect.y)**2 + (pz - insect.z)**2) + if dist_to_df < 40.0: + near_insect = True + + if not caught_insect: + if dist_to_df < 6.0: + caught_insect = insect + insects.remove(insect) + if dive_phase == "diving": + dive_phase = "recovering" + + if near_insect and buzz_sound: + buzz_timer += 1 + if buzz_timer > 25 and not pygame.mixer.get_busy(): + buzz_sound.play() + buzz_timer = 0 + + if current_mosquitos < MOSQUITO_MIN: + insects.append(Insect("mosquito")) + if current_flies < FLY_MIN: + insects.append(Insect("fly")) + + cam_dist = 18.0 + cam_height = 8.0 + rad_yaw = math.radians(yaw) + + cam_x = px + math.sin(rad_yaw) * cam_dist + cam_y = py - math.cos(rad_yaw) * cam_dist + cam_z = pz + cam_height + + gluLookAt(cam_x, cam_y, cam_z, + px, py, pz + 0.5, + 0, 0, 1) + + draw_map(water_timer) + frog.draw() + + for p_df in purple_dragonflies: + p_df.draw() + + for insect in insects: + insect.draw() + + glPushMatrix() + glTranslatef(px, py, pz) + glRotatef(yaw, 0, 0, 1) + glRotatef(pitch, 1, 0, 0) + glRotatef(roll, 0, 1, 0) + draw_dragonfly(wing_angle, caught_insect) + glPopMatrix() + + draw_hud((px, py, pz), yaw, insects, eaten_mosquitos, eaten_flies, font) + + pygame.display.flip() + + pygame.quit() + sys.exit() + +if __name__ == "__main__": + main() diff --git a/previos/libelula_v9.3.py b/previos/libelula_v9.3.py new file mode 100644 index 0000000..4817603 --- /dev/null +++ b/previos/libelula_v9.3.py @@ -0,0 +1,1199 @@ +import sys +import math +import random +import struct +import pygame +from pygame.locals import * +from OpenGL.GL import * +from OpenGL.GLU import * + +# ========================================== +# CONFIGURACIÓN Y CONSTANTES +# ========================================== +WIDTH, HEIGHT = 800, 600 + +# Colores (R, G, B) +COLOR_BG = (0.12, 0.15, 0.12) # Fondo ambiente +COLOR_WATER = (0.0, 0.5, 0.85) # Agua animada (Cian/Azul) +COLOR_LAND = (0.45, 0.32, 0.15) # Tierra / Relieve +COLOR_LAND_RIVER = (0.40, 0.28, 0.12) # Tierra en riachuelos +COLOR_ISLAND = (0.5, 0.38, 0.18) # Islas +COLOR_REED = (0.15, 0.85, 0.25) # Juncos verdes +COLOR_CATTAIL = (0.35, 0.18, 0.08) # Espigas marrones densas +COLOR_DRAGONFLY = (0.0, 1.0, 1.0) # Libélula del jugador (Cian) +COLOR_PURPLE_DRAGONFLY = (0.7, 0.2, 0.8) # Libélulas decorativas (Morado) +COLOR_MOSQUITO = (1.0, 0.9, 0.0) # Mosquitos (Amarillo) +COLOR_FLY = (0.8, 0.25, 0.15) # Moscas (Rojo-Marrón) +COLOR_FROG = (0.15, 0.85, 0.35) # Rana verde detallada +COLOR_HUD = (1.0, 1.0, 1.0) # Retícula HUD (Blanco) + +# Dimensiones del Mundo +POND_RADIUS = 1800.0 +STREAM_LEN = 900.0 +STREAM_MIN_Y = -(POND_RADIUS + STREAM_LEN) +STREAM_MAX_Y = (POND_RADIUS + STREAM_LEN) +WORLD_LEN = STREAM_MAX_Y - STREAM_MIN_Y + +# Cantidades de insectos +MOSQUITO_MIN, MOSQUITO_MAX = 17, 27 +FLY_MIN, FLY_MAX = 9, 14 + +# ========================================== +# ELEMENTOS DEL MAPA +# ========================================== +REEDS = [ + (-250, 350, 0, 20), (-160, -500, 0, 22), (500, 200, 0, 24), (-700, 200, 0, 21), (200, -800, 0, 23), + (-900, -300, 0, 25), (760, -600, 0, 22), (900, 500, 0, 26), (-500, 900, 0, 23), (600, -1000, 0, 22), + (-1200, 400, 0, 24), (1300, -300, 0, 20), (-1400, -700, 0, 25), (1100, 1200, 0, 23), (-360, -1300, 0, 22), + (-90, -1950, 0, 20), (-110, -2150, 0, 23), (-80, -2350, 0, 19), (-100, -2550, 0, 22), + (90, 1950, 0, 22), (110, 2150, 0, 20), (85, 2350, 0, 24), (105, 2550, 0, 21) +] + +ISLANDS = [ + (-250, 350, 90), (-160, -500, 85), (500, 200, 100), (-700, 200, 110), (200, -800, 95), + (-900, -300, 120), (760, -600, 90), (900, 500, 105), (-500, 900, 115), (600, -1000, 100) +] + +def get_river_center(y): + if y < -POND_RADIUS: + dy = y + POND_RADIUS + return -50 + 100 * math.sin(dy * 0.008) + elif y > POND_RADIUS: + dy = y - POND_RADIUS + return 50 + 100 * math.sin(dy * 0.008) + else: + return (y / POND_RADIUS) * 50.0 + +def is_over_water(x, y): + dist_center = math.hypot(x, y) + if dist_center < POND_RADIUS - 50: + return True + if abs(y) >= POND_RADIUS: + cx = get_river_center(y) + if abs(x - cx) < 80: + return True + return False + +def get_random_water_position(): + while True: + if random.random() < 0.7: + rad = random.uniform(0, math.pi * 2) + r = random.uniform(100, POND_RADIUS - 150) + x = r * math.cos(rad) + y = r * math.sin(rad) + else: + y = random.choice([random.uniform(POND_RADIUS, STREAM_MAX_Y), random.uniform(STREAM_MIN_Y, -POND_RADIUS)]) + cx = get_river_center(y) + x = cx + random.uniform(-60, 60) + + if is_over_water(x, y): + z = random.uniform(5.0, 35.0) + return x, y, z + +# ========================================== +# GENERADOR DE AUDIO PROCEDURAL (SIN ARCHIVOS) +# ========================================== +def generate_buzz_sound(): + try: + sample_rate = 22050 + duration = 0.3 + num_samples = int(sample_rate * duration) + buffer = bytearray() + for i in range(num_samples): + t = i / sample_rate + val = int(32767 * 0.25 * math.sin(2 * math.pi * 180 * t + math.sin(2 * math.pi * 25 * t) * 3)) + buffer.extend(struct.pack(' STREAM_MAX_Y: + self.y -= WORLD_LEN + elif self.y < STREAM_MIN_Y: + self.y += WORLD_LEN + + def draw(self, attached=False): + if self.type == "mosquito": + glColor3f(*COLOR_MOSQUITO) + else: + glColor3f(*COLOR_FLY) + + glPushMatrix() + if not attached: + glTranslatef(self.x, self.y, self.z) + glRotatef(self.yaw, 0, 0, 1) + + s = self.size + glBegin(GL_LINES) + glVertex3f(0, s, 0) + glVertex3f(0, -s, 0) + w_offset = 0.0 if attached else math.sin(self.timer * 30) * (s * 0.8) + glVertex3f(0, 0, 0) + glVertex3f(-s * 1.5, 0, w_offset) + glVertex3f(0, 0, 0) + glVertex3f(s * 1.5, 0, w_offset) + glEnd() + + glPopMatrix() + +class PurpleDragonfly: + def __init__(self, behavior_type): + self.behavior = behavior_type + self.x, self.y, self.z = get_random_water_position() + self.yaw = random.uniform(0, 360) + self.timer = random.uniform(0, 100) + self.wing_timer = random.uniform(0, 100) + self.is_perched = False + self.perch_timer = 0 + + def update(self): + self.wing_timer += 60.0 + if self.is_perched: + self.perch_timer -= 1 + if self.perch_timer <= 0: + self.is_perched = False + return + + self.timer += 0.05 + if self.behavior == 1: + self.yaw += 1.2 + rad = math.radians(self.yaw) + self.x += -math.sin(rad) * 1.1 + self.y += math.cos(rad) * 1.1 + elif self.behavior == 2: + if random.random() < 0.02: + self.yaw += random.uniform(-40, 40) + rad = math.radians(self.yaw) + self.x += -math.sin(rad) * 1.3 + self.y += math.cos(rad) * 1.3 + else: + self.yaw += math.sin(self.timer * 3) * 3.5 + rad = math.radians(self.yaw) + self.x += -math.sin(rad) * 1.4 + self.y += math.cos(rad) * 1.4 + + self.z = 15.0 + math.sin(self.timer * 2) * 6.0 + + if random.random() < 0.0015 and REEDS: + reed = random.choice(REEDS) + self.x, self.y, self.z = reed[0], reed[1], reed[2] + reed[3] + self.is_perched = True + self.perch_timer = random.randint(300, 600) + + if not is_over_water(self.x, self.y): + self.yaw += 150.0 + + if self.y > STREAM_MAX_Y: + self.y -= WORLD_LEN + elif self.y < STREAM_MIN_Y: + self.y += WORLD_LEN + + def draw(self): + glColor3f(*COLOR_PURPLE_DRAGONFLY) + glPushMatrix() + glTranslatef(self.x, self.y, self.z) + glRotatef(self.yaw, 0, 0, 1) + + glBegin(GL_LINES) + glVertex3f(0, 1.5, 0) + glVertex3f(0, -3.0, 0) + glVertex3f(-0.4, 0.6, 0) + glVertex3f(0.4, 0.6, 0) + glVertex3f(-0.3, -0.6, 0) + glVertex3f(0.3, -0.6, 0) + glEnd() + + glBegin(GL_LINE_LOOP) + for i in range(8): + angle = 2.0 * math.pi * i / 8 + glVertex3f(0.3 * math.cos(angle), 1.4 + 0.3 * math.sin(angle), 0) + glEnd() + + wing_angle = 0.0 if self.is_perched else math.sin(self.wing_timer) * 35.0 + rad_wing = math.radians(wing_angle) + w_z = math.sin(rad_wing) * 1.2 if wing_angle != 0 else 0.0 + + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(-4.5, 1.0, w_z) + glVertex3f(-3.8, 0.2, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(4.5, 1.0, w_z) + glVertex3f(3.8, 0.2, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(-3.8, -0.2, w_z) + glVertex3f(-3.0, -0.8, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(3.8, -0.2, w_z) + glVertex3f(3.0, -0.8, w_z) + glEnd() + + glPopMatrix() + +class Frog: + def __init__(self): + self.pick_new_destination() + self.x, self.y = self.target_x, self.target_y + self.z = 0.6 + self.speed = 0.35 + self.yaw = 0.0 + self.swim_timer = 0.0 + + def pick_new_destination(self): + destinations = [(rx, ry) for rx, ry, _, _ in REEDS] + [(ix, iy) for ix, iy, _ in ISLANDS] + dest = random.choice(destinations) + self.target_x, self.target_y = dest[0] + random.uniform(-30, 30), dest[1] + random.uniform(-30, 30) + + def update(self): + self.swim_timer += 0.15 + dx = self.target_x - self.x + dy = self.target_y - self.y + dist = math.hypot(dx, dy) + + if dist < 6.0: + self.pick_new_destination() + else: + self.yaw = math.degrees(math.atan2(dy, dx)) - 90.0 + self.x += (dx / dist) * self.speed + self.y += (dy / dist) * self.speed + + def draw(self, custom_x=None, custom_y=None, custom_z=None, scale=1.0, menu_icon=False): + glColor3f(*COLOR_FROG) + glPushMatrix() + if custom_x is not None and custom_y is not None and custom_z is not None: + glTranslatef(custom_x, custom_y, custom_z) + glScalef(scale, scale, scale) + else: + glTranslatef(self.x, self.y, self.z) + glRotatef(self.yaw, 0, 0, 1) + + leg_offset = 0.0 if menu_icon else math.sin(self.swim_timer) * 1.5 + + glBegin(GL_LINES) + glVertex3f(0.0, 0.0, 0.5) + glVertex3f(0.0, -5.0, 0.5) + glVertex3f(-2.0, 2.0, 0.5) + glVertex3f(2.0, 2.0, 0.5) + glVertex3f(-2.5, -2.0, 0.5) + glVertex3f(2.5, -2.0, 0.5) + + glVertex3f(-2.0, 2.0, 0.5) + glVertex3f(0.0, 0.0, 0.5) + glVertex3f(2.0, 2.0, 0.5) + glVertex3f(0.0, 0.0, 0.5) + + glVertex3f(-2.0, 2.0, 0.6) + glVertex3f(2.0, 2.0, 0.6) + glVertex3f(-2.0, 2.0, 0.6) + glVertex3f(0.0, 4.5, 0.6) + glVertex3f(2.0, 2.0, 0.6) + glVertex3f(0.0, 4.5, 0.6) + + glVertex3f(-1.5, 3.5, 1.5) + glVertex3f(-1.5, 3.5, 0.6) + glVertex3f(1.5, 3.5, 1.5) + glVertex3f(1.5, 3.5, 0.6) + + glVertex3f(-2.0, 2.0, 0.5) + glVertex3f(-4.0, 3.0, 0.0) + glVertex3f(-4.0, 3.0, 0.0) + glVertex3f(-3.5, 4.0, 0.0) + + glVertex3f(2.0, 2.0, 0.5) + glVertex3f(4.0, 3.0, 0.0) + glVertex3f(4.0, 3.0, 0.0) + glVertex3f(3.5, 4.0, 0.0) + + # Patas traseras con los pies hacia el exterior + glVertex3f(-2.5, -2.0, 0.5) + glVertex3f(-5.0 - leg_offset, -4.0, 0.0) + glVertex3f(-5.0 - leg_offset, -4.0, 0.0) + glVertex3f(-6.5 - leg_offset, -3.2, 0.0) + + glVertex3f(2.5, -2.0, 0.5) + glVertex3f(5.0 + leg_offset, -4.0, 0.0) + glVertex3f(5.0 + leg_offset, -4.0, 0.0) + glVertex3f(6.5 + leg_offset, -3.2, 0.0) + glEnd() + + # Pequeño cuadrado/rectángulo central para representar el cuerpo sólido en el icono del menú + if menu_icon: + glBegin(GL_LINE_LOOP) + glVertex3f(-1.5, -1.5, 0.4) + glVertex3f(1.5, -1.5, 0.4) + glVertex3f(1.5, 1.5, 0.4) + glVertex3f(-1.5, 1.5, 0.4) + glEnd() + + glPopMatrix() + +# ========================================== +# RENDERIZADO DE ESCENARIO Y MODELOS +# ========================================== +def draw_water_surface(time_counter): + glColor3f(*COLOR_WATER) + + grid_size = 15 + for x in range(int(-POND_RADIUS), int(POND_RADIUS), grid_size * 2): + glBegin(GL_LINE_STRIP) + for y in range(int(-POND_RADIUS), int(-POND_RADIUS) + int(POND_RADIUS)*2, grid_size): + dist = math.hypot(x, y) + if dist < POND_RADIUS - 30: + z = 1.0 * math.sin(dist * 0.015 - time_counter * 3.0) + 0.5 * math.cos(x * 0.01 + time_counter) + glVertex3f(x, y, z) + glEnd() + + for y in range(int(STREAM_MIN_Y), int(STREAM_MAX_Y), 15): + if abs(y) < POND_RADIUS: + continue + cx = get_river_center(y) + glBegin(GL_LINE_STRIP) + for dx in range(-100, 110, 10): + x = cx + dx + z = 0.5 * math.sin(y * 0.03 + dx * 0.08 + time_counter * 4.0) + glVertex3f(x, y, z) + glEnd() + +def draw_terrain(): + glColor3f(*COLOR_LAND) + + grid_step = 120 + for y in range(int(STREAM_MIN_Y - 300), int(STREAM_MAX_Y + 300), grid_step): + glBegin(GL_LINE_STRIP) + for x in range(-2500, 2500, grid_step): + z = 1.5 * math.sin(x * 0.005) + 1.5 * math.cos(y * 0.005) + glVertex3f(x, y, z) + glEnd() + + width_river = 100.0 + margin_land = 70.0 + glColor3f(*COLOR_LAND_RIVER) + + for y in range(int(STREAM_MIN_Y), int(STREAM_MAX_Y), 30): + if abs(y) < POND_RADIUS: + continue + cx = get_river_center(y) + + glBegin(GL_LINE_STRIP) + for dx in range(int(-width_river - margin_land), int(-width_river), 15): + x = cx + dx + z = 0.6 * math.sin(y * 0.02 + x * 0.02) + 0.3 + glVertex3f(x, y, z) + glEnd() + + glBegin(GL_LINE_STRIP) + for dx in range(int(width_river), int(width_river + margin_land), 15): + x = cx + dx + z = 0.6 * math.sin(y * 0.02 + x * 0.02) + 0.3 + glVertex3f(x, y, z) + glEnd() + + for ix, iy, ir in ISLANDS: + glColor3f(*COLOR_ISLAND) + for layer in range(1, 4): + rad_layer = ir * (1 - layer * 0.25) + height = layer * 1.2 + glBegin(GL_LINE_LOOP) + for i in range(16): + angle = 2.0 * math.pi * i / 16 + dx = rad_layer * math.cos(angle) + dy = rad_layer * math.sin(angle) + glVertex3f(ix + dx, iy + dy, height) + glEnd() + +def draw_map(time_counter): + draw_terrain() + draw_water_surface(time_counter) + + offsets = [(-1.5, 0.6), (1.3, -0.6), (-0.7, -1.4), (1.1, 1.2), (0.0, 1.6), (-1.2, 0.0)] + + for rx, ry, rz, rh in REEDS: + glColor3f(*COLOR_REED) + glBegin(GL_LINES) + glVertex3f(rx, ry, rz) + glVertex3f(rx, ry, rz + rh) + for ox, oy in offsets: + glVertex3f(rx, ry, rz) + glVertex3f(rx + ox, ry + oy, rz + rh * 0.85) + glEnd() + + glColor3f(*COLOR_CATTAIL) + glBegin(GL_LINES) + for d in [-0.2, 0.0, 0.2]: + glVertex3f(rx + d, ry, rz + rh - 4.5) + glVertex3f(rx + d, ry, rz + rh) + for ox, oy in offsets: + for d in [-0.15, 0.15]: + glVertex3f(rx + ox + d, ry + oy, (rz + rh * 0.85) - 3.8) + glVertex3f(rx + ox + d, ry + oy, rz + rh * 0.85) + glEnd() + +def draw_dragonfly(wing_angle, caught_insect=None): + glColor3f(*COLOR_DRAGONFLY) + + glBegin(GL_LINES) + glVertex3f(0, 1.5, 0) + glVertex3f(0, -3.0, 0) + glVertex3f(-0.4, 0.6, 0) + glVertex3f(0.4, 0.6, 0) + glVertex3f(-0.3, -0.6, 0) + glVertex3f(0.3, -0.6, 0) + + glVertex3f(-0.3, 0.5, 0) + glVertex3f(-0.2, 0.2, -0.6) + glVertex3f(0.3, 0.5, 0) + glVertex3f(0.2, 0.2, -0.6) + glVertex3f(-0.2, -0.2, 0) + glVertex3f(-0.1, -0.3, -0.6) + glVertex3f(0.2, -0.2, 0) + glVertex3f(0.1, -0.3, -0.6) + glEnd() + + glBegin(GL_LINE_LOOP) + for i in range(8): + angle = 2.0 * math.pi * i / 8 + glVertex3f(0.3 * math.cos(angle), 1.4 + 0.3 * math.sin(angle), 0) + glEnd() + + rad_wing = math.radians(wing_angle) + w_z = math.sin(rad_wing) * 1.2 if wing_angle != 0 else 0.0 + + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(-4.5, 1.0, w_z) + glVertex3f(-3.8, 0.2, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(4.5, 1.0, w_z) + glVertex3f(3.8, 0.2, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(-3.8, -0.2, w_z) + glVertex3f(-3.0, -0.8, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(3.8, -0.2, w_z) + glVertex3f(3.0, -0.8, w_z) + glEnd() + + if caught_insect: + glPushMatrix() + glTranslatef(0.0, 0.2, -0.6) + caught_insect.draw(attached=True) + glPopMatrix() + +# ========================================== +# INTERFAZ 2D / HUD Y PANTALLA INICIAL +# ========================================== +def draw_text_2d(text_surface, x, y): + text_data = pygame.image.tostring(text_surface, "RGBA", True) + w, h = text_surface.get_size() + + glEnable(GL_BLEND) + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) + + glRasterPos2i(x, y) + glDrawPixels(w, h, GL_RGBA, GL_UNSIGNED_BYTE, text_data) + glDisable(GL_BLEND) + +def draw_start_screen(font_title, font_subtitle, font_text, wing_angle, frog_instance): + glDisable(GL_DEPTH_TEST) + + glMatrixMode(GL_PROJECTION) + glPushMatrix() + glLoadIdentity() + gluOrtho2D(0, WIDTH, 0, HEIGHT) + + glMatrixMode(GL_MODELVIEW) + glPushMatrix() + glLoadIdentity() + + # TÍTULO PRINCIPAL Y LIBÉLULA SUPERIOR + txt_title = font_title.render("LIBÉLULA", True, (0, 220, 255)) + draw_text_2d(txt_title, WIDTH // 2 - 130, HEIGHT - 100) + + glPushMatrix() + glTranslatef(WIDTH // 2 + 150, HEIGHT - 75, 0.0) + glScalef(12.0, 12.0, 1.0) + + glColor3f(*COLOR_DRAGONFLY) + glBegin(GL_LINES) + glVertex3f(0, 1.5, 0) + glVertex3f(0, -3.0, 0) + glVertex3f(-0.4, 0.6, 0) + glVertex3f(0.4, 0.6, 0) + glEnd() + + rad_w = math.radians(wing_angle) + w_off = math.sin(rad_w) * 1.5 + + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(-4.5, 1.0, w_off) + glVertex3f(-3.8, 0.2, w_off) + glEnd() + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(4.5, 1.0, w_off) + glVertex3f(3.8, 0.2, w_off) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(-3.8, -0.2, w_off) + glVertex3f(-3.0, -0.8, w_off) + glEnd() + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(3.8, -0.2, w_off) + glVertex3f(3.0, -0.8, w_off) + glEnd() + + glPopMatrix() + + # OBJETIVO DEL JUEGO + obj_line1 = font_text.render("Objetivo: Vuela libre por el estanque y sus arroyos capturando moscas", True, (255, 255, 255)) + obj_line2 = font_text.render("y mosquitos. Una vez cazados debes devorarlos posado en un junco.", True, (255, 255, 255)) + draw_text_2d(obj_line1, 70, HEIGHT - 170) + draw_text_2d(obj_line2, 70, HEIGHT - 195) + + # CONTROLES Y TECLAS + controls = [ + "CONTROLES:", + "W / S : Acelerar / Frenar", + "Flecha Arriba / Abajo : Cambiar altitud", + "Flecha Izquierda / Derecha : Girar / Virar", + "ESPACIO : Picado rápido para cazar", + "P : Aterrizaje automático en junco cercano", + "ESC : Salir del juego" + ] + + y_pos = HEIGHT - 250 + for ctrl in controls: + is_title = ctrl.endswith(":") + col = (255, 220, 50) if is_title else (200, 200, 200) + t_surf = font_subtitle.render(ctrl, True, col) if is_title else font_text.render(ctrl, True, col) + draw_text_2d(t_surf, 70, y_pos) + y_pos -= 25 + + # LEYENDA VISUAL INFERIOR (Mosquito, Mosca, Junco, Rana y Libélula) + y_icon = 90 + + # 1. Mosquito + glColor3f(*COLOR_MOSQUITO) + glBegin(GL_LINES) + glVertex2f(60, y_icon + 5) + glVertex2f(60, y_icon - 5) + glVertex2f(55, y_icon) + glVertex2f(65, y_icon) + glEnd() + txt_m = font_text.render("Mosquito", True, (255, 230, 0)) + draw_text_2d(txt_m, 75, y_icon - 8) + + # 2. Mosca + glColor3f(*COLOR_FLY) + glBegin(GL_LINES) + glVertex2f(210, y_icon + 5) + glVertex2f(210, y_icon - 5) + glVertex2f(205, y_icon - 3) + glVertex2f(215, y_icon + 3) + glVertex2f(205, y_icon + 3) + glVertex2f(215, y_icon - 3) + glEnd() + txt_f = font_text.render("Mosca", True, (200, 65, 40)) + draw_text_2d(txt_f, 225, y_icon - 8) + + # 3. Junco + glColor3f(*COLOR_REED) + glBegin(GL_LINES) + glVertex2f(340, y_icon - 10) + glVertex2f(340, y_icon + 15) + glEnd() + glColor3f(*COLOR_CATTAIL) + glBegin(GL_LINES) + glVertex2f(340, y_icon + 8) + glVertex2f(340, y_icon + 15) + glEnd() + txt_r = font_text.render("Junco", True, (100, 220, 100)) + draw_text_2d(txt_r, 355, y_icon - 8) + + # 4. Rana (Con su cuadradito de cuerpo sólido incorporado) + frog_instance.draw(custom_x=475, custom_y=y_icon, custom_z=0.0, scale=3.5, menu_icon=True) + txt_frog = font_text.render("Rana", True, (100, 255, 100)) + draw_text_2d(txt_frog, 505, y_icon - 8) + + # 5. Libélula (Con alas animadas) + glPushMatrix() + glTranslatef(625, y_icon, 0.0) + glScalef(6.0, 6.0, 1.0) + glColor3f(*COLOR_PURPLE_DRAGONFLY) + glBegin(GL_LINES) + glVertex3f(0, 1.5, 0) + glVertex3f(0, -3.0, 0) + glVertex3f(-0.4, 0.6, 0) + glVertex3f(0.4, 0.6, 0) + glEnd() + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(-4.5, 1.0, w_off) + glVertex3f(-3.8, 0.2, w_off) + glEnd() + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(4.5, 1.0, w_off) + glVertex3f(3.8, 0.2, w_off) + glEnd() + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(-3.8, -0.2, w_off) + glVertex3f(-3.0, -0.8, w_off) + glEnd() + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(3.8, -0.2, w_off) + glVertex3f(3.0, -0.8, w_off) + glEnd() + glPopMatrix() + + txt_df = font_text.render("Libélulas", True, (178, 51, 204)) + draw_text_2d(txt_df, 654, y_icon - 8) + + # MENSAJE PARA COMENZAR + start_msg = font_subtitle.render("Pulsa INTRO o cualquier tecla para comenzar...", True, (100, 255, 100)) + draw_text_2d(start_msg, WIDTH // 2 - 200, 30) + + glMatrixMode(GL_PROJECTION) + glPopMatrix() + glMatrixMode(GL_MODELVIEW) + glPopMatrix() + glEnable(GL_DEPTH_TEST) + +def draw_hud(player_pos, yaw, insects, eaten_mosquitos, eaten_flies, font): + glDisable(GL_DEPTH_TEST) + + glMatrixMode(GL_PROJECTION) + glPushMatrix() + glLoadIdentity() + gluOrtho2D(0, WIDTH, 0, HEIGHT) + + glMatrixMode(GL_MODELVIEW) + glPushMatrix() + glLoadIdentity() + + px, py, pz = player_pos + + cx, cy = WIDTH // 2, HEIGHT - 35 + radius = 22 + + glColor3f(*COLOR_HUD) + glBegin(GL_LINE_LOOP) + for i in range(24): + angle = 2.0 * math.pi * i / 24 + glVertex2f(cx + radius * math.cos(angle), cy + radius * math.sin(angle)) + glEnd() + + glBegin(GL_LINES) + glVertex2f(cx, cy + radius) + glVertex2f(cx, cy + radius - 5) + glVertex2f(cx, cy - radius) + glVertex2f(cx, cy - radius + 5) + glVertex2f(cx - radius, cy) + glVertex2f(cx - radius + 5, cy) + glVertex2f(cx + radius, cy) + glVertex2f(cx + radius - 5, cy) + glEnd() + + closest_insect = None + min_dist = 999999.0 + relative_angle = 0.0 + diff_z = 0.0 + + for insect in insects: + dx = insect.x - px + dy = insect.y - py + dist = math.hypot(dx, dy) + + if dist < min_dist and dist > 0.1: + angle_to_insect = math.degrees(math.atan2(dy, dx)) + heading = (angle_to_insect - (90 - yaw)) % 360 + if heading > 180: + heading -= 360 + + if abs(heading) < 60: + min_dist = dist + closest_insect = insect + relative_angle = heading + diff_z = insect.z - pz + + if closest_insect: + radar_x = cx - (relative_angle / 60.0) * (radius - 4) + radar_y = cy + (diff_z / 20.0) * (radius - 4) + + dist_from_center = math.hypot(radar_x - cx, radar_y - cy) + if dist_from_center > (radius - 3): + factor = (radius - 3) / dist_from_center + radar_x = cx + (radar_x - cx) * factor + radar_y = cy + (radar_y - cy) * factor + + glColor3f(1.0, 1.0, 1.0) + glBegin(GL_LINE_LOOP) + for i in range(8): + ang = 2.0 * math.pi * i / 8 + glVertex2f(radar_x + 3 * math.cos(ang), radar_y + 3 * math.sin(ang)) + glEnd() + + glColor3f(*COLOR_MOSQUITO) + glBegin(GL_LINES) + glVertex2f(25, HEIGHT - 33) + glVertex2f(25, HEIGHT - 27) + glVertex2f(21, HEIGHT - 30) + glVertex2f(29, HEIGHT - 30) + glEnd() + + glColor3f(*COLOR_FLY) + glBegin(GL_LINES) + glVertex2f(WIDTH - 82, HEIGHT - 33) + glVertex2f(WIDTH - 82, HEIGHT - 27) + glVertex2f(WIDTH - 86, HEIGHT - 28) + glVertex2f(WIDTH - 78, HEIGHT - 32) + glVertex2f(WIDTH - 86, HEIGHT - 32) + glVertex2f(WIDTH - 78, HEIGHT - 28) + glEnd() + + txt_mosq = font.render(f"x {eaten_mosquitos}", True, (255, 230, 0)) + txt_flies = font.render(f"x {eaten_flies}", True, (200, 65, 40)) + + draw_text_2d(txt_mosq, 38, HEIGHT - 40) + draw_text_2d(txt_flies, WIDTH - 68, HEIGHT - 40) + + glMatrixMode(GL_PROJECTION) + glPopMatrix() + glMatrixMode(GL_MODELVIEW) + glPopMatrix() + glEnable(GL_DEPTH_TEST) + +# ========================================== +# BUCLE PRINCIPAL +# ========================================== +def main(): + pygame.init() + pygame.mixer.init(frequency=22050, size=-16, channels=1) + pygame.font.init() + + font = pygame.font.SysFont("Arial", 22, bold=True) + font_title = pygame.font.SysFont("Arial", 48, bold=True) + font_subtitle = pygame.font.SysFont("Arial", 20, bold=True) + font_text = pygame.font.SysFont("Arial", 16, bold=False) + + pygame.display.set_mode((WIDTH, HEIGHT), DOUBLEBUF | OPENGL) + pygame.display.set_caption("Libélula") + + glEnable(GL_DEPTH_TEST) + glClearColor(*COLOR_BG, 1.0) + + glMatrixMode(GL_PROJECTION) + gluPerspective(50, (WIDTH / HEIGHT), 0.1, 5000.0) + glMatrixMode(GL_MODELVIEW) + + buzz_sound = generate_buzz_sound() + wind_sound = generate_wind_sound() + + px, py, pz = 0.0, 0.0, 15.0 + yaw = 0.0 + roll = 0.0 + pitch = 0.0 + max_roll = 30.0 + max_pitch = 20.0 + + speed = 0.0 + max_speed = 3.0 + acceleration = 0.06 + deceleration = 0.03 + + min_altitude = 1.0 + max_altitude = 120.0 + + wing_timer = 0.0 + water_timer = 0.0 + is_perched = False + + dive_phase = "none" + pre_dive_alt = 15.0 + pre_dive_speed = 0.0 + + auto_landing = False + target_perch = None + + caught_insect = None + eaten_mosquitos = 0 + eaten_flies = 0 + + frog = Frog() + + purple_dragonflies = [ + PurpleDragonfly(1), + PurpleDragonfly(2), + PurpleDragonfly(3) + ] + + insects = [] + num_mosquitos = random.randint(MOSQUITO_MIN, MOSQUITO_MAX) + num_flies = random.randint(FLY_MIN, FLY_MAX) + + for _ in range(num_mosquitos): + insects.append(Insect("mosquito")) + for _ in range(num_flies): + insects.append(Insect("fly")) + + clock = pygame.time.Clock() + + in_start_screen = True + running = True + + buzz_timer = 0 + + while running: + dt = clock.tick(60) / 1000.0 + water_timer += dt + wing_timer += 60.0 + + for event in pygame.event.get(): + if event.type == QUIT or (event.type == KEYDOWN and event.key == K_ESCAPE): + running = False + elif event.type == KEYDOWN: + if in_start_screen: + in_start_screen = False + else: + if event.key == K_SPACE: + if not is_perched and dive_phase == "none": + dive_phase = "diving" + pre_dive_alt = pz + pre_dive_speed = speed + auto_landing = False + elif event.key == K_p: + if not is_perched and dive_phase == "none": + best_reed = None + min_d = 999.0 + for rx, ry, rz, rh in REEDS: + dist = math.hypot(px - rx, py - ry) + if dist < 35.0: + angle_to_reed = math.degrees(math.atan2(ry - py, rx - px)) + heading = (angle_to_reed - (90 - yaw)) % 360 + if heading > 180: + heading -= 360 + if abs(heading) < 45 and dist < min_d: + min_d = dist + best_reed = (rx, ry, rz + rh) + + if best_reed: + target_perch = best_reed + auto_landing = True + + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) + glLoadIdentity() + + if in_start_screen: + cam_x, cam_y, cam_z = 0.0, -200.0, 50.0 + gluLookAt(cam_x, cam_y, cam_z, 0, 0, 15, 0, 0, 1) + + draw_map(water_timer) + frog.update() + frog.draw() + for p_df in purple_dragonflies: + p_df.update() + p_df.draw() + + wing_angle = math.sin(wing_timer * 0.5) * 35.0 + draw_start_screen(font_title, font_subtitle, font_text, wing_angle, frog) + else: + keys = pygame.key.get_pressed() + + if is_perched: + if keys[K_LEFT]: + yaw += 3.0 + if keys[K_RIGHT]: + yaw -= 3.0 + + if keys[K_w]: + is_perched = False + speed = 0.5 + + wing_angle = 0.0 + pitch = 0.0 + roll = 0.0 + dive_phase = "none" + auto_landing = False + else: + if auto_landing and target_perch: + tx, ty, tz = target_perch + dx = tx - px + dy = ty - py + dist_to_target = math.hypot(dx, dy) + + speed = 1.2 + px += (dx / dist_to_target) * speed if dist_to_target > 0.1 else 0 + py += (dy / dist_to_target) * speed if dist_to_target > 0.1 else 0 + pz += (tz - pz) * 0.1 + + if dist_to_target < 2.5 and abs(pz - tz) < 2.5: + px, py, pz = target_perch + speed = 0.0 + roll = 0.0 + pitch = 0.0 + is_perched = True + auto_landing = False + target_perch = None + + if caught_insect: + if caught_insect.type == "mosquito": + eaten_mosquitos += 1 + else: + eaten_flies += 1 + caught_insect = None + + elif dive_phase == "diving": + speed = max_speed * 1.5 + pz -= 1.2 + target_pitch = -45.0 + if wind_sound and not pygame.mixer.get_busy(): + wind_sound.play() + + if pz <= min_altitude: + pz = min_altitude + dive_phase = "recovering" + + elif dive_phase == "recovering": + target_pitch = 30.0 + pz += 0.8 + speed += (pre_dive_speed - speed) * 0.1 + + if pz >= pre_dive_alt: + pz = pre_dive_alt + speed = pre_dive_speed + dive_phase = "none" + else: + if keys[K_w]: + speed = min(speed + acceleration, max_speed) + if speed > 2.0 and wind_sound and random.random() < 0.1 and not pygame.mixer.get_busy(): + wind_sound.play() + elif keys[K_s]: + speed = max(speed - deceleration, 0.0) + + if speed < 0.3: + for rx, ry, rz, rh in REEDS: + dist = math.hypot(px - rx, py - ry) + if dist < 10.0 and abs(pz - (rz + rh)) < 10.0: + px, py, pz = rx, ry, rz + rh + speed = 0.0 + roll = 0.0 + pitch = 0.0 + is_perched = True + + if caught_insect: + if caught_insect.type == "mosquito": + eaten_mosquitos += 1 + else: + eaten_flies += 1 + caught_insect = None + break + + target_pitch = 0.0 + if keys[K_UP]: + pz = min(pz + 0.8, max_altitude) + target_pitch = max_pitch + elif keys[K_DOWN]: + pz = max(pz - 0.8, min_altitude) + target_pitch = -max_pitch + + if not auto_landing: + pitch += (target_pitch - pitch) * 0.15 + + target_roll = 0.0 + if keys[K_LEFT]: + yaw += 2.8 + target_roll = -max_roll + elif keys[K_RIGHT]: + yaw -= 2.8 + target_roll = max_roll + + roll += (target_roll - roll) * 0.15 + + rad_yaw = math.radians(yaw) + px += -math.sin(rad_yaw) * speed + py += math.cos(rad_yaw) * speed + + if abs(py) < POND_RADIUS: + max_limit_x = POND_RADIUS + 200.0 + else: + cx = get_river_center(py) + max_limit_x = abs(cx) + 200.0 + + px = max(-max_limit_x, min(max_limit_x, px)) + + if py > STREAM_MAX_Y: + py -= WORLD_LEN + elif py < STREAM_MIN_Y: + py += WORLD_LEN + + wing_timer += 80.0 if (dive_phase != "none" or auto_landing) else (60.0 if speed > 0.1 else 30.0) + wing_angle = math.sin(wing_timer) * 35.0 + + frog.update() + + for p_df in purple_dragonflies: + p_df.update() + + current_mosquitos = 0 + current_flies = 0 + near_insect = False + + for insect in insects[:]: + insect.update((px, py, pz)) + + if insect.type == "mosquito": + current_mosquitos += 1 + else: + current_flies += 1 + + dist_to_df = math.sqrt((px - insect.x)**2 + (py - insect.y)**2 + (pz - insect.z)**2) + if dist_to_df < 40.0: + near_insect = True + + if not caught_insect: + if dist_to_df < 6.0: + caught_insect = insect + insects.remove(insect) + if dive_phase == "diving": + dive_phase = "recovering" + + if near_insect and buzz_sound: + buzz_timer += 1 + if buzz_timer > 25 and not pygame.mixer.get_busy(): + buzz_sound.play() + buzz_timer = 0 + + if current_mosquitos < MOSQUITO_MIN: + insects.append(Insect("mosquito")) + if current_flies < FLY_MIN: + insects.append(Insect("fly")) + + cam_dist = 18.0 + cam_height = 8.0 + rad_yaw = math.radians(yaw) + + cam_x = px + math.sin(rad_yaw) * cam_dist + cam_y = py - math.cos(rad_yaw) * cam_dist + cam_z = pz + cam_height + + gluLookAt(cam_x, cam_y, cam_z, + px, py, pz + 0.5, + 0, 0, 1) + + draw_map(water_timer) + frog.draw() + + for p_df in purple_dragonflies: + p_df.draw() + + for insect in insects: + insect.draw() + + glPushMatrix() + glTranslatef(px, py, pz) + glRotatef(yaw, 0, 0, 1) + glRotatef(pitch, 1, 0, 0) + glRotatef(roll, 0, 1, 0) + draw_dragonfly(wing_angle, caught_insect) + glPopMatrix() + + draw_hud((px, py, pz), yaw, insects, eaten_mosquitos, eaten_flies, font) + + pygame.display.flip() + + pygame.quit() + sys.exit() + +if __name__ == "__main__": + main() diff --git a/previos/libelula_v9.4.py b/previos/libelula_v9.4.py new file mode 100644 index 0000000..3398d7d --- /dev/null +++ b/previos/libelula_v9.4.py @@ -0,0 +1,1204 @@ +import sys +import math +import random +import struct +import pygame +from pygame.locals import * +from OpenGL.GL import * +from OpenGL.GLU import * + +# ========================================== +# CONFIGURACIÓN Y CONSTANTES +# ========================================== +WIDTH, HEIGHT = 800, 600 + +# Colores (R, G, B) +COLOR_BG = (0.12, 0.15, 0.12) # Fondo ambiente +COLOR_WATER = (0.0, 0.5, 0.85) # Agua animada (Cian/Azul) +COLOR_LAND = (0.45, 0.32, 0.15) # Tierra / Relieve +COLOR_LAND_RIVER = (0.40, 0.28, 0.12) # Tierra en riachuelos +COLOR_ISLAND = (0.5, 0.38, 0.18) # Islas +COLOR_REED = (0.15, 0.85, 0.25) # Juncos verdes +COLOR_CATTAIL = (0.35, 0.18, 0.08) # Espigas marrones densas +COLOR_DRAGONFLY = (0.0, 1.0, 1.0) # Libélula del jugador (Cian) +COLOR_PURPLE_DRAGONFLY = (0.7, 0.2, 0.8) # Libélulas decorativas (Morado) +COLOR_MOSQUITO = (1.0, 0.9, 0.0) # Mosquitos (Amarillo) +COLOR_FLY = (0.8, 0.25, 0.15) # Moscas (Rojo-Marrón) +COLOR_FROG = (0.15, 0.85, 0.35) # Rana verde detallada +COLOR_HUD = (1.0, 1.0, 1.0) # Retícula HUD (Blanco) + +# Dimensiones del Mundo +POND_RADIUS = 1800.0 +STREAM_LEN = 900.0 +STREAM_MIN_Y = -(POND_RADIUS + STREAM_LEN) +STREAM_MAX_Y = (POND_RADIUS + STREAM_LEN) +WORLD_LEN = STREAM_MAX_Y - STREAM_MIN_Y + +# Cantidades de insectos +MOSQUITO_MIN, MOSQUITO_MAX = 17, 27 +FLY_MIN, FLY_MAX = 9, 14 + +# ========================================== +# ELEMENTOS DEL MAPA +# ========================================== +REEDS = [ + (-250, 350, 0, 20), (-160, -500, 0, 22), (500, 200, 0, 24), (-700, 200, 0, 21), (200, -800, 0, 23), + (-900, -300, 0, 25), (760, -600, 0, 22), (900, 500, 0, 26), (-500, 900, 0, 23), (600, -1000, 0, 22), + (-1200, 400, 0, 24), (1300, -300, 0, 20), (-1400, -700, 0, 25), (1100, 1200, 0, 23), (-360, -1300, 0, 22), + (-90, -1950, 0, 20), (-110, -2150, 0, 23), (-80, -2350, 0, 19), (-100, -2550, 0, 22), + (90, 1950, 0, 22), (110, 2150, 0, 20), (85, 2350, 0, 24), (105, 2550, 0, 21) +] + +ISLANDS = [ + (-250, 350, 90), (-160, -500, 85), (500, 200, 100), (-700, 200, 110), (200, -800, 95), + (-900, -300, 120), (760, -600, 90), (900, 500, 105), (-500, 900, 115), (600, -1000, 100) +] + +def get_river_center(y): + if y < -POND_RADIUS: + dy = y + POND_RADIUS + return -50 + 100 * math.sin(dy * 0.008) + elif y > POND_RADIUS: + dy = y - POND_RADIUS + return 50 + 100 * math.sin(dy * 0.008) + else: + return (y / POND_RADIUS) * 50.0 + +def is_over_water(x, y): + dist_center = math.hypot(x, y) + if dist_center < POND_RADIUS - 50: + return True + if abs(y) >= POND_RADIUS: + cx = get_river_center(y) + if abs(x - cx) < 80: + return True + return False + +def get_random_water_position(): + while True: + if random.random() < 0.7: + rad = random.uniform(0, math.pi * 2) + r = random.uniform(100, POND_RADIUS - 150) + x = r * math.cos(rad) + y = r * math.sin(rad) + else: + y = random.choice([random.uniform(POND_RADIUS, STREAM_MAX_Y), random.uniform(STREAM_MIN_Y, -POND_RADIUS)]) + cx = get_river_center(y) + x = cx + random.uniform(-60, 60) + + if is_over_water(x, y): + z = random.uniform(5.0, 35.0) + return x, y, z + +# ========================================== +# GENERADOR DE AUDIO PROCEDURAL (SIN ARCHIVOS) +# ========================================== +def generate_buzz_sound(): + try: + sample_rate = 22050 + duration = 0.3 + num_samples = int(sample_rate * duration) + buffer = bytearray() + for i in range(num_samples): + t = i / sample_rate + val = int(32767 * 0.25 * math.sin(2 * math.pi * 180 * t + math.sin(2 * math.pi * 25 * t) * 3)) + buffer.extend(struct.pack(' STREAM_MAX_Y: + self.y -= WORLD_LEN + elif self.y < STREAM_MIN_Y: + self.y += WORLD_LEN + + def draw(self, attached=False): + if self.type == "mosquito": + glColor3f(*COLOR_MOSQUITO) + else: + glColor3f(*COLOR_FLY) + + glPushMatrix() + if not attached: + glTranslatef(self.x, self.y, self.z) + glRotatef(self.yaw, 0, 0, 1) + + s = self.size + glBegin(GL_LINES) + glVertex3f(0, s, 0) + glVertex3f(0, -s, 0) + w_offset = 0.0 if attached else math.sin(self.timer * 30) * (s * 0.8) + glVertex3f(0, 0, 0) + glVertex3f(-s * 1.5, 0, w_offset) + glVertex3f(0, 0, 0) + glVertex3f(s * 1.5, 0, w_offset) + glEnd() + + glPopMatrix() + +class PurpleDragonfly: + def __init__(self, behavior_type): + self.behavior = behavior_type + self.x, self.y, self.z = get_random_water_position() + self.yaw = random.uniform(0, 360) + self.timer = random.uniform(0, 100) + self.wing_timer = random.uniform(0, 100) + self.is_perched = False + self.perch_timer = 0 + + def update(self): + self.wing_timer += 60.0 + if self.is_perched: + self.perch_timer -= 1 + if self.perch_timer <= 0: + self.is_perched = False + return + + self.timer += 0.05 + if self.behavior == 1: + self.yaw += 1.2 + rad = math.radians(self.yaw) + self.x += -math.sin(rad) * 1.1 + self.y += math.cos(rad) * 1.1 + elif self.behavior == 2: + if random.random() < 0.02: + self.yaw += random.uniform(-40, 40) + rad = math.radians(self.yaw) + self.x += -math.sin(rad) * 1.3 + self.y += math.cos(rad) * 1.3 + else: + self.yaw += math.sin(self.timer * 3) * 3.5 + rad = math.radians(self.yaw) + self.x += -math.sin(rad) * 1.4 + self.y += math.cos(rad) * 1.4 + + self.z = 15.0 + math.sin(self.timer * 2) * 6.0 + + if random.random() < 0.0015 and REEDS: + reed = random.choice(REEDS) + self.x, self.y, self.z = reed[0], reed[1], reed[2] + reed[3] + self.is_perched = True + self.perch_timer = random.randint(300, 600) + + if not is_over_water(self.x, self.y): + self.yaw += 150.0 + + if self.y > STREAM_MAX_Y: + self.y -= WORLD_LEN + elif self.y < STREAM_MIN_Y: + self.y += WORLD_LEN + + def draw(self): + glColor3f(*COLOR_PURPLE_DRAGONFLY) + glPushMatrix() + glTranslatef(self.x, self.y, self.z) + glRotatef(self.yaw, 0, 0, 1) + + glBegin(GL_LINES) + glVertex3f(0, 1.5, 0) + glVertex3f(0, -3.0, 0) + glVertex3f(-0.4, 0.6, 0) + glVertex3f(0.4, 0.6, 0) + glVertex3f(-0.3, -0.6, 0) + glVertex3f(0.3, -0.6, 0) + glEnd() + + glBegin(GL_LINE_LOOP) + for i in range(8): + angle = 2.0 * math.pi * i / 8 + glVertex3f(0.3 * math.cos(angle), 1.4 + 0.3 * math.sin(angle), 0) + glEnd() + + wing_angle = 0.0 if self.is_perched else math.sin(self.wing_timer) * 35.0 + rad_wing = math.radians(wing_angle) + w_z = math.sin(rad_wing) * 1.2 if wing_angle != 0 else 0.0 + + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(-4.5, 1.0, w_z) + glVertex3f(-3.8, 0.2, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(4.5, 1.0, w_z) + glVertex3f(3.8, 0.2, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(-3.8, -0.2, w_z) + glVertex3f(-3.0, -0.8, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(3.8, -0.2, w_z) + glVertex3f(3.0, -0.8, w_z) + glEnd() + + glPopMatrix() + +class Frog: + def __init__(self): + self.pick_new_destination() + self.x, self.y = self.target_x, self.target_y + self.z = 0.6 + self.speed = 0.35 + self.yaw = 0.0 + self.swim_timer = 0.0 + + def pick_new_destination(self): + destinations = [(rx, ry) for rx, ry, _, _ in REEDS] + [(ix, iy) for ix, iy, _ in ISLANDS] + dest = random.choice(destinations) + self.target_x, self.target_y = dest[0] + random.uniform(-30, 30), dest[1] + random.uniform(-30, 30) + + def update(self): + self.swim_timer += 0.15 + dx = self.target_x - self.x + dy = self.target_y - self.y + dist = math.hypot(dx, dy) + + if dist < 6.0: + self.pick_new_destination() + else: + self.yaw = math.degrees(math.atan2(dy, dx)) - 90.0 + self.x += (dx / dist) * self.speed + self.y += (dy / dist) * self.speed + + def draw(self, custom_x=None, custom_y=None, custom_z=None, scale=1.0, menu_icon=False): + glColor3f(*COLOR_FROG) + glPushMatrix() + if custom_x is not None and custom_y is not None and custom_z is not None: + glTranslatef(custom_x, custom_y, custom_z) + glScalef(scale, scale, scale) + else: + glTranslatef(self.x, self.y, self.z) + glRotatef(self.yaw, 0, 0, 1) + + leg_offset = 0.0 if menu_icon else math.sin(self.swim_timer) * 1.5 + + glBegin(GL_LINES) + glVertex3f(0.0, 0.0, 0.5) + glVertex3f(0.0, -5.0, 0.5) + glVertex3f(-2.0, 2.0, 0.5) + glVertex3f(2.0, 2.0, 0.5) + glVertex3f(-2.5, -2.0, 0.5) + glVertex3f(2.5, -2.0, 0.5) + + glVertex3f(-2.0, 2.0, 0.5) + glVertex3f(0.0, 0.0, 0.5) + glVertex3f(2.0, 2.0, 0.5) + glVertex3f(0.0, 0.0, 0.5) + + glVertex3f(-2.0, 2.0, 0.6) + glVertex3f(2.0, 2.0, 0.6) + glVertex3f(-2.0, 2.0, 0.6) + glVertex3f(0.0, 4.5, 0.6) + glVertex3f(2.0, 2.0, 0.6) + glVertex3f(0.0, 4.5, 0.6) + + glVertex3f(-1.5, 3.5, 1.5) + glVertex3f(-1.5, 3.5, 0.6) + glVertex3f(1.5, 3.5, 1.5) + glVertex3f(1.5, 3.5, 0.6) + + glVertex3f(-2.0, 2.0, 0.5) + glVertex3f(-4.0, 3.0, 0.0) + glVertex3f(-4.0, 3.0, 0.0) + glVertex3f(-3.5, 4.0, 0.0) + + glVertex3f(2.0, 2.0, 0.5) + glVertex3f(4.0, 3.0, 0.0) + glVertex3f(4.0, 3.0, 0.0) + glVertex3f(3.5, 4.0, 0.0) + + # Patas traseras con los pies hacia el exterior + glVertex3f(-2.5, -2.0, 0.5) + glVertex3f(-5.0 - leg_offset, -4.0, 0.0) + glVertex3f(-5.0 - leg_offset, -4.0, 0.0) + glVertex3f(-6.5 - leg_offset, -3.2, 0.0) + + glVertex3f(2.5, -2.0, 0.5) + glVertex3f(5.0 + leg_offset, -4.0, 0.0) + glVertex3f(5.0 + leg_offset, -4.0, 0.0) + glVertex3f(6.5 + leg_offset, -3.2, 0.0) + glEnd() + + if menu_icon: + glBegin(GL_LINE_LOOP) + glVertex3f(-1.5, -1.5, 0.4) + glVertex3f(1.5, -1.5, 0.4) + glVertex3f(1.5, 1.5, 0.4) + glVertex3f(-1.5, 1.5, 0.4) + glEnd() + + glPopMatrix() + +# ========================================== +# RENDERIZADO DE ESCENARIO Y MODELOS +# ========================================== +def draw_water_surface(time_counter): + glColor3f(*COLOR_WATER) + + grid_size = 15 + for x in range(int(-POND_RADIUS), int(POND_RADIUS), grid_size * 2): + glBegin(GL_LINE_STRIP) + for y in range(int(-POND_RADIUS), int(-POND_RADIUS) + int(POND_RADIUS)*2, grid_size): + dist = math.hypot(x, y) + if dist < POND_RADIUS - 30: + z = 1.0 * math.sin(dist * 0.015 - time_counter * 3.0) + 0.5 * math.cos(x * 0.01 + time_counter) + glVertex3f(x, y, z) + glEnd() + + for y in range(int(STREAM_MIN_Y), int(STREAM_MAX_Y), 15): + if abs(y) < POND_RADIUS: + continue + cx = get_river_center(y) + glBegin(GL_LINE_STRIP) + for dx in range(-100, 110, 10): + x = cx + dx + z = 0.5 * math.sin(y * 0.03 + dx * 0.08 + time_counter * 4.0) + glVertex3f(x, y, z) + glEnd() + +def draw_terrain(): + glColor3f(*COLOR_LAND) + + grid_step = 120 + for y in range(int(STREAM_MIN_Y - 300), int(STREAM_MAX_Y + 300), grid_step): + glBegin(GL_LINE_STRIP) + for x in range(-2500, 2500, grid_step): + z = 1.5 * math.sin(x * 0.005) + 1.5 * math.cos(y * 0.005) + glVertex3f(x, y, z) + glEnd() + + width_river = 100.0 + margin_land = 70.0 + glColor3f(*COLOR_LAND_RIVER) + + for y in range(int(STREAM_MIN_Y), int(STREAM_MAX_Y), 30): + if abs(y) < POND_RADIUS: + continue + cx = get_river_center(y) + + glBegin(GL_LINE_STRIP) + for dx in range(int(-width_river - margin_land), int(-width_river), 15): + x = cx + dx + z = 0.6 * math.sin(y * 0.02 + x * 0.02) + 0.3 + glVertex3f(x, y, z) + glEnd() + + glBegin(GL_LINE_STRIP) + for dx in range(int(width_river), int(width_river + margin_land), 15): + x = cx + dx + z = 0.6 * math.sin(y * 0.02 + x * 0.02) + 0.3 + glVertex3f(x, y, z) + glEnd() + + for ix, iy, ir in ISLANDS: + glColor3f(*COLOR_ISLAND) + for layer in range(1, 4): + rad_layer = ir * (1 - layer * 0.25) + height = layer * 1.2 + glBegin(GL_LINE_LOOP) + for i in range(16): + angle = 2.0 * math.pi * i / 16 + dx = rad_layer * math.cos(angle) + dy = rad_layer * math.sin(angle) + glVertex3f(ix + dx, iy + dy, height) + glEnd() + +def draw_map(time_counter): + draw_terrain() + draw_water_surface(time_counter) + + offsets = [(-1.5, 0.6), (1.3, -0.6), (-0.7, -1.4), (1.1, 1.2), (0.0, 1.6), (-1.2, 0.0)] + + for rx, ry, rz, rh in REEDS: + glColor3f(*COLOR_REED) + glBegin(GL_LINES) + glVertex3f(rx, ry, rz) + glVertex3f(rx, ry, rz + rh) + for ox, oy in offsets: + glVertex3f(rx, ry, rz) + glVertex3f(rx + ox, ry + oy, rz + rh * 0.85) + glEnd() + + glColor3f(*COLOR_CATTAIL) + glBegin(GL_LINES) + for d in [-0.2, 0.0, 0.2]: + glVertex3f(rx + d, ry, rz + rh - 4.5) + glVertex3f(rx + d, ry, rz + rh) + for ox, oy in offsets: + for d in [-0.15, 0.15]: + glVertex3f(rx + ox + d, ry + oy, (rz + rh * 0.85) - 3.8) + glVertex3f(rx + ox + d, ry + oy, rz + rh * 0.85) + glEnd() + +def draw_dragonfly(wing_angle, caught_insect=None): + glColor3f(*COLOR_DRAGONFLY) + + glBegin(GL_LINES) + glVertex3f(0, 1.5, 0) + glVertex3f(0, -3.0, 0) + glVertex3f(-0.4, 0.6, 0) + glVertex3f(0.4, 0.6, 0) + glVertex3f(-0.3, -0.6, 0) + glVertex3f(0.3, -0.6, 0) + + glVertex3f(-0.3, 0.5, 0) + glVertex3f(-0.2, 0.2, -0.6) + glVertex3f(0.3, 0.5, 0) + glVertex3f(0.2, 0.2, -0.6) + glVertex3f(-0.2, -0.2, 0) + glVertex3f(-0.1, -0.3, -0.6) + glVertex3f(0.2, -0.2, 0) + glVertex3f(0.1, -0.3, -0.6) + glEnd() + + glBegin(GL_LINE_LOOP) + for i in range(8): + angle = 2.0 * math.pi * i / 8 + glVertex3f(0.3 * math.cos(angle), 1.4 + 0.3 * math.sin(angle), 0) + glEnd() + + rad_wing = math.radians(wing_angle) + w_z = math.sin(rad_wing) * 1.2 if wing_angle != 0 else 0.0 + + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(-4.5, 1.0, w_z) + glVertex3f(-3.8, 0.2, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(4.5, 1.0, w_z) + glVertex3f(3.8, 0.2, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(-3.8, -0.2, w_z) + glVertex3f(-3.0, -0.8, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(3.8, -0.2, w_z) + glVertex3f(3.0, -0.8, w_z) + glEnd() + + if caught_insect: + glPushMatrix() + glTranslatef(0.0, 0.2, -0.6) + caught_insect.draw(attached=True) + glPopMatrix() + +# ========================================== +# INTERFAZ 2D / HUD Y PANTALLA INICIAL +# ========================================== +def draw_text_2d(text_surface, x, y): + text_data = pygame.image.tostring(text_surface, "RGBA", True) + w, h = text_surface.get_size() + + glEnable(GL_BLEND) + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) + + glRasterPos2i(x, y) + glDrawPixels(w, h, GL_RGBA, GL_UNSIGNED_BYTE, text_data) + glDisable(GL_BLEND) + +def draw_start_screen(font_title, font_subtitle, font_text, wing_angle, menu_wing_angle, frog_instance): + glDisable(GL_DEPTH_TEST) + + glMatrixMode(GL_PROJECTION) + glPushMatrix() + glLoadIdentity() + gluOrtho2D(0, WIDTH, 0, HEIGHT) + + glMatrixMode(GL_MODELVIEW) + glPushMatrix() + glLoadIdentity() + + # TÍTULO PRINCIPAL + txt_title = font_title.render("LIBÉLULA", True, (0, 220, 255)) + draw_text_2d(txt_title, WIDTH // 2 - 130, HEIGHT - 100) + + # LIBÉLULA SUPERIOR AL LADO DEL TÍTULO (CON AMPLITUD DE ALAS AMPLIADA) + glPushMatrix() + glTranslatef(WIDTH // 2 + 150, HEIGHT - 75, 0.0) + glScalef(12.0, 12.0, 1.0) + + glColor3f(*COLOR_DRAGONFLY) + glBegin(GL_LINES) + glVertex3f(0, 1.5, 0) + glVertex3f(0, -3.0, 0) + glVertex3f(-0.4, 0.6, 0) + glVertex3f(0.4, 0.6, 0) + glEnd() + + # Amplitud mayor amplificada para que se aprecie con claridad el aleteo + rad_menu_w = math.radians(menu_wing_angle) + menu_w_off = math.sin(rad_menu_w) * 3.5 + + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(-4.5, 1.0, menu_w_off) + glVertex3f(-3.8, 0.2, menu_w_off) + glEnd() + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(4.5, 1.0, menu_w_off) + glVertex3f(3.8, 0.2, menu_w_off) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(-3.8, -0.2, menu_w_off) + glVertex3f(-3.0, -0.8, menu_w_off) + glEnd() + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(3.8, -0.2, menu_w_off) + glVertex3f(3.0, -0.8, menu_w_off) + glEnd() + + glPopMatrix() + + # OBJETIVO DEL JUEGO + obj_line1 = font_text.render("Objetivo: Vuela libre por el estanque y sus arroyos capturando moscas", True, (255, 255, 255)) + obj_line2 = font_text.render("y mosquitos. Una vez cazados debes devorarlos posado en un junco.", True, (255, 255, 255)) + draw_text_2d(obj_line1, 70, HEIGHT - 170) + draw_text_2d(obj_line2, 70, HEIGHT - 195) + + # CONTROLES Y TECLAS + controls = [ + "CONTROLES:", + "W / S : Acelerar / Frenar", + "Flecha Arriba / Abajo : Cambiar altitud", + "Flecha Izquierda / Derecha : Girar / Virar", + "ESPACIO : Picado rápido para caza", + "P : Aterrizaje automático en junco cercano", + "ESC : Salir del juego" + ] + + y_pos = HEIGHT - 250 + for ctrl in controls: + is_title = ctrl.endswith(":") + col = (255, 220, 50) if is_title else (200, 200, 200) + t_surf = font_subtitle.render(ctrl, True, col) if is_title else font_text.render(ctrl, True, col) + draw_text_2d(t_surf, 70, y_pos) + y_pos -= 25 + + # LEYENDA VISUAL INFERIOR + y_icon = 90 + + # 1. Mosquito + glColor3f(*COLOR_MOSQUITO) + glBegin(GL_LINES) + glVertex2f(60, y_icon + 5) + glVertex2f(60, y_icon - 5) + glVertex2f(55, y_icon) + glVertex2f(65, y_icon) + glEnd() + txt_m = font_text.render("Mosquito", True, (255, 230, 0)) + draw_text_2d(txt_m, 75, y_icon - 8) + + # 2. Mosca + glColor3f(*COLOR_FLY) + glBegin(GL_LINES) + glVertex2f(210, y_icon + 5) + glVertex2f(210, y_icon - 5) + glVertex2f(205, y_icon - 3) + glVertex2f(215, y_icon + 3) + glVertex2f(205, y_icon + 3) + glVertex2f(215, y_icon - 3) + glEnd() + txt_f = font_text.render("Mosca", True, (200, 65, 40)) + draw_text_2d(txt_f, 225, y_icon - 8) + + # 3. Junco + glColor3f(*COLOR_REED) + glBegin(GL_LINES) + glVertex2f(340, y_icon - 10) + glVertex2f(340, y_icon + 15) + glEnd() + glColor3f(*COLOR_CATTAIL) + glBegin(GL_LINES) + glVertex2f(340, y_icon + 8) + glVertex2f(340, y_icon + 15) + glEnd() + txt_r = font_text.render("Junco", True, (100, 220, 100)) + draw_text_2d(txt_r, 355, y_icon - 8) + + # 4. Rana + frog_instance.draw(custom_x=475, custom_y=y_icon, custom_z=0.0, scale=3.5, menu_icon=True) + txt_frog = font_text.render("Rana", True, (100, 255, 100)) + draw_text_2d(txt_frog, 505, y_icon - 8) + + # 5. Libélulas (Moradas) + rad_w = math.radians(wing_angle) + w_off = math.sin(rad_w) * 1.5 + + glPushMatrix() + glTranslatef(625, y_icon, 0.0) + glScalef(6.0, 6.0, 1.0) + glColor3f(*COLOR_PURPLE_DRAGONFLY) + glBegin(GL_LINES) + glVertex3f(0, 1.5, 0) + glVertex3f(0, -3.0, 0) + glVertex3f(-0.4, 0.6, 0) + glVertex3f(0.4, 0.6, 0) + glEnd() + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(-4.5, 1.0, w_off) + glVertex3f(-3.8, 0.2, w_off) + glEnd() + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(4.5, 1.0, w_off) + glVertex3f(3.8, 0.2, w_off) + glEnd() + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(-3.8, -0.2, w_off) + glVertex3f(-3.0, -0.8, w_off) + glEnd() + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(3.8, -0.2, w_off) + glVertex3f(3.0, -0.8, w_off) + glEnd() + glPopMatrix() + + txt_df = font_text.render("Libélulas", True, (178, 51, 204)) + draw_text_2d(txt_df, 654, y_icon - 8) + + # MENSAJE PARA COMENZAR + start_msg = font_subtitle.render("Pulsa INTRO o cualquier tecla para comenzar...", True, (100, 255, 100)) + draw_text_2d(start_msg, WIDTH // 2 - 200, 30) + + glMatrixMode(GL_PROJECTION) + glPopMatrix() + glMatrixMode(GL_MODELVIEW) + glPopMatrix() + glEnable(GL_DEPTH_TEST) + +def draw_hud(player_pos, yaw, insects, eaten_mosquitos, eaten_flies, font): + glDisable(GL_DEPTH_TEST) + + glMatrixMode(GL_PROJECTION) + glPushMatrix() + glLoadIdentity() + gluOrtho2D(0, WIDTH, 0, HEIGHT) + + glMatrixMode(GL_MODELVIEW) + glPushMatrix() + glLoadIdentity() + + px, py, pz = player_pos + + cx, cy = WIDTH // 2, HEIGHT - 35 + radius = 22 + + glColor3f(*COLOR_HUD) + glBegin(GL_LINE_LOOP) + for i in range(24): + angle = 2.0 * math.pi * i / 24 + glVertex2f(cx + radius * math.cos(angle), cy + radius * math.sin(angle)) + glEnd() + + glBegin(GL_LINES) + glVertex2f(cx, cy + radius) + glVertex2f(cx, cy + radius - 5) + glVertex2f(cx, cy - radius) + glVertex2f(cx, cy - radius + 5) + glVertex2f(cx - radius, cy) + glVertex2f(cx - radius + 5, cy) + glVertex2f(cx + radius, cy) + glVertex2f(cx + radius - 5, cy) + glEnd() + + closest_insect = None + min_dist = 999999.0 + relative_angle = 0.0 + diff_z = 0.0 + + for insect in insects: + dx = insect.x - px + dy = insect.y - py + dist = math.hypot(dx, dy) + + if dist < min_dist and dist > 0.1: + angle_to_insect = math.degrees(math.atan2(dy, dx)) + heading = (angle_to_insect - (90 - yaw)) % 360 + if heading > 180: + heading -= 360 + + if abs(heading) < 60: + min_dist = dist + closest_insect = insect + relative_angle = heading + diff_z = insect.z - pz + + if closest_insect: + radar_x = cx - (relative_angle / 60.0) * (radius - 4) + radar_y = cy + (diff_z / 20.0) * (radius - 4) + + dist_from_center = math.hypot(radar_x - cx, radar_y - cy) + if dist_from_center > (radius - 3): + factor = (radius - 3) / dist_from_center + radar_x = cx + (radar_x - cx) * factor + radar_y = cy + (radar_y - cy) * factor + + glColor3f(1.0, 1.0, 1.0) + glBegin(GL_LINE_LOOP) + for i in range(8): + ang = 2.0 * math.pi * i / 8 + glVertex2f(radar_x + 3 * math.cos(ang), radar_y + 3 * math.sin(ang)) + glEnd() + + glColor3f(*COLOR_MOSQUITO) + glBegin(GL_LINES) + glVertex2f(25, HEIGHT - 33) + glVertex2f(25, HEIGHT - 27) + glVertex2f(21, HEIGHT - 30) + glVertex2f(29, HEIGHT - 30) + glEnd() + + glColor3f(*COLOR_FLY) + glBegin(GL_LINES) + glVertex2f(WIDTH - 82, HEIGHT - 33) + glVertex2f(WIDTH - 82, HEIGHT - 27) + glVertex2f(WIDTH - 86, HEIGHT - 28) + glVertex2f(WIDTH - 78, HEIGHT - 32) + glVertex2f(WIDTH - 86, HEIGHT - 32) + glVertex2f(WIDTH - 78, HEIGHT - 28) + glEnd() + + txt_mosq = font.render(f"x {eaten_mosquitos}", True, (255, 230, 0)) + txt_flies = font.render(f"x {eaten_flies}", True, (200, 65, 40)) + + draw_text_2d(txt_mosq, 38, HEIGHT - 40) + draw_text_2d(txt_flies, WIDTH - 68, HEIGHT - 40) + + glMatrixMode(GL_PROJECTION) + glPopMatrix() + glMatrixMode(GL_MODELVIEW) + glPopMatrix() + glEnable(GL_DEPTH_TEST) + +# ========================================== +# BUCLE PRINCIPAL +# ========================================== +def main(): + pygame.init() + pygame.mixer.init(frequency=22050, size=-16, channels=1) + pygame.font.init() + + font = pygame.font.SysFont("Arial", 22, bold=True) + font_title = pygame.font.SysFont("Arial", 48, bold=True) + font_subtitle = pygame.font.SysFont("Arial", 20, bold=True) + font_text = pygame.font.SysFont("Arial", 16, bold=False) + + pygame.display.set_mode((WIDTH, HEIGHT), DOUBLEBUF | OPENGL) + pygame.display.set_caption("Libélula") + + glEnable(GL_DEPTH_TEST) + glClearColor(*COLOR_BG, 1.0) + + glMatrixMode(GL_PROJECTION) + gluPerspective(50, (WIDTH / HEIGHT), 0.1, 5000.0) + glMatrixMode(GL_MODELVIEW) + + buzz_sound = generate_buzz_sound() + wind_sound = generate_wind_sound() + + px, py, pz = 0.0, 0.0, 15.0 + yaw = 0.0 + roll = 0.0 + pitch = 0.0 + max_roll = 30.0 + max_pitch = 20.0 + + speed = 0.0 + max_speed = 3.0 + acceleration = 0.06 + deceleration = 0.03 + + min_altitude = 1.0 + max_altitude = 120.0 + + wing_timer = 0.0 + water_timer = 0.0 + is_perched = False + + dive_phase = "none" + pre_dive_alt = 15.0 + pre_dive_speed = 0.0 + + auto_landing = False + target_perch = None + + caught_insect = None + eaten_mosquitos = 0 + eaten_flies = 0 + + frog = Frog() + + purple_dragonflies = [ + PurpleDragonfly(1), + PurpleDragonfly(2), + PurpleDragonfly(3) + ] + + insects = [] + num_mosquitos = random.randint(MOSQUITO_MIN, MOSQUITO_MAX) + num_flies = random.randint(FLY_MIN, FLY_MAX) + + for _ in range(num_mosquitos): + insects.append(Insect("mosquito")) + for _ in range(num_flies): + insects.append(Insect("fly")) + + clock = pygame.time.Clock() + + in_start_screen = True + running = True + + buzz_timer = 0 + + while running: + dt = clock.tick(60) / 1000.0 + water_timer += dt + wing_timer += 60.0 + + for event in pygame.event.get(): + if event.type == QUIT or (event.type == KEYDOWN and event.key == K_ESCAPE): + running = False + elif event.type == KEYDOWN: + if in_start_screen: + in_start_screen = False + else: + if event.key == K_SPACE: + if not is_perched and dive_phase == "none": + dive_phase = "diving" + pre_dive_alt = pz + pre_dive_speed = speed + auto_landing = False + elif event.key == K_p: + if not is_perched and dive_phase == "none": + best_reed = None + min_d = 999.0 + for rx, ry, rz, rh in REEDS: + dist = math.hypot(px - rx, py - ry) + if dist < 35.0: + angle_to_reed = math.degrees(math.atan2(ry - py, rx - px)) + heading = (angle_to_reed - (90 - yaw)) % 360 + if heading > 180: + heading -= 360 + if abs(heading) < 45 and dist < min_d: + min_d = dist + best_reed = (rx, ry, rz + rh) + + if best_reed: + target_perch = best_reed + auto_landing = True + + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) + glLoadIdentity() + + if in_start_screen: + cam_x, cam_y, cam_z = 0.0, -200.0, 50.0 + gluLookAt(cam_x, cam_y, cam_z, 0, 0, 15, 0, 0, 1) + + draw_map(water_timer) + frog.update() + frog.draw() + for p_df in purple_dragonflies: + p_df.update() + p_df.draw() + + wing_angle = math.sin(wing_timer * 0.5) * 35.0 + menu_wing_angle = math.sin(wing_timer * 0.8) * 45.0 + draw_start_screen(font_title, font_subtitle, font_text, wing_angle, menu_wing_angle, frog) + else: + keys = pygame.key.get_pressed() + + if is_perched: + if keys[K_LEFT]: + yaw += 3.0 + if keys[K_RIGHT]: + yaw -= 3.0 + + if keys[K_w]: + is_perched = False + speed = 0.5 + + wing_angle = 0.0 + pitch = 0.0 + roll = 0.0 + dive_phase = "none" + auto_landing = False + else: + if auto_landing and target_perch: + tx, ty, tz = target_perch + dx = tx - px + dy = ty - py + dist_to_target = math.hypot(dx, dy) + + speed = 1.2 + px += (dx / dist_to_target) * speed if dist_to_target > 0.1 else 0 + py += (dy / dist_to_target) * speed if dist_to_target > 0.1 else 0 + pz += (tz - pz) * 0.1 + + if dist_to_target < 2.5 and abs(pz - tz) < 2.5: + px, py, pz = target_perch + speed = 0.0 + roll = 0.0 + pitch = 0.0 + is_perched = True + auto_landing = False + target_perch = None + + if caught_insect: + if caught_insect.type == "mosquito": + eaten_mosquitos += 1 + else: + eaten_flies += 1 + caught_insect = None + + elif dive_phase == "diving": + speed = max_speed * 1.5 + pz -= 1.2 + target_pitch = -45.0 + if wind_sound and not pygame.mixer.get_busy(): + wind_sound.play() + + if pz <= min_altitude: + pz = min_altitude + dive_phase = "recovering" + + elif dive_phase == "recovering": + target_pitch = 30.0 + pz += 0.8 + speed += (pre_dive_speed - speed) * 0.1 + + if pz >= pre_dive_alt: + pz = pre_dive_alt + speed = pre_dive_speed + dive_phase = "none" + else: + if keys[K_w]: + speed = min(speed + acceleration, max_speed) + if speed > 2.0 and wind_sound and random.random() < 0.1 and not pygame.mixer.get_busy(): + wind_sound.play() + elif keys[K_s]: + speed = max(speed - deceleration, 0.0) + + if speed < 0.3: + for rx, ry, rz, rh in REEDS: + dist = math.hypot(px - rx, py - ry) + if dist < 10.0 and abs(pz - (rz + rh)) < 10.0: + px, py, pz = rx, ry, rz + rh + speed = 0.0 + roll = 0.0 + pitch = 0.0 + is_perched = True + + if caught_insect: + if caught_insect.type == "mosquito": + eaten_mosquitos += 1 + else: + eaten_flies += 1 + caught_insect = None + break + + target_pitch = 0.0 + if keys[K_UP]: + pz = min(pz + 0.8, max_altitude) + target_pitch = max_pitch + elif keys[K_DOWN]: + pz = max(pz - 0.8, min_altitude) + target_pitch = -max_pitch + + if not auto_landing: + pitch += (target_pitch - pitch) * 0.15 + + target_roll = 0.0 + if keys[K_LEFT]: + yaw += 2.8 + target_roll = -max_roll + elif keys[K_RIGHT]: + yaw -= 2.8 + target_roll = max_roll + + roll += (target_roll - roll) * 0.15 + + rad_yaw = math.radians(yaw) + px += -math.sin(rad_yaw) * speed + py += math.cos(rad_yaw) * speed + + if abs(py) < POND_RADIUS: + max_limit_x = POND_RADIUS + 200.0 + else: + cx = get_river_center(py) + max_limit_x = abs(cx) + 200.0 + + px = max(-max_limit_x, min(max_limit_x, px)) + + if py > STREAM_MAX_Y: + py -= WORLD_LEN + elif py < STREAM_MIN_Y: + py += WORLD_LEN + + wing_timer += 80.0 if (dive_phase != "none" or auto_landing) else (60.0 if speed > 0.1 else 30.0) + wing_angle = math.sin(wing_timer) * 35.0 + + frog.update() + + for p_df in purple_dragonflies: + p_df.update() + + current_mosquitos = 0 + current_flies = 0 + near_insect = False + + for insect in insects[:]: + insect.update((px, py, pz)) + + if insect.type == "mosquito": + current_mosquitos += 1 + else: + current_flies += 1 + + dist_to_df = math.sqrt((px - insect.x)**2 + (py - insect.y)**2 + (pz - insect.z)**2) + if dist_to_df < 40.0: + near_insect = True + + if not caught_insect: + if dist_to_df < 6.0: + caught_insect = insect + insects.remove(insect) + if dive_phase == "diving": + dive_phase = "recovering" + + if near_insect and buzz_sound: + buzz_timer += 1 + if buzz_timer > 25 and not pygame.mixer.get_busy(): + buzz_sound.play() + buzz_timer = 0 + + if current_mosquitos < MOSQUITO_MIN: + insects.append(Insect("mosquito")) + if current_flies < FLY_MIN: + insects.append(Insect("fly")) + + cam_dist = 18.0 + cam_height = 8.0 + rad_yaw = math.radians(yaw) + + cam_x = px + math.sin(rad_yaw) * cam_dist + cam_y = py - math.cos(rad_yaw) * cam_dist + cam_z = pz + cam_height + + gluLookAt(cam_x, cam_y, cam_z, + px, py, pz + 0.5, + 0, 0, 1) + + draw_map(water_timer) + frog.draw() + + for p_df in purple_dragonflies: + p_df.draw() + + for insect in insects: + insect.draw() + + glPushMatrix() + glTranslatef(px, py, pz) + glRotatef(yaw, 0, 0, 1) + glRotatef(pitch, 1, 0, 0) + glRotatef(roll, 0, 1, 0) + draw_dragonfly(wing_angle, caught_insect) + glPopMatrix() + + draw_hud((px, py, pz), yaw, insects, eaten_mosquitos, eaten_flies, font) + + pygame.display.flip() + + pygame.quit() + sys.exit() + +if __name__ == "__main__": + main() diff --git a/previos/libelula_v9.5.py b/previos/libelula_v9.5.py new file mode 100644 index 0000000..24a09b5 --- /dev/null +++ b/previos/libelula_v9.5.py @@ -0,0 +1,1204 @@ +import sys +import math +import random +import struct +import pygame +from pygame.locals import * +from OpenGL.GL import * +from OpenGL.GLU import * + +# ========================================== +# CONFIGURACIÓN Y CONSTANTES +# ========================================== +WIDTH, HEIGHT = 800, 600 + +# Colores (R, G, B) +COLOR_BG = (0.12, 0.15, 0.12) # Fondo ambiente +COLOR_WATER = (0.0, 0.5, 0.85) # Agua animada (Cian/Azul) +COLOR_LAND = (0.45, 0.32, 0.15) # Tierra / Relieve +COLOR_LAND_RIVER = (0.40, 0.28, 0.12) # Tierra en riachuelos +COLOR_ISLAND = (0.5, 0.38, 0.18) # Islas +COLOR_REED = (0.15, 0.85, 0.25) # Juncos verdes +COLOR_CATTAIL = (0.35, 0.18, 0.08) # Espigas marrones densas +COLOR_DRAGONFLY = (0.0, 1.0, 1.0) # Libélula del jugador (Cian) +COLOR_PURPLE_DRAGONFLY = (0.7, 0.2, 0.8) # Libélulas decorativas (Morado) +COLOR_MOSQUITO = (1.0, 0.9, 0.0) # Mosquitos (Amarillo) +COLOR_FLY = (0.8, 0.25, 0.15) # Moscas (Rojo-Marrón) +COLOR_FROG = (0.15, 0.85, 0.35) # Rana verde detallada +COLOR_HUD = (1.0, 1.0, 1.0) # Retícula HUD (Blanco) + +# Dimensiones del Mundo +POND_RADIUS = 1800.0 +STREAM_LEN = 900.0 +STREAM_MIN_Y = -(POND_RADIUS + STREAM_LEN) +STREAM_MAX_Y = (POND_RADIUS + STREAM_LEN) +WORLD_LEN = STREAM_MAX_Y - STREAM_MIN_Y + +# Cantidades de insectos +MOSQUITO_MIN, MOSQUITO_MAX = 17, 27 +FLY_MIN, FLY_MAX = 9, 14 + +# ========================================== +# ELEMENTOS DEL MAPA +# ========================================== +REEDS = [ + (-250, 350, 0, 20), (-160, -500, 0, 22), (500, 200, 0, 24), (-700, 200, 0, 21), (200, -800, 0, 23), + (-900, -300, 0, 25), (760, -600, 0, 22), (900, 500, 0, 26), (-500, 900, 0, 23), (600, -1000, 0, 22), + (-1200, 400, 0, 24), (1300, -300, 0, 20), (-1400, -700, 0, 25), (1100, 1200, 0, 23), (-360, -1300, 0, 22), + (-90, -1950, 0, 20), (-110, -2150, 0, 23), (-80, -2350, 0, 19), (-100, -2550, 0, 22), + (90, 1950, 0, 22), (110, 2150, 0, 20), (85, 2350, 0, 24), (105, 2550, 0, 21) +] + +ISLANDS = [ + (-250, 350, 90), (-160, -500, 85), (500, 200, 100), (-700, 200, 110), (200, -800, 95), + (-900, -300, 120), (760, -600, 90), (900, 500, 105), (-500, 900, 115), (600, -1000, 100) +] + +def get_river_center(y): + if y < -POND_RADIUS: + dy = y + POND_RADIUS + return -50 + 100 * math.sin(dy * 0.008) + elif y > POND_RADIUS: + dy = y - POND_RADIUS + return 50 + 100 * math.sin(dy * 0.008) + else: + return (y / POND_RADIUS) * 50.0 + +def is_over_water(x, y): + dist_center = math.hypot(x, y) + if dist_center < POND_RADIUS - 50: + return True + if abs(y) >= POND_RADIUS: + cx = get_river_center(y) + if abs(x - cx) < 80: + return True + return False + +def get_random_water_position(): + while True: + if random.random() < 0.7: + rad = random.uniform(0, math.pi * 2) + r = random.uniform(100, POND_RADIUS - 150) + x = r * math.cos(rad) + y = r * math.sin(rad) + else: + y = random.choice([random.uniform(POND_RADIUS, STREAM_MAX_Y), random.uniform(STREAM_MIN_Y, -POND_RADIUS)]) + cx = get_river_center(y) + x = cx + random.uniform(-60, 60) + + if is_over_water(x, y): + z = random.uniform(5.0, 35.0) + return x, y, z + +# ========================================== +# GENERADOR DE AUDIO PROCEDURAL (SIN ARCHIVOS) +# ========================================== +def generate_buzz_sound(): + try: + sample_rate = 22050 + duration = 0.3 + num_samples = int(sample_rate * duration) + buffer = bytearray() + for i in range(num_samples): + t = i / sample_rate + val = int(32767 * 0.25 * math.sin(2 * math.pi * 180 * t + math.sin(2 * math.pi * 25 * t) * 3)) + buffer.extend(struct.pack(' STREAM_MAX_Y: + self.y -= WORLD_LEN + elif self.y < STREAM_MIN_Y: + self.y += WORLD_LEN + + def draw(self, attached=False): + if self.type == "mosquito": + glColor3f(*COLOR_MOSQUITO) + else: + glColor3f(*COLOR_FLY) + + glPushMatrix() + if not attached: + glTranslatef(self.x, self.y, self.z) + glRotatef(self.yaw, 0, 0, 1) + + s = self.size + glBegin(GL_LINES) + glVertex3f(0, s, 0) + glVertex3f(0, -s, 0) + w_offset = 0.0 if attached else math.sin(self.timer * 30) * (s * 0.8) + glVertex3f(0, 0, 0) + glVertex3f(-s * 1.5, 0, w_offset) + glVertex3f(0, 0, 0) + glVertex3f(s * 1.5, 0, w_offset) + glEnd() + + glPopMatrix() + +class PurpleDragonfly: + def __init__(self, behavior_type): + self.behavior = behavior_type + self.x, self.y, self.z = get_random_water_position() + self.yaw = random.uniform(0, 360) + self.timer = random.uniform(0, 100) + self.wing_timer = random.uniform(0, 100) + self.is_perched = False + self.perch_timer = 0 + + def update(self): + self.wing_timer += 60.0 + if self.is_perched: + self.perch_timer -= 1 + if self.perch_timer <= 0: + self.is_perched = False + return + + self.timer += 0.05 + if self.behavior == 1: + self.yaw += 1.2 + rad = math.radians(self.yaw) + self.x += -math.sin(rad) * 1.1 + self.y += math.cos(rad) * 1.1 + elif self.behavior == 2: + if random.random() < 0.02: + self.yaw += random.uniform(-40, 40) + rad = math.radians(self.yaw) + self.x += -math.sin(rad) * 1.3 + self.y += math.cos(rad) * 1.3 + else: + self.yaw += math.sin(self.timer * 3) * 3.5 + rad = math.radians(self.yaw) + self.x += -math.sin(rad) * 1.4 + self.y += math.cos(rad) * 1.4 + + self.z = 15.0 + math.sin(self.timer * 2) * 6.0 + + if random.random() < 0.0015 and REEDS: + reed = random.choice(REEDS) + self.x, self.y, self.z = reed[0], reed[1], reed[2] + reed[3] + self.is_perched = True + self.perch_timer = random.randint(300, 600) + + if not is_over_water(self.x, self.y): + self.yaw += 150.0 + + if self.y > STREAM_MAX_Y: + self.y -= WORLD_LEN + elif self.y < STREAM_MIN_Y: + self.y += WORLD_LEN + + def draw(self): + glColor3f(*COLOR_PURPLE_DRAGONFLY) + glPushMatrix() + glTranslatef(self.x, self.y, self.z) + glRotatef(self.yaw, 0, 0, 1) + + glBegin(GL_LINES) + glVertex3f(0, 1.5, 0) + glVertex3f(0, -3.0, 0) + glVertex3f(-0.4, 0.6, 0) + glVertex3f(0.4, 0.6, 0) + glVertex3f(-0.3, -0.6, 0) + glVertex3f(0.3, -0.6, 0) + glEnd() + + glBegin(GL_LINE_LOOP) + for i in range(8): + angle = 2.0 * math.pi * i / 8 + glVertex3f(0.3 * math.cos(angle), 1.4 + 0.3 * math.sin(angle), 0) + glEnd() + + wing_angle = 0.0 if self.is_perched else math.sin(self.wing_timer) * 35.0 + rad_wing = math.radians(wing_angle) + w_z = math.sin(rad_wing) * 1.2 if wing_angle != 0 else 0.0 + + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(-4.5, 1.0, w_z) + glVertex3f(-3.8, 0.2, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(4.5, 1.0, w_z) + glVertex3f(3.8, 0.2, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(-3.8, -0.2, w_z) + glVertex3f(-3.0, -0.8, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(3.8, -0.2, w_z) + glVertex3f(3.0, -0.8, w_z) + glEnd() + + glPopMatrix() + +class Frog: + def __init__(self): + self.pick_new_destination() + self.x, self.y = self.target_x, self.target_y + self.z = 0.6 + self.speed = 0.35 + self.yaw = 0.0 + self.swim_timer = 0.0 + + def pick_new_destination(self): + destinations = [(rx, ry) for rx, ry, _, _ in REEDS] + [(ix, iy) for ix, iy, _ in ISLANDS] + dest = random.choice(destinations) + self.target_x, self.target_y = dest[0] + random.uniform(-30, 30), dest[1] + random.uniform(-30, 30) + + def update(self): + self.swim_timer += 0.15 + dx = self.target_x - self.x + dy = self.target_y - self.y + dist = math.hypot(dx, dy) + + if dist < 6.0: + self.pick_new_destination() + else: + self.yaw = math.degrees(math.atan2(dy, dx)) - 90.0 + self.x += (dx / dist) * self.speed + self.y += (dy / dist) * self.speed + + def draw(self, custom_x=None, custom_y=None, custom_z=None, scale=1.0, menu_icon=False): + glColor3f(*COLOR_FROG) + glPushMatrix() + if custom_x is not None and custom_y is not None and custom_z is not None: + glTranslatef(custom_x, custom_y, custom_z) + glScalef(scale, scale, scale) + else: + glTranslatef(self.x, self.y, self.z) + glRotatef(self.yaw, 0, 0, 1) + + leg_offset = 0.0 if menu_icon else math.sin(self.swim_timer) * 1.5 + + glBegin(GL_LINES) + glVertex3f(0.0, 0.0, 0.5) + glVertex3f(0.0, -5.0, 0.5) + glVertex3f(-2.0, 2.0, 0.5) + glVertex3f(2.0, 2.0, 0.5) + glVertex3f(-2.5, -2.0, 0.5) + glVertex3f(2.5, -2.0, 0.5) + + glVertex3f(-2.0, 2.0, 0.5) + glVertex3f(0.0, 0.0, 0.5) + glVertex3f(2.0, 2.0, 0.5) + glVertex3f(0.0, 0.0, 0.5) + + glVertex3f(-2.0, 2.0, 0.6) + glVertex3f(2.0, 2.0, 0.6) + glVertex3f(-2.0, 2.0, 0.6) + glVertex3f(0.0, 4.5, 0.6) + glVertex3f(2.0, 2.0, 0.6) + glVertex3f(0.0, 4.5, 0.6) + + glVertex3f(-1.5, 3.5, 1.5) + glVertex3f(-1.5, 3.5, 0.6) + glVertex3f(1.5, 3.5, 1.5) + glVertex3f(1.5, 3.5, 0.6) + + glVertex3f(-2.0, 2.0, 0.5) + glVertex3f(-4.0, 3.0, 0.0) + glVertex3f(-4.0, 3.0, 0.0) + glVertex3f(-3.5, 4.0, 0.0) + + glVertex3f(2.0, 2.0, 0.5) + glVertex3f(4.0, 3.0, 0.0) + glVertex3f(4.0, 3.0, 0.0) + glVertex3f(3.5, 4.0, 0.0) + + # Patas traseras con los pies hacia el exterior + glVertex3f(-2.5, -2.0, 0.5) + glVertex3f(-5.0 - leg_offset, -4.0, 0.0) + glVertex3f(-5.0 - leg_offset, -4.0, 0.0) + glVertex3f(-6.5 - leg_offset, -3.2, 0.0) + + glVertex3f(2.5, -2.0, 0.5) + glVertex3f(5.0 + leg_offset, -4.0, 0.0) + glVertex3f(5.0 + leg_offset, -4.0, 0.0) + glVertex3f(6.5 + leg_offset, -3.2, 0.0) + glEnd() + + # Cuadrado vacío central simulando el cuerpo sólido con el borde del color de las patas + if menu_icon: + glBegin(GL_LINE_LOOP) + glVertex3f(-2.0, -2.5, 0.4) + glVertex3f(2.0, -2.5, 0.4) + glVertex3f(2.0, 1.5, 0.4) + glVertex3f(-2.0, 1.5, 0.4) + glEnd() + + glPopMatrix() + +# ========================================== +# RENDERIZADO DE ESCENARIO Y MODELOS +# ========================================== +def draw_water_surface(time_counter): + glColor3f(*COLOR_WATER) + + grid_size = 15 + for x in range(int(-POND_RADIUS), int(POND_RADIUS), grid_size * 2): + glBegin(GL_LINE_STRIP) + for y in range(int(-POND_RADIUS), int(-POND_RADIUS) + int(POND_RADIUS)*2, grid_size): + dist = math.hypot(x, y) + if dist < POND_RADIUS - 30: + z = 1.0 * math.sin(dist * 0.015 - time_counter * 3.0) + 0.5 * math.cos(x * 0.01 + time_counter) + glVertex3f(x, y, z) + glEnd() + + for y in range(int(STREAM_MIN_Y), int(STREAM_MAX_Y), 15): + if abs(y) < POND_RADIUS: + continue + cx = get_river_center(y) + glBegin(GL_LINE_STRIP) + for dx in range(-100, 110, 10): + x = cx + dx + z = 0.5 * math.sin(y * 0.03 + dx * 0.08 + time_counter * 4.0) + glVertex3f(x, y, z) + glEnd() + +def draw_terrain(): + glColor3f(*COLOR_LAND) + + grid_step = 120 + for y in range(int(STREAM_MIN_Y - 300), int(STREAM_MAX_Y + 300), grid_step): + glBegin(GL_LINE_STRIP) + for x in range(-2500, 2500, grid_step): + z = 1.5 * math.sin(x * 0.005) + 1.5 * math.cos(y * 0.005) + glVertex3f(x, y, z) + glEnd() + + width_river = 100.0 + margin_land = 70.0 + glColor3f(*COLOR_LAND_RIVER) + + for y in range(int(STREAM_MIN_Y), int(STREAM_MAX_Y), 30): + if abs(y) < POND_RADIUS: + continue + cx = get_river_center(y) + + glBegin(GL_LINE_STRIP) + for dx in range(int(-width_river - margin_land), int(-width_river), 15): + x = cx + dx + z = 0.6 * math.sin(y * 0.02 + x * 0.02) + 0.3 + glVertex3f(x, y, z) + glEnd() + + glBegin(GL_LINE_STRIP) + for dx in range(int(width_river), int(width_river + margin_land), 15): + x = cx + dx + z = 0.6 * math.sin(y * 0.02 + x * 0.02) + 0.3 + glVertex3f(x, y, z) + glEnd() + + for ix, iy, ir in ISLANDS: + glColor3f(*COLOR_ISLAND) + for layer in range(1, 4): + rad_layer = ir * (1 - layer * 0.25) + height = layer * 1.2 + glBegin(GL_LINE_LOOP) + for i in range(16): + angle = 2.0 * math.pi * i / 16 + dx = rad_layer * math.cos(angle) + dy = rad_layer * math.sin(angle) + glVertex3f(ix + dx, iy + dy, height) + glEnd() + +def draw_map(time_counter): + draw_terrain() + draw_water_surface(time_counter) + + offsets = [(-1.5, 0.6), (1.3, -0.6), (-0.7, -1.4), (1.1, 1.2), (0.0, 1.6), (-1.2, 0.0)] + + for rx, ry, rz, rh in REEDS: + glColor3f(*COLOR_REED) + glBegin(GL_LINES) + glVertex3f(rx, ry, rz) + glVertex3f(rx, ry, rz + rh) + for ox, oy in offsets: + glVertex3f(rx, ry, rz) + glVertex3f(rx + ox, ry + oy, rz + rh * 0.85) + glEnd() + + glColor3f(*COLOR_CATTAIL) + glBegin(GL_LINES) + for d in [-0.2, 0.0, 0.2]: + glVertex3f(rx + d, ry, rz + rh - 4.5) + glVertex3f(rx + d, ry, rz + rh) + for ox, oy in offsets: + for d in [-0.15, 0.15]: + glVertex3f(rx + ox + d, ry + oy, (rz + rh * 0.85) - 3.8) + glVertex3f(rx + ox + d, ry + oy, rz + rh * 0.85) + glEnd() + +def draw_dragonfly(wing_angle, caught_insect=None): + glColor3f(*COLOR_DRAGONFLY) + + glBegin(GL_LINES) + glVertex3f(0, 1.5, 0) + glVertex3f(0, -3.0, 0) + glVertex3f(-0.4, 0.6, 0) + glVertex3f(0.4, 0.6, 0) + glVertex3f(-0.3, -0.6, 0) + glVertex3f(0.3, -0.6, 0) + + glVertex3f(-0.3, 0.5, 0) + glVertex3f(-0.2, 0.2, -0.6) + glVertex3f(0.3, 0.5, 0) + glVertex3f(0.2, 0.2, -0.6) + glVertex3f(-0.2, -0.2, 0) + glVertex3f(-0.1, -0.3, -0.6) + glVertex3f(0.2, -0.2, 0) + glVertex3f(0.1, -0.3, -0.6) + glEnd() + + glBegin(GL_LINE_LOOP) + for i in range(8): + angle = 2.0 * math.pi * i / 8 + glVertex3f(0.3 * math.cos(angle), 1.4 + 0.3 * math.sin(angle), 0) + glEnd() + + rad_wing = math.radians(wing_angle) + w_z = math.sin(rad_wing) * 1.2 if wing_angle != 0 else 0.0 + + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(-4.5, 1.0, w_z) + glVertex3f(-3.8, 0.2, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(4.5, 1.0, w_z) + glVertex3f(3.8, 0.2, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(-3.8, -0.2, w_z) + glVertex3f(-3.0, -0.8, w_z) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(3.8, -0.2, w_z) + glVertex3f(3.0, -0.8, w_z) + glEnd() + + if caught_insect: + glPushMatrix() + glTranslatef(0.0, 0.2, -0.6) + caught_insect.draw(attached=True) + glPopMatrix() + +# ========================================== +# INTERFAZ 2D / HUD Y PANTALLA INICIAL +# ========================================== +def draw_text_2d(text_surface, x, y): + text_data = pygame.image.tostring(text_surface, "RGBA", True) + w, h = text_surface.get_size() + + glEnable(GL_BLEND) + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) + + glRasterPos2i(x, y) + glDrawPixels(w, h, GL_RGBA, GL_UNSIGNED_BYTE, text_data) + glDisable(GL_BLEND) + +def draw_start_screen(font_title, font_subtitle, font_text, wing_angle, menu_wing_angle, frog_instance): + glDisable(GL_DEPTH_TEST) + + glMatrixMode(GL_PROJECTION) + glPushMatrix() + glLoadIdentity() + gluOrtho2D(0, WIDTH, 0, HEIGHT) + + glMatrixMode(GL_MODELVIEW) + glPushMatrix() + glLoadIdentity() + + # TÍTULO PRINCIPAL + txt_title = font_title.render("LIBÉLULA", True, (0, 220, 255)) + draw_text_2d(txt_title, WIDTH // 2 - 130, HEIGHT - 100) + + # LIBÉLULA SUPERIOR + glPushMatrix() + glTranslatef(WIDTH // 2 + 150, HEIGHT - 75, 0.0) + glScalef(12.0, 12.0, 1.0) + + glColor3f(*COLOR_DRAGONFLY) + glBegin(GL_LINES) + glVertex3f(0, 1.5, 0) + glVertex3f(0, -3.0, 0) + glVertex3f(-0.4, 0.6, 0) + glVertex3f(0.4, 0.6, 0) + glEnd() + + rad_menu_w = math.radians(menu_wing_angle) + menu_w_off = math.sin(rad_menu_w) * 3.5 + + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(-4.5, 1.0, menu_w_off) + glVertex3f(-3.8, 0.2, menu_w_off) + glEnd() + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(4.5, 1.0, menu_w_off) + glVertex3f(3.8, 0.2, menu_w_off) + glEnd() + + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(-3.8, -0.2, menu_w_off) + glVertex3f(-3.0, -0.8, menu_w_off) + glEnd() + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(3.8, -0.2, menu_w_off) + glVertex3f(3.0, -0.8, menu_w_off) + glEnd() + + glPopMatrix() + + # OBJETIVO DEL JUEGO + obj_line1 = font_text.render("Objetivo: Vuela libre por el estanque y sus arroyos capturando moscas", True, (255, 255, 255)) + obj_line2 = font_text.render("y mosquitos. Una vez cazados debes devorarlos posado en un junco.", True, (255, 255, 255)) + draw_text_2d(obj_line1, 70, HEIGHT - 170) + draw_text_2d(obj_line2, 70, HEIGHT - 195) + + # CONTROLES Y TECLAS + controls = [ + "CONTROLES:", + "W / S : Acelerar / Frenar", + "Flecha Arriba / Abajo : Cambiar altitud", + "Flecha Izquierda / Derecha : Girar / Virar", + "ESPACIO : Picado rápido para cazar", + "P : Aterrizaje automático en junco cercano", + "ESC : Salir del juego" + ] + + y_pos = HEIGHT - 250 + for ctrl in controls: + is_title = ctrl.endswith(":") + col = (255, 220, 50) if is_title else (200, 200, 200) + t_surf = font_subtitle.render(ctrl, True, col) if is_title else font_text.render(ctrl, True, col) + draw_text_2d(t_surf, 70, y_pos) + y_pos -= 25 + + # LEYENDA VISUAL INFERIOR + y_icon = 90 + + # 1. Mosquito + glColor3f(*COLOR_MOSQUITO) + glBegin(GL_LINES) + glVertex2f(60, y_icon + 5) + glVertex2f(60, y_icon - 5) + glVertex2f(55, y_icon) + glVertex2f(65, y_icon) + glEnd() + txt_m = font_text.render("Mosquito", True, (255, 230, 0)) + draw_text_2d(txt_m, 75, y_icon - 8) + + # 2. Mosca + glColor3f(*COLOR_FLY) + glBegin(GL_LINES) + glVertex2f(210, y_icon + 5) + glVertex2f(210, y_icon - 5) + glVertex2f(205, y_icon - 3) + glVertex2f(215, y_icon + 3) + glVertex2f(205, y_icon + 3) + glVertex2f(215, y_icon - 3) + glEnd() + txt_f = font_text.render("Mosca", True, (200, 65, 40)) + draw_text_2d(txt_f, 225, y_icon - 8) + + # 3. Junco + glColor3f(*COLOR_REED) + glBegin(GL_LINES) + glVertex2f(340, y_icon - 10) + glVertex2f(340, y_icon + 15) + glEnd() + glColor3f(*COLOR_CATTAIL) + glBegin(GL_LINES) + glVertex2f(340, y_icon + 8) + glVertex2f(340, y_icon + 15) + glEnd() + txt_r = font_text.render("Junco", True, (100, 220, 100)) + draw_text_2d(txt_r, 355, y_icon - 8) + + # 4. Rana (Con el cuadrado central de cuerpo sólido) + frog_instance.draw(custom_x=475, custom_y=y_icon, custom_z=0.0, scale=3.5, menu_icon=True) + txt_frog = font_text.render("Rana", True, (100, 255, 100)) + draw_text_2d(txt_frog, 505, y_icon - 8) + + # 5. Libélulas (Moradas) + rad_w = math.radians(wing_angle) + w_off = math.sin(rad_w) * 1.5 + + glPushMatrix() + glTranslatef(625, y_icon, 0.0) + glScalef(6.0, 6.0, 1.0) + glColor3f(*COLOR_PURPLE_DRAGONFLY) + glBegin(GL_LINES) + glVertex3f(0, 1.5, 0) + glVertex3f(0, -3.0, 0) + glVertex3f(-0.4, 0.6, 0) + glVertex3f(0.4, 0.6, 0) + glEnd() + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(-4.5, 1.0, w_off) + glVertex3f(-3.8, 0.2, w_off) + glEnd() + glBegin(GL_LINE_LOOP) + glVertex3f(0, 0.6, 0) + glVertex3f(4.5, 1.0, w_off) + glVertex3f(3.8, 0.2, w_off) + glEnd() + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(-3.8, -0.2, w_off) + glVertex3f(-3.0, -0.8, w_off) + glEnd() + glBegin(GL_LINE_LOOP) + glVertex3f(0, -0.1, 0) + glVertex3f(3.8, -0.2, w_off) + glVertex3f(3.0, -0.8, w_off) + glEnd() + glPopMatrix() + + txt_df = font_text.render("Libélulas", True, (178, 51, 204)) + draw_text_2d(txt_df, 654, y_icon - 8) + + # MENSAJE PARA COMENZAR + start_msg = font_subtitle.render("Pulsa INTRO o cualquier tecla para comenzar...", True, (100, 255, 100)) + draw_text_2d(start_msg, WIDTH // 2 - 200, 30) + + glMatrixMode(GL_PROJECTION) + glPopMatrix() + glMatrixMode(GL_MODELVIEW) + glPopMatrix() + glEnable(GL_DEPTH_TEST) + +def draw_hud(player_pos, yaw, insects, eaten_mosquitos, eaten_flies, font): + glDisable(GL_DEPTH_TEST) + + glMatrixMode(GL_PROJECTION) + glPushMatrix() + glLoadIdentity() + gluOrtho2D(0, WIDTH, 0, HEIGHT) + + glMatrixMode(GL_MODELVIEW) + glPushMatrix() + glLoadIdentity() + + px, py, pz = player_pos + + cx, cy = WIDTH // 2, HEIGHT - 35 + radius = 22 + + glColor3f(*COLOR_HUD) + glBegin(GL_LINE_LOOP) + for i in range(24): + angle = 2.0 * math.pi * i / 24 + glVertex2f(cx + radius * math.cos(angle), cy + radius * math.sin(angle)) + glEnd() + + glBegin(GL_LINES) + glVertex2f(cx, cy + radius) + glVertex2f(cx, cy + radius - 5) + glVertex2f(cx, cy - radius) + glVertex2f(cx, cy - radius + 5) + glVertex2f(cx - radius, cy) + glVertex2f(cx - radius + 5, cy) + glVertex2f(cx + radius, cy) + glVertex2f(cx + radius - 5, cy) + glEnd() + + closest_insect = None + min_dist = 999999.0 + relative_angle = 0.0 + diff_z = 0.0 + + for insect in insects: + dx = insect.x - px + dy = insect.y - py + dist = math.hypot(dx, dy) + + if dist < min_dist and dist > 0.1: + angle_to_insect = math.degrees(math.atan2(dy, dx)) + heading = (angle_to_insect - (90 - yaw)) % 360 + if heading > 180: + heading -= 360 + + if abs(heading) < 60: + min_dist = dist + closest_insect = insect + relative_angle = heading + diff_z = insect.z - pz + + if closest_insect: + radar_x = cx - (relative_angle / 60.0) * (radius - 4) + radar_y = cy + (diff_z / 20.0) * (radius - 4) + + dist_from_center = math.hypot(radar_x - cx, radar_y - cy) + if dist_from_center > (radius - 3): + factor = (radius - 3) / dist_from_center + radar_x = cx + (radar_x - cx) * factor + radar_y = cy + (radar_y - cy) * factor + + glColor3f(1.0, 1.0, 1.0) + glBegin(GL_LINE_LOOP) + for i in range(8): + ang = 2.0 * math.pi * i / 8 + glVertex2f(radar_x + 3 * math.cos(ang), radar_y + 3 * math.sin(ang)) + glEnd() + + glColor3f(*COLOR_MOSQUITO) + glBegin(GL_LINES) + glVertex2f(25, HEIGHT - 33) + glVertex2f(25, HEIGHT - 27) + glVertex2f(21, HEIGHT - 30) + glVertex2f(29, HEIGHT - 30) + glEnd() + + glColor3f(*COLOR_FLY) + glBegin(GL_LINES) + glVertex2f(WIDTH - 82, HEIGHT - 33) + glVertex2f(WIDTH - 82, HEIGHT - 27) + glVertex2f(WIDTH - 86, HEIGHT - 28) + glVertex2f(WIDTH - 78, HEIGHT - 32) + glVertex2f(WIDTH - 86, HEIGHT - 32) + glVertex2f(WIDTH - 78, HEIGHT - 28) + glEnd() + + txt_mosq = font.render(f"x {eaten_mosquitos}", True, (255, 230, 0)) + txt_flies = font.render(f"x {eaten_flies}", True, (200, 65, 40)) + + draw_text_2d(txt_mosq, 38, HEIGHT - 40) + draw_text_2d(txt_flies, WIDTH - 68, HEIGHT - 40) + + glMatrixMode(GL_PROJECTION) + glPopMatrix() + glMatrixMode(GL_MODELVIEW) + glPopMatrix() + glEnable(GL_DEPTH_TEST) + +# ========================================== +# BUCLE PRINCIPAL +# ========================================== +def main(): + pygame.init() + pygame.mixer.init(frequency=22050, size=-16, channels=1) + pygame.font.init() + + font = pygame.font.SysFont("Arial", 22, bold=True) + font_title = pygame.font.SysFont("Arial", 48, bold=True) + font_subtitle = pygame.font.SysFont("Arial", 20, bold=True) + font_text = pygame.font.SysFont("Arial", 16, bold=False) + + pygame.display.set_mode((WIDTH, HEIGHT), DOUBLEBUF | OPENGL) + pygame.display.set_caption("Libélula") + + glEnable(GL_DEPTH_TEST) + glClearColor(*COLOR_BG, 1.0) + + glMatrixMode(GL_PROJECTION) + gluPerspective(50, (WIDTH / HEIGHT), 0.1, 5000.0) + glMatrixMode(GL_MODELVIEW) + + buzz_sound = generate_buzz_sound() + wind_sound = generate_wind_sound() + + px, py, pz = 0.0, 0.0, 15.0 + yaw = 0.0 + roll = 0.0 + pitch = 0.0 + max_roll = 30.0 + max_pitch = 20.0 + + speed = 0.0 + max_speed = 3.0 + acceleration = 0.06 + deceleration = 0.03 + + min_altitude = 1.0 + max_altitude = 120.0 + + wing_timer = 0.0 + water_timer = 0.0 + is_perched = False + + dive_phase = "none" + pre_dive_alt = 15.0 + pre_dive_speed = 0.0 + + auto_landing = False + target_perch = None + + caught_insect = None + eaten_mosquitos = 0 + eaten_flies = 0 + + frog = Frog() + + purple_dragonflies = [ + PurpleDragonfly(1), + PurpleDragonfly(2), + PurpleDragonfly(3) + ] + + insects = [] + num_mosquitos = random.randint(MOSQUITO_MIN, MOSQUITO_MAX) + num_flies = random.randint(FLY_MIN, FLY_MAX) + + for _ in range(num_mosquitos): + insects.append(Insect("mosquito")) + for _ in range(num_flies): + insects.append(Insect("fly")) + + clock = pygame.time.Clock() + + in_start_screen = True + running = True + + buzz_timer = 0 + + while running: + dt = clock.tick(60) / 1000.0 + water_timer += dt + wing_timer += 60.0 + + for event in pygame.event.get(): + if event.type == QUIT or (event.type == KEYDOWN and event.key == K_ESCAPE): + running = False + elif event.type == KEYDOWN: + if in_start_screen: + in_start_screen = False + else: + if event.key == K_SPACE: + if not is_perched and dive_phase == "none": + dive_phase = "diving" + pre_dive_alt = pz + pre_dive_speed = speed + auto_landing = False + elif event.key == K_p: + if not is_perched and dive_phase == "none": + best_reed = None + min_d = 999.0 + for rx, ry, rz, rh in REEDS: + dist = math.hypot(px - rx, py - ry) + if dist < 35.0: + angle_to_reed = math.degrees(math.atan2(ry - py, rx - px)) + heading = (angle_to_reed - (90 - yaw)) % 360 + if heading > 180: + heading -= 360 + if abs(heading) < 45 and dist < min_d: + min_d = dist + best_reed = (rx, ry, rz + rh) + + if best_reed: + target_perch = best_reed + auto_landing = True + + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) + glLoadIdentity() + + if in_start_screen: + cam_x, cam_y, cam_z = 0.0, -200.0, 50.0 + gluLookAt(cam_x, cam_y, cam_z, 0, 0, 15, 0, 0, 1) + + draw_map(water_timer) + frog.update() + frog.draw() + for p_df in purple_dragonflies: + p_df.update() + p_df.draw() + + wing_angle = math.sin(wing_timer * 0.5) * 35.0 + menu_wing_angle = math.sin(wing_timer * 0.8) * 45.0 + draw_start_screen(font_title, font_subtitle, font_text, wing_angle, menu_wing_angle, frog) + else: + keys = pygame.key.get_pressed() + + if is_perched: + if keys[K_LEFT]: + yaw += 3.0 + if keys[K_RIGHT]: + yaw -= 3.0 + + if keys[K_w]: + is_perched = False + speed = 0.5 + + wing_angle = 0.0 + pitch = 0.0 + roll = 0.0 + dive_phase = "none" + auto_landing = False + else: + if auto_landing and target_perch: + tx, ty, tz = target_perch + dx = tx - px + dy = ty - py + dist_to_target = math.hypot(dx, dy) + + speed = 1.2 + px += (dx / dist_to_target) * speed if dist_to_target > 0.1 else 0 + py += (dy / dist_to_target) * speed if dist_to_target > 0.1 else 0 + pz += (tz - pz) * 0.1 + + if dist_to_target < 2.5 and abs(pz - tz) < 2.5: + px, py, pz = target_perch + speed = 0.0 + roll = 0.0 + pitch = 0.0 + is_perched = True + auto_landing = False + target_perch = None + + if caught_insect: + if caught_insect.type == "mosquito": + eaten_mosquitos += 1 + else: + eaten_flies += 1 + caught_insect = None + + elif dive_phase == "diving": + speed = max_speed * 1.5 + pz -= 1.2 + target_pitch = -45.0 + if wind_sound and not pygame.mixer.get_busy(): + wind_sound.play() + + if pz <= min_altitude: + pz = min_altitude + dive_phase = "recovering" + + elif dive_phase == "recovering": + target_pitch = 30.0 + pz += 0.8 + speed += (pre_dive_speed - speed) * 0.1 + + if pz >= pre_dive_alt: + pz = pre_dive_alt + speed = pre_dive_speed + dive_phase = "none" + else: + if keys[K_w]: + speed = min(speed + acceleration, max_speed) + if speed > 2.0 and wind_sound and random.random() < 0.1 and not pygame.mixer.get_busy(): + wind_sound.play() + elif keys[K_s]: + speed = max(speed - deceleration, 0.0) + + if speed < 0.3: + for rx, ry, rz, rh in REEDS: + dist = math.hypot(px - rx, py - ry) + if dist < 10.0 and abs(pz - (rz + rh)) < 10.0: + px, py, pz = rx, ry, rz + rh + speed = 0.0 + roll = 0.0 + pitch = 0.0 + is_perched = True + + if caught_insect: + if caught_insect.type == "mosquito": + eaten_mosquitos += 1 + else: + eaten_flies += 1 + caught_insect = None + break + + target_pitch = 0.0 + if keys[K_UP]: + pz = min(pz + 0.8, max_altitude) + target_pitch = max_pitch + elif keys[K_DOWN]: + pz = max(pz - 0.8, min_altitude) + target_pitch = -max_pitch + + if not auto_landing: + pitch += (target_pitch - pitch) * 0.15 + + target_roll = 0.0 + if keys[K_LEFT]: + yaw += 2.8 + target_roll = -max_roll + elif keys[K_RIGHT]: + yaw -= 2.8 + target_roll = max_roll + + roll += (target_roll - roll) * 0.15 + + rad_yaw = math.radians(yaw) + px += -math.sin(rad_yaw) * speed + py += math.cos(rad_yaw) * speed + + if abs(py) < POND_RADIUS: + max_limit_x = POND_RADIUS + 200.0 + else: + cx = get_river_center(py) + max_limit_x = abs(cx) + 200.0 + + px = max(-max_limit_x, min(max_limit_x, px)) + + if py > STREAM_MAX_Y: + py -= WORLD_LEN + elif py < STREAM_MIN_Y: + py += WORLD_LEN + + wing_timer += 80.0 if (dive_phase != "none" or auto_landing) else (60.0 if speed > 0.1 else 30.0) + wing_angle = math.sin(wing_timer) * 35.0 + + frog.update() + + for p_df in purple_dragonflies: + p_df.update() + + current_mosquitos = 0 + current_flies = 0 + near_insect = False + + for insect in insects[:]: + insect.update((px, py, pz)) + + if insect.type == "mosquito": + current_mosquitos += 1 + else: + current_flies += 1 + + dist_to_df = math.sqrt((px - insect.x)**2 + (py - insect.y)**2 + (pz - insect.z)**2) + if dist_to_df < 40.0: + near_insect = True + + if not caught_insect: + if dist_to_df < 6.0: + caught_insect = insect + insects.remove(insect) + if dive_phase == "diving": + dive_phase = "recovering" + + if near_insect and buzz_sound: + buzz_timer += 1 + if buzz_timer > 25 and not pygame.mixer.get_busy(): + buzz_sound.play() + buzz_timer = 0 + + if current_mosquitos < MOSQUITO_MIN: + insects.append(Insect("mosquito")) + if current_flies < FLY_MIN: + insects.append(Insect("fly")) + + cam_dist = 18.0 + cam_height = 8.0 + rad_yaw = math.radians(yaw) + + cam_x = px + math.sin(rad_yaw) * cam_dist + cam_y = py - math.cos(rad_yaw) * cam_dist + cam_z = pz + cam_height + + gluLookAt(cam_x, cam_y, cam_z, + px, py, pz + 0.5, + 0, 0, 1) + + draw_map(water_timer) + frog.draw() + + for p_df in purple_dragonflies: + p_df.draw() + + for insect in insects: + insect.draw() + + glPushMatrix() + glTranslatef(px, py, pz) + glRotatef(yaw, 0, 0, 1) + glRotatef(pitch, 1, 0, 0) + glRotatef(roll, 0, 1, 0) + draw_dragonfly(wing_angle, caught_insect) + glPopMatrix() + + draw_hud((px, py, pz), yaw, insects, eaten_mosquitos, eaten_flies, font) + + pygame.display.flip() + + pygame.quit() + sys.exit() + +if __name__ == "__main__": + main() diff --git a/radar-ayuda-captura.png b/radar-ayuda-captura.png new file mode 100644 index 0000000000000000000000000000000000000000..bbbe6dd778d2836fcf4e6c0bd5ece56dc5ebffab GIT binary patch literal 13380 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 literal 0 HcmV?d00001