Files
cat-haven/src/ui/init.lua
T
2026-07-23 21:23:01 +00:00

306 lines
10 KiB
Lua

-- 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