From 27459787c1d3402a4f32d530c969c0c7653b8c52 Mon Sep 17 00:00:00 2001 From: Nyeogmi Date: Sat, 22 Feb 2025 19:22:06 -0800 Subject: [PATCH 1/3] Blockify collectibles --- src/art/pickups/collectibles.png | Bin 0 -> 605 bytes src/art/pickups/collectibles_silhouettes.png | Bin 0 -> 471 bytes src/art/pickups/resources.png | Bin 455 -> 0 bytes src/art/pickups/stats.png | Bin 431 -> 0 bytes src/colors.ts | 34 +++ src/huntmode.ts | 130 ++++------ src/pickups.ts | 238 ++++++++++++------- src/sprites.ts | 20 +- src/world3d.ts | 140 +++++++++++ 9 files changed, 384 insertions(+), 178 deletions(-) create mode 100644 src/art/pickups/collectibles.png create mode 100644 src/art/pickups/collectibles_silhouettes.png delete mode 100644 src/art/pickups/resources.png delete mode 100644 src/art/pickups/stats.png create mode 100644 src/world3d.ts diff --git a/src/art/pickups/collectibles.png b/src/art/pickups/collectibles.png new file mode 100644 index 0000000000000000000000000000000000000000..cc1c22c450d72e4ab08d5235dff6f48720c1794a GIT binary patch literal 605 zcmV-j0;2tiP)001Be1^@s6m49>f00001b5ch_0Itp) z=>Px%7fD1xRCt{2*uP4{KpY3~U!-(!C=Q~iAQrWh4n9CBMQLB4dk2TQ_yoGS`39W= zc5wFv3`nVY01=TEO%*D%I+P9$BAkP!=5L#5?%Jf^50oakgx@v!^>=q60ssI200000 z0002sNSbs9AbdU!=b3T8bGt%BM9KOC@(ILXKbq|xYqon#%WtyrSJ@$BeLBvbjJ(mO z>Ph79X^<7r_XTdjb)w2}y4*1J2iaKNJ68+!kX=v((iDK=azWZcAkmY+E4y&KfMy|rA+Oq zjU`{P=lpgcUVV2BAfo!|$wx$_e;eC)$2U^?o#BT!r{v48Ur>Ji!qd6aDwGv4ro51~ z0!GMq@~Qd$Kl$ilsUJZBM+L2EYOpxvQr7N7E(|U zF-g&gIifQEp2D@`%l_Q{|NsBL&j)^8+wpb5^cPvhd%R{>S~WNr2dgNVD9K0}X=@ZZ zTJJ7(zr7^+%cVKD9=zJT^J4ep^|__JULi@wmX1y;#?gireL+FHO0!;WUHtgj|DF4P zx6FPL(Yw#RwpBUZ&!{COc75-pJG;()fAr+is^c5F7qlf+riVuPxj0#xnAuCt0s6(E zB*-tA0q7J;fUx#FL!eoeo-U3d5v^~hIC32@;9%MO|9{ECDU+OEdAaU66LbBD;k0$E z3=9+8%rA@exjC-v19{Zos3>hYxmE*(v>Ze|NdHYVT2*Wfky5})r=|gTxa|SI)cH| L)z4*}Q$iB}!ZGSJ literal 0 HcmV?d00001 diff --git a/src/art/pickups/resources.png b/src/art/pickups/resources.png deleted file mode 100644 index bc989aa97f5ab1dc339e22af9214f84ef9047864..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 455 zcmV;&0XY7NP)Px$fk{L`RA_-?U>F6XU=)mkQ801=<3Kxr9PlfP>pwF8=@m%^1_lNO#Y1<8q<_c) z|DoX4gIE7=J$Uv1zMb-a!n!cQkPI^68$2ST1k*xd(i>8 z=8gCiK#~S*4q(~)3`5Yj(M8IC!SZ$A??95s{y$tgf}!F^N%(6WF=%#r`J$^x_iL^~8c z|DoqQY=D%!NRAtDxrL@>05Uk{um`6cwabEESzP}a`j4ZC!D^q==!)%>{}WpyQ4|1! z0sLNc97Bwhz#lSM0KM(Oz<@{r*Z{F9fFuWEa|AZPHtict3PD=5Lr8Y$tp~601_CYH z0V?zF{l{pBP-y>%ZwHKK0SetjH$baGct}@+)M$r{_%dKfrvR%1E;!{x7U=yaCJPLi xGJt`BHihtz3IGNM>bFBid>JsLQUEQo000&o+%8IVcAo$M002ovPDHLkV1jKSx!eE% diff --git a/src/art/pickups/stats.png b/src/art/pickups/stats.png deleted file mode 100644 index 7301deb037ab571450a8e3910610fce8e8865adf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 431 zcmV;g0Z{&lP)Px$X-PyuRCt{2*s*HDKp4RBFGV_ZkZz?d2o44D5ky?_1n!PbeS>apeS=Pp?mj`5 zAbEmPgi-=R3fZJXhf+wVlUy6Emzpz4|347n&Uhd9eRsJ;1ONa400000P;<_)1E`w2 z-Xz)8e;-ULHK)eGe%zjE6A(RKYW;2h#mWS9Mhh*D<*Z-$xW6S|DY^)r-o}}6bemJ- zaT|XR$G*;Jk$k#W((qojHXB8d=AK{wX!F)lVdHH}{P%v7LO3xE;`HHt>z~_L{VMq) zBI-NcSM>O4-a6WRik6l9L2E99)_kwuZev^kWuR7mdIzX8lZ2XHldAx-b^%-Wex;1b zx3Lo(S|xXY|0d7jZr^Ki7eIO!u(|qo9rSD;0+p=8;GuP!QzwW+t<7Hz#a|7zwjd6T z3Z{}z0n1t6ImTn4Vk6;M#y6fLA~Jn;lio#J>kHy=({G|uFD=TTZZCoW000000002B Z_XUM7du#jK&Yu7P002ovPDHLkV1l>Ry9odQ diff --git a/src/colors.ts b/src/colors.ts index b980880..7e6215b 100644 --- a/src/colors.ts +++ b/src/colors.ts @@ -1,4 +1,5 @@ import { Color } from "./engine/datatypes.ts"; +import { Stat } from "./datatypes.ts"; export const BG_OUTER = Color.parseHexCode("#143464"); export const BG_WALL_OR_UNREVEALED = Color.parseHexCode("#143464"); @@ -10,3 +11,36 @@ export const FG_TEXT_ENDORSED = Color.parseHexCode("#80ff80"); export const FG_BOLD = Color.parseHexCode("#ffffff"); export const BG_CEILING = Color.parseHexCode("#143464"); export const FG_MOULDING = FG_TEXT; + +// stat colors +export const SWATCH_EXP: [Color, Color] = [ + Color.parseHexCode("#b9bffb"), + Color.parseHexCode("#e3e6ff"), +]; + +export const SWATCH_AGI: [Color, Color] = [ + Color.parseHexCode("#df3e23"), + Color.parseHexCode("#fa6a0a"), +]; + +export const SWATCH_INT: [Color, Color] = [ + Color.parseHexCode("#285cc4"), + Color.parseHexCode("#249fde"), +]; + +export const SWATCH_CHA: [Color, Color] = [ + Color.parseHexCode("#793a80"), + Color.parseHexCode("#bc4a9b"), +]; + +export const SWATCH_PSI: [Color, Color] = [ + Color.parseHexCode("#9cdb43"), + Color.parseHexCode("#d6f264"), +]; + +export const SWATCH_STAT: Record = { + AGI: SWATCH_AGI, + INT: SWATCH_INT, + CHA: SWATCH_CHA, + PSI: SWATCH_PSI, +}; diff --git a/src/huntmode.ts b/src/huntmode.ts index b45b14e..0137c0c 100644 --- a/src/huntmode.ts +++ b/src/huntmode.ts @@ -4,9 +4,6 @@ import { D, I } from "./engine/public.ts"; import { sprThrallLore } from "./sprites.ts"; import { BG_INSET, - BG_WALL_OR_UNREVEALED, - FG_BOLD, - FG_MOULDING, FG_TEXT, FG_TEXT_ENDORSED, FG_TOO_EXPENSIVE, @@ -18,6 +15,7 @@ import { shadowcast } from "./shadowcast.ts"; import { withCamera } from "./layout.ts"; import { getCheckModal } from "./checkmodal.ts"; import { CARDINAL_DIRECTIONS } from "./mapgen.ts"; +import { Block3D, Floor3D, World3D } from "./world3d.ts"; export class HuntMode { map: LoadedNewMap; @@ -113,14 +111,22 @@ export class HuntMode { this.#updateFov(); this.#updatePickups(); + let world3d = new World3D(this.map.size); + for (let y = 0; y < this.map.size.h; y += 1) { + for (let x = 0; x < this.map.size.w; x += 1) { + this.#writeMapCellToWorld(world3d, new Point(x, y)); + } + } + for (let y = 0; y < this.map.size.h; y += 1) { for (let x = 0; x < this.map.size.w; x += 1) { let offsetInPixels = new Point(x, y) .scale(FLOOR_CELL_SIZE) .offset(this.pixelPlayer.negate()); - this.#drawMapCell(offsetInPixels, new Point(x, y)); + this.#drawMapCell(offsetInPixels, world3d, new Point(x, y)); } } + this.#drawPlayer(globalOffset); this.#drawBadges(globalOffset); @@ -181,8 +187,8 @@ export class HuntMode { } let nSteps = 40; - let szX = 0.75; - let szY = 0.75; + let szX = 0.5; + let szY = 0.5; this.velocity = new Point(dx, dy); @@ -190,7 +196,7 @@ export class HuntMode { for (let offset of CARDINAL_DIRECTIONS.values()) { let bigBbox = new Rect( this.floatingPlayer - .offset(offset.scale(new Size(0.02, 0.02))) + .offset(offset.scale(new Size(0.12, 0.12))) .offset(new Point(-szX / 2, -szY / 2)), new Size(szX, szY), ); @@ -293,26 +299,14 @@ export class HuntMode { this.drawpile.draw(); } - #drawMapCell(offsetInPixels: Point, mapPosition: Point) { - const OFFSET_UNDER_FLOOR = -512 + mapPosition.y; + #drawMapCell(offsetInPixels: Point, world3d: World3D, mapPosition: Point) { const OFFSET_FLOOR = -256 + mapPosition.y; - const OFFSET_AIR = 0 + mapPosition.y; - const OFFSET_TOP = 256 + mapPosition.y; - const OFFSET_TOP_OF_TOP = 512 + mapPosition.y; const gridArt = new GridArt(offsetInPixels); - let cellData = this.map.get(mapPosition); + world3d.drawCell(this.drawpile, gridArt, mapPosition); - this.drawpile.add(OFFSET_UNDER_FLOOR, () => { - gridArt.drawCeiling(BG_WALL_OR_UNREVEALED); - }); - if (cellData.architecture == Architecture.Wall || !cellData.revealed) { - this.drawpile.add(OFFSET_TOP, () => { - gridArt.drawCeiling(BG_WALL_OR_UNREVEALED); - }); - return; - } + let cellData = this.map.get(mapPosition); if (!cellData.revealed) { return; @@ -343,7 +337,6 @@ export class HuntMode { } gridArt.drawFloor(color); - pickup?.drawFloor(gridArt); }, gridArt.floorRect, true, @@ -369,70 +362,32 @@ export class HuntMode { ); if (pickup != null) { - this.drawpile.add(OFFSET_AIR, () => { - pickup.drawInAir(gridArt); - }); - } - - const isRevealedBlock = (dx: number, dy: number) => { - let other = this.map.get(mapPosition.offset(new Point(dx, dy))); - return other.revealed && other.architecture == Architecture.Wall; - }; - if (isRevealedBlock(0, -1) && isRevealedBlock(-1, 0)) { - this.drawpile.add(OFFSET_TOP_OF_TOP, () => { - gridArt.drawMouldingTopLeft(FG_MOULDING); - }); - } - if (isRevealedBlock(0, -1)) { - this.drawpile.add(OFFSET_AIR, () => { - gridArt.drawWallTop(FG_TEXT); - }); - this.drawpile.add(OFFSET_TOP_OF_TOP, () => { - gridArt.drawMouldingTop(FG_MOULDING); - }); - } - if (isRevealedBlock(0, -1) && isRevealedBlock(1, 0)) { - this.drawpile.add(OFFSET_TOP_OF_TOP, () => { - gridArt.drawMouldingTopRight(FG_MOULDING); - }); - } - if (isRevealedBlock(-1, 0)) { - this.drawpile.add(OFFSET_AIR, () => { - gridArt.drawWallLeft(FG_TEXT); - }); - this.drawpile.add(OFFSET_TOP_OF_TOP, () => { - gridArt.drawMouldingLeft(FG_MOULDING); - }); - } - if (isRevealedBlock(0, 1) && isRevealedBlock(-1, 0)) { - this.drawpile.add(OFFSET_TOP_OF_TOP, () => { - gridArt.drawMouldingBottomLeft(FG_MOULDING); - }); - } - if (isRevealedBlock(0, 1)) { - this.drawpile.add(OFFSET_AIR, () => { - gridArt.drawWallBottom(FG_BOLD); - }); - this.drawpile.add(OFFSET_TOP_OF_TOP, () => { - gridArt.drawMouldingBottom(FG_MOULDING); - }); - } - if (isRevealedBlock(0, 1) && isRevealedBlock(1, 0)) { - this.drawpile.add(OFFSET_TOP_OF_TOP, () => { - gridArt.drawMouldingBottomRight(FG_MOULDING); - }); - } - if (isRevealedBlock(1, 0)) { - this.drawpile.add(OFFSET_AIR, () => { - gridArt.drawWallRight(FG_BOLD); - }); - this.drawpile.add(OFFSET_TOP_OF_TOP, () => { - gridArt.drawMouldingRight(FG_MOULDING); - }); + pickup.draw(this.drawpile, gridArt); } } - #drawPlayer(globalOffset: Point) { + #writeMapCellToWorld(world3d: World3D, mapPosition: Point) { + let cellData = this.map.get(mapPosition); + if (!cellData.revealed) { + return; + } + + let pickupBlock = cellData.pickup?.getBlock(); + if (pickupBlock) { + world3d.set(mapPosition, pickupBlock); + return; + } + + if (cellData.architecture == Architecture.Wall) { + world3d.set(mapPosition, Block3D.standardWall()); + return; + } + + world3d.set(mapPosition, new Floor3D()); + } + + #drawPlayer(_globalOffset: Point) { + /* let cellOffset = this.pixelPlayer.offset(globalOffset.negate()); this.drawpile.add(1024, () => { D.drawSprite(sprThrallLore, cellOffset, 1, { @@ -440,6 +395,13 @@ export class HuntMode { yScale: 2, }); }); + */ + this.drawpile.add(1024, () => { + D.drawSprite(sprThrallLore, new Point(192, 192), 1, { + xScale: this.faceLeft ? -2 : 2, + yScale: 2, + }); + }); } #drawBadges(globalOffset: Point) { diff --git a/src/pickups.ts b/src/pickups.ts index 5847aaa..95ffa7c 100644 --- a/src/pickups.ts +++ b/src/pickups.ts @@ -6,16 +6,24 @@ import { generateMap } from "./mapgen.ts"; import { ALL_STATS, Stat } from "./datatypes.ts"; import { D } from "./engine/public.ts"; import { + sprCollectibles, + sprCollectiblesSilhouettes, sprLadder, sprLock, - sprResourcePickup, - sprStatPickup, } from "./sprites.ts"; import { GridArt } from "./gridart.ts"; import { getCheckModal } from "./checkmodal.ts"; import { Point, Size } from "./engine/datatypes.ts"; import { choose } from "./utils.ts"; -import { FG_TEXT } from "./colors.ts"; +import { + BG_CEILING, + FG_BOLD, + FG_TEXT, + SWATCH_EXP, + SWATCH_STAT, +} from "./colors.ts"; +import { Block3D } from "./world3d.ts"; +import { DrawPile } from "./drawpile.ts"; export type Pickup = | LockPickup @@ -52,14 +60,19 @@ export class LockPickup { return true; } - drawFloor() {} - drawInAir(gridArt: GridArt) { - for (let z = 0; z < 5; z += 0.25) { - D.drawSprite(sprLock, gridArt.project(z), 0, { - xScale: 2.0, - yScale: 2.0, - }); - } + draw(drawpile: DrawPile, gridArt: GridArt) { + drawpile.add(0, () => { + for (let z = 0; z < 5; z += 0.25) { + D.drawSprite(sprLock, gridArt.project(z), 0, { + xScale: 2.0, + yScale: 2.0, + }); + } + }); + } + + getBlock(): Block3D | null { + return null; } update() {} @@ -102,23 +115,32 @@ export class BreakableBlockPickup { return true; } - drawFloor() {} - drawInAir(gridArt: GridArt) { - let progress = Math.pow(this.breakProgress, 2.15); - let extraMult = 1.0; - let angleRange = 0; - if (progress != 0) { - extraMult = 1.2; - angleRange = 10; - } + get #adjustedProgress(): number { + return Math.pow(this.breakProgress, 2.15); + } - this.callbacks.draw(gridArt.project(5), { - xScale: 2 * (1.0 - progress * 0.7) * extraMult, - yScale: 2 * (1.0 - progress * 0.7) * extraMult, - angle: (2 * progress - 1) * angleRange, + draw(drawpile: DrawPile, gridArt: GridArt) { + drawpile.add(384, () => { + let progress = this.#adjustedProgress; + let extraMult = 1.0; + let angleRange = 0; + if (progress != 0) { + extraMult = 1.2; + angleRange = 10; + } + + this.callbacks.draw(gridArt, { + xScale: 2 * (1.0 - progress * 0.7) * extraMult, + yScale: 2 * (1.0 - progress * 0.7) * extraMult, + angle: (2 * progress - 1) * angleRange, + }); }); } + getBlock(): Block3D | null { + return this.callbacks.getBlock(this.breakProgress); + } + update(cellData: CellView) { if (this.breakProgress >= 1.0) { getPlayerProgress().spendBlood(this.callbacks.cost); @@ -157,8 +179,16 @@ export class StatPickupCallbacks { getPlayerProgress().purloinItem(); } + getBlock(progress: number) { + return new Block3D( + progress > 0.6 ? FG_BOLD : SWATCH_STAT[this.#stat][1], + progress > 0.6 ? FG_TEXT : SWATCH_STAT[this.#stat][0], + progress > 0.6 ? FG_BOLD : SWATCH_STAT[this.#stat][1], + ); + } + draw( - at: Point, + gridArt: GridArt, options: { xScale?: number; yScale?: number; angle?: number }, ) { let statIndex = ALL_STATS.indexOf(this.#stat); @@ -166,7 +196,8 @@ export class StatPickupCallbacks { return; } - D.drawSprite(sprStatPickup, at, statIndex, options); + let at = gridArt.project(100); + D.drawSprite(sprCollectiblesSilhouettes, at, statIndex, options); } } @@ -182,11 +213,20 @@ export class ExperiencePickupCallbacks { getPlayerProgress().purloinItem(); } + getBlock(progress: number) { + return new Block3D( + progress > 0.6 ? FG_BOLD : SWATCH_EXP[1], + progress > 0.6 ? FG_TEXT : SWATCH_EXP[0], + progress > 0.6 ? FG_BOLD : SWATCH_EXP[1], + ); + } + draw( - at: Point, + gridArt: GridArt, options: { xScale?: number; yScale?: number; angle?: number }, ) { - D.drawSprite(sprResourcePickup, at, 0, options); + let at = gridArt.project(100); + D.drawSprite(sprCollectiblesSilhouettes, at, 0, options); } } @@ -211,13 +251,18 @@ export class LadderPickup { return false; } - drawFloor(gridArt: GridArt) { - D.drawSprite(sprLadder, gridArt.project(0.0), 0, { - xScale: 2.0, - yScale: 2.0, + draw(drawpile: DrawPile, gridArt: GridArt) { + drawpile.add(-128, () => { + D.drawSprite(sprLadder, gridArt.project(0.0), 0, { + xScale: 2.0, + yScale: 2.0, + }); }); } - drawInAir() {} + + getBlock(): Block3D | null { + return null; + } update() {} @@ -257,15 +302,20 @@ export class ThrallPickup { return false; } - drawFloor() {} - drawInAir(gridArt: GridArt) { - let data = getThralls().get(this.thrall); - D.drawSprite(data.sprite, gridArt.project(0.0), 0, { - xScale: 2.0, - yScale: 2.0, + draw(drawpile: DrawPile, gridArt: GridArt) { + drawpile.add(0, () => { + let data = getThralls().get(this.thrall); + D.drawSprite(data.sprite, gridArt.project(0.0), 0, { + xScale: 2.0, + yScale: 2.0, + }); }); } + getBlock(): Block3D | null { + return null; + } + update() {} onClick(cell: CellView): boolean { @@ -307,15 +357,20 @@ export class ThrallPosterPickup { return false; } - drawFloor() {} - drawInAir(gridArt: GridArt) { - let data = getThralls().get(this.thrall); - D.drawSprite(data.sprite, gridArt.project(0.0), 2, { - xScale: 2.0, - yScale: 2.0, + draw(drawpile: DrawPile, gridArt: GridArt) { + drawpile.add(0, () => { + let data = getThralls().get(this.thrall); + D.drawSprite(data.sprite, gridArt.project(0.0), 2, { + xScale: 2.0, + yScale: 2.0, + }); }); } + getBlock(): Block3D | null { + return null; + } + update() {} onClick(cell: CellView): boolean { @@ -358,27 +413,32 @@ export class ThrallRecruitedPickup { return false; } - drawFloor() {} - drawInAir(gridArt: GridArt) { - let data = getThralls().get(this.thrall); - let lifeStage = getPlayerProgress().getThrallLifeStage(this.thrall); - let ix = 0; - let rot = 0; + draw(drawpile: DrawPile, gridArt: GridArt) { + drawpile.add(0, () => { + let data = getThralls().get(this.thrall); + let lifeStage = getPlayerProgress().getThrallLifeStage(this.thrall); + let ix = 0; + let rot = 0; - if (lifeStage == LifeStage.Vampirized) { - ix = 1; - } - if (lifeStage == LifeStage.Dead) { - ix = 1; - rot = 270; - } - D.drawSprite(data.sprite, gridArt.project(0.0), ix, { - xScale: 2.0, - yScale: 2.0, - angle: rot, + if (lifeStage == LifeStage.Vampirized) { + ix = 1; + } + if (lifeStage == LifeStage.Dead) { + ix = 1; + rot = 270; + } + D.drawSprite(data.sprite, gridArt.project(0.0), ix, { + xScale: 2.0, + yScale: 2.0, + angle: rot, + }); }); } + getBlock(): Block3D | null { + return null; + } + update() {} onClick(_cell: CellView): boolean { @@ -477,23 +537,28 @@ export class ThrallCollectionPlatePickup { return false; } - drawFloor() {} - drawInAir(gridArt: GridArt) { - let itemStage = getPlayerProgress().getThrallItemStage(this.thrall); - let data = getThralls().get(this.thrall); + draw(drawpile: DrawPile, gridArt: GridArt) { + drawpile.add(0, () => { + let itemStage = getPlayerProgress().getThrallItemStage(this.thrall); + let data = getThralls().get(this.thrall); - if (itemStage != ItemStage.Delivered) { - D.drawRect( - gridArt.project(0).offset(new Point(-18, -18)), - new Size(36, 36), - FG_TEXT, - ); - } else { - D.drawSprite(data.sprite, gridArt.project(2), 3, { - xScale: 2.0, - yScale: 2.0, - }); - } + if (itemStage != ItemStage.Delivered) { + D.drawRect( + gridArt.project(0).offset(new Point(-18, -18)), + new Size(36, 36), + FG_TEXT, + ); + } else { + D.drawSprite(data.sprite, gridArt.project(2), 3, { + xScale: 2.0, + yScale: 2.0, + }); + } + }); + } + + getBlock(): Block3D | null { + return null; } update() {} @@ -585,16 +650,21 @@ export class ThrallItemPickup { return false; } - drawFloor() {} - drawInAir(gridArt: GridArt) { - let data = getThralls().get(this.thrall); + draw(drawpile: DrawPile, gridArt: GridArt) { + drawpile.add(0, () => { + let data = getThralls().get(this.thrall); - D.drawSprite(data.sprite, gridArt.project(2), 3, { - xScale: 2.0, - yScale: 2.0, + D.drawSprite(data.sprite, gridArt.project(2), 3, { + xScale: 2.0, + yScale: 2.0, + }); }); } + getBlock(): Block3D | null { + return null; + } + update() {} onClick(cell: CellView): boolean { diff --git a/src/sprites.ts b/src/sprites.ts index 54b897d..6c903d3 100644 --- a/src/sprites.ts +++ b/src/sprites.ts @@ -1,7 +1,7 @@ import { Sprite } from "./engine/internal/sprite.ts"; -import imgResourcePickup from "./art/pickups/resources.png"; -import imgStatPickup from "./art/pickups/stats.png"; +import imgCollectibles from "./art/pickups/collectibles.png"; +import imgCollectiblesSilhouettes from "./art/pickups/collectibles_silhouettes.png"; import imgLadder from "./art/pickups/ladder.png"; import imgLock from "./art/pickups/lock.png"; import { Point, Size } from "./engine/datatypes.ts"; @@ -13,20 +13,20 @@ import imgThrallParty from "./art/thralls/thrall_party.png"; import imgThrallStare from "./art/thralls/thrall_stare.png"; import imgThrallStealth from "./art/thralls/thrall_stealth.png"; -export let sprResourcePickup = new Sprite( - imgResourcePickup, +export let sprCollectibles = new Sprite( + imgCollectibles, new Size(32, 32), new Point(16, 16), - new Size(1, 1), - 1, + new Size(5, 1), + 5, ); -export let sprStatPickup = new Sprite( - imgStatPickup, +export let sprCollectiblesSilhouettes = new Sprite( + imgCollectiblesSilhouettes, new Size(32, 32), new Point(16, 16), - new Size(4, 1), - 4, + new Size(5, 1), + 5, ); export let sprLadder = new Sprite( diff --git a/src/world3d.ts b/src/world3d.ts new file mode 100644 index 0000000..13074d0 --- /dev/null +++ b/src/world3d.ts @@ -0,0 +1,140 @@ +import { Color, Grid, Point, Rect, Size } from "./engine/datatypes.ts"; +import { DrawPile } from "./drawpile.ts"; +import { GridArt } from "./gridart.ts"; +import { + BG_CEILING, + BG_WALL_OR_UNREVEALED, + FG_BOLD, + FG_TEXT, +} from "./colors.ts"; + +export class World3D { + #grid: Grid; + + constructor(size: Size) { + this.#grid = new Grid(size, () => null); + } + + set(at: Point, value: Element3D) { + this.#grid.set(at, value); + } + + drawCell(drawpile: DrawPile, gridArt: GridArt, xy: Point) { + const OFFSET_AIR = 0; + const OFFSET_TOP = 256; + const OFFSET_TOP_OF_TOP = 512; + let here = this.#grid.get(xy); + + if (here == null) { + drawpile.add(OFFSET_TOP, () => { + gridArt.drawCeiling(BG_CEILING); + }); + return; + } + + const getRevealedBlock = (dx: number, dy: number): Block3D | null => { + let xy2 = xy.offset(new Point(dx, dy)); + if (!new Rect(new Point(0, 0), this.#grid.size).contains(xy2)) { + return null; + } + + let other = this.#grid.get(xy.offset(new Point(dx, dy))); + if (other instanceof Block3D) { + return other; + } + return null; + }; + + let center = getRevealedBlock(0, 0); + if (center) { + drawpile.add(OFFSET_TOP, () => { + gridArt.drawCeiling(center.ceiling); + }); + return; + } + + let west = getRevealedBlock(-1, 0); + let east = getRevealedBlock(1, 0); + let north = getRevealedBlock(0, -1); + let south = getRevealedBlock(0, 1); + + if (north && west) { + drawpile.add(OFFSET_TOP_OF_TOP, () => { + gridArt.drawMouldingTopLeft(north.moulding); + }); + } + if (north) { + drawpile.add(OFFSET_AIR, () => { + gridArt.drawWallTop(north.dark); + }); + drawpile.add(OFFSET_TOP_OF_TOP, () => { + gridArt.drawMouldingTop(north.moulding); + }); + } + if (north && east) { + drawpile.add(OFFSET_TOP_OF_TOP, () => { + gridArt.drawMouldingTopRight(north.moulding); + }); + } + if (west) { + drawpile.add(OFFSET_AIR, () => { + gridArt.drawWallLeft(west.dark); + }); + drawpile.add(OFFSET_TOP_OF_TOP, () => { + gridArt.drawMouldingLeft(west.moulding); + }); + } + if (south && west) { + drawpile.add(OFFSET_TOP_OF_TOP, () => { + gridArt.drawMouldingBottomLeft(south.moulding); + }); + } + if (south) { + drawpile.add(OFFSET_AIR, () => { + gridArt.drawWallBottom(south.bright); + }); + drawpile.add(OFFSET_TOP_OF_TOP, () => { + gridArt.drawMouldingBottom(south.moulding); + }); + } + if (south && east) { + drawpile.add(OFFSET_TOP_OF_TOP, () => { + gridArt.drawMouldingBottomRight(south.moulding); + }); + } + if (east) { + drawpile.add(OFFSET_AIR, () => { + gridArt.drawWallRight(east.bright); + }); + drawpile.add(OFFSET_TOP_OF_TOP, () => { + gridArt.drawMouldingRight(east.moulding); + }); + } + } +} + +export type Element3D = Floor3D | Block3D | null; + +export class Floor3D { + constructor() {} +} + +export class Block3D { + readonly bright: Color; + readonly dark: Color; + readonly ceiling: Color; + + get moulding(): Color { + return this.dark; + } + + constructor(bright: Color, dark: Color, ceiling: Color) { + this.bright = bright; + this.dark = dark; + this.ceiling = ceiling; + } + + static standardWall(): Block3D { + return new Block3D(FG_BOLD, FG_TEXT, BG_WALL_OR_UNREVEALED); + } +} From 09e619208a086200b89ee8ae9670a932ac536e2d Mon Sep 17 00:00:00 2001 From: Nyeogmi Date: Sat, 22 Feb 2025 20:48:44 -0800 Subject: [PATCH 2/3] Flashier loot animations --- src/floater.ts | 132 ++++++++++++++++++++++++++++++++++++++++++++++++ src/huntmode.ts | 85 +++++++++++++++++-------------- src/newmap.ts | 4 ++ src/physics.ts | 36 +++++++++++++ src/pickups.ts | 59 ++++++++++++++++++---- 5 files changed, 268 insertions(+), 48 deletions(-) create mode 100644 src/floater.ts create mode 100644 src/physics.ts diff --git a/src/floater.ts b/src/floater.ts new file mode 100644 index 0000000..46f2093 --- /dev/null +++ b/src/floater.ts @@ -0,0 +1,132 @@ +import { BreakableBlockPickupCallbacks } from "./pickups.ts"; +import { Point, Rect, Size } from "./engine/datatypes.ts"; +import { displace } from "./physics.ts"; +import { getHuntMode } from "./huntmode.ts"; +import { DrawPile } from "./drawpile.ts"; +import { FLOOR_CELL_SIZE } from "./gridart.ts"; + +export class Floater { + xy: Point; + velocity: Point; + z: number; + velZ: number; + frame: number; + spin: number; + collected: boolean; + + #callbacks: BreakableBlockPickupCallbacks; + + constructor(xy: Point, z: number, callbacks: BreakableBlockPickupCallbacks) { + this.xy = xy; + this.velocity = new Point(0, 0); + + this.z = z; + this.velZ = 0; + + this.frame = 0; + this.spin = 0; + + this.collected = false; + + this.#callbacks = callbacks; + } + + update() { + let bbox = this.bbox; + let { displacement, dxy } = displace( + bbox, + this.velocity, + (r) => getHuntMode().isBlocked(r), + { bounce: 0.6 }, + ); + + this.xy = this.xy.offset(displacement); + this.velocity = dxy; + + this.velocity = new Point(this.velocity.x * 0.99, this.velocity.y * 0.99); + + this.velZ -= 0.04; + this.z += this.velZ; + if (this.z < 0) { + this.z = 0; + this.velZ = -this.velZ * 0.8; // minor bounce + } + + this.frame += 1; + this.spin += this.velocity.distance(new Point(0, 0)) * 4; + + let playerPos = getHuntMode().floatingPlayer; + let dist = playerPos.distance(this.xy); + let dir = Math.atan2(this.xy.y - playerPos.y, this.xy.x - playerPos.x); + + if (dist < 0.3) { + this.collect(); + } + let gravityAmt = (0.6 - dist) / 0.6; + gravityAmt = Math.pow(gravityAmt, 0.8); + if (gravityAmt > 0) { + let dx = -Math.cos(dir) * 0.005; + let dy = -Math.sin(dir) * 0.005; + this.velocity = this.velocity.offset( + new Point(gravityAmt * dx, gravityAmt * dy), + ); + } + } + collect() { + if (this.collected) { + return; + } + this.collected = true; + this.#callbacks.obtain(); + } + + draw(drawpile: DrawPile, globalOffset: Point) { + // TODO: Use some kind of global projection type + let xyLow = this.xy + .offset(new Point(-0.5, -0.5)) + .scale(FLOOR_CELL_SIZE) + .offset(globalOffset.negate()); + let z = this.z / 100.0; + let xy = new Point( + xyLow.x, + xyLow.y - z * 16, // not perspectivally accurate, but convincing + ); + + drawpile.add(0, () => { + this.drawParticle(xyLow.offset(new Point(0, 2)), true); + }); + if (this.frame >= 1200) { + if (this.frame % 30 < 15) { + return; + } + } else if (this.frame >= 960) { + if (this.frame % 60 < 30) { + return; + } + } else if (this.frame >= 720) { + if (this.frame % 120 < 60) { + return; + } + } + drawpile.add(128, () => { + this.drawParticle(xy, false); + }); + } + + get alive(): boolean { + return !this.collected && this.frame < 1440; + } + + get bbox(): Rect { + let w = 0.25; + let h = 0.25; + return new Rect(this.xy.offset(new Point(-w / 2, -h / 2)), new Size(w, h)); + } + drawParticle(projected: Point, isShadow: boolean): any { + this.#callbacks.drawParticle( + projected, + isShadow, + ((0 * Math.PI) / 4) * Math.cos(this.spin), + ); + } +} diff --git a/src/huntmode.ts b/src/huntmode.ts index 0137c0c..3ae9d03 100644 --- a/src/huntmode.ts +++ b/src/huntmode.ts @@ -16,9 +16,12 @@ import { withCamera } from "./layout.ts"; import { getCheckModal } from "./checkmodal.ts"; import { CARDINAL_DIRECTIONS } from "./mapgen.ts"; import { Block3D, Floor3D, World3D } from "./world3d.ts"; +import { Floater } from "./floater.ts"; +import { displace } from "./physics.ts"; export class HuntMode { map: LoadedNewMap; + floaters: Floater[]; floatingPlayer: Point; velocity: Point; faceLeft: boolean; @@ -29,6 +32,7 @@ export class HuntMode { constructor(depth: number, map: LoadedNewMap) { this.map = map; + this.floaters = []; this.floatingPlayer = map.entrance.offset(new Point(0.5, 0.5)); this.velocity = new Point(0, 0); this.faceLeft = false; @@ -109,6 +113,7 @@ export class HuntMode { this.#updatePlayer(); this.#updateFov(); + this.#updateFloaters(); this.#updatePickups(); let world3d = new World3D(this.map.size); @@ -127,6 +132,7 @@ export class HuntMode { } } + this.#drawFloaters(globalOffset); this.#drawPlayer(globalOffset); this.#drawBadges(globalOffset); @@ -134,6 +140,10 @@ export class HuntMode { this.drawpile.executeOnClick(); } + spawnFloater(floater: Floater) { + this.floaters.push(floater); + } + #updatePlayer() { let dx = this.velocity.x; let dy = this.velocity.y; @@ -186,7 +196,6 @@ export class HuntMode { this.faceLeft = false; } - let nSteps = 40; let szX = 0.5; let szY = 0.5; @@ -215,42 +224,17 @@ export class HuntMode { } } - let initialXy = this.floatingPlayer; - for (let i = 0; i < nSteps; i++) { - let oldXy = this.floatingPlayer; - let newXy = oldXy.offset(new Point(this.velocity.x / nSteps, 0)); - - let bbox = new Rect( - newXy.offset(new Point(-szX / 2, -szY / 2)), - new Size(szX, szY), - ); - - for (let cell of bbox.overlappedCells(new Size(1, 1)).values()) { - if (this.#blocksMovement(cell.top)) { - this.velocity = new Point(0, this.velocity.y); - newXy = oldXy; - } - } - - oldXy = newXy; - newXy = oldXy.offset(new Point(0, this.velocity.y / nSteps)); - - bbox = new Rect( - newXy.offset(new Point(-szX / 2, -szY / 2)), - new Size(szX, szY), - ); - - for (let cell of bbox.overlappedCells(new Size(1, 1)).values()) { - if (this.#blocksMovement(cell.top)) { - this.velocity = new Point(this.velocity.x, 0); - newXy = oldXy; - } - } - - this.floatingPlayer = newXy; - } - let finalXy = this.floatingPlayer; - getPlayerProgress().spendBlood(finalXy.distance(initialXy) * 10); + let origin = new Point(szX / 2, szY / 2); + let bbox = new Rect( + this.floatingPlayer.offset(origin.negate()), + new Size(szX, szY), + ); + let { displacement, dxy } = displace(bbox, this.velocity, (b: Rect) => + this.isBlocked(b), + ); + this.floatingPlayer = this.floatingPlayer.offset(displacement); + this.velocity = dxy; + getPlayerProgress().spendBlood(displacement.distance(new Point(0, 0)) * 10); } #updateFov() { @@ -280,6 +264,18 @@ export class HuntMode { ); } + #updateFloaters() { + let newFloaters = []; + for (let f of this.floaters.values()) { + f.update(); + if (f.alive) { + newFloaters.push(f); + } + } + + this.floaters = newFloaters; + } + #updatePickups() { for (let y = 0; y < this.map.size.h; y++) { for (let x = 0; x < this.map.size.w; x++) { @@ -386,6 +382,12 @@ export class HuntMode { world3d.set(mapPosition, new Floor3D()); } + #drawFloaters(globalOffset: Point) { + for (let f of this.floaters.values()) { + f.draw(this.drawpile, globalOffset); + } + } + #drawPlayer(_globalOffset: Point) { /* let cellOffset = this.pixelPlayer.offset(globalOffset.negate()); @@ -452,6 +454,15 @@ export class HuntMode { }); } + isBlocked(bbox: Rect): boolean { + for (let cell of bbox.overlappedCells(new Size(1, 1)).values()) { + if (this.#blocksMovement(cell.top)) { + return true; + } + } + return false; + } + #blocksMovement(xy: Point) { let cell = this.map.get(xy); if (cell.architecture == Architecture.Wall) { diff --git a/src/newmap.ts b/src/newmap.ts index a8fc213..6e12717 100644 --- a/src/newmap.ts +++ b/src/newmap.ts @@ -116,6 +116,10 @@ export class CellView { this.#point = point; } + get xy(): Point { + return this.#point; + } + set architecture(value: Architecture) { this.#map.setArchitecture(this.#point, value); } diff --git a/src/physics.ts b/src/physics.ts new file mode 100644 index 0000000..67595eb --- /dev/null +++ b/src/physics.ts @@ -0,0 +1,36 @@ +import { Point, Rect } from "./engine/datatypes.ts"; + +export function displace( + bbox: Rect, + dxy: Point, + blocked: (where: Rect) => boolean, + options?: { bounce?: number }, +): { bbox: Rect; displacement: Point; dxy: Point } { + let nSteps = 40; + let bounce = options?.bounce ?? 0; + + let xy = bbox.top; + for (let i = 0; i < nSteps; i++) { + let trialXy = xy.offset(new Point(dxy.x / nSteps, 0)); + let trialBbox = new Rect(trialXy, bbox.size); + + if (blocked(trialBbox)) { + dxy = new Point(bounce * -dxy.x, dxy.y); + } else { + xy = trialXy; + } + + trialXy = xy.offset(new Point(0, dxy.y / nSteps)); + trialBbox = new Rect(trialXy, bbox.size); + if (blocked(trialBbox)) { + dxy = new Point(dxy.x, bounce * -dxy.y); + } else { + xy = trialXy; + } + } + return { + bbox: new Rect(xy, bbox.size), + displacement: xy.offset(bbox.top.negate()), + dxy, + }; +} diff --git a/src/pickups.ts b/src/pickups.ts index 95ffa7c..eefed2a 100644 --- a/src/pickups.ts +++ b/src/pickups.ts @@ -15,15 +15,10 @@ import { GridArt } from "./gridart.ts"; import { getCheckModal } from "./checkmodal.ts"; import { Point, Size } from "./engine/datatypes.ts"; import { choose } from "./utils.ts"; -import { - BG_CEILING, - FG_BOLD, - FG_TEXT, - SWATCH_EXP, - SWATCH_STAT, -} from "./colors.ts"; +import { FG_BOLD, FG_TEXT, SWATCH_EXP, SWATCH_STAT } from "./colors.ts"; import { Block3D } from "./world3d.ts"; import { DrawPile } from "./drawpile.ts"; +import { Floater } from "./floater.ts"; export type Pickup = | LockPickup @@ -85,12 +80,16 @@ export class LockPickup { onSqueeze() {} } +export type BreakableBlockPickupCallbacks = + | StatPickupCallbacks + | ExperiencePickupCallbacks; + const RECOVERY_PER_TICK: number = 0.1; export class BreakableBlockPickup { - callbacks: StatPickupCallbacks | ExperiencePickupCallbacks; + callbacks: BreakableBlockPickupCallbacks; breakProgress: number; - constructor(callbacks: StatPickupCallbacks | ExperiencePickupCallbacks) { + constructor(callbacks: BreakableBlockPickupCallbacks) { this.callbacks = callbacks; this.breakProgress = 0.0; } @@ -145,7 +144,23 @@ export class BreakableBlockPickup { if (this.breakProgress >= 1.0) { getPlayerProgress().spendBlood(this.callbacks.cost); cellData.pickup = null; - this.callbacks.obtain(); + + let n = choose([1, 1, 1, 1, 1, 2, 3]); + for (let i = 0; i < n; i++) { + let floater = new Floater( + cellData.xy.offset(new Point(0.5, 0.5)), + 50, + this.callbacks, + ); + let speed = 0.015; + let direction = Math.random() * Math.PI * 2; + floater.velocity = new Point( + Math.cos(direction) * speed, + Math.sin(direction) * speed * 0.1, + ); + floater.velZ = 0.8; + getHuntMode().spawnFloater(floater); + } } this.breakProgress = Math.max(0.0, this.breakProgress - RECOVERY_PER_TICK); @@ -199,6 +214,19 @@ export class StatPickupCallbacks { let at = gridArt.project(100); D.drawSprite(sprCollectiblesSilhouettes, at, statIndex, options); } + + drawParticle(at: Point, isShadow: boolean, rotation: number) { + let statIndex = ALL_STATS.indexOf(this.#stat); + if (statIndex == -1) { + return; + } + D.drawSprite( + isShadow ? sprCollectiblesSilhouettes : sprCollectibles, + at, + statIndex, + { xScale: 2 * Math.cos(rotation), yScale: 2 }, + ); + } } export class ExperiencePickupCallbacks { @@ -226,7 +254,16 @@ export class ExperiencePickupCallbacks { options: { xScale?: number; yScale?: number; angle?: number }, ) { let at = gridArt.project(100); - D.drawSprite(sprCollectiblesSilhouettes, at, 0, options); + D.drawSprite(sprCollectiblesSilhouettes, at, 4, options); + } + + drawParticle(at: Point, isShadow: boolean, rotation: number) { + D.drawSprite( + isShadow ? sprCollectiblesSilhouettes : sprCollectibles, + at, + 4, + { xScale: 2 * Math.cos(rotation), yScale: 2 }, + ); } } From bd1cff68e64ae7cfb9a728fd6075ffccd695a5b2 Mon Sep 17 00:00:00 2001 From: Nyeogmi Date: Sat, 22 Feb 2025 21:08:03 -0800 Subject: [PATCH 3/3] Rewrite thrall rewards --- src/floater.ts | 24 +++++++++++++----------- src/pickups.ts | 27 +++++++++++++++++++++++++-- src/thralls.ts | 32 ++++++++++++++++---------------- 3 files changed, 54 insertions(+), 29 deletions(-) diff --git a/src/floater.ts b/src/floater.ts index 46f2093..48474b4 100644 --- a/src/floater.ts +++ b/src/floater.ts @@ -59,17 +59,19 @@ export class Floater { let dist = playerPos.distance(this.xy); let dir = Math.atan2(this.xy.y - playerPos.y, this.xy.x - playerPos.x); - if (dist < 0.3) { - this.collect(); - } - let gravityAmt = (0.6 - dist) / 0.6; - gravityAmt = Math.pow(gravityAmt, 0.8); - if (gravityAmt > 0) { - let dx = -Math.cos(dir) * 0.005; - let dy = -Math.sin(dir) * 0.005; - this.velocity = this.velocity.offset( - new Point(gravityAmt * dx, gravityAmt * dy), - ); + if (this.frame >= 60) { + if (dist < 0.5) { + this.collect(); + } + let gravityAmt = (0.6 - dist) / 0.6; + gravityAmt = Math.pow(gravityAmt, 0.8); + if (gravityAmt > 0) { + let dx = -Math.cos(dir) * 0.005; + let dy = -Math.sin(dir) * 0.005; + this.velocity = this.velocity.offset( + new Point(gravityAmt * dx, gravityAmt * dy), + ); + } } } collect() { diff --git a/src/pickups.ts b/src/pickups.ts index eefed2a..460bd98 100644 --- a/src/pickups.ts +++ b/src/pickups.ts @@ -600,7 +600,7 @@ export class ThrallCollectionPlatePickup { update() {} - onClick(_cell: CellView): boolean { + onClick(cell: CellView): boolean { let lifeStage = getPlayerProgress().getThrallLifeStage(this.thrall); let itemStage = getPlayerProgress().getThrallItemStage(this.thrall); let data = getThralls().get(this.thrall); @@ -650,7 +650,7 @@ export class ThrallCollectionPlatePickup { }, null, ); - data.rewardCallback(); + data.rewardCallback((what) => this.spawn(cell.xy, what)); } } @@ -658,6 +658,29 @@ export class ThrallCollectionPlatePickup { return true; } + spawn(xy: Point, what: Stat | "EXP") { + let callbacks = null; + if (what == "EXP") { + callbacks = new ExperiencePickupCallbacks(); + } else { + callbacks = new StatPickupCallbacks(what); + } + if (callbacks == null) { return; } + let floater = new Floater( + xy.offset(new Point(0.5, 0.5)), + 50, + callbacks, + ); + let speed = 0.015; + let direction = Math.random() * Math.PI * 2; + floater.velocity = new Point( + Math.cos(direction) * speed, + Math.sin(direction) * speed * 0.1, + ); + floater.velZ = 0.8; + getHuntMode().spawnFloater(floater); + } + onSqueeze() {} } diff --git a/src/thralls.ts b/src/thralls.ts index 6de2d12..84ed49c 100644 --- a/src/thralls.ts +++ b/src/thralls.ts @@ -22,7 +22,7 @@ import { sprThrallStealth, } from "./sprites.ts"; import { Sprite } from "./engine/internal/sprite.ts"; -import { getPlayerProgress } from "./playerprogress.ts"; +import {Stat} from "./datatypes.ts"; export type Thrall = { id: number; @@ -62,7 +62,7 @@ export type ThrallData = { itemPickupMessage: string; deliveryMessage: string; rewardMessage: string; - rewardCallback: () => void; + rewardCallback: (spawn: (what: Stat | "EXP") => void) => void; lifeStageText: Record; }; @@ -132,8 +132,8 @@ export let thrallParty = table.add({ deliveryMessage: '"Oh, that? Yeah, I won it." And then lost it, apparently.\n\nHe\'s elated. He will never leave.', rewardMessage: "Garrett showers you with INT!", - rewardCallback: () => { - getPlayerProgress().add("INT", 10); + rewardCallback: (spawn) => { + for (let i = 0; i < 30; i++) { spawn("INT"); } }, lifeStageText: { fresh: { @@ -204,8 +204,8 @@ export let thrallLore = table.add({ deliveryMessage: "Lupin looks at his own reflection -- with interest, confusion, dismissal, and then deep satisfaction. He loves it. He will never leave.", rewardMessage: "Lupin showers you with AGI!", - rewardCallback: () => { - getPlayerProgress().add("AGI", 10); + rewardCallback: (spawn) => { + for (let i = 0; i < 30; i++) { spawn("AGI"); } }, lifeStageText: { fresh: { @@ -273,9 +273,9 @@ export let thrallBat = table.add({ deliveryMessage: 'Monica salivates. "This is... this is... simply exquisite!"\n\nShe is happy. She will never leave.', rewardMessage: "Monica showers you with CHA and INT!", - rewardCallback: () => { - getPlayerProgress().add("CHA", 5); - getPlayerProgress().add("INT", 5); + rewardCallback: (spawn) => { + for (let i = 0; i < 15; i++) { spawn("CHA"); } + for (let i = 0; i < 15; i++) { spawn("INT"); } }, lifeStageText: { fresh: { @@ -343,8 +343,8 @@ export let thrallCharm = table.add({ deliveryMessage: "Renfield inhales sharply and widens his stance, trying to hide his physical reaction to your face. He is elated and will never leave.", rewardMessage: "Renfield showers you with PSI!", - rewardCallback: () => { - getPlayerProgress().add("PSI", 10); + rewardCallback: (spawn) => { + for (let i = 0; i < 24; i++) { spawn("PSI"); } }, lifeStageText: { fresh: { @@ -410,9 +410,9 @@ export let thrallStealth = table.add({ deliveryMessage: "\"That? That's not mine.\" But she wants it. Now it's hers. She will never leave.", rewardMessage: "Narthyss showers you with CHA and AGI!", - rewardCallback: () => { - getPlayerProgress().add("CHA", 5); - getPlayerProgress().add("AGI", 5); + rewardCallback: (spawn) => { + for (let i = 0; i < 15; i++) { spawn("CHA"); } + for (let i = 0; i < 15; i++) { spawn("AGI"); } }, lifeStageText: { fresh: { @@ -479,8 +479,8 @@ export let thrallStare = table.add({ deliveryMessage: "Ridley admires the gear but -- to your surprise -- refuses to jam it into its brain.\n\nThe pup is elated and will never leave.", rewardMessage: "Ridley showers you with EXP!", - rewardCallback: () => { - getPlayerProgress().addExperience(100); + rewardCallback: (spawn) => { + for (let i = 0; i < 6; i++) { spawn("EXP"); } }, lifeStageText: { fresh: {