if FirstLoad then RoofVisualsEnabled = true GableRoofDirections = { "North-South", "East-West" } end 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 local noneWallMat = const.SlabNoMaterial CardinalDirectionNames = { "East", "South", "West", "North" } local cardinal_direction_names = CardinalDirectionNames local cardinal_directions = { 0 * 60, 90 * 60, 180 * 60, 270 * 60, East = 0 * 60, South = 90 * 60, West = 180 * 60, North = 270 * 60, } local cardinal_steps = {point(voxelSizeX, 0, 0), point(0, voxelSizeY, 0), point(-voxelSizeX, 0, 0), point(0, -voxelSizeY, 0)} local cardinal_offsets = {{voxelSizeX, 0, 0}, {0, voxelSizeY, 90 * 60}, {-voxelSizeX, 0, 180 * 60}, {0, -voxelSizeY, 270 * 60}} function GetCardinalOffsets() return cardinal_offsets end DefineClass.SkewAlign = { __parents = { "CObject" }, flags = { cfSkewAlign = true, }, } DefineClass.RoomRoof = { __parents = { "PropertyObject" }, properties = { { category = "Roof", id = "roof_type", name = "Roof Type", editor = "preset_id", default = "", preset_class = "RoofTypes" }, { category = "Roof", id = "roof_mat", name = "Roof Material", editor = "preset_id", preset_class = "SlabPreset", preset_group = "RoofSlabMaterials", default = "" }, { category = "Roof", id = "roof_parapet", name = "Roof Parapet", editor = "bool", default = false }, { category = "Roof", id = "roof_direction", name = "Roof Direction", editor = "dropdownlist", items = function(obj) return obj:IsAnyOfRoofTypes("Gable") and GableRoofDirections or cardinal_direction_names end, default = "North", no_edit = function(obj) return not obj:IsAnyOfRoofTypes("Shed", "Gable") end }, { category = "Roof", id = "roof_inclination", name = "Roof Inclination", editor = "number", scale = "deg", default = 20 * 60, min = 0*60, max = 45*60, slider = true, no_edit = function(obj) return not obj:IsAnyOfRoofTypes("Shed", "Gable") end }, { category = "Roof", id = "roof_additional_height", name = "Additional Height", editor = "number", scale = "m", default = 0, min = 0, max = const.SlabSizeZ, slider = true }, { category = "Roof", name = "Has Ceiling", id = "build_ceiling", editor = "bool", default = true, }, { category = "Roof", name = "Ceiling Material", id = "ceiling_mat", editor = "preset_id", preset_class = "SlabPreset", preset_group = "FloorSlabMaterials", extra_item = noneWallMat, default = noneWallMat, }, { category = "Roof", id = "roof_colors", name = "Roof Color Modifier", editor = "nested_obj", base_class = "ColorizationPropSet", inclusive = true, default = false, }, { category = "Not Room Specific", id = "roofVisualsEnabled", name = "Toggle Roof Visuals", default = true, editor = "bool", dont_save = true }, { id = "roof_objs", no_edit = true, editor = "objects", default = false, }, { category = "Debug", id = "is_roof_visible", editor = "bool", default = true, dont_save = true, read_only = true }, { id = "RoofInclinationIP", name = T(770322265919, "Roof Inclination"), editor = "number", default = 0, no_edit = true, dont_save = true, min = 0*60, max = 45*60 }, { category = "Roof", id = "keep_roof_passable", name = "Keep Roof Passable", editor = "bool", default = false, }, { category = "Roof", id = "roof_buttons", name = "Buttons", editor = "buttons", default = false, dont_save = true, read_only = true, buttons = { {name = "Make Roof Passable", func = "MakeRoofPassable"}, }, }, { id = "vfx_roof_surface_controllers", editor = "objects", default = false, dont_save = true, no_edit = true}, { id = "vfx_eaves_controllers", editor = "objects", default = false, dont_save = true, no_edit = true}, }, roof_box = false, prop_map = false, box_at_last_roof_edit = false, rr_recursing = false, } function RoomRoof:GetRoofInclinationIP() return self.roof_inclination end function RebuildChimneysInBox2D() --print("stub") end function RoomRoof:SetRoofInclinationIP(roof_inclination) if self.roof_inclination ~= roof_inclination then NetSyncEvent("ObjFunc", self, "rfnSetRoofInclination", roof_inclination) end end function RoomRoof:rfnSetRoofInclination(roof_inclination) if self.roof_inclination ~= roof_inclination then self.roof_inclination = roof_inclination self:RecreateRoof() ObjModified(self) end end function RoomRoof:GetRoofSize() return self.size end function RoomRoof:GetRoofType() return self.roof_type end function RoomRoof:GetRoofEntitySet() local roof_mat = self.roof_mat or "" if roof_mat == "" then return end local preset = Presets.SlabPreset.RoofSlabMaterials[roof_mat] return preset and preset.EntitySet or "" end function RoomRoof:GetRoofInclination() if self:GetRoofType() == "Flat" then return 0 end return self.roof_inclination end function RoomRoof:HasRoofSet() return self.roof_mat ~= "" and self.roof_type ~= "" end function RoomRoof:HasRoomAbove() local bbox = self.box:grow(const.SlabSizeX, const.SlabSizeY, const.SlabSizeZ) local volumes = EnumVolumes(bbox, function(volume) if volume.floor == self.floor + 1 then return true end end) return volumes and #volumes > 0 end function RoomRoof:HasRoof(scan_rooms_above) if self:HasRoofSet() then if self:IsRoofOnly() then return true end local mrz = select(3, self:GetPosXYZ()) + self.size:z() * voxelSizeZ local biggestRoom = self:GetBiggestEncompassingRoom(function(o, mrz) local rz = select(3, o:GetPosXYZ()) + o.size:z() * voxelSizeZ if rz < mrz then return false elseif not o:HasRoof() then return false end return true end, mrz) if biggestRoom ~= self then return scan_rooms_above and self:HasRoomAbove() end local adjacent_rooms = self.adjacent_rooms local sizex, sizey = self.box:sizexyz() local boxes local totalFace = 0 local myFace = sizex * sizey for _, room in ipairs(adjacent_rooms) do if not room.being_placed then local data = adjacent_rooms[room] local ib = data[1] if ib:sizez() == 0 and table.find(data[2], "Roof") then --he is above us local szx, szy = ib:sizexyz() local hisFace = szx * szy boxes = boxes or {} for i = 1, #boxes do local dx, dy = IntersectSize(ib, boxes[i]) if dx > 0 and dy > 0 then hisFace = hisFace - dx * dy end end table.insert(boxes, ib) totalFace = totalFace + hisFace end end end if myFace - totalFace <= 0 then return scan_rooms_above and self:HasRoomAbove() --completely covered by upper nbrs end return true end return scan_rooms_above and self:HasRoomAbove() end function RoomRoof:IsAnyOfRoofTypes(...) local roof_type = self:GetRoofType() for i=1,select("#", ...) do if roof_type == select(i, ...) then return true end end end function RoomRoof:GetRoofThickness() local entity_set = self:GetRoofEntitySet() if entity_set == "" then return 0 end local entity_name = string.format("Roof_%s_Plane_01", entity_set) if not IsValidEntity(entity_name) then return 0 end local bbox = GetEntityBoundingBox(entity_name) return bbox:sizez() end function RoomRoof:SetRoofVisibility(visible) local objs = self.roof_objs if not objs or #objs <= 0 then return end if self.is_roof_visible == visible then return end self.is_roof_visible = visible local hide = not visible for i=1,#objs do local o = objs[i] if IsValid(o) then o:SetShadowOnly(hide) end end local wire_supporters = {} assert(self.roof_box, string.format("Room %s with .roof_objs (count:%d), but no roof_box!", self.name, #objs)) if g_Classes.WireSupporter and self.roof_box then --this does not exist in bacon, apparently the old code never found any objs on roofs in bacon so it never asserted. local query_box = self.roof_box:grow(1) local min_z = query_box:minz() MapForEach(query_box, "WireSupporter", function(o, min_z, wire_supporters) local x, y, z = o:GetVisualPosXYZ() local is_on_roof = o:GetGameFlags(const.gofOnRoof) ~= 0 if z >= min_z and is_on_roof then table.insert(wire_supporters, o) end end, min_z, wire_supporters) end ForEachConnectedWire(wire_supporters, function(wire) wire:SetVisible(visible) end) for side, t in sorted_pairs(self.spawned_windows or empty_table) do for i = 1, #(t or "") do local w = t[i] if w.hide_with_wall and IsKindOf(w.main_wall, "RoofWallSlab") then w:SetShadowOnly(hide) end end end if hide then (rawget(_G, "CollectionsToHideHideCollections") or empty_func)(self, "Roof") else (rawget(_G, "CollectionsToHideShowCollections") or empty_func)(self, "Roof") end self:SetVfxControllersVisibility(visible) end function RoomRoof:DeleteRoofObjs() for _, roof in ipairs(self.roof_objs) do if IsValid(roof) then DoneObject(roof) end end self.roof_objs = nil end function RoomRoof:PostProcessPlaneSlab(slab, is_flat) if is_flat then slab:ClearEnumFlags(const.efInclinedSlab) slab:SetEnumFlags(const.efApplyToGrids) elseif self:GetRoofInclination() <= const.MaxPassableTerrainSlope then slab:SetEnumFlags(const.efApplyToGrids | const.efInclinedSlab) else slab:ClearEnumFlags(const.efApplyToGrids | const.efInclinedSlab) end end local function IsRoofTile(o) --roof tile or part of walls return not IsKindOfClasses(o, "RoofWallSlab", "RoofCornerWallSlab", "CeilingSlab", "DestroyableWallDecoration") end local slabPoint = point(voxelSizeX, voxelSizeY, voxelSizeZ) local function GetElHashId(o, pivots) pivots = pivots or o.room:GetPivots() if not IsRoofTile(o) then local s = o.side local dp = (o:GetPos() - pivots[s]) local x, y, z = abs(dp:x()) / voxelSizeX, abs(dp:y()) / voxelSizeY, abs(dp:z()) / voxelSizeZ --print(pivots[s], o:GetPos(), dp, s, o.class, x, y, z, o.room:GetRoofZAndDir(o.room.box:min())) return xxhash(s, x, y, z, IsKindOf(o, "RoomCorner") or false) else local dp = (o:GetPos() - pivots[false]) --print(pivots[false], o:GetPos(), dp, false, o.class, 0, 0, 0, o.room:GetRoofZAndDir(o.room.box:min())) return xxhash(dp:x(), dp:y(), o:GetAngle(), o.class, rawget(o, "dir") or false) end end function RoomRoof:GetWallBeginPos(side, box) local b = box or self.box if side == "North" then return point(b:minx(), b:miny(), b:maxz()) elseif side == "South" then return point(b:maxx(), b:maxy(), b:maxz()) elseif side == "West" then return point(b:minx(), b:maxy(), b:maxz()) else --east return point(b:maxx(), b:miny(), b:maxz()) end end function RoomRoof:GetPivots(box) box = box or self.box local pivots = {} for _, side in ipairs(cardinal_direction_names) do pivots[side] = self:GetWallBeginPos(side, box) end pivots[false] = pivots.North --pivot for roof tiles return pivots end function RoomRoof:CleanupPropMap() self.prop_map = false end function RoomRoof:ApplyPropsFromPropObj(o, prop_map, pivots) prop_map = prop_map or self.prop_map pivots = pivots or self:GetPivots() if prop_map then local id = GetElHashId(o, pivots) local po = prop_map[id] if po then o:CopyProperties(po, po:GetProperties()) --else --print("not found!", id) end end end function RoomRoof:PopulatePropMap() self:CleanupPropMap() self.prop_map = {} local pivots = self:GetPivots(self.box_at_last_roof_edit or self.box) for i = 1, #(self.roof_objs or "") do local o = self.roof_objs[i] if IsValid(o) and not IsKindOfClasses(o, "CeilingSlab") then local id = GetElHashId(o, pivots) --assert(self.prop_map[id] == nil) --in rooms of size 1 tile we cannot guarantee unique ids for all elements local propO = SlabPropHolder:new() propO:CopyProperties(o) self.prop_map[id] = propO end end self.prop_map["minz"] = self.box_at_last_roof_edit and self.box_at_last_roof_edit:maxz() or self.box:maxz() --"minz" is used as roof box minz, which is box:maxz self.box_at_last_roof_edit = self.box end local visibilityStateForNewRoofPieces = true function RoomRoof:RecreateRoof(force) SuspendPassEdits("RoomRoof") self:PopulatePropMap() self:DeleteRoofObjs() if self:HasRoofSet() and (force or self:HasRoof()) then visibilityStateForNewRoofPieces = self.is_roof_visible and RoofVisualsEnabled and (not IsEditorActive() or LocalStorage.FilteredCategories["Roofs"]) local roof_type = self:GetRoofType() local method_name = string.format("Create%sRoof", roof_type) local method = self[method_name] self.roof_objs, self.roof_box = method(self) if self.build_ceiling then self:CreateCeiling(self.roof_objs) end self:SnapObjects() end visibilityStateForNewRoofPieces = true self:UpdateRoofSlabVisibility() if not self.is_roof_visible then self.is_roof_visible = true --force setter self:SetRoofVisibility(false) end if not RoofVisualsEnabled then self:SetroofVisualsEnabledForRoom(false) end RebuildChimneysInBox2D(self.box) if self.keep_roof_passable and not self.rr_recursing then --sub optimal, dobule recreate, should be fine for f3 only. self.rr_recursing = true self:MakeRoofPassable() self.rr_recursing = false end ResumePassEdits("RoomRoof") end local up = point(0, 0, 4096) local ptx = point(4096, 0, 0) local pty = point(0, 4096, 0) function RoomRoof:SnapObject(obj) obj:SetGameFlags(const.gofOnRoof) local skew = obj:GetClassFlags(const.cfSkewAlign) ~= 0 --snap to roof pos local pos = obj:GetVisualPos() local roof_z, roof_dir = self:GetRoofZAndDir(pos) if not IsKindOf(obj, "Decal") then local thickness = self:GetRoofThickness() roof_z = roof_z + thickness end local target_pos = pos:SetZ(roof_z) obj:SetPos(target_pos) --roof forward vector local roof_fwd_y, roof_fwd_x = sincos(roof_dir) local roof_fwd_z = sin(self:GetRoofInclination()) local roof_forward = SetLen(point(roof_fwd_x, roof_fwd_y, roof_fwd_z), 4096) --object target up vector local target_up if skew then target_up = up else local roof_right = SetLen(point(-roof_fwd_y, roof_fwd_x, 0), 4096) local roof_up = Cross(roof_forward, roof_right) / 4096 target_up = roof_up end --rotate object (align with target up) local obj_axis = obj:GetAxis() local obj_angle = obj:GetAngle() local obj_up = RotateAxis(point(0, 0, 4096), obj_axis, obj_angle) if obj_up ~= target_up then local axis, angle = GetAxisAngle(obj_up, target_up) local axis, angle = ComposeRotation(obj_axis, obj_angle, axis, angle) obj:SetAxisAngle(axis, angle) end --skew object if skew then --determine roof 2D forward vector local roof_forward_2d = SetLen(roof_forward:SetZ(0), 4096) --determine object-local forward/right vectors local obj_axis = obj:GetAxis() local obj_angle = obj:GetAngle() local obj_x = RotateAxis(ptx, obj_axis, obj_angle) local obj_y = RotateAxis(pty, obj_axis, obj_angle) --calculate skew local dot_x = Dot(roof_forward_2d, obj_x) / 4096 local dot_y = Dot(roof_forward_2d, obj_y) / 4096 local skew_x = MulDivRound(roof_fwd_z, dot_x*guim, 4096*4096) local skew_y = MulDivRound(roof_fwd_z, dot_y*guim, 4096*4096) obj:SetSkew(skew_x, skew_y) else obj:SetSkew(0, 0) end return target_pos, target_up end function RoomRoof:GetSkewAtPos(pos) local ox,oy,oz, fx,fy,fz,fs, rx,ry,rz,rs = self:GetRoofCoordSystem(pos) local skew_x, skew_y = MulDivRound(-fz, guim, abs(fx + rx)), 0 return skew_x, skew_y end function RoomRoof:SnapObjects() if not self:HasRoof() then return end local above_rooms = MapGet(self:GetPos(), roomQueryRadius, "Room", function(room, my_maxz) return room.box and room.box:maxz() > my_maxz end, self.box:maxz()) local min_z = self.prop_map and self.prop_map["minz"] or self.roof_box:minz() MapForEach( self.roof_box, --area "CObject", --class false, --enum flags (all) false, --enum flags (any) false, --game flags (all) const.gofOnRoof, --game flags (any) false, --class flags (all) false, --class flags (any) function(obj, above_rooms, min_z) --action local x, y, z = obj:GetVisualPosXYZ() if z < min_z then return end for _, room in ipairs(above_rooms) do if IsPointInVolume2D(room, obj) then return end end self:SnapObject(obj) end, above_rooms, min_z) end function RoomRoof:RecalcRoof() if not self.roof_objs then return end local roof_type = self:GetRoofType() local method_name = string.format("Recalc%sRoof", roof_type) local method = self[method_name] method(self) end function RoomRoof:GetRoofCoordSystem(pt) if not self:HasRoof() then return end local roof_type = self:GetRoofType() local method_name = string.format("Get%sRoofCoordSystem", roof_type) local method = self[method_name] return method(self, pt) end function RoomRoof:GetRoofClippingPlane(pt) local ox,oy,oz, fx,fy,fz,fs, rx,ry,rz,rs = self:GetRoofCoordSystem(pt) local p1, p2, p3 = point(ox, oy, oz), point(ox + rx*rs, oy + ry*rs, oz + rz*rs), point(ox + fx*fs, oy + fy*fs, oz + fz*fs) return PlaneFromPoints(p1, p2, p3) end local eastPt, southPt, westPt, northPt = point(4096,0,0), point(0,4096,0), point(-4096,0,0), point(0,-4096,0) function RoomRoof:GetRoofZAndDir(pt) if not self:HasRoof() then return InvalidPos():z(), 0 end local ox,oy,oz, fx,fy,fz,fs, rx,ry,rz,rs = self:GetRoofCoordSystem(pt) local z = CalcRoofZAt(ox,oy,oz, fx,fy,fz, rx,ry,rz, pt:xy()) local dir = cardinal_directions[self.roof_direction] if not dir then --non-standard roof direction --estimate using dot products local fwd = point(fx, fy, fz) local dots = { Dot(fwd, eastPt), Dot(fwd, southPt), Dot(fwd, westPt), Dot(fwd, northPt) } local max = Max(table.unpack(dots)) local idx = table.find(dots, max) dir = cardinal_directions[idx] end return z, dir end ---- roof creation function RoomRoof:UpdateRoofSlabVisibility() if self.roof_box and self:GetGameFlags(const.gofPermanent) ~= 0 then --DbgAddBox(self.roof_box) ComputeSlabVisibilityInBox(self.roof_box) end end -- +--+ -- | | function RoomRoof:GetFlatRoofParams() local px, py, pz = WorldToVoxel(self.position) local sx, sy, sz = self.size:xyz() local voxel_box = box(px, py, pz, px + sx, py + sy, pz + sz) return px, py, pz, sx, sy, sz, voxel_box end function RoomRoof:GetFlatRoofCoordSystem(pt) local px, py, pz, sx, sy, sz, voxel_box = self:GetFlatRoofParams() return SolveRoofCoordSystem(voxel_box, self.roof_additional_height, self.roof_direction, self:GetRoofInclination()) end function RoomRoof:CreateFlatRoof() local px, py, pz, sx, sy, sz, voxel_box = self:GetFlatRoofParams() return self:CreateRoof("Flat", voxel_box, self.roof_direction, self.roof_parapet) end function RoomRoof:RecalcFlatRoof() local px, py, pz, sx, sy, sz, voxel_box = self:GetFlatRoofParams() self.roof_box = self:RecalcRoof_Generic("Flat", voxel_box, self.roof_direction, self.roof_parapet) end -- /| -- / | function RoomRoof:GetShedRoofParams() local px, py, pz = WorldToVoxel(self.position) local sx, sy, sz = self.size:xyz() local voxel_box = box(px, py, pz, px + sx, py + sy, pz + sz) return px, py, pz, sx, sy, sz, voxel_box end function RoomRoof:GetShedRoofCoordSystem(pt) local px, py, pz, sx, sy, sz, voxel_box = self:GetShedRoofParams() return SolveRoofCoordSystem(voxel_box, self.roof_additional_height, self.roof_direction, self:GetRoofInclination()) end function RoomRoof:CreateShedRoof() local px, py, pz, sx, sy, sz, voxel_box = self:GetShedRoofParams() return self:CreateRoof("Shed", voxel_box, self.roof_direction, self.roof_parapet) end function RoomRoof:RecalcShedRoof() local px, py, pz, sx, sy, sz, voxel_box = self:GetShedRoofParams() self.roof_box = self:RecalcRoof_Generic("Shed", voxel_box, self.roof_direction, self.roof_parapet) end -- /\ -- / \ function RoomRoof:GetGableRoofBoxes() local px, py, pz = WorldToVoxel(self.position) local sx, sy, sz = self.size:xyz() local dir1, dir2 local box1, box2 local odd if self.roof_direction == "East-West" or self.roof_direction == "East" or self.roof_direction == "West" then dir1 = "South" dir2 = "North" box1 = box(px, py, pz, px + sx, py + sy/2, pz + sz) box2 = box(px, py + sy/2, pz, px + sx, py + sy, pz + sz) odd = (sy % 2) == 1 if odd then box1 = box1:grow(0, 0, 0, 1) end else -- North-South dir1 = "East" dir2 = "West" box1 = box(px, py, pz, px + sx/2, py + sy, pz + sz) box2 = box(px + sx/2, py, pz, px + sx, py + sy, pz + sz) odd = (sx % 2) == 1 if odd then box1 = box1:grow(0, 0, 1, 0) end end return dir1, dir2, box1, box2, odd end function RoomRoof:GetGableRoofCoordSystem(pt) local dir1, dir2, box1, box2, odd = self:GetGableRoofBoxes() local bx, by, bz = box1:size():xyz() local sx, sy, sz = self.size:xyz() local dx, dy = 0, 0 --when gable roof is odd, box1 and box2 are 1/2 voxel larger in the direction of inclination --in other words, the topmost voxel is shared by both boxes and there are two clipped tiles on top of each other --it is technically correct, however in that voxel we would always snap as if we are in box1, whereas we should --snap as if in box2 when in the second half of said voxel if bx < sx and bx * 2 > sx then --even x, the box should be half a voxel smaller on the x side dx = voxelSizeX / 2 end if by < sy and by * 2 > sy then --even y dy = voxelSizeY / 2 end local minx, miny, minz, maxx, maxy, maxz = BoxVoxelToWorld(box1) minx, miny, minz, maxx, maxy, maxz = minx - 1, miny - 1, minz - 1, maxx + 1 - dx, maxy + 1 - dy, maxz + 1 local world_box1 = box(minx, miny, minz, maxx, maxy, maxz) local in_box1 = pt:InBox2D(world_box1) local box = in_box1 and box1 or box2 local dir = in_box1 and dir1 or dir2 return SolveRoofCoordSystem(box, self.roof_additional_height, dir, self:GetRoofInclination()) end function RoomRoof:CreateGableRoof() local dir1, dir2, box1, box2, odd = self:GetGableRoofBoxes() local objs1, box1 = self:CreateRoof("Gable", box1, dir1, self.roof_parapet, odd and "odd_gable_short") local objs2, box2 = self:CreateRoof("Gable", box2, dir2, self.roof_parapet, odd and "odd_gable_long") local px, py, pz = WorldToVoxel(self.position) local sx, sy, sz = self.size:xyz() local full_box = box(px, py, pz, px + sx, py + sy, pz + sz) --in voxels local objs3 = self:CreateRoof_GableCaps(full_box, dir1, self.roof_parapet) local objs = { } table.iappend(objs, objs1 or empty_table) table.iappend(objs, objs2 or empty_table) table.iappend(objs, objs3 or empty_table) local box = AddRects(box1, box2) return objs, box end function RoomRoof:RecalcGableRoof() local dir1, dir2, box1, box2, odd = self:GetGableRoofBoxes() local roof_box1 = self:RecalcRoof_Generic("Gable", box1, dir1, self.roof_parapet, odd and "odd_gable_short") local roof_box2 = self:RecalcRoof_Generic("Gable", box2, dir2, self.roof_parapet, odd and "odd_gable_long") self.roof_box = AddRects(roof_box1, roof_box2) end ---- generic roof creation function BoxVoxelToWorld(voxel_box) local minx, miny, minz, maxx, maxy, maxz = voxel_box:xyzxyz() minx, miny, minz = VoxelToWorld(minx, miny, minz) minx, miny, minz = minx - halfVoxelSizeX, miny - halfVoxelSizeY, minz maxx, maxy, maxz = VoxelToWorld(maxx, maxy, maxz) maxx, maxy, maxz = maxx - halfVoxelSizeX, maxy - halfVoxelSizeY, maxz return minx, miny, minz, maxx, maxy, maxz end --calculates vector for roof local coordinate system function SolveRoofCoordSystem(voxel_box, z_offset, direction, inclination) local ox, oy, oz --origin x, y, z local fx, fy, fz, fs --forward x, y, z, size local rx, ry, rz, rs --right x, y, z, size --center (in 2D world space) local minx, miny, minz, maxx, maxy, maxz = BoxVoxelToWorld(voxel_box) local cx, cy, cz = (minx + maxx) / 2, (miny + maxy) / 2, maxz + z_offset --DbgAddBox(box(minx,miny,minz,maxx,maxy,maxz)) local angle = cardinal_directions[direction] --roof slope direction local sin, cos = sincos(angle) sin, cos = sin / 4096, cos / 4096 --rotate from world space to local space local function rotate_vector(x, y, z) return x * cos - y * sin, x * sin + y * cos, z end --forward/right sizes + inclination local inclination_size if direction == "North" or direction == "South" then rs, fs = voxel_box:sizexyz() inclination_size = const.SlabSizeX else fs, rs = voxel_box:sizexyz() inclination_size = const.SlabSizeY end --forward inclination per 1 voxel local incl_sin, incl_cos = sincos(inclination) local incl_tan = MulDivRound(incl_sin, 4096, incl_cos) local voxel_incline = MulDivRound(inclination_size, incl_tan, 4096) --forward/right vectors local fx, fy, fz = rotate_vector(const.SlabSizeX, 0, voxel_incline) local rx, ry, rz = rotate_vector(0, const.SlabSizeY, 0) --origin ox, oy, oz = cx - MulDivRound(fs, fx, 2) - MulDivRound(rs, rx, 2), cy - MulDivRound(fs, fy, 2) - MulDivRound(rs, ry, 2), cz --DbgAddVector(point(ox, oy, oz), point(0, 0, const.SlabSizeZ), const.clrGreen) --DbgAddVector(point(ox, oy, oz), point(fx, fy, fz), const.clrBlue) --DbgAddVector(point(ox, oy, oz), point(rx, ry, rz), const.clrRed) return ox, oy, oz, --origin x, y, z fx, fy, fz, fs, --forward x, y, z, size rx, ry, rz, rs --right x, y, z, size end local function RoofCoordSystemBBox(ox,oy,oz, fx,fy,fz,fs, rx,ry,rz,rs, additional_height) local minx, miny, minz = ox, oy, oz - additional_height local maxx, maxy, maxz = ox + fs*fx + rs*rx + 1, oy + fs*fy + rs*ry + 1, oz + fs*fz + rs*rz + 1 minx, maxx = Min(minx, maxx), Max(minx, maxx) miny, maxy = Min(miny, maxy), Max(miny, maxy) minz, maxz = Min(minz, maxz), Max(minz, maxz) return box( minx, miny, minz, maxx, maxy, maxz) end --calculates position (x, y, z) of roof corner local function SolveRoofCornerPosition(ox,oy,oz, fx,fy,fz,fs, rx,ry,rz,rs, side) if side == "Front" then return ox + fx*fs, oy + fy*fs, oz + fz*fs elseif side == "Right" then return ox + fx*fs + rx*rs, oy + fy*fs + ry*rs, oz + fz*fs + rz*rs elseif side == "Back" then return ox + rx*rs, oy + ry*rs, oz + rz*rs elseif side == "Left" then return ox, oy, oz else assert(false, "Invalid side of roof corner") return ox, oy, oz end end --calculates vectors for roof edge coordinate system local function SolveRoofEdgeCoordSystem(ox,oy,oz, fx,fy,fz,fs, rx,ry,rz,rs, side) local front_or_back = (side == "Front" or side == "Back") local dx, dy, dz --delta if front_or_back then dx, dy, dz = rx, ry, rz else dx, dy, dz = fx, fy, fz end local ds = front_or_back and rs or fs --delta size --base x, y, z local bx, by, bz if side == "Left" then bx, by, bz = ox, oy, oz elseif side == "Front" then bx, by, bz = ox + fx*fs, oy + fy*fs, oz + fz*fs elseif side == "Right" then bx, by, bz = ox + rx*rs, oy + ry*rs, oz + rz*rs elseif side == "Back" then bx, by, bz = ox, oy, oz end return bx, by, bz, --base x, y, z dx, dy, dz, ds --delta x, y, z, size end local function GetGableClipPlane(ox,oy,oz, fx,fy,fz,fs, rx,ry,rz,rs) local p1, p2 = point(ox + (fs-1)*fx + fx/2, oy + (fs-1)*fy + fy/2, oz + (fs-1)*fz + fz/2), point(ox + (fs-1)*fx + rs*rx + fx/2, oy + (fs-1)*fy + rs*ry + fy/2, oz + (fs-1)*fz + rs*rz + fz/2) local p3 = p2:AddZ(const.SlabSizeZ) --DbgAddPoly({p1, p2, p3, p1:AddZ(const.SlabSizeZ)}) return PlaneFromPoints(p1, p3, p2) end function CalcRoofZAt(ox,oy,oz, fx,fy,fz, rx,ry,rz, px,py) local ppx = MulDivRound(px - ox, guim, fx + rx) local ppy = MulDivRound(py - oy, guim, fy + ry) if abs(fx) > abs(fy) then return (ppx*fz + ppy*rz) / guim + oz else return (ppx*rz + ppy*fz) / guim + oz end end local function SideNames(direction) local first_dir_i = table.find(cardinal_direction_names, direction) local sides = { } for i=1,4 do local dir_i = (((first_dir_i - 1) + (i - 1)) % 4) + 1 sides[i] = cardinal_direction_names[dir_i] end local side_front, side_right, side_back, side_left = table.unpack(sides) return side_front, side_right, side_back, side_left end local side_to_i = { ["Front"] = 0, ["Right"] = 1, ["Back"] = 2, ["Left"] = 3, } function RoomRoof:CreateSlab(slab_class, params) local slab_classdef = g_Classes[slab_class] return slab_classdef:new(params) end function RoomRoof:CreateRoofComponents_RoofPlane(objs, ox,oy,oz, fx,fy,fz,fs, rx,ry,rz,rs, direction, special) --determine roof skew local skew_x, skew_y = MulDivRound(-fz, guim, abs(fx + rx)), 0 --determine angle local angle = cardinal_directions[direction] + 180*60 --handle odd length gable roof top slabs local clip_plane local odd_gable = (special == "odd_gable_long") or (special == "odd_gable_short") if odd_gable then clip_plane = GetGableClipPlane(ox,oy,oz, fx,fy,fz,fs, rx,ry,rz,rs) end local is_flat = self:GetRoofType() == "Flat" for fi=1,fs do local gable_clip = odd_gable and (fi == fs) for ri=1,rs do local x = ox + (fi-1)*fx + (ri-1)*rx + (fx+rx)/2 local y = oy + (fi-1)*fy + (ri-1)*ry + (fy+ry)/2 local z = oz + (fi-1)*fz + (ri-1)*rz + (fz+rz)/2 --DbgAddVector(point(x, y, z), point(0, 0, const.SlabSizeZ), const.clrBlue) local slab = self:CreateSlab("RoofPlaneSlab", { floor = self.floor, room = self, dir = direction, material = self.roof_mat, }) self:SetupNewObj(slab, x, y, z, angle, self.roof_colors, nil, objs, gable_clip and clip_plane or nil, nil, skew_x, skew_y) self:PostProcessPlaneSlab(slab, is_flat) end end end RoomRoof.ShouldPlaceRoofEdge = return_true function RoomRoof:CreateRoofComponents_RoofEdge(objs, ox,oy,oz, fx,fy,fz,fs, rx,ry,rz,rs, direction, side, special) if not self:ShouldPlaceRoofEdge(direction, side) then return end local front_or_back = (side == "Front" or side == "Back") --determine entity local edge_type = (side == "Front" and "Ridge") or (side == "Back" and "Eave") or "Rake" --local coord system local bx, by, bz, dx, dy, dz, ds = SolveRoofEdgeCoordSystem(ox,oy,oz, fx,fy,fz,fs, rx,ry,rz,rs, side) --adjust for voxel edges bx, by, bz = bx + dx/2, by + dy/2, bz + dz/2 --determine how the edge will be skewed local skew_x, skew_y if side == "Front" then skew_x, skew_y = MulDivRound(fz, guim, abs(fx + rx)), 0 elseif side == "Back" then skew_x, skew_y = MulDivRound(-fz, guim, abs(fx + rx)), 0 else skew_x, skew_y = 0, MulDivRound(fz, guim, abs(fy + ry)) end --rakes (left and right side) are mirrored only on the right side local mirror = (side == "Right") --determine object angle local direction_i = table.find(cardinal_direction_names, direction) local direction_i = (((direction_i-1) + side_to_i[side]) % 4) + 1 local angle = cardinal_directions[direction_i] --handle odd length gable roof top slabs local clip_plane local odd_gable = (special == "odd_gable_long") or (special == "odd_gable_short") if odd_gable then clip_plane = GetGableClipPlane(ox,oy,oz, fx,fy,fz,fs, rx,ry,rz,rs) end for di=1,ds do local x = bx + (di-1)*dx local y = by + (di-1)*dy local z = bz + (di-1)*dz --DbgAddVector(point(x, y, z), point(0, 0, const.SlabSizeZ), const.clrMagenta) local slab = self:CreateSlab("RoofEdgeSlab", { floor = self.floor, room = self, dir = direction, roof_comp = edge_type, material = self.roof_mat, }) self:SetupNewObj(slab, x, y, z, angle, self.roof_colors, nil, objs, odd_gable and di == ds and clip_plane or nil, mirror, skew_x, skew_y) end end RoomRoof.ShouldCreateRoofCorner = return_true function RoomRoof:CreateRoofComponents_RoofCorner(objs, ox,oy,oz, fx,fy,fz,fs, rx,ry,rz,rs, direction, side) if not self:ShouldCreateRoofCorner(direction, side) then return end local mirror, angle local x, y, z --determine angle angle = cardinal_directions[direction] if side == "Left" or side == "Back" then angle = cardinal_directions[direction] + 180*60 end --determine if component should be mirrored local mirror = (side == "Front" or side == "Back") --determine position local x, y, z = SolveRoofCornerPosition(ox,oy,oz, fx,fy,fz,fs, rx,ry,rz,rs, side) --determine entity local corner_type = (side == "Front" or side == "Right") and "RakeRidge" or "RakeEave" --determine skew local skew = (side == "Front" or side == "Right") and fz or -fz local skew_x, skew_y = MulDivRound(skew, guim, abs(fx + rx)), 0 --DbgAddVector(point(x, y, z), point(0, 0, const.SlabSizeZ), const.clrOrange) local slab = self:CreateSlab("RoofCorner", { floor = self.floor, room = self, dir = direction, roof_comp = corner_type, material = self.roof_mat, }) self:SetupNewObj(slab, x, y, z, angle, self.roof_colors, nil, objs, nil, mirror, skew_x, skew_y) end RoomRoof.ShouldCreateRoofWallEdge = return_true function RoomRoof:CreateRoofComponents_WallEdge(objs, ox,oy,oz, fx,fy,fz,fs, rx,ry,rz,rs, direction, side, clip_plane, special) if not self:ShouldCreateRoofWallEdge(direction, side) then return end --local coord system local bx, by, bz, dx, dy, dz, ds = SolveRoofEdgeCoordSystem(ox,oy,oz, fx,fy,fz,fs, rx,ry,rz,rs, side) --adjust for walls & voxel edges dz = 0 bx, by = bx + dx/2, by + dy/2 bz = oz - self.roof_additional_height --determine object angle local front_or_back = (side == "Front" or side == "Back") local dir_i = table.find(cardinal_direction_names, direction) local side_i = side_to_i[side] local wall_side = cardinal_direction_names[(((dir_i-1) + side_i) % 4) + 1] local angle = cardinal_directions[wall_side] local oc = self.outer_colors local ic = self.inner_colors local dbg_outwards_vec = (side == "Left" or side == "Front") and point(dy, -dx, dz) or point(-dy, dx, dz) dbg_outwards_vec = SetLen(dbg_outwards_vec, guim) --handle middle column of gable roofs with odd length local odd_gable_long_half = (special == "odd_gable_long") local odd_gable_short_half = (special == "odd_gable_short") local odd_gable = odd_gable_long_half or odd_gable_short_half local max_height = max_int if odd_gable and not front_or_back then local size = ds if odd_gable_short_half then size = size + 1 end local x, y = (bx + (size-1)*dx), (by + (size-1)*dy) local roof_z1, roof_z2 = CalcRoofZAt(ox,oy,oz, fx,fy,fz, rx,ry,rz, x + dx/2, y + dy/2), CalcRoofZAt(ox,oy,oz, fx,fy,fz, rx,ry,rz, x - dx/2, y - dy/2) local min_roof_z = Min(roof_z1, roof_z2) - bz max_height = min_roof_z / const.SlabSizeZ end local is_flat = self:GetRoofType() == "Flat" for di=1,ds do local x = bx + (di-1)*dx local y = by + (di-1)*dy --determine wall column height --note: when creating parapet this fn is called without clipping plane local roof_z if clip_plane then local roof_z1, roof_z2 = CalcRoofZAt(ox,oy,oz, fx,fy,fz, rx,ry,rz, x + dx/2, y + dy/2), CalcRoofZAt(ox,oy,oz, fx,fy,fz, rx,ry,rz, x - dx/2, y - dy/2) --DbgAddVector(point(x + dx/2, y + dy/2, roof_z1), dbg_outwards_vec, const.clrRed) --DbgAddVector(point(x - dx/2, y - dy/2, roof_z2), dbg_outwards_vec, const.clrRed) roof_z = Max(roof_z1, roof_z2) - bz else roof_z = Max(fz*fs, const.SlabSizeZ) + self.roof_additional_height end local height = DivCeil(roof_z, const.SlabSizeZ) height = Min(height, max_height) local mat = self:GetWallMatHelperSide(wall_side) for j=1,height do local z = bz + (di-1)*dz + (j-1)*const.SlabSizeZ --DbgAddVector(point(x, y, z), dbg_outwards_vec, const.clrYellow) local variant = self.inner_wall_mat ~= noneWallMat and "OutdoorIndoor" or "Outdoor" local wall = self:CreateSlab("RoofWallSlab", { --left floor = self.floor, material = mat, room = self, side = wall_side, variant = variant, indoor_material_1 = self.inner_wall_mat, }) self:SetupNewObj(wall, x, y, z, angle, oc, ic, objs, clip_plane) end if not clip_plane and self.roof_parapet and is_flat then --only for flat parapet roofs local extra_dec_e = "WallDec_material_FenceTop_Body_01" extra_dec_e = extra_dec_e:gsub("material", mat) if IsValidEntity(extra_dec_e) then --ent exists local z = bz + (di-1)*dz + (height-1)*const.SlabSizeZ local o = PlaceObject(extra_dec_e, {side = wall_side}) o:SetGameFlags(const.gofPermanent) self:SetupNewObj(o, x, y, z, angle, oc or o:GetDefaultColorizationSet(), ic, objs, clip_plane) end end end --now place the half of middle column of gable roofs with odd length if odd_gable and not front_or_back then local size = ds if odd_gable_long_half then size = size - 1 end local di_from = (fz > 0) and ((max_height*const.SlabSizeZ - self.roof_additional_height)/fz) or 0 for di=di_from,size do local x = bx + (di-1)*dx + dx/2 local y = by + (di-1)*dy + dy/2 --determine wall column height --note: when creating parapet this fn is called without clipping plane local roof_z if clip_plane then local roof_z1, roof_z2 = CalcRoofZAt(ox,oy,oz, fx,fy,fz, rx,ry,rz, x + dx/2, y + dy/2), CalcRoofZAt(ox,oy,oz, fx,fy,fz, rx,ry,rz, x - dx/2, y - dy/2) --DbgAddVector(point(x + dx/2, y + dy/2, roof_z1), dbg_outwards_vec, const.clrRed) --DbgAddVector(point(x - dx/2, y - dy/2, roof_z2), dbg_outwards_vec, const.clrRed) roof_z = Max(roof_z1, roof_z2) - bz else roof_z = Max(fz*fs, const.SlabSizeZ) + self.roof_additional_height end local height = DivCeil(roof_z, const.SlabSizeZ) for i=max_height+1,height do local z = bz + (size-1)*dz + (i-1)*const.SlabSizeZ --DbgAddVector(point(x, y, z), dbg_outwards_vec, const.clrYellow) local mat = self:GetWallMatHelperSide(wall_side) local variant = self.inner_wall_mat ~= noneWallMat and "OutdoorIndoor" or "Outdoor" local wall = self:CreateSlab("GableRoofWallSlab", { --left floor = self.floor, material = mat, room = self, side = wall_side, variant = variant, indoor_material_1 = self.inner_wall_mat, }) self:SetupNewObj(wall, x, y, z, angle, oc, ic, objs, clip_plane) end end end end function RoomRoof:SetupNewObj(obj, x, y, z, a, colors, inner_colors, container, clip_plane, mirror, skew_x, skew_y) obj:SetPosAngle(x, y, z, a) obj:AlignObj() if colors then obj:Setcolors(colors) end if inner_colors then obj:Setinterior_attach_colors(inner_colors) end if clip_plane then obj:SetClipPlane(clip_plane) end obj:UpdateEntity() obj:UpdateVariantEntities() obj:SetMirrored(mirror or false) if skew_x and skew_y then obj:SetSkew(skew_x, skew_y) end self:ApplyPropsFromPropObj(obj) if container then table.insert(container, obj) end if not visibilityStateForNewRoofPieces then obj:SetHierarchyGameFlags(const.gofSolidShadow) obj:SetOpacity(0) end end function RoomRoof:CreateRoofComponents_WallCorner(objs, ox,oy,oz, fx,fy,fz,fs, rx,ry,rz,rs, direction, side, clip_plane, has_plug) --determine angle local angle = cardinal_directions[direction] if side == "Left" or side == "Back" then angle = cardinal_directions[direction] + 180*60 end --determine position local bx, by = SolveRoofCornerPosition(ox,oy,oz, fx,fy,fz,fs, rx,ry,rz,rs, side) local bz = oz - self.roof_additional_height --determine skew local skew = (side == "Front" or side == "Right") and fz or -fz local skew_x, skew_y = MulDivRound(skew, guim, abs(fx + rx)), 0 --determine corner column height --note: when creating parapet this fn is called without clipping plane local roof_z if clip_plane then roof_z = CalcRoofZAt(ox,oy,oz, fx,fy,fz, rx,ry,rz, bx, by) - bz else roof_z = Max(fz*fs, const.SlabSizeZ)+ self.roof_additional_height end local height = DivCeil(roof_z, const.SlabSizeZ) local dir_i = table.find(cardinal_direction_names, direction) local side_i = side_to_i[side] local corner_side = cardinal_direction_names[(((dir_i-1) + side_i) % 4) + 1] local mat = self:GetWallMatHelperSide(corner_side) for i=1,height do local z = bz + (i-1)*const.SlabSizeZ --DbgAddVector(point(bx, by, z), point(0, 0, const.SlabSizeZ), const.clrYellow) local corner = self:CreateSlab("RoofCornerWallSlab", { room = self, material = mat, side = corner_side, isPlug = false, floor = self.floor, }) self:SetupNewObj(corner, bx, by, z, angle, self.outer_colors, nil, objs, clip_plane) end if has_plug then local z = bz + (height-1)*const.SlabSizeZ local corner = self:CreateSlab("RoofCornerWallSlab", { room = self, material = mat, side = corner_side, isPlug = true, floor = self.floor, }) self:SetupNewObj(corner, bx, by, z, angle, self.outer_colors, nil, objs, clip_plane) end local is_flat = self:GetRoofType() == "Flat" if not clip_plane and self.roof_parapet and is_flat then local extra_dec_e = "WallDec_material_FenceTop_Corner_01" extra_dec_e = extra_dec_e:gsub("material", mat) if IsValidEntity(extra_dec_e) then --ent exists local z = bz + (height-1)*const.SlabSizeZ local o = PlaceObject(extra_dec_e, {side = corner_side}) o:SetGameFlags(const.gofPermanent) local a = cardinal_directions[corner_side] - 90 * 60 --corners probably self align, since angle is bogus self:SetupNewObj(o, bx, by, z, a, self.outer_colors or o:GetDefaultColorizationSet(), nil, objs, clip_plane) end end end local gable_cap_30_degree = -100 local gable_cap_max_adjustment = 120 function RoomRoof:AdjustGableCapZ(z) --adjust the caps depending on roof inclination angle local inclination = self:GetRoofInclination() inclination = 30*60 - Clamp(inclination, 0, 30*60) return (z + gable_cap_30_degree) + MulDivRound(inclination, gable_cap_max_adjustment, 30*60) end function RoomRoof:CreateRoof_GableCaps(voxel_box, direction, parapet) local objs = { } --local coordinate system (origin, forward, right) local ox, oy, oz, fx, fy, fz, fs, rx, ry, rz, rs = SolveRoofCoordSystem(voxel_box, self.roof_additional_height, direction, self:GetRoofInclination()) --base x, y, z (where the caps begin) local bx, by, bz = ox + MulDivRound(fx, fs, 2), oy + MulDivRound(fy, fs, 2), oz + (fz*fs) / 2 bz = self:AdjustGableCapZ(bz) --determine angle for componnets local angle = cardinal_directions[direction] + 180*60 local angle_rake, angle_gable = angle + 90*60, angle --determine classes for the two types of components local class_rake, class_gable if (fs % 2) == 0 then class_rake, class_gable = "GableCapRoofCorner", "GableCapRoofEdgeSlab" else class_rake, class_gable = "GableCapRoofEdgeSlab", "GableCapRoofPlaneSlab" end --create rake components if not parapet then --left rake local slab = self:CreateSlab(class_rake, { floor = self.floor, room = self, material = self.roof_mat, roof_comp = "RakeGable", }) self:SetupNewObj(slab, bx, by, bz, angle_rake, self.roof_colors, nil, objs) --right rake local slab = self:CreateSlab(class_rake, { floor = self.floor, room = self, material = self.roof_mat, roof_comp = "RakeGable", }) self:SetupNewObj(slab, bx + rx*rs, by + ry*rs, bz + rz*rs, angle_rake + 180*60, self.roof_colors, nil, objs, nil, true) end --adjust the base x, y, z for the gable components bx, by, bz = bx + rx/2, by + ry/2, bz + rz/2 --create gable components for i=1,rs do local x, y, z = bx + (i-1)*rx, by + (i-1)*ry, bz + (i-1)*rz --DbgAddVector(point(x, y, z), point(0, 0, const.SlabSizeZ), const.clrCyan) local slab = self:CreateSlab(class_gable, { floor = self.floor, room = self, material = self.roof_mat, roof_comp = "Gable", }) self:SetupNewObj(slab, x, y, z, angle_gable, self.roof_colors, nil, objs) end return objs end --[[@@@ Create a roof plane. Can be called multiple times to create a more complex roof. @function objects RoomRoof@CreateRoof(box voxel_box, string direction, bool parapet, string special) @param string roof_type - Type of roof ("Flat", "Shed", "Gable"). @param box voxel_box - 2D box, in voxels, which the roof should cover. @param string direction - Roof direction ("East", "South", "West", "North"). @param bool parapet - If the roof should have a parapet. @param string special - Special behaviour for some circumstances ("odd_gable_long", "odd_gable_short"). @result array objects - Created objects. ]] function RoomRoof:CreateRoof(roof_type, voxel_box, direction, parapet, special) local objs = { } local odd_gable = (special == "odd_gable_long" or special == "odd_gable_short") local odd_gable_long_half = (special == "odd_gable_long") --local coordinate system (origin, forward, right) local inclination = self:GetRoofInclination() local ox, oy, oz, fx, fy, fz, fs, rx, ry, rz, rs = SolveRoofCoordSystem(voxel_box, self.roof_additional_height, direction, inclination) --geometric plane local p1, p2, p3 = point(ox, oy, oz), point(ox + rx*rs, oy + ry*rs, oz + rz*rs), point(ox + fx*fs, oy + fy*fs, oz + fz*fs) local clip_plane = PlaneFromPoints(p1, p2, p3) --DbgAddVector(p1, p2 - p1, const.clrWhite) --DbgAddVector(p1, p3 - p1, const.clrWhite) --DbgAddVector(p3, p2 - p1, const.clrWhite) --DbgAddVector(p2, p3 - p1, const.clrWhite) --create flat inner roof planes self:CreateRoofComponents_RoofPlane(objs, ox,oy,oz, fx,fy,fz,fs, rx,ry,rz,rs, direction, special) --create roof edges if (roof_type ~= "Flat" or not parapet) and roof_type ~= "Gable" then self:CreateRoofComponents_RoofEdge(objs, ox,oy,oz, fx,fy,fz,fs, rx,ry,rz,rs, direction, "Front", special) end if not parapet then self:CreateRoofComponents_RoofEdge(objs, ox,oy,oz, fx,fy,fz,fs, rx,ry,rz,rs, direction, "Right", special) end if (roof_type ~= "Flat" or not parapet) then self:CreateRoofComponents_RoofEdge(objs, ox,oy,oz, fx,fy,fz,fs, rx,ry,rz,rs, direction, "Back", special) end if not parapet then self:CreateRoofComponents_RoofEdge(objs, ox,oy,oz, fx,fy,fz,fs, rx,ry,rz,rs, direction, "Left", special) end --create roof corners if not parapet then if roof_type ~= "Gable" then self:CreateRoofComponents_RoofCorner(objs, ox,oy,oz, fx,fy,fz,fs, rx,ry,rz,rs, direction, "Front") self:CreateRoofComponents_RoofCorner(objs, ox,oy,oz, fx,fy,fz,fs, rx,ry,rz,rs, direction, "Right") end self:CreateRoofComponents_RoofCorner(objs, ox,oy,oz, fx,fy,fz,fs, rx,ry,rz,rs, direction, "Back") self:CreateRoofComponents_RoofCorner(objs, ox,oy,oz, fx,fy,fz,fs, rx,ry,rz,rs, direction, "Left") end --prepare clipping planes for walls (fpn=front-(clipping)plane-normal;fpd=front-(clipping)plane-distance;...) local fp, rp, bp, lp = clip_plane, clip_plane, clip_plane, clip_plane if parapet then --when creating a parapets we don't provide clipping planes if roof_type == "Flat" then --parapets on all sides fp, rp, bp, lp = false, false, false, false else --parapets on left and right sides only rp, lp = false, false end end local wfs = fs --side walls forward size (left and right walls) if odd_gable and not odd_gable_long_half then wfs = wfs - 1 end --create walls if roof_type ~= "Gable" then self:CreateRoofComponents_WallEdge(objs, ox,oy,oz, fx,fy,fz,fs, rx,ry,rz,rs, direction, "Front", fp, special) end self:CreateRoofComponents_WallEdge(objs, ox,oy,oz, fx,fy,fz,wfs, rx,ry,rz,rs, direction, "Right", rp, special) self:CreateRoofComponents_WallEdge(objs, ox,oy,oz, fx,fy,fz,fs, rx,ry,rz,rs, direction, "Back", bp, special) self:CreateRoofComponents_WallEdge(objs, ox,oy,oz, fx,fy,fz,wfs, rx,ry,rz,rs, direction, "Left", lp, special) --preapre clipping planes for wall corners (fcpn=front-corner-(clipping)plane-normal;...) --where there are two adjacent parapets - there is a corner without a clipping plane local fcp, rcp, bcp, lcp = clip_plane, clip_plane, clip_plane, clip_plane if not lp and not fp then fcp = false end if not fp and not rp then rcp = false end if not rp and not bp then bcp = false end if not bp and not lp then lcp = false end --prepare wall corner plugs/caps --if the corners aren't clipped, they should have a plug at the top local f_plug, r_plug, b_plug, l_plug = not fcp, not rcp, not bcp, not lcp --create wall corners if roof_type ~= "Gable" then self:CreateRoofComponents_WallCorner(objs, ox,oy,oz, fx,fy,fz,fs, rx,ry,rz,rs, direction, "Front", fcp, f_plug) self:CreateRoofComponents_WallCorner(objs, ox,oy,oz, fx,fy,fz,fs, rx,ry,rz,rs, direction, "Right", rcp, r_plug) end self:CreateRoofComponents_WallCorner(objs, ox,oy,oz, fx,fy,fz,fs, rx,ry,rz,rs, direction, "Back", bcp, b_plug) self:CreateRoofComponents_WallCorner(objs, ox,oy,oz, fx,fy,fz,fs, rx,ry,rz,rs, direction, "Left", lcp, l_plug) local roof_box = RoofCoordSystemBBox(ox,oy,oz, fx,fy,fz,fs, rx,ry,rz,rs, self.roof_additional_height) return objs, roof_box end function RoomRoof:RecalcRoof_Generic(roof_type, voxel_box, direction, parapet, special) local objs = self.roof_objs if not objs or not next(objs) then return end --recalc common properties for i=1,#objs do local obj = objs[i] if IsValid(obj) then obj.room = self obj.floor = self.floor if IsRoofTile(obj) then SetSlabColorHelper(obj, obj.colors or self.roof_colors) else SetSlabColorHelper(obj, obj.colors or self.outer_colors) end end end --prepare for gable roofs local odd_gable = (special == "odd_gable_long" or special == "odd_gable_short") local odd_gable_long_half = (special == "odd_gable_long") --local coordinate system (origin, forward, right) local inclination = self:GetRoofInclination() local ox, oy, oz, fx, fy, fz, fs, rx, ry, rz, rs = SolveRoofCoordSystem(voxel_box, self.roof_additional_height, direction, inclination) --geometric plane local clip_plane = PlaneFromPoints( ox, oy, oz, ox + rx*rs, oy + ry*rs, oz + rz*rs, ox + fx*fs, oy + fy*fs, oz + fz*fs) --find side cardinal directions and their angles local side_front, side_right, side_back, side_left = SideNames(direction) local angle_front = cardinal_directions[side_front] local angle_right = cardinal_directions[side_right] local angle_back = cardinal_directions[side_back] local angle_left = cardinal_directions[side_left] --filter objects depending on their role in the roof local roof_box = box(BoxVoxelToWorld(voxel_box)):grow(1) local walls_box = roof_box --gable roofs of odd length use a different box for their walls if odd_gable then local dminx, dminy, dmaxx, dmaxy = 0, 0, 0, 0 if direction == "West" then dminx = 1 end if direction == "North" then dminy = 1 end if direction == "East" then dmaxx = 1 end if direction == "South" then dmaxy = 1 end if odd_gable_long_half then walls_box = walls_box:grow( MulDivRound(dminx, const.SlabSizeX, 2), MulDivRound(dminy, const.SlabSizeY, 2), MulDivRound(dmaxx, const.SlabSizeX, 2), MulDivRound(dmaxy, const.SlabSizeY, 2)) else local walls_voxel_box = voxel_box:grow(-dminx, -dminy, -dmaxx, -dmaxy) walls_box = box(BoxVoxelToWorld(walls_voxel_box)):grow(1) end end --DbgAddBox(roof_box, const.clrWhite) --DbgAddBox(walls_box:grow(10,10,10), const.clrRed) --filter objects into categories and recover some properties local wall_objs, wall_corner_objs, roof_objs = {}, {}, {} --also assign entities local class_gable, class_rake if odd_gable then class_rake, class_gable = "RoofEdgeSlab", "RoofPlaneSlab" else class_rake, class_gable = "RoofCorner", "RoofEdgeSlab" end local is_flat = self:GetRoofType() == "Flat" local center = point( ox + fx*fs/2 + rx*rs/2, oy + fy*fs/2 + ry*rs/2, oz + fz*fs/2 + rz*rs/2) local right = point(rx, ry, rz) local north_x, north_y, south_x, south_y = self.box:xyxy() local east_x, east_y = south_x, north_y local epsilon = voxelSizeX / 10 for i, obj in ipairs(objs) do if obj then if IsKindOf(obj, "RoofCornerWallSlab") and walls_box:Point2DInside(obj) then table.insert(wall_corner_objs, obj) if IsCloser2D(obj, north_x, north_y, epsilon) then obj.side = "North" elseif IsCloser2D(obj, south_x, south_y, epsilon) then obj.side = "South" elseif IsCloser2D(obj, east_x, east_y, epsilon) then obj.side = "East" else obj.side = "West" end elseif IsKindOf(obj, "RoofWallSlab") and walls_box:Point2DInside(obj) then table.insert(wall_objs, obj) obj.side = slabAngleToDir[obj:GetAngle()] elseif IsKindOf(obj, "RoofSlab") and roof_box:Point2DInside(obj) and (not obj.dir or obj.dir == direction) then local angle = obj:GetAngle() if (not IsKindOf(obj, "RoofPlaneSlab") or angle == angle_back) and obj.dir then table.insert(roof_objs, obj) end obj.side = direction if not obj.dir then if IsKindOf(obj, class_gable) then obj.roof_comp = "Gable" elseif IsKindOf(obj, class_rake) then obj.roof_comp = "RakeGable" end elseif IsKindOf(obj, "RoofEdgeSlab") then if angle == angle_front then obj.roof_comp = "Ridge" elseif angle == angle_back then obj.roof_comp = "Eave" else obj.roof_comp = "Rake" if angle == angle_right then obj:SetMirrored(true) end end elseif IsKindOf(obj, "RoofCorner") then obj.roof_comp = (angle == angle_front or angle == angle_right) and "RakeRidge" or "RakeEave" if (angle == angle_front) == (Dot(right, obj:GetPos() - center) < 0) then obj:SetMirrored(true) end elseif IsKindOf(obj, "RoofPlaneSlab") then obj.roof_comp = "Plane" self:PostProcessPlaneSlab(obj, is_flat) end end obj:DelayedUpdateEntity() end end --roofs without inclination don't have skewed slabs if inclination > 0 then local sx = MulDivRound(fz, guim, const.SlabSizeX) local sy = MulDivRound(fz, guim, const.SlabSizeY) for i,slab in ipairs(roof_objs) do local skew_x, skew_y local angle = slab:GetAngle() if angle == angle_front then skew_x, skew_y = sx, 0 elseif angle == angle_right or angle == angle_left then skew_x, skew_y = 0, sy elseif angle == angle_back then skew_x, skew_y = -sx, 0 end if skew_x and skew_y then slab:SetSkew(skew_x, skew_y) end end end --clip top parts of a gable roof if odd_gable then local gable_clip_plane = GetGableClipPlane(ox,oy,oz, fx,fy,fz,fs, rx,ry,rz,rs) for i,slab in ipairs(roof_objs) do slab:SetClipPlane(gable_clip_plane) end end --prepare clipping planes for walls (fpn=front-(clipping)plane-normal;fpd=front-(clipping)plane-distance;...) local fp, rp, bp, lp = clip_plane, clip_plane, clip_plane, clip_plane if parapet then --when creating a parapets we don't provide clipping planes if roof_type == "Flat" then --parapets on all sides fp, rp, bp, lp = false, false, false, false else --parapets on left and right sides only rp, lp = false, false end end --clip walls for i,wall in ipairs(wall_objs) do local clip_plane local angle = wall:GetAngle() if angle == angle_front then clip_plane = fp end if angle == angle_right then clip_plane = rp end if angle == angle_back then clip_plane = bp end if angle == angle_left then clip_plane = lp end if clip_plane then wall:SetClipPlane(clip_plane) end end --preapre clipping planes for wall corners (fcpn=front-corner-(clipping)plane-normal;...) --where there are two adjacent parapets - there is a corner without a clipping plane local fcp, rcp, bcp, lcp = clip_plane, clip_plane, clip_plane, clip_plane if not lp and not fp then fcp = false end if not fp and not rp then rcp = false end if not rp and not bp then bcp = false end if not bp and not lp then lcp = false end --clip wall corners for i,corner in ipairs(wall_corner_objs) do local clip_plane local angle = corner:GetAngle() if angle == angle_front then clip_plane = fcp end if angle == angle_right then clip_plane = rcp end if angle == angle_back then clip_plane = bcp end if angle == angle_left then clip_plane = lcp end if clip_plane then corner:SetClipPlane(clip_plane) end end return RoofCoordSystemBBox(ox,oy,oz, fx,fy,fz,fs, rx,ry,rz,rs, self.roof_additional_height) end function RoomRoof:Setkeep_roof_passable(val) self.keep_roof_passable = val if val then self:MakeRoofPassable() end end --moves roof so it becomes passable function RoomRoof:MakeRoofPassable() local nrah, rah = self:GetPassableRoofAdditionalHeight() if nrah then self:OnSetroof_additional_height(nrah, rah) end end function RoomRoof:GetPassableRoofAdditionalHeight() if not self:HasRoof() then return end if self:GetRoofType() ~= "Flat" then --only flat roof may be passable return end local objs = self.roof_objs local rs for i = 1, #(objs or "") do local o = objs[i] if IsKindOf(o, "RoofPlaneSlab") and o:HasSpot("Slab") then rs = o break end end if not rs then return end local rah = self.roof_additional_height local si = rs:GetSpotBeginIndex("Slab") local p = rs:GetPos() local sp = rs:GetSpotPos(si) local offs = sp - p local minHeight = sp:z() - rah local vx, vy, vz = SnapToVoxel(sp:xyz()) if vz < minHeight then vz = vz + voxelSizeZ end local np = point(vx, vy, vz) - offs local d = np:z() - p:z() local nrah = rah + d assert(nrah >= 0 and nrah <= voxelSizeZ) return nrah, rah end ---- roof getters and setters function RoomRoof:GetroofVisualsEnabled() return RoofVisualsEnabled end function RoomRoof:SetroofVisualsEnabledForRoom(v) local roof_objs = self.roof_objs if roof_objs then for j=1,#roof_objs do local roof_obj = roof_objs[j] if IsValid(roof_obj) then local opacity if v then roof_obj:ClearHierarchyGameFlags(const.gofSolidShadow) opacity = 100 else roof_obj:SetHierarchyGameFlags(const.gofSolidShadow) opacity = 0 end if not IsKindOf(roof_obj, "Decal") then roof_obj:SetOpacity(opacity) end end end MapForEach(self.roof_box, "CObject", function(o) if o:GetGameFlags(const.gofOnRoof)~=0 then local opacity if v then o:ClearHierarchyGameFlags(const.gofSolidShadow) opacity = 100 else o:SetHierarchyGameFlags(const.gofSolidShadow) opacity = 0 end if not IsKindOf(o, "Decal") then o:SetOpacity(opacity) end end end) end end function RoomRoof:SetroofVisualsEnabled(v) RoofVisualsEnabled = v MapForEach("map", "Room", function(roof) roof:SetroofVisualsEnabledForRoom(v) end) end function RoomRoof:OnSetroof_type(new_type, old_type) if old_type == new_type then return end self.roof_type = new_type if new_type == "Gable" and not table.find(GableRoofDirections, self.roof_direction) then self.roof_direction = GableRoofDirections[1] elseif table.find(GableRoofDirections, self.roof_direction) then self.roof_direction = cardinal_direction_names[1] end self:RecreateRoof() end function RoomRoof:OnSetroof_mat(new_mat, old_mat) if old_mat == new_mat then return end self.roof_mat = new_mat self:UnlockRoof() self:RecreateRoof() end function RoomRoof:OnSetroof_direction(new_dir, old_dir) if old_dir == new_dir then return end self.roof_direction = new_dir self:RecreateRoof() end function RoomRoof:OnSetroof_inclination(new_incl, old_incl) if old_incl == new_incl then return end self.roof_inclination = new_incl self:RecreateRoof() end function RoomRoof:OnSetroof_parapet(new_parapet, old_parapet) if old_parapet == new_parapet then return end self.roof_parapet = new_parapet self:RecreateRoof() end function RoomRoof:OnSetroof_additional_height(new_height, old_height) if old_height == new_height then return end self.roof_additional_height = new_height self:RecreateRoof() end function RoomRoof:OnSetbuild_ceiling(val) self.build_ceiling = val self:RecreateRoof() end function RoomRoof:OnSetceiling_mat(mat, oldmat) if oldmat == mat then return end if not self.build_ceiling then return end Notify(self, "SetCeilingMatToCeilingSlabs") end function RoomRoof:SetCeilingMatToCeilingSlabs() if not self.build_ceiling then return end local objs = self.roof_objs local mat = self.ceiling_mat local bb = box() for i = #(objs or ""), 1, -1 do local o = objs[i] if not IsKindOf(o, "CeilingSlab") then break end o.material = mat o:UpdateEntity() bb = Extend(bb, o:GetPos()) end if bb:IsValid() and not bb:IsEmpty() then ComputeSlabVisibilityInBox(bb) end end function RoomRoof:CreateCeiling(objs) local mat = self.ceiling_mat local sx, sy = self.position:x(), self.position:y() local sizeX, sizeY, sizeZ = self.size:xyz() sx = sx + halfVoxelSizeX sy = sy + halfVoxelSizeY local gz = self:CalcZ() + sizeZ * voxelSizeZ SuspendPassEdits("Room:CreateCeiling") for xOffset = 0, sizeX - 1 do for yOffset = 0, sizeY - 1 do local x = sx + xOffset * voxelSizeX local y = sy + yOffset * voxelSizeY local ceil = self:CreateSlab("CeilingSlab", { floor = self.floor, material = mat, side = false, room = self }) self:SetupNewObj(ceil, x, y, gz, 0, nil, nil, objs) end end ResumePassEdits("Room:CreateCeiling") end function RoomRoof:OnSetroof_colors(val, oldVal) for i = 1, #(self.roof_objs or "") do local o = self.roof_objs[i] if o and IsRoofTile(o) then o:Setcolors(val) end end end ---- roof slabs --[[@@@ @class RoofSlab All slabs that build up the roof (exist in room.roof_objs) derive from this class. ]] DefineClass.RoofSlab = { __parents = { "Slab", "Mirrorable" }, properties = { { category = "Slabs", id = "material", name = "Material", editor = "preset_id", preset_class = "SlabPreset", preset_group = "RoofSlabMaterials", extra_item = noneWallMat, default = "none", }, { category = "Slabs", id = "forceInvulnerableBecauseOfGameRules", name = "Invulnerable", editor = "bool", default = false, help = "In context of destruction."}, { id = "dir", name = "Direction", editor = "choice", default = false, items = cardinal_direction_names }, { id = "SkewX" }, { id = "SkewY" }, { id = "Mirrored", editor = "bool", default = false, dont_save = true }, }, roof_comp = false, colors_room_member = "roof_colors", entity_base_name = "Roof", room_container_name = "roof_objs", invulnerable = false, } function RoofSlab:GetBaseEntityName() local material_list = Presets.SlabPreset[self.MaterialListClass] or Presets.SlabPreset.RoofSlabMaterials local svd = material_list[self.material] return string.format("%s_%s_%s", self.entity_base_name, svd.EntitySet, self.roof_comp) end local roofCompToSubvariantArr = { Plane = "subvariants", Eave = "eave_subvariants", Rake = "rake_subvariants", Ridge = "ridge_subvariants", Gable = "gable_subvariants", RakeGable = "rake_gable_subvariants", RakeRidge = "rake_ridge_subvariants", RakeEave = "rake_eave_subvariants", GableCrest = "crest_subvariants", RakeGableCrestTop = "crest_top_subvariants", RakeGableCrestBot = "crest_bot_subvariants", GableSlope = "slope_subvariants", RakeGableSlopeTop = "slope_top_subvariants", RakeGableSlopeBot = "slope_bot_subvariants", } function RoofSlab:ComposeEntityName() local material_list = Presets.SlabPreset[self.MaterialListClass] or Presets.SlabPreset.RoofSlabMaterials local svd = material_list[self.material] local sm = roofCompToSubvariantArr[self.roof_comp] local subvariants = svd and svd[sm] if subvariants and #subvariants > 0 then if self.subvariant ~= -1 then --user selected subvar local digit = ((self.subvariant - 1) % #subvariants) + 1 --assumes "01, 02, etc. suffixes local digitStr = digit < 10 and "0" .. tostring(digit) or tostring(digit) return string.format("Roof_%s_%s_%s", svd.EntitySet, self.roof_comp, digitStr) else local subvariant, i = table.weighted_rand(subvariants, "chance", self:GetSeed()) while subvariant do local name = string.format("Roof_%s_%s_%s", svd.EntitySet, self.roof_comp, subvariant.suffix) if IsValidEntity(name) then return name end i = i - 1 subvariant = subvariants[i] end end end return string.format("Roof_%s_%s_01", self.material or noneWallMat, self.roof_comp) end function RoofSlab:MirroringFromRoom() end function RoofSlab:EditorCallbackClone(source) Slab.EditorCallbackClone(self, source) if source.room then source.room:RecreateRoof() end end DefineClass.RoofPlaneSlab = { __parents = { "RoofSlab", "HFloorAlignedObj" }, flags = { efPathSlab = true }, properties = { { category = "Slabs", id = "material", name = "Material", editor = "preset_id", preset_class = "SlabPreset", preset_group = "RoofSlabMaterials", extra_item = noneWallMat, default = "none", }, }, MaterialListClass = "RoofSlabMaterials", roof_comp = "Plane", } DefineClass.RoofEdgeSlab = { __parents = { "RoofSlab", "HWallAlignedObj" }, MaterialListClass = "RoofSlabMaterials", roof_comp = "Rake", } DefineClass.RoofCorner = { __parents = { "RoofSlab", "HCornerAlignedObj" }, MaterialListClass = "RoofSlabMaterials", roof_comp = "RakeRidge", } --[[@@@ @class BaseRoofWallSlab All walls and wall corners that build up the roof derive from this class. They will behave exactly like their normal wall counterparts, but are identifiable as part of the roof by this class. ]] DefineClass.BaseRoofWallSlab = { __parents = { "CObject" }, properties = { { category = "Slabs", id = "forceInvulnerableBecauseOfGameRules", name = "Invulnerable", editor = "bool", default = false, help = "In context of destruction."}, }, room_container_name = "roof_objs", invulnerable = false, } DefineClass.RoofWallSlab = { __parents = { "BaseRoofWallSlab", "WallSlab" }, room_container_name = "roof_objs", forceInvulnerableBecauseOfGameRules = false, invulnerable = false, } --[[@@@ @class GableRoofWallSlab This is a workaround class for gable roofs of odd length require walls that are aligned on voxel corners, instead of their edges. This type of walls is needed, because we cannot clip a single object with multiple planes. ]] DefineClass.GableRoofWallSlab = { __parents = { "RoofWallSlab", "CornerAlignedObj" }, } function GableRoofWallSlab:AlignObj(pos, angle) CornerAlignedObj.AlignObj(self, pos, angle) end DefineClass("GableCapRoofPlaneSlab", "RoofPlaneSlab") DefineClass("GableCapRoofEdgeSlab", "RoofEdgeSlab") DefineClass("GableCapRoofCorner", "RoofCorner") DefineClass.RoofCornerWallSlab = { __parents = { "BaseRoofWallSlab", "RoomCorner" }, room_container_name = "roof_objs", forceInvulnerableBecauseOfGameRules = false, invulnerable = false, } ---- horizontally aligned objects (can move freely on the Z axis) DefineClass.HWallAlignedObj = { __parents = { "WallAlignedObj" }, } function HWallAlignedObj:AlignObjAttached() local p = self:GetParent() assert(p) local ap = self:GetPos() + self:GetAttachOffset() local x, y, z, angle = WallWorldToVoxel(ap:x(), ap:y(), ap:z(), self:GetAngle()) x, y, z = WallVoxelToWorld(x, y, z, angle) local my_x, my_y, my_z = self:GetPosXYZ() my_z = my_z or InvalidZ px, py, pz = p:GetPosXYZ() self:SetAttachOffset(x - px, y - py, my_z - pz) self:SetAngle(angle) --havn't tested with parents with angle ~= 0, might not work end function HWallAlignedObj:AlignObj(pos, angle) local x, y, z if pos then x, y, z = pos:xyz() x, y, z, angle = WallWorldToVoxel(x, y, z or terrain.GetHeight(x, y), angle or self:GetAngle()) else x, y, z, angle = WallWorldToVoxel(self) end local my_x, my_y, my_z = self:GetPosXYZ() my_z = my_z or InvalidZ x, y, z = WallVoxelToWorld(x, y, z, angle) self:SetPosAngle(x, y, my_z, angle) end DefineClass.HFloorAlignedObj = { __parents = { "FloorAlignedObj" }, } function HFloorAlignedObj:AlignObj(pos, angle) local x, y, z if pos then x, y, z, angle = WorldToVoxel(pos, angle or self:GetAngle()) else x, y, z, angle = WorldToVoxel(self) end local my_x, my_y, my_z = self:GetPosXYZ() my_z = my_z or InvalidZ x, y, z = VoxelToWorld(x, y, z) self:SetPosAngle(x, y, my_z, angle) end DefineClass.HCornerAlignedObj = { __parents = { "CornerAlignedObj" }, } function HCornerAlignedObj:AlignObj(pos, angle) local x, y, z if pos then x, y, z, angle = CornerWorldToVoxel(pos, angle or self:GetAngle()) else x, y, z, angle = CornerWorldToVoxel(self) end local my_x, my_y, my_z = self:GetPosXYZ() my_z = my_z or InvalidZ x, y, z = CornerVoxelToWorld(x, y, z, angle) self:SetPosAngle(x, y, my_z, angle) end --------------------------------------------------- --vfx controllers --------------------------------------------------- local function IsOutsideVolumes(obj) local inside = false local mr = obj.room local mp = obj:GetPos() MapForEach(obj:GetObjectBBox():grow(voxelSizeX * 30, voxelSizeY * 30, 0), "Room", function(v, mp, mr) if v ~= mr then if not v:IsRoofOnly() and v.box:PointInsideInclusive(mp) and mp:z() < v.box:maxz() then --horizontally inclusive only! inside = "volume" --DbgAddBox(v.box) return "break" end if v.roof_box and v.roof_box:PointInsideInclusive(mp) then inside = "roof" --DbgAddBox(v.roof_box) return "break" end end end, mp, mr) return not inside, inside end function ShouldHaveRoofEavesSegment(roof_edge) if not roof_edge.isVisible or roof_edge.is_destroyed then return false end local room = roof_edge.room if roof_edge.roof_comp == "Eave" or (room and room.roof_type == "Flat") then if IsOutsideVolumes(roof_edge) then return true end end return false end function RoomRoof:OnRoofEdgeTilesDestroyed() self:UpdateRoofVfxControllers() end function RoomRoof:OnRoofPlaneTilesDestroyed() --TODO: something should happen end function RoomRoof:SetVfxControllersVisibility(val) for i = 1, #(self.vfx_roof_surface_controllers or "") do self.vfx_roof_surface_controllers[i]:SetVisibility(val) end for i = 1, #(self.vfx_eaves_controllers or "") do self.vfx_eaves_controllers[i]:SetVisibility(val) end end function RoomRoof:UpdateRoofVfxControllers() local b = self.roof_box if not self:HasRoof() or not b then DoneObjects(self.vfx_roof_surface_controllers) DoneObjects(self.vfx_eaves_controllers) self.vfx_roof_surface_controllers = false self.vfx_eaves_controllers = false return end --surfaces local controllers = self.vfx_roof_surface_controllers if self.roof_type == "Gable" then controllers = controllers or {} local vc1 = IsValid(controllers[1]) and controllers[1] or PlaceObject("RoofSurface") local vc2 = IsValid(controllers[2]) and controllers[2] or PlaceObject("RoofSurface") local v11, v12, v13, v21, v22, v23 v11 = b:min() if self.roof_direction == "East-West" then v12 = v11 + point(b:sizex(), 0, 0) v13 = v11 + point(0, b:sizey() / 2, 0) v21 = v13 v22 = v21 + point(b:sizex(), 0, 0) v23 = v21 + point(0, b:sizey() / 2, 0) else v12 = v11 + point(b:sizex() / 2, 0, 0) v13 = v11 + point(0, b:sizey(), 0) v21 = v12 v22 = v21 + point(b:sizex() / 2, 0, 0) v23 = v21 + point(0, b:sizey(), 0) end vc1:InitFromParent(self) vc2:InitFromParent(self) vc1:SetVertexes(v11, v12, v13) local _, angle = self:GetRoofZAndDir(v11) vc1.angle = angle + 180 * 60 vc2:SetVertexes(v21, v22, v23) vc2.angle = angle controllers[1] = vc1 controllers[2] = vc2 elseif self.roof_type == "Shed" or self.roof_type == "Flat" then controllers = controllers or {} local vc = IsValid(controllers[1]) and controllers[1] or PlaceObject("RoofSurface") local v1 = b:min() local v2 = v1 + point(b:sizex(), 0, 0) local v3 = v1 + point(0, b:sizey(), 0) vc:InitFromParent(self) vc:SetVertexes(v1, v2, v3) local _, angle = self:GetRoofZAndDir(v1) vc.angle = angle + 180 * 60 controllers[1] = vc if IsValid(controllers[2]) then DoneObject(controllers[2]) end controllers[2] = nil else for i = #(controllers or ""), 1, -1 do DoneObject(controllers[i]) end controllers = false end self.vfx_roof_surface_controllers = controllers local map = {} MapForEach(b:grow(10, 10, 10), "RoofEdgeSlab", function(o, self, map) if o.room == self then if ShouldHaveRoofEavesSegment(o) then local s = slabAngleToDir[o:GetAngle()] map[s] = map[s] or {} local x, y, z = WorldToVoxel(o) local sigcoord if s == "East" or s == "West" then sigcoord = y else sigcoord = x end map[s][sigcoord] = o map[s].min = not map[s].min and sigcoord or Min(map[s].min, sigcoord) map[s].max = not map[s].max and sigcoord or Max(map[s].max, sigcoord) end end end, self, map) local function DetermineVertex(o, side, last) local curb = o:GetObjectBBox() local dir = (o:GetRelativePoint(axis_x) - o:GetPos()) local sx, sy, sz = curb:sizexyz() if side == "East" or side == "West" then local p = curb:Center() + MulDivRound(dir, sx / 2, 4096) local x, y, z = p:xyz() return point(x, y + halfVoxelSizeY * (not last and -1 or 1), z) else local p = curb:Center() + MulDivRound(dir, sy / 2, 4096) local x, y, z = p:xyz() return point(x + halfVoxelSizeX * (not last and -1 or 1), y, z) end end local controllers = self.vfx_eaves_controllers local cidx = 1 for side, t in pairs(map) do local last local v1, v2 for i = t.min, t.max + 1 do local cur = t[i] if cur then if not last then v1 = DetermineVertex(cur, side, false) end else if last then v2 = DetermineVertex(last, side, true) controllers = controllers or {} local vc = IsValid(controllers[cidx]) and controllers[cidx] or PlaceObject("RoofEavesSegment") controllers[cidx] = vc cidx = cidx + 1 vc:InitFromParent(self) if side == "South" or side == "West" then vc.vertex1 = v2 vc.vertex2 = v1 else vc.vertex1 = v1 vc.vertex2 = v2 end vc.angle = last:GetAngle() --[[DbgAddVector(v1) DbgAddVector(v2) DbgAddVector(v1, v2 - v1)]] if vc.playing then vc:Stop() vc:Play() end v1 = nil v2 = nil end end last = cur end end for i = #(controllers or ""), cidx, -1 do if IsValid(controllers[i]) then DoneObject(controllers[i]) end controllers[i] = nil end if #(controllers or "") <= 0 then controllers = false end self.vfx_eaves_controllers = controllers end DefineClass.RoofFXController = { __parents = { "Object" }, entity = "InvisibleObject", properties = { {id = "material", editor = "text", default = false }, {id = "parent_obj", editor = "object", default = false }, {id = "disabled", editor = "bool", default = false }, }, particles = false, playing = false, } function RoofFXController:Done() self:Stop() end function RoofFXController:SetVisibility(val) if self.playing then for i = 1, #(self.particles or "") do if val then self.particles[i]:SetEnumFlags(const.efVisible) else self.particles[i]:ClearEnumFlags(const.efVisible) end end end end function RoofFXController:SetDisabled(val) if val then self:Stop() end self.disabled = val end function RoofFXController:InitFromParent(parent_obj) self:SetPos(parent_obj:GetPos()) self:SetAngle(parent_obj:GetAngle()) self.parent_obj = parent_obj self.material = parent_obj.roof_mat end function RoofFXController:Play() if self.disabled then return end if self.playing then return end PlayFX("ClearSky", "end", self, self.material) PlayFX("RainHeavy", "start", self, self.material) self.playing = true end function RoofFXController:Stop() if not self.playing then return end PlayFX("RainHeavy", "end", self, self.material) PlayFX("ClearSky", "start", self, self.material) DoneObjects(self.particles) self.particles = false self.playing = false end DefineClass.RoofEavesSegment = { __parents = { "RoofFXController" }, properties = { {id = "vertex1", editor = "point", default = false }, {id = "vertex2", editor = "point", default = false }, {id = "angle", editor = "number", default = false }, }, } function RoofEavesSegment:Dbg() local v1 = self.vertex1 local v2 = self.vertex2 DbgAddVector(v1) DbgAddVector(v2) DbgAddVector(v1, v2 - v1) end function RoofEavesSegment:Play() if self.disabled then return end if self.playing then return end RoofFXController.Play(self) local d = self.vertex1:Dist2D(self.vertex2) local angle = CalcSignedAngleBetween2D(point(4096, 0, 0), self.vertex2 - self.vertex1) if angle < 0 then angle = 360 * 60 + angle end local par = PlaceParticles("Rain_Pouring_Dyn") par:SetPos(self.vertex1) par:SetAngle(angle) par:SetParam("width", d) self.particles = self.particles or {} table.insert(self.particles, par) end DefineClass.RoofSurface = { __parents = { "RoofFXController" }, properties = { {id = "vertex1", editor = "point", default = false }, {id = "vertex2", editor = "point", default = false }, {id = "vertex3", editor = "point", default = false }, {id = "angle", editor = "number", default = false }, }, } function RoofSurface:GetOffset() if self.material == "Tin" then return 74 --boxmaxz - originz elseif self.material == "Tiles" then return 129 elseif self.material == "Concrete" then return 190 end return 0 end function RoofSurface:SetVertexes(v1, v2, v3) local parent_obj = self.parent_obj assert(parent_obj) --different material pieces have different heights local hoff = self:GetOffset() --GetRoofZAndDir doesn't always work on the extreme edges.. local minx = Min(v1:x(), v2:x(), v3:x()) + 1 local maxx = Max(v1:x(), v2:x(), v3:x()) - 1 local miny = Min(v1:y(), v2:y(), v3:y()) + 1 local maxy = Max(v1:y(), v2:y(), v3:y()) - 1 v1 = point(minx, maxy, 0) v2 = point(maxx, maxy, 0) v3 = point(minx, miny, 0) v1 = v1:SetZ(parent_obj:GetRoofZAndDir(v1) + hoff) v2 = v2:SetZ(parent_obj:GetRoofZAndDir(v2) + hoff) v3 = v3:SetZ(parent_obj:GetRoofZAndDir(v3) + hoff) self.vertex1 = v1 self.vertex2 = v2 self.vertex3 = v3 --[[DbgAddVector(self.vertex1) DbgAddVector(self.vertex2) DbgAddVector(self.vertex3)]] end function RoofSurface:Play() RoofFXController.Play(self) local v1 = self.vertex1 local v2 = self.vertex2 local v3 = self.vertex3 local xmax = v2:Dist(v1) local ymax = v3:Dist(v1) assert(ymax < 1000000 and ymax > 0) local angle = CalcSignedAngleBetween2D(point(4096, 0, 0), v2 - v1) if angle < 0 then angle = 360 * 60 + angle end local par = PlaceParticles("Splashes_Raindrop_Dyn") par:SetPos(v1) par:SetAngle(angle) par:SetParam("area", MulDivRound(xmax, ymax, 1000)) par:SetParam("width", xmax) par:SetParam("height", ymax) self.parent_obj:SnapObject(par) self.particles = self.particles or {} table.insert(self.particles, par) end function CreateVfxControllersForAllRoomsOnMap() MapForEach("map", "RoofFXController", DoneObject) --in case of old version ones existing MapForEach("map", "Room", RoomRoof.UpdateRoofVfxControllers) end function PlayRoofFX() MapForEach("map", "RoofFXController", function(o) o:Play() end) end function StopRoofFX() MapForEach("map", "RoofFXController", function(o) o:Stop() end) end