140 lines
5.8 KiB
Lua
Executable File
140 lines
5.8 KiB
Lua
Executable File
-- Bounded room flood-fill. Revisions invalidate cached geometry on building edits.
|
|
local C, M = core, hearth.model
|
|
local ROOM_RADIUS, ROOM_BELOW, ROOM_ABOVE = 3, 1, 3
|
|
local MAX_CELLS = (ROOM_RADIUS * 2 + 1) ^ 2 * (ROOM_BELOW + ROOM_ABOVE + 1)
|
|
local CACHE_SECONDS, CACHE_LIMIT = 1, 256
|
|
local cache, cache_count, revision = {}, 0, 0
|
|
|
|
local function key(pos)
|
|
return math.floor(pos.x + 0.5) .. ":" .. math.floor(pos.y + 0.5) .. ":" .. math.floor(pos.z + 0.5)
|
|
end
|
|
|
|
local function read(pos)
|
|
local node = C.get_node_or_nil(pos)
|
|
if not node or node.name == "ignore" then return nil, nil, true end
|
|
local def = C.registered_nodes[node.name]
|
|
if not def then return node, nil, false end
|
|
return node, def, false
|
|
end
|
|
|
|
local function room_geometry(pos)
|
|
local head = vector.round({x = pos.x, y = pos.y + 1, z = pos.z})
|
|
local cache_key = key(head)
|
|
local now = C.get_us_time() / 1000000
|
|
local old = cache[cache_key]
|
|
if old and old.revision == revision and now - old.at < CACHE_SECONDS then
|
|
return old.geometry
|
|
end
|
|
|
|
local minx, maxx = head.x - ROOM_RADIUS, head.x + ROOM_RADIUS
|
|
local minz, maxz = head.z - ROOM_RADIUS, head.z + ROOM_RADIUS
|
|
local miny, maxy = head.y - ROOM_BELOW, head.y + ROOM_ABOVE
|
|
local first_node, first_def, unloaded = read(head)
|
|
local geometry = {roof = false, shelter = 0, enclosed = false, ventilated = false,
|
|
open_sides = 4, unloaded = unloaded}
|
|
local function blocks_room(node, def)
|
|
if not def then return true end -- Unknown nodes fail closed as room boundaries.
|
|
if node.name == "hearth:door_open" then return false end
|
|
return def.walkable ~= false
|
|
end
|
|
local function has_vent_near(p)
|
|
for _, d in ipairs({{1,0,0},{-1,0,0},{0,1,0},{0,-1,0},{0,0,1},{0,0,-1}}) do
|
|
local node, def, missing = read({x = p.x + d[1], y = p.y + d[2], z = p.z + d[3]})
|
|
unloaded = unloaded or missing
|
|
if node and def and def.groups and (def.groups.hearth_vent or 0) > 0 then return true end
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- Flood only passable nodes inside a fixed 7 x 7 x 5 volume. Reaching a
|
|
-- horizontal boundary means that side admits weather; an upward/downward
|
|
-- escape means the room lacks a ceiling/floor. This never searches map-scale.
|
|
local queue, seen, index = {}, {}, 1
|
|
if first_node and first_def and not blocks_room(first_node, first_def) then
|
|
queue[1] = head
|
|
else
|
|
geometry.unloaded = unloaded
|
|
local roof = false
|
|
for y = 1, 5 do
|
|
local _, def, missing = read({x = head.x, y = head.y + y, z = head.z})
|
|
unloaded = unloaded or missing
|
|
if def and def.walkable ~= false then roof = true; break end
|
|
end
|
|
geometry.roof, geometry.unloaded = roof, unloaded
|
|
return geometry
|
|
end
|
|
|
|
local open_sides, ventilated = {}, false
|
|
local directions = {{1,0,0},{-1,0,0},{0,1,0},{0,-1,0},{0,0,1},{0,0,-1}}
|
|
while index <= #queue and index <= MAX_CELLS do
|
|
local p = queue[index]; index = index + 1
|
|
local cell_key = key(p)
|
|
if not seen[cell_key] then
|
|
seen[cell_key] = true
|
|
if p.x == minx then open_sides.west = true end
|
|
if p.x == maxx then open_sides.east = true end
|
|
if p.z == minz then open_sides.north = true end
|
|
if p.z == maxz then open_sides.south = true end
|
|
if p.y == maxy then open_sides.sky = true end
|
|
if p.y == miny then open_sides.floor = true end
|
|
ventilated = ventilated or has_vent_near(p)
|
|
for _, d in ipairs(directions) do
|
|
local npos = {x = p.x + d[1], y = p.y + d[2], z = p.z + d[3]}
|
|
if npos.x >= minx and npos.x <= maxx and npos.z >= minz and npos.z <= maxz
|
|
and npos.y >= miny and npos.y <= maxy then
|
|
local nkey = key(npos)
|
|
if not seen[nkey] then
|
|
local node, def, missing = read(npos)
|
|
unloaded = unloaded or missing
|
|
if node and not blocks_room(node, def) then queue[#queue + 1] = npos end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
if index > MAX_CELLS then open_sides.sky = true end
|
|
local horizontal_open = (open_sides.west and 1 or 0) + (open_sides.east and 1 or 0)
|
|
+ (open_sides.north and 1 or 0) + (open_sides.south and 1 or 0)
|
|
local roof = not open_sides.sky
|
|
local enclosed = roof and not open_sides.floor and horizontal_open == 0
|
|
local roof_def
|
|
for y = 1, 5 do
|
|
local _, def, missing = read({x = head.x, y = head.y + y, z = head.z})
|
|
unloaded = unloaded or missing
|
|
if def and def.walkable ~= false then roof_def = true; break end
|
|
end
|
|
roof = roof_def == true
|
|
local closed_sides = 4 - horizontal_open
|
|
local shelter = enclosed and roof and 1 or (roof and (0.25 + closed_sides * 0.1875) or 0)
|
|
if unloaded then enclosed, shelter, ventilated = false, 0, true end
|
|
geometry = {roof = roof, shelter = math.max(0, math.min(1, shelter)), enclosed = enclosed,
|
|
ventilated = not enclosed or ventilated, open_sides = horizontal_open, unloaded = unloaded}
|
|
if cache_count >= CACHE_LIMIT then cache, cache_count = {}, 0 end
|
|
cache[cache_key] = {revision = revision, at = now, geometry = geometry}
|
|
cache_count = cache_count + 1
|
|
return geometry
|
|
end
|
|
|
|
function hearth.invalidate_environment()
|
|
revision = revision + 1
|
|
cache, cache_count = {}, 0
|
|
end
|
|
|
|
function hearth.environment(pos)
|
|
local room = room_geometry(pos)
|
|
local fire = C.find_node_near(pos, 4, {"hearth:fire_lit"})
|
|
local heat = 0
|
|
if fire then
|
|
local deadline = tonumber(C.get_meta(fire):get_string("fuel_until")) or 0
|
|
if M.fuel_remaining(deadline, os.time()) > 0 then
|
|
local head = vector.round({x = pos.x, y = pos.y + 1, z = pos.z})
|
|
local from = vector.offset(fire, 0, 0.6, 0)
|
|
if C.line_of_sight(from, head, 0.5) then heat = 1 - math.min(0.8, vector.distance(pos, fire) / 5) end
|
|
end
|
|
end
|
|
return {roof = room.roof, shelter = room.shelter, enclosed = room.enclosed,
|
|
ventilated = room.ventilated, open_sides = room.open_sides, heat = heat,
|
|
smoke = heat > 0 and room.enclosed and not room.ventilated,
|
|
rain = hearth.weather == "rain", temperature = hearth.clock.temperature - math.max(0, pos.y) * 0.0065}
|
|
end
|