-- Cat Haven - UI System -- HUD, menus, cat logs, inventory local ui = {} local UIState = { showInventory = false, showCatLog = true, hoveredItem = nil, selectedFurniture = nil, inventoryPage = 1 } -- Cached module references local core = nil local voxel = nil -- HUD rendering function ui.renderHUD(pass) if not pass then return end if not core then core = require('src.core') end if not voxel then voxel = require('src.voxel.init') end local windowWidth, windowHeight = lovr.system.getWindowDimensions() if not windowWidth then windowWidth, windowHeight = 1280, 720 end -- Calculate scale based on window size local scale = math.min(windowWidth, windowHeight) / 1000 -- Happiness meter local happiness = core.getHappinessPercentage and core.getHappinessPercentage() or 0 local barWidth = 300 * scale local barHeight = 20 * scale local x = windowWidth / 2 - barWidth / 2 local y = 50 * scale -- Background pass:box('fill', x - 5 * scale, y - 5 * scale, 0, barWidth + 10 * scale, barHeight + 10 * scale, 0.01, { 0, 0, 0, 0.5 }) -- Bar background pass:box('fill', x, y, 0, barWidth, barHeight, 0.01, { 0.3, 0.3, 0.3 }) -- Happiness fill local fillWidth = barWidth * (happiness / 100) local color = { 0.5, 0.8, 0.5 } if happiness < 50 then color = { 0.8, 0.6, 0.5 } elseif happiness < 80 then color = { 0.8, 0.8, 0.5 } end pass:box('fill', x, y, 0, fillWidth, barHeight, 0.01, color) -- Text pass:text(string.format('Perfect Cat Happiness: %d%%', happiness), x + barWidth / 2, y + barHeight, 0, 0.02 * scale, { 1, 1, 1 }, 'center') -- Cat count local catCount = core.state and #core.state.cats or 0 local maxCats = core.CONFIG and core.CONFIG.maxCats or 8 pass:text(string.format('Cats: %d/%d', catCount, maxCats), windowWidth - 200 * scale, 50 * scale, 0, 0.02 * scale) -- Game time local gameTime = core.state and core.state.gameTime or 0 local hours = math.floor(gameTime / 3600) local minutes = math.floor((gameTime % 3600) / 60) pass:text(string.format('Time: %02d:%02d', hours, minutes), windowWidth - 200 * scale, 80 * scale, 0, 0.02 * scale) -- Controls hint pass:text('RThumb: Save/Load | ESC: Pause', 50 * scale, windowHeight - 50 * scale, 0, 0.02 * scale) -- Inventory (if open) if UIState.showInventory then ui.renderInventory(pass, windowWidth, windowHeight, scale) end -- Cat log (if open) if UIState.showCatLog then ui.renderCatLog(pass, windowWidth, scale) end -- Placement preview if core.state and core.state.placementMode and UIState.selectedFurniture then ui.renderPlacementPreview(pass, windowWidth, windowHeight) end end function ui.renderInventory(pass, windowWidth, windowHeight, scale) if not core then core = require('src.core') end local x = windowWidth / 2 - 300 * scale local y = windowHeight / 2 - 200 * scale local width = 600 * scale local height = 400 * scale -- Background pass:box('fill', x, y, 0, width, height, 0.01, { 0, 0, 0, 0.8 }) -- Title pass:text('Inventory', x + width / 2, y + 30 * scale, 0, 0.03 * scale, { 1, 1, 1 }, 'center') -- Furniture items local itemHeight = 50 * scale local startX = x + 20 * scale local startY = y + 80 * scale if core.furnitureTypes then for i, furn in ipairs(core.furnitureTypes) do local itemY = startY + (i - 1) * itemHeight -- Item box pass:box('fill', startX, itemY, 0, width - 40 * scale, itemHeight - 10 * scale, 0.01, { 0.2, 0.2, 0.2 }) if UIState.selectedFurniture == furn.id then pass:box('line', startX, itemY, 0, width - 40 * scale, itemHeight - 10 * scale, 0.01, { 1, 1, 0 }, 0.02) end -- Icon pass:box('fill', startX + 10 * scale, itemY + 10 * scale, 0, 30 * scale, 30 * scale, 0.01, furn.color or { 1, 1, 1 }) -- Text local color = { 1, 1, 1 } if UIState.selectedFurniture == furn.id then color = { 1, 1, 0 } end pass:text(furn.name, startX + 50 * scale, itemY + 20 * scale, 0, 0.02 * scale, color) local happinessBonus = math.floor((furn.baseHappiness or 0.5) * 100) pass:text(string.format('Happiness: +%d%%', happinessBonus), startX + 250 * scale, itemY + 20 * scale, 0, 0.015 * scale) end end -- Close button local closeY = y + height - 40 * scale pass:box('fill', x + width - 100 * scale, closeY, 0, 80 * scale, 30 * scale, 0.01, { 0.8, 0.3, 0.3 }) pass:text('Close', x + width - 60 * scale, closeY + 15 * scale, 0, 0.02 * scale) end function ui.renderCatLog(pass, windowWidth, scale) if not core then core = require('src.core') end local x = windowWidth - 300 * scale local y = 150 * scale local width = 280 * scale local height = 300 * scale -- Background pass:box('fill', x, y, 0, width, height, 0.01, { 0, 0, 0, 0.6 }) -- Title pass:text('Cat Log', x + width / 2, y + 25 * scale, 0, 0.02 * scale, { 1, 1, 1 }, 'center') -- Log entries local entries = core.state and core.state.catLog or {} local maxEntries = 8 local startY = y + 60 * scale local lineHeight = 30 * scale for i = math.max(1, #entries - maxEntries + 1), #entries do local entry = entries[i] local entryY = startY + (i - math.max(1, #entries - maxEntries + 1)) * lineHeight local color = { 1, 1, 1 } if entry and entry.type == 'arrival' then color = { 0.5, 1, 0.5 } elseif entry and entry.type == 'departure' then color = { 1, 0.5, 0.5 } end if entry then local label = entry.type == 'arrival' and 'Arrived' or 'Left' pass:text(string.format('%s: %s', label, entry.catName or 'Unknown'), x + 10 * scale, entryY, 0, 0.015 * scale, color) end end end function ui.renderPlacementPreview(pass, windowWidth, windowHeight) if not core then core = require('src.core') end if not voxel then voxel = require('src.voxel.init') end -- Get mouse position in 3D local rayStart, rayEnd = ui.getWorldRay(windowWidth, windowHeight) local direction = { rayEnd[1] - rayStart[1], rayEnd[2] - rayStart[2], rayEnd[3] - rayStart[3] } local length = math.sqrt(direction[1]^2 + direction[2]^2 + direction[3]^2) if length > 0.001 then direction = { direction[1]/length, direction[2]/length, direction[3]/length } else direction = { 0, 0, -1 } end -- Raycast to floor local results = voxel.raycast and voxel.raycast(rayStart, direction, 10) or {} if #results > 0 then local hit = results[1] local pos = hit.position -- Get furniture size local furnType = nil if core.furnitureTypes then for _, t in ipairs(core.furnitureTypes) do if t.id == UIState.selectedFurniture then furnType = t break end end end if furnType and furnType.size then local size = furnType.size local half = { size[1]/2, size[2]/2, size[3]/2 } pass:box('line', pos.x, pos.y, pos.z, size[1], size[2], size[3], { 1, 1, 0, 0.5 }, 0.1) end end end function ui.getWorldRay(windowWidth, windowHeight) if not core then core = require('src.core') end local camera = core.state and core.state.camera if not camera then camera = { position = { 0, 2, 0 }, orientation = { 0, 0, 0, 1 }, fov = 90 } end local mouseX, mouseY = lovr.system.getMousePosition() if not mouseX then mouseX = windowWidth / 2 mouseY = windowHeight / 2 end -- Simple perspective projection local fov = math.rad(camera.fov or 90) local aspect = windowWidth / windowHeight local x = (mouseX / windowWidth - 0.5) * aspect * 2 * math.tan(fov / 2) local y = -(mouseY / windowHeight - 0.5) * 2 * math.tan(fov / 2) local z = -1 -- Normalize direction local len = math.sqrt(x*x + y*y + z*z) if len > 0.001 then x, y, z = x/len, y/len, z/len end -- Apply camera rotation (simplified - just use position) local dir = { x, y, z } local rayStart = { camera.position[1] or 0, camera.position[2] or 2, camera.position[3] or 0 } local rayEnd = { rayStart[1] + dir[1]*10, rayStart[2] + dir[2]*10, rayStart[3] + dir[3]*10 } return rayStart, rayEnd end function ui.handleMousePress(x, y, button) local windowWidth, windowHeight = lovr.system.getWindowDimensions() if not windowWidth then windowWidth, windowHeight = 1280, 720 end -- Check inventory close button if UIState.showInventory then local scale = math.min(windowWidth, windowHeight) / 1000 local closeX = windowWidth / 2 + 300 * scale - 100 * scale local closeY = windowHeight / 2 + 200 * scale - 40 * scale local closeWidth = 80 * scale local closeHeight = 30 * scale if x >= closeX and x <= closeX + closeWidth and y >= closeY and y <= closeY + closeHeight then UIState.showInventory = false if core and core.state then core.state.placementMode = false end return end end -- Check furniture selection in inventory if UIState.showInventory then local scale = math.min(windowWidth, windowHeight) / 1000 local startX = windowWidth / 2 - 300 * scale + 20 * scale local startY = windowHeight / 2 - 200 * scale + 80 * scale local itemHeight = 50 * scale if core and core.furnitureTypes then for i, furn in ipairs(core.furnitureTypes) do local itemY = startY + (i - 1) * itemHeight local width = 600 * scale - 40 * scale local height = itemHeight - 10 * scale if x >= startX and x <= startX + width and y >= itemY and y <= itemY + height then if UIState.selectedFurniture == furn.id then UIState.selectedFurniture = nil if core and core.state then core.state.placementMode = false end else UIState.selectedFurniture = furn.id if core and core.state then core.state.placementMode = true end end return end end end end end function ui.updateHUD() -- Update any dynamic UI elements end function ui.toggleInventory() UIState.showInventory = not UIState.showInventory end function ui.toggleCatLog() UIState.showCatLog = not UIState.showCatLog end return ui