sirnii's picture
Upload 1816 files
b6a38d7 verified
NSEW_pairs = sorted_pairs -- todo: make an iterator that explicitly visits them in a predefined order
-- EnumVolumes([class, ] { box, | obj, | point, | x, y | x, y, z, }) - returns an array of the volumes interesecting the box/point
-- EnumVolumes(..., "smallest") - returns the smallest volume (by surface) interesecting the box/point
-- EnumVolumes(..., filter, ...) - returns an array with all volumes interesecting the box/point for which filter(volume, ...) returned true
local gofPermanent = const.gofPermanent
if FirstLoad then
GedRoomEditor = false
GedRoomEditorObjList = false
g_RoomCornerTaskList = {}
SelectedVolume = false
VolumeCollisonEnabled = false
HideFloorsAboveThisOne = false
--moves wall slabs on load to their expected pos
--it seems that lvl designers commonly misplace wall slabs which causes weird glitches, this should fix that
RepositionWallSlabsOnLoad = Platform.developer or false
end
function VolumeStructuresList()
local list = {""}
EnumVolumes(function (volume, list, find)
if not find(list, volume.structure) then
list[#list + 1] = volume.structure
end
end, list, table.find)
table.sort(list)
return list
end
-- the map is considered in bottom-right quadrant, which means that (0, 0) is north, west
local noneWallMat = const.SlabNoMaterial
local defaultWallMat = "default"
DefineClass.Volume = {
__parents = { "RoomRoof", "StripObjectProperties", "AlignedObj", "ComponentAttach", "EditorVisibleObject" },
flags = { gofPermanent = true, cofComponentVolume = true, efVisible = true },
properties = {
{ category = "Not Room Specific", id = "volumeCollisionEnabled", name = "Toggle Global Volume Collision", default = true, editor = "bool", dont_save = true },
{ category = "Not Room Specific", id = "buttons3", name = "Buttons", editor = "buttons", default = false, dont_save = true, read_only = true,
buttons = {
{name = "Recreate All Walls", func = "RecreateAllWallsOnMap"},
{name = "Recreate All Roofs", func = "RecreateAllRoofsOnMap"},
{name = "Recreate All Floors", func = "RecreateAllFloorsOnMap"},
},
},
{ category = "General", id = "buttons2", name = "Buttons", editor = "buttons", default = false, dont_save = true, read_only = true,
buttons = {
{name = "Recreate Walls", func = "RecreateWalls"},
{name = "Recreate Floor", func = "RecreateFloor"},
{name = "Recreate Roof", func = "RecreateRoofBtn"},
{name = "Re Randomize", func = "ReRandomize"},
{name = "Copy Above", func = "CopyAbove"},
{name = "Copy Below", func = "CopyBelow"},
},
},
{ category = "General", id = "buttons2row2", name = "Buttons", editor = "buttons", default = false, dont_save = true, read_only = true,
buttons = {
{name = "Lock Subvariants", func = "LockAllSlabsToCurrentSubvariants"},
{name = "Unlock Subvariants", func = "UnlockAllSlabs"},
{name = "Make Slabs Vulnerable", func = "MakeOwnedSlabsVulnerable"},
{name = "Make Slabs Invulnerable", func = "MakeOwnedSlabsInvulnerable"},
},
},
{ category = "General", id = "box", name = "Box", editor = "box", default = false, no_edit = true },
{ category = "General", id = "locked_slabs_count", name = "Locked Slabs Count", editor = "text", default = "", read_only = true, dont_save = true},
{ category = "General", id = "wireframe_visible", name = "Wireframe Visible", editor = "bool", default = false,},
{ category = "General", id = "wall_text_markers_visible", name = "Wall Text ID Visible", editor = "bool", default = false, dont_save = true},
{ category = "General", id = "dont_use_interior_lighting", name = "No Interior Lighting", editor = "bool", default = false, },
{ category = "General", id = "seed", name = "Random Seed", editor = "number", default = false},
{ category = "General", id = "floor", name = "Floor", editor = "number", default = 1, min = -9, max = 99 },
--bottom left corner of room in world coords
{ category = "General", id = "position", name = "Position", editor = "point", default = false, no_edit = true, },
{ category = "General", id = "size", name = "Size", editor = "point", default = point30, no_edit = true, },
{ category = "General", id = "override_terrain_z", editor = "number", default = false, no_edit = true },
{ category = "General", id = "structure", name = "Structure", editor = "combo", default = "", items = VolumeStructuresList },
{ category = "Roof", id = "room_is_roof", name = "Room is Roof", editor = "bool", default = false, help = "Mark room as roof, roofs are hidden entirely (all walls, floors, etc.) when their floor is touched. Rooms that have zero height are considered roofs by default."},
},
wireframeColor = RGB(100, 100, 100),
lines = false,
adjacent_rooms = false, --{ ? }
text_markers = false,
last_wall_recreate_seed = false,
building = false, --{ [floor] = {room, room, room} }
being_placed = false,
enable_collision = true, --with other rooms
EditorView = Untranslated("<opt(u(structure),'',' - ')><u(name)>"),
light_vol_obj = false,
entity = "InvisibleObject",
editor_force_excluded = true, -- exclude from Map editor palette (causes crash)
}
local voxelSizeX = const.SlabSizeX or 0
local voxelSizeY = const.SlabSizeY or 0
local voxelSizeZ = const.SlabSizeZ or 0
local halfVoxelSizeX = voxelSizeX / 2
local halfVoxelSizeY = voxelSizeY / 2
local halfVoxelSizeZ = voxelSizeZ / 2
local InvalidZ = const.InvalidZ
maxRoomVoxelSizeX = const.MaxRoomVoxelSizeX or 40
maxRoomVoxelSizeY = const.MaxRoomVoxelSizeY or 40
maxRoomVoxelSizeZ = const.MaxRoomVoxelSizeZ or 40
roomQueryRadius = Max((maxRoomVoxelSizeX + 1) * voxelSizeX, (maxRoomVoxelSizeY + 1) * voxelSizeY, (maxRoomVoxelSizeZ + 1) * voxelSizeZ)
defaultVolumeVoxelHeight = 4
local halfVoxelPtZeroZ = point(halfVoxelSizeX, halfVoxelSizeY, 0)
local halfVoxelPt = point(halfVoxelSizeX, halfVoxelSizeY, halfVoxelSizeZ)
function SnapVolumePos(pos)
return SnapToVoxel(pos) - halfVoxelPtZeroZ
end
function snapZRound(z)
z = (z + halfVoxelSizeZ) / voxelSizeZ
return z * voxelSizeZ
end
function snapZCeil(z)
z = DivCeil(z, voxelSizeZ)
return z * voxelSizeZ
end
function snapZ(z)
z = z / voxelSizeZ
return z * voxelSizeZ
end
function Volume:Getlocked_slabs_count()
return "N/A"
end
function Volume:GetvolumeCollisionEnabled()
return VolumeCollisonEnabled
end
function Volume:Setroom_is_roof(val)
if self.room_is_roof == val then return end
self.room_is_roof = val
ComputeSlabVisibilityInBox(self.box)
end
function Volume:IsRoofOnly()
return self.room_is_roof or self.size:z() == 0
end
function Volume:SetvolumeCollisionEnabled(v)
VolumeCollisonEnabled = v
end
function Volume:Init()
--todo: if platform.somethingorother,
self.text_markers = {
North = PlaceObject("Text"),
South = PlaceObject("Text"),
West = PlaceObject("Text"),
East = PlaceObject("Text"),
}
for k, v in pairs(self.text_markers) do
v.hide_in_editor = false
v:SetText(k)
v:SetColor(RGB(255, 0, 0))
v:SetGameFlags(const.gofDetailClass1)
v:ClearGameFlags(const.gofDetailClass0)
end
self:SetPosMarkersVisible(self.wall_text_markers_visible)
self:CopyBoxToCCD()
self:InitEntity()
end
function Volume:InitEntity()
if IsEditorActive() then
self:ChangeEntity("RoomHelper")
end
end
function Volume:EditorEnter()
--dont mess with visibility flags
self:ChangeEntity("RoomHelper")
end
function Volume:EditorExit()
--dont mess with visibility flags
self:ChangeEntity("InvisibleObject")
end
function Volume:Setdont_use_interior_lighting(val)
self.dont_use_interior_lighting = val
self:UpdateInteriorLighting()
end
function Volume:CopyBoxToCCD()
local box = self.box
if not box then return end
SetVolumeBox(self, box)
self:UpdateInteriorLighting()
end
function Volume:UpdateInteriorLighting()
local box = self.box
if not box then
return
end
if self.dont_use_interior_lighting then
DoneObject(self.light_vol_obj)
self.light_vol_obj = nil
return
end
local lo = self.light_vol_obj
if not IsValid(lo) then
lo = PlaceObject("ComponentLight")
lo:SetLightType(const.eLightTypeClusterVolume)
self.light_vol_obj = lo
end
if not self.dont_use_interior_lighting and self.floor_mat == noneWallMat and self.floor == 1 then
lo:SetClusterVolumeBox(box:minx(), box:miny(), box:minz() - 100, box:maxx(), box:maxy(), box:maxz()) --this is here so vfx can properly catch ground below rooms
else
lo:SetClusterVolumeBox(box:minx(), box:miny(), box:minz(), box:maxx(), box:maxy(), box:maxz())
end
lo:SetVolumeId(self.handle)
lo:SetPos(self:GetPos())
end
function Volume:CalcZ()
local posZ = self.position:z()
if self.being_placed then
local z = self.override_terrain_z or terrain.GetHeight(self.position)
posZ = snapZ(z + voxelSizeZ / 2)
self.position = self.position:SetZ(posZ)
end
if posZ == nil then
--save compat
local z = self.override_terrain_z or terrain.GetHeight(self.position)
z = snapZ(z + voxelSizeZ / 2)
posZ = (rawget(self, "z_offset") or 0) * voxelSizeZ + z + (self.floor - 1) * self.size:z() * voxelSizeZ
self.position = self.position:SetZ(posZ)
end
return posZ
end
function Volume:LockToCurrentTerrainZ()
self.override_terrain_z = terrain.GetHeight(self.position)
return self.override_terrain_z
end
function Volume:CalcSnappedZ()
local z = self:CalcZ()
z = z / voxelSizeZ
z = z * voxelSizeZ
return z
end
function FloorFromZ(z, roomHeight, ground_level)
return (z - ground_level) / (roomHeight * voxelSizeZ) + 1
end
function ZFromFloor(f, roomHeight, ground_level) --essentialy calcz but makes assumptions about floor size
return ground_level + (f - 1) * (roomHeight * voxelSizeZ)
end
function Volume:Move(pos)
--bottom left corner of the voxel at pos will be the new positon
self.position = SnapVolumePos(pos)
self:AlignObj()
end
function Volume:ChangeFloor(newFloor)
if self.floor == newFloor then
return
end
self.floor = newFloor
self:AlignObj()
end
function Volume:SetSize(newSize)
if self.size == newSize then
return
end
self.size = newSize
self:AlignObj()
end
function Volume:AlignObj(pos, angle)
if pos then
local v = pos - self:GetPos()
self.position = SnapVolumePos(self.position + v)
end
self:InternalAlignObj()
end
function Volume:InternalAlignObj(test)
local w, h, d = self.size:x() * voxelSizeX, self.size:y() * voxelSizeY, self.size:z() * voxelSizeZ
local cx, cy = w / 2, h / 2
local z = self:CalcZ()
local pos = point(self.position:x() + cx, self.position:y() + cy, z)
local p = self.position
local newBox = box(p:x(), p:y(), z, p:x() + w, p:y() + h, z + d)
if not test and self:GetPos() == pos and self.box == newBox then return p end --nothing to align
self:SetPos(pos)
self:SetAngle(0)
self.box = newBox
if not test then
self:FinishAlign()
end
return p
end
function GetOppositeSide(side)
if side == "North" then return "South"
elseif side == "South" then return "North"
elseif side == "West" then return "East"
elseif side == "East" then return "West"
end
end
local function GetOppositeCorner(c)
if c == "NW" then return "SE"
elseif c == "NE" then return "SW"
elseif c == "SW" then return "NE"
elseif c == "SE" then return "NW"
end
end
local function SetAdjacentRoom(adjacent_rooms, room, data)
if not adjacent_rooms then
return
end
if data then
if not adjacent_rooms[room] then
adjacent_rooms[#adjacent_rooms + 1] = room
end
adjacent_rooms[room] = data
return data
end
data = adjacent_rooms[room]
if data then
adjacent_rooms[room] = nil
table.remove_value(adjacent_rooms, room)
return data
end
end
function Volume:ClearAdjacencyData()
local adjacent_rooms = self.adjacent_rooms
self.adjacent_rooms = nil
for _, room in ipairs(adjacent_rooms or empty_table) do
local hisData = SetAdjacentRoom(room.adjacent_rooms, self, false)
if hisData then
local hisAW = hisData[2]
for i = 1, #(hisAW or empty_table) do
room:OnAdjacencyChanged(hisAW[i])
end
end
end
end
local AdjacencyEvents = {}
function Volume:RebuildAdjacencyData()
-- must be called inside an undo op, otherwise delayed updating may cause changes not captured by undo
assert(XEditorUndo:AssertOpCapture())
local adjacent_rooms = self.adjacent_rooms
local new_adjacent_rooms = {}
local mb = self.box
local events = {}
--DbgAddBox(mb)
local is_permanent = self:GetGameFlags(gofPermanent) ~= 0
local gameFlags = is_permanent and gofPermanent or nil
MapForEach(self, roomQueryRadius, self.class, nil, nil, gameFlags, function(o, mb, is_permanent)
if o == self or not is_permanent and o:GetGameFlags(gofPermanent) ~= 0 then
return
end
--TODO: use +roof box for sides, -roof box for ceiling/floor
local hb = o.box
--DbgAddBox(hb, RGB(0, 255, 0))
local ib = IntersectRects(hb, mb)
--DbgAddBox(ib, RGB(255, 0, 0))
if not ib:IsValid() then
return
end
local myData = adjacent_rooms and adjacent_rooms[o]
local oldIb = myData and myData[1]
local myNewData = {}
local hisData = o.adjacent_rooms and o.adjacent_rooms[self]
local hisNewData = {}
--restore previously affected walls
local myaw = myData and myData[2]
local hisaw = hisData and hisData[2]
for i = 1, #(myaw or empty_table) do
table.insert(events, {self, myaw[i]})
end
for i = 1, #(hisaw or empty_table) do
table.insert(events, {o, hisaw[i]})
end
hisNewData[1] = ib
myNewData[1] = ib
hisNewData[2] = {}
myNewData[2] = {}
if ib:sizez() > 0 then
if ib:minx() == ib:maxx() and ib:miny() == ib:maxy() then
--corner adj, rebuild corner
local p = ib:min()
if p:x() == mb:minx() then --west
if p:y() == mb:miny() then --north
table.insert(events, {self, "NW"})
table.insert(hisNewData[2], "SE")
table.insert(myNewData[2], "NW")
else
table.insert(events, {self, "SW"})
table.insert(hisNewData[2], "NE")
table.insert(myNewData[2], "SW")
end
else --east
if p:y() == mb:miny() then
table.insert(events, {self, "NE"})
table.insert(hisNewData[2], "SW")
table.insert(myNewData[2], "NE")
else
table.insert(events, {self, "SE"})
table.insert(hisNewData[2], "NW")
table.insert(myNewData[2], "SE")
end
end
elseif ib:minx() == ib:maxx()
and ib:miny() ~= ib:maxy() then
--east/west adjacency
if mb:maxx() == ib:maxx() then
--my east, his west
table.insert(events, {self, "East"})
table.insert(events, {o, "West"})
table.insert(hisNewData[2], "West")
table.insert(myNewData[2], "East")
else
--my west, his east
table.insert(events, {self, "West"})
table.insert(events, {o, "East"})
table.insert(hisNewData[2], "East")
table.insert(myNewData[2], "West")
end
elseif ib:minx() ~= ib:maxx()
and ib:miny() == ib:maxy() then
--nort/south adjacency
if mb:maxy() == ib:maxy() then
--my north, his south
table.insert(events, {self, "South"})
table.insert(events, {o, "North"})
table.insert(hisNewData[2], "North")
table.insert(myNewData[2], "South")
else
--my south, his north
table.insert(events, {self, "North"})
table.insert(events, {o, "South"})
table.insert(hisNewData[2], "South")
table.insert(myNewData[2], "North")
end
else
--rooms intersect
if (ib:maxx() == mb:maxx() or ib:minx() == mb:maxx())
and mb:maxx() == hb:maxx() then
--east
table.insert(events, {self, "East"})
table.insert(myNewData[2], "East")
table.insert(hisNewData[2], "East")
end
if (ib:minx() == mb:minx() or ib:maxx() == mb:minx())
and mb:minx() == hb:minx() then
--west
table.insert(events, {self, "West"})
table.insert(myNewData[2], "West")
table.insert(hisNewData[2], "West")
end
if (ib:maxy() == mb:maxy() or ib:miny() == mb:maxy())
and mb:maxy() == hb:maxy() then
--south
table.insert(events, {self, "South"})
table.insert(myNewData[2], "South")
table.insert(hisNewData[2], "South")
end
if ib:maxy() == mb:miny() or ib:miny() == mb:miny()
and mb:miny() == hb:miny() then
--north
table.insert(events, {self, "North"})
table.insert(myNewData[2], "North")
table.insert(hisNewData[2], "North")
end
end
end
if ib:sizex() > 0 and ib:sizey() > 0 then
if (mb:minz() >= ib:minz() and mb:minz() <= ib:maxz()) or
(hb:maxz() >= ib:minz() and hb:maxz() <= ib:maxz()) then
--floor
table.insert(events, {self, "Floor"})
table.insert(myNewData[2], "Floor")
table.insert(hisNewData[2], "Roof")
end
if (mb:maxz() <= ib:maxz() and mb:maxz() >= ib:minz()) or
(hb:minz() <= ib:maxz() and hb:minz() >= ib:minz()) then
--roof
table.insert(events, {self, "Roof"})
table.insert(myNewData[2], "Roof")
table.insert(hisNewData[2], "Floor")
end
end
SetAdjacentRoom(o.adjacent_rooms, self, #hisNewData[2] > 0 and hisNewData)
SetAdjacentRoom(new_adjacent_rooms, o, #myNewData[2] > 0 and myNewData)
end, mb, is_permanent)
for _, room in ipairs(adjacent_rooms or empty_table) do
if not new_adjacent_rooms[room] then
--adjacency removed
local data = adjacent_rooms[room]
local myaw = data[2]
local hisData = SetAdjacentRoom(room.adjacent_rooms, self, false)
local hisaw = hisData and hisData[2]
for i = 1, #(myaw or empty_table) do
table.insert(events, {self, myaw[i]})
end
for i = 1, #(hisaw or empty_table) do
table.insert(events, {room, hisaw[i]})
end
end
end
self.adjacent_rooms = new_adjacent_rooms
if IsChangingMap() or XEditorUndo.undoredo_in_progress then
return
end
if #(events or empty_table) > 0 then
table.insert(AdjacencyEvents, events)
Wakeup(PeriodicRepeatThreads["AdjacencyEvents"])
end
end
function ProcessVolumeAdjacencyEvents()
local passed = {}
for i = 1, #AdjacencyEvents do
local events = AdjacencyEvents[i]
for i = 1, #(events or empty_table) do
local ev = events[i]
local o = ev[1]
local s = ev[2]
if IsValid(o) and (not passed[o] or (passed[o] and not passed[o][s])) then
passed[o] = passed[o] or {}
passed[o][s] = true
--print(o.name, s)
o:OnAdjacencyChanged(s)
end
end
end
table.clear(AdjacencyEvents)
end
-- make sure all changes to rooms are completed before we finish capturing undo data
OnMsg.EditorObjectOperationEnding = ProcessVolumeAdjacencyEvents
MapGameTimeRepeat("AdjacencyEvents", -1, function(sleep)
PauseInfiniteLoopDetection("AdjacencyEvents")
ProcessVolumeAdjacencyEvents()
ResumeInfiniteLoopDetection("AdjacencyEvents")
WaitWakeup()
end)
local dirToWallMatMember = {
North = "north_wall_mat",
South = "south_wall_mat",
West = "west_wall_mat",
East = "east_wall_mat",
Floor = "floor_mat",
}
local sideToFuncName = {
NW = "RecreateNWCornerBeam",
NE = "RecreateNECornerBeam",
SW = "RecreateSWCornerBeam",
SE = "RecreateSECornerBeam",
}
function Volume:CheckWallSizes()
if not Platform.developer then return end
local t = self.spawned_walls
assert(#t.West == #t.East)
assert(#t.North == #t.South)
end
function Volume:OnAdjacencyChanged(side)
if #side == 2 then
self[sideToFuncName[side]](self)
elseif side == "Floor" then
self:CreateFloor(self.floor_mat)
elseif side == "Roof" then
if not self.being_placed then
self:UpdateRoofSlabVisibility()
end
else
self:CreateWalls(side, self[dirToWallMatMember[side]])
self:CheckWallSizes()
end
self:DelayedRecalcRoof()
end
if FirstLoad then
SelectedRooms = false
RoomSelectionMode = false
end
function SetRoomSelectionMode(bVal)
RoomSelectionMode = bVal
print(string.format("RoomSelectionMode is %s", RoomSelectionMode and "ON" or "OFF"))
end
function ToggleRoomSelectionMode()
SetRoomSelectionMode(not RoomSelectionMode)
end
if FirstLoad then
roomsToDeselect = false
end
local function selectRoomHelper(r, t)
t = t or SelectedRooms
t = t or {}
r:SetPosMarkersVisible(true)
table.insert(t, r)
if roomsToDeselect then
table.remove_entry(roomsToDeselect, r)
end
end
local function deselectRoomHelper(r)
if IsValid(r) then
r:SetPosMarkersVisible(false)
r:ClearSelectedWall()
end
end
local function deselectRooms()
for i = 1, #(roomsToDeselect or "") do
deselectRoomHelper(roomsToDeselect[i])
end
roomsToDeselect = false
end
function OnMsg.EditorSelectionChanged(objects)
--room selection
if RoomSelectionMode then
--if 1 slab is selected?
local o = #objects == 1 and objects[1]
if o and IsKindOf(o, "Slab") and IsValid(o.room) then
editor.ClearSel()
editor.AddToSel({o.room})
return --don't do further analysis this pass
end
end
--selected rooms
local newSelectedRooms = {}
for i = 1, #objects do
local o = objects[i]
if IsKindOf(o, "Slab") then
local r = o.room
if IsValid(r) then
selectRoomHelper(r, newSelectedRooms)
end
elseif IsKindOf(o, "Room") then
selectRoomHelper(o, newSelectedRooms)
end
end
for i = 1, #(SelectedRooms or "") do
local r = SelectedRooms[i]
if not table.find(newSelectedRooms, r) then
--deselect
roomsToDeselect = roomsToDeselect or {}
table.insert(roomsToDeselect, r)
DelayedCall(0, deselectRooms)
end
end
SelectedRooms = #newSelectedRooms > 0 and newSelectedRooms or false
end
function Volume:TogglePosMarkersVisible()
local el = self.text_markers.North
self:SetPosMarkersVisible(el:GetEnumFlags(const.efVisible) == 0)
end
function Volume:SetPosMarkersVisible(val)
for k, v in pairs(self.text_markers) do
if not val then
v:ClearEnumFlags(const.efVisible)
else
v:SetEnumFlags(const.efVisible)
end
end
end
function Volume:PositionWallTextMarkers()
local t = self.text_markers
local gz = self:CalcZ() + self.size:z() * voxelSizeZ / 2
local p = self.position + point(self.size:x() * voxelSizeX / 2, 0)
p = p:SetZ(gz)
t.North:SetPos(p)
p = self.position + point(self.size:x() * voxelSizeX / 2, self.size:y() * voxelSizeY)
p = p:SetZ(gz)
t.South:SetPos(p)
p = self.position + point(0, self.size:y() * voxelSizeY / 2)
p = p:SetZ(gz)
t.West:SetPos(p)
p = self.position + point(self.size:x() * voxelSizeX, self.size:y() * voxelSizeY / 2)
p = p:SetZ(gz)
t.East:SetPos(p)
end
function Volume:FinishAlign()
if not self.seed then
self.seed = EncodeVoxelPos(self)
end
self:CopyBoxToCCD()
self:RebuildAdjacencyData()
if self.wireframe_visible then
self:GenerateGeometry()
else
self:DoneLines()
end
self:PositionWallTextMarkers()
self.box_at_last_roof_edit = self.box
if not IsChangingMap() then
self:RefreshFloorCombatStatus()
end
Msg("RoomAligned", self)
end
function Volume:RefreshFloorCombatStatus()
end
function Volume:Setfloor(v)
self.floor = v
self:RefreshFloorCombatStatus()
end
function Volume:VolumeDestructor()
self:DoneLines()
DoneObject(self.light_vol_obj)
DoneObjects(self.light_vol_objs)
self.light_vol_obj = nil
self.light_vol_objs = nil
for k, v in pairs(self.text_markers) do
DoneObject(v)
end
self["VolumeDestructor"] = empty_func
end
function Volume:Done()
self:VolumeDestructor()
end
function Volume.ToggleVolumeCollision(_, self)
VolumeCollisonEnabled = not VolumeCollisonEnabled
end
function Volume:CheckCollision(cls, box)
if not VolumeCollisonEnabled then return false end
if not self.enable_collision then return false end
cls = cls or self.class
local ret = false
box = box or self.box
MapForEach(self:GetPos(), roomQueryRadius, cls, function(o)
if o ~= self and o.enable_collision then
if box:Intersect(o.box) ~= 0 then
ret = true
return "break"
end
end
end, box)
return ret
end
local dontCopyTheeseProps = {
name = true,
floor = true,
adjacent_rooms = true,
box = true,
position = true,
roof_objs = true,
spawned_doors = true,
spawned_windows = true,
spawned_decals = true,
spawned_walls = true,
spawned_corners = true,
spawned_floors = true,
text_markers = true,
}
function Volume:RecreateAllWallsOnMap()
MapForEach("map", "Volume", Volume.RecreateWalls)
end
function Volume:RecreateAllRoofsOnMap()
local all_volumes = MapGet("map", "Volume")
table.sortby_field(all_volumes, "floor")
for i,volume in ipairs(all_volumes) do
volume:RecreateRoof()
end
end
function Volume:RecreateAllFloorsOnMap()
MapForEach("map", "Volume", Volume.RecreateFloor)
end
function Volume:RecreateWalls()
SuspendPassEdits("Volume:RecreateWalls")
self:DeleteAllWallObjs()
self:DeleteAllCornerObjs()
self:CreateAllWalls()
self:CreateAllCorners()
self:OnSetouter_colors(self.outer_colors)
self:OnSetinner_colors(self.inner_colors)
ResumePassEdits("Volume:RecreateWalls")
end
function Volume:RecreateFloor()
SuspendPassEdits("Volume:RecreateFloor")
self:DeleteAllFloors()
self:CreateFloor()
self:OnSetfloor_colors(self.floor_colors)
ResumePassEdits("Volume:RecreateFloor")
end
function Volume:RecreateRoofBtn()
self:RecreateRoof()
end
function Volume:ReRandomize()
self.last_wall_recreate_seed = self.seed
self.seed = BraidRandom(self.seed)
self:CreateAllWalls()
self.last_wall_recreate_seed = self.seed
ObjModified(self)
end
function Volume:CopyAbove()
XEditorUndo:BeginOp()
local nv = self:Copy(1)
SetSelectedVolume(nv)
XEditorUndo:EndOp{ nv }
end
function Volume:CopyBelow()
XEditorUndo:BeginOp()
local nv = self:Copy(-1)
SetSelectedVolume(nv)
XEditorUndo:EndOp{ nv }
end
function Volume:CollisionCheckNextFloor(floorOffset)
if not VolumeCollisonEnabled then return false end
if not self.enable_collision then return false end
local b = self.box
local offset = point(0, 0, voxelSizeZ * self.size:z() * floorOffset)
b = Offset(b, offset)
local collision = false
MapForEach(self:GetPos(), roomQueryRadius, self.class, function(o)
if o ~= self and o.enable_collision then
if b:Intersect(o.box) ~= 0 then
collision = true
return "break"
end
end
end)
return collision
end
function Volume:Copy(floorOffset, inputObj, skipCollisionTest)
local offset = point(0, 0, voxelSizeZ * self.size:z() * floorOffset)
local collision = false
if not skipCollisionTest then
collision = self:CollisionCheckNextFloor(floorOffset)
end
if skipCollisionTest or not collision then
inputObj = inputObj or {}
inputObj.floor = inputObj.floor or self.floor + floorOffset
inputObj.position = inputObj.position or self.position + offset
inputObj.size = inputObj.size or self.size
inputObj.name = inputObj.name or self.name .. " Copy"
local doNotCopyTheseEither = table.copy(inputObj)
local cpy = PlaceObject(self.class, inputObj)
local prps = self:GetProperties()
for i = 1, #prps do
local prop = prps[i]
if not dontCopyTheeseProps[prop.id] and not doNotCopyTheseEither[prop.id] then
cpy:SetProperty(prop.id, self:GetProperty(prop.id))
end
end
cpy:OnCopied(self, offset)
DelayedCall(500, BuildBuildingsData)
return cpy
end
end
function Volume:OnCopied(from)
self:AlignObj()
end
function Volume:ToggleGeometryVisible()
if self.lines == false then
self:GenerateGeometry()
return
end
if self.lines and self.lines[1] then
local visible = self.lines[1]:GetEnumFlags(const.efVisible) == 0
for i = 1, #(self.lines or empty_table) do
if visible then
self.lines[i]:SetEnumFlags(const.efVisible)
else
self.lines[i]:ClearEnumFlags(const.efVisible)
end
end
end
end
function Volume:DoneLines()
DoneObjects(self.lines)
self.lines = false
end
function Volume:GetWallBox(side, roomBox)
local ret = false
local b = roomBox or self.box
if side == "North" then
ret = box(b:minx(), b:miny(), b:minz(), b:maxx(), b:miny() + 1, b:maxz())
elseif side == "South" then
ret = box(b:minx(), b:maxy() - 1, b:minz(), b:maxx(), b:maxy(), b:maxz())
elseif side == "East" then
ret = box(b:maxx() - 1, b:miny(), b:minz(), b:maxx(), b:maxy(), b:maxz())
elseif side == "West" then
ret = box(b:minx(), b:miny(), b:minz(), b:minx() + 1, b:maxy(), b:maxz())
end
return ret
end
local function SetLineMesh(line, line_pstr)
if not line_pstr or line_pstr:size() == 0 then return end
line:SetMesh(line_pstr)
return line
end
local offsetFromVoxelEdge = 20
function Volume:GenerateGeometry()
self:DoneLines()
local lines = {}
local xPoints = {}
local xPointsRoof = {}
local yPoints = {}
local yPointsRoof = {}
local zOrigin = self:CalcSnappedZ()
local p = self.position
local x, y = p:xyz()
local sx = abs(self.size:x())
local sy = abs(self.size:y())
local sz = abs(self.size:z())
for inX = 0, sx - 1 do
for inY = 0, sy - 1 do
xPoints[inY] = xPoints[inY] or pstr("")
yPoints[inX] = yPoints[inX] or pstr("")
xPointsRoof[inY] = xPointsRoof[inY] or pstr("")
yPointsRoof[inX] = yPointsRoof[inX] or pstr("")
local xx, yy, zz, ox, oy, oz
xx = x + inX * voxelSizeX + halfVoxelSizeX
if inX == 0 then
ox = xx - halfVoxelSizeX + offsetFromVoxelEdge
elseif inX == sx - 1 then
ox = xx + halfVoxelSizeX - offsetFromVoxelEdge
else
ox = xx
end
yy = y + inY * voxelSizeY + halfVoxelSizeY
if inY == 0 then
oy = yy - halfVoxelSizeY + offsetFromVoxelEdge
elseif inY == sy - 1 then
oy = yy + halfVoxelSizeY - offsetFromVoxelEdge
else
oy = yy
end
zz = zOrigin + offsetFromVoxelEdge
oz = zz + sz * voxelSizeZ - offsetFromVoxelEdge*2
if inX == 0 then
--wall
xPoints[inY]:AppendVertex(ox, yy, oz, self.wireframeColor)
end
xPoints[inY]:AppendVertex(ox, yy, zz, self.wireframeColor)
if sx == 1 then
xPointsRoof[inY]:AppendVertex(ox, yy, oz, self.wireframeColor)
ox = xx + halfVoxelSizeX - offsetFromVoxelEdge
xPoints[inY]:AppendVertex(ox, yy, zz, self.wireframeColor)
end
if inX == sx - 1 then
--wall
xPoints[inY]:AppendVertex(ox, yy, oz, self.wireframeColor)
end
if inY == 0 then
--wall
yPoints[inX]:AppendVertex(xx, oy, oz, self.wireframeColor)
end
yPoints[inX]:AppendVertex(xx, oy, zz, self.wireframeColor)
if sy == 1 then
yPointsRoof[inX]:AppendVertex(xx, oy, oz, self.wireframeColor)
oy = yy + halfVoxelSizeY - offsetFromVoxelEdge
yPoints[inX]:AppendVertex(xx, oy, zz, self.wireframeColor)
end
if inY == sy - 1 then
--wall
yPoints[inX]:AppendVertex(xx, oy, oz, self.wireframeColor)
end
xPointsRoof[inY]:AppendVertex(ox, yy, oz, self.wireframeColor)
yPointsRoof[inX]:AppendVertex(xx, oy, oz, self.wireframeColor)
end
end
local visible = self.wireframe_visible
local function SetVisibilityHelper(line)
if not visible then
line:ClearEnumFlags(const.efVisible)
end
end
for inX = 0, sx - 1 do
local line = PlaceObject("Polyline")
SetVisibilityHelper(line)
line:SetPos(p)
SetLineMesh(line, yPoints[inX])
table.insert(lines, line)
self:Attach(line)
line = PlaceObject("Polyline")
SetVisibilityHelper(line)
line:SetPos(p)
SetLineMesh(line, yPointsRoof[inX])
table.insert(lines, line)
self:Attach(line)
end
for inY = 0, sy - 1 do
local line = PlaceObject("Polyline")
SetVisibilityHelper(line)
line:SetPos(p)
SetLineMesh(line, xPoints[inY])
table.insert(lines, line)
self:Attach(line)
line = PlaceObject("Polyline")
SetVisibilityHelper(line)
line:SetPos(p)
SetLineMesh(line, xPointsRoof[inY])
table.insert(lines, line)
self:Attach(line)
end
self.lines = lines
end
function Volume:GetBiggestEncompassingRoom(func, ...)
--this presumes no wall crossing
local biggestRoom = self
if self.box then
local sizex, sizey = self.box:sizexyz()
local biggestRoomSize = sizex + sizey
EnumVolumes(self.box, function(o, ...)
local szx, szy = o.box:sizexyz()
local size = szx + szy
if size > biggestRoomSize then
if not func or func(o, ...) then
biggestRoom = o
biggestRoomSize = size
end
end
end, ...)
end
return biggestRoom
end
local function MakeSlabInvulnerable(o, val)
o.forceInvulnerableBecauseOfGameRules = val
o.invulnerable = val
SetupObjInvulnerabilityColorMarkingOnValueChanged(o)
end
function Volume:MakeOwnedSlabsInvulnerable()
self:ForEachSpawnedObj(function(o)
MakeSlabInvulnerable(o, true)
if IsKindOf(o, "SlabWallObject") then
local os = o.owned_slabs
if os then
for _, oo in ipairs(os) do
MakeSlabInvulnerable(oo, true)
end
end
end
end)
end
function Volume:MakeOwnedSlabsVulnerable()
local floorsInvul = self.floor == 1
self:ForEachSpawnedObj(function(o)
if not floorsInvul or not IsKindOf(o, "FloorSlab") then
MakeSlabInvulnerable(o, false)
if IsKindOf(o, "SlabWallObject") then
local os = o.owned_slabs
if os then
for _, oo in ipairs(os) do
MakeSlabInvulnerable(oo, false)
end
end
end
end
end)
end
function Volume:Destroy()
--so shift + d in f3 doesn't kill these
end
function ShowVolumes(bShow, volume_class, max_floor, fn)
MapClearEnumFlags(const.efVisible, "map", "Volume")
if not bShow or not volume_class then return end
MapSetEnumFlags(const.efVisible, "map", volume_class, function(volume, max_floor, fn)
if volume.floor <= max_floor then
fn(volume)
return true
end
end, max_floor or max_int, fn or empty_func)
end
function SelectVolume(pt) -- in screen coordinates, terminal.GetMousePos()
-- enumerate all visible volumes on the map and select the one under the mouse point pt
local start = ScreenToGame(pt)
local pos = cameraRTS.GetPos()
local dir = start - pos
dir = dir * 1000
local dir2 = start + dir
local camFloor = cameraTac.GetFloor() + 1
--DbgAddCircle(start, 100)
--DbgAddVector(start, pos - start)
--DbgAddVector(start, dir*1000, RGB(0, 255, 0))
return MapFindMin("map", "Volume", nil, nil, nil, nil, nil, nil, function(volume, dir2, camFloor)
if HideFloorsAboveThisOne then
if volume.floor > HideFloorsAboveThisOne then
return false
end
end
--return distance to intersection between the camera ray and volume box
local p1, p2 = ClipSegmentWithBox3D(start, dir2, volume.box)
if p1 then
--DbgAddCircle(p1, 100)
--DbgAddCircle(p2, 100, RGB(0, 0, 255))
return p1:Dist2(start)
end
return false
end, start, dir2, camFloor) or false, start, dir2
end
local lastSelectedVolume = false
local function SetSelectedVolumeAndFireEvents(vol)
if vol ~= SelectedVolume then
local oldVolume = SelectedVolume
SelectedVolume = vol
if oldVolume then
lastSelectedVolume = oldVolume
if IsValid(oldVolume) then
oldVolume.wall_text_markers_visible = false
oldVolume:SetPosMarkersVisible(false)
end
Msg("VolumeDeselected", oldVolume)
end
if SelectedVolume then
if SelectedVolume ~= lastSelectedVolume then --only deselect wall if another vol is selected
SelectedVolume.selected_wall = false
ObjModified(SelectedVolume)
end
SelectedVolume.wall_text_markers_visible = true
SelectedVolume:SetPosMarkersVisible(true)
editor.ClearSel()
end
Msg("VolumeSelected", SelectedVolume)
end
end
function SetSelectedVolume(vol)
SetSelectedVolumeAndFireEvents(vol)
if GedRoomEditor then
GedRoomEditorObjList = GedRoomEditor:ResolveObj("root")
CreateRealTimeThread(function()
GedRoomEditor:SetSelection("root", table.find(GedRoomEditorObjList, SelectedVolume))
end)
end
end
local doorId = "Door"
local windowId = "Window"
local doorTemplate = "%s_%s"
local Doors_WidthNames = { "Single", "Double" }
local Windows_WidthNames = { "Single", "Double", "Triple" }
function DoorsDropdown()
return function()
local ret = { {name = "", id = ""} }
for j = 1, #Doors_WidthNames do
local name = string.format(doorTemplate, doorId, Doors_WidthNames[j])
local data = {mat = false, width = j, height = 3}
table.insert(ret, {name = name, id = data})
end
return ret
end
end
function WindowsDropdown()
return function()
local ret = { {name = "", id = ""} }
for j = 1, #Windows_WidthNames do
local name = string.format(doorTemplate, windowId, Windows_WidthNames[j])
local data = {mat = false, width = j, height = 2}
table.insert(ret, {name = name, id = data})
end
return ret
end
end
function GetDecalPresetData()
return Presets.RoomDecalData.Default
end
function DecalsDropdown()
return function()
local ret = { {name = "", id = ""} }
local presetData = GetDecalPresetData()
for _, entry in ipairs(presetData) do
local data = { entity = entry.id, }
table.insert(ret, {name = entry.id, id = data})
end
return ret
end
end
local function GetAllWindowEntitiesForMaterial(obj)
local material = type(obj) == "string" and obj or obj.linked_obj and obj.linked_obj.material or obj.material
local ret = { false }
for w = 0, 3 do
for h = 1, 3 do
for v = 1, 10 do
local e = SlabWallObjectName(material, h, w, v, false)
if IsValidEntity(e) then
ret[#ret + 1] = { name = e, value = {entity = e, height = h, width = w, subvariant = v, material = material} }
end
end
end
end
return ret
end
local function GetAllDoorEntitiesForMaterial(obj)
local material = type(obj) == "string" and obj or obj.linked_obj and obj.linked_obj.material or obj.material
local ret = { false }
for w = 1, 3 do
for h = 3, 4 do
for v = 1, 10 do
local e = SlabWallObjectName(material, h, w, v, true)
if IsValidEntity(e) then
ret[#ret + 1] = { name = e, value = {entity = e, height = h, width = w, subvariant = v, material = material} }
end
if v == 1 then
e = SlabWallObjectName(material, h, w, nil, true)
if IsValidEntity(e) then
ret[#ret + 1] = { name = e, value = {entity = e, height = h, width = w, subvariant = v, material = material} }
end
end
end
end
end
return ret
end
local function SelectedWallNoEdit(self)
return self.selected_wall == false
end
slabDirToAngle = {
North = 270 * 60,
South = 90 * 60,
West = 180 * 60,
East = 0,
}
slabAngleToDir = {
[270 * 60] = "North",
[90 * 60] = "South",
[180 * 60] = "West",
[0] = "East",
}
slabCornerAngleToDir = {
[270 * 60] = "East",
[90 * 60] = "West",
[180 * 60] = "North",
[0] = "South",
}
function _RoomVisibilityCategoryNoEdit()
return RoomVisibilityCategoryNoEdit()
end
function RoomVisibilityCategoryNoEdit()
return true
end
local VisibilityStateItems = {
"Closed",
"Hidden",
"Open"
}
function SlabMaterialComboItemsWithNone()
return PresetGroupCombo("SlabPreset", "SlabMaterials", nil, noneWallMat)
end
function SlabMaterialComboItemsOnly()
return function()
local f1 = SlabMaterialComboItemsWithNone()
local ret = f1()
table.remove(ret, 1)
return ret
end
end
function SlabMaterialComboItemsWithDefault()
return function()
local f1 = SlabMaterialComboItemsWithNone()
local ret = f1()
table.insert(ret, 2, defaultWallMat)
return ret
end
end
DefineClass.Room = {
__parents = { "Volume", "EditorSubVariantObject" },
flags = { gofWarped = true },
properties = {
{ category = "General", name = "Doors And Windows Are Blocked", id = "doors_windows_blocked", editor = "bool", default = false, },
{ category = "General", id = "name", name = "Name", editor = "text", default = false, help = "Default 'Room <handle>', renameable." },
{ category = "General", id = "size_z", name = "Height (z)", editor = "number", default = defaultVolumeVoxelHeight, min = 0, max = maxRoomVoxelSizeZ, dont_save = true},
{ category = "General", id = "size_x", name = "Width (x)", editor = "number", default = 1, min = 1, max = maxRoomVoxelSizeX, dont_save = true},
{ category = "General", id = "size_y", name = "Depth (y)", editor = "number", default = 1, min = 1, max = maxRoomVoxelSizeY, dont_save = true},
{ category = "General", id = "move_x", name = "Move EW (x)", editor = "number", default = 0, dont_save = true},
{ category = "General", id = "move_y", name = "Move NS (y)", editor = "number", default = 0, dont_save = true},
{ category = "General", id = "move_z", name = "Move UD (z)", editor = "number", default = 0, dont_save = true},
--materials
{ category = "Materials", id = "wall_mat", name = "Wall Material", editor = "preset_id", preset_class = "SlabPreset", preset_group = "SlabMaterials", extra_item = noneWallMat, default = "Planks",
buttons = {
{name = "Reset", func = "ResetWallMaterials"},
},
},
{ category = "Materials", id = "outer_colors", name = "Outer Color Modifier", editor = "nested_obj", base_class = "ColorizationPropSet", inclusive = true, default = false, },
{ category = "Materials", id = "inner_wall_mat", name = "Inner Wall Material", editor = "preset_id", preset_class = "SlabIndoorMaterials", extra_item = noneWallMat, default = "Planks", },
{ category = "Materials", id = "inner_colors", name = "Inner Color Modifier", editor = "nested_obj", base_class = "ColorizationPropSet", inclusive = true, default = false, },
{ category = "Materials", id = "north_wall_mat", name = "North Wall Material", editor = "dropdownlist", items = SlabMaterialComboItemsWithDefault, default = defaultWallMat,
buttons = {
{name = "Select", func = "ViewNorthWallFromOutside"},
}
},
{ category = "Materials", id = "south_wall_mat", name = "South Wall Material", editor = "dropdownlist", items = SlabMaterialComboItemsWithDefault, default = defaultWallMat,
buttons = {
{name = "Select", func = "ViewSouthWallFromOutside"},
}
},
{ category = "Materials", id = "east_wall_mat", name = "East Wall Material", editor = "dropdownlist", items = SlabMaterialComboItemsWithDefault, default = defaultWallMat,
buttons = {
{name = "Select", func = "ViewEastWallFromOutside"},
}
},
{ category = "Materials", id = "west_wall_mat", name = "West Wall Material", editor = "dropdownlist", items = SlabMaterialComboItemsWithDefault, default = defaultWallMat,
buttons = {
{name = "Select", func = "ViewWestWallFromOutside"},
}
},
{ category = "Materials", id = "floor_mat", name = "Floor Material", editor = "preset_id", preset_class = "SlabPreset", preset_group = "FloorSlabMaterials", extra_item = noneWallMat, default = "Planks", },
{ category = "Materials", id = "floor_colors", name = "Floor Color Modifier", editor = "nested_obj", base_class = "ColorizationPropSet", inclusive = true, default = false, },
{ category = "Materials", id = "Warped", name = "Warped", editor = "bool", default = true },
{ category = "Materials", id = "selected_wall_buttons", name = "selected wall buttons", editor = "buttons", default = false, dont_save = true, read_only = true,
no_edit = SelectedWallNoEdit,
buttons = {
{name = "Clear Wall Selection", func = "ClearSelectedWall"},
{name = "Delete Doors", func = "UIDeleteDoors"},
{name = "Delete Windows", func = "UIDeleteWindows"},
},
},
{ category = "Materials", id = "place_decal", name = "Place Decal", editor = "choice", items = DecalsDropdown, default = "", no_edit = SelectedWallNoEdit,},
{ category = "General", id = "spawned_doors", editor = "objects", no_edit = true,},
{ category = "General", id = "spawned_windows", editor = "objects", no_edit = true,},
{ category = "General", id = "spawned_decals", editor = "objects", no_edit = true,}, --todo: kill
{ category = "General", id = "spawned_floors", editor = "objects", no_edit = true},
{ category = "General", id = "spawned_walls", editor = "objects", no_edit = true,},
{ category = "General", id = "spawned_corners", editor = "objects", no_edit = true,},
{ category = "Not Room Specific", id = "hide_floors_editor", editor = "number", default = 100, name = "Hide Floors Above", dont_save = true},
--bacon specific?
{ category = "Visibility", name = "Visibility State", id = "visibility_state", editor = "choice", items = VisibilityStateItems, dont_save = true, no_edit = _RoomVisibilityCategoryNoEdit },
{ category = "Visibility", name = "Focused", id = "is_focused", editor = "bool", default = false, dont_save = true, no_edit = _RoomVisibilityCategoryNoEdit },
{ category = "Ignore None Material", name = "Wall", id = "none_wall_mat_does_not_affect_nbrs", editor = "bool", default = false, help = "By default, setting a wall material to none will hide overlapping walls, tick this for it to stop happening. Affects all walls of a room." },
{ category = "Ignore None Material", name = "Roof Wall", id = "none_roof_wall_mat_does_not_affect_nbrs", editor = "bool", default = false, help = "Same as walls (see above), but for walls that are part of the roof - roof walls." },
{ category = "Ignore None Material", name = "Floor", id = "none_floor_mat_does_not_affect_nbrs", editor = "bool", default = false, help = "By default, setting a floor material to none will hide overlapping floors, tick this for it to stop happening. Affects all floors of a room." },
},
auto_add_in_editor = true, -- for Room editor
spawned_walls = false, -- {["North"] = {}, etc.}
spawned_corners = false,
spawned_floors = false,
spawned_doors = false,
spawned_windows = false,
spawned_decals = false,
selected_wall = false, -- false, "North", "South", etc.
next_visibility_state = false,
visibility_state = false, -- when defined purely as a prop, something strips it.
open_state_collapsed_walls = false,
outside_border = false,
nametag = false,
}
local function moveHelper(self, key, old_v, x, y, z, ignore_collision)
if IsChangingMap() then return end
self:InternalAlignObj(true) -- this moves box
if not ignore_collision and self:CheckCollision() then
self[key] = old_v
self:InternalAlignObj(true)
print("Could not move room due to collision with other room!")
return false
else
self:MoveAllSpawnedObjs(x, y, z)
Volume.FinishAlign(self)
Msg("RoomMoved", self, x, y, z) -- x, y, z - move delta in voxels
return true
end
end
function sign(v)
return v ~= 0 and abs(v) / v or 0
end
function moveHelperHelper(r, delta)
local old = r.position
delta = delta + halfVoxelPt
r.position = SnapVolumePos(old + delta)
return moveHelper(r, "position", old, delta:x() / voxelSizeX, delta:y() / voxelSizeY, delta:z() / voxelSizeZ)
end
function Room:EditorExit()
if IsValid(self.nametag) then
self.nametag:ClearEnumFlags(const.efVisible)
end
end
function Room:EditorEnter()
if IsValid(self.nametag) and self:GetEnumFlags(const.efVisible) ~= 0 then
self.nametag:SetEnumFlags(const.efVisible)
end
end
function Room:Setname(n)
self.name = n
if not IsValid(self.nametag) then
self.nametag = PlaceObject("TextEditor")
self:Attach(self.nametag)
self.nametag:SetAttachOffset((axis_z * 3 * voxelSizeZ) / 4096)
if not IsEditorActive() or self:GetEnumFlags(const.efVisible) == 0 then
self.nametag:ClearEnumFlags(const.efVisible)
end
end
self.nametag:SetText(self.name)
end
local movedRooms = false
function Room_RecalcRoofsOfMovedRooms()
if not movedRooms then return end
for i = 1, #movedRooms do
local room = movedRooms[i]
if IsValid(room) then
room:RecalcRoof()
room:UpdateRoofVfxControllers()
end
end
movedRooms = false
end
function Room:DelayedRecalcRoof()
movedRooms = table.create_add_unique(movedRooms, self)
if LocalStorage.FilteredCategories["Roofs"] and not XEditorUndo.undoredo_in_progress then
DelayedCall(200, Room_RecalcRoofsOfMovedRooms)
end
end
-- make sure all changes to roofs are completed before we finish capturing undo data
OnMsg.EditorObjectOperationEnding = Room_RecalcRoofsOfMovedRooms
function Room:AlignObj(pos, angle)
if pos then
assert(IsEditorActive())
local offset = pos - self:GetPos()
local box = self.box
local didMove = false
if abs(offset:x()) / voxelSizeX > 0 or abs(offset:y()) / voxelSizeY > 0 or abs(offset:z()) / voxelSizeZ > 0 then
didMove = moveHelperHelper(self, offset)
end
if didMove then
ObjModified(self)
assert(self:GetGameFlags(const.gofPermanent) ~= 0)
box = AddRects(box, self.box)
ComputeSlabVisibilityInBox(box)
DelayedCall(500, BuildBuildingsData)
self:DelayedRecalcRoof()
end
else
self:InternalAlignObj()
end
end
function Room:GetEditorLabel()
return self.name or self.class
end
function InsertMaterialProperties(name, count)
assert(count >= 1 and count <= 4)
for i = 1, count do
table.insert(Room.properties, {
id = name .. "color" .. count,
editor = "color",
alpha = false,
})
table.insert(Room.properties, {
id = name .. "metallic" .. count,
editor = "number",
})
end
end
local room_NSWE_lists = {
"spawned_walls",
"spawned_corners",
"spawned_doors",
"spawned_windows",
"spawned_decals",
}
local room_NSWE_lists_no_DoorsWindows = {
"spawned_walls",
"spawned_corners",
}
local room_regular_lists = {
"spawned_floors",
"roof_objs",
}
local room_regular_list_sides = {
"Floor",
false -- see RoomRoof:GetPivots
}
function ForEachInTable(t, f, ...)
for i = 1, #(t or "") do
local o = t[i]
if IsValid(o) then
f(o, ...)
end
end
end
function Room:UnlockAllSlabs()
self:UnlockFloor()
self:UnlockAllWalls()
self:UnlockRoof()
end
function Room:UnlockFloor()
ForEachInTable(self.spawned_floors, Slab.UnlockSubvariant)
end
function Room:UnlockAllWalls()
for side, t in pairs(self.spawned_walls or empty_table) do
ForEachInTable(t, Slab.UnlockSubvariant)
end
for side, t in pairs(self.spawned_corners or empty_table) do
ForEachInTable(t, Slab.UnlockSubvariant)
end
ForEachInTable(self.roof_objs, function(o)
if not IsKindOf(o, "RoofSlab") then
o:UnlockSubvariant()
end
end)
end
function Room:UnlockRoof()
ForEachInTable(self.roof_objs, function(o)
if IsKindOf(o, "RoofSlab") then
o:UnlockSubvariant()
end
end)
end
sideToCornerSides = {
East = { "East", "South" },
South = { "West", "South" },
West = { "West", "North" },
North = { "East", "North" },
}
function Room:UnlockWallSide(side) --both walls and corners + roof walls n corners in one
ForEachInTable(self.spawned_walls and self.spawned_walls[side], Slab.UnlockSubvariant)
local css = sideToCornerSides[side]
for _, cs in ipairs(css) do
ForEachInTable(self.spawned_corners and self.spawned_corners[cs], Slab.UnlockSubvariant)
end
ForEachInTable(self.roof_objs, function(o, side)
if o.side == side and not IsKindOf(o, "RoofSlab") then
o:UnlockSubvariant()
end
end ,side)
end
function Room:ForEachSpawnedObjNoDoorsWindows(func, ...)
return self:_ForEachSpawnedObj(room_NSWE_lists_no_DoorsWindows, room_regular_lists, func, ...)
end
function Room:ForEachSpawnedObj(func, ...)
return self:_ForEachSpawnedObj(room_NSWE_lists, room_regular_lists, func, ...)
end
function Room:_ForEachSpawnedObj(NSWE_lists, regular_lists, func, ...)
for i = 1, #NSWE_lists do
for side, objs in NSEW_pairs(self[NSWE_lists[i]] or empty_table) do
for j = 1, #objs do
if IsValid(objs[j]) then func(objs[j], ...) end
end
end
end
for i = 1, #regular_lists do
local lst = self[regular_lists[i]] or ""
for j = 1, #lst do
if IsValid(lst[j]) then func(lst[j], ...) end
end
end
end
function Room:GetEditorRelatedObjects()
local ret = {}
for i = 1, #room_NSWE_lists do
for side, objs in NSEW_pairs(self[room_NSWE_lists[i]] or empty_table) do
for _, obj in ipairs(objs) do
if obj then
ret[#ret + 1] = obj
if obj:HasMember("owned_objs") and obj.owned_objs then
table.iappend(ret, obj.owned_objs)
end
if obj:HasMember("owned_slabs") and obj.owned_slabs then
table.iappend(ret, obj.owned_slabs)
end
end
end
end
end
for i = 1, #room_regular_lists do
table.iappend(ret, self[room_regular_lists[i]] or empty_table)
end
Msg("GatherRoomRelatedObjects", self, ret)
return ret
end
function Room:SetWarped(warped, force)
CObject.SetWarped(self, warped)
if force or not IsChangingMap() then
self:ForEachSpawnedObj(function(obj)
obj:SetWarped(warped)
end)
end
end
local function copyWallObjs(t, offset, room)
local ret = {}
for side, objs in NSEW_pairs(t or empty_table) do
ret[side] = {}
for i = 1, #objs do
local o = objs[i]
local no = PlaceObject(o.class)
no.floor = room.floor
no.width = o.width
no.height = o.height
no.material = o.material
no:SetPos(o:GetPos() + offset)
no:SetAngle(o:GetAngle())
table.insert(ret[side], no)
no:UpdateEntity()
end
end
return ret
end
local function copyDecals(t, offset, room)
local ret = {}
for side, objs in NSEW_pairs(t or empty_table) do
ret[side] = {}
for i = 1, #objs do
local o = objs[i]
local no = PlaceObject(o.class)
no.floor = room.floor
no:SetPos(o:GetPos() + offset)
no:SetAngle(o:GetAngle())
no.restriction_box = Offset(o.restriction_box, offset)
table.insert(ret[side], no)
end
end
return ret
end
function Room:OnSetdoors_windows_blocked()
self:ForEachSpawnedWallObj(function(o, val)
o:SetlockpickState(val and "blocked" or "closed")
end, self.doors_windows_blocked)
end
function Room:OnSetnone_roof_wall_mat_does_not_affect_nbrs()
self:ComputeRoomVisibility()
end
function Room:OnSetnone_wall_mat_does_not_affect_nbrs()
self:ComputeRoomVisibility()
end
function Room:OnCopied(from, offset)
Volume.OnCopied(self, from, offset)
self:CreateAllSlabs()
self.spawned_doors = copyWallObjs(from.spawned_doors, offset, self)
self.spawned_windows = copyWallObjs(from.spawned_windows, offset, self)
self.spawned_decals = copyDecals(from.spawned_decals, offset, self)
end
function Room:OnAfterEditorNew(parent, ged, is_paste)
--undo deletion from ged
self.adjacent_rooms = nil
self:AlignObj()
self:CreateAllSlabs()
end
function Room:OnEditorSetProperty(prop_id, old_value, ged)
if not IsValid(self) then return end --undo on deleted obj
local f = rawget(Room, string.format("OnSet%s", prop_id))
if f then
f(self, self[prop_id], old_value)
DelayedCall(500, BuildBuildingsData)
end
end
function Room:OnSethide_floors_editor()
assert(IsEditorActive())
HideFloorsAboveThisOne = rawget(self, "hide_floors_editor")
HideFloorsAbove(HideFloorsAboveThisOne)
end
function Room:Gethide_floors_editor()
return HideFloorsAboveThisOne
end
function Room:OnSetwireframe_visible()
self:ToggleGeometryVisible()
end
function Room:OnSetwall_text_markers_visible()
self:TogglePosMarkersVisible()
end
function Room:OnSetinner_wall_mat(val, oldVal)
if val == "" then
val = noneWallMat
self.inner_wall_mat = val
end
if (val == noneWallMat or oldVal == noneWallMat) and val ~= oldVal then
self:UnlockAllWalls()
end
self:SetInnerMaterialToSlabs("North")
self:SetInnerMaterialToSlabs("South")
self:SetInnerMaterialToSlabs("West")
self:SetInnerMaterialToSlabs("East")
self:SetInnerMaterialToRoofObjs()
end
function Room:OnSetinner_colors(val, oldVal)
self:SetInnerMaterialToSlabs("North")
self:SetInnerMaterialToSlabs("South")
self:SetInnerMaterialToSlabs("West")
self:SetInnerMaterialToSlabs("East")
self:SetInnerMaterialToRoofObjs()
end
function Room:OnSetouter_colors(val, oldVal)
local function iterateNSEWTableAndSetColor(t)
if not t then return end
for side, list in NSEW_pairs(t) do
for i = 1, #list do
local o = list[i]
if IsValid(o) then --wall piece might be deleted by lvl designer
o:Setcolors(val)
end
end
end
end
iterateNSEWTableAndSetColor(self.spawned_walls)
for side, list in NSEW_pairs(self.spawned_corners) do
for i = 1, #list do
local o = list[i]
if IsValid(o) then
o:SetColorFromRoom()
end
end
end
for side, list in NSEW_pairs(self.spawned_windows or empty_table) do
for i = 1, #list do
list[i]:UpdateManagedSlabs()
list[i]:RefreshColors()
end
end
for side, list in NSEW_pairs(self.spawned_doors or empty_table) do
for i = 1, #list do
list[i]:RefreshColors()
end
end
if self.roof_objs then
for i=1,#self.roof_objs do
local o = self.roof_objs[i]
if IsValid(o) and not IsKindOf(o, "RoofSlab") then
o:Setcolors(val)
end
end
end
end
function Room:OnSetfloor_colors(val, oldVal)
for i = 1, #(self.spawned_floors or "") do
local o = self.spawned_floors[i]
if IsValid(o) then
o:Setcolors(val)
end
end
end
function Room:OnSetfloor_mat(val)
self:UnlockFloor()
self:CreateFloor()
end
function Room:ResetWallMaterials()
local wm = defaultWallMat
local change = self.north_wall_mat ~= wm
self.north_wall_mat = wm
change = change or self.south_wall_mat ~= wm
self.south_wall_mat = wm
change = change or self.west_wall_mat ~= wm
self.west_wall_mat = wm
change = change or self.east_wall_mat ~= wm
self.east_wall_mat = wm
--todo: wall added/removed msgs
if change then
ObjModified(self)
self:UnlockAllWalls()
self:CreateAllWalls()
self:RecreateRoof()
end
end
local function FireWallChangedEventsHelper(self, side, val, oldVal)
local wasWall = oldVal ~= noneWallMat and (oldVal ~= defaultWallMat or self.wall_mat ~= noneWallMat)
local isWall = val ~= noneWallMat and (val ~= defaultWallMat or self.wall_mat ~= noneWallMat)
if not wasWall and isWall then
Msg("RoomAddedWall", self, side)
elseif wasWall and not isWall then
Msg("RoomRemovedWall", self, side)
end
end
function Room:OnSetnorth_wall_mat(val, oldVal)
self:UnlockWallSide("North")
self:CreateWalls("North", val)
self:RecreateNECornerBeam()
self:RecreateNWCornerBeam()
self:RecreateRoof()
FireWallChangedEventsHelper(self, "North", val, oldVal)
self:CheckWallSizes()
end
function Room:OnSetsouth_wall_mat(val, oldVal)
self:UnlockWallSide("South")
self:CreateWalls("South", val)
self:RecreateSECornerBeam()
self:RecreateSWCornerBeam()
self:RecreateRoof()
FireWallChangedEventsHelper(self, "South", val, oldVal)
self:CheckWallSizes()
end
function Room:OnSetwest_wall_mat(val, oldVal)
self:UnlockWallSide("West")
self:CreateWalls("West", val)
self:RecreateSWCornerBeam()
self:RecreateNWCornerBeam()
self:RecreateRoof()
FireWallChangedEventsHelper(self, "West", val, oldVal)
self:CheckWallSizes()
end
function Room:OnSeteast_wall_mat(val, oldVal)
self:UnlockWallSide("East")
self:CreateWalls("East", val)
self:RecreateSECornerBeam()
self:RecreateNECornerBeam()
self:RecreateRoof()
FireWallChangedEventsHelper(self, "East", val, oldVal)
self:CheckWallSizes()
end
function Room:SetWallMaterial(val)
local ov = self.wall_mat
self.wall_mat = val
if IsChangingMap() then return end
self:OnSetwall_mat(val, ov)
end
function Room:OnSetwall_mat(val, oldVal)
if val == "" then
val = noneWallMat
self.wall_mat = val
end
self:UnlockAllWalls()
self:CreateAllWalls()
self:RecreateRoof()
local wasWall = oldVal ~= noneWallMat
local isWall = val ~= noneWallMat
local ev = false
if wasWall and not isWall then
ev = "RoomRemovedWall"
elseif not wasWall and isWall then
ev = "RoomAddedWall"
end
if ev then
if self.south_wall_mat == defaultWallMat then
Msg(ev, self, "South")
end
if self.north_wall_mat == defaultWallMat then
Msg(ev, self, "North")
end
if self.west_wall_mat == defaultWallMat then
Msg(ev, self, "West")
end
if self.east_wall_mat == defaultWallMat then
Msg(ev, self, "East")
end
end
end
function SizeSetterHelper(self, old_v)
if IsChangingMap() then return end
local oldBox = self.box
self:InternalAlignObj(true)
if self:CheckCollision() then
self.size = old_v
self:InternalAlignObj(true)
return false
else
self:Resize(old_v, self.size, oldBox)
Volume.FinishAlign(self)
Msg("RoomResized", self, old_v) --old_v == old self.size
return true
end
end
function Room:OnSetsize_x(val)
local old_v = self.size
self.size = point(val, self.size:y(), self.size:z())
SizeSetterHelper(self, old_v)
end
function Room:OnSetsize_y(val)
local old_v = self.size
self.size = point(self.size:x(), val, self.size:z())
SizeSetterHelper(self, old_v)
end
function Room:OnSetsize_z(val)
local old_v = self.size
self.size = point(self.size:x(), self.size:y(), val)
SizeSetterHelper(self, old_v)
if val == 0 then
self:DeleteAllWallObjs()
self:DeleteAllFloors()
self:DeleteAllCornerObjs()
end
end
function Room:Getsize_x()
return self.size:x()
end
function Room:Getsize_y()
return self.size:y()
end
function Room:Getsize_z()
return self.size:z()
end
function Room:Getmove_x()
local x = WorldToVoxel(self.position)
return x
end
function Room:Getmove_y()
local _, y = WorldToVoxel(self.position)
return y
end
function Room:Getmove_z()
local x, y, z = WorldToVoxel(self.position)
return z
end
function Room:OnSetz_offset(val, old_v)
moveHelper(self, "z_offset", old_v, 0, 0, val - old_v)
end
function Room:OnSetmove_x(val)
local old_v = self.position
local x, y, z = WorldToVoxel(self.position)
self.position = SnapVolumePos(VoxelToWorld(val, y, z, true))
moveHelper(self, "position", old_v, val - x, 0, 0)
end
function Room:OnSetmove_y(val)
local old_v = self.position
local x, y, z = WorldToVoxel(self.position)
self.position = SnapVolumePos(VoxelToWorld(x, val, z, true))
moveHelper(self, "position", old_v, 0, val - y, 0)
end
function Room:OnSetmove_z(val)
local old_v = self.position
local x, y, z = WorldToVoxel(self.position)
self.position = SnapVolumePos(VoxelToWorld(x, y, val, true))
moveHelper(self, "position", old_v, 0, 0, val - z)
end
--left to right sort on selected wall
local dirToComparitor = {
South = function(o1, o2)
local x1, _, _ = o1:GetPosXYZ()
local x2, _, _ = o2:GetPosXYZ()
return x1 < x2
end,
North = function(o1, o2)
local x1, _, _ = o1:GetPosXYZ()
local x2, _, _ = o2:GetPosXYZ()
return x2 < x1
end,
West = function(o1, o2)
local _, y1, _ = o1:GetPosXYZ()
local _, y2, _ = o2:GetPosXYZ()
return y1 < y2
end,
East = function(o1, o2)
local _, y1, _ = o1:GetPosXYZ()
local _, y2, _ = o2:GetPosXYZ()
return y2 < y1
end,
}
function Room:SortWallObjs(objs, dir)
table.sort(objs, dirToComparitor[dir])
end
--for doors/windows
function Room:CalculateRestrictionBox(dir, wallPos, wallSize, height, width)
local xofs, nxofs = 0, 0
local yofs, nyofs = 0, 0
width = Max(width, 1)
if dir == "North" or dir == "South" then
xofs = (wallSize / 2 - width * voxelSizeX / 2)
nxofs = xofs
if width % 2 == 0 then
local m = (dir == "South" and -1 or 1)
xofs = xofs + m * voxelSizeX / 2
nxofs = nxofs - m * voxelSizeX / 2
end
else
yofs = (wallSize / 2 - width * voxelSizeY / 2)
nyofs = yofs
if width % 2 == 0 then
local m = (dir == "West" and -1 or 1)
yofs = yofs + m * voxelSizeX / 2
nyofs = nyofs - m * voxelSizeX / 2
end
end
local maxZ = wallPos:z() + (self.size:z() * voxelSizeZ - height * voxelSizeZ)
return box(wallPos:x() - nxofs, wallPos:y() - nyofs, wallPos:z(), wallPos:x() + xofs, wallPos:y() + yofs, maxZ)
end
function Room:FindSlabObjPos(dir, width, height)
local sizeX, sizeY = self.size:x(), self.size:y()
if dir == "North" or dir == "South" then
if width > sizeX then
print("Obj is too big")
return false
end
else
if width > sizeY then
print("Obj is too big")
return false
end
end
local z = self:CalcZ() + (3 - height) * voxelSizeZ
local angle = 0
local sx, sy = self.position:x(), self.position:y()
local offsx = 0
local offsy = 0
local max = 0
if dir == "North" then
angle = 270 * 60
offsx = voxelSizeX
sx = sx + halfVoxelSizeX
max = sizeX
elseif dir == "East" then
angle = 0
offsy = voxelSizeY
sx = sx + sizeX * voxelSizeX
sy = sy + halfVoxelSizeY
max = sizeY
elseif dir == "South" then
angle = 90 * 60
offsx = voxelSizeX
sy = sy + sizeY * voxelSizeY
sx = sx + halfVoxelSizeX
max = sizeX
elseif dir == "West" then
angle = 180 * 60
offsy = voxelSizeY
sy = sy + halfVoxelSizeY
max = sizeY
end
local iStart = width == 3 and 1 or 0
for i = iStart, max - 1 do
local x = sx + offsx * i
local y = sy + offsy * i
local newPos = point(x, y, z)
local canPlace = not IntersectWallObjs(nil, newPos, width, height, angle)
if canPlace then
return newPos
end
end
return false
end
function Room:NewSlabWallObj(obj, class)
class = class or SlabWallObject
return class:new(obj)
end
function Room:ForEachSpawnedWindow(func, ...)
for _, t in sorted_pairs(self.spawned_windows or empty_table) do
for i = #t, 1, -1 do
func(t[i], ...)
end
end
end
function Room:ForEachSpawnedDoor(func, ...)
for _, t in sorted_pairs(self.spawned_doors or empty_table) do
for i = #t, 1, -1 do --functor may del
func(t[i], ...)
end
end
end
function Room:ForEachSpawnedWallObj(func, ...) --doors and windows
self:ForEachSpawnedDoor(func, ...)
self:ForEachSpawnedWindow(func, ...)
end
function Room:PlaceWallObj(val, side, class)
local dir = side or self.selected_wall
assert(dir)
if not dir then return end
--check for collision, pick pos
local freePos = self:FindSlabObjPos(dir, val.width, val.height)
if not freePos then
print("No free pos found!")
return
end
local wallPos, wallSize, center = self:GetWallPos(dir)
local obj = self:NewSlabWallObj({
entity = false, room = self, material = val.mat or "Planks",
building_class = val.building_class or nil, building_template = val.building_template or nil,
side = dir
},
class.class)
local a = slabDirToAngle[dir]
local zPosOffset = (3 - val.height) * voxelSizeZ
local vx, vy, vz, va = WallWorldToVoxel(freePos:x(), freePos:y(), wallPos:z() + zPosOffset, a)
local pos = point(WallVoxelToWorld(vx, vy, vz, va))
obj.room = self
obj.floor = self.floor
obj.subvariant = 1
obj:SetPos(pos)
obj:SetAngle(a)
obj:SetProperty("width", val.width)
obj:SetProperty("height", val.height)
obj:AlignObj()
obj:UpdateEntity()
local container, nestedList
if val.is_door or obj:IsDoor() then --door
self.spawned_doors = self.spawned_doors or {}
self.spawned_doors[dir] = self.spawned_doors[dir] or {}
container = self.spawned_doors
else --window
self.spawned_windows = self.spawned_windows or {}
self.spawned_windows[dir] = self.spawned_windows[dir] or {}
container = self.spawned_windows
end
if container then
table.insert(container[dir], obj)
end
if Platform.editor and IsEditorActive() then
editor.ClearSel()
editor.AddToSel({obj})
end
return obj
end
function Room:CalculateDecalRestrictionBox(dir, wallPos, wallSize)
local xofs, nxofs = 0, 0
local yofs, nyofs = 0, 0
if dir == "North" or dir == "South" then
xofs = wallSize / 2
nxofs = xofs
wallPos = wallPos:SetY(wallPos:y() + 100 * (dir == "North" and -1 or 1))
else
yofs = wallSize / 2
nyofs = yofs
wallPos = wallPos:SetX(wallPos:x() + 100 * (dir == "West" and -1 or 1))
end
local maxZ = wallPos:z() + (self.size:z() * voxelSizeZ) + 1
return box(wallPos:x() - nxofs, wallPos:y() - nyofs, wallPos:z(), wallPos:x() + xofs, wallPos:y() + yofs, maxZ)
end
DefineClass.RoomDecal = {
__parents = { "AlignedObj", "Decal", "Shapeshifter", "Restrictor", "HideOnFloorChange" },
properties = {
{ category = "General", id = "entity", editor = "text", default = false, no_edit = true },
},
flags = { cfAlignObj = true, cfDecal = true, efCollision = false, gofPermanent = true, },
}
function RoomDecal:AlignObj(pos, angle, axis)
pos = pos or self:GetPos()
local x, y, z = self:RestrictXYZ(pos:xyz())
self:SetPos(x, y, z)
self:SetAxisAngle(axis or self:GetAxis(), angle or self:GetAngle())
end
function RoomDecal:ChangeEntity(val)
Shapeshifter.ChangeEntity(self, val)
self.entity = val
end
function RoomDecal:GameInit()
if IsChangingMap() and self.entity then
Shapeshifter.ChangeEntity(self, self.entity)
end
end
function RoomDecal:Done()
local safe = rawget(self, "safe_deletion")
if not safe then
--decals dont have ref to the room they belong to, so the check is all weird
local box = self.restriction_box
if box then
local passed = {}
MapForEach(box:grow(100, 100, 0), "WallSlab", function(s)
local side = s.side
local room = s.room
if room then
local id = xxhash(room.handle, side)
if not passed[id] then
passed[id] = true
local t = room.spawned_decals[side]
local t_idx = table.find(t, self)
if t_idx then
table.remove(t, t_idx)
ObjModified(room)
return "break"
end
end
end
end)
else
local b = self:GetObjectBBox()
local success = false
b = b:grow(guim, guim, guim)
EnumVolumes(b, function(r)
local t = r.spawned_decals
for side, tt in pairs(t or empty_table) do
local t_idx = table.find(tt, self)
if t_idx then
table.remove(tt, t_idx)
ObjModified(r)
success = true
return "break"
end
end
end)
if not success then
assert(false, "RoomDecal not safely deleted, ref in room remains!")
end
end
end
end
function Room:Setplace_decal(val)
local dir = self.selected_wall
if not dir then return end
local wallPos, wallSize, center = self:GetWallPos(dir)
local a = slabDirToAngle[dir]
local obj = RoomDecal:new()
obj.floor = self.floor
obj:ChangeEntity(val.entity)
obj:SetAngle(a)
local xOffs = 0
local yOffs = 0
if dir == "East" then
obj:SetAxis(axis_y)
obj:SetAngle(90 * 60)
xOffs = 100
elseif dir == "West" then
obj:SetAxis(axis_y)
obj:SetAngle(-90 * 60)
xOffs = -100
elseif dir == "North" then
obj:SetAxis(axis_x)
obj:SetAngle(90 * 60)
yOffs = -100
elseif dir == "South" then
obj:SetAxis(axis_x)
obj:SetAngle(-90 * 60)
yOffs = 100
end
obj:SetPos(wallPos + point(xOffs, yOffs, voxelSizeZ * self.size:z() / 2))
obj.restriction_box = self:CalculateDecalRestrictionBox(dir, wallPos, wallSize)
--DbgAddBox(obj.restriction_box, RGB(255, 0, 0))
self.spawned_decals = self.spawned_decals or {}
self.spawned_decals[dir] = self.spawned_decals[dir] or {}
table.insert(self.spawned_decals[dir], obj)
editor.ClearSel()
editor.AddToSel({obj})
self.place_decal = "temp"
ObjModified(self)
self.place_decal = ""
ObjModified(self)
end
function Room:DeleteWallObjHelper(d)
d:RestoreAffectedSlabs()
DoneObject(d)
ObjModified(self)
end
function Room:DeleteWallObjs(container, dir)
if not dir then
self:DeleteWallObjs("North", container)
self:DeleteWallObjs("South", container)
self:DeleteWallObjs("East", container)
self:DeleteWallObjs("West", container)
self:DeleteAllFloors()
self:DeleteAllCornerObjs()
else
local t = container and container[dir]
for i = #(t or empty_table), 1, -1 do
if IsValid(t[i]) then --can be killed from editor
self:DeleteWallObjHelper(t[i])
end
t[i] = nil
end
ObjModified(self)
end
end
function Room:RebuildAllSlabs()
self:DeleteAllSlabs()
self:CreateAllSlabs()
self:RecreateRoof("force")
end
function Room:DoneObjectsInNWESTable(t)
for k, v in NSEW_pairs(t or empty_table) do
--windows and doors are so clever that they remove themselves from these lists when deleted, which causes DoneObjects to sometimes fail
while #v > 0 do
local idx = #v
DoneObject(v[idx])
v[idx] = nil
end
end
end
function Room:DeleteAllSlabs()
SuspendPassEdits("Room:DeleteAllSpawnedObjs")
self:DeleteAllWallObjs()
self:DeleteAllCornerObjs()
self:DeleteAllFloors()
self:DeleteRoofObjs()
ResumePassEdits("Room:DeleteAllSpawnedObjs")
end
function Room:DeleteAllSpawnedObjs()
SuspendPassEdits("Room:DeleteAllSpawnedObjs")
self:DeleteAllWallObjs()
self:DeleteAllCornerObjs()
self:DeleteAllFloors()
self:DeleteRoofObjs()
self:DoneObjectsInNWESTable(self.spawned_doors)
self:DoneObjectsInNWESTable(self.spawned_windows)
self:DoneObjectsInNWESTable(self.spawned_decals)
ResumePassEdits("Room:DeleteAllSpawnedObjs")
end
function Room:DeleteAllFloors()
SuspendPassEdits("Room:DeleteAllFloors")
DoneObjects(self.spawned_floors, "clear")
ResumePassEdits("Room:DeleteAllFloors")
Msg("RoomDestroyedFloor", self)
end
function Room:DeleteAllCornerObjs()
for k, v in NSEW_pairs(self.spawned_corners or empty_table) do
DoneObjects(v, "clear")
end
end
function Room:DeleteAllWallObjs()
SuspendPassEdits("Room:DeleteAllWallObjs")
for k, v in NSEW_pairs(self.spawned_walls or empty_table) do
DoneObjects(v, "clear")
end
ResumePassEdits("Room:DeleteAllWallObjs")
end
function Room:HasWall(mat)
return mat ~= noneWallMat and (mat ~= defaultWallMat or self.wall_mat ~= noneWallMat)
end
function Room:HasWallOnSide(side)
return self:GetWallMatHelperSide(side) ~= noneWallMat
end
function Room:HasAllWalls()
for _, side in ipairs(CardinalDirectionNames) do
if not self:HasWallOnSide(side) then
return false
end
end
return true
end
function Room:RecreateNWCornerBeam()
local mat = self.north_wall_mat
if mat == noneWallMat then
mat = self.west_wall_mat
end
self:CreateCornerBeam("North", mat) --nw
end
function Room:RecreateSWCornerBeam()
local mat = self.west_wall_mat
if mat == noneWallMat then
mat = self.south_wall_mat
end
self:CreateCornerBeam("West", mat) --sw
end
function Room:RecreateNECornerBeam()
local mat = self.east_wall_mat
if mat == noneWallMat then
mat = self.north_wall_mat
end
self:CreateCornerBeam("East", mat) --ne
end
function Room:RecreateSECornerBeam()
local mat = self.south_wall_mat
if mat == noneWallMat then
mat = self.east_wall_mat
end
self:CreateCornerBeam("South", mat) --se
end
function Room:CreateAllWalls()
SuspendPassEdits("Room:CreateAllWalls")
self:CreateWalls("North", self.north_wall_mat)
self:CreateWalls("South", self.south_wall_mat)
self:CreateWalls("West", self.west_wall_mat)
self:CreateWalls("East", self.east_wall_mat)
self:CheckWallSizes()
ResumePassEdits("Room:CreateAllWalls")
end
function Room:CreateAllSlabs()
SuspendPassEdits("Room:CreateAllSlabs")
self:CreateAllWalls()
self:CreateFloor()
self:CreateAllCorners()
if not self.being_placed then
self:RecreateRoof()
end
self:SetWarped(self:GetWarped(), true)
ResumePassEdits("Room:CreateAllSlabs")
end
function Room:RefreshFloorCombatStatus()
local floorsAreCO = g_Classes.CombatObject and IsKindOf(FloorSlab, "CombatObject")
if not floorsAreCO then return end
local flr = self.floor
local val = not self:IsRoofOnly() and flr == 1
for i = 1, #(self.spawned_floors or "") do
local f = self.spawned_floors[i]
if IsValid(f) then
f.impenetrable = val
f.invulnerable = val
f.forceInvulnerableBecauseOfGameRules = val
end
end
end
function Room:CreateFloor(mat, startI, startJ)
mat = mat or self.floor_mat
self.spawned_floors = self.spawned_floors or {}
local objs = self.spawned_floors
local gz = self:CalcZ()
local sx, sy = self.position:x(), self.position:y()
local sizeX, sizeY, sizeZ = self.size:xyz()
if sizeZ <= 0 then
self:DeleteAllFloors()
print("<color 0 255 38>Removed floor because it is a zero height room. </color>")
return
end
sx = sx + halfVoxelSizeX
sy = sy + halfVoxelSizeY
startI = startI or 0
startJ = startJ or 0
if self:GetGameFlags(const.gofPermanent) ~= 0 then
local floorBBox = box(sx, sy, gz, sx + voxelSizeX * (sizeX - 1), sy + voxelSizeY * (sizeY - 1), gz + 1)
ComputeSlabVisibilityInBox(floorBBox)
end
SuspendPassEdits("Room:CreateFloor")
local insertElements = startJ ~= 0 and #objs < sizeX * sizeY
local floorsAreCO = g_Classes.CombatObject and IsKindOf(FloorSlab, "CombatObject")
local floorsAreInvulnerable = floorsAreCO and not self:IsRoofOnly() and self.floor == 1
for xOffset = startI, sizeX - 1 do
for yOffset = xOffset == startI and startJ or 0, sizeY - 1 do
local x = sx + xOffset * voxelSizeX
local y = sy + yOffset * voxelSizeY
local idx = xOffset * sizeY + yOffset + 1
if insertElements then
if #objs < idx then
objs[idx] = false
else
table.insert(objs, idx, false)
end
insertElements = insertElements and #objs < sizeX * sizeY
end
local floor = objs[idx]
if not IsValid(floor) then
floor = FloorSlab:new{floor = self.floor, material = mat, side = "Floor", room = self}
floor:SetPos(x, y, gz)
floor:AlignObj()
floor:UpdateEntity()
floor:Setcolors(self.floor_colors)
objs[idx] = floor
else
floor:SetPos(x, y, gz)
if floor.material ~= mat then
floor.material = mat
floor:UpdateEntity()
else
floor:UpdateSimMaterialId()
end
end
floor.floor = self.floor
if floorsAreCO then --zulu specific
floor.impenetrable = floorsAreInvulnerable
floor.invulnerable = floorsAreInvulnerable
floor.forceInvulnerableBecauseOfGameRules = floorsAreInvulnerable
end
end
end
ResumePassEdits("Room:CreateFloor")
Msg("RoomCreatedFloor", self, mat)
end
function Room:CreateCornerBeam(dir, mat) --corner is next clockwise corner from dir
self.spawned_corners = self.spawned_corners or {North = {}, South = {}, West = {}, East = {}}
local objs = self.spawned_corners[dir]
if mat == defaultWallMat then
mat = self.wall_mat
end
local gz = self:CalcSnappedZ()
local sx, sy = self.position:x(), self.position:y()
local sizeX, sizeY = self.size:x(), self.size:y()
if dir == "South" or dir == "West" then
sy = sy + sizeY * voxelSizeY
end
if dir == "South" or dir == "East" then
sx = sx + sizeX * voxelSizeX
end
local count = self.size:z() + 1
if count < #objs then
for i = #objs, count + 1, -1 do
DoneObject(objs[i])
objs[i] = nil
end
end
local isPermanent = self:GetGameFlags(const.gofPermanent) ~= 0
local sz = self.size:z()
if sz > 0 then
for j = 0, sz do
local z = gz + voxelSizeZ * Min(j, self.size:z() - 1)
local pt = point(sx, sy, z)
local obj = objs[j + 1]
if not IsValid(obj) then
obj = PlaceObject("RoomCorner", {room = self, side = dir, floor = self.floor, material = mat})
objs[j + 1] = obj
end
obj.isPlug = j == self.size:z()
obj:SetPos(pt)
obj.material = mat
obj.invulnerable = false
obj.forceInvulnerableBecauseOfGameRules = false
if not isPermanent then
obj:UpdateEntity() --corners rely on ComputeSlabVisibilityInBox to update their ents
end
end
end
if isPermanent then
local box = box(sx, sy, gz, sx, sy, gz + voxelSizeZ * (self.size:z() - 1))
ComputeSlabVisibilityInBox(box)
end
end
function Room:GetWallMatHelperSide(side)
local m = dirToWallMatMember[side]
return m and self:GetWallMatHelper(self[m]) or nil
end
function Room:GetWallMatHelper(mat)
return mat == defaultWallMat and self.wall_mat or mat
end
function Room:RecalcAllRestrictionBoxes(dir, containers)
local wallPos, wallSize, center = self:GetWallPos(dir)
for j = 1, #(containers or empty_table) do
local container = containers[j]
local t = container and container[dir]
--save fixup, idk how, sometimes decal lists have false entries
for i = #(t or ""), 1, -1 do
if type(t[i]) == "boolean" then
table.remove(t, i)
print("once", "Found badly saved decals/windows/doors!")
end
end
if t and IsKindOf(t[1], "RoomDecal") then
for i = 1, #(t or empty_table) do
local o = t[i]
if o then
o.restriction_box = self:CalculateDecalRestrictionBox(dir, wallPos, wallSize)
o:AlignObj()
end
end
end
end
end
function Room:Resize(oldSize, newSize, oldBox)
if oldSize == newSize then
return
end
SuspendPassEdits("Room:Resize")
local delta = newSize - oldSize
local offsetY = delta:y() * voxelSizeY
local offsetX = delta:x() * voxelSizeX
local offsetZ = delta:z() * voxelSizeZ
local sx, sy = self.position:x(), self.position:y()
sx = sx + halfVoxelSizeX
sy = sy + halfVoxelSizeY
local sizeX, sizeY = newSize:x(), newSize:y()
local function moveObjs(objs)
if not objs then return end
for i = 1, #objs do
local o = objs[i]
if IsValid(o) then
local x, y, z = o:GetPosXYZ()
o:SetPos(x + offsetX, y + offsetY, z + offsetZ)
end
end
end
local function moveObjX(o)
local x, y, z = o:GetPosXYZ()
o:SetPos(x + offsetX, y, z)
if IsKindOf(o, "SlabWallObject") then
o:UpdateManagedObj()
end
end
local function moveObjsX(objs)
if not objs then return end
for i = 1, #objs do
local o = objs[i]
if IsValid(o) then
moveObjX(o)
end
end
end
local function moveObjY(o)
local x, y, z = o:GetPosXYZ()
o:SetPos(x, y + offsetY, z)
if IsKindOf(o, "SlabWallObject") then
o:UpdateManagedObj()
end
end
local function moveObjsY(objs)
if not objs then return end
for i = 1, #objs do
local o = objs[i]
if IsValid(o) then
moveObjY(o)
end
end
end
local function moveObjsZ(objs)
if not objs then return end
for i = 1, #objs do
local o = objs[i]
if IsValid(o) then
local x, y, z = o:GetPosXYZ()
o:SetPos(x, y, z + offsetZ)
end
end
end
if delta:y() ~= 0 then
--south wall moves
moveObjsY(self.spawned_walls and self.spawned_walls.South)
moveObjsY(self.spawned_doors and self.spawned_doors.South)
moveObjsY(self.spawned_windows and self.spawned_windows.South)
if self.spawned_corners then
moveObjsY(self.spawned_corners.South)
moveObjsY(self.spawned_corners.West)
end
local containers = {self.spawned_doors, self.spawned_windows, self.spawned_decals}
self:RecalcAllRestrictionBoxes("East", containers)
self:RecalcAllRestrictionBoxes("West", containers)
self:RecalcAllRestrictionBoxes("South", containers)
end
if delta:x() ~= 0 then
--east wall moves
moveObjsX(self.spawned_walls and self.spawned_walls.East)
moveObjsX(self.spawned_doors and self.spawned_doors.East)
moveObjsX(self.spawned_windows and self.spawned_windows.East)
if self.spawned_corners then
moveObjsX(self.spawned_corners.South)
moveObjsX(self.spawned_corners.East)
end
local containers = {self.spawned_doors, self.spawned_windows, self.spawned_decals}
self:RecalcAllRestrictionBoxes("North", containers)
self:RecalcAllRestrictionBoxes("East", containers)
self:RecalcAllRestrictionBoxes("South", containers)
end
if delta:z() ~= 0 then
if delta:z() > 0 then
local move = delta:x() ~= 0 or delta:y() ~= 0 --needs to reorder slabs so let it go through the entire wall
self:CreateWalls("South", self.south_wall_mat, nil, not move and oldSize:z(), nil, nil, move)
self:CreateWalls("North", self.north_wall_mat, nil, not move and oldSize:z(), nil, nil, move)
self:CreateWalls("East", self.east_wall_mat, nil, not move and oldSize:z(), nil, nil, move)
self:CreateWalls("West", self.west_wall_mat, nil, not move and oldSize:z(), nil, nil, move)
else
self:DestroyWalls("East", nil, oldSize, nil, newSize:z())
self:DestroyWalls("West", nil, oldSize, nil, newSize:z())
self:DestroyWalls("North", nil, oldSize, nil, newSize:z())
self:DestroyWalls("South", nil, oldSize, nil, newSize:z())
end
end
if delta:y() ~= 0 then
if delta:y() < 0 then
local count = abs(delta:y())
self:DestroyWalls("East", count, oldSize:SetZ(newSize:z()))
self:DestroyWalls("West", count, oldSize:SetZ(newSize:z()))
local floors = self.spawned_floors
for i = oldSize:x() - 1, 0, -1 do
for j = oldSize:y() - 1, newSize:y(), -1 do
local idx = (i * oldSize:y()) + j + 1
local o = floors[idx]
if o then
DoneObject(o)
table.remove(floors, idx)
end
end
end
else
if self.spawned_walls then
local ew = self.spawned_walls.East
local ww = self.spawned_walls.West
if ew then
self:CreateWalls("East", self.east_wall_mat, newSize:y() - delta:y())
end
if ww then
self:CreateWalls("West", self.west_wall_mat, newSize:y() - delta:y())
end
end
self:CreateFloor(self.floor_mat, 0, oldSize:y())
end
end
if delta:x() ~= 0 then
if delta:x() < 0 then
local count = abs(delta:x())
self:DestroyWalls("South", count, oldSize:SetZ(newSize:z()))
self:DestroyWalls("North", count, oldSize:SetZ(newSize:z()))
local floors = self.spawned_floors
local nc = newSize:x() * newSize:y()
local lc = #floors - nc
for i = 1, lc do
local idx = #floors
local f = floors[idx]
DoneObject(f)
floors[idx] = nil
end
else
if self.spawned_walls then
local sw = self.spawned_walls.South
local nw = self.spawned_walls.North
if sw then
self:CreateWalls("South", self.south_wall_mat, newSize:x() - delta:x())
end
if nw then
self:CreateWalls("North", self.north_wall_mat, newSize:x() - delta:x())
end
end
self:CreateFloor(self.floor_mat, oldSize:x())
end
end
if oldSize:z() > 0 and newSize:z() <= 0 then
self:DeleteAllFloors()
self:DestroyCorners()
elseif oldSize:z() <= 0 and newSize:z() > 0 then
self:CreateFloor(self.floor_mat)
end
self:RecreateNECornerBeam()
self:RecreateSECornerBeam()
self:RecreateNWCornerBeam()
self:RecreateSWCornerBeam()
if not self.being_placed then
self:RecreateRoof()
end
ResumePassEdits("Room:Resize")
self:CheckWallSizes()
end
function Room:DestroyCorners()
for side, t in NSEW_pairs(self.spawned_corners or empty_table) do
DoneObjects(t)
self.spawned_corners[side] = {}
end
end
function Room:MoveAllSpawnedObjs(dvx, dvy, dvz)
local offsetX = dvx * voxelSizeX
local offsetY = dvy * voxelSizeY
local offsetZ = dvz * voxelSizeZ
if offsetX == 0 and offsetY == 0 and offsetZ == 0 then
return
end
SuspendPassEdits("Room:MoveAllSpawnedObjs")
local function move(o)
if not IsValid(o) then return end
local x, y, z = o:GetPosXYZ()
o:SetPos(x + offsetX, y + offsetY, z + offsetZ)
--align should not be required, omitted for performance
end
local function move_window(o)
if not IsValid(o) then return end
local x, y, z = o:GetPosXYZ()
o:SetPos(x + offsetX, y + offsetY, z + offsetZ)
o:AlignObj() --specifically so small windows realign managed slabs, t.t
end
local function iterateNSWETable(t, m)
m = m or move
for _, st in NSEW_pairs(t or empty_table) do
for i = 1, #st do
local o = st[i]
m(o)
end
end
end
for i = 1, #(self.spawned_floors or empty_table) do
move(self.spawned_floors[i])
end
iterateNSWETable(self.spawned_walls)
iterateNSWETable(self.spawned_corners)
for i = 1, #(self.roof_objs or empty_table) do
move(self.roof_objs[i])
end
-- move doors & windows after roof_objs, otherwise doors & windows won't find their "main_wall"
iterateNSWETable(self.spawned_doors)
iterateNSWETable(self.spawned_windows, move_window)
iterateNSWETable(self.spawned_decals)
local containers = {self.spawned_doors, self.spawned_windows, self.spawned_decals}
self:RecalcAllRestrictionBoxes("East", containers)
self:RecalcAllRestrictionBoxes("West", containers)
self:RecalcAllRestrictionBoxes("South", containers)
self:RecalcAllRestrictionBoxes("North", containers)
ResumePassEdits("Room:MoveAllSpawnedObjs")
end
function Room:DestroyWalls(dir, count, size, startJ, endJ)
local objs = self.spawned_walls and self.spawned_walls[dir]
local wnd = self.spawned_windows and self.spawned_windows[dir]
local doors = self.spawned_doors and self.spawned_doors[dir]
local len = 0
local offsX = 0
local flatOffsX = 0
local offsY = 0
local flatOffsY = 0
local sx, sy = self.position:x(), self.position:y()
local mat
sx = sx + halfVoxelSizeX
sy = sy + halfVoxelSizeY
size = size or self.size
if dir == "North" then
len = size:x()
mat = self:GetWallMatHelper(self.north_wall_mat)
offsX = voxelSizeX
flatOffsY = -voxelSizeY / 2
elseif dir == "East" then
len = size:y()
mat = self:GetWallMatHelper(self.east_wall_mat)
flatOffsX = voxelSizeX / 2
offsY = voxelSizeY
sx = sx + (size:x() - 1) * voxelSizeX
elseif dir == "South" then
len = size:x()
mat = self:GetWallMatHelper(self.south_wall_mat)
offsX = voxelSizeX
flatOffsY = voxelSizeY / 2
sy = sy + (size:y() - 1) * voxelSizeY
elseif dir == "West" then
len = size:y()
mat = self:GetWallMatHelper(self.west_wall_mat)
flatOffsX = -voxelSizeX / 2
offsY = voxelSizeY
end
startJ = startJ or size:z()
endJ = endJ or 0
count = count or len
local gz = self:CalcZ()
SuspendPassEdits("Room:DestroyWalls")
self:SortWallObjs(doors or empty_table, dir)
self:SortWallObjs(wnd or empty_table, dir)
for i = len - 1, len - count, -1 do
for j = startJ - 1, endJ, -1 do
if endJ == 0 then
--clear wind/door
local px = sx + i * offsX + flatOffsX
local py = sy + i * offsY + flatOffsY
local pz = gz + j * voxelSizeZ
local p = point(px, py, pz)
end
if objs and #objs > 0 then
local idx = i * size:z() + j + 1
local o = objs[idx]
DoneObject(o)
if #objs >= idx then
table.remove(objs, idx)
end
end
end
end
local containers = {self.spawned_decals}
self:RecalcAllRestrictionBoxes(dir, containers)
self:TouchWallsAndWindows(dir)
ResumePassEdits("Room:DestroyWalls")
Msg("RoomDestroyedWall", self, dir)
end
function Room:GetWallSlabPos(dir, idx)
local x, y, z = self.position:xyz()
local sizeX, sizeY, sizeZ = self.size:xyz()
assert(voxelSizeX == voxelSizeY)
local offs = ((idx - 1) / sizeZ) * voxelSizeX + halfVoxelSizeX
z = z + ((idx - 1) % sizeZ) * voxelSizeZ
if dir == "North" then
x = x + offs
elseif dir == "South" then
x = x + offs
y = y + sizeY * voxelSizeY
elseif dir == "West" then
y = y + offs
else --dir == "East"
y = y + offs
x = x + sizeX * voxelSizeX
end
return x, y, z
end
function Room:TestAllWallPositions()
self:TestWallPositions("North")
self:TestWallPositions("South")
self:TestWallPositions("West")
self:TestWallPositions("East")
end
function Room:TestWallPositions(dir)
self.spawned_walls = self.spawned_walls or {North = {}, South = {}, East = {}, West = {}}
dir = dir or "North"
local objs = self.spawned_walls[dir]
local gz = self:CalcZ()
local angle = 0
local sx, sy = self.position:x(), self.position:y()
--local size = oldSize or self.size
local size = self.size
local sizeX, sizeY, sizeZ = size:x(), size:y(), size:z()
local offsx = 0
local offsy = 0
local endI = (dir == "North" or dir == "South") and sizeX or sizeY
local endJ = sizeZ
local startI = 0
local startJ = 0
if dir == "North" then
angle = 270 * 60
offsx = voxelSizeX
sx = sx + halfVoxelSizeX
elseif dir == "East" then
angle = 0
offsy = voxelSizeY
sx = sx + sizeX * voxelSizeX
sy = sy + halfVoxelSizeY
elseif dir == "South" then
angle = 90 * 60
offsx = voxelSizeX
sy = sy + sizeY * voxelSizeY
sx = sx + halfVoxelSizeX
elseif dir == "West" then
angle = 180 * 60
offsy = voxelSizeY
sy = sy + halfVoxelSizeY
end
local insertElements = startJ ~= 0 and #objs < ((dir == "North" or dir == "South") and sizeX or sizeY) * sizeZ
for i = startI, endI - 1 do
for j = startJ, endJ - 1 do
local px = sx + i * offsx
local py = sy + i * offsy
local z = gz + j * voxelSizeZ
local idx = i * sizeZ + j + 1
local s = objs[idx]
if not s or s:GetPos() ~= point(px, py, z) then
print(dir, idx)
end
end
end
end
function Room:CreateWalls(dir, mat, startI, startJ, endI, endJ, move)
self.spawned_walls = self.spawned_walls or {North = {}, South = {}, East = {}, West = {}}
mat = mat or "Planks"
dir = dir or "North"
local objs = self.spawned_walls[dir]
if mat == defaultWallMat then
mat = self.wall_mat
end
local oppositeDir = nil
local gz = self:CalcZ()
local angle = 0
local sx, sy = self.position:x(), self.position:y()
local size = self.size
local sizeX, sizeY, sizeZ = size:x(), size:y(), size:z()
local offsx = 0
local offsy = 0
endI = endI or (dir == "North" or dir == "South") and sizeX or sizeY
endJ = endJ or sizeZ
startI = startI or 0
startJ = startJ or 0
if dir == "North" then
angle = 270 * 60
offsx = voxelSizeX
sx = sx + halfVoxelSizeX
oppositeDir = "South"
elseif dir == "East" then
angle = 0
offsy = voxelSizeY
sx = sx + sizeX * voxelSizeX
sy = sy + halfVoxelSizeY
oppositeDir = "West"
elseif dir == "South" then
angle = 90 * 60
offsx = voxelSizeX
sy = sy + sizeY * voxelSizeY
sx = sx + halfVoxelSizeX
oppositeDir = "North"
elseif dir == "West" then
angle = 180 * 60
offsy = voxelSizeY
sy = sy + halfVoxelSizeY
oppositeDir = "East"
end
if self:GetGameFlags(const.gofPermanent) ~= 0 then
local wallBBox = self:GetWallBox(dir)
ComputeSlabVisibilityInBox(wallBBox)
end
SuspendPassEdits("Room:CreateWalls")
local insertElements = startJ ~= 0 and #objs < ((dir == "North" or dir == "South") and sizeX or sizeY) * sizeZ
local forceUpdate = self.last_wall_recreate_seed ~= self.seed
local isLoadingMap = IsChangingMap()
local affectedRooms = {}
for i = startI, endI - 1 do
for j = startJ, endJ - 1 do
local px = sx + i * offsx
local py = sy + i * offsy
local z = gz + j * voxelSizeZ
local idx = i * sizeZ + j + 1
local m = mat
if insertElements then
if idx > #objs then
objs[idx] = false --Happens when resizing in (x or y) ~= 0 and z ~= 0 in the same time. Fill it in, second pass will fill in the missing ones without breaking integrity, probably..
else
table.insert(objs, idx, false)
end
end
local wall = objs[idx]
if not IsValid(wall) then
wall = WallSlab:new{floor = self.floor, material = m, room = self, side = dir,
variant = self.inner_wall_mat ~= noneWallMat and "OutdoorIndoor" or "Outdoor", indoor_material_1 = self.inner_wall_mat}
wall:SetAngle(angle)
wall:SetPos(px, py, z)
wall:AlignObj()
wall:UpdateEntity()
wall:UpdateVariantEntities()
wall:Setcolors(self.outer_colors)
wall:Setinterior_attach_colors(self.inner_colors)
wall.invulnerable = false
wall.forceInvulnerableBecauseOfGameRules = false
objs[idx] = wall
else
if move then
--sometimes if we change both z and x or y sizing at the same time we end up with the same number of slabs per wall but they need to be re-arranged.
wall:SetAngle(angle)
local op = wall:GetPos()
wall:SetPos(px, py, z)
if op ~= wall:GetPos() and wall.wall_obj then
local o = wall.wall_obj
o:RestoreAffectedSlabs()
end
wall:AlignObj()
end
wall:UpdateSimMaterialId()
end
if not isLoadingMap then
if forceUpdate or wall.material ~= m or wall.indoor_material_1 ~= self.inner_wall_mat then
wall.material = m
wall.indoor_material_1 = self.inner_wall_mat
wall:UpdateEntity()
wall:UpdateVariantEntities()
end
end
end
end
affectedRooms[self] = nil
for room, isMySide in pairs(affectedRooms) do
if IsValid(room) then
local d = isMySide and dir or oppositeDir
room:TouchWallsAndWindows(d)
room:TouchCorners(d)
end
end
self:TouchWallsAndWindows(dir)
self:TouchCorners(dir)
ResumePassEdits("Room:CreateWalls")
Msg("RoomCreatedWall", self, dir, mat)
end
local postComputeBatch = false
function Room:SetInnerMaterialToRoofObjs()
local objs = self.roof_objs
if not objs or #objs <= 0 then return end
local passedSWO = {}
local col = self.inner_colors
for i = 1, #objs do
local o = objs[i]
if IsValid(o) and IsKindOf(o, "WallSlab") then
if o.indoor_material_1 ~= self.inner_wall_mat then
o.indoor_material_1 = self.inner_wall_mat
o:UpdateVariantEntities()
end
o:Setinterior_attach_colors(col)
local swo = o.wall_obj
if swo and not passedSWO[swo] then
passedSWO[swo] = true
end
end
end
if next(passedSWO) then
--in some cases slabs need to recalibrate in computeslabvisibility, we need to pass after that
postComputeBatch = postComputeBatch or {}
postComputeBatch[#postComputeBatch + 1] = passedSWO
end
ComputeSlabVisibilityInBox(self.roof_box)
end
function Room:SetInnerMaterialToSlabs(dir)
local objs = self.spawned_walls and self.spawned_walls[dir]
local gz = self:CalcZ()
local sizeX, sizeY = self.size:x(), self.size:y()
local endI = (dir == "North" or dir == "South") and sizeX or sizeY
local passedSWO = {}
local wallBBox = box()
local col = self.inner_colors
if objs then
for i = 0, endI - 1 do
for j = 0, self.size:z() - 1 do
local idx = i * self.size:z() + j + 1
local o = objs[idx]
if IsValid(o) then
wallBBox = Extend(wallBBox, o:GetPos())
if o.indoor_material_1 ~= self.inner_wall_mat then
o.indoor_material_1 = self.inner_wall_mat
o:UpdateVariantEntities()
end
o:Setinterior_attach_colors(col)
local swo = o.wall_obj
if swo and not passedSWO[swo] then
passedSWO[swo] = true
end
end
end
end
end
objs = self.spawned_corners[dir]
for i = 1, #(objs or "") do
if IsValid(objs[i]) then --can be gone
objs[i]:SetColorFromRoom()
end
end
if next(passedSWO) then
--in some cases slabs need to recalibrate in computeslabvisibility, we need to pass after that
postComputeBatch = postComputeBatch or {}
postComputeBatch[#postComputeBatch + 1] = passedSWO
end
if self:GetGameFlags(const.gofPermanent) ~= 0 then
ComputeSlabVisibilityInBox(wallBBox)
end
self:CreateAllCorners()
end
function OnMsg.SlabVisibilityComputeDone()
if not postComputeBatch then return end
local allPassed = {}
for i, batch in ipairs(postComputeBatch) do
for swo, _ in pairs(batch) do
if not allPassed[swo] then
allPassed[swo] = true
swo:UpdateManagedSlabs()
swo:UpdateManagedObj()
swo:RefreshColors()
end
end
end
postComputeBatch = false
end
function TouchWallsAndWindowsHelper(objs)
if not objs then return end
table.validate(objs)
for i = #(objs or empty_table), 1, -1 do
local o = objs[i]
o:AlignObj()
if o.room == false then
DoneObject(o)
else
o:UpdateSimMaterialId()
end
end
end
function Room:TouchWallsAndWindows(side)
TouchWallsAndWindowsHelper(self.spawned_doors and self.spawned_doors[side])
TouchWallsAndWindowsHelper(self.spawned_windows and self.spawned_windows[side])
end
function Room:TouchCorners(side)
if side == "North" then
self:RecreateNECornerBeam()
self:RecreateNWCornerBeam()
elseif side == "South" then
self:RecreateSECornerBeam()
self:RecreateSWCornerBeam()
elseif side == "East" then
self:RecreateSECornerBeam()
self:RecreateNECornerBeam()
elseif side == "West" then
self:RecreateSWCornerBeam()
self:RecreateNWCornerBeam()
end
end
function GedOpViewRoom(socket, obj)
if IsValid(obj) then
Room.CenterCameraOnMe(nil, obj)
else
print("No room selected.")
end
end
function GedOpNewVolume(socket, obj)
print("Use f3 -> map -> new room or ctrl+shift+n instead. This method is no longer supported.")
end
function Room:SelectWall(side)
self:ClearBoldedMarker()
self.selected_wall = side
local m = self.text_markers[side]
m:SetTextStyle("EditorTextBold")
m:SetColor(RGB(0, 255, 0))
ObjModified(self)
end
function Room:ViewNorthWallFromOutside()
self:SelectWall("North")
self:ViewWall("North")
ObjModified(self)
end
function Room:ViewSouthWallFromOutside()
self:SelectWall("South")
self:ViewWall("South")
ObjModified(self)
end
function Room:ViewWestWallFromOutside()
self:SelectWall("West")
self:ViewWall("West")
ObjModified(self)
end
function Room:ViewEastWallFromOutside()
self:SelectWall("East")
self:ViewWall("East")
ObjModified(self)
end
function Room:ClearBoldedMarker()
if self.selected_wall then
local m = self.text_markers[self.selected_wall]
m:SetTextStyle("EditorText")
m:SetColor(RGB(255, 0, 0))
end
end
function Room:ClearSelectedWall()
self:ClearBoldedMarker()
self.selected_wall = false
ObjModified(self)
end
function GetSelectedRoom()
--find a selected room..
return SelectedRooms and SelectedRooms[1] or SelectedVolume
end
function SelectedRoomClearSelectedWall()
local r = GetSelectedRoom()
if IsValid(r) then
r:ClearSelectedWall()
print("Cleared selected wall")
else
print("No selected room found!")
end
end
function SelectedRoomSelectWall(side)
local r = GetSelectedRoom()
if IsValid(r) then
r:SelectWall(side)
print(string.format("Selected wall %s of room %s", side, r.name))
else
print("No selected room found!")
end
end
function SelectedRoomResetWallMaterials()
local r = GetSelectedRoom()
if IsValid(r) then
if r.selected_wall then
local side = r.selected_wall
local matMember = string.format("%s_wall_mat", string.lower(side))
local curMat = r[matMember]
if curMat ~= defaultWallMat then
r[matMember] = defaultWallMat
local matPostSetter = string.format("OnSet%s", matMember)
r[matPostSetter](r, defaultWallMat, curMat)
end
else
r:ResetWallMaterials()
print(string.format("Reset wall materials."))
end
else
print("No selected room found!")
end
end
function Room:CycleWallMaterial(delta, side)
local mats
local matMember = "wall_mat"
if side then
mats = SlabMaterialComboItemsWithDefault()()
matMember = string.format("%s_wall_mat", string.lower(side))
else
mats = SlabMaterialComboItemsWithNone()()
end
local matPostSetter = string.format("OnSet%s", matMember)
local curMat = self[matMember]
local idx = table.find(mats, curMat) or 1
local newIdx = idx + delta
if newIdx > #mats then
newIdx = 1
elseif newIdx <= 0 then
newIdx = #mats
end
local newMat = mats[newIdx]
self[matMember] = newMat
self[matPostSetter](self, newMat, curMat)
print(string.format("Changed wall material of room %s side %s new material %s", self.name, side or "all", newMat))
end
function Room:CycleEntity(delta)
local sw = self.selected_wall
if not sw then
self:CycleWallMaterial(delta)
return
end
self:CycleWallMaterial(delta, sw)
end
function Room:UIDeleteDoors()
self:DeleteWallObjs(self.spawned_doors, self.selected_wall)
end
function Room:UIDeleteWindows()
self:DeleteWallObjs(self.spawned_windows, self.selected_wall)
end
local decalIdPrefix = "decal_lst_"
function Room:UIDeleteDecal(gedRoot, prop_id)
local sh = string.gsub(prop_id, decalIdPrefix, "")
local h = tonumber(sh)
local t = self.spawned_decals[self.selected_wall]
local idx = table.find(t, "handle", h)
if idx then
local d = t[idx]
table.remove(t, idx)
rawset(d, "safe_deletion", true)
DoneObject(d)
ObjModified(self)
end
end
function Room:UISelectDecal(gedRoot, prop_id)
local sh = string.gsub(prop_id, decalIdPrefix, "")
local h = tonumber(sh)
local t = self.spawned_decals[self.selected_wall]
local idx = table.find(t, "handle", h)
if idx then
local d = t[idx]
if d then
editor.ClearSel()
editor.AddToSel({d})
end
end
end
function Room:GetWallPos(dir, zOffset)
local wallSize, wallPos
local wsx = self.size:x() * voxelSizeX
local wsy = self.size:y() * voxelSizeY
local pos = self:GetPos()
if zOffset then
pos = pos:SetZ(pos:z() + zOffset)
end
if dir == "North" then
wallPos = point(pos:x(), pos:y() - wsy / 2, pos:z())
wallSize = wsx
elseif dir == "South" then
wallPos = point(pos:x(), pos:y() + wsy / 2, pos:z())
wallSize = wsx
elseif dir == "West" then
wallPos = point(pos:x() - wsx / 2, pos:y(), pos:z())
wallSize = wsy
elseif dir == "East" then
wallPos = point(pos:x() + wsx / 2, pos:y(), pos:z())
wallSize = wsy
end
return wallPos, wallSize, pos
end
function Room:ViewWall(dir, inside)
dir = dir or "North"
local wallPos, wallSize, pos = self:GetWallPos(dir, self.size:z() * voxelSizeZ / 2)
--fit wall to screen
local fovX = camera.GetFovX()
local a = (180 * 60 - fovX) / 2
local wallWidth = wallSize
local s = MulDivRound(wallWidth, sin(a), sin(fovX))
local x = wallWidth / 2
local dist = sqrt(s * s - x * x)
local fovY = camera.GetFovY()
a = (180 * 60 - fovY) / 2
local wallHeight = self.size:z() * voxelSizeZ
s = MulDivRound(wallHeight, sin(a), sin(fovY))
x = wallHeight / 2
dist = Max(sqrt(s * s - x * x), dist)
local offset
if inside then
offset = pos - wallPos
else
offset = wallPos - pos
end
dist = dist + 3 * guim --some x margin so wall edge is not stuck to the screen edge
offset = SetLen(offset, dist)
offset = offset:SetZ(offset:z() + self.size:z() * voxelSizeZ * 3) --move eye up a little bit
local cPos, cLookAt, cType = GetCamera()
local cam = _G[string.format("camera%s", cType)]
cam.SetCamera(wallPos + offset, wallPos, 1000, "Cubic out")
if rawget(terminal, "BringToTop") then
return terminal.BringToTop()
end
end
function Room.CenterCameraOnMe(_, self)
local cPos, cLookAt, cType = GetCamera()
local cOffs = cPos - cLookAt
local mPos = self:GetPos()
local cam = _G[string.format("camera%s", cType)]
if cType == "Max" then
local len = 20*guim * ((Max(self.size:x(), self.size:y()) / 10) + 1)
cOffs = SetLen(cOffs, len)
end
cam.SetCamera(mPos + cOffs, mPos, 1000, "Cubic out")
if rawget(terminal, "BringToTop") then
return terminal.BringToTop()
end
end
local defaultDecalProp = { category = "Materials", id = decalIdPrefix, name = "Decal ", editor = "text", default = "", read_only = true,
buttons = {
{name = "Delete", func = "UIDeleteDecal"},
{name = "Select", func = "UISelectDecal"},
},}
local function AddDecalPropsFromContainerHelper(self, props, container, idx, defaultProp)
for i = 1, #container do
local np = table.copy(defaultProp)
local obj = container[i]
np.id = string.format("%s%s", np.id, obj.handle)
np.name = string.format("%s%s", np.name, obj:GetEntity())
table.insert(props, idx + i, np)
end
end
function Room:GetProperties()
local decals = self.spawned_decals and self.spawned_decals[self.selected_wall]
if #(decals or empty_table) > 0 then
local p = table.copy(self.properties)
if decals then
local idx = table.find(p, "id", "place_decal")
AddDecalPropsFromContainerHelper(self, p, decals, idx, defaultDecalProp)
end
return p
else
return self.properties
end
end
function Room:GenerateName()
return string.format("Room %d%s", self.handle, self:IsRoofOnly() and " - Roof only" or "")
end
function Room:Init()
self.name = self.name or self:GenerateName()
if self.auto_add_in_editor then
self:AddInEditor()
end
end
function ComputeVisibilityOfNearbyShelters()
--stub
end
function Room:ClearRoomAdjacencyData()
self:ClearAdjacencyData()
end
function Room:RoomDestructor()
Msg("RoomDone", self)
local wasPermanent = self:GetGameFlags(const.gofPermanent) ~= 0
self:ClearGameFlags(const.gofPermanent) --this is to dodge this assert -> assert(false, "Passability rebuild provoked from destructor! Obj class: " .. (obj.class or "N/A"))
self:DeleteAllSpawnedObjs()
self:ClearRoomAdjacencyData()
if GedRoomEditor then
table.remove_entry(GedRoomEditorObjList, self)
ObjModified(GedRoomEditorObjList)
if SelectedVolume == self then
SetSelectedVolumeAndFireEvents(false)
--todo: this does not work to clear the props pane
GedRoomEditor:UnbindObjs("SelectedObject")
ObjModified(GedRoomEditorObjList)
end
end
if wasPermanent then
ComputeSlabVisibilityInBox(self.box) --since we are no longer gofPermanent, call directly
ComputeVisibilityOfNearbyShelters(self.box)
end
self["RoomDestructor"] = empty_func
end
function Room:ComputeRoomVisibility()
if self:GetGameFlags(const.gofPermanent) ~= 0 then
ComputeSlabVisibilityInBox(self.box)
end
end
function Room:OnEditorDelete()
self:VolumeDestructor()
self:RoomDestructor()
end
function Room:Done()
self:RoomDestructor()
self.spawned_walls = nil
self.spawned_corners = nil
self.spawned_floors = nil
self.spawned_doors = nil
self.spawned_windows = nil
self.spawned_decals = nil
end
function Room:AddInEditor()
if GedRoomEditor then
table.insert_unique(GedRoomEditorObjList, self)
ObjModified(GedRoomEditorObjList)
end
end
function WallObjToNestedListEntry(d, cls)
cls = cls or "DoorNestedListEntry"
local entry = PlaceObject(cls)
entry.linked_obj = d
entry.width = d.width
entry.material = d.material
entry.subvariant = d.subvariant or 1
return entry
end
function Room:TestCorners()
for k, v in NSEW_pairs(self.spawned_corners or empty_table) do
for i = 1, #v do
if not IsValid(v[i]) then
print(k, v[i])
end
end
end
end
function Room:AssignPropValuesToMySlabs()
-- assign prop values that are convenient to have per slab but are not saved per slab
local reposition = RepositionWallSlabsOnLoad
for i = 1, #room_NSWE_lists do
for side, objs in NSEW_pairs(self[room_NSWE_lists[i]] or empty_table) do
local isDecals = "spawned_decals" == room_NSWE_lists[i]
local isCorners = "spawned_corners" == room_NSWE_lists[i]
local isWalls = "spawned_walls" == room_NSWE_lists[i]
local isFloors = "spawned_floors" == room_NSWE_lists[i]
local isWindows = "spawned_windows" == room_NSWE_lists[i]
local isDoors = "spawned_doors" == room_NSWE_lists[i]
for j = 1, #objs do
local obj = objs[j]
if obj then
obj.room = self
obj.side = side
obj.floor = self.floor
if not isDecals then -- could be a decal from spawned_decals
obj.invulnerable = obj.forceInvulnerableBecauseOfGameRules
end
if isCorners and j == #objs then
obj.isPlug = true -- last one is always a plug
end
if isWalls or isCorners then
obj:DelayedUpdateEntity() -- call this after room is set for the correct seed
end
if isWalls then
obj:DelayedUpdateVariantEntities() -- default colors are not available 'till .room gets assigned, this will reapply them to attaches
end
if reposition and isWalls then
obj:SetPos(self:GetWallSlabPos(side, j))
end
end
end
end
end
local floorsAreCO = g_Classes.CombatObject and IsKindOf(FloorSlab, "CombatObject")
for i = 1, #room_regular_lists do
local isFloors = room_regular_lists[i] == "spawned_floors"
local t = self[room_regular_lists[i]] or empty_table
local side = room_regular_list_sides[i]
for j = #t, 1, -1 do
local o = t[j]
if o then
o.room = self
o.side = side
o.floor = self.floor
o.invulnerable = o.forceInvulnerableBecauseOfGameRules
if isFloors then
o:DelayedUpdateEntity() -- these now have random ents as well, so rerandomize after seed is setup
end
end
end
end
end
function Room:CreateAllCorners()
self:RecreateNECornerBeam()
self:RecreateNWCornerBeam()
self:RecreateSWCornerBeam()
self:RecreateSECornerBeam()
end
-- used manually when resaving maps from old schema to new shcema
function RecreateAllCornersAndColors()
MapForEach("map", "RoomCorner", DoneObject)
MapForEach("map", "Room", function(room)
room:RecreateWalls()
room:RecreateFloor()
room:RecreateRoof()
room:OnSetouter_colors(room.outer_colors)
room:OnSetinner_colors(room.inner_colors)
end)
end
function RefreshAllRoomColors()
MapForEach("map", "Room", function(room)
room:OnSetouter_colors(room.outer_colors)
room:OnSetinner_colors(room.inner_colors)
end)
end
function Room:SaveFixups()
local hasCeiling = type(self.roof_objs) == "table" and IsKindOf(self.roof_objs[#self.roof_objs], "CeilingSlab") or false
if not self.build_ceiling and hasCeiling then
-- tweaked this default value, so now there are blds with disabled ceiling who have ceilings
-- because we avoid touching roofs during load they remain
while IsKindOf(self.roof_objs[#self.roof_objs], "CeilingSlab") do
local o = self.roof_objs[#self.roof_objs]
self.roof_objs[#self.roof_objs] = nil
DoneObject(o)
end
end
end
function Room:Getlocked_slabs_count()
local total, locked = 0, 0
local function iterateAndCount(t)
for i = 1, #(t or "") do
local slab = t[i]
if IsValid(slab) and slab.isVisible then
total = total + 1
locked = locked + (slab.subvariant ~= -1 and 1 or 0)
end
end
end
local function iterateAndCountNSEW(objs)
for side, t in NSEW_pairs(objs or empty_table) do
iterateAndCount(t)
end
end
iterateAndCountNSEW(self.spawned_walls)
local ws = string.format("%d/%d walls", locked, total)
locked, total = 0, 0
iterateAndCountNSEW(self.spawned_corners)
local cs = string.format("%d/%d corners", locked, total)
locked, total = 0, 0
iterateAndCount(self.spawned_floors)
local fs = string.format("%d/%d floors", locked, total)
locked, total = 0, 0
iterateAndCount(self.roof_objs)
local rs = string.format("%d/%d roof objs", locked, total)
return string.format("%s; %s; %s; %s;", ws, cs, fs, rs)
end
function Room:LockAllSlabsToCurrentSubvariants()
-- goes through all slabs and switches -1 subvariant val to their current subvariant.
-- this will lock those variants in case of random generator changes
local function iterateAndSet(t)
for i = 1, #(t or "") do
local slab = t[i]
if IsValid(slab) and slab.isVisible then
slab:LockSubvariantToCurrentEntSubvariant()
end
end
end
local function iterateAndSetNSEW(objs)
for side, t in NSEW_pairs(objs or empty_table) do
iterateAndSet(t)
end
end
iterateAndSetNSEW(self.spawned_walls)
iterateAndSetNSEW(self.spawned_corners)
iterateAndSet(self.spawned_floors)
iterateAndSet(self.roof_objs)
ObjModified(self)
end
local function extractCpyId(str)
local r = string.gmatch(str, "copy%d+")()
return r and tonumber(string.gmatch(r, "%d+")()) or 0
end
function Room:GenerateNameWithCpyTag()
local n = self.name
local pid = extractCpyId(n)
local topId = pid
EnumVolumes(function(v, n, find, sub)
local hn = v.name
local mn = n
if #hn < #mn then
mn = string.sub(mn, 1, #hn)
elseif #hn > #mn then
hn = string.sub(hn, 1, #mn)
end
if hn == mn then
local hpid = extractCpyId(v.name)
if hpid > topId then
topId = hpid
end
end
end, string.gsub(n, " copy%d+", ""), string.find, string.sub)
local tag = string.format("copy%d", tonumber(topId) + 1)
if pid == 0 then
return string.format("%s %s", self.name, tag)
else
return string.gsub(self.name, "copy%d+", tag)
end
end
function Room:PostLoad(reason)
if reason == "paste" then
self:Setname(self:GenerateNameWithCpyTag())
end
SuspendPassEdits("Room:PostLoad")
self:SaveFixups()
self:InternalAlignObj()
self:SetWarped(self:GetWarped(), true)
self:AssignPropValuesToMySlabs() -- sets slab properties that are not saved - .room, .side, etc.
self:RecalcRoof()
self:ComputeRoomVisibility()
ResumePassEdits("Room:PostLoad")
end
function Room:OnWallObjDeletedOutsideOfGedRoomEditor(obj)
local dir = slabAngleToDir[obj:GetAngle()]
local t = self[obj:IsDoor() and string.format("placed_doors_nl_%s", string.lower(dir)) or string.format("placed_windows_nl_%s", string.lower(dir))]
local container = obj:IsDoor() and self.spawned_doors or self.spawned_windows
if container then
for i = 1, #(t or empty_table) do
if t[i].linked_obj == obj then
DoneObject(t[i])
table.remove(t, i)
table.remove_entry(container[dir], obj)
return
end
end
elseif Platform.developer then
local cs = obj:IsDoor() and "spawned_doors" or "spawned_windows"
local dirFound
local r = MapGetFirst("map", "Room", function(o, cs, obj, et)
for side, t in sorted_pairs(o[cs] or et) do
if table.find(t or et, obj) then
dirFound = side
return true
end
end
end, cs, obj, empty_table)
if r then
print(string.format("Wall obj was found in room %s, side %s container", r.name, dirFound))
else
print("Wall obj was not found in any room container")
end
assert(false, string.format("Room %s had no contaier initialized for wall obj with entity %s, deduced side %s, member side %s", self.name, obj.entity, dir, obj.side))
end
end
local dirs = { "North", "East", "South", "West" }
function rotate_direction(direction, angle)
local idx = table.find(dirs, direction)
if not idx then return direction end
idx = idx + angle / (90 * 60)
if idx > 4 then idx = idx - 4 end
return dirs[idx]
end
function Room:EditorRotate(center, axis, angle, last_angle)
angle = angle - last_angle
if axis:z() < 0 then angle = -angle end
angle = (angle + 360 * 60 + 45 * 60 ) / (90 * 60) * (90 * 60)
while angle >= 360 * 60 do angle = angle - 360 * 60 end
if axis:z() == 0 or angle == 0 then return end
-- rotate room properties
local a = center + Rotate(self.box:min() - center, angle)
local b = center + Rotate(self.box:max() - center, angle)
self.box = boxdiag(a, b)
self.position = self.box:min()
local x, y, z = self.box:size():xyz()
self.size = point(x / voxelSizeX, y / voxelSizeY, z / voxelSizeZ)
-- rotate roof properties
if self:GetRoofType() == "Gable" then
if angle == 90 * 60 or angle == 270 * 60 then
self.roof_direction = self.roof_direction == GableRoofDirections[1] and GableRoofDirections[2] or GableRoofDirections[1]
end
elseif self:GetRoofType() == "Shed" then
self.roof_direction = rotate_direction(self.roof_direction, angle)
end
-- rotate slabs
self:ForEachSpawnedObj(function(obj, center, angle)
local new_angle = 0
if not IsKindOf(obj, "FloorAlignedObj") then
new_angle = obj:GetAngle() + angle
end
obj:SetPosAngle(center + Rotate(obj:GetPos() - center, angle), new_angle)
obj.side = rotate_direction(obj.side, angle)
if obj:IsKindOf("SlabWallObject") then obj:UpdateManagedObj() end
end, center, angle)
-- assign slabs to the proper lists after rotation
local d = table.copy(dirs)
while angle >= 90 * 60 do
d[1], d[2], d[3], d[4] = d[2], d[3], d[4], d[1]
angle = angle - 90 * 60
end
for i = 1, #room_NSWE_lists do
local lists = self[room_NSWE_lists[i]]
if lists then
lists[d[1]], lists[d[2]], lists[d[3]], lists[d[4]] = lists.North, lists.East, lists.South, lists.West
end
end
self:InternalAlignObj()
self:RecreateRoof()
end
function OnMsg.GedClosing(ged_id)
if GedRoomEditor and GedRoomEditor.ged_id == ged_id then
GedRoomEditor = false
GedRoomEditorObjList = false
end
end
function OnMsg.GedOnEditorSelect(obj, selected, editor)
if editor == GedRoomEditor then
SetSelectedVolumeAndFireEvents(selected and obj or false)
end
end
function OpenGedRoomEditor()
CreateRealTimeThread(function()
if not IsValid(GedRoomEditor) then
GedRoomEditorObjList = MapGet("map", "Room") or {}
table.sortby_field(GedRoomEditorObjList, "name")
table.sortby_field(GedRoomEditorObjList, "structure")
GedRoomEditor = OpenGedApp("GedRoomEditor", GedRoomEditorObjList) or false
end
end)
end
function OnMsg.ChangeMap()
if GedRoomEditor then
GedRoomEditor:Send("rfnClose")
GedRoomEditor = false
end
end
--------------------------------------------------------------------------
--------------------------------------------------------------------------
--------------------------------------------------------------------------
DefineClass.SlabPreset = {
__parents = { "Preset", },
properties = {
{ id = "Group", no_edit = false, },
},
HasSortKey = false,
PresetClass = "SlabPreset",
NoInstances = true,
EditorMenubarName = "Slab Presets",
EditorMenubar = "Editors.Art",
}
DefineClass.SlabMaterialSubvariant = {
__parents = {"PropertyObject"},
properties = {
{ id = "suffix", name = "Suffix", editor = "text", default = "01" },
{ id = "chance", name = "Chance", editor = "number", default = 100 },
},
}
--------------------------------------------------------------------------
--------------------------------------------------------------------------
--------------------------------------------------------------------------
function TouchAllRoomCorners()
MapForEach("map", "Room", function(o)
o:TouchCorners("North")
o:TouchCorners("South")
o:TouchCorners("West")
o:TouchCorners("East")
end)
end
DefineClass.HideOnFloorChange = {
__parents = { "Object" },
properties = {
{ id = "floor", name = "Floor", editor = "number", min = -10, max = 100, default = 1, dont_save = function (obj) return obj.room end },
},
room = false,
invisible_reasons = false,
}
function HideOnFloorChange:Getfloor()
local room = self.room
return room and room.floor or self.floor
end
HideSlab = false -- used only if defined
function HideFloorsAbove(floor, fnHide)
SuspendPassEdits("HideFloorsAbove")
HideFloorsAboveC(floor, fnHide or HideSlab or nil)
Msg("FloorsHiddenAbove", floor, fnHide)
ResumePassEdits("HideFloorsAbove")
end
function CountRoomSlabs()
local t = 0
MapForEach("map", "Room", function(o)
t = t + (o.size:x() + o.size:y()) * 2 * o.size:z()
end)
return t
end
function CountMirroredSlabs()
local t, tm = 0, 0
MapForEach("map", "WallSlab", function(o)
if o:CanMirror() and o:GetEnumFlags(const.efVisible) ~= 0 then
if o:GetGameFlags(const.gofMirrored) ~= 0 then
tm = tm + 1
else
t = t + 1
end
end
end)
return t, tm
end
function BuildBuildingsData()
end
function DbgWindowDoorOwnership()
MapForEach("map", "SlabWallObject", function(o)
if o.room then
DbgAddVector(o:GetPos(), o.room:GetPos() - o:GetPos())
else
DbgAddVector(o:GetPos())
end
end)
end