From a93802ebd94953cb39ac0795cf3b949bc4c1f533 Mon Sep 17 00:00:00 2001 From: MeatSlicer Developer Date: Thu, 30 Jul 2026 15:44:31 +1000 Subject: [PATCH] Phase 3: 12 hook passives with conditional damage, auras, and kill/room effects - dmgLiveMul live damage channel wired into fireWeapon, sawTick, fireBeam, orbitals - Dead Weight (execute), Cauterized Veins (burn bonus), Pain Engine, Iron Lung, Slaughter Rhythm, Hollow Choir, Sawbone Coil, Glutton's Gut, Thresher Plate, Blood Moat, Meat Hook, Blood Debt - Iron Lung re-armed per room in enterRoom; Slaughter Rhythm stamps kills - Targeted smoke tests for the five stat-conditional effects; icons + docs --- assets/i_blooddebt.png | Bin 0 -> 1222 bytes assets/i_bloodmoat.png | Bin 0 -> 1418 bytes assets/i_cauterizedveins.png | Bin 0 -> 1225 bytes assets/i_deadweight.png | Bin 0 -> 1662 bytes assets/i_gluttonsgut.png | Bin 0 -> 1702 bytes assets/i_hollowchoir.png | Bin 0 -> 1385 bytes assets/i_ironlung.png | Bin 0 -> 1335 bytes assets/i_meathook.png | Bin 0 -> 948 bytes assets/i_painengine.png | Bin 0 -> 1544 bytes assets/i_sawbonecoil.png | Bin 0 -> 1631 bytes assets/i_slaughterrhythm.png | Bin 0 -> 1122 bytes assets/i_thresherplate.png | Bin 0 -> 1610 bytes docs/items.md | 26 ++++++++++++++++- js/bullets.js | 14 +++++++++ js/enemies.js | 21 +++++++++++++ js/items.js | 14 +++++++++ js/pickups.js | 9 ++++-- js/player.js | 46 +++++++++++++++++++++++++++-- js/rooms.js | 2 ++ js/sprites.js | 3 ++ js/weapons.js | 6 ++-- test/smoke.js | 55 ++++++++++++++++++++++++++++++++--- tools/gen_assets.py | 12 ++++++++ 23 files changed, 195 insertions(+), 13 deletions(-) create mode 100644 assets/i_blooddebt.png create mode 100644 assets/i_bloodmoat.png create mode 100644 assets/i_cauterizedveins.png create mode 100644 assets/i_deadweight.png create mode 100644 assets/i_gluttonsgut.png create mode 100644 assets/i_hollowchoir.png create mode 100644 assets/i_ironlung.png create mode 100644 assets/i_meathook.png create mode 100644 assets/i_painengine.png create mode 100644 assets/i_sawbonecoil.png create mode 100644 assets/i_slaughterrhythm.png create mode 100644 assets/i_thresherplate.png diff --git a/assets/i_blooddebt.png b/assets/i_blooddebt.png new file mode 100644 index 0000000000000000000000000000000000000000..a28738447617d314410d4e1ba69184bf024f2d34 GIT binary patch literal 1222 zcmV;%1UdVOP)*QOu*R|1;Mm5Y7RZ)kCAFnCk=j&>s$SZAKTVJQ3jGL?qN=^LhemA@ zRf%G$1hftfY`}oA3t^Xfoz@t{_Q39R{f`Q+?VM(3X6Jd{cb?~+f&Y4Q4VwJwi%(7G z_rpwyk5=c;U0cj4RE-X|Z><5K)--CnWikKVZ@cGWc?N*fs}}%R`QSso{pkhQCIh4r zG0I-wnf2G|1$u!Q{43WfRT~6DLE>`w43X2#Js<%h`Y*qJKsv7ym&263K1$Wb*^Q=_ zh|6J0)drH@Leg6_LqPyK-r$8GoeK$i9zi`4Xj_Cpcj}yt!e1*-Xt!E#fCPZq_+Tw%VFYjSbVEBO^~z4 zhR#4QFvEgJ!#%ZxKN_KA0UCNsWWsKlZr@?69-eV5{S!T{3{#ho~bQD z``l;kIZL3n6G$!6QeIR4vokz_W+;f?@h#~sGF=y|Ru(c{7popqq}u*RfID0;Y8_y% zeZ*Y*NPOFXr~A!N@Pze504vu~Ya08N!{G_oX+E#&*}fN1IwE%dZ)Ee-cFVXW?`VZ+ zJego_`xWEK1jp?o%3fdpSS)h$(TM0b9d#g;h@oWjLWI3$dw2pyHqUYUi1B0sfJY4j zNw0{=Z4rv-9seq?D&h{&`$Z=DM|F@*}2gQk=^b@ z{FH66d7znEbSV7_^EZ|-vU!53MIqeTY6BuyL?9#E&^~)3o2MBHa^thR0>r;^ojVb~ z=-cD=J>hJFvT$JriaCXz<18h;#joG|C;;ayff|l6!%}|mlqPoG91Nd=}(5n}C z{5;M52T!>F;0eW?BBCs)Wr{gPME>}BnzefyCoZgFPC?fj!@FR8Gife(Gl9I~~o1B!@vVL2r%cl*{F=^viqKG-9^oD!GXS^)D|rS2M9ggaWBt!ivGj?1DvNI(8Z7<+^rnL)g$JZj%dI zmVGfmgq}f#h^ZxFt~Y{2%;lj{Ib?k^DZriUCC97Dc0#XSV5~jDZD_0bz4s^SM16w9 z`eyP3@QKIgsQ`*Og&nOu+Oyn!um^IOD|bKPb~cYXtBDksstx8Hmy+#$fR^$a_rV?q zmUcQ(A9zP7#lJL6S1+KMar*w2XXkJq?4bg2Ag#RS?Ucl&dXscsWesTci;inTHOx-b k$G+KR+InIC-!5u@1Eup_-0C)aPyhe`07*qoM6N<$f(=qt*Z=?k literal 0 HcmV?d00001 diff --git a/assets/i_bloodmoat.png b/assets/i_bloodmoat.png new file mode 100644 index 0000000000000000000000000000000000000000..765045faeb2777631ae0e0a545d0b6074d1895d8 GIT binary patch literal 1418 zcmV;51$Fv~P)@Swlv_9MOS9Q5mruoUHA;LxpL@FjS1wNT z&D{rFJ{4yq=xGDOD_?w++AYC*7J$ z@zs}~@$Gj%0C33OY=#2>6v|al!;mZDYLv)oaOdhx3XL5ivJ+WVaqk0d`ucH|Dl~ol zG=a6uOVaubZpFj7v8W9UYM4Sf-v*@2%c9sX4A?i>E~Kq6R?QlPMumv%BsSm|VEZEw zY;93@^wPgkr0(d&5s;Aw!ceMkXYM&}#qyrFM9yIk60#G3iRlR@rYFRL$nJdAG-%{< z99}OmP-_W&OK(s$4e`VfH*iiW3BD`{O8ee?0J-d+S-u>SN zxGnpE|4tkMnU>yQqtEhj`qT5MAuqEF`Hov6?Y}Mw+1UoTGxz)-1HUP@0d`GfHG^kc z4Jn&h=uAOuDBL!~0kWDwp|SI?5zRoY#Xzk^)ilV;J)nm1y>52bfL1m;Hm+2*-pTtR z>Cihz0brz2r>C{m?lPc|UP3AA0Msg0F$H^F3=UbBZt~o?81Dyv+1~D$8ys>od2Sp> zKxS~rjUyoMIgu-p)ePphTHV*sW-lM`)BGaC{k_D_pG7O1Z0Jc#7at{>TEbxzXn7*! zb1O`$!#Ior0LMOzGq){~IDMRCW|5}s!!LubuL5xK)KTv0HO%?N?i5r_gV}{VM~{T~ zSx^3VL=K?T=?KxUdUih66P93d8MoN1n}a05#;rTrXn+ zYRHRUGc4fLl6a6%!JosfR3WPwWb%0c)^>K-++1h*d5U~) z#WFVJMJejy0RwO~N+K;Yvt}5_}V+z5rtSZN@RRg zEL^QLJNkiqZiVubC&KGuZBL)P{m^<y_XI&pi-^zg|ZjY zLb-}rt0Su_zW6AUAM^nb?sJj2G=<+EV)ER$;19RrA#rJnL~M+?Z3#8xrD__iND^y1 zJGkq6Onau!+igIn5&Jg=<%DvRY|UtTk!MK3=S@cdWCkK6(2g0P{C~ z<=WMo;yE64I^Rm)2lj+xK|gNAYRe94B6OULC<+m;Ma_1>+FjHBUcf#>$0?OG?cKZL zv7jG-MQzi%K1YsFFB?Q;CvL?<)il~Rhz)R}*FoLUOM2}U%|>0EGf-=FRQ`K;FaIg| Y6Y~6kV*yuRng9R*07*qoM6N<$f|4+)Z~y=R literal 0 HcmV?d00001 diff --git a/assets/i_cauterizedveins.png b/assets/i_cauterizedveins.png new file mode 100644 index 0000000000000000000000000000000000000000..27642f4d024ce28125e86f37b992bf82fd70ff96 GIT binary patch literal 1225 zcmV;)1UCDLP)~cD}cDhq^b+n2z2eyeuxl{nO7gyxnZ(K|AnImm%IP^Q$PIrn8 zJ6sHVpK<9A57p6GK55^_H!el|*7E?m??`Lj`L)xXxuY1ma#nP8w2D27O#mEiZW3J` zt>U9?)nbog6T6&F{W-&0ke@hynCEE``29A36U+_Xbhxs))rx0NnEh->iV6%}t!|zLU4s(%|O!$NTjl#Rl#ZkZgDZ zQTA`n;14C{I?q^jI{;#_5Yq-TlT!)F8;-r+f`t<23aPVO1%TV70AN-}87O~;QGbYu zz9-Zgqc}EM__;R#fL2(h6K5l70Qw4qq$U%~XW-bmQ+)T~Udr8Vm_>&srzDa#8DjW2!+lwo>JQZ17z*nVJd>u#jBxsB!)JLgGaVTsYiF$3+jF#5VJZ;O0FR`a36mtfn$>efZGG1ybW{Y9yKoIn(EO~EG%T%*YId_6Mz6P zB%TA%cCdwqw{Eg?-vIE`}YipUp* zu)VlKXpSO%fYff7qPIVWA{iO87GRT2XoY3^I0eW?(sR^ zc{f%^XYq;5lJk5u&@H$;6`{Sj0*@N!fjKMLWK*8pK9M0R8zhe!m(*lp(ZAbOEAT4j nl>iVvk(^s literal 0 HcmV?d00001 diff --git a/assets/i_deadweight.png b/assets/i_deadweight.png new file mode 100644 index 0000000000000000000000000000000000000000..d7cea7a20ccb8c16ae8595eb50f97c422de1300c GIT binary patch literal 1662 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 literal 0 HcmV?d00001 diff --git a/assets/i_hollowchoir.png b/assets/i_hollowchoir.png new file mode 100644 index 0000000000000000000000000000000000000000..4cc70358642cf87abbcaeb593dc3bb885fff9796 GIT binary patch literal 1385 zcmV-v1(y1WP))SEvy$E7J6xSQPzuX-EMYK7^7nh zh8Nw{V60`m@M0VGqU(@?l&veQPDvO&#u}?9!<>A_e0W;lT|^JH6zyVn*9Q*gyw7>g zdH?V8{GShaAr(k6IL4sgCkFjKasKQO$>12jxqB-5m*H8A-uwC!F1`Obr%oOP5WAf9 zhV~Yxm|KXE3eIBlbsBE|DdTNz;_}t8|L@eNP97D5exDc)42Vk?&k6a*FU4?RKs;%? z2K4!P;M#>V9QpJcu3b1I0Divt7ZEi>z~4?-Nf4NbvgNqAfhEXLAeI55Dai|0rN$DR*>#$<|PLx*wn zbTB?O$9JRS$clo^*Gd2Jg8)cNaeTX5k(S~_)QtG)$_;UK>{qdVfX$?OD*zu|zQ&Ew zuK<`_inII}Fo1}f0bsty#H1YNodbJ`+igr=Lj8PtMS*0(tM_SRhhw&KL50PMb_T)S{)gKj1~PejcSRuarTtitWE(r#G6 zUo9|8WnRoJ#5nkhM+^rB#8zC`6u=j!jxaJhA?6lhx-L5{>(48pdqmU>eO?d!#}Cq6 zZ`Q-(=;>g&sgaM*epWevu#(`|&|wy1X|mHY<5P3^cDL%W{~@%1tSCg(3^U<*lF1lh zB|%t8;I9_!>+jj7?0;U@$TCI{2nfQW$NM9 ziLhxJ?uexbD+!Xx7=3FSrGm3~8|?V21tX&q7)vP}wMO=Ixp>oQA?2`^Jp?3g{jt4L zyvOYpn$1F+5V##yCgm^y#j0A`Ob=Pgm(xxyXbca&!Ni@(4LY-AMNTBiXETP1LeSjP ziN##Efe!$=&?r{b>bBknd*vK()Jxc{5&?fZZiiLh zFj{(qX|+I$rMzA#t+15Wc)7KlhP?y59P)S5TyLhir?YYZYUn=E&}_NmsTo=N4I2k9Kq?RLX?gF$Eg zdTOZ!O|SW;)dFfk!)|w?7Bq6pnGM&o$fh1>b2*V07com#UiY{Fmp+P(PtD z^kMuZwGn)={A*LoPKZP*oCh+?dOh&C8t}Lp2>9Ev^m+m4d+Rj<{&pIZDOxN>_V4Wj zpv7V=ZwzE}`N~6(&E+vmRvh&bfZhq)N=1$h9j2{R1mOPPK{~p9SbDwe-`j_~p$2bj z6PnF}F_o==6#+2Tn2;=H;g+PQzBjEF rHq`ycGiJRJX)7xRZ7ozU>_0>X4X&TN9^#G=5~g_HJs&M9X30jUBscRH;ZH=Jeau{ARv0 z^PQPp_+S2ua^d_r0498jxiAwmFP%AM&PJo=Y&6=X+xIL0T)BE3fP7<10Pw`d(Tyri zkJAR%l6}uI6C1TEQJ)I{S#lANFX5DjpM3_}C0(jgSx%uFRW6)AM@`@O=K!>7?um_4 zrk^WUuak%`@qzomlL53VJ&jEG6oPKqY(3A1Mp&4MnX}O-pKA|oJ)zg-cfa_MFW>(Z zfVV!IMgDdePi&mZatcpu9DvGlio1<1jyw7RFalmad1Hl-zZoNV;}@l}%^SBK51^I4rpHMlzVuWXICeI|()XEG8SG3aOX%4JbEi+D zxFqXBw!r92jHu6r5%6~Cl{BOCVzvO`0MSMrBj6QUtr$QS0 zwsW6bB0PEAvR%{w2>ac(ZrJZ8Th#1#S+|5#C|f03PIV2P;FMK5sZi#3P47*@QPZ@Y zw;~*i_zC;nNQE+E5kF%Qf5-dwvk~x;EovNpX^x;ox(pj#&BmLXeUb}~fTA)*`A zCwGC*Es-s1Oiqk47V+bWjkA&|+78>#y8!|CrVi0`c)GbY8A6s^-6dFf`9-t6HZA$? z_OE1%8p#I*6qiK&_FeLtju;t1aY>zJC~A~6!v@zbc}>T+ts8X9#CvgIRS zgj3ZbgXIQ9Mo@R@mNdg6*ks7oRTMv}T7QL`p5N*`KG+EU*P-@$T!vRW~ z!D_BEqV?fEd@Vq`K;$(Y?&Tm`px!rRJ@*aS0jduQ)E`Gez*-jJ0IPyvx644KMkAXA zKvDc04uw!$(#~AZ7Ff*{$tO}asC=6fpo4hr=Y8!B!;Mr_;MJ7uw(uRSTtq$j{`|)&I=*qyQGpG3K=VbvfgMbQ@aybr%EJFRb zt}=*$Lli-jC>sWjEs@q*k?RKe#ukV7D+Jv#$#s>oV-SGN#wLS~K`^S6>F)@-YZrKY zJ{+QG0*FH*M<)*gaK<~##iVNMU6xF~UQ99{8nMYYKM~?ixyFg3WBm5Vim;bG(nZ3L z7UpQw>%98hLEf7?Vi7A_;C^l$fR%?8E^n`j`4^@wo`eI45erC*tS63+nE(qjG28ZQ zlK1fd=1!jk;D^N(9HL0NROMn)wG9Kj{Yc{-4AKq}BjCm7mhibHt2;^yMAegB$K?gFr{$HX!Z^!Jn3bQUz7;M6!O@Y7<_CRIL>BHtsO<*hBEl0-eT zMiOXlZa%RDHh|sVYKezyoINsOMOQP3N)pZO7VM=D;02o7b3>^a?DzXv%VhQj;Q4!b z5j}YGqFaxEL%*PtUjm@jWIr&#TVnF`lK=b zd6|iEOMF>Plm}fFolz5K>z_Kuf*piHTN-*9h4QDAm7fn3mc6yCcTYQSzkAQk`TWlL zo$t9ncwoD**FJm;fXRsvV?K|V3I>GV>Ae3S2YY+*JDp;3A|%G1f2IZ1j%jw|e9CA6 zpazV(WPDn|;^w@#m(6I}`RK)?4gh}0mZ>Wy31Hb6RUe>hJOF^n(+@y85+@N)Hek-p z-=7TRu7{Z^{|?(_xL4a1K=hlRF?&K}FK_Z&Y#D%;hCKj`Jn>jt0NR~8#xHs?55CLn zjsLhAON!4v`I-f#KvI%~%W9S$K2hcBuPSCBCSJkp32`%)6zkXKaVbTd*8S+#*3sMT zTsS+|Rx9oppkwwD0Dpgf6@ZVu1H4m`7bK9x}q;PZr*H+v4TpcDZ3(rxDD zN}dI!KxM83K>e?%dI0CY3iH9nT5G-Um>cXibk#Mwjr#!@IPs)nl$^Rs_=w-jcmG_c zSSsMOHV$2}R3KWZ(+oZp3@{Z8r~@$ml)n*V>~V&k8v(lwYo`tiTL5^E_o6AJ0mO*I zw)X(>J|K*#rQ#E}htOfXRsvmRzF+BbTl;Yfntc zG2GR|L+8Q(xa0%sl)#wD3Rz^h{L>#KH?(YMI?2)@HUE;%pBYwn8Uid!joKP|4`Nf6 zD3-RwbSWprFM8=eJ;VExF9=y=SV7;ZPa3-gZ|V1G8GI6 zS!9?Cr_{+nF1HF1z;44?-N5JTBer~tQ4lV91kX_i*M9ktE42z5iyq2*EJS{%Qyeke zrlvQwo;`i&_MHsfsxljC=z&AhO4Ft6-U1B1{xbA z=SPTHHZ{L206w-WOkKY!NDYWr3c#~snTaujCmTEb>C^iRpGlC>!#Mkn$AlhH(IYCO zdQ8mDOna=n6o9#p{tUooy-9JW&hVLp=UX!y1cCwSoe$mv5Iv?sYM8})i}Zhg^3@a} zz)!ACQYsciiWwukjajT*MbTeJ?!QFv(mQox-lq7 z4X~Mi3_vHUA=fOnx?Nh1gAxcZ-+{N0bU2O zR4TE#x=!|VjPc2FQi%u)pUy+Q$r@nQ8?+QZ?Nr)HuySI7u$v&qbd?EbRJ2YM5jsbbJOz^_W-H6?|k=_aM1u;cRTv zv>RUVm}N7!y~ja$nNJ=(p;RmYu-o6~eUsnXX0zVpa)0D_0J4`aim}nOm&>UeZ<4)y zk*OPRdN=w)x^}zVU2Agd!9#96sZieQc)m_lL-zaWL^Ti83%g)q%wTz?Kyjyz9+W*c zhR-AzKQf$-n!Mp~Q;oAOo^nuWL4O|<%V?U)@JO1p5G=WJExi!H!g9$AV(xeN?Ozt4 zSf-de$K0bQ0HlpXPwh~}Q_f1K7<^5GM;7biDFthv>|wnuZEYhD4YJwlO2=YgGyj-O z%K@`N1s|dOW14n@YITRw{sD2iU?iwEp0SX)@-@yGr-6rbtPbhQGrCnRL^2YhX*XE0 zjsc+Nmsv9#0JK|e)_``aO9dac)g-P3p;!i2Zn3jxx%k!<0Ia7Ho%|}W=baU|yJYk* zOQq8BQgEtTkq8>1AW8TSm2A&ykW^yCwIF&>rXWdt^Xvd-ScPJl`9FN_j_-ooDaO{ofOJ{*xvpgM;{eIF~Q0grmTiq^OyLGJJ-TuOEI6gVH_uV#Zx5*6+ zVh_Ym2B2v-2&rM>blE)eJZ9Ng@hD6GTqZX(=soYXryfxuHv)$fa?Q4B+Kp%WVOEa^ zAZr-XVbPFOeA3ZbpDvb>hX%c%UbUAiZu=?$lDQG|h)S|Q`UM`n51O z-~P5cpz^B}B#E>T-t6GoTW3Y%2J?Wy7!dy<7J5!>_milz_`b%W+Q5!00I%fMg`ZX5H=RkYjs;3dMKW uEqH+Zil3z71G?@Zhwq1g8vnQdw0{G;+=PRfeQUA+0000`}RTbVDhW5AjC+ZdN{aUlHnK>S~_5eVdQ zJ5PW8Zs+?y=X}5C!2h}lD=nUU`wd#Y^AiACni{0Qm`H(61Asn0PoFpq!0C4`3JdXh zZeHBQ_OD&%r4zNZws$@`0D#8&I;sxrkqR~~$!nvZbTOA+fZ1x4zk{i1oNPSv&^xay zp5|2SpBo3z($s*_pqKXUG!ypR!0qtjbdRz$;Uy`@NJg;<$N4`{xWj@kG)b0aJK51G zx+Akf`ko!U_g2f3MWC_1PAb=A0N`|w^6Sfg#iZ2&a65K~ZHldYx$cmhk_XM{9;L1S zy3q6fFZl8EudEq~O{;C1tw#F9X{xL{cymVJtVu~knI+dlU`)jByoo`pBBR)Z(V)iy z)F`tgrKMK5a^Wo08BY=a;eu&G%7Nmv>8MA4%p0m zU3Y^y*E}gjX6PNjR`wi+)^NUR5x#nGpHy0Hqq?R7fO~!~fyE>Mx_Sr5j!t1Q=K+vX zWX9_YVl?RS`htA>hrgoo;6CB<2Y=oefT**R_RcOYwOx_&)tQu5+o*a*huh(mQO)qRPMo?(u?cAT!> z0mj7`xf%r*&b^N>92K5e2U?mM2#kqRxh4ardzAC<{e_NV3jk+LN_?S70A_Ns*)kD% z=&nJl0^rR{{lZ5o9s6#%Rt1o>(&E)2uT-wdVDoGMfNZ0V!6Q}%k60gal&etya4YkQ|>vo2v()+Yuld z6;UcRGKkV@n@mqYS}nz^rl6=~7tii2ka9H&st)YouFFj@HI2ZS$ha6I?DCVZ&cvWq z$xL|A`ow9Pn%f?$7FLYF)!+RTrKMJ|<>g2Tx}B)4BD%Q9J--*XL$}QL`waMW(2t_> zF#7mB%@_K~S7&0&%fVFk+?vg4MU<~Ap5}|c{T^Mt0}S`trIG`sjEgbKb{mO(^Z|;> z!-+66bzcd6p-Bu{72T0phDJQX?qU-g)`3JfEYN8r`=731_xK3UEl_3M!OOlF{rd~$ za|uQ~Xjzu+^84drY}F_vJxYlF*f&gIOyp=q2^C*Cgh{KVxiFhORoi993Fo2lAa;+B z(0Fi}HPI;m#>JS_+TOV;6IP5sT~i6dP#$Hwjgme-PcSu2?tIM``iVwGa?+DoNX-Jk z6AGc4UE;1Li<+Y^$N(IR0%4b5ICriIrKNU#&8W4#ld#J|zB-d*uN?=#>kCqHpcH`N zUi-iMM$Pjw8=US@V41ofpL&cgoK>`a^CF- z$oJJng_P|!3MU)SJbn!%tj21gsku$~*{OF4jEQvg4AUo0bJykO^jkk?O#F7bv; zwHUs|1vEA@rPVerwOv77RJi7Jv?hRrie-9((zqC7%S42u6(w}^3}eu$@ZWICXFm+Y zP;X8aluC_s+2IrnY9%i&q;mW4-bc#uy2hu(uxj`CP%1U3iwYU}c!bXzvY4G1ANY*?_VV9AmVRO)}g z0=8_rsf2h*2&B?VM5T2Sr_I>TygZ(0@&5RW8c^<~P|(=kX9bSzHPI^RT>J7{+EF0Jih7B56wpFqiXD<2Jp*zVE}!S8 z?|&X&3jofZJjv|xLbm$LZvd=?F|wktG-;8R4NhbkH}9>}34`2zDv*Cx!Utb`0>H|p z6>L+dsFx_}C6;H7vNUM{aPIUx_VRP2y&jWubBqF1A&_a-a$cmP~EwSjZE$J6K^P7BAcOcw{(adMz4#d@VP!`;*;-*MdC8wu5WvV%)UK z;{gC<^@dNc+Y>}HiWAtTF75?)AEh+o6sx4~dT%TMsuh!_ACdM30Aw!|3+YaaeDFY= zj!*JnEHXUx;Z^DlA2*1Q6$MX`X-9!*%VvFztvey>*&|`m`wi)-yLaxhpdR`3*6lxW z<1SV|1;E?#k-`pI%>&>EX$MJ?Q1TL5KgR3CEk;KT(==))^Bx5 z@`b^I{g`$X3|er2cshW(19bj{}@{h0ie%B)#L>&GPd0(L>hbsHr4f{pq3 z2Zlrgc;L5b$LZtp51&)oL)$KZ>&9q>65S-BnOl4mA};m82}%au_3&X}JLmR0!Hdk%brT-axz$vMA6^QDm1civk6* z>K6!#et-h;B0yHIniOc_3RUaSRxC<0w8)W$^XE|<(bRM?oF_80*4_B6?&I8<``$VC zo_h&z*8h1a|MJq8Km5g3v~PPn`q#ge%Y1(Os}C?cE&#SG zX#C?_p4*Omer4SG#bF1t<3{%G_fRf?+2)rw%M9igspNG;VF}w6tkDr-ss-==Let? zZ9E4cn^|Dr_P}gUe#mTt*_OebtQKGFG79e9-oVLf5rF-)LDN5FEjv#}(@CG70??nC z2cVeC$oHCxLiXe$`1x@Nz}fO5jT49Jj|>KjSpY)QR9;KLN1uMo-~RY#iX-T4;N60b zachIkw@Me6>bR7bbJ7OL16n&PDN^^oW3c`<`J6`j@z>bGqgq?1X?19YLw=L{F@JDh zP9v~csZiglM@h0|G@W8?{Q}w%e2ynDbWOrKZu>!xwJ3sefu_|VG)(}ickkeMg0CKb zJrPspz{3aksc+SzWWV+Pd!$!a$mcYf7x|nI#0)L+$!81-@Vq z_GPSz9Eg5$0D#s$o~UQr9VOhz9{)Yz)piAgxkUzZi&7_&chs&gQA+eBi8WOP4Cm5uNdZ`pSPmr%&XJOeu?KsRY8{~7^gxyP}$es& z1_YKxOVg!af`V;X95V~sJ23)T4cq|37gCC48E${^SlQX$kpi>(@@RsZV`j-{`b0S< zA`_&w3BLsXWHnAMINuk7zgtJjR1!Y79k3mxSX+-APq3DqXRchP{?0XQS5O2hVV}-d z-(lP+k*{5kKYq|=>uN#TKlQ~#+s+&w5ikU^jRBm6XG+iargI<}Acw>APa>Ml7qqLOO0*OF4~Rr;BQv%sR)UhO-3W8I?QlfUbeAp)ckn z|Ci2$N!^c+T}l5RtiKKAf~2aoyCM&$GmEJV}+VZNK?P_GZcTKMjsO`)NsQ5C8xG07*qo IM6N<$g6rB9;Q#;t literal 0 HcmV?d00001 diff --git a/docs/items.md b/docs/items.md index 093a01c..400ec3c 100644 --- a/docs/items.md +++ b/docs/items.md @@ -1,6 +1,6 @@ # Items -All **60 passive items** in MeatSlicer, with exact effects pulled from `js/items.js` and the proc logic in `js/enemies.js` / `js/player.js`. +All **72 passive items** in MeatSlicer, with exact effects pulled from `js/items.js` and the proc logic in `js/enemies.js` / `js/player.js`. --- @@ -77,6 +77,18 @@ All of these trigger from your weapon hits (orbital knives proc at half chance). | **Ember Jar** | 20% chance to ignite | `+0.20` chance — burn for 1.8 s at `5 × dmgMul` DPS | | **Chill Gland** | 18% chance to chill | `+0.18` chance — slows enemies (activates the previously unused `slowOnHit` stat) | | **Cinder Sump** | +12% ignite, +12% acid | `igniteChance +0.12`, `acidOnHit +0.12` — dual-element hybrid | +| **Dead Weight** | +40% damage to enemies under 30% HP | `executeBonus +0.40` — checked in `damageEnemy` | +| **Cauterized Veins** | +15% ignite, +25% damage to burning enemies | `igniteChance +0.15`, `burnDamageBonus +0.25` | +| **Hollow Choir** | Every 4th shot fires a free extra volley | `choirEvery +1` — fires a second `fireWeapon` every 4th trigger pull | +| **Sawbone Coil** | Expiring bullets split into 2 shards | `sawboneCoil +1` — on bullet expiry, spawns 2 shards at 40% damage | +| **Glutton's Gut** | Hearts heal +1 extra; overheal becomes score | `gluttonGut +1` — heart heal `2 + tier`, full-HP score `25 + tier × 10` | +| **Slaughter Rhythm** | +4% fire rate per recent kill (cap +40%) | `slaughterRhythm +0.04` — kills stamp `killStamps`; rate bonus decays after 3 s | +| **Pain Engine** | +30% damage for 4s after being hit | `painEngine +0.30` — `dmgLiveMul` rises while `painEngineT` is active | +| **Thresher Plate** | Passive contact-damage aura | `thresherPlate +1` — ticks `6 × tier × dmgMul` on nearby enemies every 0.4 s | +| **Blood Moat** | Kills leave an acid pool | `bloodMoat +1` — on kill, spawns an acid hazard at the corpse | +| **Iron Lung** | The first hit each room is blocked | `ironLung +1` — re-armed by `enterRoom`; blocks one hit with a 0.5 s i-frame | +| **Meat Hook** | Kills yank nearby enemies to the corpse | `meatHook +1` — on kill, nearby enemies are pulled toward the body | +| **Blood Debt** | +35% damage, −½ heart container | `dmgMul ×1.35`, `maxHp −1` — the Rare trade-off item | | **Acid Gland** | Hits may leave acid pools | `+0.12` chance (cap 65%) — pool radius 18 × sizeMul, 2.5 s, `5 × dmgMul` DPS | | **Hook Rounds** | Hits pull enemies toward you | `+0.18` chance (cap 80%) — yanks the enemy toward you at speed 170 | | **Sledge Rounds** | More knockback, hits may stun | `knockbackMul ×1.35`, `+0.08` stun chance (cap 75%) — 0.35 s stun, non-bosses only | @@ -200,6 +212,18 @@ All of these trigger from your weapon hits (orbital knives proc at half chance). | 58 | Volatile Bile | Rare | III | On-kill | | 59 | Whipcord Tendon | Uncommon | V | Damage | | 60 | Worm Gut | Rare | III | Survivability | +| 61 | Blood Debt | Rare | III | Damage | +| 62 | Blood Moat | Uncommon | IV | On-kill | +| 63 | Cauterized Veins | Uncommon | V | On-hit proc | +| 64 | Dead Weight | Uncommon | V | Damage | +| 65 | Glutton's Gut | Common | V | Survivability | +| 66 | Hollow Choir | Rare | III | Shot modifier | +| 67 | Iron Lung | Rare | III | Survivability | +| 68 | Meat Hook | Uncommon | IV | On-kill | +| 69 | Pain Engine | Rare | III | Damage | +| 70 | Sawbone Coil | Uncommon | V | Shot modifier | +| 71 | Slaughter Rhythm | Rare | III | Fire rate | +| 72 | Thresher Plate | Uncommon | V | Survivability | ## Synergy notes diff --git a/js/bullets.js b/js/bullets.js index 20ec078..dd8cf08 100644 --- a/js/bullets.js +++ b/js/bullets.js @@ -89,6 +89,20 @@ function updateBullets(dt) { if (b.behavior === 'cone' && chance(0.35)) { G.hazards.push({ kind: 'acid', x: b.x, y: b.y, r: 24 * (b.sizeMul || 1), life: 3.5, t: 0, dps: b.dmg * 1.5 }); } + // Sawbone Coil: bullets split into 2 shards on expiry + if (p.stats.sawboneCoil > 0 && !b.shard && b.behavior !== 'lob' && b.behavior !== 'lob_trap' && b.behavior !== 'lob_swarm') { + for (let k = 0; k < 2; k++) { + const sa = rand(0, TAU); + G.bullets.push({ + x: b.x, y: b.y, ang: sa, + vx: Math.cos(sa) * 260 * p.stats.shotSpeedMul, vy: Math.sin(sa) * 260 * p.stats.shotSpeedMul, + r: 3 * p.stats.sizeMul, dmg: b.dmg * 0.4, pierce: 0, bounce: p.stats.bounce, + life: 0.4 * p.stats.rangeMul, t: 0, behavior: 'bullet', sprite: 'bullet_bone', + homing: p.stats.homing ? 1.6 + p.stats.homing * 0.7 : 0, shard: true, sizeMul: p.stats.sizeMul, + }); + } + spawnSpark(b.x, b.y, rand(0, TAU)); + } } // lobbed projectiles trigger on expiry (reached target) diff --git a/js/enemies.js b/js/enemies.js index e4f70d0..0b61771 100644 --- a/js/enemies.js +++ b/js/enemies.js @@ -102,6 +102,10 @@ function damageEnemy(e, dmg, ang, knockback, opts) { const o = opts || {}; const crit = !!(p && !o.noCrit && chance(p.stats.crit || 0)); if (crit) dmg *= p.stats.critMul || 2; + // Dead Weight: execute wounded enemies + if (p && p.stats.executeBonus > 0 && e.maxHp > 0 && e.hp / e.maxHp < 0.30) dmg *= 1 + p.stats.executeBonus; + // Cauterized Veins: bonus damage to burning enemies + if (p && p.stats.burnDamageBonus > 0 && e.burnT > 0) dmg *= 1 + p.stats.burnDamageBonus; const mitigated = e.dmgTakenMul !== undefined && e.dmgTakenMul < 1; dmg *= e.dmgTakenMul === undefined ? 1 : e.dmgTakenMul; e.hp -= dmg; @@ -222,6 +226,7 @@ function killEnemy(e, ang) { const p = G.player; if (p.stats.frenzy > 0) p.frenzyT = Math.max(p.frenzyT || 0, 3); G.kills++; + if (p.stats.slaughterRhythm > 0) { p.killStamps = p.killStamps || []; p.killStamps.push(G.time); } addScore(e.boss ? 500 : (e.elite ? 40 : 10)); spawnBlood(e.x, e.y, ang || rand(0, TAU), e.boss ? 40 : (e.elite ? 20 : 10), e.boss || e.elite); spawnGibs(e.x, e.y, e.boss ? 16 : (e.elite ? 8 : 4), e.boss || e.elite); @@ -236,6 +241,22 @@ function killEnemy(e, ang) { if (e.boss) { onBossDeath(e); return; } + // Blood Moat: kills leave an acid pool + if (p.stats.bloodMoat > 0 && G.hazards.length < 80) { + G.hazards.push({ kind: 'acid', x: e.x, y: e.y, r: 16 * p.stats.sizeMul, life: 2.5, t: 0, dps: 5 * p.stats.dmgMul * p.stats.dmgLiveMul }); + } + // Meat Hook: kills yank nearby enemies to the corpse + if (p.stats.meatHook > 0) { + const yankR = 120 + p.stats.meatHook * 15; + for (const o of G.enemies) { + if (o === e || o.hp <= 0 || o.boss) continue; + if (dist2(o.x, o.y, e.x, e.y) < yankR * yankR) { + const a = angleTo(o.x, o.y, e.x, e.y); + o.vx += Math.cos(a) * 240; o.vy += Math.sin(a) * 240; + } + } + } + // Volatile Bile: kills explode — radius and damage scale with tier if (p.stats.explodeOnKill > 0) { const rad = 60 + p.stats.explodeOnKill * 15; diff --git a/js/items.js b/js/items.js index 8381795..2716373 100644 --- a/js/items.js +++ b/js/items.js @@ -75,6 +75,20 @@ const ITEMS = { rimedfang: { name: 'Rimed Fang', desc: '+4% crit chance, +25% crit damage', rarity: 'common', cap: 6, apply: s => { s.crit += 0.04; s.critMul += 0.25; } }, butcherstwine: { name: 'Butcher\'s Twine', desc: '+12% bleed, +8% fire rate', rarity: 'uncommon', cap: 5, apply: s => { s.bleed += 0.12; s.rateMul *= 1.08; } }, cindersump: { name: 'Cinder Sump', desc: '+12% ignite, +12% acid', rarity: 'uncommon', cap: 5, apply: s => { s.igniteChance += 0.12; s.acidOnHit += 0.12; } }, + + // ---- phase 3 additions (small engine hooks) ---- + deadweight: { name: 'Dead Weight', desc: '+40% damage to enemies under 30% HP', rarity: 'uncommon', cap: 5, apply: s => { s.executeBonus += 0.40; } }, + cauterizedveins:{ name: 'Cauterized Veins', desc: '+15% ignite, +25% damage to burning enemies', rarity: 'uncommon', cap: 5, apply: s => { s.igniteChance += 0.15; s.burnDamageBonus += 0.25; } }, + hollowchoir: { name: 'Hollow Choir', desc: 'Every 4th shot fires a free extra volley', rarity: 'rare', cap: 3, apply: s => { s.choirEvery += 1; } }, + sawbonecoil: { name: 'Sawbone Coil', desc: 'Expiring bullets split into 2 shards', rarity: 'uncommon', cap: 5, apply: s => { s.sawboneCoil += 1; } }, + gluttonsgut: { name: "Glutton's Gut", desc: 'Hearts heal +1 extra; overheal becomes score', rarity: 'common', cap: 5, apply: s => { s.gluttonGut += 1; } }, + slaughterrhythm:{ name: 'Slaughter Rhythm', desc: '+4% fire rate per recent kill (cap +40%)', rarity: 'rare', cap: 3, apply: s => { s.slaughterRhythm += 0.04; } }, + painengine: { name: 'Pain Engine', desc: '+30% damage for 4s after being hit', rarity: 'rare', cap: 3, apply: s => { s.painEngine += 0.30; } }, + thresherplate: { name: 'Thresher Plate', desc: 'Passive contact-damage aura', rarity: 'uncommon', cap: 5, apply: s => { s.thresherPlate += 1; } }, + bloodmoat: { name: 'Blood Moat', desc: 'Kills leave an acid pool', rarity: 'uncommon', cap: 4, apply: s => { s.bloodMoat += 1; } }, + ironlung: { name: 'Iron Lung', desc: 'The first hit each room is blocked', rarity: 'rare', cap: 3, apply: s => { s.ironLung += 1; } }, + meathook: { name: 'Meat Hook', desc: 'Kills yank nearby enemies to the corpse', rarity: 'uncommon', cap: 4, apply: s => { s.meatHook += 1; } }, + blooddebt: { name: 'Blood Debt', desc: '+35% damage, −½ heart container', rarity: 'rare', cap: 3, apply: (s, p) => { s.dmgMul *= 1.35; s.maxHp = Math.max(1, s.maxHp - 1); p.hp = Math.min(p.hp, s.maxHp); } }, }; const ITEM_LEVEL_CAP = 9; diff --git a/js/pickups.js b/js/pickups.js index ea7f4ba..c01cfd1 100644 --- a/js/pickups.js +++ b/js/pickups.js @@ -69,11 +69,14 @@ function collectPickup(k) { gainXP(k.v * p.stats.xpMul); Sfx.gem(); break; - case 'heart': - if (p.hp < p.stats.maxHp) { healPlayer(2); spawnText(p.x, p.y, '+2', '#d92038'); } - else addScore(25); + case 'heart': { + // Glutton's Gut: hearts heal +1 extra per tier; overheal becomes score + const healAmt = 2 + (p.stats.gluttonGut || 0); + if (p.hp < p.stats.maxHp) { healPlayer(healAmt); spawnText(p.x, p.y, '+' + healAmt, '#d92038'); } + else addScore(25 + (p.stats.gluttonGut || 0) * 10); Sfx.heart(); break; + } case 'ammo': { // Generic rounds refill every finite-ammo weapon the butcher is carrying, // including a special weapon currently holstered behind the Bone Popper. diff --git a/js/player.js b/js/player.js index 145c5cc..9f81ece 100644 --- a/js/player.js +++ b/js/player.js @@ -22,6 +22,10 @@ function initPlayer() { roomHeal: 0, roomHealChance: 0, thorns: 0, retaliate: 0, sizeMul: 1, mortar: 0, frenzy: 0, orbSpeedMul: 1, orbDmgMul: 1, overShield: 0, rerolls: 0, rerollPerLevel: 0, + // phase-3 conditional / hook passives + dmgLiveMul: 1, executeBonus: 0, burnDamageBonus: 0, choirEvery: 0, sawboneCoil: 0, + gluttonGut: 0, slaughterRhythm: 0, painEngine: 0, thresherPlate: 0, bloodMoat: 0, + ironLung: 0, meatHook: 0, bloodDebt: 0, maxHp: 6, }, }; @@ -100,7 +104,7 @@ function updatePlayer(dt) { Sfx.syncWeaponLoop(w, sustained && Input.mdown && p.weapon.ammo > 0); if (p.fireT > 0) p.fireT -= dt; const frenzyMul = p.frenzyT > 0 ? 1 + st.frenzy : 1; - const effectiveRate = st.rateMul * frenzyMul; + const effectiveRate = st.rateMul * (1 + (st.rhythmRateBonus || 0)) * frenzyMul; const rate = w.interval / effectiveRate; if (w.behavior === 'saw') { @@ -123,6 +127,11 @@ function updatePlayer(dt) { } else { if (Input.mdown && p.fireT <= 0 && p.weapon.ammo > 0) { fireWeapon(p, w); + // Hollow Choir: every 4th shot fires a free extra volley + if (st.choirEvery > 0) { + p.choirCount = (p.choirCount || 0) + 1; + if (p.choirCount >= 4) { p.choirCount = 0; fireWeapon(p, w); } + } p.recoil = Math.min(1, p.recoil + (w.behavior === 'slam' ? 1 : 0.45)); p.muzzleT = 0.07; p.attackT = 0.4; p.actionT = 0; @@ -154,7 +163,7 @@ function updatePlayer(dt) { if (e.orbT > 0) continue; if (dist2(ox, oy, e.x, e.y) < (12 + e.r) * (12 + e.r)) { e.orbT = 0.35; - damageEnemy(e, 12 * st.dmgMul * st.orbDmgMul, a + Math.PI / 2, true, { + damageEnemy(e, 12 * st.dmgMul * st.dmgLiveMul * st.orbDmgMul, a + Math.PI / 2, true, { source: 'player', procScale: 0.5, procMagnitudeScale: 0.5, }); spawnBlood(e.x, e.y, a, 3); @@ -165,6 +174,30 @@ function updatePlayer(dt) { // open queued level-ups if (G.pendingLevelups > 0 && G.mode === 'play') openPerkDraft(); + + // ---- phase-3 live multipliers & auras ---- + // Slaughter Rhythm: +rate per recent kill, Pain Engine: +dmg after being hit + p.killStamps = (p.killStamps || []).filter(t => G.time - t < 3); + const rhythm = st.slaughterRhythm > 0 ? Math.min(p.killStamps.length * st.slaughterRhythm, 0.40) : 0; + st.rhythmRateBonus = rhythm; + if (p.painEngineT > 0) p.painEngineT -= dt; + st.dmgLiveMul = p.painEngineT > 0 ? 1 + st.painEngine : 1; + + // Thresher Plate: passive contact-damage aura (no need to be hit) + if (st.thresherPlate > 0) { + p.thresherTick = (p.thresherTick || 0) - dt; + if (p.thresherTick <= 0) { + p.thresherTick = 0.4; + const range = 46 * Math.sqrt(st.sizeMul); + for (const e of G.enemies) { + if (e.hp <= 0 || e.boss) continue; + if (dist2(e.x, e.y, p.x, p.y) < (range + e.r) * (range + e.r)) { + damageEnemy(e, 6 * st.thresherPlate * st.dmgMul * st.dmgLiveMul, angleTo(p.x, p.y, e.x, e.y), true, { noProc: true }); + } + } + } + } + // Iron Lung room-block cooldown flag is set in enterRoom } function armorBlockChance(rating) { @@ -183,6 +216,14 @@ function pressureRelief(dmgTaken) { function hurtPlayer(dmg, ang, attacker) { const p = G.player; if (p.invT > 0 || p.hp <= 0 || G.mode !== 'play') return; + // Iron Lung: the first hit each room is blocked + if (p.ironLungReady) { + p.ironLungReady = false; + p.invT = 0.5; p.hitT = 0.1; + spawnText(p.x, p.y - 20, 'IRON LUNG', '#9fb4bd'); + addShake(2); Sfx.shieldUp(); + return; + } if (p.stats.armor > 0 && chance(armorBlockChance(p.stats.armor))) { p.invT = 0.2; p.hitT = 0.1; @@ -201,6 +242,7 @@ function hurtPlayer(dmg, ang, attacker) { p.hp -= reduced; p.hitT = 0.18; p.invT = 0.9 + p.stats.invBonus; + if (reduced > 0 && p.stats.painEngine > 0) p.painEngineT = 4; G.flash = 0.35; addShake(7); Sfx.hurt(); diff --git a/js/rooms.js b/js/rooms.js index f24e330..65b0d51 100644 --- a/js/rooms.js +++ b/js/rooms.js @@ -67,6 +67,8 @@ function enterRoom(gx, gy) { // spawn protection: brief invincibility walking into a fresh room if (G.player) G.player.invT = Math.max(G.player.invT, 1.0); + // Iron Lung: re-arm the once-per-room hit block + if (G.player && G.player.stats.ironLung > 0) G.player.ironLungReady = true; if (!room.visited) { room.visited = true; diff --git a/js/sprites.js b/js/sprites.js index 872c26c..fe8577e 100644 --- a/js/sprites.js +++ b/js/sprites.js @@ -39,6 +39,9 @@ const SPRITE_MANIFEST = [ 'i_chillgland', 'i_hookedsinew', 'i_gyroscopicribs', 'i_marrowpiston', 'i_splitcortex', 'i_gristlecord', 'i_renderedfat', 'i_whipcordtendon', 'i_rusteddiadem', 'i_gorgedtick', 'i_bonemealpowder', 'i_rimedfang', 'i_butcherstwine', 'i_cindersump', + 'i_deadweight', 'i_cauterizedveins', 'i_hollowchoir', 'i_sawbonecoil', 'i_gluttonsgut', + 'i_slaughterrhythm', 'i_painengine', 'i_thresherplate', 'i_bloodmoat', 'i_ironlung', + 'i_meathook', 'i_blooddebt', // full 64px/128px eight-direction action atlases 'player_sheet', 'player_legs_sheet', 'enemy_shambler_sheet', 'enemy_runner_sheet', 'enemy_spitter_sheet', diff --git a/js/weapons.js b/js/weapons.js index 366e9f0..4b09f78 100644 --- a/js/weapons.js +++ b/js/weapons.js @@ -43,7 +43,7 @@ function fireWeapon(p, w) { const ang = p.aim; const muzzle = w.muzzle || 35; const bx = p.x + Math.cos(ang) * muzzle, by = p.y + Math.sin(ang) * muzzle; - const dmgMul = st.dmgMul; + const dmgMul = st.dmgMul * st.dmgLiveMul; const spdMul = st.shotSpeedMul; spawnMuzzleFx(bx, by, ang, w.behavior); @@ -144,7 +144,7 @@ function sawTick(p, def, inst, dt) { for (const e of G.enemies) { if (dist2(cx, cy, e.x, e.y) < (radius + e.r) * (radius + e.r)) { const tickScale = dt / Math.max(0.01, def.interval || 0.1); - damageEnemy(e, def.dmg * st.dmgMul * st.rateMul * inert * tickScale, a, false, { + damageEnemy(e, def.dmg * st.dmgMul * st.dmgLiveMul * st.rateMul * inert * tickScale, a, false, { source: 'player', procScale: tickScale, procIntervalScale: tickScale, }); hitAny = true; @@ -166,7 +166,7 @@ function fireBeam(p, def, charge) { for (let i = 1; i <= st.rear; i++) angles.push(p.aim + Math.PI + (i - (st.rear + 1) / 2) * 0.18); const len = 900 * st.rangeMul; const inert = 1 + (st.pierce + st.bounce + st.homing) * 0.06; - const dmg = def.dmg * st.dmgMul * (0.4 + 0.6 * charge) * inert; + const dmg = def.dmg * st.dmgMul * st.dmgLiveMul * (0.4 + 0.6 * charge) * inert; const width = 14 * Math.sqrt(st.sizeMul); for (const ang of angles) { const x2 = p.x + Math.cos(ang) * len, y2 = p.y + Math.sin(ang) * len; diff --git a/test/smoke.js b/test/smoke.js index 02cb719..2850556 100644 --- a/test/smoke.js +++ b/test/smoke.js @@ -318,9 +318,9 @@ step(30, 16); console.log('== expanded perk and item rosters =='); { - check('41 new items added (14 phase-2 passives)', Object.keys(ctx.ITEMS).length === 60); + check('41 new items added (14 phase-2 + 12 phase-3 passives)', Object.keys(ctx.ITEMS).length === 72); const newPerks = ['critbone', 'critmeat', 'flensing', 'ember', 'frostbile', 'heavyhand', 'thickhide', 'secondwind', 'scrapfeed', 'boneknit', 'spiteflesh', 'carrion', 'sinew']; - const newItems = ['chainsinew', 'mortarbone', 'bloatrounds', 'marrowglut', 'hollowneedle', 'bloodshoteye', 'flayerkiss', 'emberjar', 'acidgland', 'hookrounds', 'sledgerounds', 'graftedtrigger', 'deadmanswitch', 'orbitcrown', 'tannedhide', 'deadmansclock', 'hollowbones', 'boneplate', 'wormgut', 'spinecage', 'secondstomach', 'spitewell', 'twinhearts', 'brassmagazine', 'crowbait', 'gorgingleech', 'rerollrib', 'chillgland', 'hookedsinew', 'gyroscopicribs', 'marrowpiston', 'splitcortex', 'gristlecord', 'renderedfat', 'whipcordtendon', 'rusteddiadem', 'gorgedtick', 'bonemealpowder', 'rimedfang', 'butcherstwine', 'cindersump']; + const newItems = ['chainsinew', 'mortarbone', 'bloatrounds', 'marrowglut', 'hollowneedle', 'bloodshoteye', 'flayerkiss', 'emberjar', 'acidgland', 'hookrounds', 'sledgerounds', 'graftedtrigger', 'deadmanswitch', 'orbitcrown', 'tannedhide', 'deadmansclock', 'hollowbones', 'boneplate', 'wormgut', 'spinecage', 'secondstomach', 'spitewell', 'twinhearts', 'brassmagazine', 'crowbait', 'gorgingleech', 'rerollrib', 'chillgland', 'hookedsinew', 'gyroscopicribs', 'marrowpiston', 'splitcortex', 'gristlecord', 'renderedfat', 'whipcordtendon', 'rusteddiadem', 'gorgedtick', 'bonemealpowder', 'rimedfang', 'butcherstwine', 'cindersump', 'deadweight', 'cauterizedveins', 'hollowchoir', 'sawbonecoil', 'gluttonsgut', 'slaughterrhythm', 'painengine', 'thresherplate', 'bloodmoat', 'ironlung', 'meathook', 'blooddebt']; check('all new perks have manifest icons or fallbacks', newPerks.every(id => ctx.SPRITE_MANIFEST.includes('perk_' + id))); check('all new items have manifest icons or fallbacks', newItems.every(id => ctx.SPRITE_MANIFEST.includes('i_' + id))); check('new roster entries all have names, descriptions and effects', @@ -498,6 +498,53 @@ console.log('== tier-scaled items + duplicate favoring =='); check('item rolls favor owned items (' + ownedHits + '/200)', ownedHits >= 40); } +console.log('== phase-3 hook passives =='); +{ + const p = ctx.G.player; + const s = p.stats; + // Dead Weight: execute bonus on wounded enemies (isolate stats so no leftover items leak) + const savedStats = s; + p.stats = Object.assign({}, s, { executeBonus: 0.40, burnDamageBonus: 0, crit: 0, critMul: 2 }); + const wounded = ctx.makeEnemy('shambler', 500, 300, 1, false); wounded.hp = wounded.maxHp = 100; wounded.hp = 20; // 20% of maxHp + const healthy = ctx.makeEnemy('shambler', 500, 300, 1, false); healthy.hp = healthy.maxHp = 100; + const wBefore = wounded.hp, hBefore = healthy.hp; + ctx.damageEnemy(wounded, 10, 0, false, { noCrit: true }); + ctx.damageEnemy(healthy, 10, 0, false, { noCrit: true }); + check('Dead Weight executes wounded enemies', Math.abs((wBefore - wounded.hp) - 14) < 0.01 && Math.abs((hBefore - healthy.hp) - 10) < 0.01); + p.stats = savedStats; + + // Cauterized Veins: bonus damage to burning enemies + const savedBurn = s.burnDamageBonus; + s.burnDamageBonus = 0.25; + const burner = ctx.makeEnemy('shambler', 500, 300, 1, false); burner.burnT = 1; + const bBefore = burner.hp; + ctx.damageEnemy(burner, 10, 0, false, { noCrit: true }); + check('Cauterized Veins boosts damage on burning enemies', Math.abs((bBefore - burner.hp) - 12.5) < 0.01); + s.burnDamageBonus = savedBurn; + ctx.G.enemies.length = 0; + + // Pain Engine: dmgLiveMul rises after being hit + const savedPain = s.painEngine, savedArmor = s.armor, savedMax = s.maxHp; + s.painEngine = 0.30; s.armor = 0; s.maxHp = 999; p.hp = 999; p.invT = 0; p.painEngineT = 0; ctx.G.mode = 'play'; + ctx.hurtPlayer(1, 0); + check('Pain Engine arms after being hit', p.painEngineT > 0); + s.painEngine = savedPain; s.armor = savedArmor; s.maxHp = savedMax; p.invT = 1; p.painEngineT = 0; + + // Iron Lung: first hit in a room is blocked + const savedLung = s.ironLung; + s.ironLung = 1; p.ironLungReady = true; p.invT = 0; const hpBeforeLung = p.hp; + ctx.hurtPlayer(1, 0); + check('Iron Lung blocks the first room hit', p.hp === hpBeforeLung && p.ironLungReady === false); + s.ironLung = savedLung; p.invT = 1; + + // Slaughter Rhythm: recent kills grant fire rate + const savedRhythm = s.slaughterRhythm; + s.slaughterRhythm = 0.04; p.killStamps = [ctx.G.time, ctx.G.time, ctx.G.time]; + p.fireT = 0; ctx.updatePlayer(0.016); + check('Slaughter Rhythm grants live fire rate', (s.rhythmRateBonus || 0) > 0); + s.slaughterRhythm = savedRhythm; p.killStamps = []; +} + console.log('== all weapons fire without crashing =='); for (const wid of Object.keys(ctx.WEAPONS)) { ctx.G.player.weapon = { id: wid, ammo: 50 }; @@ -937,8 +984,8 @@ console.log('== item stickiness ramps with owned count =='); console.log('== help manual grows with the implant roster =='); { - check('9 help pages (3 implant pages for 60 items)', ctx.HELP_PAGES.length === 9); - check('9 help renderers', ctx.HELP_RENDERERS.length === 9); + check('10 help pages (4 implant pages for 72 items)', ctx.HELP_PAGES.length === 10); + check('10 help renderers', ctx.HELP_RENDERERS.length === 10); ctx.G.player = ctx.G.player || {}; const fctx = fakeCtx(); let renderOK = true; diff --git a/tools/gen_assets.py b/tools/gen_assets.py index 8162dc5..b774e34 100644 --- a/tools/gen_assets.py +++ b/tools/gen_assets.py @@ -279,6 +279,18 @@ ITEMS = { "rimedfang": "a long predatory fang crusted with white frost and a steel collar", "butcherstwine": "a spool of blood-stained butcher twine with a frayed loose end", "cindersump": "a smouldering iron sump grate with orange cinders and green acid drips", + "deadweight": "a heavy iron butcher weight crushing a pale bone", + "cauterizedveins": "a forearm cross-section with glowing cauterized orange veins", + "hollowchoir": "a hollow bone pipe organ with three small singing mouths", + "sawbonecoil": "a coiled spring made of interlocking bone saw teeth", + "gluttonsgut": "a distended stomach with a second mouth and spilled half-digested hearts", + "slaughterrhythm": "a metronome with a pendulum made of a bloody cleaver", + "painengine": "a small iron engine with a pressure gauge and a screaming flesh piston", + "thresherplate": "a spiked iron threshing plate bristling with rotating barbs", + "bloodmoat": "a ring-shaped moat of dark blood with a bone drawbridge", + "ironlung": "a riveted iron lung with bellows and a steel windpipe", + "meathook": "a huge rusted slaughterhouse meat hook on a taut chain", + "blooddebt": "a parchment contract signed in blood with a heart seal", } for iid, subject in ITEMS.items(): SPECS["i_" + iid] = Spec(subject + ", isolated inventory object, strong top-left light, no medallion, no icon background", 32)