From bc1b21811b6af79ca2f3ec4cad1163a743ed2d93 Mon Sep 17 00:00:00 2001 From: Artemka374 Date: Sat, 4 Apr 2026 14:37:59 +0200 Subject: [PATCH] Add pawn shop and slot machine gold economy --- data/resources.lua | 18 ++ src/inventory/inventory.lua | 10 + src/inventory/supplydepot.lua | 17 +- src/states/daystate.lua | 409 ++++++++++++++++++++++++++++++---- src/ui/hud.lua | 38 +++- src/world/casino.lua | 66 ++++-- src/world/pawnshop.lua | 86 +++++++ src/world/tilemanager.lua | 13 ++ 8 files changed, 580 insertions(+), 77 deletions(-) create mode 100644 src/world/pawnshop.lua diff --git a/data/resources.lua b/data/resources.lua index 1040cb8..c678cf2 100644 --- a/data/resources.lua +++ b/data/resources.lua @@ -5,6 +5,7 @@ return { wood = { name = "Wood", weight = 2, -- carry-weight units per item + goldValue = 1, color = {0.55, 0.35, 0.15}, -- placeholder render colour harvestTime = 3.0, -- seconds to harvest one batch yieldMin = 2, -- min items per harvest @@ -16,6 +17,7 @@ return { iron = { name = "Iron", weight = 5, + goldValue = 4, color = {0.60, 0.60, 0.65}, harvestTime = 5.0, yieldMin = 1, @@ -27,6 +29,7 @@ return { stone = { name = "Stone", weight = 4, + goldValue = 1, color = {0.50, 0.50, 0.50}, harvestTime = 4.0, yieldMin = 2, @@ -38,6 +41,7 @@ return { rope = { name = "Rope", weight = 1, + goldValue = 2, color = {0.75, 0.70, 0.40}, harvestTime = 2.0, yieldMin = 1, @@ -49,6 +53,7 @@ return { food = { name = "Food", weight = 1, + goldValue = 1, color = {0.90, 0.30, 0.30}, harvestTime = 1.5, yieldMin = 1, @@ -60,6 +65,7 @@ return { cloth = { name = "Cloth", weight = 1, + goldValue = 3, color = {0.85, 0.85, 0.90}, harvestTime = 2.5, yieldMin = 1, @@ -67,4 +73,16 @@ return { sourceTiles = {"grass"}, biomes = {"plains"}, }, + + gold = { + name = "Gold", + weight = 0, + goldValue = 0, + color = {0.98, 0.82, 0.20}, + harvestTime = 0, + yieldMin = 0, + yieldMax = 0, + sourceTiles = {}, + biomes = {}, + }, } diff --git a/src/inventory/inventory.lua b/src/inventory/inventory.lua index 94f4413..b56dfcc 100644 --- a/src/inventory/inventory.lua +++ b/src/inventory/inventory.lua @@ -41,6 +41,16 @@ function Inventory:add(resourceType, amount) return actual end +-- Add `amount` units even if it exceeds the normal carry capacity. +-- Used for special cases like casino payouts that should remain re-bettable. +function Inventory:forceAdd(resourceType, amount) + if amount <= 0 then return 0 end + + self._items[resourceType] = (self._items[resourceType] or 0) + amount + self._weight = self._weight + amount * unitWeight(resourceType) + return amount +end + -- Remove `amount` units. Returns actual amount removed. function Inventory:remove(resourceType, amount) local have = self._items[resourceType] or 0 diff --git a/src/inventory/supplydepot.lua b/src/inventory/supplydepot.lua index 61958f8..82d0bae 100644 --- a/src/inventory/supplydepot.lua +++ b/src/inventory/supplydepot.lua @@ -63,14 +63,21 @@ end -- Deposit all items from `inventory` into the depot. function SupplyDepot:depositAll(inventory) - local items = inventory:clear() + local items = inventory:getItems() + local deposited = {} for rtype, amt in pairs(items) do - self:add(rtype, amt) - if self.eventBus then - self.eventBus:publish("resource_deposited", rtype, amt) + if rtype ~= "gold" and amt > 0 then + local removed = inventory:remove(rtype, amt) + if removed > 0 then + self:add(rtype, removed) + deposited[rtype] = removed + if self.eventBus then + self.eventBus:publish("resource_deposited", rtype, removed) + end + end end end - return items + return deposited end -- Withdraw `amount` of `resourceType` into `inventory`. Returns actual withdrawn. diff --git a/src/states/daystate.lua b/src/states/daystate.lua index 2eeccce..8de2077 100644 --- a/src/states/daystate.lua +++ b/src/states/daystate.lua @@ -16,6 +16,7 @@ local RespawnManager = require("src.resources.respawn") local Inventory = require("src.inventory.inventory") local SupplyDepot = require("src.inventory.supplydepot") local Casino = require("src.world.casino") +local PawnShop = require("src.world.pawnshop") local BuildManager = require("src.buildings.buildmanager") local BuildGhost = require("src.buildings.buildghost") local HUD = require("src.ui.hud") @@ -43,12 +44,14 @@ local BASE_LAYOUT = { basecore = { tx = 0, ty = 0 }, depot = { tx = 4, ty = 1 }, casino = { tx = -4, ty = 1 }, + pawnshop = { tx = 0, ty = -4 }, player = { tx = 0, ty = 4 }, } local BASE_CLEARANCE = { basecore = { x1 = -1, y1 = -1, x2 = 1, y2 = 1 }, depot = { x1 = -1, y1 = -1, x2 = 1, y2 = 1 }, casino = { x1 = -2, y1 = -1, x2 = 2, y2 = 2 }, + pawnshop = { x1 = -1, y1 = -1, x2 = 1, y2 = 1 }, player = { x1 = -1, y1 = -1, x2 = 1, y2 = 1 }, } @@ -64,6 +67,8 @@ function DayState:new(game) self.game = game self.font = love.graphics.newFont(24) self.smallFont = love.graphics.newFont(14) + self.slotTitleFont = love.graphics.newFont(18) + self.slotFont = love.graphics.newFont(26) self.timeRemaining = 270 local sw = love.graphics.getWidth() @@ -98,11 +103,25 @@ function DayState:new(game) self.depot:add("wood", 20) self.depot:add("stone", 10) self.casino = Casino(-2, 2) + self.pawnShop = PawnShop(0, -2) self.casinoInterior = { active = false, chunks = nil, spawn = { tx = 5, ty = 7 }, - table = { tx = 5, ty = 3 }, + slotMachine = { tx = 5, ty = 3 }, + slotUIActive = false, + stakeAmount = 0, + slotSpin = { + active = false, + elapsed = 0, + duration = 1.2, + reelInterval = 0.09, + nextRollAt = 0, + reels = { "CHERRY", "CHERRY", "CHERRY" }, + displayReels = { "CHERRY", "CHERRY", "CHERRY" }, + reelStopTimes = { 0.60, 0.90, 1.20 }, + pending = nil, + }, returnPlayer = nil, } @@ -130,9 +149,13 @@ function DayState:_getActiveChunks() return self:_isInCasino() and self.casinoInterior.chunks or self.chunks end -function DayState:_isNearCasinoTable() - local dx = self.player.tx - self.casinoInterior.table.tx - local dy = self.player.ty - self.casinoInterior.table.ty +function DayState:_isWithinActiveMap(tx, ty) + return self.game.tileManager:isWithinActiveTilemap(tx, ty) +end + +function DayState:_isNearSlotMachine() + local dx = self.player.tx - self.casinoInterior.slotMachine.tx + local dy = self.player.ty - self.casinoInterior.slotMachine.ty return math.sqrt(dx * dx + dy * dy) <= 2.0 end @@ -177,6 +200,10 @@ function DayState:_exitCasino() self.casinoInterior.active = false self.casinoInterior.chunks = nil self.casinoInterior.returnPlayer = nil + self.casinoInterior.slotUIActive = false + self.casinoInterior.slotSpin.active = false + self.casinoInterior.slotSpin.pending = nil + self.casinoInterior.stakeAmount = 0 self.game.tileManager:setProceduralSource(self.worldSeed) self.player.tx = returnPlayer.tx self.player.ty = returnPlayer.ty @@ -233,6 +260,10 @@ function DayState:exit() self.casinoInterior.active = false self.casinoInterior.chunks = nil self.casinoInterior.returnPlayer = nil + self.casinoInterior.slotUIActive = false + self.casinoInterior.slotSpin.active = false + self.casinoInterior.slotSpin.pending = nil + self.casinoInterior.stakeAmount = 0 self.game.tileManager:setProceduralSource(self.worldSeed) end self.ghost:deactivate() @@ -242,6 +273,12 @@ function DayState:exit() end function DayState:_updateMovement(dt) + if self:_isInCasino() and self.casinoInterior.slotUIActive then + self.playerCharacter:setDesiredMovement(0, 0) + self.playerCharacter:updateState() + return + end + if self.dash.active then self:_updateDash(dt) return @@ -359,6 +396,7 @@ function DayState:update(dt) self:_updateCamera(dt) self:_updateBuildCursor() self:_updateWorld() + self:_updateCasinoSpin(dt) if not self:_isInCasino() then self.harvest:update(dt, self.player.tx, self.player.ty, self.nodes, self.inventory) @@ -371,6 +409,133 @@ function DayState:update(dt) self:_syncPlayerCharacterPosition() end +function DayState:_startSlotSpin() + local stakeItems, totalIn = self:_buildSlotStake() + if totalIn <= 0 then + self:_showNotice("Bring more gold for that stake.", 2.8) + return + end + + for resourceType, amount in pairs(stakeItems) do + self.inventory:remove(resourceType, amount) + end + + local ok, result = self.casino:beginSlotSpin(stakeItems) + if not ok then + for resourceType, amount in pairs(stakeItems) do + self.inventory:forceAdd(resourceType, amount) + end + self:_showNotice(result, 2.8) + return + end + + local spin = self.casinoInterior.slotSpin + spin.active = true + spin.elapsed = 0 + spin.nextRollAt = 0 + spin.pending = result + spin.reels = { "CHERRY", "BELL", "STAR" } + spin.displayReels = self.casino:rollSlotReels() +end + +function DayState:_openSlotMachineUI() + self.casinoInterior.slotUIActive = true + self:_normalizeSlotStake(true) +end + +function DayState:_closeSlotMachineUI() + if self.casinoInterior.slotSpin.active then + return + end + + self.casinoInterior.slotUIActive = false +end + +function DayState:_formatSlotStake() + local _, totalIn = self:_buildSlotStake() + local totalGold = self.inventory:count("gold") + + return string.format("%d gold selected | %d available", totalIn, totalGold) +end + +function DayState:_buildSlotStake() + local totalIn = self:_normalizeSlotStake(false) + + return { gold = totalIn }, totalIn +end + +function DayState:_normalizeSlotStake(resetIfEmpty) + local goldAmount = self.inventory:count("gold") + if goldAmount <= 0 then + self.casinoInterior.stakeAmount = 0 + return 0 + end + + if resetIfEmpty and self.casinoInterior.stakeAmount <= 0 then + self.casinoInterior.stakeAmount = 1 + end + + self.casinoInterior.stakeAmount = math.max(1, math.min(self.casinoInterior.stakeAmount, goldAmount)) + return self.casinoInterior.stakeAmount +end + +function DayState:_changeSlotStake(direction) + if self.casinoInterior.slotSpin.active then + return + end + + local currentStake = self:_normalizeSlotStake(true) + if currentStake <= 0 then + return + end + + local nextStake = currentStake + direction + self.casinoInterior.stakeAmount = nextStake + self:_normalizeSlotStake(false) +end + +function DayState:_currentStakeLabel() + return tostring(self:_normalizeSlotStake(false)) .. "g" +end + +function DayState:_updateCasinoSpin(dt) + if not self:_isInCasino() then + return + end + + local spin = self.casinoInterior.slotSpin + if self.casinoInterior.slotUIActive and not spin.active and not self:_isNearSlotMachine() then + self:_closeSlotMachineUI() + return + end + + if not spin.active then + return + end + + spin.elapsed = spin.elapsed + dt + if spin.elapsed >= spin.nextRollAt and spin.elapsed < spin.duration then + local nextReels = self.casino:rollSlotReels() + for i = 1, 3 do + if spin.elapsed < spin.reelStopTimes[i] then + spin.displayReels[i] = nextReels[i] + else + spin.displayReels[i] = spin.pending.reels[i] + end + end + spin.nextRollAt = spin.nextRollAt + spin.reelInterval + end + + if spin.elapsed >= spin.duration then + spin.active = false + spin.reels = spin.pending.reels + spin.displayReels = spin.pending.reels + local _, message = self.casino:resolveSlotSpin(spin.pending, self.inventory) + spin.pending = nil + self:_showNotice(message, 4.2) + end +end + function DayState:_ensureValidSpawn() local spawnTx, spawnTy = self:_findNearestWalkableTile(self.player.tx, self.player.ty) self.player.tx = spawnTx @@ -387,6 +552,7 @@ function DayState:_canPlaceBaseLayout(originTx, originTy) { name = "basecore", offset = BASE_LAYOUT.basecore }, { name = "depot", offset = BASE_LAYOUT.depot }, { name = "casino", offset = BASE_LAYOUT.casino }, + { name = "pawnshop", offset = BASE_LAYOUT.pawnshop }, { name = "player", offset = BASE_LAYOUT.player }, } @@ -450,6 +616,8 @@ function DayState:_ensureBaseLayout() self.depot.ty = originTy + BASE_LAYOUT.depot.ty self.casino.tx = originTx + BASE_LAYOUT.casino.tx self.casino.ty = originTy + BASE_LAYOUT.casino.ty + self.pawnShop.tx = originTx + BASE_LAYOUT.pawnshop.tx + self.pawnShop.ty = originTy + BASE_LAYOUT.pawnshop.ty self.player.tx = originTx + BASE_LAYOUT.player.tx self.player.ty = originTy + BASE_LAYOUT.player.ty self.player.ftx = 0 @@ -492,6 +660,10 @@ function DayState:_canMoveTo(tx, ty) local tileX = math.floor(point[1] + 0.5) local tileY = math.floor(point[2] + 0.5) + if self:_isInCasino() and not self:_isWithinActiveMap(tileX, tileY) then + return false + end + if not self:_isInCasino() then local building = self.buildings:getAt(tileX, tileY) if building and building.def and building.def.blocksMovement then @@ -733,10 +905,12 @@ function DayState:_buildDrawList(x1, y1, x2, y2) if self:_isInCasino() then for tx = x1, x2 do for ty = y1, y2 do - local tileType = activeChunks:getTile(tx, ty) - if tileType then - local _, sy = Iso.tileToScreen(tx, ty) - list[#list + 1] = { sy = sy, tx = tx, ty = ty, t = tileType, visible = true } + if self:_isWithinActiveMap(tx, ty) then + local tileType = activeChunks:getTile(tx, ty) + if tileType then + local _, sy = Iso.tileToScreen(tx, ty) + list[#list + 1] = { sy = sy, tx = tx, ty = ty, t = tileType, visible = true } + end end end end @@ -899,31 +1073,51 @@ function DayState:_drawCasino() end end -function DayState:_drawCasinoTable() - local tx = self.casinoInterior.table.tx - local ty = self.casinoInterior.table.ty +function DayState:_drawPawnShop() + if self:_isInCasino() then + return + end + + self.pawnShop:draw() + if self.pawnShop:isNearby(self.player.tx, self.player.ty) then + self.pawnShop:drawNearbyHint() + end +end + +function DayState:_drawSlotMachine() + local tx = self.casinoInterior.slotMachine.tx + local ty = self.casinoInterior.slotMachine.ty local sx, sy = Iso.tileToScreen(tx, ty) local alpha = 0.55 + 0.35 * math.sin(love.timer.getTime() * 3.5) + local spin = self.casinoInterior.slotSpin - love.graphics.setColor(0.86, 0.12, 0.12, 0.80) - love.graphics.polygon("fill", - sx, sy + 4, - sx + 22, sy + 16, - sx, sy + 28, - sx - 22, sy + 16) + love.graphics.setColor(0.70, 0.12, 0.12, 0.88) + love.graphics.rectangle("fill", sx - 18, sy - 10, 36, 40, 6, 6) love.graphics.setColor(0.95, 0.80, 0.22, 0.95) - love.graphics.polygon("line", - sx, sy + 4, - sx + 22, sy + 16, - sx, sy + 28, - sx - 22, sy + 16) - - if self:_isNearCasinoTable() then + love.graphics.rectangle("line", sx - 18, sy - 10, 36, 40, 6, 6) + love.graphics.setColor(0.12, 0.12, 0.12, 1) + love.graphics.rectangle("fill", sx - 12, sy - 4, 24, 12, 4, 4) + love.graphics.setColor(1, 0.95, 0.65, 1) + love.graphics.rectangle("line", sx - 12, sy - 4, 24, 12, 4, 4) + love.graphics.setFont(love.graphics.newFont(8)) + love.graphics.printf(string.sub(spin.reels[1], 1, 1) .. string.sub(spin.reels[2], 1, 1) .. string.sub(spin.reels[3], 1, 1), sx - 12, sy - 2, 24, "center") + + if spin.active then love.graphics.setColor(0.95, 0.80, 0.22, alpha) love.graphics.circle("line", sx, sy + 16, 34) love.graphics.setColor(1, 0.98, 0.90, alpha) love.graphics.setFont(love.graphics.newFont(11)) - love.graphics.print("G: Gamble | ESC: Leave", sx - 58, sy + 38) + love.graphics.print("Spinning...", sx - 28, sy + 38) + elseif self:_isNearSlotMachine() then + love.graphics.setColor(0.95, 0.80, 0.22, alpha) + love.graphics.circle("line", sx, sy + 16, 34) + love.graphics.setColor(1, 0.98, 0.90, alpha) + love.graphics.setFont(love.graphics.newFont(11)) + if self.casinoInterior.slotUIActive then + love.graphics.print("G: Spin Slots | ESC: Close", sx - 62, sy + 38) + else + love.graphics.print("G: Use Slots", sx - 28, sy + 38) + end end end @@ -949,16 +1143,27 @@ function DayState:_queueCasinoDraw(entityDrawList) } end -function DayState:_queueCasinoTableDraw(entityDrawList) +function DayState:_queuePawnShopDraw(entityDrawList) + if self:_isInCasino() then return end + if self.fog:getState(self.pawnShop.tx, self.pawnShop.ty) == "hidden" then return end + local _, pawnShopSy = Iso.tileToScreen(self.pawnShop.tx, self.pawnShop.ty) + entityDrawList[#entityDrawList + 1] = { + sy = pawnShopSy, + order = 42, + draw = function() self:_drawPawnShop() end + } +end + +function DayState:_queueSlotMachineDraw(entityDrawList) if not self:_isInCasino() then return end - local _, sy = Iso.tileToScreen(self.casinoInterior.table.tx, self.casinoInterior.table.ty) + local _, sy = Iso.tileToScreen(self.casinoInterior.slotMachine.tx, self.casinoInterior.slotMachine.ty) entityDrawList[#entityDrawList + 1] = { sy = sy, order = 45, - draw = function() self:_drawCasinoTable() end + draw = function() self:_drawSlotMachine() end } end @@ -993,7 +1198,8 @@ function DayState:draw() self:_queueVisibleNodeDraws(entityDrawList, x1, y1, x2, y2) self:_queueDepotDraw(entityDrawList) self:_queueCasinoDraw(entityDrawList) - self:_queueCasinoTableDraw(entityDrawList) + self:_queuePawnShopDraw(entityDrawList) + self:_queueSlotMachineDraw(entityDrawList) self:_queuePlayerDraw(entityDrawList) self:_sortAndDrawEntities(entityDrawList) @@ -1011,8 +1217,11 @@ function DayState:draw() self.hud:draw(self.game, self.inventory, self.depot, self.player, self.ghost, { inCasino = self:_isInCasino(), + slotUIActive = self.casinoInterior.slotUIActive, + slotSpinActive = self.casinoInterior.slotSpin.active, + slotStakeLabel = self:_currentStakeLabel(), }) - if self:_isInCasino() then + if self:_isInCasino() and self.casinoInterior.slotUIActive then self:_drawCasinoOverlay() end self:_drawNotice() @@ -1136,13 +1345,14 @@ function DayState:_countLoadedChunks() return count end -function DayState:_showNotice(text) +function DayState:_showNotice(text, holdTime) self.notice.text = text self.notice.alpha = 0 self.notice.y = 36 self.game.tweens:stop(self.notice) + holdTime = holdTime or 1.0 self.game.tweens:to(self.notice, 0.18, { alpha = 1, y = 52 }):ease("quadout"):oncomplete(function() - self.game.tweens:to(self.notice, 0.35, { alpha = 0, y = 58 }):delay(1.0) + self.game.tweens:to(self.notice, 0.35, { alpha = 0, y = 58 }):delay(holdTime) end) end @@ -1159,14 +1369,77 @@ function DayState:_drawNotice() end function DayState:_drawCasinoOverlay() + local sw, sh = love.graphics.getWidth(), love.graphics.getHeight() + local spin = self.casinoInterior.slotSpin + local panelW, panelH = 560, 260 + local panelX = (sw - panelW) * 0.5 + local panelY = 34 + local reelY = panelY + 64 + local reelW = 116 + local reelH = 92 + local reelGap = 24 + local firstReelX = panelX + 58 + + local function shortSymbol(symbol) + if symbol == "CHERRY" then return "CHERRY" end + if symbol == "BELL" then return "BELL" end + if symbol == "STAR" then return "STAR" end + return "7" + end + + local function drawReel(x, y, currentSymbol, active, index) + local upperSymbols = { "CHERRY", "BELL", "STAR" } + local lowerSymbols = { "BELL", "STAR", "7" } + + love.graphics.setColor(0.88, 0.70, 0.20, 1) + love.graphics.rectangle("fill", x - 8, y - 8, reelW + 16, reelH + 16, 12, 12) + love.graphics.setColor(0.97, 0.95, 0.90, 1) + love.graphics.rectangle("fill", x, y, reelW, reelH, 10, 10) + love.graphics.setColor(0.76, 0.58, 0.12, 1) + love.graphics.rectangle("line", x, y, reelW, reelH, 10, 10) + + love.graphics.setFont(self.smallFont) + love.graphics.setColor(0.45, 0.45, 0.45, active and 0.22 or 0.12) + love.graphics.printf(shortSymbol(upperSymbols[index]), x, y + 10, reelW, "center") + love.graphics.printf(shortSymbol(lowerSymbols[index]), x, y + 58, reelW, "center") + + love.graphics.setFont(self.slotFont) + if currentSymbol == "7" then + love.graphics.setColor(0.84, 0.08, 0.12, 1) + else + love.graphics.setColor(0.16, 0.20, 0.24, 1) + end + love.graphics.printf(shortSymbol(currentSymbol), x, y + 28, reelW, "center") + end + + love.graphics.setColor(0, 0, 0, 0.76) + love.graphics.rectangle("fill", panelX, panelY, panelW, panelH, 18, 18) + love.graphics.setColor(0.86, 0.68, 0.16, 1) + love.graphics.rectangle("line", panelX, panelY, panelW, panelH, 18, 18) + + love.graphics.setFont(self.slotTitleFont) + love.graphics.setColor(0.98, 0.84, 0.22, 1) + love.graphics.printf("HOLDFAST SLOTS", panelX, panelY + 18, panelW, "center") + + for i = 1, 3 do + drawReel( + firstReelX + (i - 1) * (reelW + reelGap), + reelY, + spin.displayReels[i], + spin.active and spin.elapsed < spin.reelStopTimes[i], + i + ) + end + love.graphics.setFont(self.smallFont) - love.graphics.setColor(0, 0, 0, 0.72) - love.graphics.rectangle("fill", 16, 56, 310, 64, 10, 10) - love.graphics.setColor(0.95, 0.80, 0.22, 1) - love.graphics.print("CASINO FLOOR", 28, 68) love.graphics.setColor(1, 0.97, 0.88, 0.95) - love.graphics.print("Reach the table and press G to gamble.", 28, 88) - love.graphics.print("Press ESC to leave the building.", 28, 104) + if spin.active then + love.graphics.printf("Spinning reels...", panelX, panelY + 172, panelW, "center") + else + love.graphics.printf("Stake:", panelX, panelY + 168, panelW, "center") + love.graphics.printf(self:_formatSlotStake(), panelX + 24, panelY + 190, panelW - 48, "center") + love.graphics.printf("LEFT / RIGHT: Adjust Gold Stake G: Run Machine ESC: Leave Machine", panelX, panelY + 222, panelW, "center") + end end function DayState:_keypressedBuild(key) @@ -1188,9 +1461,18 @@ end function DayState:_keypressedAction(key) if self:_isInCasino() then - if key == "g" and self:_isNearCasinoTable() then - local _, message = self.casino:gambleInventory(self.inventory, self.depot) - self:_showNotice(message) + if self.casinoInterior.slotUIActive and key == "left" then + self:_changeSlotStake(-1) + elseif self.casinoInterior.slotUIActive and key == "right" then + self:_changeSlotStake(1) + elseif key == "g" and self.casinoInterior.slotUIActive and not self.casinoInterior.slotSpin.active then + self:_startSlotSpin() + elseif key == "g" and self:_isNearSlotMachine() and not self.casinoInterior.slotSpin.active then + if self.casinoInterior.slotUIActive then + self:_startSlotSpin() + else + self:_openSlotMachineUI() + end end return end @@ -1209,6 +1491,11 @@ function DayState:_keypressedAction(key) if self.depot:isNearby(self.player.tx, self.player.ty) then self.depot:depositAll(self.inventory) end + elseif key == "h" then + if self.pawnShop:isNearby(self.player.tx, self.player.ty) then + local _, message = self.pawnShop:sellInventory(self.inventory) + self:_showNotice(message, 2.8) + end elseif key == "g" then if self.casino:isNearby(self.player.tx, self.player.ty) then self:_enterCasino() @@ -1219,9 +1506,17 @@ end function DayState:keypressed(key, scancode, isrepeat) if isrepeat then return end if key == "escape" then - if self:_isInCasino() then self:_exitCasino() - elseif self.ghost:isActive() then self.ghost:deactivate() - else self.game.stateMachine:setState("menu") end + if self:_isInCasino() then + if self.casinoInterior.slotUIActive and not self.casinoInterior.slotSpin.active then + self:_closeSlotMachineUI() + else + self:_exitCasino() + end + elseif self.ghost:isActive() then + self.ghost:deactivate() + else + self.game.stateMachine:setState("menu") + end elseif key == "f3" then self.debugMode = not self.debugMode else @@ -1233,10 +1528,23 @@ end function DayState:gamepadPressed(joystick, button) if self:_isInCasino() then if button == "b" then - self:_exitCasino() - elseif button == "a" and self:_isNearCasinoTable() then - local _, message = self.casino:gambleInventory(self.inventory, self.depot) - self:_showNotice(message) + if self.casinoInterior.slotUIActive and not self.casinoInterior.slotSpin.active then + self:_closeSlotMachineUI() + else + self:_exitCasino() + end + elseif self.casinoInterior.slotUIActive and button == "dpleft" then + self:_changeSlotStake(-1) + elseif self.casinoInterior.slotUIActive and button == "dpright" then + self:_changeSlotStake(1) + elseif button == "a" and self.casinoInterior.slotUIActive and not self.casinoInterior.slotSpin.active then + self:_startSlotSpin() + elseif button == "a" and self:_isNearSlotMachine() and not self.casinoInterior.slotSpin.active then + if self.casinoInterior.slotUIActive then + self:_startSlotSpin() + else + self:_openSlotMachineUI() + end end return end @@ -1269,7 +1577,10 @@ function DayState:gamepadPressed(joystick, button) self.harvest:tryStart(self.player.tx, self.player.ty, self.nodes, self.inventory) end elseif button == "square" or button == "leftshoulder" then - if self.depot:isNearby(self.player.tx, self.player.ty) then + if self.pawnShop:isNearby(self.player.tx, self.player.ty) then + local _, message = self.pawnShop:sellInventory(self.inventory) + self:_showNotice(message, 2.8) + elseif self.depot:isNearby(self.player.tx, self.player.ty) then self.depot:depositAll(self.inventory) elseif self.casino:isNearby(self.player.tx, self.player.ty) then self:_enterCasino() diff --git a/src/ui/hud.lua b/src/ui/hud.lua index 54a387b..b3a88e9 100644 --- a/src/ui/hud.lua +++ b/src/ui/hud.lua @@ -78,11 +78,12 @@ function HUD:_drawInventory(inventory, sh) -- Weight bar local ratio = inventory:fillRatio() + local fillRatio = math.min(1, ratio) local barColor = ratio > 0.85 and {0.95, 0.25, 0.25} or {0.25, 0.85, 0.35} love.graphics.setColor(0.3, 0.3, 0.3, 0.9) love.graphics.rectangle("fill", PAD, panelY, BAR_W, BAR_H) love.graphics.setColor(barColor[1], barColor[2], barColor[3], 1) - love.graphics.rectangle("fill", PAD, panelY, BAR_W * ratio, BAR_H) + love.graphics.rectangle("fill", PAD, panelY, BAR_W * fillRatio, BAR_H) love.graphics.setColor(0.7, 0.7, 0.7, 0.8) love.graphics.rectangle("line", PAD, panelY, BAR_W, BAR_H) @@ -207,26 +208,48 @@ function HUD:_drawControls(input, sh, sw, ghost, context) local panelX = (sw - panelW) * 0.5 local panelY = sh - panelH - 14 - if context and context.inCasino and input and input:isUsingGamepad() then + if context and context.inCasino and context.slotUIActive and input and input:isUsingGamepad() then + rows = { + { + { key = "D-PAD L/R", label = "Stake " .. (context.slotStakeLabel or "100%") }, + { key = "A", label = context.slotSpinActive and "Spinning" or "Run Machine" }, + { key = "B", label = "Leave Machine" }, + }, + { + { key = "GOLD", label = "Adjust exact wager" }, + } + } + elseif context and context.inCasino and context.slotUIActive then + rows = { + { + { key = "LEFT / RIGHT", label = "Stake " .. (context.slotStakeLabel or "100%") }, + { key = "G", label = context.slotSpinActive and "Spinning" or "Run Machine" }, + { key = "ESC", label = "Leave Machine" }, + }, + { + { key = "GOLD", label = "Adjust exact wager" }, + } + } + elseif context and context.inCasino and input and input:isUsingGamepad() then rows = { { { key = "L STICK", label = "Move" }, - { key = "A", label = "Gamble" }, + { key = "A", label = "Use Slots" }, { key = "B", label = "Leave" }, }, { - { key = "TABLE", label = "Use nearby" }, + { key = "SLOT", label = "Approach machine" }, } } elseif context and context.inCasino then rows = { { { key = "WASD / ARROWS", label = "Move" }, - { key = "G", label = "Gamble" }, + { key = "G", label = "Use Slots" }, { key = "ESC", label = "Leave" }, }, { - { key = "TABLE", label = "Use nearby" }, + { key = "SLOT", label = "Approach machine" }, } } elseif input and input:isUsingGamepad() then @@ -239,7 +262,7 @@ function HUD:_drawControls(input, sh, sw, ghost, context) }, { { key = "RB", label = "Build" }, - { key = "LB", label = "Deposit" }, + { key = "LB", label = "Base Action" }, { key = "B", label = "Menu" }, } } @@ -255,6 +278,7 @@ function HUD:_drawControls(input, sh, sw, ghost, context) { key = "E", label = "Harvest" }, { key = "B", label = "Build" }, { key = "F", label = "Deposit" }, + { key = "H", label = "Sell" }, { key = "G", label = "Enter Casino" }, { key = "ESC", label = "Menu" }, } diff --git a/src/world/casino.lua b/src/world/casino.lua index 95d3967..3d9e96c 100644 --- a/src/world/casino.lua +++ b/src/world/casino.lua @@ -5,7 +5,7 @@ local Iso = require("src.rendering.isometric") local Casino = Class:extend() local INTERACT_RADIUS = 3.0 -local WIN_CHANCE = 0.30 +local SLOT_SYMBOLS = { "CHERRY", "BELL", "STAR", "7" } local function getCasinoImage() return AssetManager.getCurrent():getImage("buildings.basecore.house_hay_1") @@ -22,29 +22,63 @@ function Casino:isNearby(ptx, pty) return math.sqrt(dx * dx + dy * dy) <= INTERACT_RADIUS end -function Casino:gambleInventory(inventory, depot) - if not inventory or inventory:isEmpty() then - return false, "Bring resources to gamble." +function Casino:rollSlotReels() + return { + SLOT_SYMBOLS[love.math.random(#SLOT_SYMBOLS)], + SLOT_SYMBOLS[love.math.random(#SLOT_SYMBOLS)], + SLOT_SYMBOLS[love.math.random(#SLOT_SYMBOLS)], + } +end + +function Casino:beginSlotSpin(stakeItems) + if not stakeItems then + return false, "Bring gold to the slot machine." end - local items = inventory:clear() - local won = love.math.random() < WIN_CHANCE local totalIn = 0 - for _, amount in pairs(items) do + for _, amount in pairs(stakeItems) do totalIn = totalIn + amount end + if totalIn <= 0 then + return false, "Choose a gold stake before spinning." + end + + return true, { + items = stakeItems, + totalIn = totalIn, + reels = self:rollSlotReels(), + } +end + +function Casino:resolveSlotSpin(spin, inventory) + local left, middle, right = spin.reels[1], spin.reels[2], spin.reels[3] + local multiplier = 0 + + if left == right and right == middle then + multiplier = left == "7" and 3 or 2 + elseif left == middle or middle == right or left == right then + multiplier = 1 + end - if won then - local totalOut = 0 - for resourceType, amount in pairs(items) do - local doubledAmount = amount * 2 - depot:add(resourceType, doubledAmount) - totalOut = totalOut + doubledAmount + local totalOut = 0 + if multiplier > 0 then + for resourceType, amount in pairs(spin.items) do + local payout = amount * multiplier + inventory:forceAdd(resourceType, payout) + totalOut = totalOut + payout end - return true, string.format("Casino win! %d -> %d resources sent to depot.", totalIn, totalOut) end - return true, string.format("Casino lost. %d resources vanished.", totalIn) + local reels = string.format("[%s | %s | %s]", left, middle, right) + if multiplier == 3 then + return true, string.format("Slots jackpot %s %d gold -> %d gold kept in inventory.", reels, spin.totalIn, totalOut) + elseif multiplier == 2 then + return true, string.format("Slots win %s %d gold -> %d gold kept in inventory.", reels, spin.totalIn, totalOut) + elseif multiplier == 1 then + return true, string.format("Slots push %s %d gold kept in inventory.", reels, totalOut) + end + + return true, string.format("Slots lost %s %d gold gone.", reels, spin.totalIn) end function Casino:draw() @@ -79,6 +113,6 @@ function Casino:drawNearbyHint() end Casino.INTERACT_RADIUS = INTERACT_RADIUS -Casino.WIN_CHANCE = WIN_CHANCE +Casino.SLOT_SYMBOLS = SLOT_SYMBOLS return Casino diff --git a/src/world/pawnshop.lua b/src/world/pawnshop.lua new file mode 100644 index 0000000..dc88773 --- /dev/null +++ b/src/world/pawnshop.lua @@ -0,0 +1,86 @@ +local Class = require("lib.class") +local AssetManager = require("src.core.assetmanager") +local Iso = require("src.rendering.isometric") +local Resources = require("data.resources") + +local PawnShop = Class:extend() + +local INTERACT_RADIUS = 3.0 + +local function getPawnShopImage() + return AssetManager.getCurrent():getImage("props.supply_depot") +end + +function PawnShop:new(tx, ty) + self.tx = tx or 0 + self.ty = ty or 0 +end + +function PawnShop:isNearby(ptx, pty) + local dx = ptx - self.tx + local dy = pty - self.ty + return math.sqrt(dx * dx + dy * dy) <= INTERACT_RADIUS +end + +function PawnShop:sellInventory(inventory) + if not inventory then + return false, "Bring goods to the pawn shop." + end + + local items = inventory:getItems() + local goldEarned = 0 + local soldAnything = false + + for resourceType, amount in pairs(items) do + if resourceType ~= "gold" and amount > 0 then + local def = Resources[resourceType] + local goldValue = def and def.goldValue or 0 + if goldValue > 0 then + inventory:remove(resourceType, amount) + goldEarned = goldEarned + amount * goldValue + soldAnything = true + end + end + end + + if not soldAnything then + return false, "Bring resources to sell for gold." + end + + inventory:forceAdd("gold", goldEarned) + return true, string.format("Pawn shop deal: +%d gold.", goldEarned) +end + +function PawnShop:draw() + local sx, sy = Iso.tileToScreen(self.tx, self.ty) + local image = getPawnShopImage() + + love.graphics.setColor(0.28, 0.22, 0.08, 0.22) + love.graphics.polygon("fill", sx, sy, sx + 32, sy + 16, sx, sy + 32, sx - 32, sy + 16) + Iso.drawProp(image, self.tx, self.ty, { + scale = 1.0, + anchorY = 22, + oy = 4, + r = 1.0, + g = 0.88, + b = 0.54, + a = 1.0, + }) + love.graphics.setColor(0.56, 0.40, 0.10, 0.95) + love.graphics.polygon("line", sx, sy, sx + 32, sy + 16, sx, sy + 32, sx - 32, sy + 16) +end + +function PawnShop:drawNearbyHint() + local sx, sy = Iso.tileToScreen(self.tx, self.ty) + local alpha = 0.5 + 0.5 * math.sin(love.timer.getTime() * 3) + + love.graphics.setColor(0.98, 0.84, 0.22, alpha) + love.graphics.circle("line", sx, sy + 16, 36) + love.graphics.setFont(love.graphics.newFont(11)) + love.graphics.setColor(1, 0.98, 0.90, alpha) + love.graphics.print("H: Sell for Gold", sx - 38, sy + 36) +end + +PawnShop.INTERACT_RADIUS = INTERACT_RADIUS + +return PawnShop diff --git a/src/world/tilemanager.lua b/src/world/tilemanager.lua index b846936..2adefef 100644 --- a/src/world/tilemanager.lua +++ b/src/world/tilemanager.lua @@ -82,6 +82,19 @@ function TileManager:getResourceType(tx, ty) return data and data.resourceType or nil end +function TileManager:isWithinActiveTilemap(tx, ty) + if self.source.mode ~= "tilemap" or not self.tilemapState then + return true + end + + local def = self.tilemapState.definition + local layer = self.tilemapState.layer + local localX = tx - (def.originX or 0) + local localY = ty - (def.originY or 0) + + return localX >= 0 and localY >= 0 and localX < layer.width and localY < layer.height +end + function TileManager:_getProceduralTileData(tx, ty) local tileType = WorldGen.getTileAt(tx, ty) return {