From 8eb66f2e66f3214e995482d9a1622d7064d83a02 Mon Sep 17 00:00:00 2001 From: 33333-33333 Date: Fri, 19 Jun 2026 20:52:40 +0900 Subject: [PATCH] hmm --- assets/sprites/tarinai_28_zunchi_slave.png | Bin 0 -> 116626 bytes assets/ui/tool_poke.png | Bin 770 -> 2391 bytes css/styles.css | 513 +++++----- index.html | 98 +- js/data.js | 4 +- js/items.js | 284 +++++- js/render.js | 88 +- js/sim_core.js | 28 +- js/tarinai.js | 473 +++++++-- js/ui.js | 286 +++++- js/ui_family.js | 1027 ++++++++++++++++++-- js/world.js | 450 ++++++++- 12 files changed, 2722 insertions(+), 529 deletions(-) create mode 100644 assets/sprites/tarinai_28_zunchi_slave.png diff --git a/assets/sprites/tarinai_28_zunchi_slave.png b/assets/sprites/tarinai_28_zunchi_slave.png new file mode 100644 index 0000000000000000000000000000000000000000..7722fc4bf4a90ed5d409003c7704facde8396564 GIT binary patch literal 116626 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ఒ!p(5W-#Mm%}2k0 z2;B-u{#|lw+S`3$&aIX2{BN7d>*op0pdR@>f50M_fiT=h>tDgYdFjwh-s>*s&T zr!o&$BR~SDlAa!G2s}#CAeO5bp$|$<(Y}HEVe9#P)^*{pH{ZM_Q5gmkV-cW1 zb&PGG{GDeYd#+;A$2q@koqal%;o3?h2*M@ZKcb_UwIx{}%XHpA{Z1HgNz#u!>4s3F z^%~Aqo=G8}V-XMih3O3Cf1a8Ij7=zkz+o&Flz3Qi3<7vE**En54|6hdM!tA5m-ocU zM>;sapuny&j*rf3+S2fc3qtxRn0RS!vH649&FW!cWhEuB5bfQW6zW^0|D!B%4z9>y z=FQo}pk{`}^Fhqwq;Si6-@!*VN;+y+SMHCZXDY%*cDHT%u#Ld~}p=U9iP#fg00+@i3@cfGyu4??hzrludM z_gY5Hi3q;mQ8I;1Sa9mY@22k-P5)h|^eIJfVX~%0#4YQXw^9m>h~qS#IXta(?`$+W4vC%ab2g9iPU*6hT8%Z?-oLi$#^NH z6I?GZ3Po~hL{}}cgI$SwsYUH9vJo}}aeQH1X;qQ{D8qtFA02O@`C~hyJ9;pM z3t*E2I4gAzwmhJh?+)VJp&b)$zA}dWs23l4Z+$OTG9TPA?ss?IEN3lV5)G)95xpY; z^}8O55&=jDUf=OF1J%KlK9h^A-0_rg@=my}s!_@%Mk>UI42bPft(JBwVDT z$qR)GhCiaEozKXigm85XiSW=Q{kZJr)DZe;Y%XCrRrzw*@DGVBJVdUN?LBvk-^ehf z)8cm;qVV$M{J}SPN|?mqyMETBIk7P@{7{CFa0DiC2TkbGuiNiXv>XaRKt+hil~|8H zmos&N6{&G1si&W5P0g*cYB$$@%8BUo4b$H2xV~OC^V_=2l=0ZWZQA>nNl)#a+Tf0( z!ygEHw&!E_xcWswFoO})wy#805=G{5-O|ns8lVUmx zmD;N6B5sy{Ijl?*{sm2l&R7kSr7n>^A;`C| zVB#14VL|#4<+-uHi*o*Qaafav>;yZ1H--Tg%MPg&G`JqgbdE zFYR!$#`n$jpdP?q6T~qbGXmt7hGC9?y1)6g(>%pmI*)Y{<-!X+ZX6)*di8swMr}); z7DBGyVLbL~r+)Ne&!`hm%-~FOdkpntOW02A(4&Pinj}WxfoinekIq?ga-@O zKE}iWpV3W$i@E7Wl$rEG6lyVTDTMmTr#BSJzLg4eHRmiW@dTE+jm3-HUwht&0=_G5 zbbwdY!|$~`ArMH+1+f401YOLTFE-k4TQe@UxI1q7J`?#p75kO@mR$P&s;VGq@KJ1# zdGt$VZ%2|v#mhh5#6Q3;H~aOgJyf|saPKkMx4L>ql%C(2YKP0ZGhRn} z{fNe|teSrLO+4@@HdqH6yg2}ZzW0qM52BWbegnfIEWgQ+sA8ZWGh>Y7TQok&iL&sa z4)0v{mF5WM8fHe@Ji&q#Xd?tlCwysJRl#UK zE3b^w%2cohaa0Ku7$`IxeX@v$S%e^-JPfK2TDXwBm6q!tjxuoplG|PS%n!Q>+Zu3X zZpZ46hpK|8inn7h95A=IWB;@LNMDa7t`LhnK!B1E$(^awOsj|~nA3LhDLNbicQ7KP z;4e@rb;JN^5#r(VB`xNc8GD;t&0SqJ5@Ep%*t|YeeTF$DFO96 zsT7c{-ak+?h;9+00rShhs|zW~fZc)ckxJj~4syDY(50}hj?K0+0Xpx2(tf*Zo zfv!Gd8Ycg?tV8&+eblt8x-4U6ocRKK8i13l}DwAJZc@ zgj&6vjlUNX+w~cl_K~8M1$t<$?}7Kf4n` zfK(^{ebyr3?521?_V}s)kD{{-YpRXI_!&96yFt1^x<&{HC?P2*9TEZ}9UI*xt#k-T zs|ZMqE-6VtVSsdlbnHFvb?wtW?(8|wKkoZ?&vy7fg{cv9ZRCD^Bc>U7`>y5X+$k(3 zJVX0sfA}dnEKg37Er$={$oA$Jh$m-CN*ligl9aA={=NzJJJ{A}xQuk1nE6IA?b)&` zlM7X8Mj1jON~x@6_Eef5NA#_*k~SO99aPwUKd>g`H|y9GJ~BvJ%dEGvX9`1rbh)D1 zZ#dD+l1$2^N4oG;Ti?C-&8sZ*-3{FG$`K9IiNpd4#S;pB^DS5GyReIk7fsVmKpTh~rA>bY-1GTuio-(3?wfuYfr zPE*Qm06<<0+Noaqfth{icKdy=thSD8s!Yu+=_?ByRgEV-P4G9EeEfVmiMPR=?{M`( zqnX=CHiJ>@ZU26rg5gmVT~x}i;c@3UQm&@apdm@_vQHSzK%CJl_$0tKmo7PdcWF;+ z+e!NQDdyYT?<~5o!2S0e$ap`90AMS>3Pg~xBmE-qox;Ni-1G)d>hyK}th4`pOd$h! zbW{i2%4|~3aCE3to_&`N$lw8t^g3K9+k%XuLc`ER8c3sEsxa87qY;joy$U5i{Y}oG zNgp<;h-b(L4kTN3U~MX9U%1FPtPpfpbzc5D^ZT31eYCYkvQoO{s1MP$S0Il_ThjY zKwq}Pa^hw|0AS5VqLiv8v>0vpm@cW{N-;O6zonwe z1$@~lgY|Yqaa}nDe^y&WnXoXjmdRXya1&G~t~A%uW3a6WV8P_)yi0CYn~&Naz?evP zLId!i(pHQ-f_1%vqHD@bEDFKFBRpsLNhMlu<%lcRx;*8g<)!6KT8~XXF zmRj$O?aS!4hbHQZ=BrLgiMYb4uvf1t)ov?p|LCu38jhV!i_utEckGGL-W+tT05Mn`%a|P8Sh-%RkU^QJ!k_(2BmlHtD>cnVZ%{ z{6F!&M_WM||KT7=!@ye{dE8c1SyuNua4xEkB!6k@*JJr*FK}6NbZ?fmR#MPTer4fP zz~^AbmK;X#KlglqEm~(F@ATl|(&as+0^8y5UkSRwJ874{&=&D*jS_?FCuF;S{I1AD zBz63D2b`rUtf^%Shr)AzZ6OlY#YfhX2_aD_xK>4K5F02!PU4Ky@E8F9%KCSFB+ zn`4PSKNB6@HhpnhsEREqUo-Ycx0F;YYG#CbWhnZfcXngf8)cj9+=IXGKac>m7MR?p@@uf)5o`qwk$Z+#oX!7@iyfrT&RL8-C^ zYXqs9IgYhax@EZwx*~E0-LhNu=4=6R#n}?Ba zVs0v2zKn%q0^%42w2k&?h%^so_FI^`?e=M7R^Di~d!@Z|3Uu zivIku(gES_kMaYKh;{Nqu##j}9{jz&w(39agj+;d#~41Q>bM%MTb(MnlAo5Jox+RLOI}{2_0+0Z!JiO4Y&B2f3l7 z2)3}|G6$%#AEo%mT!cPGF-nuKIbn_hgPjM83tjOL7_@X0P|GgsupAzV?aJRtBFD!k zeu@XhE!6%?j__)Es)uVKqNhTJLx?xmM^pEmart_Oq~7(H7^7z|8Ln&ftCd_1sA{CT zQ-XR^XeRnqq-)yVzkMmqP~+K&ehhYZrb*c=rNn-=;#4v^#>vcvBf@`*GtUja;zFS8 zD1SUK*xT)clvS7Qh4obx0U6~R%?JO-BWpgGlD_CEKvp^i1CJX8pekUz+GANFuxB|; ziiCb)lOOL{8OV*H(_l*erVv=)@k-{UWh=($VpEaZoX_?UMuef!D2cuPHDX%&U9eUGG;nSBr(Z0Uxa3$Y^ab}Er_ft&b04$ic zt!>HT^z;KhEs6rqn{SquTZ?O?JAuk<@nma`s9i63;z^uXy~lC}-YgG{_6?)|v}d;G zC0X++NWDHx`+Sb|;X{+vc^^xcaL7GH0=)tTxXwjrVH@)L>fg&X(QcpMI+YiDCX$=J ztXML3B+#^D9j2aG3LY%IV0n|nW~GA72NFPmF+!41&yVu<>t;x2g(@-jE8A)NUfnnB_rrk6t*=mK!4zbz!Xv<=+sZXDUUKHZ`%Q4HcGuQMI2D`MUqAYi$3+#NwaCAfa%JZ#22s z2!o)c;ec3a64|fC)Ax&Kj}sG-PcXT=%8uDL0fZiIh*%<6fZMMd(TX3F64J>sg0Pd` z=GbST&JkP?rD|rt`o0?urVdcG+!JMcQOrIzE&rNzbv3%-){sSzG_%2jPqCeX=g1Zi z5HQi^&0#DQOjVWH7Q^&mmTmBYEmen&5JN8-fv6WDeD%E;bienkq!V9ZFv!~9A9h@n zNzP2Ipe-~}O8wxVLme@OeYsw6KdAjRaO${4N4TVR_pey6^mcXpaZ6ngjex-3vw6ce zkWWWVi|&2)ENDV74kn6D+#wr3_(t_#M4QjPmPOEU+2vXRJldK_HFb0Y2#Yy$rvrsF zx6<1>1No8m57c;Fe;d3^RVL*P$&npQ{7K?pK^RJ!;EAR45mSt73Su9__H~EU1G<&# zw@s#Y7$1TvhHjKmQa-A*i@ROZ!&}tGL^fojK@D5Sg@W!3re0L&E>8#|&wzG@ZlVZR zywd3~sv;GjqHXJnsPJ$0X-m-tn>z5)pZ}QkNW9fb67)%N>k$<@w ze^fMxGZe$yneSEc#w^Up45>>{ZnP*bB~PQqgV8mB>VhvNnBdDHGKhJx$*q5E-+Xd$ zF|jtu=j-7sX@_3D=xZCJ zDucS!?O#>Bmqv+iALBVkPir%W%YJ;qm1HUP!%H!{S5oV*X_VN}3%!9y8s-cy&A*V> z2?P-a`W*Jw>#sG0tdBUkfnJx#^r&3Cemw*6-$OD~E(YGugf2(N>;3@$hrYc-Yday~ znUINjP4nl^84t1&3z_N^1iv>WLL>hYQK>|Y)Lsg=U9=G_XJ9CY>46BSSXwzB43N#A ze;0@0q;NV|a5HZ)pif681LZ`a%zeo+Tf}>UA;t| zjKV0)Bo!+IcLPYp$a`KH3i%-S4dC9K|>~uV13W=rc)bCEu7DKA|PsXazw**nU zWD0kC=_(?mW^1s>5CxjWdV`@}NxW0ZAo>pt?SGmb7Ni&*q?pWVr+L}odS60HqCh;e zQZo@=mM|JXF8(TBl`29HJLegxx6(sSb&sT_CFZXOzrw+YdJ!vdqOrBRukP!}|2eoF z+7qVpM1V4ZO-PP+c-Q9z$+Ra}GW}mn**{U>t8!l9rU1oaT@z6&;CBEU-&B;ErVXvu zN#0kJYX;fAH--LuePX1~tr)QKxf6?m6p{IVncn-l_VoBJhxUyt;ba~@V$Z=L zAoy_{#H}xi|D&$$ngo8mYk@)=YoZD_fq60C1B=?%_aEzm*7>C}jI+=evuD~W-1;gn zx;^G<=1RVQW7c5u#dz}zUgUQ4Qmq)U2WkEt$-lk`_r2qc_ng+(IK0q9{{JwZnTLNq z5gSsY4zSw&C+l|X!t>PspX9H>Yd2DitH&4NGsC81i(7j$b}waZIACU=w5ta1-^p81w~DDG^)G1K#41FF@BTe zL*YmCSktsv>aU*nGsL9_y0Sb7n-z406i&`ZytL(qubqv2@&w(v%Ct5!Cnm?Y&qzQ< zKPM>WAdjV#b{zcYZM$_O>NkEP+&#(tBLa;2+TAs>lL~t)%L!lfF?9ykSroFQPXP8O zMUbXvmUB^Jr2kaKwX{d{%*^Qc6V%d=Ia!gk$tpTgYa9Y=vFiN@j+~>Pk5i)H;I<=F zC!C~+w^{ag;e~Z_6q<0oQ>vjS&U%m(1aW9hfZ;}95@o;-+Ms)v<$<4R+?_ov6&$+G*!1WNwf(X$UF!wzaiOyU`K|Fh zxr_47)|KxZ-9d+YOdsa3V+|{_4tIMkL%fKF?{~AZAKK%;$@!tkhHaol_BYVu{)z<* zQ(5~TJ~L2&zTFJ=kZ{+1D?6_(ET|qF%%r6~<>caSva5zw!r-J7Hp4-j>PBn&7v`Pd zw8~znHRSw0Uloukufh_ciJRw2eza#OOx^v^3xWwz)>LW!jg6~1h{u!I44hrS0%FgQtV5_8}fNqE&1Ms8yz1e@>^-!NoyHMu)&I06N_w|auI3!*0z9hAt2lO<_C3s0n;&2FpbRn6Z zgZS8?c-B|nP(s%FPZrh`H z7B~&OZ9Up55%QcTlho*DwPG_C`vdb`&v3!*Uz$3255vnFN5n)`?P_itVlK$9ix)6p z2kfS&=?d}}r1vv|739=SaB)`)O;4^{rPk!;Lg)`~lE-aNN@H!<(i~rvQK*nck@3}( z94DjwWd5h6uV`CdlY<-yuZbFQsu87slULg)#xskQ z)JYuASS*ln`liYj&|J$b-Z#j8-QU(tiB_=dtOfUiDBf5v@KQOzLnkKFivZDg%#}J)sPl@!(~R2GgK(CMP8| zQF9QQDGg<#om&#h*XP=X^qzjYFMa_`S765pid>=hO@WL#Wj@@@lS0H3)A4D2pwa%* zC67u3hI%aFU(8wgE86=vi|+M1p`BJv9zdiG8TdKdA;s?#dn&&spX)X0;yM2t8(M-B zg@cQgrW%v9t>3xrfXK)Gdr~j20XSM{@?iAVB+sLr>=Bw-QRcp%+Wck2LM9B-zdmxO zY0OBRj{3xRdRZjI0YR5$*QxPb&lIsy5)|n$sFqKi1hyEokTqnY8Upkec62 zxUKm~H^2EcHmbC>(`^oh9P9}TYjUC{;+ANOKw@A6i0-~ll4@I7Ie(?m;k<;mc|jfb zNZ9qEX@f=AH8)H5=eoDnCcY$LDqW`U$5kSzx{roUI>^CAar4DeAY{+7e33@~48$As z%zu4_38K0m5)u(ZSZk>c^?n(@74q#GMdYqZx>act%Bpdq^ta7O9uKFLm? zqtB`ioDz#;viNHL4n^OGxm#XfTUvy4O?bz+sRj7d0IAG~`MBU;ggl_<;$5%LT1(A#o)@F6K96obpT zt65Ik1d=)(JGeRh`PMp&CBsXNpZvU4rTadU!BFXHxH&hR0NUn&f*6R>@V{Li^cx*hGT1Rbwxn#}2izYM+bMvm`-u7;4>Ve`X z)o>>kDYEkXV6vw&r$ImDU72|}ou+qHa#FpHULYeHkW3q0I3cXq_tdnD1v$fEM!vEk ztX0)B-y=3q=kX6Np5Qw?}HY_P5F=_QHZc9HUMM_n2`&;_%7|IJy7Or z>2-Iu`Q-Q1>2A&VZB1U^4}KHAzeTMf(I09VB1e`l^!50{>dsp+DZ=XYHOj-z7}x31#RrebUlww^-g_%M+V^dSgV)VbKzfoT2p2UyUGI${9e|5*t9p#ZnN>|Eh zC_yzeri|plwKi-PY)*Jl*e#)g*SFBNG=>2UQwu`cf+@Xv0Q-=zD#REPLFK0qEWolc|$qx0;-JA*R~;zS5n z^y8#%d+cfdC_*}K4OgkioC+bSK0<5xKYD z2D4T-EL#Z{YeaH_nBjrdz~vj}>zfc9l7>lBXaQ1RUlZc!5r+W_-@^gFZs@A1(RkN1sv8J1CA`=9 zW3E%cVHMM&0M)vF@!DLKhf>3uO8ij8(rxlODa@+oP^@71_^=Os+Gc`US6P zidZ8;`>+MH;KrI)>ueME}eY~@q`9>ESX85fqn5j+0 zLyd7oY?rK8sKbiE!Pdpt?^|0FSOXmtaxz1IQpY>|1bpS!eEIhZg5O6Dgw=eQT?_53 z?Y)CvaeVi6RW34(V4<9-m=k@qBarKJjtxFhuL(&90or&GL#Bw6!@#4WR-GNWr$Ebn zmK^F3Uvwb$?4g-f*i7=n7{QBN==KHxc$5ML2c&D43*H!Hfd&Ab_jSW%>5$bQRxLxqt8|uNM0#=9 z2rW4sWm=lI?RkNDS21^{Jov__?{u5JFy=JVAH36onw{ePy^L^)NPV3%@!fH`IZ1|x zOzX-rP(Wf_Pn7dO#+z-f5X&R}XI77#(cvKC0|p$!dZ-Gm_DywiuHuI}x3MxuSS$cp zakvXu)qhCO78Ugg%GJ& zeYR&}_4Y=3+S%ygj!llovWaT0+cB5mIwAjWUYAf=t8X8~umh`1W^kJh9(Zu~r?(s2 z&)PbCfec+BBJ7f_g%yw7U*9<<-fEFC(a(q5>PnLx-ZhP{G8CXm2;aIZ7fl;%?Jv>i#*6W-+W3Y(wjN#uIHsc6M|Nr?{t9UxU5ip(DyTcqAER(d|!y*fv< zzeG;axrN;kZ5e{5Pd9(=FI4q%PQO75ZM)WkGt${R_!XFUyJ>kdB;+nmWSJh3&WaK6vC|&uIdVb zF%m4h?NP91BG^JDxv8zyvS$*We|&;j}M69WFeqo z7OV>n!CB6e^R@YrEh@cwXj>O!o6)U;qG@hE;>MNR`D2$gL_d(N~tH1F4%5G2n~LeK5X{d$cRraufUf4T|OI zx&PosA#>jZ5=c@!H?_E`DO z;uWUG22x%#pybcbT&{NOg2wGT`J);McEa1q&M*rG$U?sUZ|WhbYrbr(Fp38DY`MAF ziLLhL8>H6~#63m?n^432b@EP1lqxY#W#qv%q$_Cn>R`$CT5!&QFQhjv+McJO4lmhi zf*bavhAJK->gxwNce^0!Jo16Vxw!EDS1fA9Al)+~8fIy)4fCzH<#HQ!+`ZmjgrL_TOaZ@%q-w^tvl zQxDc)s#5Y&^@seA_r8=um#tE$inXT@$N&-v>?Gpw*{32MO^IMAA7dgDFdgQRBmBFqf> zOVNCPijFPn*gpz;r_}6dGFZR!+cr7q3Vyd~)O=e}(z}OH&B68)5w9FT2hPux%M>?*HuS)frvHc5oxc`r18KRBo^5I&lC6 z6r+F?0OY>?`S|~6MO2^`Wa7wfLBncwT`Xh^{1p@_!iyk^#qa92cO$8Pye(vdA{7uK zIFlJz$8LHm7;jv|?$u}up+OwE98r$6IQ@-OkC1AaFj5H;i0qV1Xl{d5wfv0ly^@?)a)Dwox^y^TIS9VyQrem+e07>{5=BBD^y zkB)%+oC@zcFxax0-+GYiHTGw)IP?@xQ-|l6lGm;R-e@S?o9n$!&5i?Otg`fZo_t-{ zAxtyITVg6u^t@^ne1&#}UsU%}z*|{bTbn{jS&!mTj8GSE!{^T%P!Us}tJ|BX3R_*3 z76avW zY`9sWPv)K|kbupN^u{H{Q6{)B`zd_S?Znu9q1|P5m{jq7t-;sQchm&mqyydKez@~< z`5#*)R@*!!AYlGYU~g;dsH0_%q3MOsuhph8r30$v!;p=adkf!Wgwls!GDcpm{2VX# z^(A@Py5+`B#2?@2Etcy67(O5P{~82GTo8GwkOczaHqwI2UTzVQONh)V%#6wclQzpZ zTQkok@!{uPnea4bR1Eg_jkHo6{zkx z>OLiQD3;JS*dkX!DLJERtID-sPJg-id>)p0He>g;<)h1b0Y*R|>*l#QK~uN`MS8)b zd>=||^OgGF1QrKXv=~}EwDyGw2(oqO3HRq6`0U1CN-34Cvhz1hp-6q9tW7WKG5BSd z5i~3e7g>G6Jz?3Y%6+73TwDRyH*Dk3lJ^S*?%uRuhv>m{FP_RD%_*&QblyT-3WBiC z7D$6Es~6@gr1XfQMP7H?2}ti?F?y|TXDb{+(}%mU!O%wG6Cpq5pydb0zxXf95wpg| zhWZ3YYz1=Iw*dLvVjOn$#R?A2?h>6J@Nypv(p)W4;4L71QVD5a>y2udElyb#nxf3TG^Um+oUK@3z z*e$}?g`vXOaadGWM+4@|TVCNR9zg)s@jd|rh?AsYL0@to4Ja)|fkBFNcXya!->65$ zlssyuF)Tt)H~2D3{A&WF;?OBse< z2#8=C0^!GJ6wqGolq;N=+)SPpj9q^`!mY)}KJTUmV;H|Ss=!9S@Y`d|Hl`e^=nEEG zP}fvbG7_*p#A6o>xk9Bwbb`6n_}o)mL)$DH4Aofw$d6|Y9_AcwAL7PythVvTm=Unk zr394sZmaU87+y$dtps9&HA_05AS%K3^y8V|R=(0&QQ+%=Z%=lCv0h3gnT$G90o#MH z63@TKPztY0!yd1>wMjl+8U}%_W)?LhYEmI(@jiHuC$-#x!(yQmP2+r(d!Oc zTV>#`jgX*EORp5G>3)^V@Z#T!;NcbPf_hVtaSfM%{{_QlZfJnwY_&gQczF2VAL8R! z9v*OMXKGlPK^GW48|Mz+m6JaG@oc@&(QG$PbIw@GDuE5tVyw`CmG-@2xx=FATbmTS z?*^$~3j9;9j@jNEYS))UZ2Rb|z}VS8l8|d#r?{nrG8kB=?5~LOaK_N3^1gXOZy7rD zJA}SV68Z6DhQ(@X+_|0$k#;iwd3`j{q`;z`q$vUiOK3?Z!-^QArh=z>PI=%2@T3LA zcM+mdlO^g!H8oU?&!1R$rFiMs&FR8;+3xQCfK~>CduZwC9t@7T;NxfLD+(o^w%5BGjw;K-IDIyF@T(r(2HQohwzc8pmHHMheFpjn z?F%~V1kBPw_nWmEEabv7U4=#}q*^J*A#p_oZFpb&ayY0w3~b&!2=qMR<+ic1f|*z8 z^k+~gFzJO7L@NX?ee9Tl;hPrn>dqT~5h8)oGh?9)P4D@M56-ti;#2*VO$jV=s-a)f{Ujp31|Xlh!LpOIu-Ce8(}Jo;!KY`NH^v)~v1 zxs59<+{rU@pYNq2tqBT^!H&n0EXR0Y#6m?vB3_6Lwh&XnoIx#3b<$>k)a9I4vM?hnYYQGz%6g1|1 zzJAo>eRGxrPH%=)$Zv<1M&`YDK0Q`%b$uiTx&8QfaK52{=lC)Z6TQ(hML>5;!hG|c zX4jILvTL4^dEM_qDVX-I)fd%p(tIwev@EQbp55J$AZ}=;t^Z7 z$j`zGwR%jl&b1`DLC*muDQWBO2vkcpMuh|B|5-_J^hI6I6RBXD^1P5w+iB^85whn; z$i@sca_>rA+`Y$dhBJ%&++1GVm1HiT{R@)r(nJ941IZ-C8+Y0$F4pdo^OpK^8CVcP z29>L^giEpie7mmi& zrk+EqeesxEy{Pd{FysROSR;-=F}4cS8hGh@W$%7t6i;Iz`!&!juS+DW}lqZ1&Ewa+jd;RA-m4={&A0Z&|wt{`Ocw;{xs)y3S!wS zV7eyX8X|=GSFJ;Q@sQvuN#eDpy)v)*L@nJ%Ghz&7nmB`>PYNlq%FI0Vdww|BSC_h>@uGT$`G2a8r=q3Dq0#jX?e*0a&Mbh$3z9l#622OG3*FEF!s$m14L} z6e98#CD~o4*C{*2!1J2;oG9(%Soe^y91C}T2z7tbhZvD+uvZ1yuIdB zVqHr9I>L`?zESKOFPB{3GZHXuX|;4}T-`;R0K7wrdDM&akSG-CQ~B+;0s*H=_2VHM;t%53Z9XS5S?GTu0;7fC z-Lq~VjvDKN$=X{3A?&{?93)_Y!2mKAqBk3leZo3e*z-q;WB4O)S_aL+1Q1=-!x4X!|)^93dcGdCt8Y8Mrq^1w+w@l z`~+szM6CId;*D>`Re$7|w&scJY1Ic_yp}&YN^8D7;U~qyEqCV(MuXz`{*OeuMY3OU zl}VfN-vzxsO~3*xKs`#YgI3>tI zj4UNlY36$s^nB$wE{z>{aPYcc=9v`v$xq-um4EOTAu&33BJc zScGl_wEAGQ*aC42Tr3B3TF+hv-!;xHefB$Ewfgnheg(<^pZME1MWN2aC#tHU;aC)W zo79;M+LS(8F6vj#;jr(}RA?yB&sL$P2X0GryZmW7Ap?0qvd{UbJeX~7zA;JPmrfmz zqi@NA_H5B{JaI=Bd_V-L{Ou9uWi;H6;C1t{tj_y~kh6dn3LnOD%c;BdL%jgcnR$Xrv>^34^q1C=hI65?rp6_jfpE3T2`W$r=PRl|R~4TDrtqk|H^n z0U>S++L7lHVjDVNKjawE(}9Pe?>bLZD4lCZe{Z3YL4(B?r$2!?7={T+^WI--$7GO_ zPwZIAD^x2T)J@Jtxpom)-CJ?sXjdl7&BEOyE0fpah3M~qPw zb2~gn&qZx#<#YWOWTZEGpH0d-KLnR7w2bsBm4;D={ybRDv#JYq?&gQ*yzc<*5B<6? z%R{BXTi1g8a^_Jl-+SrTkcQn5O@6kgca=Wn$t{c+2AjVF(y*uOKQA7yri7OOgeDa- zm%9|X;Q^42tF!U7q~$u8wl;q;k4^q&=(neV70f>Z`XjgEa|`W$ZoSutcua}(Il-f3uc&}_{BeGp`$_IK`%TNZw5Li0H8mw3 zd!F1Y;DPUHoO^kb%Lf1tp)cPI_4$Dl7}OK@08CJk2!qbxBQEu0GPYvM2uX{%@*$E0 zg~2U92i{vO|Iyq3NCYreWqKt^JD}vL@qu`<0d+`B4hEW5Cq&(oCmn<9eC8WQNR6Hc z4)sXmZMp$fXwDQ6xL-FkU9|w8Dx+-Xv$DkS7Ds z;(@Kp`*L-Yr(+Zf!l^39)E2Q_*y(`&e}uRW;!HaAywRgFI5j2xTZjS2Ntd@tP)-Gf z#fDdF^#2!GC<}oS?Fz!TrL6f@2Eefn*sE5Ws)1dkOb?!2wv%(`6-Wo$K^Ktd;U;-$R}0Rin(3(Jog5kEE?l@Xslw+D^YG`mW|-j zpt~K*clLQzyE;$QYATB|ERhiZv!!?VDq2OF{4`_YrgPH!Z{y|MyvEBq(OI8n+((p6 zfOD!_h{qTy7UcT+9OZ>R#GLO4k%-Hn?zN`P8Wv=x6Tevbqnajs3EH;WcCW@LFIV@d zIMUadn3jsc-RPKb@T)z);iV?8yOON4LknNi@v$1wGlE_cjNzQ@4dpwGk4!$4FHb*? z91V>J6D|7gb^q~QCIaXlUUj8OCxOY8OE*iLPHuuLmP+U2^DVaJ%!6NzYOTa38^00& zAZ-2D>j5c(L0 zbyf(V!m7aV_;JHsm`Q+e-hC0;g5g&Xa1`|T4jkw5;&ljU%U7yF`Asi-AF;BP*%(8t zC^_?_V>HJ*ZEXHhVvMyoU`Rh(PW^hjezQtio1QJMW*wttPr30!i8#Lrj{ouH?EKCL z@E|sLu07j2Si%wELRj)jXhBNzbFtaN&cQ7Ts7W}QUW51aT%hR~3j8swDU0=Zy2}-d zMbs!IQT~vR&0p|C-}Tiu=}|K5L0CL>%4p{fbdOpjlqUDv{f2OcR~ zps&y{(|znSjQVD>jOPP48I=P|@Sm07G;hBN?LMB)`)qa7#Q~|!4z14-AfLi|HP?Bo zO@c@kwhTE<{7~S3G?4Kmx!_lmH_`QVKzE@YzS;?SRHCjz1VhI^7N|4w?m{U{?w}b{ z7=ATU;6)7pAD`w$xh+cKeSiGaGu$o@n}G92mMb=l=%qSYq2c9cWxLs%QgW+-)@K=u zmP;vO2nulmgz5u)VCJDTF3OXPj8<8IfwkI?7%5HxwB!=<2#SiPDK{(9a%0G^!%W|aj-U@hJh5+=G|Gb@lIeLgjP6|{0UBGHR?p1(ye~L!O34X|j(1N@{wNxBvkL->< zm2JNcG8db`^Oo0frT$8zu&j|vpQ9HBSE|@7DzgZkvk`FbH~fgis#v)3QEEIF4>k7P zd&|UubB48k_?=QfGW5aXLkyHumh6pB4&x;W72Vi{(z`-s{q9rw=}$(v9w)qh`{uRC zi#qhfVP@R3xO?P5%~|Y}LxEx4q5tA%`;m^XL02d3_JwjzC+~~_0OtI%^vjni^RHVo zA0WBjJxv`q?-dsFul2kyHcn^deU#h{qqAu!ohpB;(As8C;Oo2+UJYlt7P-HSUM>pW zlTIa;S`k;uHmLj>*Y_r-VSco@Wk+{18^>->dAJt>JnrnwBh!5(_MzdO6M+S3bC|4y zl}-Hc;%?VdKH_`>oOEv;$oSMv@zd9#a6)x86;>4}l!IJ{s>}@s-iTlDd6u^Uxl~d( zofQ&f!xQ{!h+j!+rYq?2T#Uqzh1WY+1YJ31YLEGOuYVDG74iM_#K`^7jNgr|@`iZ% zv?2IL{c8e!c!Mo>C`DLirlj6Zxj=zkCqWO6&*4%mlbn?8X0017{T_|tJu9Mozf(o0a=2t(w_HGyCu=3a`b1Lcb(H&!655IRQdJi~Sw0-197(Id*8n}kh}+*iu` zOquu5G!*RnRT$%Ers5*Q9AIiU6=VHQjUg3|cfAdbZcB&UXxh)=8St%CZNVWWP4_&&cm0J5^*+&0mQ}|)V<4+6u zj=_bI1~e6Y?S@}vkeq@yHi3xarniu*$a&@WS{M2M|Atj_i@E(6jfZ`BFzg z5vkcV`T|&}+*TI}B#tM`N5U{bMx@4aeKjdVGxtuZ=qYzq+N7PvF}QZQg3jtH6HU~pXrFum#JY! z7w2LZdu+vCTq{#$^SYQ||I8^=vKhdt_~sQX3XN&_i#Mq%Sb~+l7*8THL>gm+?rnp& zc*NUbApt$rJB^q=g={B+@NDLRY)$-9YMh^k-8THwt)v|RXQ z{oocff=J-&8Dg;3W+AES)LS_1N=p02n}12@xie9cIEzBGLQ`t7v`8b_@`F|(33PIJ z;0y`;lo?=m9CE~|fV)0AIUm>li3w&ymv95of3OPZ5@yVg$wF9~c3B}X77FUX8$#}$ znV$YzuD2ZtnBFzQ3SmVk5rR8UQO}=m8U26oN}ps?hLs({a~MYBp!8XkNfAKB zECXvHPgY#%9OGeAKA)D6&2L;b}7uBzQ z4(alT^Wi?$P3E(~@(^E!R-Ym@3Z)^`7#U`U^ zzPASc6RYS(yYm7=teIHrkF9RE@c%XHGEN{r33Kw10F$t?!=Y?q9J}9GUqu5YVT>F$ zKi*#mgO->6Uc)ZaM`#T$Skj169+94k2u%S3@G1ge;RTL;m@zqk7&8nPXE+Bk<{3C# zkc)nb?N;MJTOknk-2Yw6k>fr_swE zpidfzI~%khpD&&)?XM;>C{|6WzH5);E= z?V>NH8$RJ`mEW!Qc5Yr!lik%C;kiBEC|J`NQH(~?;E)mQS}&$!cj;O8@4A}??2i}h zR)z}i=Zj-(@9mWm&w?leg|6Cx_pgYP(?QgTd4IbUl@w z88-3(GU&00^|eJVe2Bg6wtLP>=SCXf%So1S+AHC~Mny5W_bxMkrvbIX@c35TwPAU^%3~A1+RMT@%rv}uecsIUqq>35EB*;Oq=tJY|;29YZ;(97aGHHq}Kk< zNbdkAFs}5y%}1n;&O1$hU^S1HhAc}fF=vIfKuYop&%bg*Ff$a2j~$^O^W0l=Kz07m zvegc#`A4dyUu8V`C6GbV)sLJ(N-_4}g*jOy(WT*ERRFg*fGN@j`^yGn=#@quBFBqZ zqs&0P-S`nqfikRE?*UKbA@;nuG}ho5>%wz$eW46H%aZp`LU*3Mr)7UGn$jA;u}w_s z{VTJDsP{T*0V|ismS&9s-njm?>4@Lh^)&j5bQzCQ zNXa41*hoR8n_qQfW*5B*=%p|Aqx^y2YP(SRAuo9{ZN^U(#^?f?>IRZCgFyg|1NK<^ z-zUwepN}fiBS}o`&loB_a60iz#&OGq8 zFqe}3tjqGuHk){`L`^u7pC=IiKbEdCDypt+&oFd%cMe^W5(6qF$bd*I4bmkoFr-K- z-6`D-A}~mYAV`CBcXxcp=l#|?Yt7#?XYYOQJFZY!9|#~EU8e`bq|37jLDg=rr;oE%HR;i_IXHW~NO8ejJ%dN%qnMe_%Li4b-5Z5C7f@3;>n7Gb;SOg-mJWgqLi zYR<_M6hSr(As<(tOWd$s?OLaxuBa8QUR3>jd!o*5R0CEOL>HuvqE5yZUjt>6vQ8*M zKN@{IrYpnGZUGk2Q7?kmU^3TdN_*V)8DJJkK#IfGY zR%@ZMp*oKv)TN#9aR-Kc+cxtIxtNY1q<5eM6+Niz*ruaF_X^!Cqxg?*NB7UIR)Ux; z)i-bah1}xIy)f?f3y2D(l_P%@++`V*F536LG`Qfzhw1ShSh@#%rFC?f`}Q5i7#QrE zmAAV8vdVsy+V1ZWD|QI+=REX|-y6z!3NmAEybCikUgLcM%X%scK^M;eCz%6I1puVU z_tsnPa-;D3#h(6S%|ix_Kvg0C*l2KTb8IW0 zTfwskBJ-CixDE$`-t^+jx5B~)BKZ#jj5*^q zS0w#@AMn9($1$3v4%}=N{b103;_toLDgU;-n}1ufSoyRJ*0Tse=V>-C`89 zvWCSw6SSSP18O*&j#Nv_>e={Z3lt=f^BY1;%#Z3rw-}q4G;V~R@9pWLtc5pCZcI)E zeJ4$R`j6yL5ia69A7kAK&}3*!*H|V(S-kv(i@PGn@wLVU2gY4ovPwzun)rI`>%9vw zzy9qz-a7exSOaw|JkSKV{*7+LVrLqB!G5l*YokEKgt@?V5##;1d-Y^`o8B?)cIIu1yBkVa77jV%q1r7!P+-KBy=&F~BeM8# z(P(sg^~~d)#B}R+Za}8mk~1X?}POw9?JQLo`Gz3fH^FXMmN1!2CBD;RonCT31dr(~mMeKNUJj7pWG8pPXxZZ@USTJB>PsBKnA^e2L*Ax`uPS#{5gL7JGNk) zb&!QBDCtnChZ5ebZ&d!S`+_d*U%9W_xa@p(VXxRKiYgD4+OcimvWah9!^7PKWIXiic?V_z5{9wE&wFtSV73j)_6rwM*BnzhCUnf984%J zSq>^{xbXuo>U)bkob58?Y8Hnr3gdK+{)r**oSRRw*L%E@M(B?D1rszF#ayGhPyeiOojq_A^ENas?j{ z^_hW^s+v;tBxlAg8d7HJ)l&Pnt@3;R%1;7F8E~GzC1fDBe){(WRToGbZty^EX#STz zIeolb!}LFh%8i<5*Lh|V85#LJI{BCa`J}oJ{uwXeMih|wZ{qG05<=>%N4mo(2vbn3 zAXqz)QC?1}lQ8*7djV~jO89fuKDLF=?)}h+j_^};P7wAHnHlp8=>A<3+1}Q}j380E zDNP4~3HxU;9Q%AwP2y4h9OW&(^L^vnd`i${0lVg`FtKJ_Cm7)}qe7AEN2 zx~PVdM!;W_NiiccHEs7%C0@WeA@X3tjyU`%omp{juEmF!sc&t4b#)4(IX+DRosvwcw0Pt; z_oW<|P;8{>dkT#lrWr2K1dEY61GPOA#{?<@cAwGWrui4!Ib!Y|x0THssWkW(Wr!ah zbg0#}lC77|9z}c2AcxWDJ{6c4?$O+Fk|jZ{VL!jz`XOPM<{0xN4rD4PsMfvgO4Ht~1eGkl9k}7pGu}zGC12X)s$+f2w@Yu+k}~1AC{~>X$8d>wB>WP@fZ=3J31W-v zhU(}a-_x{3Ki+QRy|=c9M0ADu3LJHD z$R`G0p=hZKEkO5Fd+JwmSbWS+E_&ZJIZ|wJfm%kOcdLm;GCK456IfVXCF&Rb(idqi zw%M8vARlD>c}cNDFwBw`^@E~LwmK@j6wSeu`^(6M+gw24EU$6G=-j}L4j;OMFR{2i z+WcOadninvR}7?4*OoG+UTzzvLWJ8Km|L#9HN{E>x{fwPLr^VYfNT%i-_>i#G#(Yg z?OO@(@pIcNG15dUIttWm|7##`&xGpRW8yN$$YOrQfeJePl#?a^W(dY-O<0s-*4RC2 znSqlUz<4&GNu38_B*U_=S`8ilMRSGLg;hBOb#OKsuds$SX zh^Td4_oWSdrHTqmX>GV|eGum)uOQcij~5I|yu%flC+*&00MDz{G-w`ZT_ zP=yU{jTUL?!=%DMgSzIFAL1#v#bazXI)_7+tdkJ#Xw!cF+=OucAJ6T?5Fal;e;9Q4 zw(c#Duj-3S{BiH6=H&zG6@=yy-qOSsIE+XjDAt+ePf)DHGB%9HLq@a zG6qc60S|;7-|Ai8IglOXDAoVRim#Qh=v7Cy)KU2R_nSpcvx6O`vOu~YZU!FyUF*ty zaM!8syvg7f%K2w=rEgWeSJ}!CAs+o3lO|clj66Jq`Qym5)`i~)mIE6w`5u!}nRgOB zX-4b#m7-k!I&Zld-4*+0pRMIP0dVkO@kua(b2l9&*oT=znh9aJr?`es81d-TBPWi= zp<0DS4_Vg|!r$;PrUi$O!pS*CwX+Kf$f`(OWq;u!zm-iyfQoyy^=!*MZ0i*ivQ&R&Z__&f>lf3AlQY`jC%!&5)tgStnsX|(@PWuMT9LDiMp?c75^mcHP#lW znv`Qxz3?`q4-MSOSiGIG^N=e&zHh|>_N%Y_`?{d4oG|&Y518=tfJ-07rDKtfd`k1e z*ZBrM&E1bb`uk47y0f=(gug&fTB9VCFkiH4cW+x|eQMEpKp&BM00EOCN~Y=7b*Y9eqyUt-QOZuaEYM71U5gBnSue>47w>;RE3MEr0pX zUNNFqV)a*1Z?bj@yGo`Mwt`n|xfv&I1N6QNE#p^_AtTKsju^fRF|#oWJMq6M>bE=* zAK^;u0I?o$dQh1GXZC2$z%QDC_m&4R3KNgXb7}70EU)$R*cbD^D=8h8o&uc1oIzd$ zA2Zhh6!|g7tMop_s1hCQkm5qP?3E6py+(ALLEVObRN+x{ti_BZS3wPtbQlEB4G8TP z%q#g3Qq2&=sNH0U=6r2sva;6yzPRrF%fYp*7&p3pht#wCr7X`41kdn5Vf1xlc=^^1wb124rT(XG%%9d zKFVX|ijTc!QjDUHaE>d;_5EqCnk7wO))NDSwlrNHPJE(XCi&**K+j(Gs{3SV-Rq?+i|e_ftg4%g4B9&a3##K;(Cs$*bQ(%F-lGXAVl9$M_NdzI z*4^;_*k5ks0`KS^Mcnpz{)L|4EV0225 zAL!(ElaDFIOo;X8dQ2|nSp;1#XzQZrgAyk-C?`Y#Ea<3eTpWZKtbBcv!SK>lg=YCV z`&~eG3ssj*@MMX56>?#m6a5uF5gnfjcGX7umdkU8*Gll!;SmBG6K0n8}ma)gfHJLBS1FzYv!=9H)9-t!Sw6axR zp2l!PL5*&;WIa|<0gk&%tnWttF~v!#--?vN5t*-DpLqgk=}<3s7C9Lf>6XU2`?yU?rCB`M}-rmI-7C`ep4m@ z;*Lt_3_0|>v^bP`2qt+fudjFWqwL0As*@RydB{lnU3*T2ySEVP?G|&^q6t+LsYdS> z4<~%VL`m|YX7lz4hpnz5sxufXZgh4sRK7USv^dZuAB|C_2xwK2zZ`C;Ysl6t&W7_a zVLq4C9XSOs4MqF~VP0}_DCl1id~$Q|amSeJ%kHLGA>p~YuP`G2qWLohZp1&8!jt>C+s#<`Ri-uLbQzTVtj-9MLaqX*^aplPqo_sjnCVfY@N9VuYeQVR~z?64q21UO)J8=xVCPsS2^;AI%EW z?IDy#QpPrk>WyRPC*=a4Lsciwdn|gm2${@f^!GnO=3atSX8$m< zR}Vf%Q6mrBNET4M7in3-99W0JKc>JnRQG<9;^xbr5{=X#|mCb-_eP zy@EwAt9OM|!h>TzI6$r2{X;|F;$u>-zE7oQO-eNWN!^aCyO z=fb&i45`>EO10ozgJ>4vXz-D^T98&QE%?Jy=x5_E{r*}%z#8O|6KxhNQnFwsW{U%4 zCfM#>J2?Pw?gFH*s=Ye(&-NGaRiskHd&{)?Ca{BtROQBV&=YS#j36?p{jX1se%{+wUrZM0+lgH_khCc zXjHq7Y^ku!C#6|k@nY~$pZBY(-1*MuU{9B2)=>h^H`Ymijpd~;jjHitd4D^QOmKe9 zUYS@n;e15R7Bd654DvY60vJC*pyQDF7-O+@jC1A`X*o)Ke+{jFK6xOXayWR z88f?Q`T6T*`mNE)-GVV@=zK09Ed_OCGS;7^m{b*vEW`EdGVIA|$Rs6b1s!R6tk@;x z&Yl1vGCAlt#bc_F9oNl=TvK|IveY2*hXVo@=s2@vbdW5{0ta``*On?tv1R|+pH*uq z08jB)xZoSF8~@2@Qev3cefVmfe7tKdY&UgSQKK{rH({rmuIwO0`>SGIH$}pPU7$j4lijC zbh`kPt2_;^Bn@k-5Y9hVX0t<`0tLQ2v%Tu_z)Q#%qpEP))TeqH8Ry=ARUKlX!d}TiH05Obh^|2>^=qX^{|bl2SN4;@^vLfz_kg{RjU zKZjwFyPdPV%NWcU%^d0A*t^`D8EKXI5$^7#qU#%@xRsd)Tw2tsDK+be@8FYn-tV!& z|EvLu`zU>EDMNJ<2G_Uc2$dKqApV3`(?6_n=s#{Tkw&mNkq6bHdul-(aZ#M{cxX{p zBvsJ(H+VL9qH|)$w!mM42<62wq!5Gx;q7Bick|Lg>2!DYphnQ>t?jvh#ef>E&5BccWSombaC{$a)WUuKn zREG<=N2%+*Apos%-9?XGj#$o*KA%~Tph;x6164+7ILd+`0e@QmUVZ)L@2Rd6YWxJw zuLgW1e^O8}vF!@bf3RKo*bHQ;#NgI)<-&=z(t}+nl3nG$9?^EAaP<*(<=kahe~cE% zvTse>ubaq1z5Q0X*!(Hz@#2-RQF}|A!5zOUm9s$h`Pb0)ZZOF&n&LERJ;79g2_Sy~jkZ8^dj^(UdF*MW|co(_!Xib*QIQi0&cya;$a*KEPHapTVx1Wdw=mv^7=!2yN=_GOpx# zrEb$)(BIH5TZWe_GhSIaymK6`kB$nvW@MyCTS^g19bF2`!xW5>!wlFgpLlMXt28FP|L!y+w@z9F~RmUBG{Sg=LBD>DS5Do6q#14z&!va9FzVfS}?>ZULVZd zM)72-Z-P7yD@6R0E{~Fo>^XP``~(Ek;Au3OU$bWHX4dC2gY(5Gv}$xGavaD#4o5L| zk+g~LnCzPoEPFVIMHpR@4?SIZAAC-|BCbH1Do(3{d4nD*P*RU3N0@C$%q@vMCZ}e_ z$_^v=@<9SEM1_>KBNin-;v<@|R?%xztrS#KB`DPJ0E0xouDn@!<7%rPiD~ar>!v?xBSoPRXgL?fh$7yV$#*T8dUJE zu&VmBmgms~k!4PyqKtp{sUlJrt8ZxUoQIVK^qaP(S+$fbm=X6`T?}0WRV{^a=j)hk zIJ2<^ZGHUv-knehyS9N-Zj$5y10)~hNz9BW?Y0&!HsaZ%C%S2?(cUh z`gNEtWCdM30!b941VRHXvPe>@f6fd7WS9n_PbgnVuVwqm=@xcDexSg7;ry^6%-P<^ z?yrjUpP|GWH-KSCJaaUQl=H}AuE!v?XGm8)i zN+Haq9tqyu>&; z-IQ;qIcDE44zC>zpOFIa(8PP)3j{``%vuWzKL3Kmm3 zq5O(?G&(wZ1sx@2QABKGq0BIvu|+B~a1KMC>7ao>xQUD?Ow5dGOpE&MsbqN<0&;s| z4)*rvE8^&wszU*F;0sIu=LfM>1!BBOq4SLa+=ECueSV~-Tr(pR} zp%S{?Nyn~=w=G@r5-Q`o;O82_qh<=LM`)i|!c8;KDFNWL0nJ|bjp8Bf@WQReZ+X|! zITDy)dnwWey?n|CjIX1g10aNYeaN|Qoz}on(I!a1IH1ZDR!7X}LM4DVDQGIGmDVYu{UQU=Fh}FJ@&w7@^kpN5VJlYWo1>d!U zJSKo6>W6^nr?b4Zfr-Od#q~SC zhgq@?7^%r|#`!sc;sa|b^Ap{9#IK3>=C6POmLEaPE<|uM9=_w1pn{X>kS4`p> z?MF2%^=L}#clFUT_&uR^d7~L!lj)7Q59we2A9Ooh*w{oMEO|Mkxx0Qf?=Ue4cLg&D zgD;l1pH+E?Bs{c*@~fAh{90?y(HrmMb3dnQwKa6Qtfo0m{t1pf5vZ5P5^ zRJ_!7Nap~GqIZ>AbN9=qVYd;RgMV1O{t%WK^cZ1S#!!9l^Xg!~KK5%3J{17L0r1nE zWzC8!pFceWn{omM^g*ppHK5**+qcf4YuHE}L!dDTOsZdHNKOoLajas7k~KSxR&l@L z)vtpNOSZYJtEi@oDYsv?R&+#1N9$a*`L{Emp6X_NUh>29qYV}otRHEZSd(qI)mL>U zsY_M}iDXEulQlU2BMjhqaxbs;saP>_%t>MF!slukK2q)%6UyW5Gn*3`@3k;k?1=RyPMp(d2 zIvI|jOh6Dg$#;l<*Z`b5iIL`s&*PjpUw>mOzdwfse&?9I5ld_xSXCF9rrDLPCcs2f z2x@m0OlF@RmjXa+6PIS^cpUUdnZxm2{RgZ$hR<Tp7)&r);Yb^r5HCfZE|$xMXici@OvRh;ji#%q6OZ7fD2z3xFWex0 zE-EYp!``jOhYz0I+GUjDk9Vi*2z63^JS5^v_U7k0%7mUFy2sLYKNMdH*IJRiU#8_+ z&HDYLoYN{7VN4NeUBPP%3dQ zTphWr;RTwM=Vg}DL$@PxS&3|`b$CTob?p*X-1y2z9vyz>9e$kw{`fci?o=|+-;q?mxckJ_81bSD9)jwe zxDO=q(wxc7X7{dvx+b8T2zm}@*ioFPl$6XuPab6A{%13}GuB8R(T#TQUCw8XZMNXr zOP%ohIfjBu@93eE6x5YFKqqc`^v?!bZZV zcZ5KZvud}73T^!z;Y!5|W9sL`pFV+SP`7qFqffq+i8$wOKO8M(4ZhSpi*XGWvK!r) zc}!h=H-w%n>><+=NfVQ~PqTat&Q-uO$!pt$1;{Slskrv9&%D&pp)hLuC;@Pz7k$=| zNTgU#G@Z^_k)rP+hg=RUP#zWty8|<7P=+|uegYmIOYf)AdX&99=FYU<@u8U<4@HXe zvMTb6r}9qp3L2z68s93T%5QXseJ;9u)Xn#W)F+Ze9?Toq5_o+Bz5GWAACPg(!J?^{8`?9P4*KC5T+*M zrE&(m(c^{WF~1qT4JbDZpRTEiTDRBSFX_%IGR)NBSj%DmN|2A5sklDY<{oZztY{wJ z{_a|x{<7@D$oRlbVGWApgMa;aW$qP;oWCRoLUX__{~77;%4hGEG#`JTVw9acpR!-b ze;YO57%d`rBeET@F$R6neEyi5K;+xViZgYYr|x8WUT>al++d3!tS;FqYS4JkAWIb~ z2;UjicN2)Vy~k<44PtogY!hQOa6xBGg76x4WAVKEaWw$20Gyw0*2NJD@mW^asYx)Q zrV~p#oUHYO^Objvw7Vpv7K1i#Hx$$0g%U)JbE4i)#6cscn(c=2VY)URKfx}^4J9lCPJH~xdB<=5H(6Ot?ik5^FgwZrtsU$)u zh0JNT%**pGryYc9oPM0sMrT2;@H*2sFJuiH#)t6|bCWjs%Q^_ao3$BYa8&5m4;VB# znb3wFZwp%ufB#_oo{>FWydy$Y0V$S?e$cM|%I-a}9Z3k$#!y-i8UURm6#+@?N2&xy z1TbL+HtCmpdo0{09ky@0sr0hDJox*;&ZmWhe{^&dEFVU))&xjj{Z6Gj2KqF^{O#`w zd}OM2GG%}GZ{8>pewx4>FU&N`&Kx;DEAgp**zR?@+umGFsPtoDS5FC^g_ALY@`;;X zpynb?u`VuXO=?r{_NJ^$Tm`xISUHkBVe zevpYjoiGp>#J880a;pr{!{}vK5wyC;_FYU#Gli4$I3Kv z>5x~;D3!VPL(SaEk8iAco5bYWgI}m>tp~%@wOIMEpMJgUsxW{;#=X!l)0>}|tQXG4 zHy~nXVTp^f#kjbo0?C%5(m-TVPsKnhSeAVoXbxRHuqgiSr5`X74$)!*WSGWZ0OV-6 zxG^?t;nvk_!Mw>jqG8_u@!qF=_gma6>QU)NyBXq_utomm^f-&b#H*KyJUA2{eC6>z zI_=d9wae`@ONq@Iz98Co5)~{qRFgdX9SY|iiKLa-Dc+x3qxMA;p|Ik@@<80)Pcq5x zoYwTh@ix50m-^K06zQCKAJ(jX-nxeRtA%6nI8c0t6k z6y?~C;<_;es1}jww+r6d&rikDN?qt-^?_Qy9jBwM@m&r2Vr0La3b4%}!AQ?=anGOJ z12-Cm#^yBEl74;eGaqV2Ya_F^j*Wb2P*Zc>jDEL32Fh&eyT+TFY~L5{!<2l3jdb*T zfZT2Qw}q*cCgWpPh8eGn>H>ny_)1p4tiWskUKG`TY{n~^0KwO?-EwG?;N=Y#SxrqSZv%GN3h&eTKO}}@fPwU8*}7zZHbYDdD~c$<+)ipjvvOWP{^}V z)z{u_GtLyq+5r)()!JcP#dDI&=h4f}w6K}YgLvh~7?`U%I-jev?_;L)?+3YDZ2Bra zL^H~lmwY4gO@=f9ZlDDUnZM_-aiRT>{&zopUYc2XgE)A?O@}400hv#450mT0iXL@; zClZ@;LOD30E$w>*$1Jsg_7i6^aNvf&iY6?%>VyRBsW<8GGIxBx8$^}pK53w$_jrKD zSG#l<=dNr%AsgBB`oX1tol%KI>ea>51G}RxzZ27A!6&+E6VH&st4SA%E@+HNhWLmw z&5}Gf%V3EHeeh$3%NqqaeHUS6e#{a zcqxAJ0K`sxZ>A$Ebp|ThRa#^L%j#XN}l%|;B_^)K+yfjtg_Oi z`M0zDXx_{hBLt<~-^w5CZe;cRPkJWn^`g4kc4@po#(a9OQPK|gt&A&v^hM*tfGrhYH~VpfZ#as>$3+;U%DaBFu76(T{1!?(V_+|Cy|dQJlD z)t#!uEwkUjd2(c$Xk@=HM{I2`L{I%V=4EsahK8@_N*P+B1^090{tB=>%3@=IlV0rH zHbxuhjccadYMrW9N49Wxv+rj9%HqPhx^Xi%WS%k|D3VPwRAASW_&E0U?m(i|TJ3W6 zAoNY5mAi2_UsW=gCw_lP3KRPwc>8q9ervbg?J;X0VIB?G`WVWXOzEz#fK&;a!TT|R z+a{mW5&BA;`6R+3rq_9h3gi4^e7KCm{kq3-o+IyzR{9)W%(Xxn_vky+yL~!-Ugg{S z&&xN8y2|L0R#%r3RzfGnO&5FTZE7Vm2BieQ2P&J_n9Z|pUr(a_szsS~Mhylz(=tWx zoUm^C5q_y&<#1bc+OJnXuzi{O|KE%hsDG~*|H~;I-`$4r8nrwWyr2~J?=Euw10feiDDVPF6evY#vr|ZHmp%Ze|k2I+Rr73q-G-l3ZNZ+vM}#Unm7)l1zy} zEPOr9^@uM+_a&}2Pwcu)F1%`t*hx`aDU~kHcQBbyqZA-f3V6YqPQy#r;3A4IR&o7L zIR6*5zLx1$Gamq46&<2L+lse01x4poSvozx&h%)BhAA;HfX23{Y%TU;+d-}F>G*4C z61OoAx*Z$n7lONUR-MdxWtZWghRAqrp)8eXLYnqVOi@j%jbQVdkWhagcCrT4ZI*{6 z7H#*yU2>waIa)KkBS0>=o*J41Ku`D+VMQLuNigWYV3qm$UrgBAL&aPce7dcs;kA(w zhyT>lhv`U}_J!aoEkOSIJ2$y(Fy%|FE`2P{l|U}wzV;P#^TLS63BRM za}Y^u@(k(|N2T29cX*HYInJ(L#e+wqk9rj5hvq`oQg3iA^GEg?JTqg;4Ma_hjR}f2 z1B81by2+gm?t2$f8~9_nLCtF?v=O;S1$z&WnP+{w!yoSxl7@7zYNY*^e0a!JU^%)! z59rc>UYI|1Oy#>Mm;4v%M0J0`4u&vSstVQG?%xX9wXa{V2s;l)hq+UWj@?G0A}^__ z->Yn#`SU3S`Tue0t69kiPbV?F^r|-$!`PgUBIrVU3B?(35=i-Kp_6 zl(Jyl8(uK$-3!Q#wzT;k8^8z33oiF55e2U3${Sw80kcc=N8*7uWE2Cu&;)$~U2+(s z4hXo2uE|~I>>ID?DqD^^W)w60mjg-o=igS;dGaLMVv@f6C$H@2d;hxWbi`j?4jrAE zjZu%L|6wpb2==+~Z|;FIMlIdLB5#ToU+i9o%#+rXcY>xNEJ;aKxP@J!uStnp^p%y8 z25+pxIT5%J$UWnD*B$9g-Ytfmr`gFF|E<;?w(Q}L)7<~m)%{Lw4>G*Zt}9c$uTt*) z;761ZRA#h(Ibl1Q?>_p^P#hiIU*_i5PhjC{O+Hp>fYpM~GYVKQ##j=iv-9a$PeF@= zB3KvMGnysa+FQFBT|AsGdG&Zb9>lGhnU9DQ$2~^8N*X2JYH40LB#GR|8&HI`*$1_c zng4QR7O_P@;=PNsKUMkel3&rkpjC8!xeImFQ6F*qQRSUw7;uJA`Y)<8F#gBeAyIsF9W9UhN!!J{6NPw1a`k2D|StMZ0*G-a#!)ZTDO!#ju42`=eIwz_=var67gN^i1`;c10vPu-1l`6C(@1288*sXE*UQWV5;00f0$Pr4LS<>tCxX%U^$>iRF<2 zv1q?J0}^bJ8mI~~Z(i*kcP$9``3~d#N&#;SyOGmz^}IR|0)MwdpUcMMyrpglnapFI z!D(#n{1Nnwz{z!T2^L|WG1nW*6F0Z{!pkyYr6A1}Ux3VD<@*nDv(Ug5nD!BOK53em z11yiP2841%O+=ZqJ_14hQ*nTtpZ#wKWJ5iWVr>^QK3=DZdR%zv` zBmX`XU7nlr#owJO2P`^JR%vadLAjR4+*1gAN-d8*rP;W;Z@joe`e0rO#)A9v`PK#O zY~pd~AMBuV=oV){gZ!88>fe3msdPyWKyMV$;VzjEFAbBwZ@}XXnJ81l<$U@N)6!(1 z5y%fUYlUO#B)$_`8-eXRx5W%!xq}J!X4|kd#t9FkR4_T-%`eED5`mg_N-7{1>l^=W zSlV3Rh9z@zcfXl3I$g(zk=dQ7On064YQgDdj#>?K?$LlIR@cXd^>!vH|6{q7AZDSBiXgr|mQpp@g@U#!LQ>+6GS%-J1%38L zI4tp~t8jLB>q?tph5X9(_RoV(!juZ-ht~NvjK&I;J5ek|vLNo+IigpU;XKdP{`~irynq0C(;Y9Jhq|8UM(GBv?=VcR1wt9VE;K27P3Y&eT2EOb3TSj9 zn^FBD@T#;iXhlU3EXq#xeQs#;CGj8Rx$)y1G}fNGYCv;cA0>Afi)m*x@Ze>oNt-(Vtd+-p=L-;wz^ zJ{gY<4XeFlXimEDFe}MXU#owYdYeBBObWg%_JkWHUd zxozdW-Q`L*$2Bm7zgS=Yw@ZG2o_C}2+}!FcEJ|7rhs0U&?-d#oAtg3(^EIh17aq9p z?i3fn$Imkf@tx&_ws6J*8LpCIeCaXQ;rdAKsmJ?i@SMXz|frr(?-FcT;aYn%H zAc4qP!J6nL!TegHx6od)eUG~b&2I@ZXVXeI+GBS33>W< z{6RWp)d7N;U!?O%;CDMsJdmVyz$7?Aa-q&k1%UPKzuJMmk6we*)@VQ`F0#*m#OZe$mv)3~ zz0B4s>5K0Wz(Mb~??ea9@PR!LLXaZ+Whk;99d2>WN(!z{kqcpmYPk_$Ymz4lZ|$LbOy?mIlQxv^4) zl*KzJekiEeG)@l^M&I`~Hgphvne(B0XqHX4aYMNZii**6=;haU6|F&Hzn(Kgn}Sy~4heN+E>DSeP9 zBsA0QPeB;V^NHdCLJ7ZclA3=yH6K$gOHJWZ$nl)n-O$&cBRvI0ocVPL@Yvd`pG;hUIrw4yVtAEK|2o!)WOZLCfA#&Lt zb8PEz%(^{w*`>_tH5me5g)m>7*>ztg`GO4^?Os&t0^bNl)OKxDS81r7lbz9LF<;B; zB3WvPF)fvMY25Ko&1agDj&b^A-nZRU6(sg53Z_;xBjhmF%u#0*bLvG5dfdwxCeMSi z>rDmEl@SN^T95dHB()BsDGQ1QE;Zlnng&u9i%Z^?#-frYnfIAxWWjJ(KX{eof2k=N6c~NtZgqWiS|Jj?AsGI!qN;Nr6hIT`OHWys)E6} z2Y<4ei~JC|Egz-^BP`usu zVT%V2Kq%`meaF@4BEQXn#bkGVf`5o;_5NS^#t;8jG}+rLh8O{+C4T*baWWK<880PZ z00BxcRI@qIOlE%)qE$h6|#1 zxiICv_p`HXlv z;0k0I`~ zAWc^`w16fIdfs(BJ&HSA3`+)&=gkr;bAvDH;-8m*rsV_T6CCD(u!5Ckbyj{f6MqgWhDg120g4p=7a+=FVu9FO z^9&kJr^6VbuNs443Dmcl{dtqe=0+b5)-Uw%<|EUBa0@Ce&%3`dXD8tOc^${gcwj;( zPt+|qDOHUfFW@w+w{#otlOIJ=NLyV@l8xbl2V(o*zjD$8x)MJ=Y_BZsjQCq#*RhOv z&h3R1zNQyTHbxQ0?q&wYNEBDwmyRf~H=%+c0XxJx{_iY&urYEdK(Y5{fPk4DhiWoWt(|)J9i0_bkdP2BERS}YXjXX;ZxT7ICOzq2mD%0R8XXzC zU2`+?#`5AtI=kpKjft8bK~0hnOXwcRTmg`!tLd)paZ4&kmUhdN;K5IkCUty91G3mTOEG2RN)zT zN8U6Qf!3EezV^BJe;bkWr-+HmA;)JCWR9Iw&#HC1{-{d@WEf0#UP7vE7>@r~!R{yw&dM zJe0m6f5c4!P5tg<2YiIcv z)%vyZp}SkUK|o5nTaZS&a{y_iYv?X%r38`g2I-RS?h>TChIqE;oZo-&@PgNKfA*d= z>t5@+zE{=7yjbVO=?Vz)%Xiz%cM=UzAR3W`P<%XxD21N$E!CK#RN ziyBr=qJ&*W(l{Ird+Q-oj@wtW>ZXImV1>HD3bAj$zAwP6#=(m-NW6hNjrmBu{prF4 zZ}&iauomfhM6dSc3z=5Dg@nhP#3gwvxw>fh)2fc2W{$=XEBmEeJg^eFAQ}#HDg(HLU^V~iyUx$ebRSH{ytu%z#iGg7j zGmwn6JVZz#2EmaYO`Wk^7h%Xuf$@NPj7QfUuaP%GLzW)ig`Gjcar1|RgW$d_aFeZL1j<`L0Tn;Qk%ho1J|hf=+@!SAn0YP}E}PP% z=b^CLep^rEiJd7`)z0Qr^e{%-zE|RL>hC_+z{r9oD0Iwn-mm&2DJ9P~&64Qtx2gR| zf3i7R%g1B|%N4i7ap#_S(eRV(Rlv<|g5m+e!!qH9VNRsYwa>8i8O@tYA{2~qvq}#P zajU2dZiqV$6tXF{u@YZhH$zufgjj(rKqf9xxt5#7&}G)p9-Yn_Iy~_d5-}6~Fz@La zv)D!JHonVtue`b^&4sit!MX^zskR5*BST+i63_RhD2q!zx6zII&{fv1^Y9H3Hhvt0 z4LDRDJRO61i4pBf*I4@l=G|cIT(5g~{V8G{yWoo@UI$AeX#ZRjD?i{C^Ufc_WG9Ws zpv1U4ndE<0lSba|^=GQ!xeFNt|Ici|DEWUA*TasPdfGC@j)cZheG%_1yBwYp4DpzZ zP)VH9Q{zZS|6`4X7E83Dl&Dhpg>w)GDIE!ELNAhAOsi0_R3zH|m)of7{HDO*gG>2F(#`8_|sM<4iw zc^t!Xk0aST)|I~%c0H=VC^dhDFCwszYed=|QtUeEya;tVLSg)-Mef2Pj~OyBIy!`7TVU@Q|S(z*vCu2cf2}j{MQA`X7xhb_g9NQns4Q` zoaa#zyo#3vMGYn`s9QJI;*Mo*P_HryaZ6MbN)z>kov9mB;D>J-l9gI1g>GHaLDx3S z(I|bYx-SoUeva=sL)B7I3UZT!)Ul}WXD&gKs&BNAgOghOHWx;zutG(AweqlJ1*JK5+Y++YcGUm>f+Zz75|8}8ijLI*_&lwgWdU8 zjuoMwjNd`fHCDP5a2l{4*yM(1onClZKsAE+uS+#BVb_8;r8OR(9Mkk<}_Es=N`ksk3v3#yX*7l zh@SQDn-s`g(3x5mcV-w)mqMlyagc%?o`W4fnFNTaO!-F>Q1GPhf#rT3TGf z*w|QZX=y2ttGw>5f7>tD-g+`AW^V07mFK^~LfURrYPz%%>bbpK**iHv5q&U`Q1>N* zC!Gtfs-eC>;xiaCDvtnr??%?Xw1$~ag4;WrPNgTVb4}l`rv;_VCuYQOvX^b+Ez9w1 zr{brhO@ZS#RFQ(Xd+G$g#u?Tn+$Eqoc$WEpY)(#D!0NMI@OB4|iMIm_i z{;Bl;>2_2%&Fiqj?>VcfeJ+dzOel6-s6ss4+;{|q#?RS19&1ixb9^p_3rEE6waxpL zF1Cv(P)!&Clhs0vJAuR9P{4MY*gYpP5lf2m*a$?nD3`1Fn2;(}pDL0yRR*lDChRp- z5L`by5?h~677?2=iu;z6tdEWbd*oYGvwoUwX@ZPXB!L)m!mhtkTfHvdL%3V^3y^sM zE;$mQewt*t*id7N+7rr`bIMzQ2K0qY@eJug`0qOAb5cb`$|R7m-u9PUnc?cchg9+N z@wZy05!Xa*vvQkcAg3~*q<$s!)N~)d-gz^6c(%mXhPKTZBiE*dK*dT5;{>1i%Q7qj zRf_>}9|ZRO&}~^=g;i^`8r^XW1-W|PNRyo! z_6(0R(Dq&{k5w*(mfA+s)fuN6CJ4LymLDdN{H@GA`3u9#V;;@alNrI`XND@ytgs-M~gGoEMm z63gb9lvf@Gt}GK(B$FQDaj#NfLAqg`x4yuna`U-WyF0&T+6j@OE)ndG?rtgiD3S2b zggGtuuciuE;$5LobMkjw4c!`@58o(2OQ8+I)OOUe$iV(pcw;T4(PWxiWpmc?AH~e4 zUbP4&RAA8*0o+eso5nt9lkd@qEU>UJDJ?UCB+An;5&9b>P4MAO6LlwwYs!jz7O&;2 zX54?+$*S9^j#gJTx;*S-C?^bc^iaqB>8TMx5!R+RMHlgiMf}{<-5z`BQnft7{SyhL zZ^&B%e8pCbb$cVELwfH8r zyoCdmpE+NLzCSR&dn9;LArFgjjiW+7o571W9d2O+s$0Z%Iy4lwxbInOZn?G;*z}$p zk5V8H8vcD-a8oKODk9!TJA-d`_y`#&U;4K~Xv2g%q;*!1VK}Q&h6wD|ukhR5Mj5NN zeE5J|#BU$oNEyCIp3MnS?{vXSCxM4(p>70q5XaO$_X<3LJ?+EWJ*+o&oQWF)fBfb? zN&9|guea)q$6t%``GZT4%UJGZ?Vmqy-O4FGRldO)KK>Th_5gzw`z2GC)O)P73?^Juu9q3t^J78VrBAp2?lN zM5+Nl6+Ii0(i{*l6(qv~8yjHz8&2*I3xKmdHjB*Tq!x+*DSR|dBP7}xq_`DAViNl& zxeobuX~b@rw>l%6))DE1UUCPw%g=y_xZ@_I8*+ludVIG2!@&kJ`LDN6BALrGB?C61V-<71vSoYB^ zxZJg(pzQ09Y`B%TJoj5J4VN#-9D~!do#r|QLkF_Okam&Wa zFzQZ7@$fxnxkqK!`pl6pQVW|>r!k9{H2aUN$qs=aNtk%C2W)qTRYBQ0Gj(_Opphw@pCQrqXHD`#?FYx=kkH)FFE~^ z2_uSLI#%NG2F$mah;-83R~aCr?6ar~xV>U!M?oTckW4@=*OhI}v?CBPq2p8QmPhFedYrEH4ekFEh zf2g}yEzDbl+3b%$Y`~}_bBw2d{xl(}@Fl=iSQAnhw-}Y|7IV3y3pdt~2R1anrJi5i zNZr+XdJ~<%`n4>7a2glkhGV9~CA>HTRKrw&9~>;zbUUUm@rhTd|7j{c0e@Naynx;B zQx$-sM#3&|{Io&u0jR&;zlrIM>35wcNmfP4zAYzJgRzrs02*T2lw0s3s45ro%5x+3$wpE7n z-zq^z=w!}ka|s`EI_<=>Ecec;$xwi-R+NQ*7$PwUi}p5a?}&LW)B;ffU?~>>EcHlL z@$%RaEQjGFsk7k|lJaBKrzhS)K&d1a3WZw$50U^4-(VjOiEr;;f}x@3%_J$v*B6*T z94fHor~M^~S_(B*IKua(La-BZKOllRPaaoQ&d=IBHhxAEZT{3Y3Q{XC2l6xP#us?F zWgy(ttfLC9%MWYw&Z^AVyx2p5da=-(I!Ql0%!k2uel`s(9rX@kd~^KNH(s#HWzEPB z(pz3fB&ez9j^?qRmUH+r(~8zn5OEc`8Ml&>$lnYz!TMiMeCT~oUqJ#}%Up$9A(qaE zkyIpQkRO}Pk(f?f@NXBsqG{SeG&=I7=~#Wo$_=$`up5=@hMl*Y9L1e@YiM<5IvSqq z(a^VE5kMu7^Z5JQJl3KfCMbaDwzH-#)ZJ?Tdj1kLknYT+1aP@o+0mqQ`lk^dq}L#1 z)5CWi17_2PyTCH-PAFb?&|`^cwdbebv+R=EJAI}~0}N{m2PjGf*x+Nwc>=hxkDGTV|| zzb0Mto}Qse`2{G{j#r= zUKD_Lm}k`iQ628;q5rSpsdtmCj_h(k; zX(F`gY>Ln9_mRMcuxy)AxDF^Z9l~~unlM+Pj!8D$5=oN_dJ}m?5~zI<(wc)1LYRIq z+RF)2&W54-ldm0xp`O|{dd4W{Z7KhHKEnAM6}@z^yso_~Mb_6;=fx96c^jIr=&=yG zR$si4ny#fa7o(209=zN4!Tz6HRQmB3VEizPgl?6qP)dBa+AF3Kd=<7p?f3X3T;y`m zWpq|#jnBW+7HWMMhjvl(m}2DOv)(a|i98Ivc+G+-vV|+APY({fm3?0n{<-J7ES9qB z*3j)_;z|AjLB6itev2<>Z-_IVT58YF_gL0bCw&$1B$?=fjf<}UB0e$nLd*8tG0Jp$ zK;YlY>*%@4;Km83es$fF86YiDa6R7q>sb~VW?NCC-(?VHzC<&xaGgFG)K(5s*X@*8Th6IbAiaiN8|1FQS_+tXiPbH;4FTWRDte!saCm)kiJndFo2Yjt@9oCapBlD8z zLIU&~CTbx--cy5flJk;4+Q!kPacJ>$AvG!~K&sH{Q`9#*Nq$4Z*m(@IT))BfD#OpW zQUf!5kULk5>ws#G#EigQdHD#kn#pi|Xr>ed;tqOQ!P=ThTzlodr~8iZtdJ+r0={Ey zsO|<&%b)NJtb6S4l+(w7ZLz-2M4`2+)pIP29k*Lacn{-D2%S8K0YHcqPrgK2>m9hj zlsT`eOK*9pdZTW>EJG9;!cF0S7q^P!vzf#CZ-KPz%`<={2Vz9j!GY{(SXR7Pw%B_#Rt{p>Dcw>E7=u~y;J8Kgv$P6a8`U| z5<}ns4ggRQXJQIO^rwNzampir?QYJX02dGe8%^Ce5FGfH;uhrUo;`HG?29y(4egE3 zbw49DgAwcwG97NKoY!${>Y8(SMkg@pW*VA-YmY6JLp@6p*SlX%n88*)C*SDGPfsiu zZ{$zFRfuz5qx=h@P5O=8^XoS9crtX?;=R7>y`VNTkn~y3*GpKCS-+}{P2XvND-Z(g zi3CUk_`xQuQGiy!+jQjnVvg6-A^VY0%O@LgWE71?kzb#=f&LeeM85}v88w;TQ+n0} zbdz^H9PH}n=sslx8Xiuz`e>8E%S3#z2GPT-Er=7#Sm*HLF=lzej0xV-D1PN6ccwFd zd*xez#g{>$MP5r1xbyQEM}@ZT{!)>Xp*}jJSM+0wnz#b)z^vM`bYr>8=aLa@#NJ7q zni{e%qwtpr_Xc6bs&<};#+*O-qsh{}dzCK?3%ZO-NtHmxWZS9H!KCD4s1&<{ve?ZL z36{sCNn%}+3Fv*C&`|!)y97*)yFBR9*Y#D-v}u+jrBb_fmtTN@Yz8;8`6wIWadP?00!nXnFdrI88hwi*Rzz99?-0Tl;H6F7)X6x#^Y5zNZQ!_+lViW;>E& z70W3~DQM|_0H!1y>(dbU?98x3rdb`pQoD}z45E!4iR}jQW#0EVvgyNtSh9PH0~-EW zMeMkUhqx-IvgvNI>o%~$Lnxn}`eY6%UJf;vv%Pj=o=@07V4QF5Sm`jDEUX>Movl14eBg`A2gi`)Kj`-jAP{0*ke^? zjP%oZOS>hmo0oC>Za&NQi6d&Hy}*)U6FP#rb%{>Y`!2a17XwX3S;CY-XKc|>c! z*&Sb50`gBqj5o0UJ0FBz-sX9^!++;;_W4-5_eozC8Iqqw)V@?g6iuhWElrlLWdYQ7gc>@+TSg(D8BVLLO zT(jR`jr*DV4h30L2QL((H`Ba(XOaInQ+w;G);KUSiGWs>=&GdQVwSNs-Izd_sgX4d z1gX!0_Z}|UVVGV%qjg198tF;eu`%;=X{L{JUgi<+g@H}-{Z%m>Cq3R?+#GBOj;jA< zr=QmnQ)vG>Dt!F)NaC-$fnBNPxpq^7niWLdPa^XxVk*M%rhzm5H+%(4-;Gp*1|~Hg zScxE+7jng5rT`vB)z>Q7a+4ikkTG;5!2FOqK2PU0Z5*vEGLyaWMEcE1PTi+0IhwcU zWx{FywkpFC1VXB3ew2^De4;3P$T573PQqLDJ?wSEWr?{IM6eZ#gR1zsVmb%GhCs-i zDCKmairhlcN7+savDK6VJA|%!hUtt=yE*Y3NmRRyH;dbuim}@n78e{xt-)tDq#}dZ zI-+nyJsJ#lvLC~o$%WrT=IpMeK1tKlJ$oZUOUfX1U_=#k3mSOoBTUETmy_ce5|MsAs^Xiu*g= zew9(j&DDY>U(& z79SZ4-n15ydP;^8%lU*;OpxEVQWxUu{tyIgI&DlR-52y-`8msQ!u`{(mZ|4=**t}s z%7|||_M~Ct%GkRiZ8nNMj%%1cs6W$8#M`puypn|!ZEw+>^8+d_1iqK8!oRaX@h)1b z?C>h1Sb*Dn7@y;_bSrw{nB{nSbj0fB<~Gaq>hb44nq0ua(+qqgSKuJPgwYvvbT~|D zZ|HY8YaET}-aZ=d9sFQ|Wkx8IOy~k=aG~x}uIkDVi_|7r_jFaL# z%pMTy71?TrpMZSDY>?xo93ed7)q-AZ-?wIKg~s97mB^H$P{e0z zQXNq{mALe(3Z{1RN9yPYRCTBlKFlcQ0xC@A)04l%!TN|=E6F~%5BGZtvcAoRmX~SD zDLKOzMm+_43%$FOW~&Lu0!w#@e>&0oDIs~|j*rKrReXCO^XuqW#L;T3gq$DBHvmZHD0t%m%S0nPo?Vv7`K?vMLISjb>V^t{+srN%E?iXAw@=*f< z=4kIbE@9mmDP_1Fr+JHwlZJfS$bRIsIL*Q`$?}eb&m&+?B#qK*fw{KxV^UsmBlh9q z(-wmYlb*2HyK^A)VjF8M(xn)L4cjjORwJhLy10Md@hW_rw@fp<8I&S;4E$$KZGb89 zHx&hx%QPMEe5*pPcD#ByGMF285SmIqZq^$c~_g)+ntZY~|v@kG=fqPp;7u`)E zYw(lyL;_?L2R-9SFpY94OddvEn5K2RZ`1wFoJ-jte(C*1{xw;UHI)a^A?Gqa=xE;` z`V+sOKd3W&56Sbay|wyr`Tb;@W$e52VF7;gnA-F)u@f>yfvvh1?_0|>KNn!p+yzjC zvM*))WncfJ@M$Ki`J5?wH~rO7q@uy6CkUMFeYYOo!k07A=t>JnQ@O@3fzk;>o%}1| zvPw;kxR02}3idaoXg z7%+GmRPq0Ls0zT1{%co$y`SRg6kz0A^W4^d@F?%dbA&$BdN)+`O^8$KjF@eCC1aNu zwmFmKsX0O(U>Fod(dBYHUtx;H{04e&^EBvFSf)vugp35fX=t-f zJf?8cAaVtnC~6gcz&^Qa)NtKBx0`_({VIh86)q>HqZljh!YosXLiGAt>A18Mf+waq z;x}h1u`ZBscCHUmmIw&~NP#Q}VF&Y3ke^}0r+{gY#tI!v%!$%L_nFO3uk}@d3rezg zgn7U2%!WCG;x}i@B`JHkOuxax7q7v&u`$<2V7GM6!M>UB?>fc6h@-~$-okNs+6K9sd4+99gF&TftnX4y$jX9*bV7vL{mae0OBF@k}?+e=0(yWtn?9FI$CVj>O+zO zpsG%&*aCSzT~ZpPV^<VHEAm0;E$a%N;I6+Teh&-Oo39sB80m&(R0R^$S zu-%9Jn{$mIlJK0U$lCaTL!tHi21-y+hG6pV`%|U=egM0oVsNNHMKvw{PlZSfM~f}L zqu359pgN~x7;JE@ytzKmAL+Hky=chS!fn4HRo)rsm@%6OGIc8HQ37a$TSeQ-U77dc z=H1^1fdD{c{#UVx>EBN@&(fVq_jv~~NUJdJN{_$$P_+;l*e0vbR%O46i|O0A=D)c0 zAa{}=72x-&ll&cpAC5Pg?!G*cE%Sn^_$xc(mzK5T!^t*#wwtT# zkdR<+|J;jKT*;{+(6w(21s_W79(kxhIA!SH(3qRddu#GpSOpQoRDIMyois?H>in3X z_32kn7`GJ;cs+oSgoIwgsE#W{c{7?6y|+-y!yNPN>G)1sGKp_Y<}hYs+v%|x z9DQFo4j0ynUE^QIEYsR9gT=o@dQ<;nL;%F`ujR+cK#-iL^&t%p2R&D=(rbSXvXwTI z<%fZ4*I~i4Cd__f{ypk%*{84J$ei(M;>tPwAw%AqJ3ELqJI|Z@v6>&+hA7CJpKxU*eP-OnTWMaWe$c?}+ zE`r5ozs=z~=Bt3fOTTr_?{-?BzS85T8Mn0Qc1M)x_e;=R+43T7=_ zQ&MFDL@b9ny3zUpmuSMi<2kCNM>549V385PF|LGteP)aCPlWjWBTM9ybB^qW53yKq zo!>+|0>1xOgrbdlx1?CAtP^#EpU~Ft?T5owo$ zNiw+znxyQ7z6LrcsZ&NauWa~R3)s96nglc0GQpaOS-LwLSR9w39QS!J7ZOTXV0tY2 zt^WrYj+*r1-Es z9x|Hjy1ZT?Tb9#GYrs)L~!aU6x(1kQwBH@fZs0e!LRtbNYz)+;Z^(q>D|TiJ-DGF>QdI@m(raZ=&)G zPd59ihuhSWDm!it*jJE1z~ng_mDR8Mzk<}iVjqE;9!2gtC!1!ekdf@fn!?^Ah$>!hiC-TfYgm7{B?HjLf?uNS$b% zRzl6A(bXIeYnRkGfjOnoAgm1;h2ew!3=|M&0?_5#mFkE3_;q;I1r=MSfKitSFnSPZ z=C=Cdp7M&{z;V;wc*j88=tl{Z4J3qf>CC_Z(<1_fwT0r#NnWq#7Q^7a_m$6&0hO?j zF@Ry;szvdz_?R90@2ma!eb~ED2&_rN_-$UaV1M@cK?nP>@5OWNki^f3Dp_YUpW%XF zKOxS#LZsPe2OvhSXg*9b(Pr+J>H4ViEv!HfRTNEcOuj0s`N-WMzuogB|5o}ohu@q5 zFquXxP{-N8t_WqC3Up-fA?KCj~aV%)5;}R?NSAHMbS? zs8sp*)P6Ft+^^;(Qp4|^%UDlAsQ5ZG;2+!_d^D*P=vLtJs(|Qb;MH^* z6EKen?h|Ih7wMRa`$bt>&E>E}BuOaje>~s*=mXMOo7{9`H6Amv&Unl&|KIbg#pfKw z)~UB80M>`FAbT1A+xZR$p^LNVLCWVU*!ZViWp(tMN4{5n_G9lMR-10$^uhA&btP-$GO!`Wc=<_7M ztLt=qk)c6-Z9@QKNX%uQWJ0!F4HiBf$nrV$&{+{O7jmb_l}!5mNarZqe&hL*e-GO3 zIu<@yy!x(SDqk5M4-r`jS$xeb+@#Q5+>Q*zPVabt{q*iAz_L)!n}&Glop^F;g1W?O zo)V7z>lR?Sr=xG!)xFDA;j7v|HOuv-9u?7uXlZIf5J&qZ7&gGg4KbEJmYv2zeXq z=#%x(ZXn3X9Q@{>9u;;ynEE26CV5IP2xJ~K388)$Tm1;n;4T({bW7gd)gyNv`4ms} zb(AVgo)%paaAcWxTRTXQ^);doFB0x}C?GocJ}HZZ9;?JRoblOjV^!{G>Pfvuppnd9 z6Yp5=nW{%H@b8Gx$`4EFW3m3AJ>kGJc?fLpbL$a`^Y+9cr&#E}kPDK3DJe>}HY*!{ zU&pX`V6~+5+ulu6p51zh&(6syjQ{BNlC|~sP|x8Jd4kWI3O{vNOLliHIsN|N+Mm3= zJ19Oq$)O<18EfWRKWzw@&@&!;H{Dn|Pc(U0C$C_WXHuAwpcd~Bq=W~9zHz==V2+7Z zF|{3+N*D3O4;^F{_T9kdS|qERd$)iBf}r$R+c^_Ee0|;I#b@%~Z_}Qp@1{#y|KmN= zGqip#L0u9R1v#+G1SE#MaShdJav`a=MVPH8NZxyi(zC=UmqqBxMZ%69drZ+vP=^Ca z;=PJ$Pv#6+@swC77QWEHD!1}De$Gl-bbYwr%W32H&2hGT5pmW$eSu>H#ckZ5H( zw)Y}~B7OjcH`fih($bBFw<$+9KFU*ARm?*YKmnyiX7gYpfoqxN|i8I{6x>S*^lPE8$6kE(Kb$%Z(TeWq&F3fl%CIBU`=ge}AsvPt2D5aGxm3<8c+Bwd^ya5O7SO34&T7_?x_uI| zfBW%cQKj|$dm>? zx#MH5wVxvu)!V*nBn927BwdS(R;qIRrR<{5AIbw09VvaU&W%qN1NM)%jXhnC!^V}? zOHP99|0x?MW$ z^;R~2no(oNv&Ye%%j#39)dibGm`HNJrX%y8Pc?1NC-60B7BlFJN5&?>%c7D-fSB>0 z^81h0*ca3x6x)45qISX4*xVUmWgU=lDVuOsZ%(V5Lr#H~?zLYOlztV`Z@i08& zeF+G|1N9TPhOd+bJ(YWC|2U|ZY1Vm;=4|I{tUFsvEZyxuKv{muD<8!8`9>Uu=}Lkj zgbGdyF?$}1YKJKu4ptqR`n$S*t z>>xZVW3)q~hlKT^f~{}7Enlmc(8U7Ysn|^~xZpJWx88kxS0o-|3E!hSFih4g{;=d; zjMwU#3F7Eq5CLM;Tpu3qFMnI%9krit-C~-lvUncrMNEv?zVFz-{b~WLV$HmYM3FAK zalM3$=oj(~>W4@6rCfCoLfz=!XlM(_=Ny2o_$t}$o?92zU_iHm-;#AAR(E`b7l~ia zBiPN@CT?|xS8mvJPXc5BZlj6$Dy)23e!luMO$Ztp9Yr8J6BGXDqYQLbtzv@|3O?%@ z84-5M{Cqg*v(JwXACkVIH1e^pm@n%-Y`}TQMi(AW$PG^_KH_UF5991lV;P>`xCTU? zk__#8;3gp|(|1%^2}qIrT@W0j`+Dyd)9mz=vT1jC7p_^)fQ`LNDF9DE)luyLl| u-h!+t3i?69NPhjq<}sZAFaP|cpD~I%^50bBAH#rvKY1Bt=?Y1cp#KATA?K9< literal 0 HcmV?d00001 diff --git a/assets/ui/tool_poke.png b/assets/ui/tool_poke.png index a63c6b0a383b67207ee203a713ac202720386371..ce3daaed576d7b3e3bf8a53cdca9a562f397b899 100644 GIT binary patch delta 2381 zcmV-T39|Nr2GWz{HlgLgEwl34B_GGY6#HLZTcjA~urwF&oPeHj;fX1aYu~Vn8T|SX>~^ z&U6)r?&|58_vXEMZ`XDtUTNP>&wqbkS5?fj%sT&$AcH> z%$<+`xYauvYeX5xuEPm=uMyAzPSoL`bvmKbya@oRwlj-}_X+_m;A9$~8Xy8C0>(t4 zk%NHTb_Nh}*9jo&12uRlGzdgSOxQ<<25tW0zes?p0;)L?n%~lT|J{=YOaK~&8AJg% z5v4X$TL&5nB7XxBt*C}(Jf?_5B-79~3B{cRoJ%&G5p)imhzT4IFcFx;#oVGn6Cx1+ zst{EYRaH@l29wYTTmgcTDk;cFxGMxS;6bL|8EEFvcB#!^0lKdTvW6%j~K0SGC) zXwV1&i8%&RKnaP2J4!&;-l+i}Mwy72iJ6EUQzQV6nSYpJ+7L9Rg{W@PI-<3RO`MV6Fk6Jtu&2zcSgwP#pG107c+nCUVT=922|P=4P7+uy<%qfT{|q z7gZL4Cjxnaj~5aon1n!)+reHC(CB1OqdS*pDo;_(or_?d3vEYKI41Y)udKc~?d30= zzx457?|&*@)O%4cq%}ktkifBs2L&x4K%x-@9(E0Ua?nC;RcKwG{2dca<(=aIIBeM+ ze|+`t^K|CLU;lo8B%t=9z80yyXytus*COh@s#jG}4cfC10l6(_z^&1mu=07kae71? z?3*}@wsVdP$5cG|#KC`d-}l2G{W!I+|M_oY{eStu`z5s>J$m8+FIv|kRqeH|y;QYm zU3;lTwDziAM169W3xUspn@Trx8f80idQS|QGh@ydD4de;X*jeeP#}3Gb!CF&9kHnUc*W$&t?d%A5#3 zcVsb030qE4Tgzi-reOgOM-$-O(RD}NfWLn6m6bm5x#XJg(`TQ>^XD&ptnymbUUsTl zcFZiiNbOa<3S_7Tq`)PNIw%2%b9S`hvVU+~mgB!s>3CEY@#lC{Ix5S8M}^~Z(hCjz zyz;>he*^&Vz2nCbv=JPfZ4*qH9|8e`2#Aq~4Q4ZfN#%p87mkb4aakDP0D50pxY4L^ zqfx=5Q6j?aD;U5%Ar6zwxEdz8-=sn{kuBN^WT;0l7bKsc&*%Tz?eN zrX%>WbTldp9*qj#7eVhU?N{)=QA5z!=uZN_|B==0t6LYQ_Veo21?+!hbsGRa^w6Pg zAZS4fxSHuV4rO3rwgzOXPex~&yV*Q=Svo3A%}0f6Mmik}$IHUm@$&0m{LG(#aoe8; za-QP=@Xf=Y`)lGL`JzUqMuOC6KYwLRGE?;UE~-9r`JKmCZwx}QpxEeM-MX;ScWx1x z84BBXdK0`fA1jWjIPuj({~Ge5^|iIl?y(onoqKHF4(|r+BJ$b9vB0=4j3tO6R{h~Q$qb3&;&d&v@>@aG~wje4!s3Tw}0PjS1xZ( z0&Zd&unwf_Zr!}GvYT1Jo{{fsObXdA=8K>h_BBWZ&<~Ctey4k4sPmdvc0SY8 ze&afT5DWomhFnuSWr`+7f_W@&Nr|VA9=Y|@sZ#)eXEruk*H0p%JNjk{x!nKYfyd6C zIoQ&G4*+=^0H%SM<^!%> zxxBf)w$|$m=h1{o;DM9??X3$8i3DeUFLVh30$|gEK0jdR*4NfHjl{0px_M)zYtwz_ z4ZO*Vzxl%_EG24b@-87WeZZ+>M|LIw?8;N8PT}mC)3?0Z+~pxGG@&-QwaqI zehVxjvu1!??7FjOPNPo~`sD35u1^Gi?!v2|lHly5asMz9Qhi`10UOVqFS}0fc1W%{ zLrLfYH}EfQ{qcaPimHlO`V09y*p!nNng#mgnT?HIo4_RSeb1b9?dHYphbu2t<)x}> zHSZsC0$!?^F7u&?qJRA=zRvsWGK4S_yW`CTc^5np;!P_M-ZEZhfQJoj@F$G>32|nZmgJ!f+;z1i`5|Hxv zA@iA~vH6bX1G@5y-@W>230_@9RJ>YIM7)pSYth>Kcq|eSW`*x?ZHNeX0=EK59p!`YP#O z9a0^R7kvV{OyOLa2{KQqLsEPy3X%s2KE zx|4--R{xI<&8D~H6I4aRngOwTV79Ji7y;?6Nf8LpKmrLA1XY3xpsdP5B;8VW4t{?9 zZ+ek0bjp{(r1E&MZ_z+N69qxx z+kw0*Z6;;{UdmFIvXrGPWhqNp%2JlH6ae@i(L#-9P?5>v00000NkvXXu0mjfJIr)F delta 747 zcmVRP7iGMol5QKCV;gw-BdpmRUJTtShKL~`}vb)bS&%8VH z?nR^rPcSZ$i0aLY2ljFEgQGM3os|Z70&Kx;bwT7x{MMblulA2=3M&beDG*|Uig$fx zo4|lp16AWznq_m zT$#Y`{H_D`VnoHa2{%y_LO{PD$-l1*8Fe@K^zr?>%=06ld|TWl{Ne4L zk0O`_k&gbi-{(*CZy&yV|GCMzpa!fPaWy~$w`3bM4NyXjfEnNuchdxsxPE|3yb@wU zQUes*h!EBZS3(TP(h^e&q@fXLNU9k*M_8|ZO002ovPDHLkV1j&tVL<=@ diff --git a/css/styles.css b/css/styles.css index 0b413e8..21f0d8f 100644 --- a/css/styles.css +++ b/css/styles.css @@ -50,16 +50,40 @@ button { margin-bottom: 16px; } +.top-left { + display: flex; + align-items: center; + gap: 12px; + min-width: 0; +} + +.brand { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: nowrap; + min-width: 0; +} + +.room-link { + text-decoration: none; + white-space: nowrap; + background: rgba(255,255,255,0.86); +} + h1 { - margin: 0 0 3px; + margin: 0; font-size: clamp(24px, 3.4vw, 42px); letter-spacing: 0.03em; + white-space: nowrap; } .subtitle { margin: 0; color: var(--muted); - font-size: 14px; + font-size: clamp(11px, 1.1vw, 13px); + line-height: 1.35; + white-space: nowrap; } .top-actions { @@ -115,6 +139,7 @@ h1 { min-height: var(--field-min-height, 620px); display: block; cursor: crosshair; + background: linear-gradient(180deg, #cfe3d3, #d9d0ad); } .field-dialog { @@ -161,6 +186,71 @@ h1 { margin-top: 12px; display: flex; justify-content: flex-end; + gap: 8px; +} +.save-text-dialog-panel { + width: min(760px, calc(100vw - 30px)); +} +.save-text-area { + width: 100%; + min-height: 180px; + margin-top: 10px; + padding: 10px; + border-radius: 12px; + border: 1px solid var(--line); + background: rgba(255,255,255,0.82); + color: var(--ink); + font: 12px/1.45 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + resize: vertical; + word-break: break-all; +} +.save-text-actions { flex-wrap: wrap; } +.archive-reset-dialog-panel { + width: min(420px, 100%); +} +.archive-reset-actions { + justify-content: space-between; + gap: 10px; +} +.archive-reset-actions .btn { + min-width: 132px; +} + + +.ecology-dialog-panel { + width: min(760px, 100%); +} +.ecology-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; +} +.ecology-card { + display: grid; + grid-template-columns: 84px minmax(0, 1fr); + gap: 10px; + align-items: center; + padding: 12px; + border-radius: 14px; + background: linear-gradient(180deg, rgba(255,255,255,0.88), rgba(243,237,225,0.76)); + border: 1px solid rgba(84,68,48,0.12); +} +.ecology-card img { + width: 84px; + height: 84px; + object-fit: contain; + filter: drop-shadow(0 8px 18px rgba(56, 44, 32, 0.10)); +} +.ecology-card strong { + display: block; + margin-bottom: 4px; + font-size: 15px; +} +.ecology-card p { + margin: 0; + color: var(--muted); + font-size: 12px; + line-height: 1.55; } .toast { @@ -258,7 +348,50 @@ h1 { .tool[data-tool="fence_v"] { --tool-icon: url("../assets/ui/tool_fence_v.png"); } .tool[data-tool="fence_h"] { --tool-icon: url("../assets/ui/tool_fence_h.png"); } .tool[data-tool="bed"] { --tool-icon: url("../assets/ui/tool_bed.png"); } -.tool[data-tool="poke"] { --tool-icon: url("../assets/ui/tool_poke.png"); } +.tool[data-tool="ball"] { --tool-icon: none; } +.tool[data-tool="ball"]::before { + content: "⚽"; + background: none; + font-size: 24px; + line-height: 1; + filter: drop-shadow(0 2px 3px rgba(52,40,26,0.12)); +} +.tool[data-tool="ball"]::after { + content: "⚽"; + background: none; + font-size: 48px; + line-height: 1; + opacity: 0.10; + right: -6px; + left: auto; + top: 50%; + bottom: auto; + transform: translateY(-50%); + width: 54px; + height: 54px; +} +.tool[data-tool="poke"] { --tool-icon: none; } +.tool[data-tool="poke"]::before { + content: "👈"; + background: none; + font-size: 25px; + line-height: 1; + filter: drop-shadow(0 2px 3px rgba(52,40,26,0.12)); +} +.tool[data-tool="poke"]::after { + content: "👈"; + background: none; + font-size: 48px; + line-height: 1; + opacity: 0.11; + right: -6px; + left: auto; + top: 50%; + bottom: auto; + transform: translateY(-50%); + width: 54px; + height: 54px; +} .tool[data-tool="delete"] { --tool-icon: url("../assets/ui/tool_delete.png"); } .tool[data-tool="new"] { --tool-icon: url("../assets/ui/tool_new.png"); } .tool.selected { @@ -388,6 +521,7 @@ meter { flex-direction: column; gap: 7px; font-size: 12px; + overscroll-behavior: contain; } .log-entry { padding: 8px 9px; @@ -422,6 +556,8 @@ meter { @media (max-width: 980px) { #app { padding: 10px; } .topbar { align-items: flex-start; flex-direction: column; } + .top-left { align-items: flex-start; flex-direction: column; } + .brand { align-items: center; flex-direction: row; gap: 10px; } .layout { grid-template-columns: 1fr; } .panel { max-height: none; overflow: visible; } #gameCanvas { height: var(--field-mobile-height, 66vh); min-height: var(--field-mobile-min-height, 460px); } @@ -458,15 +594,34 @@ meter { margin-bottom: 6px; } +.lineage-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + margin-bottom: 8px; + flex-wrap: wrap; +} + +.lineage-head h2 { margin-bottom: 0; } + .lineage-main-content { - max-height: 560px; - min-height: 320px; + height: min(78vh, 980px); + min-height: 520px; + max-height: calc(100vh - 120px); overflow: auto; - overscroll-behavior: auto; - padding: 8px 4px 16px; + overscroll-behavior: contain; + padding: 10px 6px 24px; background: rgba(255,250,244,0.48); border-radius: 14px; border: 1px solid rgba(84,68,48,0.10); + scrollbar-gutter: stable both-edges; +} + +.lineage-forest { + display: flex; + flex-direction: column; + gap: 12px; } .lineage-main-content .lineage-tree { @@ -513,70 +668,19 @@ body::before { box-shadow: inset 0 1px 0 rgba(255,255,255,0.55); } -/* Compact family board: parent ┳ parent / child, without unrelated founders. */ -.family-board { padding: 0; overflow: visible; } -.family-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(246px, 1fr)); - gap: 10px 12px; - align-items: start; - padding: 2px 2px 10px; -} -.family-cluster { - position: relative; - min-height: 106px; - padding: 7px 7px 8px; - border-radius: 14px; - background: rgba(255,253,248,0.56); - border: 1px solid rgba(84,68,48,0.09); - contain: layout paint; -} -.family-parents { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 8px; - align-items: center; -} -.family-child-slot { - display: flex; - justify-content: center; - margin-top: 14px; -} +/* Family tree node primitives. The archive deliberately uses these nodes inside an SVG-backed tree, not the old card/grid UI. */ .family-node { - display: grid; - grid-template-columns: 22px minmax(0, 1fr); - align-items: center; - gap: 5px; + position: relative; + display: block; min-width: 0; min-height: 34px; - padding: 4px 6px; + padding: 6px 8px; border-radius: 10px; background: linear-gradient(180deg, rgba(255,253,248,0.96), rgba(248,241,229,0.90)); border: 1px solid rgba(84,68,48,0.14); box-shadow: 0 3px 8px rgba(64,48,32,0.045); -} -.family-child-slot .family-node { width: min(150px, 68%); } -.family-connector { - position: relative; - height: 12px; - margin: 0 42px; -} -.family-connector::before { - content: ""; - position: absolute; - left: 0; - right: 0; - top: 7px; - height: 2px; - background: rgba(107, 88, 61, 0.55); -} -.family-connector span { - position: absolute; - left: calc(50% - 1px); - top: 7px; - width: 2px; - height: 16px; - background: rgba(107, 88, 61, 0.55); + overflow: hidden; + isolation: isolate; } .family-node.child { border-color: rgba(103, 178, 44, 0.38); @@ -584,10 +688,26 @@ body::before { } .family-node[data-family-tarinai-id] { cursor: pointer; transition: transform .12s ease, border-color .12s ease, box-shadow .12s ease; } .family-node[data-family-tarinai-id]:hover { transform: translateY(-1px); border-color: rgba(112,178,66,0.42); box-shadow: 0 5px 12px rgba(64,48,32,0.08); } -.family-node.dead { opacity: 0.68; filter: grayscale(0.35); } +.family-node.dead { opacity: 0.9; filter: grayscale(0.32); } .family-node.missing { opacity: 0.48; border-style: dashed; } -.family-node img { width: 22px; height: 22px; object-fit: contain; } -.family-copy { min-width: 0; line-height: 1.12; } +.family-portrait { + position: absolute; + right: -6px; + bottom: -8px; + width: 64px; + height: 64px; + z-index: 0; + pointer-events: none; +} +.family-node.alive .family-portrait { opacity: 0.80; } +.family-node.dead .family-portrait { opacity: 0.20; } +.family-portrait img { + width: 100%; + height: 100%; + object-fit: contain; + filter: saturate(0.95); +} +.family-copy { position: relative; z-index: 1; min-width: 0; line-height: 1.12; max-width: calc(100% - 8px); } .family-copy strong, .family-copy span { display: block; overflow: hidden; @@ -595,21 +715,15 @@ body::before { white-space: nowrap; } .family-copy .death-reason { - white-space: normal; - overflow: visible; - text-overflow: clip; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; line-height: 1.12; margin-top: 1px; color: rgba(126, 80, 64, 0.92); } .family-copy strong { font-size: 11px; } .family-copy span { color: var(--muted); font-size: 8.5px; margin-top: 1px; } -@media (max-width: 760px) { - .family-grid { grid-template-columns: 1fr; } - .family-cluster { min-height: 102px; } -} - - .colony-card { overflow: visible; min-height: 0; flex: 0 0 auto; } @@ -678,193 +792,134 @@ body::before { font-size: 13px; color: rgba(64,52,38,0.86); } -.lineage-generation { - display: grid; - grid-template-columns: 62px 1fr; - gap: 8px; - align-items: start; - padding: 8px 0; - border-top: 1px dashed rgba(84,68,48,0.13); +/* Family archive: actual top-down tree diagram. */ +.lineage-family-tree { + overflow-x: auto; + overflow-y: visible; + padding: 10px 10px 22px; + -webkit-overflow-scrolling: touch; + overscroll-behavior-x: contain; } -.lineage-generation:first-of-type { border-top: 0; } -.lineage-generation-label { +.lineage-family-tree h3 { position: sticky; left: 0; + z-index: 3; + width: max-content; + padding-right: 12px; + background: rgba(255,253,248,0.82); + border-radius: 8px; +} +.lineage-tree-hint { + position: sticky; + left: 0; + z-index: 3; + width: max-content; + max-width: 100%; + margin: -2px 0 8px; + padding: 5px 8px; + border-radius: 9px; + color: var(--muted); font-size: 11px; - color: var(--muted); - padding-top: 8px; + background: rgba(255,255,255,0.62); + border: 1px solid rgba(84,68,48,0.08); } -.lineage-generation-nodes { - display: flex; - flex-wrap: wrap; - gap: 8px; - align-items: stretch; -} -.family-node.lineage-node { - width: 132px; - min-height: 54px; - align-items: flex-start; -} -.lineage-children { - display: block; - margin-top: 3px; - font-size: 9px; - color: rgba(117,106,94,0.84); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} -.lineage-empty-family { - color: var(--muted); - font-size: 12px; - padding: 10px; -} - - -.lineage-tree-panel { +.lineage-family-tree .lineage-tree-panel { position: relative; overflow: visible; min-width: 360px; margin-top: 6px; + border-radius: 14px; + background: + linear-gradient(90deg, rgba(84,68,48,0.045) 1px, transparent 1px), + linear-gradient(180deg, rgba(84,68,48,0.040) 1px, transparent 1px), + rgba(255,250,244,0.48); + background-size: 34px 34px, 34px 34px, auto; + border: 1px solid rgba(84,68,48,0.10); } -.lineage-links { +.lineage-family-tree .lineage-links { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; pointer-events: none; + z-index: 1; } -.lineage-links path { +.lineage-render-status { + position: sticky; + top: 0; + left: 0; + z-index: 6; + width: max-content; + margin: 0 0 8px; + padding: 6px 10px; + border-radius: 999px; + font-size: 12px; + color: var(--muted); + background: rgba(255,255,255,0.88); + border: 1px solid rgba(84,68,48,0.10); + box-shadow: 0 4px 10px rgba(64,48,32,0.06); +} +.lineage-family-tree .lineage-links path { fill: none; stroke: rgba(107, 88, 61, 0.58); stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; } -.lineage-links .partner-link { stroke-width: 2.4; stroke: rgba(98, 80, 56, 0.66); } -.lineage-links .child-link { stroke-width: 1.9; stroke: rgba(107, 88, 61, 0.54); } -.tree-node { +.lineage-family-tree .lineage-links .partner-link { + stroke-width: 2.4; + stroke: rgba(98, 80, 56, 0.68); + stroke-dasharray: 5 4; +} +.lineage-family-tree .lineage-links .child-link { + stroke-width: 2; + stroke: rgba(107, 88, 61, 0.58); +} +.lineage-family-tree .tree-node { position: absolute; width: 132px; - min-height: 54px; + height: 68px; + min-height: 68px; + box-sizing: border-box; align-items: flex-start; z-index: 2; } -.tree-generation-label { +.lineage-family-tree .tree-node.child { + border-color: rgba(103,178,44,0.34); +} +.lineage-family-tree .tree-generation-label { position: absolute; - left: 0; + left: 10px; width: 54px; + padding: 3px 0; + border-radius: 999px; font-size: 10px; color: var(--muted); - z-index: 1; + text-align: center; + background: rgba(255,255,255,0.70); + border: 1px solid rgba(84,68,48,0.08); + z-index: 2; } -.lineage-family { overflow-x: auto; } - -.lineage-family-readable { - overflow: visible; -} -.lineage-readable-grid { - display: grid; - grid-template-columns: minmax(300px, 1.05fr) minmax(280px, 0.95fr); - gap: 12px; - align-items: start; -} -.lineage-member-column, -.lineage-reproduction-column { - min-width: 0; -} -.lineage-generation-block { - padding: 8px 0; - border-top: 1px dashed rgba(84,68,48,0.12); -} -.lineage-generation-block:first-child { border-top: 0; padding-top: 0; } -.lineage-generation-block > b, -.lineage-column-title { - display: block; - margin: 0 0 7px; - color: rgba(84,68,48,0.72); - font-size: 11px; -} -.lineage-person-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(132px, 1fr)); - gap: 7px; -} -.family-node.lineage-person { - width: auto; - min-height: 46px; -} -.lineage-pair-list { - display: grid; - gap: 8px; -} -.lineage-pair-card { - padding: 8px; - border-radius: 12px; - border: 1px solid rgba(84,68,48,0.12); - background: rgba(255,253,248,0.70); -} -.lineage-pair-parents, -.lineage-pair-children { - display: flex; - flex-wrap: wrap; - gap: 5px; - align-items: center; -} -.lineage-pair-parents { - padding-bottom: 5px; -} -.lineage-pair-children { - padding-top: 5px; - border-top: 1px solid rgba(84,68,48,0.08); -} -.lineage-plus { - color: rgba(84,68,48,0.44); - font-weight: 700; - line-height: 28px; -} -.lineage-birth-arrow { - color: rgba(107, 88, 61, 0.62); - font-size: 14px; - line-height: 1; - padding: 1px 0 3px 13px; -} -.lineage-mini { - display: inline-grid; - grid-template-columns: 20px minmax(0, 1fr); - grid-template-rows: auto auto; - gap: 0 4px; - align-items: center; - max-width: 132px; - min-width: 88px; - padding: 4px 6px; +.lineage-family-tree .lineage-empty-family { + position: absolute; + left: 72px; + bottom: 8px; + padding: 5px 8px; border-radius: 9px; - border: 1px solid rgba(84,68,48,0.12); - background: rgba(255,255,255,0.64); + background: rgba(255,255,255,0.70); } -.lineage-mini.child { - border-color: rgba(103,178,44,0.28); - background: rgba(246,255,235,0.62); -} -.lineage-mini.dead { opacity: 0.64; filter: grayscale(0.25); } -.lineage-mini img { - grid-row: 1 / span 2; - width: 20px; - height: 20px; - object-fit: contain; -} -.lineage-mini b, -.lineage-mini small { - display: block; - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} -.lineage-mini b { font-size: 10px; line-height: 1.1; } -.lineage-mini small { color: var(--muted); font-size: 8px; line-height: 1.1; } -@media (max-width: 760px) { - .lineage-readable-grid { grid-template-columns: 1fr; } +@media (max-width: 720px) { + .ecology-grid { grid-template-columns: 1fr; } + .ecology-card { grid-template-columns: 68px minmax(0, 1fr); } + .ecology-card img { width: 68px; height: 68px; } +} + +@media (max-width: 720px) { + .lineage-main-content { + height: 70vh; + min-height: 420px; + max-height: none; + } } diff --git a/index.html b/index.html index 7cef262..743e3ae 100644 --- a/index.html +++ b/index.html @@ -3,22 +3,25 @@ - たりない観察箱 + たりない観察
-
-

たりない観察箱

-

たりないが暮らす小さな庭を観察します。食べ、眠り、関係を変えます。

+
+ 333の部屋に移動 +
+

たりない観察

+

操作: 左クリックで選択・配置 / つまむでドラッグ / 右ドラッグでパン / ホイールで拡大縮小

+
+ - - +
@@ -40,6 +43,7 @@ +
@@ -48,6 +52,8 @@
+ +
@@ -57,12 +63,8 @@
+ -
- -
- -
@@ -76,9 +78,9 @@

コロニー

個体数0
-
死亡数0
+
累計死亡数0
-
+
@@ -89,15 +91,17 @@
-

記録

+

イベント

-

家系図

-

子どもは成長するまで親についていきます。この領域をスクロールして家系を確認できます。

+
+

家系図

+ +
@@ -116,6 +120,68 @@ + + + + + + + + diff --git a/js/data.js b/js/data.js index 804a27f..66f7039 100644 --- a/js/data.js +++ b/js/data.js @@ -8,7 +8,7 @@ const SPRITES = [ { id: "cry", label: "\u6ce3\u304d\u305f\u308a\u306a\u3044", path: "assets/sprites/tarinai_06_cry.png" }, { id: "sleep", label: "\u7720\u308a\u305f\u308a\u306a\u3044", path: "assets/sprites/tarinai_07_sleep.png" }, { id: "pokan", label: "\u307d\u304b\u3093\u305f\u308a\u306a\u3044", path: "assets/sprites/tarinai_08_pokan.png" }, - { id: "stretch", label: "\u4f38\u3073\u305f\u308a\u306a\u3044", path: "assets/sprites/tarinai_09_stretch.png" }, + { id: "stretch", label: "\u4f38\u3073\u305f\u308a\u306a\u3044", path: "assets/sprites/tarinai_09_stretch.png", actionOnly: true }, { id: "hurt", label: "\u75db\u304c\u308a\u305f\u308a\u306a\u3044", path: "assets/sprites/tarinai_10_hurt.png", actionOnly: true }, { id: "weak", label: "\u5f31\u308a\u305f\u308a\u306a\u3044", path: "assets/sprites/tarinai_11_weak.png", actionOnly: true }, { id: "back", label: "\u5965\u5411\u304d\u305f\u308a\u306a\u3044", path: "assets/sprites/tarinai_12_back.png", actionOnly: true }, @@ -27,6 +27,7 @@ const SPRITES = [ { id: "stress_sweat", label: "\u9ad8\u30b9\u30c8\u30ec\u30b9\u305f\u308a\u306a\u3044\u30fb\u6c57", path: "assets/sprites/tarinai_25_stress_sweat.png", actionOnly: true }, { id: "intimidate", label: "\u5a01\u5687\u305f\u308a\u306a\u3044", path: "assets/sprites/tarinai_26_intimidate.png", actionOnly: true }, { id: "birth_ritual", label: "\u7e41\u6b96\u524d\u305f\u308a\u306a\u3044", path: "assets/sprites/tarinai_27_birth_ritual.png", actionOnly: true }, + { id: "zunchi_slave", label: "\u305a\u3093\u3061\u3069\u308c\u3044", path: "assets/sprites/tarinai_28_zunchi_slave.png", displayOnly: true }, ]; const DECOR_ASSETS = [ { id: "zunchi", path: "assets/objects/zunchi.png" }, @@ -59,6 +60,7 @@ const ALPHA_BOUNDS = { stress_dizzy: { x: 16, y: 21, w: 480, h: 433, imageW: 512, imageH: 468 }, stress_sweat: { x: 16, y: 26, w: 480, h: 428, imageW: 512, imageH: 468 }, intimidate: { x: 98, y: 16, w: 316, h: 438, imageW: 512, imageH: 468 }, + zunchi_slave: { x: 17, y: 22, w: 478, h: 431, imageW: 512, imageH: 468 }, zunchi: { x: 0, y: 0, w: 236, h: 178, imageW: 236, imageH: 178 }, zunchi_02: { x: 8, y: 63, w: 220, h: 105, imageW: 236, imageH: 178 }, }; diff --git a/js/items.js b/js/items.js index 1c4083f..312fdc1 100644 --- a/js/items.js +++ b/js/items.js @@ -1,4 +1,4 @@ -"use strict"; +"use strict"; class Item { constructor(type, x, y) { @@ -7,10 +7,10 @@ class Item { this.x = x; this.y = y; this.r = { - food: 14, sweet: 13, water: 12, grass: 17, stone: 20, bed: 37, firecracker: 15, fence_v: 42, fence_h: 42, trace: 16, splat: 26, zunchi: 14 + food: 14, sweet: 13, love_mochi: 13, fight_mochi: 13, water: 12, grass: 17, stone: 20, bed: 37, ball: 18, firecracker: 15, fence_v: 42, fence_h: 42, trace: 16, splat: 26, zunchi: 14 }[type] || 12; this.amount = { - food: 85, sweet: 62, water: 64, grass: 120, stone: 999, bed: 999, firecracker: 999, fence_v: 999, fence_h: 999, trace: 220, splat: 260, zunchi: 240 + food: 85, sweet: 62, love_mochi: 62, fight_mochi: 62, water: 64, grass: 120, stone: 999, bed: 999, ball: 999, firecracker: 999, fence_v: 999, fence_h: 999, trace: 220, splat: 260, zunchi: 240 }[type] || 80; this.age = 0; this.seed = Math.random() * 1000; @@ -25,11 +25,26 @@ class Item { this.seedTimer = rand(18, 38); this.eatenAmount = 0; this.fertilityBoost = 0; + this.lifeSpan = rand(420, 860); + this.wither = 0; } if (type === "bed") { this.comfort = rand(0.86, 1.14); this.wear = 0; } + if (type === "ball") { + this.vx = rand(-5, 5); + this.vy = rand(-5, 5); + this.prevX = x; + this.prevY = y; + this.spin = rand(0, Math.PI * 2); + this.spinVelocity = rand(-1.6, 1.6); + this.lastKickedAt = -999; + this.lastKickerId = ""; + this.lastPokedAt = -999; + this.pokeCombo = 0; + this.lastPokeAngle = rand(0, Math.PI * 2); + } if (type === "zunchi") { this.stage = "fresh"; this.stageTimer = 0; @@ -53,7 +68,7 @@ class Item { } } if (this.type === "water") this.amount -= dt * 0.9; - if (this.type === "sweet") this.amount -= dt * 0.12; + if (["sweet", "love_mochi", "fight_mochi"].includes(this.type)) this.amount -= dt * 0.12; if (this.type === "trace") this.amount -= dt * 1.35; if (this.type === "splat") this.amount -= dt * 1.05; if (this.type === "firecracker") { @@ -63,6 +78,7 @@ class Item { else this.amount = 0; } } + if (this.type === "ball") this.updateBall(dt, worldRef); if (this.type === "grass") { this.lifecycleTimer += dt; const factor = worldRef.grassUpdateFactor ? worldRef.grassUpdateFactor() : 1; @@ -82,6 +98,147 @@ class Item { } } } + updateBall(dt, worldRef) { + this.prevX = this.x; + this.prevY = this.y; + const maxSpeed = 1250; + const clampVelocity = () => { + const speed = Math.hypot(this.vx || 0, this.vy || 0); + if (speed > maxSpeed) { + this.vx = (this.vx || 0) / speed * maxSpeed; + this.vy = (this.vy || 0) / speed * maxSpeed; + } + return Math.hypot(this.vx || 0, this.vy || 0); + }; + const moving = clampVelocity(); + if (moving > 0.04) { + this.x += (this.vx || 0) * dt; + this.y += (this.vy || 0) * dt; + const p = Math.max(28, CONFIG.worldPadding || 30); + if (this.x < p) { this.x = p; this.vx = Math.abs(this.vx || 0) * 0.72; this.spinVelocity *= -0.72; } + if (this.x > worldRef.w - p) { this.x = worldRef.w - p; this.vx = -Math.abs(this.vx || 0) * 0.72; this.spinVelocity *= -0.72; } + if (this.y < p) { this.y = p; this.vy = Math.abs(this.vy || 0) * 0.72; this.spinVelocity *= -0.72; } + if (this.y > worldRef.h - p) { this.y = worldRef.h - p; this.vy = -Math.abs(this.vy || 0) * 0.72; this.spinVelocity *= -0.72; } + this.resolveBallObstacleCollisions(dt, worldRef); + clampVelocity(); + const groundFriction = Math.pow(0.72, dt); + this.vx *= groundFriction; + this.vy *= groundFriction; + } + this.spin = (this.spin || 0) + (this.spinVelocity || 0) * dt + (this.vx || 0) * dt / Math.max(8, this.r || 18); + this.spinVelocity *= Math.pow(0.65, dt); + if (Math.hypot(this.vx || 0, this.vy || 0) < 0.18) { this.vx = 0; this.vy = 0; } + } + + resolveBallObstacleCollisions(dt, worldRef) { + if (!worldRef?.nearbyItems) return; + const speed = Math.hypot(this.vx || 0, this.vy || 0); + const searchRadius = Math.max(130, (this.r || 18) + speed * Math.max(dt || 0.016, 0.016) + 96); + const items = worldRef.nearbyItems(this.x, this.y, searchRadius) || []; + for (const it of items) { + if (!it || it === this || it.dead) continue; + if (it.type === "bed") { + this.applyBallHayDrag(it, dt, worldRef); + } else if (it.type === "stone") { + this.resolveBallCircleBounce(it, (it.r || 20) * 1.08 + (this.r || 18), 0.82, worldRef); + } else if (it.type === "fence_v" || it.type === "fence_h") { + this.resolveBallFenceBounce(it, worldRef); + } + } + } + + applyBallHayDrag(bed, dt, worldRef) { + const rx = Math.max(26, (bed.r || 37) * 1.72 + (this.r || 18) * 0.40); + const ry = Math.max(18, (bed.r || 37) * 0.92 + (this.r || 18) * 0.34); + const dx = this.x - bed.x; + const dy = this.y - bed.y; + const inside = (dx * dx) / (rx * rx) + (dy * dy) / (ry * ry) <= 1; + if (!inside) return; + const slow = Math.pow(0.16, Math.max(0.016, dt || 0.016)); + this.vx *= slow; + this.vy *= slow; + this.spinVelocity *= Math.pow(0.24, Math.max(0.016, dt || 0.016)); + if ((this.lastHaySlowLogAt || -999) + 3.5 < (worldRef.time || 0) && Math.hypot(this.vx || 0, this.vy || 0) > 120) { + this.lastHaySlowLogAt = worldRef.time || 0; + worldRef.effects?.push(new Effect("ring", this.x, this.y, { size: Math.max(14, this.r * 0.95), life: 0.20, color: "rgba(214,184,96,0.42)" })); + } + } + + resolveBallCircleBounce(obstacle, hitRadius, restitution, worldRef) { + let dx = this.x - obstacle.x; + let dy = this.y - obstacle.y; + let d = Math.hypot(dx, dy); + if (d >= hitRadius) return; + if (d < 0.001) { + const speed = Math.hypot(this.vx || 0, this.vy || 0); + if (speed > 0.001) { dx = -(this.vx || 0) / speed; dy = -(this.vy || 0) / speed; d = 1; } + else { dx = Math.cos(this.seed || 0); dy = Math.sin(this.seed || 0); d = 1; } + } + const nx = dx / d; + const ny = dy / d; + const toward = (this.vx || 0) * nx + (this.vy || 0) * ny; + this.x = obstacle.x + nx * (hitRadius + 0.5); + this.y = obstacle.y + ny * (hitRadius + 0.5); + if (toward < 0) { + this.vx = (this.vx || 0) - (1 + restitution) * toward * nx; + this.vy = (this.vy || 0) - (1 + restitution) * toward * ny; + } else { + this.vx = (this.vx || 0) + nx * 24; + this.vy = (this.vy || 0) + ny * 24; + } + this.vx *= 0.96; + this.vy *= 0.96; + this.spinVelocity = clamp((this.spinVelocity || 0) + (nx >= 0 ? -1 : 1) * 5.5, -38, 38); + this.emitBallBounce(worldRef, obstacle); + } + + resolveBallFenceBounce(fence, worldRef) { + const vertical = fence.type === "fence_v"; + const len = Math.max(112, (fence.r || 42) * 3.55); + const thick = Math.max(10, (fence.r || 42) * 0.31); + const halfLen = len / 2; + const sx1 = vertical ? fence.x : fence.x - halfLen; + const sy1 = vertical ? fence.y - halfLen : fence.y; + const sx2 = vertical ? fence.x : fence.x + halfLen; + const sy2 = vertical ? fence.y + halfLen : fence.y; + const vx = sx2 - sx1; + const vy = sy2 - sy1; + const segLenSq = vx * vx + vy * vy || 1; + const u = clamp(((this.x - sx1) * vx + (this.y - sy1) * vy) / segLenSq, 0, 1); + const cx = sx1 + vx * u; + const cy = sy1 + vy * u; + const hitRadius = (this.r || 18) + thick / 2 + 2; + let dx = this.x - cx; + let dy = this.y - cy; + let d = Math.hypot(dx, dy); + if (d >= hitRadius) return; + if (d < 0.001) { + if (vertical) { dx = (this.vx || 0) >= 0 ? -1 : 1; dy = 0; } + else { dx = 0; dy = (this.vy || 0) >= 0 ? -1 : 1; } + d = 1; + } + const nx = dx / d; + const ny = dy / d; + const toward = (this.vx || 0) * nx + (this.vy || 0) * ny; + this.x = cx + nx * (hitRadius + 0.5); + this.y = cy + ny * (hitRadius + 0.5); + if (toward < 0) { + this.vx = (this.vx || 0) - 1.78 * toward * nx; + this.vy = (this.vy || 0) - 1.78 * toward * ny; + } + this.vx *= 0.94; + this.vy *= 0.94; + this.spinVelocity = clamp((this.spinVelocity || 0) + (nx >= 0 ? -1 : 1) * 6.2, -38, 38); + this.emitBallBounce(worldRef, fence); + } + + emitBallBounce(worldRef, obstacle) { + const now = worldRef?.time || 0; + if ((this.lastObstacleBounceAt || -999) + 0.08 > now) return; + this.lastObstacleBounceAt = now; + worldRef?.effects?.push(new Effect("ring", this.x, this.y, { size: Math.max(13, (this.r || 18) * 0.82), life: 0.18, color: obstacle?.type === "stone" ? "rgba(165,165,150,0.45)" : "rgba(160,116,64,0.42)" })); + } + updateGrass(dt, worldRef) { const light = worldRef.lightLevel ? worldRef.lightLevel() : 0.7; const weather = worldRef.weather || "sunny"; @@ -124,13 +281,22 @@ class Item { const growthRate = (0.006 + light * 0.004 + weatherBoost * 0.01 + waterNear * 0.014 + this.fertilityBoost * 0.014) * dt * 0.5; this.growth = clamp((this.growth ?? 0.25) + growthRate - dryStress * dt, 0, 0.88); this.health = clamp((this.health ?? 1) + dt * (waterNear > 0.05 || weather === "light_rain" ? 0.017 : -0.0015 - dryStress * 0.28), 0.18, 1); - const grassLimit = CONFIG.grassLimit ?? 96; + const grassLimit = worldRef.grassLimit ? worldRef.grassLimit() : (CONFIG.grassLimit ?? 96); if (this.growth >= 0.86 && (worldRef.itemCounts?.grass || 0) < grassLimit) { const spawned = this.seedAround(worldRef, fertileSource, this.fertilityBoost > 0.25, 2); if (spawned > 0) this.growth = rand(0.52, 0.68); this.seedTimer = rand(18, 34); } - this.amount = clamp(this.growth * 120 * this.health, 3, 130); + if (this.age > (this.lifeSpan || 640) * 0.82) { + const fade = clamp((this.age - (this.lifeSpan || 640) * 0.82) / Math.max((this.lifeSpan || 640) * 0.18, 1), 0, 1); + this.wither = fade; + this.health = clamp((this.health ?? 1) - dt * (0.004 + fade * 0.020), 0, 1); + this.growth = clamp((this.growth ?? 0.25) - dt * fade * 0.005, 0, 0.88); + } else { + this.wither = Math.max(0, (this.wither || 0) - dt * 0.08); + } + this.amount = clamp(this.growth * 120 * this.health, 0, 130); + if (this.age > (this.lifeSpan || 640) * 1.05) this.amount -= dt * (1.2 + (this.wither || 0) * 24); this.seedTimer -= dt * (this.growth > 0.72 ? 1 : 0.25) * 0.5; if (this.seedTimer <= 0 && this.growth > 0.64 && (worldRef.itemCounts?.grass || 0) < grassLimit) { this.seedTimer = rand(24, 54); @@ -139,7 +305,7 @@ class Item { } seedAround(worldRef, fertileSource = null, fertile = false, maxNew = 1) { let made = 0; - const limit = CONFIG.grassLimit ?? 96; + const limit = worldRef.grassLimit ? worldRef.grassLimit() : (CONFIG.grassLimit ?? 96); for (let n = 0; n < maxNew && (worldRef.itemCounts?.grass || 0) < limit; n++) { const source = fertile && fertileSource ? fertileSource : this; const spot = worldRef.findGrassSproutSpot @@ -157,7 +323,9 @@ class Item { } updateZunchi(dt, worldRef) { - const rainBoost = worldRef.weather === "light_rain" ? 1.35 : 1; + const rainy = worldRef.weather === "light_rain"; + const rainBoost = rainy ? 1.95 : 1; + const decayBoost = rainy ? 2.15 : 1; let waterBoost = 0; const nearby = worldRef.nearbyItems ? worldRef.nearbyItems(this.x, this.y, 84) : (worldRef.items || []); for (const it of nearby) { @@ -167,17 +335,19 @@ class Item { this.stageTimer += dt * 2.0 * (rainBoost + waterBoost * 1.3); if (this.stage === "fresh") { this.freshness = clamp(1 - this.stageTimer / 110, 0, 1); + this.amount -= dt * 0.05 * decayBoost; if (this.stageTimer > 110) { this.stage = "dry"; this.stageTimer = 0; } } else if (this.stage === "dry") { this.freshness = clamp(0.55 - this.stageTimer / 220, 0.20, 0.55); + this.amount -= dt * 0.09 * decayBoost; if (this.stageTimer > 150) { this.stage = "decomposing"; this.stageTimer = 0; this.fertility = 0.35; } } else if (this.stage === "decomposing") { this.fertility = clamp(this.fertility + dt * 0.015, 0, 1); - this.amount -= dt * 0.12; + this.amount -= dt * 0.12 * decayBoost; if (this.stageTimer > 190) { this.stage = "fertile_soil"; this.stageTimer = 0; this.amount = Math.min(this.amount, 140); } } else if (this.stage === "fertile_soil") { this.fertility = clamp(1 - this.stageTimer / 360, 0, 1); - this.amount -= dt * 0.26; + this.amount -= dt * 0.26 * decayBoost; } } get dead() { @@ -187,8 +357,11 @@ class Item { return { type: this.type, x: this.x, y: this.y, amount: this.amount, age: this.age, seed: this.seed, stage: this.stage, stageTimer: this.stageTimer, fertility: this.fertility, freshness: this.freshness, - growth: this.growth, health: this.health, seedTimer: this.seedTimer, eatenAmount: this.eatenAmount, fertilityBoost: this.fertilityBoost, - comfort: this.comfort, wear: this.wear, zunchiVariant: this.zunchiVariant, toolSize: this.toolSize, blastScale: this.blastScale, fuseTimer: this.fuseTimer, fuseMax: this.fuseMax, dropTimer: this.dropTimer, dropMax: this.dropMax + growth: this.growth, health: this.health, seedTimer: this.seedTimer, eatenAmount: this.eatenAmount, fertilityBoost: this.fertilityBoost, lifeSpan: this.lifeSpan, wither: this.wither, + comfort: this.comfort, wear: this.wear, crawlOpen: this.crawlOpen, zunchiVariant: this.zunchiVariant, toolSize: this.toolSize, blastScale: this.blastScale, fuseTimer: this.fuseTimer, fuseMax: this.fuseMax, dropTimer: this.dropTimer, dropMax: this.dropMax, + vx: this.vx, vy: this.vy, prevX: this.prevX, prevY: this.prevY, + spin: this.spin, spinVelocity: this.spinVelocity, lastKickedAt: this.lastKickedAt, lastKickerId: this.lastKickerId, + lastPokedAt: this.lastPokedAt, pokeCombo: this.pokeCombo, lastPokeAngle: this.lastPokeAngle }; } static from(o) { @@ -200,10 +373,10 @@ class Item { it.x = o.x; it.y = o.y; it.r = { - food: 14, sweet: 13, water: 12, grass: 17, stone: 20, bed: 37, firecracker: 15, fence_v: 42, fence_h: 42, trace: 16, splat: 26, zunchi: 14 + food: 14, sweet: 13, love_mochi: 13, fight_mochi: 13, water: 12, grass: 17, stone: 20, bed: 37, ball: 18, firecracker: 15, fence_v: 42, fence_h: 42, trace: 16, splat: 26, zunchi: 14 }[type] || 12; it.amount = o.amount ?? ({ - food: 85, sweet: 62, water: 64, grass: 120, stone: 999, bed: 999, firecracker: 999, fence_v: 999, fence_h: 999, trace: 220, splat: 260, zunchi: 240 + food: 85, sweet: 62, love_mochi: 62, fight_mochi: 62, water: 64, grass: 120, stone: 999, bed: 999, ball: 999, firecracker: 999, fence_v: 999, fence_h: 999, trace: 220, splat: 260, zunchi: 240 }[type] || 80); it.age = o.age || 0; it.seed = o.seed || Math.random() * 1000; @@ -223,6 +396,19 @@ class Item { it.comfort = 1; it.wear = 0; } + if (type === "ball") { + it.vx = o.vx || 0; + it.vy = o.vy || 0; + it.prevX = o.prevX ?? o.x; + it.prevY = o.prevY ?? o.y; + it.spin = o.spin || 0; + it.spinVelocity = o.spinVelocity || 0; + it.lastKickedAt = o.lastKickedAt ?? -999; + it.lastKickerId = o.lastKickerId || ""; + it.lastPokedAt = o.lastPokedAt ?? -999; + it.pokeCombo = o.pokeCombo || 0; + it.lastPokeAngle = o.lastPokeAngle || rand(0, Math.PI * 2); + } if (type === "zunchi") { it.stage = "fresh"; it.stageTimer = 0; @@ -243,8 +429,22 @@ class Item { it.seedTimer = o.seedTimer ?? it.seedTimer; it.eatenAmount = o.eatenAmount || 0; it.fertilityBoost = o.fertilityBoost || 0; + it.lifeSpan = o.lifeSpan ?? it.lifeSpan; + it.wither = o.wither ?? it.wither ?? 0; it.comfort = o.comfort ?? it.comfort; it.wear = o.wear ?? it.wear; + it.crawlOpen = o.crawlOpen ?? it.crawlOpen; + it.vx = o.vx ?? it.vx ?? 0; + it.vy = o.vy ?? it.vy ?? 0; + it.prevX = o.prevX ?? it.prevX ?? it.x; + it.prevY = o.prevY ?? it.prevY ?? it.y; + it.spin = o.spin ?? it.spin ?? 0; + it.spinVelocity = o.spinVelocity ?? it.spinVelocity ?? 0; + it.lastKickedAt = o.lastKickedAt ?? it.lastKickedAt ?? -999; + it.lastKickerId = o.lastKickerId || it.lastKickerId || ""; + it.lastPokedAt = o.lastPokedAt ?? it.lastPokedAt ?? -999; + it.pokeCombo = o.pokeCombo ?? it.pokeCombo ?? 0; + it.lastPokeAngle = o.lastPokeAngle ?? it.lastPokeAngle ?? rand(0, Math.PI * 2); it.zunchiVariant = o.zunchiVariant || it.zunchiVariant || "zunchi"; it.toolSize = o.toolSize || it.toolSize || "medium"; it.blastScale = o.blastScale || it.blastScale || 1; @@ -303,10 +503,12 @@ class Item { ctx.save(); ctx.translate(this.x, this.y + dropY); ctx.globalAlpha = visibleAlpha; - if (this.type === "food" || this.type === "sweet") { + if (this.type === "food" || this.type === "sweet" || this.type === "love_mochi" || this.type === "fight_mochi") { const sweet = this.type === "sweet"; - ctx.fillStyle = sweet ? "#fffaf1" : "#f1dfb7"; - ctx.strokeStyle = sweet ? "#9a8a72" : "#8d6f48"; + const loveMochi = this.type === "love_mochi"; + const fightMochi = this.type === "fight_mochi"; + ctx.fillStyle = loveMochi ? "#fff1f6" : (fightMochi ? "#fff3ec" : (sweet ? "#fffaf1" : "#f1dfb7")); + ctx.strokeStyle = loveMochi ? "#bf6f8b" : (fightMochi ? "#bc6d42" : (sweet ? "#9a8a72" : "#8d6f48")); ctx.lineWidth = 2; ctx.beginPath(); const bob = 0; @@ -321,6 +523,30 @@ class Item { ctx.beginPath(); ctx.ellipse(-this.r * 0.32, -this.r * 0.16, this.r * 0.24, this.r * 0.10, -0.35, 0, Math.PI * 2); ctx.fill(); + } else if (loveMochi) { + ctx.fillStyle = "#ff6a9c"; + ctx.font = `${Math.round((this.r || 13) * 1.15)}px sans-serif`; + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + ctx.fillText("❤", 0, 1); + ctx.fillStyle = "rgba(255,255,255,0.55)"; + ctx.beginPath(); + ctx.ellipse(-this.r * 0.28, -this.r * 0.18, this.r * 0.23, this.r * 0.10, -0.35, 0, Math.PI * 2); + ctx.fill(); + } else if (fightMochi) { + ctx.fillStyle = "#d55632"; + ctx.font = `${Math.round((this.r || 13) * 1.02)}px sans-serif`; + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + ctx.fillText("✦", 0, 1); + ctx.strokeStyle = "rgba(168,76,42,0.72)"; + ctx.lineWidth = 1.4; + ctx.beginPath(); + ctx.moveTo(-this.r * 0.44, -this.r * 0.42); + ctx.lineTo(this.r * 0.44, this.r * 0.42); + ctx.moveTo(-this.r * 0.44, this.r * 0.42); + ctx.lineTo(this.r * 0.44, -this.r * 0.42); + ctx.stroke(); } else { ctx.fillStyle = "#fff7dc"; for (let i = 0; i < 5; i++) { @@ -343,7 +569,8 @@ class Item { const stage = growth < 0.22 ? "sprout" : growth < 0.48 ? "young" : "mature"; const eaten = this.eatenAmount > 14 || growth < 0.16; const grassLoad = 1; - ctx.strokeStyle = eaten ? "#7b8755" : (styleNight ? "#294b40" : (styleWarm ? "#64ad3f" : "#4f8438")); + const wither = clamp(this.wither || 0, 0, 1); + ctx.strokeStyle = eaten ? "#7b8755" : (wither > 0.08 ? `rgba(${Math.round(106 + wither * 40)}, ${Math.round(132 - wither * 32)}, ${Math.round(70 - wither * 18)}, 1)` : (styleNight ? "#294b40" : (styleWarm ? "#64ad3f" : "#4f8438"))); ctx.shadowColor = "transparent"; ctx.shadowBlur = 0; ctx.lineCap = "round"; @@ -399,6 +626,27 @@ class Item { ctx.lineTo(x + Math.cos(a) * len * 0.5, y + Math.sin(a) * len * 0.5); ctx.stroke(); } + } else if (this.type === "ball") { + const speed = Math.hypot(this.vx || 0, this.vy || 0); + const moving = clamp(speed / 260, 0, 1); + ctx.save(); + ctx.rotate(this.spin || 0); + ctx.shadowColor = styleNight ? "rgba(0,0,0,0.34)" : "rgba(52,40,26,0.18)"; + ctx.shadowBlur = 2 + moving * 3; + ctx.shadowOffsetY = 2; + ctx.font = `${Math.round((this.r || 18) * 2.0)}px "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", sans-serif`; + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + ctx.fillText("⚽", 0, 1); + if (moving > 0.55) { + ctx.globalAlpha = 0.14 + moving * 0.10; + ctx.shadowColor = "transparent"; + ctx.fillStyle = "#ffffff"; + ctx.beginPath(); + ctx.ellipse(-this.r * 0.35, -this.r * 0.42, this.r * 0.25, this.r * 0.12, -0.45, 0, Math.PI * 2); + ctx.fill(); + } + ctx.restore(); } else if (this.type === "firecracker") { const fuse = clamp((this.fuseTimer ?? 5) / Math.max(this.fuseMax || 5, 0.1), 0, 1); const flash = fuse < 0.35 ? (0.5 + Math.sin(t * 20 + this.seed) * 0.5) : 0; diff --git a/js/render.js b/js/render.js index 13e425d..34c1468 100644 --- a/js/render.js +++ b/js/render.js @@ -468,16 +468,58 @@ function drawAtmosphericLighting(ctx, w, h, lighting) { function drawRain(ctx, w, h, t) { if (world.weather !== "light_rain") return; ctx.save(); - ctx.globalAlpha = 0.28; - ctx.strokeStyle = "rgba(197, 220, 230, 0.70)"; - ctx.lineWidth = 1; - for (let i = 0; i < 34; i++) { - const x = (randSeed(18000 + i, 0, w) + t * 26) % w; - const y = (randSeed(18400 + i, 0, h) + t * 72) % h; + const perf = world.performanceLevel ? world.performanceLevel() : 0; + const count = perf >= 2 ? 58 : 118; + ctx.lineCap = "round"; + for (let i = 0; i < count; i++) { + const speed = randSeed(18100 + i, 132, 230); + const cycle = h + 320; + const len = randSeed(18500 + i, 13, 26); + const tilt = randSeed(18600 + i, 5, 12); + const fall = randSeed(18400 + i, -220, cycle) + t * speed; + const y = ((fall % cycle) + cycle) % cycle - 140; + // The droplet's movement vector matches the visible diagonal stroke. + const driftPerY = -tilt / Math.max(1, len); + const xCycle = w + 220; + const rawX = randSeed(18000 + i, -70, w + 120) + fall * driftPerY + Math.sin(t * 0.9 + i * 0.43) * 4; + const x = ((rawX % xCycle) + xCycle) % xCycle - 90; + const alpha = y < 110 ? clamp((y + 68) / 178, 0, 1) : 1; + ctx.globalAlpha = 0.34 + 0.30 * alpha; + ctx.strokeStyle = "rgba(198, 226, 240, 0.92)"; + ctx.lineWidth = randSeed(18700 + i, 1.0, 1.8); ctx.beginPath(); ctx.moveTo(x, y); - ctx.lineTo(x - 4, y + 10); + ctx.lineTo(x - tilt, y + len); ctx.stroke(); + if (perf < 3 && i % 2 === 0) { + ctx.globalAlpha = 0.18 + 0.18 * alpha; + ctx.fillStyle = "rgba(215, 237, 246, 0.92)"; + ctx.beginPath(); + ctx.ellipse(x - tilt * 0.9, y + len + 2, 1.8, 3.0, -0.28, 0, Math.PI * 2); + ctx.fill(); + } + } + if (perf < 3) { + const dripCount = perf >= 2 ? 10 : 18; + for (let i = 0; i < dripCount; i++) { + const phase = (t * randSeed(18800 + i, 0.55, 1.15) + randSeed(18900 + i, 0, 100)) % 1; + const x = randSeed(19000 + i, 12, w - 12); + const topY = -22 + phase * (h + 58); + const bottomY = topY + randSeed(19100 + i, 14, 30); + const lean = randSeed(19200 + i, -6, -2); + ctx.globalAlpha = 0.44; + ctx.strokeStyle = "rgba(206, 233, 247, 0.98)"; + ctx.lineWidth = randSeed(19300 + i, 1.2, 2.2); + ctx.beginPath(); + ctx.moveTo(x, topY); + ctx.lineTo(x + lean, bottomY); + ctx.stroke(); + ctx.globalAlpha = 0.26; + ctx.beginPath(); + ctx.ellipse(x + lean, bottomY + 2, 2.1, 3.5, -0.18, 0, Math.PI * 2); + ctx.fillStyle = "rgba(220, 241, 248, 0.98)"; + ctx.fill(); + } } ctx.restore(); } @@ -486,6 +528,22 @@ function weatherLabel(weather) { return { sunny: "\u6674\u308c", cloudy: "\u66c7\u308a", light_rain: "\u5c0f\u96e8" }[weather] || "\u6674\u308c"; } +function fillScreenFallback(ctx, w, h, lighting) { + const light = lighting?.light ?? 0.7; + ctx.save(); + const bg = ctx.createLinearGradient(0, 0, 0, Math.max(1, h)); + if (light > 0.42) { + bg.addColorStop(0, "#cfe3d3"); + bg.addColorStop(1, "#d9d0ad"); + } else { + bg.addColorStop(0, "#24355f"); + bg.addColorStop(1, "#4f5d71"); + } + ctx.fillStyle = bg; + ctx.fillRect(0, 0, w, h); + ctx.restore(); +} + function render() { activeCtx = ctx; const sceneW = world.w, sceneH = world.h; @@ -495,9 +553,9 @@ function render() { const cameraX = world.cameraX || 0; const cameraY = world.cameraY || 0; const viewScale = world.viewScale ? world.viewScale() : 1; - ctx.clearRect(0, 0, screenW, screenH); const lighting = getLightingState(world); + fillScreenFallback(ctx, screenW, screenH, lighting); const light = lighting.light; const cachedBackground = ensureBackgroundCache(sceneW, sceneH, lighting); @@ -523,7 +581,13 @@ function render() { beginFieldTransform(); for (const it of world.items) { - if (it.type !== "zunchi") it.draw(ctx, world.time, lighting); + if (it.type === "trace" || it.type === "splat") it.draw(ctx, world.time, lighting); + } + for (const ef of world.effects) { + if (ef.type === "bleed") ef.draw(ctx); + } + for (const it of world.items) { + if (it.type !== "zunchi" && it.type !== "trace" && it.type !== "splat") it.draw(ctx, world.time, lighting); } for (const it of world.items) { if (it.type === "zunchi") it.draw(ctx, world.time, lighting); @@ -546,7 +610,9 @@ function render() { const sorted = world.sortedDrawList ? world.sortedDrawList() : world.tarinai; for (const t of sorted) t.draw(ctx, world.time, lighting); - for (const ef of world.effects) ef.draw(ctx); + for (const ef of world.effects) { + if (ef.type !== "bleed") ef.draw(ctx); + } ctx.restore(); // Light affecting the whole visible scene, including characters and ground. @@ -649,4 +715,4 @@ function roundedRect(ctx, x, y, w, h, r) { } -window.TarinaiRender = { randSeed, roundedBlob, getLightingState, projectedShadowParams, imageVisibleBottomFromTop, drawImageProjectedShadow, drawProjectedShadow, drawRadialGlow, drawItemGlow, drawCreatureGlow, drawDawnRimLight, drawGardenBackdrop, drawGardenBed, drawLightRays, drawAtmosphericLighting, drawRain, drawSelectedCard, render, roundedRect }; +window.TarinaiRender = { randSeed, roundedBlob, getLightingState, projectedShadowParams, imageVisibleBottomFromTop, drawImageProjectedShadow, drawProjectedShadow, drawRadialGlow, drawItemGlow, drawCreatureGlow, drawDawnRimLight, drawGardenBackdrop, drawGardenBed, drawLightRays, drawAtmosphericLighting, drawRain, fillScreenFallback, drawSelectedCard, render, roundedRect }; diff --git a/js/sim_core.js b/js/sim_core.js index 3e30e94..55fa064 100644 --- a/js/sim_core.js +++ b/js/sim_core.js @@ -1,4 +1,4 @@ -"use strict"; +"use strict"; function makeName(worldRef = null) { const existing = new Set((worldRef?.tarinai || []).filter(t => !t.dead).map(t => t.name)); const build = () => { @@ -65,12 +65,13 @@ function stableUnit(seed, salt = "") { } const PERSONALITIES = { - timid: { label: "\u81c6\u75c5", desc: "\u9003\u3052\u3084\u3059\u304f\u3001\u55a7\u5629\u3092\u907f\u3051\u308b", fear: 1.42, fight: 0.48, social: 0.96, relation: 1.04, sleep: 1.00, zunda: 1.00 }, - glutton: { label: "\u98df\u3044\u3057\u3093\u574a", desc: "\u305a\u3093\u3060\u9905\u3092\u512a\u5148\u3057\u3084\u3059\u3044", fear: 0.92, fight: 0.82, social: 1.00, relation: 1.00, sleep: 0.95, zunda: 1.42 }, - sleepy: { label: "\u7720\u305f\u304c\u308a", desc: "\u65e9\u3081\u306b\u5bdd\u5e8a\u3078\u5411\u304b\u3046", fear: 0.96, fight: 0.72, social: 0.92, relation: 0.96, sleep: 1.34, zunda: 0.92 }, - lonely: { label: "\u5bc2\u3057\u304c\u308a", desc: "\u4ed6\u306e\u305f\u308a\u306a\u3044\u3078\u5bc4\u308a\u3084\u3059\u3044", fear: 1.02, fight: 0.70, social: 1.34, relation: 1.38, sleep: 0.96, zunda: 1.00 }, - irritable: { label: "\u6012\u308a\u3063\u307d\u3044", desc: "\u55a7\u5629\u304c\u8d77\u304d\u3084\u3059\u3044", fear: 0.82, fight: 1.82, social: 0.78, relation: 0.78, sleep: 0.90, zunda: 0.94 }, - calm: { label: "\u30de\u30a4\u30da\u30fc\u30b9", desc: "\u53cd\u5fdc\u304c\u7a4f\u3084\u304b\u3067\u5b89\u5b9a\u3057\u3084\u3059\u3044", fear: 0.82, fight: 0.58, social: 1.02, relation: 1.08, sleep: 1.02, zunda: 0.94 }, + timid: { label: "\u81c6\u75c5", desc: "\u9003\u3052\u3084\u3059\u304f\u3001\u55a7\u5629\u3092\u907f\u3051\u308b", fear: 1.42, fight: 0.48, social: 0.96, relation: 1.04, sleep: 1.00, zunda: 1.00, play: 0.78 }, + glutton: { label: "\u98df\u3044\u3057\u3093\u574a", desc: "\u305a\u3093\u3060\u9905\u3092\u512a\u5148\u3057\u3084\u3059\u3044", fear: 0.92, fight: 0.82, social: 1.00, relation: 1.00, sleep: 0.95, zunda: 1.42, play: 0.84 }, + sleepy: { label: "\u7720\u305f\u304c\u308a", desc: "\u65e9\u3081\u306b\u5bdd\u5e8a\u3078\u5411\u304b\u3046", fear: 0.96, fight: 0.72, social: 0.92, relation: 0.96, sleep: 1.34, zunda: 0.92, play: 0.68 }, + lonely: { label: "\u5bc2\u3057\u304c\u308a", desc: "\u4ed6\u306e\u305f\u308a\u306a\u3044\u3078\u5bc4\u308a\u3084\u3059\u3044", fear: 1.02, fight: 0.70, social: 1.34, relation: 1.38, sleep: 0.96, zunda: 1.00, play: 1.08 }, + irritable: { label: "\u6012\u308a\u3063\u307d\u3044", desc: "\u55a7\u5629\u304c\u8d77\u304d\u3084\u3059\u3044", fear: 0.82, fight: 1.82, social: 0.78, relation: 0.78, sleep: 0.90, zunda: 0.94, play: 0.82 }, + playful: { label: "\u904a\u3073\u597d\u304d", desc: "\u30dc\u30fc\u30eb\u3084\u52d5\u304f\u3082\u306e\u306b\u5f15\u304d\u5bc4\u305b\u3089\u308c\u3084\u3059\u3044", fear: 0.92, fight: 0.74, social: 1.10, relation: 1.04, sleep: 0.94, zunda: 0.96, play: 1.72 }, + calm: { label: "\u30de\u30a4\u30da\u30fc\u30b9", desc: "\u53cd\u5fdc\u304c\u7a4f\u3084\u304b\u3067\u5b89\u5b9a\u3057\u3084\u3059\u3044", fear: 0.82, fight: 0.58, social: 1.02, relation: 1.08, sleep: 1.02, zunda: 0.94, play: 0.86 }, }; const PERSONALITY_IDS = Object.keys(PERSONALITIES); @@ -199,6 +200,19 @@ class Effect { ctx.arc(Math.cos(a) * r, Math.sin(a) * r, randSeed(this.seed + i + 12, 2.2, 5.2), 0, Math.PI * 2); ctx.fill(); } + } else if (this.type === "zunchi_miasma") { + const drift = 1 - alpha; + ctx.globalCompositeOperation = "source-over"; + ctx.fillStyle = this.color || "rgba(16,78,28,0.62)"; + for (let i = 0; i < 5; i++) { + const a = this.seed + i * 1.41 + drift * 1.8; + const px = Math.cos(a) * this.size * randSeed(this.seed + i, 0.08, 0.62); + const py = Math.sin(a * 0.8) * this.size * randSeed(this.seed + i + 7, 0.05, 0.42) - drift * this.size * 0.42; + ctx.globalAlpha = alpha * randSeed(this.seed + i + 14, 0.10, 0.28); + ctx.beginPath(); + ctx.ellipse(px, py, this.size * randSeed(this.seed + i + 21, 0.22, 0.46), this.size * randSeed(this.seed + i + 35, 0.16, 0.38), a, 0, Math.PI * 2); + ctx.fill(); + } } else if (this.type === "bubble") { const w = Math.max(30, 14 + this.text.length * 7); const h = 20; diff --git a/js/tarinai.js b/js/tarinai.js index 98df229..e0a9698 100644 --- a/js/tarinai.js +++ b/js/tarinai.js @@ -1,4 +1,4 @@ -"use strict"; +"use strict"; class Tarinai { constructor(world, opts = {}) { @@ -33,8 +33,8 @@ class Tarinai { this.affection = opts.affection ?? rand(0, 30); this.need = opts.need || pick(NEEDS); this.state = opts.state || "idle"; - this.dead = false; - this.deathReason = ""; + this.dead = !!opts.dead; + this.deathReason = opts.deathReason || ""; this.thought = ""; this.target = null; this.targetKey = ""; @@ -69,8 +69,17 @@ class Tarinai { this.birthRitualRole = opts.birthRitualRole || 0; this.birthRitualLeader = !!opts.birthRitualLeader; this.pokeFlashTimer = opts.pokeFlashTimer ?? 0; - this.zunchiStain = 0; + this.zunchiStain = opts.zunchiStain ?? 0; this.zunchiStainSeed = opts.zunchiStainSeed ?? stableUnit(this.id, "zunchi-stain"); + this.zunchiDisease = Boolean(opts.zunchiDisease); + this.zunchiDiseaseSeverity = opts.zunchiDiseaseSeverity ?? (this.zunchiDisease ? 1 : 0); + this.zunchiDiseaseCooldown = opts.zunchiDiseaseCooldown ?? 0; + this.totalFightLosses = opts.totalFightLosses || 0; + this.isZunchiSlave = Boolean(opts.isZunchiSlave); + this.formerName = opts.formerName || null; + this.loveMochiTimer = opts.loveMochiTimer ?? 0; + this.fightMochiTimer = opts.fightMochiTimer ?? 0; + if (this.isZunchiSlave) this.name = "ずんちどれい"; this.tempComfort = opts.tempComfort ?? 0.62; this.tempTimer = opts.tempTimer ?? stableUnit(this.id, "temp-start") * 0.55; this.parents = opts.parents || []; @@ -105,16 +114,20 @@ class Tarinai { this.nextBubbleAt = 0; this.nextSleepBubbleAt = 0; this.nextFearBubbleAt = 0; + this.nextCursorHeartAt = opts.nextCursorHeartAt ?? 0; + this.cursorPetting = opts.cursorPetting ?? 0; this.aiTimer = opts.aiTimer ?? stableUnit(this.id, "ai-start") * 0.22; this.envTimer = opts.envTimer ?? stableUnit(this.id, "env-start") * 0.55; this.visibleSpriteId = opts.visibleSpriteId || null; this.spriteLockUntil = opts.spriteLockUntil ?? 0; + this.entryTimer = opts.entryTimer ?? 0; } serialize() { return { id: this.id, name: this.name, type: this.type, x: this.x, y: this.y, vx: this.vx, vy: this.vy, scale: this.scale, age: this.age, lifeSpan: this.lifeSpan, generation: this.generation, hasPaired: this.hasPaired, + dead: this.dead, deathReason: this.deathReason, hunger: this.hunger, loneliness: this.loneliness, energy: this.energy, stress: this.stress, hpBarTimer: this.hpBarTimer, affection: this.affection, need: this.need, state: this.state, goodMode: this.goodMode, facing: this.facing, visualFacing: this.visualFacing, facingLockUntil: this.facingLockUntil, eatTimer: this.eatTimer, @@ -123,6 +136,8 @@ class Tarinai { birthRitualTimer: this.birthRitualTimer, birthRitualMax: this.birthRitualMax, birthPartnerId: this.birthPartnerId, birthRitualRole: this.birthRitualRole, birthRitualLeader: this.birthRitualLeader, pokeFlashTimer: this.pokeFlashTimer, zunchiStain: this.zunchiStain, zunchiStainSeed: this.zunchiStainSeed, + totalFightLosses: this.totalFightLosses, isZunchiSlave: this.isZunchiSlave, formerName: this.formerName, loveMochiTimer: this.loveMochiTimer, fightMochiTimer: this.fightMochiTimer, + zunchiDisease: this.zunchiDisease, zunchiDiseaseSeverity: this.zunchiDiseaseSeverity, zunchiDiseaseCooldown: this.zunchiDiseaseCooldown, tempComfort: this.tempComfort, tempTimer: this.tempTimer, parents: this.parents, parentNames: this.parentNames, children: this.children, birthTime: this.birthTime, fightTimer: this.fightTimer, fightCooldown: this.fightCooldown, fightTargetId: this.fightTargetId, fightTargetIds: this.fightTargetIds, targetGiveupKey: this.targetGiveupKey, targetGiveupUntil: this.targetGiveupUntil, @@ -132,7 +147,8 @@ class Tarinai { defeatedTimer: this.defeatedTimer, defeatedById: this.defeatedById, fightWinnerId: this.fightWinnerId, relationships: this.relationships, personality: this.personality, fallTimer: this.fallTimer, fallMax: this.fallMax, fallDir: this.fallDir, blastSpinTimer: this.blastSpinTimer, blastSpinMax: this.blastSpinMax, postBirthPeaceTimer: this.postBirthPeaceTimer, - lastMealColor: this.lastMealColor, aiTimer: this.aiTimer, envTimer: this.envTimer, + lastMealColor: this.lastMealColor, aiTimer: this.aiTimer, envTimer: this.envTimer, nextCursorHeartAt: this.nextCursorHeartAt, cursorPetting: this.cursorPetting, + entryTimer: this.entryTimer || 0, }; } @@ -283,6 +299,21 @@ class Tarinai { return Boolean(this.hasPaired || (this.parents || []).length || (this.children || []).length); } + nearestCoParent(maxRange = 220) { + let best = null; + let bestD = maxRange; + for (const o of this.world.tarinai || []) { + if (!o || o === this || o.dead) continue; + if (!(this.world.areCoParents?.(this, o))) continue; + const d = dist(this, o); + if (d < bestD) { + best = o; + bestD = d; + } + } + return best; + } + displayGeneration() { return this.familyJoined() ? this.generation : null; } @@ -378,13 +409,26 @@ class Tarinai { } lowHealthSprite() { - const critical = this.lack > 96 || this.hunger > 112 || this.energy < 7 || this.stress > 118; + // This is physical weakness only. Stress is handled by the stress sprite branch + // below so high-stress individuals do not incorrectly render as low-health. + const critical = this.hunger > 112 || this.energy < 7; if (critical) return this.stretchTimer > 0 ? "stretch" : "weak"; - const weak = this.lack > 88 || this.hunger > 102 || this.energy < 18 || this.stress > 104; + const weak = this.hunger > 102 || this.energy < 18; if (weak) return "weak"; return null; } + briefDeathReason(reason = this.deathReason) { + const text = String(reason || ""); + if (!text) return ""; + if (text.includes("食べ物がたりない")) return "飢え"; + if (text.includes("寿命")) return "寿命"; + if (text.includes("ストレス")) return "過ストレス"; + if (text.includes("疲れ")) return "衰弱"; + if (text.includes("つつかれ")) return "つつき"; + return text.length > 10 ? `${text.slice(0, 10)}…` : text; + } + variantSprite(category) { const pools = { hurt: ["hurt", "hurt2"], @@ -400,27 +444,120 @@ class Tarinai { return stableChoice(this.id, `variant-${category}`, options) || options[0] || this.goodMode || "smile"; } + infectZunchiDisease(source = null, force = false) { + if (this.dead || this.zunchiDisease) return false; + if (!force && (this.zunchiDiseaseCooldown || 0) > 0) return false; + this.zunchiDisease = true; + this.zunchiDiseaseSeverity = Math.max(this.zunchiDiseaseSeverity || 0, source ? 0.72 : 0.86); + this.zunchiDiseaseCooldown = 12; + this.fearTimer = Math.max(this.fearTimer || 0, 0.35); + this.surpriseTimer = Math.max(this.surpriseTimer || 0, 0.45); + this.thought = "ずんち病にかかった"; + this.world?.spawnBubble?.(this.x, this.y - this.radius * 1.20, "うぅ…", "rgba(38,82,38,0.82)"); + return true; + } + + recoverZunchiDisease(reason = "") { + if (!this.zunchiDisease) return false; + this.zunchiDisease = false; + this.zunchiDiseaseSeverity = 0; + this.zunchiDiseaseCooldown = 18; + this.zunchiStain = clamp((this.zunchiStain || 0) - 35, 0, 100); + this.stress = clamp(this.stress - 8, 0, 130); + this.thought = reason || "ずんち病が治まった"; + this.world?.spawnBubble?.(this.x, this.y - this.radius * 1.18, "はう", "rgba(62,104,62,0.78)"); + return true; + } + + emitZunchiDiseaseEffect(dt) { + if (!this.zunchiDisease || !this.world?.effects) return; + if (Math.random() < dt * (1.2 + (this.zunchiDiseaseSeverity || 0) * 1.4)) { + this.world.effects.push(new Effect("zunchi_miasma", this.x + rand(-this.radius * 0.45, this.radius * 0.45), this.y - this.radius * rand(0.05, 0.72), { + vx: rand(-5, 5), + vy: rand(-10, -3), + life: rand(1.4, 2.4), + size: rand(8, 18) * (0.8 + (this.zunchiDiseaseSeverity || 0) * 0.45), + color: "rgba(16,78,28,0.70)", + })); + } + } + + updateZunchiDisease(dt) { + this.zunchiStain = clamp(this.zunchiStain || 0, 0, 100); + this.zunchiDiseaseCooldown = Math.max(0, (this.zunchiDiseaseCooldown || 0) - dt); + if (this.world?.weather === "light_rain") { + this.zunchiStain = clamp(this.zunchiStain - dt * 2.4, 0, 100); + if (this.zunchiDisease) this.zunchiDiseaseSeverity = Math.max(0, (this.zunchiDiseaseSeverity || 1) - dt * 0.006); + } + if (!this.zunchiDisease && this.zunchiStain > 82 && Math.random() < dt * clamp((this.zunchiStain - 82) / 18, 0, 1) * 0.045) { + this.infectZunchiDisease(null, true); + this.world?.log?.(`${this.name}はずんち病を発症した。`, "accident"); + } + if (this.zunchiDisease) { + this.zunchiDiseaseSeverity = clamp((this.zunchiDiseaseSeverity || 0.7) + dt * 0.002, 0, 1.25); + this.stress = clamp(this.stress + dt * 0.22 * this.zunchiDiseaseSeverity, 0, 130); + this.energy = clamp(this.energy - dt * 0.035 * this.zunchiDiseaseSeverity, 0, 100); + if (this.energy > 82) this.zunchiDiseaseSeverity = Math.max(0, this.zunchiDiseaseSeverity - dt * (this.energy > 94 ? 0.020 : 0.010)); + this.emitZunchiDiseaseEffect(dt); + if (this.zunchiDiseaseSeverity <= 0.04) { + if (this.recoverZunchiDisease("体力が戻り、ずんち病が治まった")) this.world?.log?.(`${this.name}のずんち病が治まった。`, "accident"); + } + } + } + + canEatItemType(type) { + return this.isZunchiSlave ? type === "zunchi" : true; + } + + hasLoveMochiEffect() { + return (this.loveMochiTimer || 0) > 0.04; + } + + hasFightMochiEffect() { + return (this.fightMochiTimer || 0) > 0.04; + } + + becomeZunchiSlave() { + if (this.isZunchiSlave) return false; + this.formerName = this.formerName || this.name; + this.isZunchiSlave = true; + this.name = "ずんちどれい"; + this.birthRitualTimer = 0; + this.birthPartnerId = null; + this.birthRitualLeader = false; + this.birthRitualRole = 0; + this.target = null; + this.reproductionTimer = Math.max(this.reproductionTimer || 0, CONFIG.reproductionCooldown + rand(8, 18)); + this.loveMochiTimer = 0; + this.stress = clamp(this.stress + 14, 0, 130); + this.hunger = Math.max(this.hunger, 40); + this.visibleSpriteId = "zunchi_slave"; + this.spriteLockUntil = 0; + return true; + } + rawSpriteId() { if (this.dead) return this.variantSprite("fear"); - if (this.type === "cry") return "cry"; - if (this.type === "stretch") return "stretch"; + if (this.isZunchiSlave) return "zunchi_slave"; + if (this.type === "cry" && (this.mood < 26 || this.stress > 78 || this.hunger > 96 || this.energy < 12)) return "cry"; if ((this.state === "eat" || this.eatTimer > 0.04) && this.target?.type === "sweet") return "zunda_eat"; if ((this.state === "seek_food" || this.foodReactTimer > 0.04) && this.target?.type === "sweet" && dist(this, this.target) < 80) return "zunda_eat"; if (this.hurtTimer > 0.08 || this.pokeFlashTimer > 0.08) return this.variantSprite("hurt"); if (this.birthRitualTimer > 0.04 || this.state === "birth_ritual") return "birth_ritual"; const low = this.lowHealthSprite(); - if (low) return low; + if (low === "stretch") return low; if (this.defeatedTimer > 0.04) return this.variantSprite("flee"); if (this.state === "intimidate" || this.intimidateTimer > 0.04) return this.variantSprite("intimidate"); if (this.state === "panic" || this.state === "cursor_enemy" || this.fearTimer > 0.08 || this.intimidatedTimer > 0.08) return this.variantSprite("fear"); - if (this.state === "sleep" || this.energy < 18) return this.variantSprite("sleep"); if (this.state === "fight" || this.fightTimer > 0.04) return "angry"; + if (this.stress > 62 || (this.stress > 50 && this.mood < 46)) return this.variantSprite("stress"); + if (low) return low; + if (this.state === "sleep" || this.energy < 18) return this.variantSprite("sleep"); if (this.surpriseTimer > 0.08) return "pokan"; if (this.state === "cursor_friend") return "smile"; if (this.state === "eat" || this.eatTimer > 0.04) return this.goodMode; if ((this.state === "seek_food" || this.state === "idle" || this.state === "follow_parent") && (this.vy < -18 || (this.target && this.target.y < this.y - 36 && Math.abs(this.target.y - this.y) > Math.abs(this.target.x - this.x) * 0.55))) return "back"; if (this.state === "seek_food") return "drool"; - if (this.stress > 62 || (this.stress > 50 && this.mood < 46)) return this.variantSprite("stress"); if (this.mood >= 66 && this.stress < 24 && this.hunger < 58 && this.loneliness < 62) return this.variantSprite("low_stress"); if (this.mood >= 63 && this.stress < 44 && this.hunger < 58) return this.goodMode; if (this.mood < 38 || this.hunger > 72 || this.loneliness > 74) return "angry"; @@ -460,6 +597,9 @@ class Tarinai { this.blastSpinTimer = Math.max(0, (this.blastSpinTimer || 0) - dt); this.postBirthPeaceTimer = Math.max(0, (this.postBirthPeaceTimer || 0) - dt); this.hpBarTimer = Math.max(0, (this.hpBarTimer || 0) - dt); + this.updateZunchiDisease(dt); + this.loveMochiTimer = Math.max(0, (this.loveMochiTimer || 0) - dt); + this.fightMochiTimer = Math.max(0, (this.fightMochiTimer || 0) - dt); if (this.intimidateTimer <= 0.04) this.intimidateTargetId = null; if (this.birthRitualTimer > 0) { const beforeBirthTimer = this.birthRitualTimer; @@ -471,6 +611,7 @@ class Tarinai { } } this.pokeFlashTimer = Math.max(0, this.pokeFlashTimer - dt); + this.cursorPetting = Math.max(0, this.cursorPetting - dt * 1.2); const wasFighting = this.fightTimer > 0.04; this.fightTimer = Math.max(0, this.fightTimer - dt); this.fightCooldown = Math.max(0, this.fightCooldown - dt); @@ -492,7 +633,6 @@ class Tarinai { this.grassEatTimer = Math.max(0, this.grassEatTimer - dt); this.hurtTimer = Math.max(0, this.hurtTimer - dt); this.fallTimer = Math.max(0, this.fallTimer - dt); - this.zunchiStain = 0; if (this.stress > 92 && this.state !== "sleep" && this.state !== "fight" && this.state !== "eat" && Math.random() < dt * 0.050) { this.bubble("\u306f\u3063...\u306f\u3063...", 3.4, "rgba(58,48,63,0.80)"); } @@ -612,6 +752,14 @@ class Tarinai { }; } + isSleepFurniture(target) { + return Boolean(target && target.type === "bed"); + } + + sleepFurnitureLabel(target) { + return "干草寝床"; + } + sleepSpotFor(bed) { if (!bed) return { x: this.x, y: this.y }; const angle = stableUnit(this.id, `bed-angle-${bed.id || bed.seed || "bed"}`) * Math.PI * 2; @@ -650,6 +798,7 @@ class Tarinai { } reactToFreshZunchi(dt) { + if (this.isZunchiSlave) return; if (this.state === "panic" || this.fearTimer > 0.2) return; const z = this.world.nearest(this, ["zunchi"], 90); if (!z || z.stage !== "fresh") return; @@ -738,6 +887,41 @@ class Tarinai { } } + if (this.zunchiDisease && this.fightTimer <= 0.04 && this.birthRitualTimer <= 0.04 && this.defeatedTimer <= 0.04) { + const water = this.world.nearest(this, ["water"], 360); + if (water && !water.dead) { + this.state = "seek_water"; + this.target = water; + this.thought = "ずんち病で水を探している"; + return; + } + this.state = "zunchi_sick"; + this.target = null; + this.thought = "ずんち病で当てもなく彷徨っている"; + if (Math.random() < dt * 0.35) this.wanderAngle += rand(-2.4, 2.4); + return; + } + + if (!this.isZunchiSlave) { + const specialMochi = this.world.nearest(this, ["love_mochi", "fight_mochi"], wasSleeping ? 520 : 390); + if (specialMochi && specialMochi.amount > 1.2 && this.hunger > 10 && !this.shouldAvoidTarget(specialMochi)) { + this.state = "seek_food"; + this.target = specialMochi; + this.foodReactTimer = Math.max(this.foodReactTimer, 0.34); + this.thought = specialMochi.type === "love_mochi" ? "媚薬餅を探している" : "喧嘩餅を探している"; + return; + } + } else if (this.hunger > 20) { + const zOnly = this.world.nearest(this, ["zunchi"], 520); + if (zOnly && zOnly.amount > 1.2) { + this.state = "seek_food"; + this.target = zOnly; + this.foodReactTimer = Math.max(this.foodReactTimer, 0.30); + this.thought = "ずんちを探している"; + return; + } + } + const zundaRange = wasSleeping ? 540 : 430 + 70 * this.personalityProfile().zunda; const zunda = this.world.nearest(this, ["sweet"], zundaRange); const zundaHungry = this.hunger > Math.max(7, 18 - 10 * (this.personalityProfile().zunda - 1)); @@ -760,15 +944,15 @@ class Tarinai { const sleepBias = this.personalityProfile().sleep; const keepSleeping = wasSleeping && (personalSleep || this.energy < 66 + 8 * sleepBias); if (keepSleeping || (personalSleep && this.energy < 90 + 8 * sleepBias) || this.energy < 28 + 8 * sleepBias) { - let bed = previousTarget && previousTarget.type === "bed" ? previousTarget : null; - if (!bed && friendNearby && friendNearby.target?.type === "bed" && (friendNearby.state === "sleep" || friendNearby.state === "seek_bed")) bed = friendNearby.target; + let bed = this.isSleepFurniture(previousTarget) ? previousTarget : null; + if (!bed && friendNearby && this.isSleepFurniture(friendNearby.target) && (friendNearby.state === "sleep" || friendNearby.state === "seek_bed")) bed = friendNearby.target; if (!bed && !wasSleeping) bed = this.world.bestBedFor(this, 360); if (bed && !wasSleeping && this.energy > 18) { const spot = this.sleepSpotFor(bed); if (distXY(this.x, this.y, spot.x, spot.y) > this.radius + 10) { this.state = "seek_bed"; this.target = bed; - this.thought = "\u5e72\u8349\u5bdd\u5e8a\u3078\u5411\u304b\u3063\u3066\u3044\u308b"; + this.thought = `${this.sleepFurnitureLabel(bed)}へ向かっている`; return; } } @@ -779,7 +963,7 @@ class Tarinai { this.nextSleepBubbleAt = this.world.time + 4.5; this.world.spawnBubble(this.x, this.y - this.radius * 1.18, "Zzz...", "rgba(65,70,92,0.72)"); } - this.thought = bed ? "\u30d9\u30c3\u30c9\u306e\u8fd1\u304f\u3067\u7720\u3063\u3066\u3044\u308b" : "\u7720\u3063\u3066\u3044\u308b"; + this.thought = bed ? "ベッドの近くで眠っている" : "眠っている"; return; } @@ -810,6 +994,24 @@ class Tarinai { } } + const playTrait = this.personalityProfile().play ?? 1; + const ballRange = this.personality === "playful" ? 540 : 240 + 60 * playTrait; + const ball = this.world.nearest(this, ["ball"], ballRange); + if (ball && this.energy > 26 && this.hunger < 98 && !this.shouldAvoidTarget(ball)) { + const ballSpeed = Math.hypot(ball.vx || 0, ball.vy || 0); + const chasers = this.world.countBallChasers ? this.world.countBallChasers(ball, this) : 0; + const baseChance = this.personality === "playful" ? 0.74 : 0.08; + const movingBonus = clamp(ballSpeed / 120, 0, 0.42); + const socialBonus = this.loneliness > 48 ? 0.10 : 0; + if (chasers < 5 && (this.personality === "playful" || ballSpeed > 7 || Math.random() < baseChance * playTrait + movingBonus + socialBonus)) { + this.state = "play_ball"; + this.target = ball; + this.goodMode = "smile"; + this.thought = this.personality === "playful" ? "ボールに引き寄せられている" : "転がるものが気になっている"; + return; + } + } + const parent = this.parentToFollow(); if (parent && !this.shouldAvoidTarget(parent)) { const d = dist(this, parent); @@ -822,7 +1024,7 @@ class Tarinai { } } - if (friendNearby && this.hunger > 44 && friendNearby.target && ["sweet", "grass"].includes(friendNearby.target.type) && !friendNearby.target.dead && !this.shouldAvoidTarget(friendNearby.target)) { + if (friendNearby && this.hunger > 44 && friendNearby.target && (this.isZunchiSlave ? ["zunchi"] : ["sweet", "love_mochi", "fight_mochi", "grass"]).includes(friendNearby.target.type) && !friendNearby.target.dead && !this.shouldAvoidTarget(friendNearby.target)) { this.state = "seek_food"; this.target = friendNearby.target; this.foodReactTimer = Math.max(this.foodReactTimer, 0.24); @@ -831,7 +1033,7 @@ class Tarinai { } if (this.hunger > 62) { - const itemTypes = ["sweet", "grass"]; + const itemTypes = this.isZunchiSlave ? ["zunchi"] : ["sweet", "love_mochi", "fight_mochi", "grass"]; const item = this.world.nearest(this, itemTypes, 420); if (item) { this.state = "seek_food"; @@ -877,8 +1079,9 @@ class Tarinai { } interactWithItems(dt) { - const canBite = this.eatCooldown <= 0 && this.hunger > 18; - const canSweetBite = this.eatCooldown <= 0 && this.hunger > 7; + const canBite = !this.isZunchiSlave && this.eatCooldown <= 0 && this.hunger > 18; + const canSweetBite = !this.isZunchiSlave && this.eatCooldown <= 0 && this.hunger > 7; + const canZunchiBite = this.eatCooldown <= 0 && this.hunger > (this.isZunchiSlave ? 16 : 104); for (const it of this.world.nearbyItems(this.x, this.y, this.radius + 42)) { const d = dist(this, it); if (d > this.radius + it.r + 12) continue; @@ -917,6 +1120,42 @@ class Tarinai { } } + if (canSweetBite && (it.type === "love_mochi" || it.type === "fight_mochi") && it.amount > 1.2) { + const eating = Math.min(it.amount, 6.8); + this.state = "eat"; + this.target = it; + this.vx *= Math.pow(0.25, dt * 60); + this.vy *= Math.pow(0.25, dt * 60); + it.amount -= eating; + this.hunger -= eating * 0.82; + this.recoverHealth(eating * 0.24); + this.affection += eating * 0.04; + this.stress = clamp(this.stress - eating * 0.25, 0, 130); + if (it.type === "love_mochi") { + this.loveMochiTimer = Math.max(this.loveMochiTimer || 0, 42 + rand(4, 12)); + this.bubble("♡", 2.4, "rgba(190,82,132,0.78)"); + this.lastMealColor = "#ff7bab"; + } else { + this.fightMochiTimer = Math.max(this.fightMochiTimer || 0, 36 + rand(4, 10)); + this.bubble("!", 2.0, "rgba(180,86,52,0.80)"); + this.lastMealColor = "#e07c43"; + } + this.eatTimer = Math.max(this.eatTimer, 0.30); + this.eatCooldown = rand(0.38, 0.68); + this.foodReactTimer = Math.max(this.foodReactTimer, 0.16); + if (Math.random() < dt * 2.2) { + const mouth = this.mouthPosition(it); + this.world.spawnEatEffect(mouth.x, mouth.y, it.type === "love_mochi" ? "#ff7bab" : "#e07c43"); + } + this.makePoop(eating * 0.20); + if (this.world.time > this.nextEatSound) { audio.eat(); this.nextEatSound = this.world.time + 0.42; } + if (this.world.time - this.lastLog > 9 && Math.random() < 0.02) { + this.world.log(`${this.name}は${it.type === "love_mochi" ? "媚薬餅" : "喧嘩餅"}をしばらく味わった。`, "food"); + this.lastLog = this.world.time; + } + break; + } + if (canBite && it.type === "grass") { const bite = Math.min(it.amount, rand(4.2, 7.2)); this.state = "eat"; @@ -945,21 +1184,23 @@ class Tarinai { if (it.type === "zunchi") { const touch = clamp(1 - d / (this.radius + it.r + 12), 0, 1); - this.stress = clamp(this.stress + touch * dt * 3.4, 0, 130); - this.zunchiStain = 0; - if (canBite && this.hunger > 104) { - const bite = Math.min(it.amount, rand(3.4, 5.8)); + if (!this.isZunchiSlave) this.stress = clamp(this.stress + touch * dt * 3.4, 0, 130); + this.zunchiStain = clamp((this.zunchiStain || 0) + touch * dt * (this.isZunchiSlave ? 5.2 : 9.5), 0, 100); + if (canZunchiBite) { + const bite = Math.min(it.amount, this.isZunchiSlave ? rand(4.8, 7.4) : rand(3.4, 5.8)); this.state = "eat"; this.target = it; this.vx *= Math.pow(0.18, dt * 60); this.vy *= Math.pow(0.18, dt * 60); it.amount -= bite; - this.hunger -= bite * 0.18; - this.stress = clamp(this.stress + bite * 0.52, 0, 130); - this.zunchiStain = 0; + this.hunger -= bite * (this.isZunchiSlave ? 0.66 : 0.18); + this.recoverHealth(bite * (this.isZunchiSlave ? 0.10 : 0.02)); + this.stress = clamp(this.stress + bite * (this.isZunchiSlave ? 0.06 : 0.52), 0, 130); + this.zunchiStain = clamp((this.zunchiStain || 0) + bite * (this.isZunchiSlave ? 0.75 : 1.75), 0, 100); + this.lastMealColor = "#6b8d3f"; this.eatTimer = Math.max(this.eatTimer, 0.24); this.eatCooldown = rand(0.9, 1.4); - this.bubble("...", 2.2, "rgba(74,91,50,0.78)"); + this.bubble(this.isZunchiSlave ? "もぐ" : "...", 2.2, "rgba(74,91,50,0.78)"); const mouth = this.mouthPosition(it); this.world.spawnEatEffect(mouth.x, mouth.y, "#6b8d3f"); if (this.world.time > this.nextEatSound) { audio.eat(); this.nextEatSound = this.world.time + 0.7; } @@ -971,6 +1212,13 @@ class Tarinai { this.stress -= dt * 2.4; this.energy += dt * 0.5; this.hunger -= dt * 0.2; + this.zunchiStain = clamp((this.zunchiStain || 0) - dt * 10.5, 0, 100); + if (this.zunchiDisease) { + this.zunchiDiseaseSeverity = Math.max(0, (this.zunchiDiseaseSeverity || 1) - dt * (this.energy > 70 ? 0.050 : 0.028)); + if (this.zunchiDiseaseSeverity <= 0.04 && this.recoverZunchiDisease("水でずんち病が治まった")) { + this.world.log(`${this.name}は水でずんち病が治まった。`, "accident"); + } + } it.amount -= dt * 2.4; if (this.type === "teary" || this.type === "cry") this.affection += dt * 0.18; } @@ -981,15 +1229,17 @@ class Tarinai { if (this.need === "\u3042\u305f\u305f\u304b\u3055") this.affection += dt * 0.25; } - if (it.type === "bed") { + if (this.isSleepFurniture(it)) { if (this.energy < 74 + 6 * this.personalityProfile().sleep) { const occ = this.world.bedOccupancy(it); const comfort = this.world.bedComfort(it); this.state = "sleep"; + this.target = it; this.sleeping = true; if (this.world.time > this.nextSleepBubbleAt) { this.nextSleepBubbleAt = this.world.time + 4.5; - this.world.spawnBubble(this.x, this.y - this.radius * 1.18, occ > 5 ? "mm..." : "Zzz...", "rgba(65,70,92,0.72)"); + const crowded = occ > 5; + this.world.spawnBubble(this.x, this.y - this.radius * 1.18, crowded ? "mm..." : "Zzz...", "rgba(65,70,92,0.72)"); } this.energy += dt * (1.9 + comfort * 1.4); this.stress += dt * (occ > 5 ? 0.55 : -1.3 * comfort); @@ -997,6 +1247,16 @@ class Tarinai { } } + if (it.type === "ball") { + const touch = clamp(1 - d / (this.radius + it.r + 12), 0, 1); + if (this.state === "play_ball" || this.target === it || this.personality === "playful") { + this.goodMode = "smile"; + this.stress = clamp(this.stress - dt * (0.9 + touch * 1.8), 0, 130); + this.loneliness = clamp(this.loneliness - dt * (0.4 + touch), 0, 110); + if (this.state === "play_ball") this.foodReactTimer = Math.max(this.foodReactTimer, 0.10); + } + } + if (it.type === "trace") { this.loneliness -= dt * 0.25; this.stress += dt * 0.06; @@ -1017,12 +1277,24 @@ class Tarinai { if (d < 0.01 || d > 74) continue; closeCount += 1; + if (this.zunchiDisease && !o.zunchiDisease && (o.zunchiDiseaseCooldown || 0) <= 0 && d < 54 && Math.random() < dt * 0.065 * clamp(1 - d / 58, 0.12, 1)) { + if (o.infectZunchiDisease(this)) this.world.log(`${this.name}のずんち病が${o.name}にうつった。`, "accident"); + } + if (o.zunchiDisease && !this.zunchiDisease && (this.zunchiDiseaseCooldown || 0) <= 0 && d < 54 && Math.random() < dt * 0.065 * clamp(1 - d / 58, 0.12, 1)) { + if (this.infectZunchiDisease(o)) this.world.log(`${o.name}のずんち病が${this.name}にうつった。`, "accident"); + } + + const pairBond = this.world.areCoParents?.(this, o); const overlap = this.radius + o.radius - d; if (overlap > -2) { - const push = (overlap + 6) * 0.17; + const push = (overlap + 6) * (pairBond ? 0.08 : 0.17); this.vx -= (dx / d) * push; this.vy -= (dy / d) * push; - if (Math.random() < dt * 1.2) this.surpriseTimer = Math.max(this.surpriseTimer, 0.12); + if (!pairBond && Math.random() < dt * 1.2) this.surpriseTimer = Math.max(this.surpriseTimer, 0.12); + } + if (pairBond && d > 26 && d < 120) { + this.vx += (dx / d) * dt * 4.8; + this.vy += (dy / d) * dt * 4.8; } if (d < 60) { @@ -1077,19 +1349,24 @@ class Tarinai { const relA = this.relationTo(o.id); const relB = o.relationTo(this.id); - const fightRisk = (this.mood < 55 || o.mood < 55 || this.stress > 46 || o.stress > 46 || this.type === "angry" || o.type === "angry" || closeCount > 3 || this.personality === "irritable" || o.personality === "irritable"); - const fearBrake = clamp(1 - ((relA.fear || 0) + (relB.fear || 0)) / 120, 0.34, 1); - const friendBrake = clamp(1 - Math.max(relA.affinity || 0, relB.affinity || 0) / 90, 0.48, 1); + const battleDrug = this.hasFightMochiEffect() || o.hasFightMochiEffect(); + const loveDrug = this.hasLoveMochiEffect() || o.hasLoveMochiEffect(); + const fightRisk = (this.mood < 55 || o.mood < 55 || this.stress > 46 || o.stress > 46 || this.type === "angry" || o.type === "angry" || closeCount > 3 || this.personality === "irritable" || o.personality === "irritable" || battleDrug || o.isZunchiSlave || this.isZunchiSlave); + const fearBrake = battleDrug ? clamp(1 - ((relA.fear || 0) + (relB.fear || 0)) / 220, 0.74, 1.15) : clamp(1 - ((relA.fear || 0) + (relB.fear || 0)) / 120, 0.34, 1); + const friendBrake = battleDrug ? clamp(1 - Math.max(relA.affinity || 0, relB.affinity || 0) / 180, 0.72, 1.12) : clamp(1 - Math.max(relA.affinity || 0, relB.affinity || 0) / 90, 0.48, 1); const personalityRisk = this.personalityProfile().fight * o.personalityProfile().fight; + const conflictBias = (battleDrug ? 2.9 : 1) * (o.isZunchiSlave ? 2.2 : 1) * (this.isZunchiSlave ? 1.15 : 1); const birthPeace = (this.postBirthPeaceTimer || 0) > 0 || (o.postBirthPeaceTimer || 0) > 0; - if (d < 56 && !birthPeace && fightRisk && this.fightCooldown <= 0 && o.fightCooldown <= 0 && Math.random() < dt * 0.23 * personalityRisk * fearBrake * friendBrake) { + const familyFightBlocked = this.world.areParentChild ? this.world.areParentChild(this, o) : false; + const pairFightBlocked = this.world.areCoParents ? this.world.areCoParents(this, o) : false; + if (d < 56 && !familyFightBlocked && !pairFightBlocked && !birthPeace && fightRisk && this.fightCooldown <= 0 && o.fightCooldown <= 0 && Math.random() < dt * 0.23 * personalityRisk * fearBrake * friendBrake * conflictBias) { this.world.startConflict(this, o); } - if (d < 36 && this.reproductionTimer <= 0 && o.reproductionTimer <= 0 && this.fightTimer <= 0 && o.fightTimer <= 0) { - const enough = this.hunger < 55 && o.hunger < 60 && this.energy > 44 && o.energy > 44; - const lonelyBond = this.loneliness > 24 || o.loneliness > 24; - if (enough && lonelyBond && Math.random() < dt * 0.25) { + if (!this.isZunchiSlave && !o.isZunchiSlave && d < (loveDrug ? 44 : 36) && this.reproductionTimer <= 0 && o.reproductionTimer <= 0 && this.fightTimer <= 0 && o.fightTimer <= 0) { + const enough = loveDrug ? (this.hunger < 90 && o.hunger < 92 && this.energy > 18 && o.energy > 18) : (this.hunger < 55 && o.hunger < 60 && this.energy > 44 && o.energy > 44); + const lonelyBond = loveDrug ? (this.loneliness > 4 || o.loneliness > 4 || this.affection > 0 || o.affection > 0) : (this.loneliness > 24 || o.loneliness > 24); + if (enough && lonelyBond && Math.random() < dt * (loveDrug ? 0.72 : 0.25)) { this.world.startBirthRitual(this, o); } } @@ -1108,23 +1385,27 @@ class Tarinai { move(dt) { let ax = 0, ay = 0; - const baseSpeed = 31 * this.trait.speed * (this.energy < 20 ? 0.42 : 1) * (this.fallTimer > 0 ? 0.35 : 1); + const baseSpeed = 46.5 * 1.5 * this.trait.speed * (this.energy < 20 ? 0.42 : 1) * (this.fallTimer > 0 ? 0.35 : 1); if (this.state === "sleep") { - const bedComfort = this.target?.type === "bed" ? this.world.bedComfort(this.target) : 0.72; - const bedCrowd = this.target?.type === "bed" ? this.world.bedOccupancy(this.target) : 0; + const bedComfort = this.isSleepFurniture(this.target) ? this.world.bedComfort(this.target) : 0.72; + const bedCrowd = this.isSleepFurniture(this.target) ? this.world.bedOccupancy(this.target) : 0; + const crowdedSleep = bedCrowd > 5; this.energy = clamp(this.energy + dt * (this.target ? 1.8 + bedComfort * 1.35 : 2.1), 0, 100); - this.stress = clamp(this.stress + dt * (bedCrowd > 5 ? 0.18 * (bedCrowd - 5) : -0.12 * bedComfort), 0, 130); + this.stress = clamp(this.stress + dt * (crowdedSleep ? 0.18 * Math.max(1, bedCrowd - 5) : -0.12 * bedComfort), 0, 130); this.hunger = clamp(this.hunger + dt * 0.018, 0, 115); - if (this.target && !this.target.dead && this.target.type === "bed") { + if (this.target && !this.target.dead && this.isSleepFurniture(this.target)) { const spot = this.sleepSpotFor(this.target); const dx = spot.x - this.x; const dy = spot.y - this.y; const d = Math.hypot(dx, dy); - if (d > 1.6) { + if (d > 2.2) { const step = Math.min(d, dt * (12 + bedComfort * 14)); this.x += dx / d * step; this.y += dy / d * step; + } else { + this.x = spot.x; + this.y = spot.y; } } this.vx *= Math.pow(0.55, dt * 60); @@ -1137,15 +1418,21 @@ class Tarinai { if (partner) { const midX = (this.x + partner.x) / 2; const midY = (this.y + partner.y) / 2; - const offset = (this.birthRitualRole || 1) * this.radius * 0.44; - const dx = midX - this.x; - const dy = (midY + offset) - this.y; - this.vx += dx * dt * 2.8; - this.vy += dy * dt * 2.8; + const pdx = partner.x - this.x; + const pdy = partner.y - this.y; + const plen = Math.max(0.001, Math.hypot(pdx, pdy)); + const nx = -pdy / plen; + const ny = pdx / plen; + const desiredGap = this.radius * 0.36; + const side = this.birthRitualRole || 1; + const tx = midX + nx * desiredGap * side; + const ty = midY + ny * desiredGap * side; + this.vx += (tx - this.x) * dt * 3.8; + this.vy += (ty - this.y) * dt * 3.8; } - this.vx *= Math.pow(0.56, dt * 60); - this.vy *= Math.pow(0.56, dt * 60); - this.x += Math.sin(this.world.time * 26 + this.age) * 0.32; + this.vx *= Math.pow(0.48, dt * 60); + this.vy *= Math.pow(0.48, dt * 60); + this.x += Math.sin(this.world.time * 26 + this.age) * 0.20; return; } @@ -1171,9 +1458,20 @@ class Tarinai { } if (this.target && !this.target.dead) { - const dx = this.target.x - this.x; - const dy = this.target.y - this.y; + const moveTarget = this.state === "seek_bed" && this.isSleepFurniture(this.target) ? this.sleepSpotFor(this.target) : this.target; + const dx = moveTarget.x - this.x; + const dy = moveTarget.y - this.y; const d = Math.hypot(dx, dy) || 1; + if (this.state === "seek_bed" && this.isSleepFurniture(this.target)) { + const bedDx = this.target.x - this.x; + const bedDy = this.target.y - this.y; + const bedDist = Math.hypot(bedDx, bedDy); + if (d <= Math.max(this.radius + 8, 26) || bedDist <= Math.max(this.target.r * 0.58, 20)) { + this.state = "sleep"; + this.sleeping = true; + return; + } + } let sign = (this.state === "panic" || this.state === "cursor_enemy") ? -1 : 1; if (this.target?.detour) sign = 1; let strength = 1; @@ -1186,6 +1484,9 @@ class Tarinai { } else if (this.state === "cursor_enemy") { strength = d < 150 ? 1.35 : 0.35; sign = -1; + } else if (this.state === "play_ball") { + strength = d < this.radius + 18 ? 1.75 : 1.28; + sign = 1; } else if (this.state === "fight") { const headbutt = Math.sin(this.world.time * 18 + this.age * 0.37); if (d < 18) { @@ -1202,6 +1503,10 @@ class Tarinai { this.wanderAngle += rand(-3.8, 3.8) * dt; ax += Math.cos(this.wanderAngle) * baseSpeed * 1.5; ay += Math.sin(this.wanderAngle) * baseSpeed * 1.5; + } else if (this.state === "zunchi_sick") { + this.wanderAngle += rand(-2.4, 2.4) * dt + Math.sin(this.world.time * 1.7 + this.age) * dt * 0.8; + ax += Math.cos(this.wanderAngle) * baseSpeed * 0.72; + ay += Math.sin(this.wanderAngle) * baseSpeed * 0.72; } else { ax += Math.cos(this.wanderAngle) * baseSpeed * 0.34; ay += Math.sin(this.wanderAngle) * baseSpeed * 0.34; @@ -1212,7 +1517,7 @@ class Tarinai { this.vx *= Math.pow(0.84, dt * 8); this.vy *= Math.pow(0.84, dt * 8); - const maxV = this.state === "panic" ? 116 : 76; + const maxV = this.state === "panic" ? 116 : (this.state === "zunchi_sick" ? 54 : 76); const v = Math.hypot(this.vx, this.vy); if (v > maxV) { this.vx = this.vx / v * maxV; @@ -1222,11 +1527,19 @@ class Tarinai { this.x += this.vx * dt; this.y += this.vy * dt; - const p = CONFIG.worldPadding; + if (this.entryTimer > 0) { + this.entryTimer = Math.max(0, this.entryTimer - dt); + if (this.x >= CONFIG.worldPadding && this.y >= CONFIG.worldPadding && this.x <= this.world.w - CONFIG.worldPadding && this.y <= this.world.h - CONFIG.worldPadding) { + this.entryTimer = 0; + } + } + const p = this.entryTimer > 0 ? -this.radius * 2.4 : CONFIG.worldPadding; + const maxX = this.entryTimer > 0 ? this.world.w + this.radius * 2.4 : this.world.w - CONFIG.worldPadding; + const maxY = this.entryTimer > 0 ? this.world.h + this.radius * 2.4 : this.world.h - CONFIG.worldPadding; if (this.x < p) { this.x = p; this.vx = Math.abs(this.vx) * 0.5; this.wanderAngle = rand(-0.8, 0.8); this.surpriseTimer = 0.15; } if (this.y < p) { this.y = p; this.vy = Math.abs(this.vy) * 0.5; this.wanderAngle = rand(0.3, 1.8); this.surpriseTimer = 0.15; } - if (this.x > this.world.w - p) { this.x = this.world.w - p; this.vx = -Math.abs(this.vx) * 0.5; this.wanderAngle = rand(2.5, 3.8); this.surpriseTimer = 0.15; } - if (this.y > this.world.h - p) { this.y = this.world.h - p; this.vy = -Math.abs(this.vy) * 0.5; this.wanderAngle = rand(-2.2, -0.7); this.surpriseTimer = 0.15; } + if (this.x > maxX) { this.x = maxX; this.vx = -Math.abs(this.vx) * 0.5; this.wanderAngle = rand(2.5, 3.8); this.surpriseTimer = 0.15; } + if (this.y > maxY) { this.y = maxY; this.vy = -Math.abs(this.vy) * 0.5; this.wanderAngle = rand(-2.2, -0.7); this.surpriseTimer = 0.15; } } checkLife() { @@ -1277,8 +1590,13 @@ class Tarinai { const blastSpin = this.blastSpinTimer > 0.04 ? this.fallDir * ((this.blastSpinMax || 1) - this.blastSpinTimer) * Math.PI * 70.0 : 0; const fallSpin = this.blastSpinTimer > 0.04 ? blastSpin : (fallPose > 0 ? this.fallDir * (1 - fallPose) * Math.PI * 2 : 0); const motionTilt = clamp(this.vx / 240, -0.22, 0.22) + (this.state === "fight" ? headbuttPulse * 0.05 : 0) + eatingShake * 0.105 + this.fallDir * fallPose * 0.18 + fallSpin; + const insideCardboard = false; let drawW = w * this.scale * 0.30; let drawH = h * this.scale * 0.30; + if (insideCardboard) { + drawW *= 0.72; + drawH *= 0.72; + } if (sprite === "stretch") { drawW *= 1.16; drawH *= 1.24; @@ -1324,6 +1642,7 @@ class Tarinai { ctx.save(); ctx.translate(this.x + intimidateShake, this.y + bob + fearShakeY); ctx.rotate(angle); + if (insideCardboard) ctx.globalAlpha *= 0.58; if (this.world.selected === this) { ctx.save(); @@ -1375,13 +1694,16 @@ class Tarinai { if ((this.world.performanceLevel ? this.world.performanceLevel() : 0) < 2 && (this.goodMode === "smile" || this.state === "cursor_friend") && this.mood > 58 && this.stress < 44 && this.state !== "sleep") { ctx.save(); - ctx.globalAlpha = this.state === "cursor_friend" ? 0.82 : 0.34; + const cursorLove = this.state === "cursor_friend" ? Math.max(0, this.cursorPetting || 0) : 0; + ctx.globalAlpha = this.state === "cursor_friend" ? clamp(0.82 + cursorLove * 0.16, 0.82, 1) : 0.34; ctx.fillStyle = this.state === "cursor_friend" ? "#f05b87" : (lightState.nightStrength > 0.20 ? "#eef5ff" : (lightState.goldenStrength > 0.35 ? "#ffe398" : "#d6f5a9")); - for (let i = 0; i < 2; i++) { - const px = Math.cos(t * 2.2 + this.age + i * 2.4) * drawW * 0.34; - const py = -drawH * 0.24 + Math.sin(t * 2.9 + this.age * 0.2 + i) * drawH * 0.11; + const count = this.state === "cursor_friend" ? (cursorLove > 0.18 ? 5 : 3) : 2; + for (let i = 0; i < count; i++) { + const orbit = 0.24 + (i % 3) * 0.08 + cursorLove * 0.05; + const px = Math.cos(t * 2.2 + this.age + i * 1.42) * drawW * orbit; + const py = -drawH * (0.22 + (i % 2) * 0.06) + Math.sin(t * 2.9 + this.age * 0.2 + i) * drawH * (0.08 + cursorLove * 0.02); if (this.state === "cursor_friend") { - const s = Math.max(5, drawW * 0.075); + const s = Math.max(5, drawW * (0.070 + (i % 3) * 0.012 + cursorLove * 0.01)); drawHeartShape(ctx, px, py, s, { fill: "rgba(240,91,135,0.90)", stroke: "rgba(255,252,246,0.92)", alpha: 1, rotation: Math.sin(t * 3 + i) * 0.18 }); } else { ctx.beginPath(); @@ -1402,7 +1724,24 @@ class Tarinai { const cacheH = Math.max(1, Math.round((dh * renderScale) / 8) * 8); const spriteCanvas = typeof getCachedSpriteCanvas === "function" ? getCachedSpriteCanvas(sprite, img, cacheW, cacheH) : img; ctx.drawImage(spriteCanvas, -dw / 2, -dh / 2, dw, dh); - this.zunchiStain = 0; + if ((this.zunchiStain || 0) > 3 && typeof getCachedStainOverlayCanvas === "function") { + const stainCanvas = getCachedStainOverlayCanvas(sprite, img, cacheW, cacheH, clamp((this.zunchiStain || 0) / 100, 0, 1), this.zunchiStainSeed || 0.5); + if (stainCanvas) { + ctx.save(); + ctx.globalAlpha = clamp((this.zunchiStain || 0) / 100, 0.10, 0.88); + ctx.drawImage(stainCanvas, -dw / 2, -dh / 2, dw, dh); + ctx.restore(); + } + } + if (this.zunchiDisease) { + ctx.save(); + ctx.globalAlpha = 0.12 + clamp(this.zunchiDiseaseSeverity || 0, 0, 1) * 0.10; + ctx.fillStyle = "rgba(9, 74, 23, 0.75)"; + ctx.beginPath(); + ctx.ellipse(0, drawH * 0.05, drawW * 0.56, drawH * 0.38, Math.sin(t + this.age) * 0.12, 0, Math.PI * 2); + ctx.fill(); + ctx.restore(); + } drawDawnRimLight(ctx, dw, dh, lightState); if (faceRight) ctx.scale(-1, 1); diff --git a/js/ui.js b/js/ui.js index 1f0de71..a7f7f53 100644 --- a/js/ui.js +++ b/js/ui.js @@ -5,10 +5,12 @@ const ctx = canvas.getContext("2d"); const ui = { pauseBtn: document.getElementById("pauseBtn"), speedBtn: document.getElementById("speedBtn"), + ecologyBtn: document.getElementById("ecologyBtn"), soundBtn: document.getElementById("soundBtn"), - saveBtn: document.getElementById("saveBtn"), - loadBtn: document.getElementById("loadBtn"), + creditsBtn: document.getElementById("creditsBtn"), resetBtn: document.getElementById("resetBtn"), + creditsDialog: document.getElementById("creditsDialog"), + creditsCloseBtn: document.getElementById("creditsCloseBtn"), toolPalette: document.getElementById("toolPalette"), toolCard: document.getElementById("toolCard"), colonyChartTabs: document.getElementById("colonyChartTabs"), @@ -20,8 +22,14 @@ const ui = { colonyChart: document.getElementById("colonyChart"), colonyChartLegend: document.getElementById("colonyChartLegend"), archiveContent: document.getElementById("archiveContent"), + archiveResetBtn: document.getElementById("archiveResetBtn"), + archiveResetDialog: document.getElementById("archiveResetDialog"), + archiveResetCancelBtn: document.getElementById("archiveResetCancelBtn"), + archiveResetConfirmBtn: document.getElementById("archiveResetConfirmBtn"), fieldDialog: document.getElementById("fieldDialog"), fieldCancelBtn: document.getElementById("fieldCancelBtn"), + ecologyDialog: document.getElementById("ecologyDialog"), + ecologyCloseBtn: document.getElementById("ecologyCloseBtn"), }; const uiCache = { @@ -65,7 +73,7 @@ function renderLog(logs) { time.textContent = formatLogTime(entry.time || 0); div.appendChild(tag); div.appendChild(time); - div.append(document.createTextNode(entry.text || "")); + div.append(document.createTextNode(formatLogDisplayText(entry.text || ""))); frag.appendChild(div); } ui.log.appendChild(frag); @@ -120,14 +128,21 @@ function renderSelected() { fmt(t.age), fmt(t.lifeSpan), fmt(t.hunger), fmt(t.loneliness), fmt(t.energy), fmt(t.stress), fmt(t.mood), t.parentNames?.join("+") || "", t.children?.length || 0, fmt(t.lack), - rel.friend?.id || "", rel.friend?.score || 0, rel.friend?.id ? tarinaiSpritePathForId(rel.friend.id) : "", rel.fear?.id || "", rel.fear?.score || 0, rel.fear?.id ? tarinaiSpritePathForId(rel.fear.id) : "", fightRecord + rel.friend?.id || "", rel.friend?.score || 0, rel.friend?.id ? tarinaiSpritePathForId(rel.friend.id) : "", rel.fear?.id || "", rel.fear?.score || 0, rel.fear?.id ? tarinaiSpritePathForId(rel.fear.id) : "", fightRecord, + t.isZunchiSlave ? 1 : 0, fmt(t.totalFightLosses || 0), fmt(t.loveMochiTimer || 0), fmt(t.fightMochiTimer || 0), + t.zunchiDisease ? 1 : 0, fmt(t.zunchiDiseaseSeverity || 0), fmt(t.zunchiStain || 0) ].join("|"); if (uiCache.selectedSnapshot === snapshot) return; uiCache.selectedSnapshot = snapshot; ui.selectedInfo.className = "selected-info"; + const effectBadges = []; + if (t.isZunchiSlave) effectBadges.push(`ずんちどれい`); + if (t.zunchiDisease) effectBadges.push(`ずんち病 ${fmt((t.zunchiDiseaseSeverity || 0) * 100)}`); + if ((t.loveMochiTimer || 0) > 0.1) effectBadges.push(`媚薬餅 ${fmt(t.loveMochiTimer)}`); + if ((t.fightMochiTimer || 0) > 0.1) effectBadges.push(`喧嘩餅 ${fmt(t.fightMochiTimer)}`); ui.selectedInfo.innerHTML = ` -
${escapeHtml(t.label())}${escapeHtml(t.name)}${(t.displayGeneration ? t.displayGeneration() : ((t.hasPaired || t.parents?.length || t.children?.length) ? t.generation : null)) ? `\u4e16\u4ee3 ${t.generation}` : ""}
+
${escapeHtml(t.label())}${escapeHtml(t.name)}${(t.displayGeneration ? t.displayGeneration() : ((t.hasPaired || t.parents?.length || t.children?.length) ? t.generation : null)) ? `\u4e16\u4ee3 ${t.generation}` : ""}${effectBadges.join("")}
\u6027\u683c${escapeHtml(t.personalityLabel ? t.personalityLabel() : t.personality || "\u4e0d\u660e")}
\u72b6\u614b${escapeHtml(stateLabel(t.state))}
\u7406\u7531${escapeHtml(reasonLabel(t))}
@@ -137,10 +152,12 @@ function renderSelected() {
\u5bc2\u3057\u3055${fmt(t.loneliness)}
\u4f53\u529b${fmt(t.energy)}
\u30b9\u30c8\u30ec\u30b9${fmt(t.stress)}
+
ずんち汚れ${fmt(t.zunchiStain || 0)}
\u6c17\u5206${fmt(t.mood)}
\u4ef2\u826f\u3057${rel.friend ? `${relationIconHtml(rel.friend.id)}${escapeHtml(`${rel.friend.name} +${fmt(rel.friend.score)}`)}` : "\u306a\u3057"}
\u907f\u3051\u308b\u76f8\u624b${rel.fear ? `${relationIconHtml(rel.fear.id)}${escapeHtml(`${rel.fear.name} ${fmt(rel.fear.score)}`)}` : "\u306a\u3057"}
\u55a7\u5629\u8a18\u61b6${escapeHtml(fightRecord)}
+
\u7d2f\u8a08\u6557\u5317${t.totalFightLosses || 0}
\u89aa${escapeHtml(t.parentNames?.length ? t.parentNames.join(" + ") : "\u4e0d\u660e")}
\u5b50${t.children?.length || 0}
\u305f\u308a\u306a\u3044${fmt(t.lack)}
@@ -151,7 +168,8 @@ function stateLabel(s) { return { idle: "\u5f85\u6a5f", seek_food: "\u98df\u3079\u7269\u3078\u5411\u304b\u3046", - seek_bed: "\u5e72\u8349\u5bdd\u5e8a\u3078\u5411\u304b\u3046", + seek_bed: "寝床へ向かう", + play_ball: "ボール遊び", seek_friend: "\u4ef2\u9593\u3092\u63a2\u3059", follow_parent: "\u89aa\u306b\u3064\u3044\u3066\u3044\u304f", cursor_friend: "\u30ab\u30fc\u30bd\u30eb\u306b\u3064\u3044\u3066\u3044\u304f", @@ -180,7 +198,7 @@ function reasonLabel(t) { function targetLabel(target) { if (!target) return "\u306a\u3057"; if (target.name) return target.name; - if (target.type) return { sweet: "\u305a\u3093\u3060\u9905", food: "\u98df\u4e8b", grass: "\u8349", water: "\u6c34", bed: "\u5e72\u8349\u5bdd\u5e8a", stone: "\u77f3", zunchi: "\u305a\u3093\u3061", trace: "\u8db3\u8de1", splat: "\u3057\u3076\u304d" }[target.type] || target.type; + if (target.type) return { sweet: "\u305a\u3093\u3060\u9905", love_mochi: "\u5a9a\u85ac\u9905", fight_mochi: "\u55a7\u5629\u9905", food: "\u98df\u4e8b", grass: "\u8349", water: "\u6c34", bed: "\u5e72\u8349\u5bdd\u5e8a", ball: "\u30dc\u30fc\u30eb", stone: "\u77f3", zunchi: "\u305a\u3093\u3061", trace: "\u8db3\u8de1", splat: "\u3057\u3076\u304d" }[target.type] || target.type; if (Number.isFinite(target.x) && Number.isFinite(target.y)) return "\u30ab\u30fc\u30bd\u30eb"; return "\u4e0d\u660e"; } @@ -218,20 +236,41 @@ function escapeHtml(value) { "'": "'" }[c])); } + +function escapeRegExp(value) { + return String(value ?? "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + + +function formatLogDisplayText(text) { + let out = String(text ?? ""); + const names = new Set(); + for (const t of world?.tarinai || []) if (t?.name) names.add(t.name); + for (const n of Object.values(world?.family || {})) if (n?.name) names.add(n.name); + const sorted = [...names].filter(Boolean).sort((a, b) => b.length - a.length); + for (const name of sorted) { + const re = new RegExp(`(^|[^「])(${escapeRegExp(name)})(?=[とはがをにのもへからで、。]|$)`, "g"); + out = out.replace(re, (m, prefix, found) => `${prefix}「${found}」`); + } + return out; +} function resizeCanvas() { const rect = canvas.getBoundingClientRect(); const perf = world?.performanceLevel ? world.performanceLevel() : 0; const maxDpr = perf >= 2 ? 1 : (perf === 1 ? 1.5 : 2); const dpr = Math.max(1, Math.min(window.devicePixelRatio || 1, maxDpr)); - const nextW = Math.floor(rect.width * dpr); - const nextH = Math.floor(rect.height * dpr); + const nextW = Math.max(1, Math.floor(rect.width * dpr)); + const nextH = Math.max(1, Math.floor(rect.height * dpr)); if (canvas.width === nextW && canvas.height === nextH && uiCache.canvasDpr === dpr) return; uiCache.canvasDpr = dpr; canvas.width = nextW; canvas.height = nextH; ctx.setTransform(dpr, 0, 0, dpr, 0, 0); - world.setViewportSize(rect.width, rect.height); + ctx.fillStyle = "#cfe3d3"; + ctx.fillRect(0, 0, Math.max(1, rect.width), Math.max(1, rect.height)); + world.setViewportSize(Math.max(1, rect.width), Math.max(1, rect.height)); uiCache.lastChartDraw = ""; + if (typeof render === "function") render(); } function applyFieldLayout(fieldType = world.fieldType || "garden") { @@ -255,6 +294,22 @@ function closeFieldDialog() { ui.fieldDialog?.classList.add("hidden"); } +function openEcologyDialog() { + ui.ecologyDialog?.classList.remove("hidden"); +} + +function closeEcologyDialog() { + ui.ecologyDialog?.classList.add("hidden"); +} + +function openArchiveResetDialog() { + ui.archiveResetDialog?.classList.remove("hidden"); +} + +function closeArchiveResetDialog() { + ui.archiveResetDialog?.classList.add("hidden"); +} + function resetWithField(fieldType) { const field = FIELD_TYPES?.[fieldType] || FIELD_TYPES.garden; closeFieldDialog(); @@ -282,7 +337,7 @@ function resizeFieldFromWheel(e) { const sx = e.clientX - rect.left; const sy = e.clientY - rect.top; const focus = world.screenToWorld ? world.screenToWorld(sx, sy) : { x: sx, y: sy }; - const factor = e.deltaY > 0 ? 1.08 : 1 / 1.08; + const factor = e.deltaY > 0 ? 1 / 1.08 : 1.08; if (!world.setFieldZoom((world.fieldZoom || 1) * factor)) return; const off = world.fieldScreenOffset ? world.fieldScreenOffset() : { x: 0, y: 0 }; const scale = world.viewScale ? world.viewScale() : 1; @@ -298,7 +353,7 @@ function selectTool(tool) { btn.classList.toggle("selected", btn.dataset.tool === tool); } syncToolSizeBadges(); - canvas.style.cursor = tool === "observe" ? "default" : (tool === "poke" || tool === "delete" ? "pointer" : "crosshair"); + canvas.style.cursor = tool === "observe" ? "default" : (tool === "pinch" ? "grab" : (tool === "poke" || tool === "delete" ? "pointer" : "crosshair")); } function showToast(text) { @@ -308,33 +363,12 @@ function showToast(text) { showToast._timer = setTimeout(() => ui.toast.classList.add("hidden"), 1400); } -function saveGame() { - localStorage.setItem(CONFIG.saveKey, JSON.stringify(world.serialize())); - showToast("\u4fdd\u5b58\u3057\u307e\u3057\u305f\u3002"); +function openCreditsDialog() { + ui.creditsDialog?.classList.remove("hidden"); } -function loadGame() { - const raw = localStorage.getItem(CONFIG.saveKey); - if (!raw) { showToast("\u30bb\u30fc\u30d6\u30c7\u30fc\u30bf\u304c\u3042\u308a\u307e\u305b\u3093\u3002"); return; } - try { - const t0 = performance.now(); - world.load(JSON.parse(raw)); - applyFieldLayout(world.fieldType || "garden"); - uiCache.archiveVersion = ""; - uiCache.deferArchiveUntil = performance.now() + 80; - if (ui.archiveContent) ui.archiveContent.innerHTML = `
\u5bb6\u7cfb\u56f3\u3092\u8aad\u307f\u8fbc\u3093\u3067\u3044\u307e\u3059\u3002
`; - renderSelected(); - renderStats(); - requestAnimationFrame(() => { - uiCache.deferArchiveUntil = 0; - renderArchive(); - }); - if (typeof console !== "undefined") console.debug?.(`loaded save in ${Math.round(performance.now() - t0)}ms`); - showToast("\u8aad\u307f\u8fbc\u307f\u307e\u3057\u305f\u3002"); - } catch (e) { - console.error(e); - showToast("\u8aad\u307f\u8fbc\u307f\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002"); - } +function closeCreditsDialog() { + ui.creditsDialog?.classList.add("hidden"); } function syncTopButtons() { @@ -343,7 +377,7 @@ function syncTopButtons() { if (ui.soundBtn) ui.soundBtn.textContent = audio.enabled ? "\u97f3 ON" : "\u97f3 OFF"; } -const SCALABLE_TOOLS = new Set(["zunchi", "sweet", "grass", "stone", "firecracker", "fence_v", "fence_h"]); +const SCALABLE_TOOLS = new Set(["zunchi", "sweet", "love_mochi", "fight_mochi", "grass", "stone", "firecracker", "fence_v", "fence_h"]); const SIZE_ORDER = ["small", "medium", "large"]; const SIZE_LABELS = { small: "\u5c0f", medium: "\u4e2d", large: "\u5927" }; @@ -400,6 +434,20 @@ function bindUI() { }); syncToolSizeBadges(); ui.archiveContent?.addEventListener("scroll", scheduleArchiveWindowRender, { passive: true }); + const bindArchiveHorizontal = (root) => { + if (!root) return; + root.addEventListener("wheel", (e) => { + const scroller = e.target.closest(".lineage-family-tree, .lineage-main-content"); + if (!scroller) return; + const canX = scroller.scrollWidth > scroller.clientWidth + 4; + if (!canX) return; + const dominant = Math.abs(e.deltaX) > Math.abs(e.deltaY) ? e.deltaX : (e.shiftKey ? e.deltaY : 0); + if (!dominant) return; + e.preventDefault(); + scroller.scrollLeft += dominant; + }, { passive: false }); + }; + bindArchiveHorizontal(ui.archiveContent); ui.archiveContent?.addEventListener("click", (e) => { const node = e.target.closest("[data-family-tarinai-id]"); if (!node) return; @@ -422,13 +470,17 @@ function bindUI() { world.speed = speeds[(i + 1) % speeds.length]; syncTopButtons(); }); + ui.ecologyBtn?.addEventListener("click", openEcologyDialog); ui.soundBtn?.addEventListener("click", () => { const on = audio.toggle(); ui.soundBtn.textContent = on ? "\u97f3 ON" : "\u97f3 OFF"; showToast(on ? "\u97f3\u3092\u6709\u52b9\u306b\u3057\u307e\u3057\u305f\u3002" : "\u97f3\u3092\u7121\u52b9\u306b\u3057\u307e\u3057\u305f\u3002"); }); - ui.saveBtn.addEventListener("click", saveGame); - ui.loadBtn.addEventListener("click", loadGame); + ui.creditsBtn?.addEventListener("click", openCreditsDialog); + ui.creditsCloseBtn?.addEventListener("click", closeCreditsDialog); + ui.creditsDialog?.addEventListener("click", (e) => { + if (e.target === ui.creditsDialog) closeCreditsDialog(); + }); ui.resetBtn.addEventListener("click", () => { openFieldDialog(); }); @@ -439,6 +491,23 @@ function bindUI() { if (!btn) return; resetWithField(btn.dataset.fieldType || "garden"); }); + ui.ecologyCloseBtn?.addEventListener("click", closeEcologyDialog); + ui.ecologyDialog?.addEventListener("click", (e) => { + if (e.target === ui.ecologyDialog) closeEcologyDialog(); + }); + ui.archiveResetBtn?.addEventListener("click", openArchiveResetDialog); + ui.archiveResetCancelBtn?.addEventListener("click", closeArchiveResetDialog); + ui.archiveResetDialog?.addEventListener("click", (e) => { + if (e.target === ui.archiveResetDialog) closeArchiveResetDialog(); + }); + ui.archiveResetConfirmBtn?.addEventListener("click", () => { + world.resetFamilyTree?.(); + uiCache.archiveVersion = ""; + closeArchiveResetDialog(); + renderArchive(); + renderSelected(); + showToast("家系図をリセットしました。"); + }); ui.toolPalette.addEventListener("click", (e) => { const toggle = e.target.closest(".tool-category-toggle"); @@ -468,34 +537,139 @@ function bindUI() { } }, { passive: true }); }; + const containOwnVerticalScroll = (el) => { + if (!el) return; + el.addEventListener("wheel", (e) => { + if (Math.abs(e.deltaY) <= Math.abs(e.deltaX)) return; + const canScroll = el.scrollHeight > el.clientHeight + 1; + if (!canScroll) return; + const atTop = el.scrollTop <= 0; + const atBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 1; + const movingInside = (e.deltaY < 0 && !atTop) || (e.deltaY > 0 && !atBottom); + if (!movingInside) return; + e.preventDefault(); + e.stopPropagation(); + el.scrollTop += e.deltaY; + }, { passive: false }); + }; chainScroll(document.querySelector(".panel")); chainScroll(ui.archiveContent); - chainScroll(ui.log); + containOwnVerticalScroll(ui.log); canvas.addEventListener("contextmenu", (e) => e.preventDefault()); canvas.addEventListener("mousedown", (e) => { + if (e.button === 0 && world.tool === "pinch") { + const p = screenToWorld(e); + if (!p.inside) return; + const target = world.findGrabTargetAt ? world.findGrabTargetAt(p.x, p.y) : null; + if (!target) { + showToast("つまめる対象がありません。"); + return; + } + e.preventDefault(); + uiCache.grabbing = true; + uiCache.grabMoved = false; + uiCache.grabTarget = target; + uiCache.grabKind = target instanceof Tarinai ? "tarinai" : "item"; + uiCache.grabLastWorldX = p.x; + uiCache.grabLastWorldY = p.y; + uiCache.grabStartX = e.clientX; + uiCache.grabStartY = e.clientY; + if (uiCache.grabKind === "tarinai") { + world.selected = target; + target.sleeping = false; + if (target.state === "sleep" || target.state === "seek_bed") target.state = "idle"; + target.surpriseTimer = Math.max(target.surpriseTimer || 0, 0.18); + } else { + target.dropTimer = 0; + target.dropMax = 0; + target.dropImpactDone = true; + } + canvas.style.cursor = "grabbing"; + renderStats(); + return; + } if (e.button !== 2) return; e.preventDefault(); uiCache.panning = true; uiCache.panMoved = false; + uiCache.panButton = e.button; + uiCache.panStartX = e.clientX; + uiCache.panStartY = e.clientY; uiCache.panLastX = e.clientX; uiCache.panLastY = e.clientY; - canvas.style.cursor = "grabbing"; }); window.addEventListener("mouseup", (e) => { - if (e.button !== 2 && !uiCache.panning) return; - if (uiCache.panning) { - uiCache.panning = false; - canvas.style.cursor = world.tool === "observe" ? "default" : (world.tool === "poke" || world.tool === "delete" ? "pointer" : "crosshair"); + if (uiCache.grabbing) { + const target = uiCache.grabTarget; + if (target) { + if (uiCache.grabKind === "tarinai") { + target.target = null; + target.targetKey = ""; + target.surpriseTimer = Math.max(target.surpriseTimer || 0, 0.15); + } else if (target.type === "ball") { + target.prevX = target.x; + target.prevY = target.y; + } + } + uiCache.grabbing = false; + uiCache.grabTarget = null; + uiCache.grabKind = ""; + world.rebuildSpatial?.(); + canvas.style.cursor = world.tool === "pinch" ? "grab" : (world.tool === "observe" ? "default" : (world.tool === "poke" || world.tool === "delete" ? "pointer" : "crosshair")); + renderStats(); + return; } + if (!uiCache.panning) return; + if (uiCache.panButton !== null && e.button !== uiCache.panButton) return; + uiCache.panning = false; + uiCache.panButton = null; + canvas.style.cursor = world.tool === "observe" ? "default" : (world.tool === "pinch" ? "grab" : (world.tool === "poke" || world.tool === "delete" ? "pointer" : "crosshair")); }); window.addEventListener("mousemove", (e) => { + if (uiCache.grabbing) { + const target = uiCache.grabTarget; + if (!target || target.dead) { + uiCache.grabbing = false; + uiCache.grabTarget = null; + canvas.style.cursor = world.tool === "pinch" ? "grab" : "default"; + return; + } + const p = screenToWorld(e); + const dx = p.x - (uiCache.grabLastWorldX ?? p.x); + const dy = p.y - (uiCache.grabLastWorldY ?? p.y); + uiCache.grabLastWorldX = p.x; + uiCache.grabLastWorldY = p.y; + const totalMove = Math.hypot(e.clientX - (uiCache.grabStartX || e.clientX), e.clientY - (uiCache.grabStartY || e.clientY)); + if (totalMove > 3) uiCache.grabMoved = true; + const pad = target.radius || target.r || 16; + target.x = clamp(p.x, CONFIG.worldPadding + pad * 0.15, world.w - CONFIG.worldPadding - pad * 0.15); + target.y = clamp(p.y, CONFIG.worldPadding + pad * 0.15, world.h - CONFIG.worldPadding - pad * 0.15); + target.vx = clamp(dx * 16, -160, 160); + target.vy = clamp(dy * 16, -160, 160); + if (uiCache.grabKind === "tarinai") { + target.state = "idle"; + target.target = null; + target.sleeping = false; + } else if (target.type === "ball") { + target.prevX = target.x - dx; + target.prevY = target.y - dy; + target.spinVelocity = clamp((target.spinVelocity || 0) + dx * 0.06, -38, 38); + } + world.drawListDirty = true; + world.rebuildSpatial?.(); + canvas.style.cursor = "grabbing"; + render(); + return; + } if (!uiCache.panning) return; const dx = e.clientX - uiCache.panLastX; const dy = e.clientY - uiCache.panLastY; uiCache.panLastX = e.clientX; uiCache.panLastY = e.clientY; - if (Math.abs(dx) + Math.abs(dy) > 0.5) { + const totalMove = Math.hypot(e.clientX - uiCache.panStartX, e.clientY - uiCache.panStartY); + if (totalMove > 8) { + canvas.style.cursor = "grabbing"; world.panCamera?.(-dx, -dy); uiCache.panMoved = true; render(); @@ -503,6 +677,10 @@ function bindUI() { }); canvas.addEventListener("click", (e) => { + if (uiCache.grabMoved) { + uiCache.grabMoved = false; + return; + } if (uiCache.panMoved) { uiCache.panMoved = false; return; @@ -515,13 +693,27 @@ function bindUI() { canvas.addEventListener("mousemove", (e) => { const p = screenToWorld(e); + const prevX = world.pointer.x; + const prevY = world.pointer.y; world.pointer.x = p.x; world.pointer.y = p.y; world.pointer.inside = !!p.inside; + const delta = Math.hypot((p.x || 0) - (prevX || 0), (p.y || 0) - (prevY || 0)); + world.pointer.motion = clamp(delta / 18, 0, 2); + world.pointer.movedAt = performance.now(); }); canvas.addEventListener("wheel", resizeFieldFromWheel, { passive: false }); canvas.addEventListener("mouseleave", () => { world.pointer.inside = false; + world.pointer.motion = 0; + }); + + window.addEventListener("keydown", (e) => { + if (e.key !== "Escape") return; + closeFieldDialog(); + closeEcologyDialog(); + closeArchiveResetDialog(); + closeCreditsDialog(); }); window.addEventListener("resize", resizeCanvas); diff --git a/js/ui_family.js b/js/ui_family.js index 9a1dcff..a2f3154 100644 --- a/js/ui_family.js +++ b/js/ui_family.js @@ -1,28 +1,102 @@ -"use strict"; +"use strict"; function buildArchiveRows(family) { return liveFamilyComponents(family); } +function lineageEdgeFamily(family = world.family || {}) { + return family || {}; +} + +function lineageMutualParentIds(n, family = world.family || {}, idSet = null) { + const seen = new Set(); + const parents = []; + if (!n?.id) return parents; + const candidates = new Set((n.parents || []).filter(Boolean)); + // Older saves may contain only one side of the parent/child edge. For + // rendering, recover the missing side by scanning parent.children as well. + for (const parent of Object.values(family || {})) { + if (!parent?.id || parent.id === n.id) continue; + if (Array.isArray(parent.children) && parent.children.includes(n.id)) candidates.add(parent.id); + } + for (const id of candidates) { + if (!id || id === n.id || seen.has(id)) continue; + if (idSet && !idSet.has(id)) continue; + const parent = family[id]; + if (!parent) continue; + const childListsParent = Array.isArray(n.parents) && n.parents.includes(id); + const parentListsChild = Array.isArray(parent.children) && parent.children.includes(n.id); + if (!childListsParent && !parentListsChild) continue; + seen.add(id); + parents.push(id); + } + parents.sort((a, b) => lineageNodeSort(family[a], family[b])); + return parents; +} + +function lineageMutualChildIds(n, family = world.family || {}, idSet = null) { + const seen = new Set(); + const children = []; + if (!n?.id) return children; + const candidates = new Set((n.children || []).filter(Boolean)); + // Same recovery as above, in the opposite direction: child.parents is enough + // to draw a real parent/child edge even if parent.children was not persisted. + for (const child of Object.values(family || {})) { + if (!child?.id || child.id === n.id) continue; + if (Array.isArray(child.parents) && child.parents.includes(n.id)) candidates.add(child.id); + } + for (const id of candidates) { + if (!id || id === n.id || seen.has(id)) continue; + if (idSet && !idSet.has(id)) continue; + const child = family[id]; + if (!child) continue; + const parentListsChild = Array.isArray(n.children) && n.children.includes(id); + const childListsParent = Array.isArray(child.parents) && child.parents.includes(n.id); + if (!parentListsChild && !childListsParent) continue; + seen.add(id); + children.push(id); + } + children.sort((a, b) => lineageNodeSort(family[a], family[b])); + return children; +} + +function lineageHasActualRelation(n, family = world.family || {}) { + return lineageMutualParentIds(n, family).length > 0 || lineageMutualChildIds(n, family).length > 0; +} + +function lineageRelatedIdSet(family) { + const ids = new Set(); + for (const n of Object.values(family || {}).filter(Boolean)) { + const parents = lineageMutualParentIds(n, family); + const children = lineageMutualChildIds(n, family); + if (parents.length || children.length) ids.add(n.id); + for (const id of parents) ids.add(id); + for (const id of children) ids.add(id); + } + return ids; +} + function liveFamilyComponents(family) { - const nodes = Object.values(family || {}).filter(Boolean).filter(n => n.hasPaired || (n.parents || []).length || (n.children || []).length); + // Childless pairs, including pairs currently performing the birth ritual, are + // intentionally excluded. A 家系図 node is created only after an actual parent/child + // relation exists; otherwise ritual participants become isolated pseudo-families. + family = lineageEdgeFamily(family); + const relatedIds = lineageRelatedIdSet(family); + const nodes = Array.from(relatedIds).map(id => family[id]).filter(Boolean); const ids = new Set(nodes.map(n => n.id)); - const graph = new Map(); + const graph = new Map(nodes.map(n => [n.id, new Set()])); const addEdge = (a, b) => { if (!a || !b || !ids.has(a) || !ids.has(b)) return; - if (!graph.has(a)) graph.set(a, new Set()); - if (!graph.has(b)) graph.set(b, new Set()); graph.get(a).add(b); graph.get(b).add(a); }; for (const n of nodes) { - if (!graph.has(n.id)) graph.set(n.id, new Set()); - for (const p of n.parents || []) addEdge(n.id, p); - for (const c of n.children || []) addEdge(n.id, c); + for (const p of lineageMutualParentIds(n, family, ids)) addEdge(n.id, p); + for (const c of lineageMutualChildIds(n, family, ids)) addEdge(n.id, c); } const seen = new Set(); const comps = []; - for (const n of nodes) { + for (const n of nodes.sort(lineageNodeSort)) { if (seen.has(n.id)) continue; const stack = [n.id]; const comp = []; @@ -50,93 +124,837 @@ function liveFamilyComponents(family) { } function familyStatusText(n) { - if (!n) return "\u4e0d\u660e"; - return n.alive ? "\u751f\u5b58" : "\u6b7b\u4ea1"; + if (!n) return "不明"; + return n.alive ? "生存" : "死亡"; } -function lineagePersonHtml(n) { - const live = n?.id ? world.tarinai.find(t => t.id === n.id && !t.dead) : null; +function lineageNodeSort(a, b) { + return (a.generation || 1) - (b.generation || 1) || + (a.birthTime || 0) - (b.birthTime || 0) || + String(a.name || "").localeCompare(String(b.name || "")) || + String(a.id || "").localeCompare(String(b.id || "")); +} + +function lineageTreeNodeHtml(n, x, y) { + const entity = n?.id ? world.tarinai.find(t => t.id === n.id) : null; + const live = entity && !entity.dead ? entity : null; const img = n ? tarinaiSpritePathForId(n.id) : SPRITES[0].path; - const name = escapeHtml(n?.name || "\u4e0d\u660e"); - const joined = Boolean(n?.hasPaired || (n?.parents || []).length || (n?.children || []).length); - const gen = joined && n?.generation ? `\u4e16\u4ee3 ${n.generation}` : ""; - const status = escapeHtml(familyStatusText(n)); - const reason = !n?.alive && n?.deathReason ? `\u7406\u7531: ${escapeHtml(n.deathReason)}` : ""; + const name = escapeHtml(n?.name || "不明"); + const status = familyStatusText(n); + const gen = n?.generation ? `G${n.generation}` : "G?"; + const rawReason = !n?.alive && n?.deathReason ? String(n.deathReason) : ""; + const briefReason = rawReason && entity?.briefDeathReason ? entity.briefDeathReason(rawReason) : (rawReason ? String(rawReason) : ""); + const reason = briefReason ? ` / ${briefReason}` : ""; + const title = escapeHtml(`${n?.name || "不明"} / 世代 ${n?.generation || "?"} / ${status}${reason}`); + const deathLine = briefReason ? `${escapeHtml(briefReason)}` : ""; const cls = n ? ((live || n.alive) ? "alive" : "dead") : "missing"; + const childCls = (n?.parents || []).length ? " child" : ""; const data = n?.id ? ` data-family-tarinai-id="${escapeHtml(n.id)}"` : ""; - return `
- + return `
+
- ${name} - ${gen ? `${escapeHtml(gen)} / ${status}` : `${status}`}${reason} + ${name} + ${escapeHtml(gen)} / ${escapeHtml(status)} + ${deathLine}
`; } -function lineageMiniHtml(n, role = "") { - if (!n) return `\u4e0d\u660e`; - const img = tarinaiSpritePathForId(n.id); - const name = escapeHtml(n.name || n.id); - const gen = n.generation ? `G${n.generation}` : ""; - const status = n.alive ? "\u751f" : "\u6b7b"; - return ` - - ${name} - ${gen} / ${status} - `; +/* + * Family archive rendering is intentionally a top-down tree diagram. + * Do not replace this section with list/cards/readable grids: the archive must remain a 家系図. + * The layout below may reorder siblings/parents inside the same generation only to reduce crossings. + */ +function lineageParentIds(n, idSet, family = world.family || {}) { + return lineageMutualParentIds(n, family, idSet).slice(0, 2); +} + +function lineageParentKey(n, idSet) { + const parents = lineageParentIds(n, idSet); + if (!parents.length) return `root:${n?.id || Math.random()}`; + return parents.slice().sort().join("+"); +} + +function lineageMedian(values) { + if (!values.length) return Number.POSITIVE_INFINITY; + const sorted = values.slice().sort((a, b) => a - b); + const mid = Math.floor(sorted.length / 2); + return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2; +} + +function lineageRankMap(rows) { + const rank = new Map(); + rows.forEach((row, rowIndex) => { + row.forEach((n, i) => rank.set(n.id, rowIndex * 10000 + i)); + }); + return rank; +} + +function lineageChildIds(n, idSet, family = world.family || {}) { + return lineageMutualChildIds(n, family, idSet); +} + +function lineageOrderChildGroups(row, idSet, rank) { + const groups = new Map(); + for (const n of row) { + const key = lineageParentKey(n, idSet); + if (!groups.has(key)) groups.set(key, []); + groups.get(key).push(n); + } + const records = Array.from(groups.entries()).map(([key, items]) => { + const parents = key.startsWith("root:") ? [] : key.split("+").filter(Boolean); + const parentRanks = parents.map(id => rank.get(id)).filter(v => Number.isFinite(v)); + const score = parentRanks.length ? lineageMedian(parentRanks) : Number.POSITIVE_INFINITY; + items.sort(lineageNodeSort); + return { + key, + items, + score, + fallback: Math.min(...items.map(n => n.birthTime || 0)), + generation: Math.min(...items.map(n => n.generation || 1)), + }; + }); + records.sort((a, b) => a.score - b.score || a.generation - b.generation || a.fallback - b.fallback || String(a.key).localeCompare(String(b.key))); + return records.flatMap(r => r.items); +} + +function lineageOrderParentsByChildren(row, idSet, childRank) { + if (!row.length || !childRank?.size) return row; + const records = row.map((n, index) => { + const childRanks = lineageChildIds(n, idSet).map(id => childRank.get(id)).filter(v => Number.isFinite(v)); + return { + node: n, + score: childRanks.length ? lineageMedian(childRanks) : Number.POSITIVE_INFINITY, + original: index, + }; + }); + records.sort((a, b) => a.score - b.score || a.original - b.original || lineageNodeSort(a.node, b.node)); + return records.map(r => r.node); +} + +function lineagePartnerEdgesByGeneration(nodes, idToNode, idSet) { + const byGeneration = new Map(); + const seen = new Set(); + for (const child of nodes) { + const parents = lineageParentIds(child, idSet); + if (parents.length < 2) continue; + const a = idToNode.get(parents[0]); + const b = idToNode.get(parents[1]); + if (!a || !b) continue; + const generation = Math.min(a.generation || 1, b.generation || 1); + const key = parents.slice().sort().join("+"); + if (seen.has(key)) continue; + seen.add(key); + if (!byGeneration.has(generation)) byGeneration.set(generation, []); + byGeneration.get(generation).push(parents.slice()); + } + return byGeneration; +} + +function lineageOrderPartnerComponent(ids, adjacency, rank, idToNode) { + if (ids.length <= 2) { + return ids.slice().sort((a, b) => (rank.get(a) ?? 0) - (rank.get(b) ?? 0) || lineageNodeSort(idToNode.get(a), idToNode.get(b))); + } + const degree = id => (adjacency.get(id) || new Set()).size; + const start = ids.slice().sort((a, b) => degree(b) - degree(a) || (rank.get(a) ?? 0) - (rank.get(b) ?? 0))[0]; + const neighbors = Array.from(adjacency.get(start) || []).filter(id => ids.includes(id)); + const left = []; + const right = []; + neighbors.sort((a, b) => (rank.get(a) ?? 0) - (rank.get(b) ?? 0) || lineageNodeSort(idToNode.get(a), idToNode.get(b))); + neighbors.forEach((id, i) => (i % 2 ? left : right).push(id)); + const placed = new Set([start, ...neighbors]); + const rest = ids.filter(id => !placed.has(id)).sort((a, b) => (rank.get(a) ?? 0) - (rank.get(b) ?? 0) || lineageNodeSort(idToNode.get(a), idToNode.get(b))); + return [...left.reverse(), start, ...right, ...rest]; +} + +function lineageOrderPartnerClusters(row, partnerEdges, rank, idToNode) { + if (!partnerEdges?.length) return row; + const rowIds = new Set(row.map(n => n.id)); + const adjacency = new Map(); + for (const [a, b] of partnerEdges) { + if (!rowIds.has(a) || !rowIds.has(b)) continue; + if (!adjacency.has(a)) adjacency.set(a, new Set()); + if (!adjacency.has(b)) adjacency.set(b, new Set()); + adjacency.get(a).add(b); + adjacency.get(b).add(a); + } + if (!adjacency.size) return row; + + const clusteredIds = new Set(adjacency.keys()); + const seen = new Set(); + const records = []; + for (const n of row) { + if (!clusteredIds.has(n.id)) { + records.push({ ids: [n.id], score: rank.get(n.id) ?? Number.POSITIVE_INFINITY, fallback: idToNode.get(n.id)?.birthTime || 0 }); + continue; + } + if (seen.has(n.id)) continue; + const stack = [n.id]; + const ids = []; + seen.add(n.id); + while (stack.length) { + const id = stack.pop(); + ids.push(id); + for (const next of adjacency.get(id) || []) { + if (seen.has(next)) continue; + seen.add(next); + stack.push(next); + } + } + const orderedIds = lineageOrderPartnerComponent(ids, adjacency, rank, idToNode); + records.push({ + ids: orderedIds, + score: lineageMedian(orderedIds.map(id => rank.get(id)).filter(v => Number.isFinite(v))), + fallback: Math.min(...orderedIds.map(id => idToNode.get(id)?.birthTime || 0)), + }); + } + records.sort((a, b) => a.score - b.score || a.fallback - b.fallback || String(a.ids[0]).localeCompare(String(b.ids[0]))); + return records.flatMap(c => c.ids.map(id => idToNode.get(id)).filter(Boolean)); +} + +function lineageAdjacentCrossCost(parentRow, childRow, idSet) { + if (!parentRow?.length || !childRow?.length) return 0; + const parentPos = new Map(parentRow.map((n, i) => [n.id, i])); + const edges = []; + for (let childIndex = 0; childIndex < childRow.length; childIndex++) { + const child = childRow[childIndex]; + const parents = lineageParentIds(child, idSet).filter(id => parentPos.has(id)); + if (!parents.length) continue; + const from = lineageMedian(parents.map(id => parentPos.get(id))); + edges.push({ from, to: childIndex, weight: parents.length >= 2 ? 1.4 : 1 }); + } + let cost = 0; + for (let i = 0; i < edges.length; i++) { + for (let j = i + 1; j < edges.length; j++) { + const a = edges[i]; + const b = edges[j]; + const cross = (a.from - b.from) * (a.to - b.to) < 0; + if (cross) cost += a.weight * b.weight * 4.5; + } + } + return cost; +} + +function lineageRowCost(row, rowIndex, rows, idSet) { + const pos = new Map(row.map((n, i) => [n.id, i])); + let cost = 0; + for (const n of row) { + const i = pos.get(n.id); + const parents = lineageParentIds(n, idSet); + if (parents.length && rowIndex > 0) { + const parentRow = rows[rowIndex - 1] || []; + const parentPos = parents.map(id => parentRow.findIndex(x => x.id === id)).filter(v => v >= 0); + if (parentPos.length) cost += Math.abs(i - lineageMedian(parentPos)) * (parents.length >= 2 ? 1.5 : 1.2); + } + const childRow = rows[rowIndex + 1] || []; + const childPos = childRow.filter(x => lineageParentIds(x, idSet).includes(n.id)).map(x => childRow.findIndex(y => y.id === x.id)); + if (childPos.length) cost += Math.abs(i - lineageMedian(childPos)) * 1.0; + + const siblingPos = row.filter(x => x.id !== n.id && lineageParentKey(x, idSet) === lineageParentKey(n, idSet)).map(x => pos.get(x.id)); + if (siblingPos.length) cost += Math.abs(i - lineageMedian(siblingPos)) * 0.22; + } + + const seenPairs = new Set(); + for (const child of rows[rowIndex + 1] || []) { + const parents = lineageParentIds(child, idSet).filter(id => pos.has(id)); + if (parents.length < 2) continue; + const key = parents.slice().sort().join("+"); + if (seenPairs.has(key)) continue; + seenPairs.add(key); + const pair = parents.slice().sort((a, b) => pos.get(a) - pos.get(b)); + const span = pos.get(pair[1]) - pos.get(pair[0]); + cost += span * span * 3.4; + if (span > 1) cost += 12; + } + + return cost; +} + +function lineageTotalCost(rows, idSet) { + let cost = 0; + for (let i = 0; i < rows.length; i++) { + cost += lineageRowCost(rows[i], i, rows, idSet); + if (i < rows.length - 1) cost += lineageAdjacentCrossCost(rows[i], rows[i + 1], idSet); + } + return cost; +} + +function lineageMoveItem(row, fromIndex, toIndex) { + if (fromIndex === toIndex) return row.slice(); + const next = row.slice(); + const [item] = next.splice(fromIndex, 1); + next.splice(toIndex, 0, item); + return next; +} + +function lineageOptimizeRows(rows, idSet) { + const totalNodes = rows.reduce((sum, row) => sum + row.length, 0); + const maxRow = Math.max(0, ...rows.map(row => row.length)); + // The old exhaustive optimizer was cubic-to-quartic in practice and froze on + // large archives. Keep it only for small families; large trees use the + // barycentric ordering above, which is stable and linear-ish. + if (totalNodes > 90 || maxRow > 16) return rows.map(row => row.slice()); + let bestRows = rows.map(row => row.slice()); + let bestCost = lineageTotalCost(bestRows, idSet); + const passes = totalNodes > 54 || maxRow > 10 ? 3 : 6; + for (let pass = 0; pass < passes; pass++) { + let changed = false; + for (let rowIndex = 0; rowIndex < bestRows.length; rowIndex++) { + const row = bestRows[rowIndex]; + if (!row || row.length < 2) continue; + for (let from = 0; from < row.length; from++) { + const span = row.length > 10 ? 3 : row.length; + const minTo = Math.max(0, from - span); + const maxTo = Math.min(row.length - 1, from + span); + for (let to = minTo; to <= maxTo; to++) { + if (from === to) continue; + const moved = lineageMoveItem(bestRows[rowIndex], from, to); + const candidate = bestRows.slice(); + candidate[rowIndex] = moved; + const cost = lineageTotalCost(candidate, idSet); + if (cost + 0.001 < bestCost) { + bestRows = candidate; + bestCost = cost; + changed = true; + } + } + } + } + if (!changed) break; + } + return bestRows; +} + +function lineageRowsForLayout(nodes, generations, idToNode, idSet) { + const byGeneration = new Map(generations.map(g => [g, []])); + for (const n of nodes) byGeneration.get(n.generation || 1).push(n); + let rows = generations.map(g => (byGeneration.get(g) || []).slice().sort(lineageNodeSort)); + const partnerEdges = lineagePartnerEdgesByGeneration(nodes, idToNode, idSet); + const totalNodes = nodes.length; + const maxRow = Math.max(0, ...rows.map(row => row.length)); + const passes = totalNodes > 180 || maxRow > 32 ? 2 : (totalNodes > 90 || maxRow > 18 ? 3 : 6); + + for (let pass = 0; pass < passes; pass++) { + let rank = lineageRankMap(rows); + rows = rows.map(row => lineageOrderChildGroups(row, idSet, rank)); + rank = lineageRankMap(rows); + for (let i = rows.length - 2; i >= 0; i--) { + const childRank = new Map(rows[i + 1].map((n, index) => [n.id, index])); + rows[i] = lineageOrderParentsByChildren(rows[i], idSet, childRank); + } + rank = lineageRankMap(rows); + rows = rows.map((row, i) => lineageOrderPartnerClusters(row, partnerEdges.get(generations[i]), rank, idToNode)); + if (totalNodes <= 90 && maxRow <= 16) rows = lineageOptimizeRows(rows, idSet); + } + return rows; +} + +function lineageBuildRowGroups(row, idSet) { + const groups = []; + let i = 0; + while (i < row.length) { + const key = lineageParentKey(row[i], idSet); + const items = [row[i]]; + i += 1; + while (i < row.length && lineageParentKey(row[i], idSet) === key) { + items.push(row[i]); + i += 1; + } + groups.push({ key, items }); + } + return groups; +} + +function lineageLayoutRows(rows, idSet) { + const NODE_W = 132; + const NODE_H = 68; + const SIBLING_GAP = 32; + const GROUP_GAP = 52; + const PARTNER_GROUP_GAP = 24; + const ROW_GAP = 138; + const LEFT_PAD = 82; + const TOP_PAD = 44; + const RIGHT_PAD = 48; + const BOTTOM_PAD = 30; + + const rowGroups = rows.map(row => lineageBuildRowGroups(row, idSet).map(group => ({ + ...group, + groupW: group.items.length * NODE_W + Math.max(0, group.items.length - 1) * SIBLING_GAP, + }))); + + const pairedKeysByRow = rows.map((row) => { + const set = new Set(); + const rowIds = new Set(row.map(n => n.id)); + for (const futureRow of rows) { + for (const child of futureRow) { + const parents = lineageParentIds(child, rowIds); + if (parents.length >= 2) set.add(parents.slice().sort().join("+")); + } + } + return set; + }); + + let positions = new Map(); + let width = 420; + + const placePass = (existingPositions) => { + const nextPositions = new Map(); + let nextWidth = 420; + const rowLayouts = []; + + rows.forEach((row, rowIndex) => { + const groups = rowGroups[rowIndex] || []; + const desiredLefts = []; + const y = TOP_PAD + rowIndex * (NODE_H + ROW_GAP); + const nextRow = rows[rowIndex + 1] || []; + + for (let gIndex = 0; gIndex < groups.length; gIndex++) { + const group = groups[gIndex]; + const first = group.items[0]; + const parentCenters = lineageParentIds(first, idSet) + .map(id => existingPositions.get(id) || nextPositions.get(id)) + .filter(Boolean) + .map(p => p.x + NODE_W / 2); + const groupIds = new Set(group.items.map(n => n.id)); + const childCenters = nextRow + .filter(child => lineageParentIds(child, idSet).some(id => groupIds.has(id))) + .map(child => existingPositions.get(child.id)) + .filter(Boolean) + .map(p => p.x + NODE_W / 2); + const desiredCenterValues = []; + if (parentCenters.length) desiredCenterValues.push(...parentCenters, ...parentCenters); + if (childCenters.length) desiredCenterValues.push(...childCenters); + const fallbackCenter = LEFT_PAD + group.groupW / 2 + gIndex * (group.groupW + GROUP_GAP); + const desiredCenter = Number.isFinite(lineageMedian(desiredCenterValues)) ? lineageMedian(desiredCenterValues) : fallbackCenter; + desiredLefts[gIndex] = desiredCenter - group.groupW / 2; + } + + const lefts = []; + const SOFT_EXTRA_GAP = 68; + for (let gIndex = 0; gIndex < groups.length; gIndex++) { + const prevGroup = groups[gIndex - 1]; + const pairGap = prevGroup && prevGroup.items.length === 1 && groups[gIndex].items.length === 1 + && pairedKeysByRow[rowIndex].has([prevGroup.items[0].id, groups[gIndex].items[0].id].sort().join("+")); + const gap = pairGap ? PARTNER_GROUP_GAP : GROUP_GAP; + const minLeft = gIndex === 0 ? LEFT_PAD : lefts[gIndex - 1] + groups[gIndex - 1].groupW + gap; + const desired = Number.isFinite(desiredLefts[gIndex]) ? desiredLefts[gIndex] : minLeft; + lefts[gIndex] = Math.max(minLeft, Math.min(desired, minLeft + SOFT_EXTRA_GAP)); + } + for (let gIndex = groups.length - 2; gIndex >= 0; gIndex--) { + const nextGroup = groups[gIndex + 1]; + const pairGap = nextGroup && groups[gIndex].items.length === 1 && nextGroup.items.length === 1 + && pairedKeysByRow[rowIndex].has([groups[gIndex].items[0].id, nextGroup.items[0].id].sort().join("+")); + const gap = pairGap ? PARTNER_GROUP_GAP : GROUP_GAP; + const maxLeft = lefts[gIndex + 1] - groups[gIndex].groupW - gap; + lefts[gIndex] = Math.min(lefts[gIndex], maxLeft); + if (gIndex === 0) lefts[gIndex] = Math.max(lefts[gIndex], LEFT_PAD); + } + + const contentW = groups.length ? (lefts[lefts.length - 1] + groups[groups.length - 1].groupW - LEFT_PAD) : 0; + nextWidth = Math.max(nextWidth, LEFT_PAD + contentW + RIGHT_PAD); + groups.forEach((group, gIndex) => { + const left = lefts[gIndex]; + group.items.forEach((n, i) => { + const x = left + i * (NODE_W + SIBLING_GAP); + nextPositions.set(n.id, { x, y, rowIndex, node: n }); + }); + }); + rowLayouts.push({ rowIndex, groups, lefts, contentW, y }); + }); + + return { positions: nextPositions, width: nextWidth, rowLayouts }; + }; + + for (let pass = 0; pass < 5; pass++) { + const result = placePass(positions); + positions = result.positions; + width = result.width; + } + + // Re-run once more and center the entire family as one block to avoid left-heavy layouts + // without breaking parent/child alignment between rows. + const finalResult = placePass(positions); + positions = new Map(finalResult.positions); + let minX = Number.POSITIVE_INFINITY; + let maxX = 0; + for (const pos of positions.values()) { + minX = Math.min(minX, pos.x); + maxX = Math.max(maxX, pos.x + NODE_W); + } + const contentW = Number.isFinite(minX) ? Math.max(0, maxX - minX) : 0; + width = Math.max(finalResult.width, LEFT_PAD + contentW + RIGHT_PAD); + if (Number.isFinite(minX)) { + const targetLeft = LEFT_PAD + Math.max(0, (width - LEFT_PAD - RIGHT_PAD - contentW) / 2); + const shift = targetLeft - minX; + if (shift) { + for (const [id, prev] of positions.entries()) positions.set(id, { ...prev, x: prev.x + shift }); + } + } + + const groupBounds = (group) => { + const xs = group.items.map(n => positions.get(n.id)?.x).filter(Number.isFinite); + if (!xs.length) return null; + return { left: Math.min(...xs), right: Math.max(...xs) + NODE_W, width: group.groupW }; + }; + const groupGap = (leftGroup, rightGroup, rowIndex) => { + const pairGap = leftGroup && rightGroup && leftGroup.items.length === 1 && rightGroup.items.length === 1 + && pairedKeysByRow[rowIndex].has([leftGroup.items[0].id, rightGroup.items[0].id].sort().join("+")); + return pairGap ? PARTNER_GROUP_GAP : GROUP_GAP; + }; + const shiftGroup = (group, dx) => { + if (!dx) return; + for (const n of group.items) { + const prev = positions.get(n.id); + if (prev) positions.set(n.id, { ...prev, x: prev.x + dx }); + } + }; + for (let pass = 0; pass < 3; pass++) { + for (let rowIndex = 1; rowIndex < rowGroups.length; rowIndex++) { + const groups = rowGroups[rowIndex] || []; + for (let gIndex = 0; gIndex < groups.length; gIndex++) { + const group = groups[gIndex]; + const first = group.items[0]; + const parentIds = lineageParentIds(first, idSet); + if (!parentIds.length) continue; + const parentPositions = parentIds.map(id => positions.get(id)).filter(Boolean); + if (!parentPositions.length) continue; + const parentCenter = parentPositions.reduce((sum, p) => sum + p.x + NODE_W / 2, 0) / parentPositions.length; + const bounds = groupBounds(group); + if (!bounds) continue; + const idealLeft = parentCenter - bounds.width / 2; + let minShift = LEFT_PAD - bounds.left; + let maxShift = Number.POSITIVE_INFINITY; + const prevGroup = groups[gIndex - 1]; + if (prevGroup) { + const pb = groupBounds(prevGroup); + if (pb) minShift = Math.max(minShift, pb.right + groupGap(prevGroup, group, rowIndex) - bounds.left); + } + const nextGroup = groups[gIndex + 1]; + if (nextGroup) { + const nb = groupBounds(nextGroup); + if (nb) maxShift = nb.left - groupGap(group, nextGroup, rowIndex) - bounds.right; + } + const desiredShift = idealLeft - bounds.left; + const shift = clamp(desiredShift, minShift, maxShift); + if (Math.abs(shift) > 0.2) shiftGroup(group, shift); + } + } + } + width = Math.max(width, ...Array.from(positions.values()).map(p => p.x + NODE_W + RIGHT_PAD)); + + // Fallback placement for any node that somehow missed layout. + rows.forEach((row, rowIndex) => { + const y = TOP_PAD + rowIndex * (NODE_H + ROW_GAP); + let fallbackX = LEFT_PAD; + for (const n of row) { + if (positions.has(n.id)) { + fallbackX = Math.max(fallbackX, positions.get(n.id).x + NODE_W + GROUP_GAP); + continue; + } + positions.set(n.id, { x: fallbackX, y, rowIndex, node: n }); + fallbackX += NODE_W + GROUP_GAP; + width = Math.max(width, fallbackX + RIGHT_PAD); + } + }); + + const height = TOP_PAD + Math.max(0, rows.length - 1) * (NODE_H + ROW_GAP) + NODE_H + BOTTOM_PAD; + return { positions, width, height, nodeW: NODE_W, nodeH: NODE_H, topPad: TOP_PAD, rowGap: ROW_GAP }; +} + +function lineagePartnerPathFromPositions(a, b, nodeW, nodeH, unionY, attachOffsetA = 0, attachOffsetB = 0) { + const left = a.x <= b.x ? a : b; + const right = left === a ? b : a; + const midYLeft = left.y + nodeH / 2 + attachOffsetA; + const midYRight = right.y + nodeH / 2 + attachOffsetB; + const x1 = left.x + nodeW; + const x2 = right.x; + if (x2 <= x1) return { d: "", segments: [] }; + const direct = left.rowIndex === right.rowIndex && x2 - x1 <= 156 && Math.abs(midYLeft - midYRight) < 10 + && (!Number.isFinite(unionY) || unionY <= Math.max(midYLeft, midYRight) + 16); + if (direct) { + const y = (midYLeft + midYRight) / 2; + return { d: `M ${x1.toFixed(1)} ${y.toFixed(1)} L ${x2.toFixed(1)} ${y.toFixed(1)}`, segments: [{ kind: "h", x1, y1: y, x2, y2: y }] }; + } + const bend = 10; + const y = Number.isFinite(unionY) ? unionY : Math.max(midYLeft, midYRight); + const points = [ + [x1, midYLeft], [x1 + bend, midYLeft], [x1 + bend, y], [x2 - bend, y], [x2 - bend, midYRight], [x2, midYRight] + ]; + const d = `M ${points[0][0].toFixed(1)} ${points[0][1].toFixed(1)} ` + points.slice(1).map(p => `L ${p[0].toFixed(1)} ${p[1].toFixed(1)}`).join(' '); + const segments = []; + for (let i = 1; i < points.length; i++) { + const [x1s, y1s] = points[i - 1]; + const [x2s, y2s] = points[i]; + segments.push({ kind: Math.abs(y2s - y1s) < 0.01 ? 'h' : (Math.abs(x2s - x1s) < 0.01 ? 'v' : 'd'), x1: x1s, y1: y1s, x2: x2s, y2: y2s }); + } + return { d, segments }; +} + +function lineageVerticalBridgePath(x, y1, y2, bridges = [], radius = 8.5) { + if (!Number.isFinite(x) || !Number.isFinite(y1) || !Number.isFinite(y2)) return ''; + const down = y2 >= y1; + const sign = down ? 1 : -1; + const sorted = bridges.slice().sort((a, b) => down ? a - b : b - a).filter(y => Math.abs(y - y1) > radius + 1 && Math.abs(y - y2) > radius + 1); + let d = `M ${x.toFixed(1)} ${y1.toFixed(1)}`; + let cursor = y1; + for (const by of sorted) { + const before = by - sign * radius; + const after = by + sign * radius; + d += ` L ${x.toFixed(1)} ${before.toFixed(1)}`; + d += ` Q ${(x + radius * 1.55).toFixed(1)} ${by.toFixed(1)} ${x.toFixed(1)} ${after.toFixed(1)}`; + cursor = after; + } + if (Math.abs(cursor - y2) > 0.01) d += ` L ${x.toFixed(1)} ${y2.toFixed(1)}`; + return d; +} + +function lineageIntersectionYsForVertical(x, y1, y2, horizontalSegments) { + const top = Math.min(y1, y2); + const bottom = Math.max(y1, y2); + const ys = []; + for (const seg of horizontalSegments || []) { + if (seg.kind !== 'h') continue; + const minX = Math.min(seg.x1, seg.x2); + const maxX = Math.max(seg.x1, seg.x2); + const y = seg.y1; + if (x > minX + 3 && x < maxX - 3 && y > top + 3 && y < bottom - 3) ys.push(y); + } + return ys; +} + +function lineageBuildChildPathGroup(group, nodeW, nodeH, bridgeHorizontals) { + const children = group.children; + if (!children.length) return { d: '' }; + const joinX = group.joinX; + const fromY = group.fromY; + const laneY = group.busY; + const childPoints = children + .map(c => ({ x: c.x + nodeW / 2, y: c.y })) + .sort((a, b) => a.x - b.x || a.y - b.y); + if (childPoints.length === 1 && Math.abs(childPoints[0].x - joinX) <= 40) { + const p = childPoints[0]; + const bridges = lineageIntersectionYsForVertical(p.x, fromY, p.y, bridgeHorizontals); + return { d: lineageVerticalBridgePath(p.x, fromY, p.y, bridges) }; + } + const minX = Math.min(joinX, ...childPoints.map(p => p.x)); + const maxX = Math.max(joinX, ...childPoints.map(p => p.x)); + let d = lineageVerticalBridgePath(joinX, fromY, laneY, lineageIntersectionYsForVertical(joinX, fromY, laneY, bridgeHorizontals)); + d += ` M ${minX.toFixed(1)} ${laneY.toFixed(1)} L ${maxX.toFixed(1)} ${laneY.toFixed(1)}`; + for (const p of childPoints) { + const bridges = lineageIntersectionYsForVertical(p.x, laneY, p.y, bridgeHorizontals); + const branch = lineageVerticalBridgePath(p.x, laneY, p.y, bridges); + d += ` ${branch}`; + } + return { d }; +} + +function lineageAssignPartnerAttachOffsets(linkGroups) { + const buckets = new Map(); + const pushBucket = (nodeId, side, group) => { + const key = `${nodeId}:${side}`; + if (!buckets.has(key)) buckets.set(key, []); + buckets.get(key).push(group); + }; + for (const group of linkGroups) { + if ((group.parents || []).length < 2) continue; + const left = group.parents[0]; + const right = group.parents[group.parents.length - 1]; + if (!group.attachOffsets) group.attachOffsets = new Map(); + pushBucket(left.node.id, 'right', group); + pushBucket(right.node.id, 'left', group); + } + for (const [key, groups] of buckets.entries()) { + groups.sort((a, b) => a.joinX - b.joinX || a.childTop - b.childTop); + const n = groups.length; + const step = 9; + groups.forEach((group, i) => { + const offset = n <= 1 ? 0 : (i - (n - 1) / 2) * step; + const [nodeId] = key.split(':'); + group.attachOffsets.set(nodeId, offset); + }); + } +} + +function lineageBuildLinks(nodes, idSet, layout) { + const { positions, nodeW, nodeH } = layout; + const groups = new Map(); + for (const child of nodes) { + const parents = lineageParentIds(child, idSet).filter(id => positions.has(id)); + if (parents.length < 1 || !positions.has(child.id)) continue; + const key = parents.slice().sort().join('+'); + if (!groups.has(key)) groups.set(key, { parentIds: parents.slice().sort(), childIds: [] }); + groups.get(key).childIds.push(child.id); + } + + const linkGroups = []; + for (const group of groups.values()) { + const parents = group.parentIds.map(id => positions.get(id)).filter(Boolean).sort((a, b) => a.x - b.x); + const children = group.childIds.map(id => positions.get(id)).filter(Boolean).sort((a, b) => a.x - b.x); + if (!parents.length || !children.length) continue; + const parentBottom = Math.max(...parents.map(p => p.y + nodeH)); + const childTop = Math.min(...children.map(c => c.y)); + const joinX = parents.reduce((sum, p) => sum + p.x + nodeW / 2, 0) / parents.length; + const directCouple = parents.length >= 2 && parents[0].rowIndex === parents[1].rowIndex && (parents[1].x - (parents[0].x + nodeW) <= 168); + linkGroups.push({ parents, children, parentBottom, childTop, joinX, directCouple, attachOffsets: new Map() }); + } + lineageAssignPartnerAttachOffsets(linkGroups); + + const byGap = new Map(); + for (const group of linkGroups) { + const key = `${group.parentBottom.toFixed(1)}:${group.childTop.toFixed(1)}`; + if (!byGap.has(key)) byGap.set(key, []); + byGap.get(key).push(group); + } + + const partnerLinks = []; + const childLinks = []; + const horizontalSegments = []; + + for (const groupsInGap of byGap.values()) { + groupsInGap.sort((a, b) => a.joinX - b.joinX || a.children.length - b.children.length); + const top = Math.min(...groupsInGap.map(g => g.parentBottom)) + 44; + const bottom = Math.max(...groupsInGap.map(g => g.childTop)) - 42; + const span = Math.max(16, bottom - top); + groupsInGap.forEach((group, i) => { + const laneY = top + span * ((i + 1) / (groupsInGap.length + 1)); + if (group.parents.length >= 2) { + const left = group.parents[0]; + const right = group.parents[group.parents.length - 1]; + // Marriage line stays at the parent-node center. The child trunk drops + // to a visible point below the parents before branching, so the fork is + // never hidden behind a node. + const attachA = 0; + const attachB = 0; + const partnerY = ((left.y + nodeH / 2) + (right.y + nodeH / 2)) / 2; + const clearY = clamp(group.parentBottom + 22 + i * 2, partnerY + 18, group.childTop - 36); + group.fromY = clearY; + group.busY = group.children.length === 1 ? Math.min(group.childTop - 18, clearY + 28) : Math.min(group.childTop - 24, Math.max(clearY + 24, laneY + 18 + i * 3)); + const partner = lineagePartnerPathFromPositions(left, right, nodeW, nodeH, partnerY, attachA, attachB); + if (partner.d) { + partnerLinks.push(partner.d); + horizontalSegments.push(...partner.segments.filter(seg => seg.kind === 'h')); + } + const trunk = lineageVerticalBridgePath(group.joinX, partnerY, group.fromY, []); + if (trunk) childLinks.push(trunk); + } else { + group.fromY = group.parentBottom; + group.busY = group.children.length === 1 ? Math.min(group.childTop - 18, group.fromY + 30) : Math.min(group.childTop - 24, Math.max(group.fromY + 28, laneY + 18 + i * 3)); + } + }); + } + + for (const groupsInGap of byGap.values()) { + groupsInGap.sort((a, b) => a.joinX - b.joinX || a.children.length - b.children.length); + for (const group of groupsInGap) { + if (group.children.length > 1) { + const childCenters = group.children.map(c => c.x + nodeW / 2).sort((a, b) => a - b); + horizontalSegments.push({ kind: 'h', x1: Math.min(group.joinX, ...childCenters), y1: group.busY, x2: Math.max(group.joinX, ...childCenters), y2: group.busY }); + } + const child = lineageBuildChildPathGroup(group, nodeW, nodeH, horizontalSegments); + if (child.d) childLinks.push(child.d); + } + } + + return { partnerLinks, childLinks }; +} + +function lineageSplitByActualEdges(component) { + const family = lineageEdgeFamily(world.family || {}); + const nodes = Array.from(new Map(lineageCleanComponent(component, family).filter(Boolean).map(n => [n.id, n])).values()); + const idSet = new Set(nodes.map(n => n.id)); + const byId = new Map(nodes.map(n => [n.id, n])); + const graph = new Map(nodes.map(n => [n.id, new Set()])); + const connect = (a, b) => { + if (!a || !b || !idSet.has(a) || !idSet.has(b)) return; + graph.get(a).add(b); + graph.get(b).add(a); + }; + for (const n of nodes) { + for (const p of lineageMutualParentIds(n, family, idSet)) connect(n.id, p); + for (const c of lineageMutualChildIds(n, family, idSet)) connect(n.id, c); + } + const seen = new Set(); + const parts = []; + for (const n of nodes.sort(lineageNodeSort)) { + if (seen.has(n.id)) continue; + const stack = [n.id]; + const part = []; + seen.add(n.id); + while (stack.length) { + const id = stack.pop(); + const node = byId.get(id); + if (node) part.push(node); + for (const next of graph.get(id) || []) { + if (seen.has(next)) continue; + seen.add(next); + stack.push(next); + } + } + if (part.length && part.some(x => x.alive)) parts.push(part.sort(lineageNodeSort)); + } + return parts; +} + +function lineageExpandComponent(component, family) { + family = lineageEdgeFamily(family); + const ids = new Set(component.filter(Boolean).map(n => n.id)); + let changed = true; + while (changed) { + changed = false; + for (const id of Array.from(ids)) { + const n = family[id]; + if (!n) continue; + for (const p of lineageMutualParentIds(n, family)) { + if (!ids.has(p)) { ids.add(p); changed = true; } + } + for (const c of lineageMutualChildIds(n, family)) { + if (!ids.has(c)) { ids.add(c); changed = true; } + } + } + } + return Array.from(ids).map(id => family[id]).filter(Boolean); +} + +function lineageCleanComponent(component, family) { + family = lineageEdgeFamily(family); + const expanded = lineageExpandComponent(component, family); + const idSet = new Set(expanded.map(n => n.id)); + return expanded.filter(n => lineageMutualParentIds(n, family, idSet).length || lineageMutualChildIds(n, family, idSet).length); +} + +function buildLineageTreeLayout(component, family) { + family = lineageEdgeFamily(family); + const cleaned = lineageCleanComponent(component, family); + const nodes = Array.from(new Map(cleaned.filter(Boolean).map(n => [n.id, n])).values()).sort(lineageNodeSort); + const idToNode = new Map(nodes.map(n => [n.id, n])); + const idSet = new Set(idToNode.keys()); + const generations = Array.from(new Set(nodes.map(n => n.generation || 1))).sort((a, b) => a - b); + const rows = lineageRowsForLayout(nodes, generations, idToNode, idSet); + const layout = lineageLayoutRows(rows, idSet); + const links = lineageBuildLinks(nodes, idSet, layout); + + const labels = generations.map((g, rowIndex) => ({ generation: g, y: layout.topPad + rowIndex * (layout.nodeH + layout.rowGap) + 18 })); + const renderedNodes = nodes.map(n => ({ node: n, ...(layout.positions.get(n.id) || {}) })).filter(n => Number.isFinite(n.x) && Number.isFinite(n.y)); + return { ...layout, labels, nodes: renderedNodes, childLinks: links.childLinks, partnerLinks: links.partnerLinks }; } function lineageFamilyHtml(component, index, family) { - const nodeSort = (a, b) => - (a.generation || 1) - (b.generation || 1) || - (a.birthTime || 0) - (b.birthTime || 0) || - String(a.name || "").localeCompare(String(b.name || "")) || - String(a.id || "").localeCompare(String(b.id || "")); - const uniqueComponent = Array.from(new Map(component.filter(Boolean).map(n => [n.id, n])).values()).sort(nodeSort); - const byGeneration = new Map(); - for (const n of uniqueComponent) { - const key = n.generation || 1; - if (!byGeneration.has(key)) byGeneration.set(key, []); - byGeneration.get(key).push(n); - } - const generationBlocks = Array.from(byGeneration.entries()).sort((a, b) => a[0] - b[0]).map(([generation, nodes]) => ` -
- \u4e16\u4ee3 ${generation} -
${nodes.sort(nodeSort).map(lineagePersonHtml).join("")}
-
- `).join(""); - - const pairMap = new Map(); - for (const child of uniqueComponent) { - const parents = (child.parents || []).filter(id => family[id]); - if (!parents.length) continue; - const key = parents.slice().sort().join("+"); - if (!pairMap.has(key)) pairMap.set(key, { parents: parents.slice().sort(), children: [] }); - pairMap.get(key).children.push(child.id); - } - const pairs = Array.from(pairMap.values()).sort((a, b) => { - const achild = family[a.children[0]]; - const bchild = family[b.children[0]]; - return nodeSort(achild, bchild); - }); - const pairCards = pairs.length ? pairs.map(pair => { - const parents = pair.parents.map(id => family[id]).filter(Boolean).sort(nodeSort); - const children = pair.children.map(id => family[id]).filter(Boolean).sort(nodeSort); - return `
-
${parents.map(n => lineageMiniHtml(n, "parent")).join('+')}
-
\u2193
-
${children.map(n => lineageMiniHtml(n, "child")).join("")}
-
`; - }).join("") : `
\u307e\u3060\u89aa\u5b50\u95a2\u4fc2\u306f\u8a18\u9332\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002
`; + const uniqueComponent = Array.from(new Map(component.filter(Boolean).map(n => [n.id, n])).values()).sort(lineageNodeSort); + const layout = buildLineageTreeLayout(uniqueComponent, family); const aliveCount = uniqueComponent.filter(n => n.alive).length; - return `
-

\u5bb6\u7cfb ${index + 1} / ${uniqueComponent.length}\u5339 / \u751f\u5b58 ${aliveCount}

-
-
${generationBlocks}
-
-
\u7e41\u6b96\u30da\u30a2
-
${pairCards}
-
+ const labels = layout.labels.map(label => `
世代 ${label.generation}
`).join(""); + const partnerPaths = layout.partnerLinks.map(d => ``).join(""); + const childPaths = layout.childLinks.map(d => ``).join(""); + const nodeHtml = layout.nodes.map(p => lineageTreeNodeHtml(p.node, p.x, p.y)).join(""); + const empty = !layout.childLinks.length ? `
親子関係の線はまだありません。
` : ""; + + return `
+

家系 ${index + 1} / ${uniqueComponent.length}匹 / 生存 ${aliveCount}

+
+ + ${labels}${nodeHtml}${empty}
`; } @@ -148,19 +966,47 @@ function renderArchive() { if (world.normalizeFamily) world.normalizeFamily(); const family = world.family || {}; const ids = Object.keys(family); - const version = `${world.familyVersion || 0}:${ids.length}`; + const edgeCount = ids.reduce((sum, id) => { + const n = family[id]; + return sum + (Array.isArray(n?.parents) ? n.parents.length : 0) + (Array.isArray(n?.children) ? n.children.length : 0); + }, 0); + const version = `${world.familyVersion || 0}:${ids.length}:${edgeCount}`; if (uiCache.archiveVersion === version) return; uiCache.archiveVersion = version; uiCache.lastArchiveWindow = ""; - const components = liveFamilyComponents(family); + const components = liveFamilyComponents(family).flatMap(lineageSplitByActualEdges).map(c => lineageCleanComponent(c, family)).filter(c => c.length); uiCache.archiveRows = components; + const token = `${version}:${Math.random().toString(36).slice(2)}`; + uiCache.archiveRenderToken = token; if (!components.length) { - ui.archiveContent.innerHTML = `
\u751f\u304d\u3066\u3044\u308b\u69cb\u6210\u54e1\u3092\u6301\u3064\u5bb6\u7cfb\u306f\u307e\u3060\u3042\u308a\u307e\u305b\u3093\u3002
`; + ui.archiveContent.innerHTML = `
生きている構成員を持つ家系はまだありません。
`; return; } - const shown = components.reduce((a, c) => a + c.length, 0); - const note = `
${components.length} \u4ef6\u306e\u5bb6\u7cfb / ${shown} \u5339\u3092\u8868\u793a\u3002\u5168\u6ec5\u3057\u305f\u5bb6\u7cfb\u306f\u975e\u8868\u793a\u3067\u3059\u3002
`; - ui.archiveContent.innerHTML = `${note}
${components.map((c, i) => lineageFamilyHtml(c, i, family)).join("")}
`; + + ui.archiveContent.innerHTML = `
家系図を描画中… 0 / ${components.length}
`; + const status = ui.archiveContent.querySelector(".lineage-render-status"); + const forest = ui.archiveContent.querySelector(".lineage-forest"); + let index = 0; + const pump = () => { + if (uiCache.archiveRenderToken !== token || !forest) return; + const started = performance.now(); + let nodeBudget = 180; + while (index < components.length) { + const component = components[index]; + forest.insertAdjacentHTML("beforeend", lineageFamilyHtml(component, index, family)); + nodeBudget -= Math.max(1, component.length); + index += 1; + if (index < components.length && (nodeBudget <= 0 || performance.now() - started > 14)) break; + } + if (status) status.textContent = `家系図を描画中… ${index} / ${components.length}`; + if (index < components.length) { + const schedule = window.requestIdleCallback || ((fn) => setTimeout(fn, 0)); + schedule(pump, { timeout: 120 }); + } else if (status) { + status.remove(); + } + }; + pump(); } function validateFamilyTree() { @@ -173,6 +1019,11 @@ function validateFamilyTree() { for (const [id, count] of rendered.entries()) { if (count > 1) issues.push({ type: "duplicate-render-node", id, count }); } + for (const component of uiCache.archiveRows || []) { + for (const n of component || []) { + if (n?.id && !rendered.has(n.id)) issues.push({ type: "missing-render-node", id: n.id }); + } + } return issues; } diff --git a/js/world.js b/js/world.js index 9bf36e1..b5134aa 100644 --- a/js/world.js +++ b/js/world.js @@ -29,7 +29,7 @@ class World { this.familyVersion = 0; this.relationNotices = {}; this.resolvedFightIds = {}; - this.pointer = { x: 0, y: 0, inside: false }; + this.pointer = { x: 0, y: 0, inside: false, motion: 0, movedAt: 0 }; this.lastPhase = ""; this.weather = "sunny"; this.fieldType = "garden"; @@ -85,8 +85,49 @@ class World { return Math.max(0.22, field.worldScale || 1); } + grassLimit() { + const field = this.fieldDefinition(); + if (Number.isFinite(field?.grassLimit)) return Math.max(0, Math.round(field.grassLimit)); + const base = CONFIG.grassLimit ?? 96; + const mediumScale = Math.max(0.22, FIELD_TYPES?.garden?.worldScale || 1); + const ratio = this.fieldWorldScale() / mediumScale; + // 中サイズを CONFIG.grassLimit の基準にし、フィールドの論理サイズに応じて増減する。 + return Math.max(12, Math.round(base * ratio)); + } + + baseZoomMin() { + return CONFIG.fieldZoomMin || 0.42; + } + + baseZoomMax() { + return CONFIG.fieldZoomMax || 2.45; + } + + fitFieldZoom() { + const vw = Math.max(1, this.viewportW || this.w || 1000); + const vh = Math.max(1, this.viewportH || this.h || 720); + const ww = Math.max(1, this.w || vw); + const wh = Math.max(1, this.h || vh); + // The minimum visible zoom is field-size dependent. Small fields must not + // be allowed to shrink below the viewport, otherwise empty margins appear. + return Math.max(vw / ww, vh / wh); + } + + zoomLimits() { + const min = Math.max(this.baseZoomMin(), this.fitFieldZoom()); + // Keep the old upper bound for normal/large fields, but give small fields + // proportional zoom-in headroom because their no-margin minimum is higher. + const max = Math.max(this.baseZoomMax(), min * this.baseZoomMax()); + return { min, max }; + } + + normalizedFieldZoom(value = this.fieldZoom) { + const limits = this.zoomLimits(); + return clamp(Number(value) || limits.min, limits.min, limits.max); + } + viewScale() { - return clamp(Number(this.fieldZoom) || 1, 0.42, 2.45); + return this.normalizedFieldZoom(this.fieldZoom); } visibleWorldW() { @@ -161,6 +202,7 @@ class World { const scale = Math.max(requestedScale, 220 / this.viewportW, 180 / this.viewportH); this.w = this.viewportW * scale; this.h = this.viewportH * scale; + this.fieldZoom = this.normalizedFieldZoom(this.fieldZoom); this.clampCamera(); if (opts.scaleContents && oldW > 0 && oldH > 0) { const sx = this.w / oldW; @@ -183,15 +225,20 @@ class World { } setFieldZoom(nextZoom) { - const next = clamp(Number(nextZoom) || 1, 0.42, 2.45); - if (Math.abs(next - (this.fieldZoom || 1)) < 0.001) return false; + const current = this.normalizedFieldZoom(this.fieldZoom); + const next = this.normalizedFieldZoom(nextZoom); + if (Math.abs(next - current) < 0.001) { + this.fieldZoom = current; + this.clampCamera(); + return false; + } this.fieldZoom = next; this.clampCamera(); this.drawListDirty = true; return true; } - edgeSpawnPoint(targetX = null, targetY = null, margin = 34) { + edgeSpawnPoint(targetX = null, targetY = null, margin = 34, outside = false) { const w = Math.max(this.w || 1000, margin * 2 + 1); const h = Math.max(this.h || 720, margin * 2 + 1); let side; @@ -201,19 +248,20 @@ class World { } else { side = Math.floor(Math.random() * 4); } - if (side === 0) return { x: clamp(targetX ?? rand(margin, w - margin), margin, w - margin), y: margin, vx: rand(-10, 10), vy: rand(18, 34), angle: Math.PI / 2 }; - if (side === 1) return { x: w - margin, y: clamp(targetY ?? rand(margin, h - margin), margin, h - margin), vx: rand(-34, -18), vy: rand(-10, 10), angle: Math.PI }; - if (side === 2) return { x: clamp(targetX ?? rand(margin, w - margin), margin, w - margin), y: h - margin, vx: rand(-10, 10), vy: rand(-34, -18), angle: -Math.PI / 2 }; - return { x: margin, y: clamp(targetY ?? rand(margin, h - margin), margin, h - margin), vx: rand(18, 34), vy: rand(-10, 10), angle: 0 }; + const p = outside ? -(margin + 24) : margin; + if (side === 0) return { x: clamp(targetX ?? rand(margin, w - margin), margin, w - margin), y: p, vx: rand(-10, 10), vy: rand(28, 48), angle: Math.PI / 2 }; + if (side === 1) return { x: outside ? w - p : w - margin, y: clamp(targetY ?? rand(margin, h - margin), margin, h - margin), vx: rand(-48, -28), vy: rand(-10, 10), angle: Math.PI }; + if (side === 2) return { x: clamp(targetX ?? rand(margin, w - margin), margin, w - margin), y: outside ? h - p : h - margin, vx: rand(-10, 10), vy: rand(-48, -28), angle: -Math.PI / 2 }; + return { x: p, y: clamp(targetY ?? rand(margin, h - margin), margin, h - margin), vx: rand(28, 48), vy: rand(-10, 10), angle: 0 }; } toolSizeFor(type = "") { - if (!["firecracker", "fence_v", "fence_h", "stone", "grass", "sweet", "zunchi"].includes(type)) return "medium"; + if (!["firecracker", "fence_v", "fence_h", "stone", "grass", "sweet", "love_mochi", "fight_mochi", "zunchi"].includes(type)) return "medium"; return (this.toolSizes && this.toolSizes[type]) || this.toolSize || "medium"; } toolSizeScale(type = "") { - if (!["firecracker", "fence_v", "fence_h", "stone", "grass", "sweet", "zunchi"].includes(type)) return 1; + if (!["firecracker", "fence_v", "fence_h", "stone", "grass", "sweet", "love_mochi", "fight_mochi", "zunchi"].includes(type)) return 1; return { small: 0.68, medium: 1.0, large: 1.48 }[this.toolSizeFor(type)] || 1; } @@ -224,7 +272,7 @@ class World { item.toolSize = size; this.toolSize = size; item.r *= scale; - if (["sweet", "grass", "zunchi"].includes(item.type)) item.amount *= scale * scale; + if (["sweet", "love_mochi", "fight_mochi", "grass", "zunchi"].includes(item.type)) item.amount *= scale * scale; if (item.type === "firecracker") item.blastScale = scale; return item; } @@ -256,7 +304,7 @@ class World { this.familyVersion = 0; this.relationNotices = {}; this.resolvedFightIds = {}; - this.pointer = { x: this.w / 2, y: this.h / 2, inside: false }; + this.pointer = { x: this.w / 2, y: this.h / 2, inside: false, motion: 0, movedAt: 0 }; this.cameraX = Math.max(0, (this.w - (this.viewportW || this.w)) / 2); this.cameraY = Math.max(0, (this.h - (this.viewportH || this.h)) / 2); this.clampCamera(); @@ -425,6 +473,20 @@ class World { return changed; } + resetFamilyTree() { + this.family = {}; + this.familyCleanVersion = 0; + this.familyVersion = (this.familyVersion || 0) + 1; + this.maxGeneration = 1; + for (const t of this.tarinai || []) { + t.parents = []; + t.parentNames = []; + t.children = []; + t.generation = 1; + t.hasPaired = false; + } + } + validateFamily() { this.normalizeFamily(); const family = this.family || {}; @@ -516,16 +578,16 @@ class World { startBirthRitual(a, b) { if (!a || !b || a.dead || b.dead) return false; + if (a.isZunchiSlave || b.isZunchiSlave) return false; if (a.birthRitualTimer > 0.04 || b.birthRitualTimer > 0.04) return false; const aJoined = a.familyJoined ? a.familyJoined() : Boolean((a.parents || []).length || (a.children || []).length); const bJoined = b.familyJoined ? b.familyJoined() : Boolean((b.parents || []).length || (b.children || []).length); if (aJoined && !bJoined) b.generation = a.generation; else if (bJoined && !aJoined) a.generation = b.generation; else if (!aJoined && !bJoined) { a.generation = 1; b.generation = 1; } - a.hasPaired = true; - b.hasPaired = true; - this.recordFamily(a); - this.recordFamily(b); + // Do not mark either parent as part of a family during the ritual. + // The archive should only gain nodes after a child has actually been created; + // otherwise childless ritual participants appear as isolated family trees. const duration = 10.0; a.birthRitualTimer = b.birthRitualTimer = duration; a.birthRitualMax = b.birthRitualMax = duration; @@ -568,6 +630,29 @@ class World { return children[0] || null; } + areParentChild(a, b) { + if (!a || !b) return false; + const aId = a.id || a; + const bId = b.id || b; + if (!aId || !bId) return false; + return Boolean((a.parents || []).includes(bId) || (a.children || []).includes(bId) || (b.parents || []).includes(aId) || (b.children || []).includes(aId)); + } + + areCoParents(a, b) { + if (!a || !b) return false; + const aId = a.id || a; + const bId = b.id || b; + if (!aId || !bId || aId === bId) return false; + if (a.birthPartnerId && a.birthPartnerId === bId) return true; + if (b.birthPartnerId && b.birthPartnerId === aId) return true; + const aChildren = new Set((a.children || []).filter(Boolean)); + if (!aChildren.size) return false; + for (const id of (b.children || [])) { + if (aChildren.has(id)) return true; + } + return false; + } + spawnChild(a, b, index = 0, total = 1) { const inherited = Math.random() < 0.56 ? a.type : b.type; const mutation = Math.random() < 0.18 ? baseSpriteId() : inherited; @@ -590,8 +675,14 @@ class World { birthTime: this.time, }); this.maxGeneration = Math.max(this.maxGeneration, child.generation); + a.hasPaired = true; + b.hasPaired = true; a.affection = 0; b.affection = 0; a.loneliness *= 0.65; b.loneliness *= 0.65; + a.adjustRelation?.(b, 0.65, -0.20, "co_parent"); + b.adjustRelation?.(a, 0.65, -0.20, "co_parent"); + a.postBirthPeaceTimer = Math.max(a.postBirthPeaceTimer || 0, 28); + b.postBirthPeaceTimer = Math.max(b.postBirthPeaceTimer || 0, 28); a.children.push(child.id); b.children.push(child.id); this.recordFamily(a); @@ -607,16 +698,18 @@ class World { startConflict(a, b) { if (!a || !b || a.dead || b.dead) return; + if (this.areParentChild(a, b) || this.areCoParents?.(a, b)) return; if ((a.postBirthPeaceTimer || 0) > 0 || (b.postBirthPeaceTimer || 0) > 0) return; if (a.fightTimer > 0.04 || b.fightTimer > 0.04 || a.intimidateTimer > 0.04 || b.intimidateTimer > 0.04) return; - const scoreA = a.energy * 0.38 + a.mood * 0.18 + a.stress * 0.12 + (a.type === "angry" ? 12 : 0) + (a.personality === "irritable" ? 8 : 0) + ((b.relationTo(a.id).fear || 0) * 0.65) + rand(-5, 5); - const scoreB = b.energy * 0.38 + b.mood * 0.18 + b.stress * 0.12 + (b.type === "angry" ? 12 : 0) + (b.personality === "irritable" ? 8 : 0) + ((a.relationTo(b.id).fear || 0) * 0.65) + rand(-5, 5); + const scoreA = a.energy * 0.38 + a.mood * 0.18 + a.stress * 0.12 + (a.type === "angry" ? 12 : 0) + (a.personality === "irritable" ? 8 : 0) + ((b.relationTo(a.id).fear || 0) * 0.65) + (a.isZunchiSlave ? -10 : 0) + ((a.fightMochiTimer || 0) > 0.04 ? 9 : 0) + rand(-5, 5); + const scoreB = b.energy * 0.38 + b.mood * 0.18 + b.stress * 0.12 + (b.type === "angry" ? 12 : 0) + (b.personality === "irritable" ? 8 : 0) + ((a.relationTo(b.id).fear || 0) * 0.65) + (b.isZunchiSlave ? -10 : 0) + ((b.fightMochiTimer || 0) > 0.04 ? 9 : 0) + rand(-5, 5); const actor = scoreA >= scoreB ? a : b; const target = actor === a ? b : a; const actorScore = actor === a ? scoreA : scoreB; const targetScore = actor === a ? scoreB : scoreA; const canIntimidate = !actor.lowHealthSprite || !actor.lowHealthSprite(); - const intimidationChance = 0.40; + const battleDrug = (actor.fightMochiTimer || 0) > 0.04 || (target.fightMochiTimer || 0) > 0.04; + const intimidationChance = clamp(0.40 + (battleDrug ? 0.24 : 0) + (target.isZunchiSlave ? 0.18 : 0), 0.24, 0.90); if (canIntimidate && Math.random() < intimidationChance) { this.startIntimidation(actor, target, actorScore, targetScore); return; @@ -686,8 +779,8 @@ class World { b.fightTargetIds = b.fightTargetIds.slice(-4); a.fightTargetId = a.fightTargetIds[0]; b.fightTargetId = b.fightTargetIds[0]; - const aScore = a.energy * 0.52 + a.mood * 0.30 + (a.type === "angry" ? 10 : 0) + (a.personality === "irritable" ? 5 : 0) + rand(-7, 7); - const bScore = b.energy * 0.52 + b.mood * 0.30 + (b.type === "angry" ? 10 : 0) + (b.personality === "irritable" ? 5 : 0) + rand(-7, 7); + const aScore = a.energy * 0.52 + a.mood * 0.30 + (a.type === "angry" ? 10 : 0) + (a.personality === "irritable" ? 5 : 0) + ((a.fightMochiTimer || 0) > 0.04 ? 12 : 0) + (a.isZunchiSlave ? -14 : 0) + rand(-7, 7); + const bScore = b.energy * 0.52 + b.mood * 0.30 + (b.type === "angry" ? 10 : 0) + (b.personality === "irritable" ? 5 : 0) + ((b.fightMochiTimer || 0) > 0.04 ? 12 : 0) + (b.isZunchiSlave ? -14 : 0) + rand(-7, 7); const loser = aScore <= bScore ? a : b; const winner = loser === a ? b : a; loser.defeatedById = winner.id; @@ -739,6 +832,15 @@ class World { } for (const t of this.tarinai) { if (t.dead) continue; + const wasSleeping = t.state === "sleep" || t.sleeping || t.state === "seek_bed"; + t.sleeping = false; + if (wasSleeping) { + t.state = "panic"; + t.target = { x, y, dead: false }; + t.surpriseTimer = Math.max(t.surpriseTimer, 0.95); + t.fearTimer = Math.max(t.fearTimer, 1.2); + t.thought = "爆竹でたたき起こされた"; + } t.stress = clamp(t.stress + rand(8, 15), 0, 130); t.fearTimer = Math.max(t.fearTimer, 0.58); const dx = t.x - x; @@ -764,8 +866,35 @@ class World { this.spawnFallEffect(t.x, t.y + t.radius * 0.45, 1.1 + p); if (!t.dead && Math.random() < 0.45) this.spawnBubble(t.x, t.y - t.radius * 1.35, "\u306f\u3041\u3041\u3041\uff01", "rgba(84,66,86,0.80)"); } + let blastedBalls = 0; + for (const ball of this.items) { + if (!ball || ball.dead || ball.type !== "ball") continue; + let dx = ball.x - x; + let dy = ball.y - y; + let d = Math.hypot(dx, dy) || 1; + if (d > blastRadius * 1.18) continue; + const p = clamp(1 - d / (blastRadius * 1.18), 0, 1); + if (d < 0.001) { + const a = rand(0, Math.PI * 2); + dx = Math.cos(a); dy = Math.sin(a); d = 1; + } + const blast = 430 + p * 960; + ball.vx = (ball.vx || 0) + dx / d * blast + rand(-80, 80); + ball.vy = (ball.vy || 0) + dy / d * blast + rand(-80, 80); + const speed = Math.hypot(ball.vx || 0, ball.vy || 0); + const cap = 1250; + if (speed > cap) { + ball.vx = ball.vx / speed * cap; + ball.vy = ball.vy / speed * cap; + } + ball.spinVelocity = clamp((ball.spinVelocity || 0) + rand(-24, 24), -38, 38); + ball.lastPokedAt = this.time || 0; + ball.pokeCombo = Math.max(ball.pokeCombo || 0, 5); + this.effects.push(new Effect("ring", ball.x, ball.y, { size: Math.max(18, ball.r * 1.2), life: 0.24, color: "rgba(255,230,116,0.58)" })); + blastedBalls += 1; + } audio.explode(); - this.log("\u7206\u7af9\u304c\u6d3e\u624b\u306b\u306f\u3058\u3051\u3001\u5ead\u304c\u3056\u308f\u3064\u3044\u305f\u3002", "accident"); + this.log(blastedBalls ? "爆竹が派手にはじけ、ボールが急加速した。" : "爆竹が派手にはじけ、庭がざわついた。", "accident"); } spawnZunchi(x, y) { @@ -845,8 +974,12 @@ class World { })); } + isSleepFurniture(it) { + return Boolean(it && it.type === "bed"); + } + bedOccupancy(bed) { - if (!bed || bed.type !== "bed") return 0; + if (!this.isSleepFurniture(bed)) return 0; let n = 0; const radius = Math.max(86, bed.r * 2.45); for (const t of this.nearbyTarinai(bed.x, bed.y, radius)) { @@ -857,7 +990,7 @@ class World { } bedComfort(bed) { - if (!bed || bed.type !== "bed") return 0.7; + if (!this.isSleepFurniture(bed)) return 0.7; const occ = this.bedOccupancy(bed); return clamp((bed.comfort ?? 1) - Math.max(0, occ - 3) * 0.075 - (bed.wear || 0) * 0.18, 0.42, 1.18); } @@ -866,12 +999,13 @@ class World { let best = null; let bestScore = Infinity; for (const bed of this.nearbyItems(t.x, t.y, maxDist)) { - if (bed.dead || bed.type !== "bed") continue; + if (bed.dead || !this.isSleepFurniture(bed)) continue; if (t?.shouldAvoidTarget && t.shouldAvoidTarget(bed)) continue; const d = dist(t, bed); const occ = this.bedOccupancy(bed); const comfort = this.bedComfort(bed); - const score = d - comfort * 46 + Math.max(0, occ - 3) * 34; + const crowdPenalty = Math.max(0, occ - 3) * 34; + const score = d - comfort * 46 + crowdPenalty; if (score < bestScore) { best = bed; bestScore = score; } } return best; @@ -901,6 +1035,12 @@ class World { loser.adjustRelation(winner, -1.2, 7.2 * loser.personalityProfile().fear, "\u55a7\u5629\u306b\u8ca0\u3051\u305f"); winner.relationTo(loser.id).fightsWon = (winner.relationTo(loser.id).fightsWon || 0) + 1; loser.relationTo(winner.id).fightsLost = (loser.relationTo(winner.id).fightsLost || 0) + 1; + loser.totalFightLosses = (loser.totalFightLosses || 0) + 1; + if ((loser.totalFightLosses || 0) >= 5 && loser.becomeZunchiSlave?.(winner)) { + const oldName = loser.formerName || loser.name; + this.spawnBubble(loser.x, loser.y - loser.radius * 1.20, "ぶりゅ…", "rgba(74,91,50,0.78)"); + this.log(`${oldName}は5回喧嘩に負け、ずんちどれいになった。`, "fight"); + } loser.fearTimer = Math.max(loser.fearTimer, 1.2 * loser.personalityProfile().fear); this.spawnBubble(loser.x, loser.y - loser.radius * 1.28, "\u306f\u3041\u3041\u3041\uff01", "rgba(84,66,86,0.80)"); winner.affection = clamp(winner.affection + 0.8, 0, 80); @@ -939,6 +1079,183 @@ class World { if (hit > 0) this.log(`${isStone ? "\u77f3" : "\u9053\u5177"}\u304c\u843d\u3061\u3001${hit}\u5339\u306e\u305f\u308a\u306a\u3044\u304c\u5dfb\u304d\u8fbc\u307e\u308c\u305f\u3002`, "accident"); } + countBallChasers(ball, except = null) { + if (!ball) return 0; + let count = 0; + for (const t of this.tarinai || []) { + if (!t || t === except || t.dead) continue; + if (t.state === "play_ball" && t.target === ball) count += 1; + } + return count; + } + + limitBallChasers() { + if (!this.itemCounts?.ball) return; + const groups = new Map(); + for (const t of this.tarinai || []) { + if (!t || t.dead || t.state !== "play_ball" || !t.target || t.target.dead || t.target.type !== "ball") continue; + const arr = groups.get(t.target) || []; + arr.push(t); + groups.set(t.target, arr); + } + for (const [ball, arr] of groups) { + if (arr.length <= 5) continue; + arr.sort((a, b) => { + const ap = a.personality === "playful" ? -120 : 0; + const bp = b.personality === "playful" ? -120 : 0; + return dist(a, ball) + ap - (dist(b, ball) + bp); + }); + for (const t of arr.slice(5)) { + t.state = "idle"; + t.target = null; + t.thought = "ボールが混み合っているので眺めている"; + t.wanderAngle = Math.atan2(t.y - ball.y, t.x - ball.x) + rand(-0.45, 0.45); + } + } + } + + pokeBall(ball, x, y) { + if (!ball || ball.dead || ball.type !== "ball") return false; + const now = (typeof performance !== "undefined" && performance.now) ? performance.now() / 1000 : (this.time || 0); + let dx = ball.x - x; + let dy = ball.y - y; + let d = Math.hypot(dx, dy); + if (d < 0.001) { + const a = (ball.lastPokeAngle ?? rand(0, Math.PI * 2)) + rand(-0.55, 0.55); + dx = Math.cos(a); + dy = Math.sin(a); + d = 1; + } + const nx = dx / d; + const ny = dy / d; + const recent = now - (ball.lastPokedAt || -999) < 0.82; + ball.pokeCombo = recent ? Math.min(10, (ball.pokeCombo || 0) + 1) : 1; + ball.lastPokedAt = now; + ball.lastPokeAngle = Math.atan2(ny, nx); + const currentSpeed = Math.hypot(ball.vx || 0, ball.vy || 0); + const impulse = 145 + ball.pokeCombo * 64 + Math.min(260, currentSpeed * 0.24); + ball.vx = (ball.vx || 0) + nx * impulse; + ball.vy = (ball.vy || 0) + ny * impulse; + const speed = Math.hypot(ball.vx || 0, ball.vy || 0); + const cap = 1250; + if (speed > cap) { + ball.vx = ball.vx / speed * cap; + ball.vy = ball.vy / speed * cap; + } + ball.spinVelocity = clamp((ball.spinVelocity || 0) + (nx >= 0 ? 1 : -1) * (5.6 + ball.pokeCombo * 1.15), -38, 38); + ball.amount = Math.max(ball.amount || 999, 999); + audio.poke(); + this.effects.push(new Effect("ring", ball.x, ball.y, { size: Math.max(16, ball.r * (0.95 + ball.pokeCombo * 0.05)), life: 0.22, color: "rgba(255,255,255,0.58)" })); + if (ball.pokeCombo >= 4 || speed > 430) this.log(`ボールを連続でつつき、勢いが増した。`, "observe"); + else this.log(`ボールをつついて弾いた。`, "observe"); + return true; + } + + resolveBallInteractions(dt) { + if (!this.itemCounts?.ball) return; + for (const ball of this.items) { + if (!ball || ball.dead || ball.type !== "ball") continue; + const ballSpeed = Math.hypot(ball.vx || 0, ball.vy || 0); + const sweep = Math.min(420, ballSpeed * Math.max(0.016, dt || 0.016) + 84); + const range = (ball.r || 18) + 72 + sweep; + const px = Number.isFinite(ball.prevX) ? ball.prevX : ball.x; + const py = Number.isFinite(ball.prevY) ? ball.prevY : ball.y; + const sx = ball.x - px; + const sy = ball.y - py; + const segLenSq = sx * sx + sy * sy; + for (const t of this.nearbyTarinai(ball.x, ball.y, range)) { + if (!t || t.dead || t.state === "sleep") continue; + let hitX = ball.x; + let hitY = ball.y; + if (segLenSq > 1) { + const u = clamp(((t.x - px) * sx + (t.y - py) * sy) / segLenSq, 0, 1); + hitX = px + sx * u; + hitY = py + sy * u; + } + let d = Math.max(0.001, distXY(hitX, hitY, t.x, t.y)); + const hitDistance = (ball.r || 18) + t.radius * 0.74; + if (d > hitDistance) continue; + const now = this.time || 0; + const repeat = ball.lastKickerId === t.id && now - (ball.lastKickedAt || -999) < 0.18; + if (repeat && ballSpeed < 300) continue; + let nx = (hitX - t.x) / d; + let ny = (hitY - t.y) / d; + if (!Number.isFinite(nx) || !Number.isFinite(ny) || Math.abs(nx) + Math.abs(ny) < 0.001) { + if (ballSpeed > 0.01) { + nx = (ball.vx || 0) / ballSpeed; + ny = (ball.vy || 0) / ballSpeed; + } else { + nx = t.facingDir ? t.facingDir() : 1; + ny = rand(-0.22, 0.22); + } + } + + const isDangerous = ballSpeed >= 320; + if (isDangerous && now - (t.lastBallDamageAt || -999) > 0.46) { + const bvx = ballSpeed > 0.01 ? (ball.vx || 0) / ballSpeed : nx; + const bvy = ballSpeed > 0.01 ? (ball.vy || 0) / ballSpeed : ny; + const damage = clamp((ballSpeed - 285) / 18, 3.5, 38); + const push = clamp(ballSpeed * 0.34, 95, 310); + t.lastBallDamageAt = now; + t.vx += bvx * push; + t.vy += bvy * push; + t.damage(damage, "超高速のボールに当たった"); + const timidStress = t.personality === "timid" ? 1.55 : 1.0; + t.stress = clamp(t.stress + clamp(damage * 0.55 * timidStress, 3, t.personality === "timid" ? 26 : 18), 0, 130); + t.hurtTimer = Math.max(t.hurtTimer, damage > 16 ? 2.4 : 1.35); + t.fallTimer = Math.max(t.fallTimer, damage > 14 ? 0.82 : 0.38); + t.fallMax = Math.max(t.fallMax || 0.82, t.fallTimer); + t.fallDir = bvx >= 0 ? 1 : -1; + t.fearTimer = Math.max(t.fearTimer, 0.8); + this.spawnFallEffect(t.x, t.y + t.radius * 0.55, clamp(damage / 18, 0.55, 1.6)); + this.effects.push(new Effect("ring", hitX, hitY, { size: Math.max(20, ball.r * 1.25), life: 0.22, color: "rgba(210,75,65,0.50)" })); + if (this.relationNotice(t.id, ball.id || "ball", "fast-ball-hit", 2.8)) this.log(`${t.name}は超高速のボールに当たってダメージを受けた。`, "accident"); + ball.vx = (ball.vx || 0) * 0.58 - nx * Math.min(110, ballSpeed * 0.10); + ball.vy = (ball.vy || 0) * 0.58 - ny * Math.min(110, ballSpeed * 0.10); + ball.spinVelocity = clamp((ball.spinVelocity || 0) + (nx >= 0 ? -1 : 1) * 8.5, -38, 38); + if (t.dead) continue; + } + + const relVx = (t.vx || 0) - (ball.vx || 0); + const relVy = (t.vy || 0) - (ball.vy || 0); + const relativeSpeed = Math.hypot(relVx, relVy); + const playIntent = t.state === "play_ball" || t.target === ball; + const playful = t.personality === "playful"; + const impulse = clamp(48 + relativeSpeed * 0.72 + (playIntent ? 52 : 0) + (playful ? 24 : 0), 48, isDangerous ? 360 : 260); + ball.vx = (ball.vx || 0) * (isDangerous ? 0.78 : 0.92) + nx * impulse + (t.vx || 0) * 0.52; + ball.vy = (ball.vy || 0) * (isDangerous ? 0.78 : 0.92) + ny * impulse + (t.vy || 0) * 0.52; + const kickedSpeed = Math.hypot(ball.vx || 0, ball.vy || 0); + const kickCap = 1250; + if (kickedSpeed > kickCap) { + ball.vx = ball.vx / kickedSpeed * kickCap; + ball.vy = ball.vy / kickedSpeed * kickCap; + } + ball.spinVelocity = clamp((ball.spinVelocity || 0) + (nx * (t.vy || 0) - ny * (t.vx || 0)) / 16 + impulse / Math.max(12, ball.r || 18) * (nx >= 0 ? 1 : -1), -38, 38); + ball.x = clamp(t.x + nx * (hitDistance + 2), CONFIG.worldPadding, this.w - CONFIG.worldPadding); + ball.y = clamp(t.y + ny * (hitDistance + 2), CONFIG.worldPadding, this.h - CONFIG.worldPadding); + ball.lastKickedAt = now; + ball.lastKickerId = t.id; + t.vx -= nx * Math.min(22, impulse * 0.10); + t.vy -= ny * Math.min(22, impulse * 0.10); + t.energy = clamp(t.energy - (playIntent ? 0.20 : 0.08), 0, 100); + const playRelief = playful && playIntent ? 2.8 : playful ? 1.5 : playIntent ? 0.75 : 0.20; + const timidBump = t.personality === "timid" && !playIntent ? clamp(ballSpeed / 190, 0.9, 3.8) : 0; + t.stress = clamp(t.stress - playRelief + timidBump, 0, 130); + if (playful && playIntent) { + t.loneliness = clamp(t.loneliness - 0.9, 0, 100); + t.affection = clamp(t.affection + 0.18, 0, 100); + } + t.goodMode = playIntent || playful ? "smile" : t.goodMode; + if (playIntent && now > (t.nextPlayBubbleAt || 0)) { + t.nextPlayBubbleAt = now + rand(2.0, 3.8); + this.spawnBubble(t.x, t.y - t.radius * 1.15, pick(["!", "はう", "?" ]), "rgba(70,96,50,0.76)"); + } + if (playful && this.relationNotice(t.id, ball.id || "ball", "ball-kick", 16)) this.log(`${t.name}はボールを追いかけて弾いた。`, "observe"); + this.effects.push(new Effect("ring", ball.x, ball.y, { size: Math.max(12, ball.r * 0.80), life: 0.20, color: "rgba(170,210,95,0.50)" })); + } + } + } + nearest(entity, types, maxDist = Infinity) { let best = null, bestD = maxDist; const candidates = Number.isFinite(maxDist) ? this.nearbyItems(entity.x, entity.y, maxDist) : this.items; @@ -1205,7 +1522,7 @@ class World { pruneOldest("zunchi", CONFIG.zunchiLimit ?? 56); pruneOldest("trace", CONFIG.traceLimit ?? 64); pruneOldest("splat", CONFIG.splatLimit ?? 48); - pruneOldest("grass", CONFIG.grassLimit ?? 96); + pruneOldest("grass", this.grassLimit ? this.grassLimit() : (CONFIG.grassLimit ?? 96)); const limit = CONFIG.itemLimit ?? Infinity; if (!Number.isFinite(limit) || this.items.length <= limit) return; @@ -1253,18 +1570,22 @@ class World { this.time += dt; this.day = Math.floor(this.time / CONFIG.dayLength) + 1; + if (this.pointer) this.pointer.motion = Math.max(0, (this.pointer.motion || 0) - dt * 2.4); this.updateWeather(dt); this.rebuildSpatial(); const perf = this.performanceLevel(); const itemCountBefore = this.items.length; for (const it of this.items) it.update(dt, this); + if (this.itemCounts?.ball) this.rebuildSpatial(); for (const ef of this.effects) ef.update(dt); for (const t of this.tarinai) { t.update(dt); } + this.limitBallChasers(); + this.resolveBallInteractions(dt); this.compactTimer += dt; const compactInterval = perf >= 2 ? 0.95 : 0.58; @@ -1297,16 +1618,17 @@ class World { audio.phase(); } - if (this.tarinai.length > 0 && this.tarinai.length < 5 && Math.random() < dt * 0.03) { - const spot = this.edgeSpawnPoint(); - const t = this.addTarinai({ x: spot.x, y: spot.y, vx: spot.vx, vy: spot.vy, type: baseSpriteId() }); + if (this.tarinai.length > 0 && this.tarinai.length <= 10 && Math.random() < dt * 0.022) { + const spot = this.edgeSpawnPoint(null, null, 42, true); + const t = this.addTarinai({ x: spot.x, y: spot.y, vx: spot.vx, vy: spot.vy, type: baseSpriteId(), entryTimer: 2.0 }); t.wanderAngle = spot.angle; - this.log("\u3069\u3053\u304b\u304b\u3089\u305f\u308a\u306a\u3044\u304c\u8ff7\u3044\u8fbc\u3093\u3067\u304d\u305f\u3002"); + t.surpriseTimer = Math.max(t.surpriseTimer || 0, 0.35); + this.log(`${t.name}が画面外から迷い込んできた。`); } const grassCount = this.itemCounts.grass || 0; - if (grassCount < Math.min(24, CONFIG.grassLimit ?? 96) && Math.random() < dt * 0.036) { + if (grassCount < Math.min(24, this.grassLimit ? this.grassLimit() : (CONFIG.grassLimit ?? 96)) && Math.random() < dt * 0.036) { const spot = this.findGrassPlantingSpot(rand(60, this.w - 60), rand(60, this.h - 60), { allowOriginal: true, minRadius: 28, @@ -1355,8 +1677,34 @@ class World { this.log(`\u5929\u6c17: ${weatherLabel(this.weather)}`); } + findGrabTargetAt(x, y) { + let foundT = null; + for (let i = this.tarinai.length - 1; i >= 0; i--) { + const t = this.tarinai[i]; + if (!t || t.dead) continue; + if (t.contains ? t.contains(x, y) : distXY(x, y, t.x, t.y) <= (t.radius || 22) * 1.4) { foundT = t; break; } + } + let foundItem = null, foundItemD = Infinity; + for (let i = this.items.length - 1; i >= 0; i--) { + const it = this.items[i]; + if (!it || it.dead || it.type === "trace" || it.type === "splat") continue; + const hit = Math.max(24, (it.r || 12) * (it.type === "ball" ? 4.0 : 2.2)); + const d = distXY(x, y, it.x, it.y); + if (d <= hit && d < foundItemD) { foundItem = it; foundItemD = d; } + } + if (foundT && foundItem) { + const td = distXY(x, y, foundT.x, foundT.y); + return td <= foundItemD + 8 ? foundT : foundItem; + } + return foundT || foundItem; + } + placeItem(item, dropped = true) { if (!item) return null; + if (item.type === "grass" && (this.itemCounts?.grass || 0) >= (this.grassLimit ? this.grassLimit() : (CONFIG.grassLimit ?? 96))) { + showToast("このフィールドの草の上限に達しています。"); + return null; + } if (dropped) { item.dropMax = item.type === "stone" ? 0.72 : 0.58; item.dropTimer = item.dropMax; @@ -1381,19 +1729,30 @@ class World { } if (tool === "poke") { + let ballTarget = null; + let ballTargetD = Infinity; + for (let i = this.items.length - 1; i >= 0; i--) { + const it = this.items[i]; + if (!it || it.dead || it.type !== "ball") continue; + const hit = Math.max(72, (it.r || 18) * 4.0); + const d = distXY(x, y, it.x, it.y); + if (d <= hit && d < ballTargetD) { ballTarget = it; ballTargetD = d; } + } + if (ballTarget && this.pokeBall(ballTarget, x, y)) return; + const target = [...this.tarinai].reverse().find(t => t.contains(x, y)); if (target) { target.poke(); - if (!target.dead) this.log(`${target.name}\u306f\u3064\u3064\u304b\u308c\u3001\u305f\u308a\u306a\u3044\u3053\u3068\u3092\u899a\u3048\u305f\u3002`); + if (!target.dead) this.log(`${target.name}はつつかれ、たりないことを覚えた。`); } else { - this.log("\u7a7a\u6c17\u3092\u3064\u3064\u3044\u305f\u3002\u305d\u3053\u306b\u306f\u4f55\u3082\u305f\u308a\u306a\u304b\u3063\u305f\u3002"); + this.log("空気をつついた。そこには何もたりなかった。"); } return; } if (tool === "new") { - const spot = this.edgeSpawnPoint(x, y); - const t = this.addTarinai({ x: spot.x, y: spot.y, vx: spot.vx, vy: spot.vy, type: baseSpriteId(), generation: 1 }); + const spot = this.edgeSpawnPoint(x, y, 42, true); + const t = this.addTarinai({ x: spot.x, y: spot.y, vx: spot.vx, vy: spot.vy, type: baseSpriteId(), generation: 1, entryTimer: 2.0 }); t.wanderAngle = spot.angle; audio.birth(); this.log(`${t.name}\u304c\u753b\u9762\u5916\u304b\u3089\u8ff7\u3044\u8fbc\u3093\u3067\u304d\u305f\u3002`); @@ -1415,14 +1774,14 @@ class World { this.compactItems(); this.updateItemCounts(); this.rebuildSpatial(); - const label = { zunchi: "\u305a\u3093\u3061", food: "\u98df\u3079\u7269", sweet: "\u305a\u3093\u3060\u9905", water: "\u6c34", grass: "\u8349", stone: "\u77f3", bed: "\u5e72\u8349\u5bdd\u5e8a", firecracker: "\u7206\u7af9", fence_v: "\u7e26\u306e\u67f5", fence_h: "\u6a2a\u306e\u67f5", trace: "\u8db3\u8de1", splat: "\u3057\u3076\u304d" }[best.type] || best.type; + const label = { zunchi: "\u305a\u3093\u3061", food: "\u98df\u3079\u7269", sweet: "\u305a\u3093\u3060\u9905", love_mochi: "\u5a9a\u85ac\u9905", fight_mochi: "\u55a7\u5629\u9905", water: "\u6c34", grass: "\u8349", stone: "\u77f3", bed: "\u5e72\u8349\u5bdd\u5e8a", ball: "\u30dc\u30fc\u30eb", firecracker: "\u7206\u7af9", fence_v: "\u7e26\u306e\u67f5", fence_h: "\u6a2a\u306e\u67f5", trace: "\u8db3\u8de1", splat: "\u3057\u3076\u304d" }[best.type] || best.type; this.log(`${label}\u3092\u524a\u9664\u3057\u305f\u3002`, "observe"); return; } const itemType = { - zunchi: "zunchi", sweet: "sweet", water: "water", grass: "grass", - stone: "stone", bed: "bed", firecracker: "firecracker", fence_v: "fence_v", fence_h: "fence_h" + zunchi: "zunchi", sweet: "sweet", love_mochi: "love_mochi", fight_mochi: "fight_mochi", water: "water", grass: "grass", + stone: "stone", bed: "bed", ball: "ball", firecracker: "firecracker", fence_v: "fence_v", fence_h: "fence_h" }[tool]; if (itemType) { let px = x, py = y; @@ -1436,9 +1795,10 @@ class World { px = spot.x; py = spot.y; } - this.placeItem(this.applyToolSize(new Item(itemType, px, py)), true); - const label = { zunchi: "\u305a\u3093\u3061", food: "\u98df\u3079\u7269", sweet: "\u305a\u3093\u3060\u9905", water: "\u6c34", grass: "\u8349", stone: "\u77f3", bed: "\u5e72\u8349\u5bdd\u5e8a", firecracker: "\u7206\u7af9", fence_v: "\u7e26\u306e\u67f5", fence_h: "\u6a2a\u306e\u67f5" }[itemType]; - this.log(`${label}\u3092\u914d\u7f6e\u3057\u305f\u3002`); + const placed = this.placeItem(this.applyToolSize(new Item(itemType, px, py)), true); + if (!placed) return; + const label = { zunchi: "ずんち", food: "食べ物", sweet: "ずんだ餅", love_mochi: "媚薬餅", fight_mochi: "喧嘩餅", water: "水", grass: "草", stone: "石", bed: "干草寝床", ball: "ボール", firecracker: "爆竹", fence_v: "縦の柵", fence_h: "横の柵" }[itemType]; + this.log(`${label}を配置した。`); } } @@ -1494,11 +1854,11 @@ class World { this.toolSizes = data.toolSizes || this.toolSizes || {}; this.maxGeneration = data.maxGeneration || 1; this.fieldType = FIELD_TYPES?.[data.fieldType]?.id || "garden"; - this.fieldZoom = clamp(Number(data.fieldZoom) || 1, 0.42, 2.45); + this.fieldZoom = Number(data.fieldZoom) || 1; this.cameraX = Number(data.cameraX) || 0; this.cameraY = Number(data.cameraY) || 0; this.tarinai = (data.tarinai || []).map(o => Tarinai.from(this, o)); - this.items = (data.items || []).filter(o => o?.type !== "mirror" && o?.type !== "food").map(o => Item.from(o)); + this.items = (data.items || []).filter(o => o?.type !== "mirror" && o?.type !== "food" && o?.type !== "cardboard").map(o => Item.from(o)); this.effects = []; this.compactItems(); this.compactEffects();