From 95527450cbe814a19a60b5e7da35e10621e591c5 Mon Sep 17 00:00:00 2001 From: Norl4 Date: Sun, 1 Nov 2026 00:11:00 +0100 Subject: [PATCH 01/32] Fix Nullex Voyd Data Anchor freeze and Callback Ping speed reset Data Anchor called OwnerProperties.SpeedManager on the client, which no longer exists, so pressing Q errored. The freeze now runs on the server through PlayerSpeedManager like the other abilities. Callback Ping used self.owner instead of self.Owner, so its speed factor was never removed. --- .../Characters/Killers/NullexVoyd.luau | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/ReplicatedStorage/Characters/Killers/NullexVoyd.luau b/src/ReplicatedStorage/Characters/Killers/NullexVoyd.luau index 8892ba69..3c532303 100644 --- a/src/ReplicatedStorage/Characters/Killers/NullexVoyd.luau +++ b/src/ReplicatedStorage/Characters/Killers/NullexVoyd.luau @@ -30,7 +30,11 @@ local function DataAnchorBehaviour(self: Types.Ability) self.Setup = false else self.Setup = true - + + PlayerSpeedManager.AddSpeedFactor(self.Owner, self.Name, 0) + self:AddConnection(task.delay(2.1, function() + PlayerSpeedManager.RemoveSpeedFactor(self.Owner, self.Name) + end)) self:AddConnection(task.delay(0.75, function() self.DevicePosition = self.OwnerProperties.Character.HumanoidRootPart.Position - Vector3.new(0, 3 * self.OwnerProperties.Character:GetScale(), 0) Sounds.PlaySound(self.PlaceSound, { @@ -56,13 +60,7 @@ local function DataAnchorBehaviour(self: Types.Ability) else self.Cooldown = self.SecondCooldown self.Setup = true - - self.OwnerProperties.SpeedManager:AddSpeedFactor(self.Name, 0) - self:PlayUseAnimation() - self:AddConnection(task.delay((self.UseAnimationTrack.Length or 1.7) + 0.4, function() - self.OwnerProperties.SpeedManager:RemoveSpeedFactor(self.Name) - end)) end end end @@ -73,7 +71,7 @@ local function CallbackPingBehaviour(self: Types.Ability) self.OwnerProperties.Character:SetAttribute("PreventSlash", true) self:AddConnection(task.delay(self.Duration, function() - PlayerSpeedManager.RemoveSpeedFactor(self.owner, self.Name) + PlayerSpeedManager.RemoveSpeedFactor(self.Owner, self.Name) self.OwnerProperties.Character:SetAttribute("PreventSlash", false) end)) From 5d7ba190135c932e3752c6b6a529de2144482dd1 Mon Sep 17 00:00:00 2001 From: Norl4 Date: Sun, 1 Nov 2026 00:12:37 +0100 Subject: [PATCH 02/32] Ignore local build output and Studio MCP config --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 01b71857..bb14271c 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,6 @@ commitdraft.txt aftman.toml -*.DS_Store \ No newline at end of file +*.DS_Store +build/ +.mcp.json From f8ed87d21fd5cf682a94b0f547be27f6140bef64 Mon Sep 17 00:00:00 2001 From: Norl4 Date: Sun, 1 Nov 2026 00:15:53 +0100 Subject: [PATCH 03/32] Add Studio MCP bridge for background playtests --- tools/studio.py | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 tools/studio.py diff --git a/tools/studio.py b/tools/studio.py new file mode 100644 index 00000000..2ca03e86 --- /dev/null +++ b/tools/studio.py @@ -0,0 +1,39 @@ +import base64, json, os, subprocess, sys, time + +calls = [(sys.argv[i], json.loads(sys.argv[i + 1])) for i in range(1, len(sys.argv) - 1, 2)] +proc = subprocess.Popen(["cmd.exe", "/c", os.path.expandvars(r"%LOCALAPPDATA%\Roblox\mcp.bat")], + stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True, encoding="utf-8") +next_id = 0 + +def call(method, params=None): + global next_id + next_id += 1 + proc.stdin.write(json.dumps({"jsonrpc": "2.0", "id": next_id, "method": method, "params": params or {}}) + "\n") + proc.stdin.flush() + while True: + msg = json.loads(proc.stdout.readline()) + if msg.get("id") == next_id: + return msg + +call("initialize", {"protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": {"name": "anomalis", "version": "0"}}) +proc.stdin.write(json.dumps({"jsonrpc": "2.0", "method": "notifications/initialized"}) + "\n") +for _ in range(15): + if not call("tools/call", {"name": "get_studio_state", "arguments": {}})["result"].get("isError"): + break + time.sleep(1) + +for name, args in calls: + if name == "list": + for tool in call("tools/list")["result"]["tools"]: + print(tool["name"], json.dumps(tool["inputSchema"].get("properties", {}))[:300]) + continue + result = call("tools/call", {"name": name, "arguments": args}).get("result", {}) + print(f"== {name}{' (ERROR)' if result.get('isError') else ''}") + for part in result.get("content", []): + if part["type"] == "image": + path = os.path.join(os.path.dirname(__file__), f"shot_{int(time.time())}.png") + open(path, "wb").write(base64.b64decode(part["data"])) + print("image:", path) + else: + print(part.get("text", part)) +proc.kill() From f1b71aec4fe00099a75553a8c9d6ef98202703a2 Mon Sep 17 00:00:00 2001 From: Norl4 Date: Sun, 1 Nov 2026 00:21:35 +0100 Subject: [PATCH 04/32] Skip chase theme layers that were never registered When the killer's chase theme layers are missing, the terror radius loop iterated over nil every frame and flooded the output with errors. --- .../Miscellaneous/TerrorRadiusManager.luau | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/StarterCharacterScripts/Miscellaneous/TerrorRadiusManager.luau b/src/StarterCharacterScripts/Miscellaneous/TerrorRadiusManager.luau index 62d105d1..3fb7ba8d 100644 --- a/src/StarterCharacterScripts/Miscellaneous/TerrorRadiusManager.luau +++ b/src/StarterCharacterScripts/Miscellaneous/TerrorRadiusManager.luau @@ -160,7 +160,7 @@ function TerrorRadiusManager:PlayLayer4(Subfolder: string, delta: number) end self.Playing = true - for _, s: Sound in self.LayerMagnitudes[Subfolder] do + for _, s: Sound in self.LayerMagnitudes[Subfolder] or {} do if s.Name == "L4" then s:Play() self:TweenVolume(s, 0.5, delta) @@ -341,7 +341,7 @@ function TerrorRadiusManager:_CheckAsKiller(delta: number) self.InChase = if self.InChase then ClosestDistance <= Radiuses.L1 else ClosestDistance <= Radiuses.L4 - for r: number, s: Sound in self.LayerMagnitudes[self.LocalPlayerProps.Char.Name] do + for r: number, s: Sound in self.LayerMagnitudes[self.LocalPlayerProps.Char.Name] or {} do if s.Name == "L4" then self:TweenVolume(s, self.InChase and 0.5 or 0, delta) end From 667e3958a8d35c21668bd5d43b21dcfe593d6412 Mon Sep 17 00:00:00 2001 From: Norl4 Date: Sun, 1 Nov 2026 00:59:28 +0100 Subject: [PATCH 05/32] Pass the hit humanoid to character OnHit callbacks Hitbox calls OnHit on the static character module, so Commander's self.OwnerProperties was nil and every hit on him threw errors. OnHit now receives the humanoid that was hit. --- src/ReplicatedStorage/Characters/Survivors/Commander.luau | 4 ++-- src/ReplicatedStorage/Classes/Hitbox.luau | 2 +- src/ReplicatedStorage/Classes/Types.luau | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/ReplicatedStorage/Characters/Survivors/Commander.luau b/src/ReplicatedStorage/Characters/Survivors/Commander.luau index 3c90f815..7eb81ed5 100644 --- a/src/ReplicatedStorage/Characters/Survivors/Commander.luau +++ b/src/ReplicatedStorage/Characters/Survivors/Commander.luau @@ -160,9 +160,9 @@ local function JusticeBehaviour(self: Types.Ability) end end -local function OnCommanderHit(self: Types.Ability, _hitbox: Types.HitboxDetails, _damage: number) +local function OnCommanderHit(_self: Types.Survivor, _hitbox: Types.HitboxDetails, _damage: number, humanoid: Humanoid) CommonFunctions.ApplyEffect({ - TargetHumanoid = self.OwnerProperties.Humanoid, + TargetHumanoid = humanoid, EffectSettings = { Name = "Resistance", Level = 2, diff --git a/src/ReplicatedStorage/Classes/Hitbox.luau b/src/ReplicatedStorage/Classes/Hitbox.luau index 308170e4..84fdf436 100644 --- a/src/ReplicatedStorage/Classes/Hitbox.luau +++ b/src/ReplicatedStorage/Classes/Hitbox.luau @@ -351,7 +351,7 @@ function Hitbox.New(SourcePlayer: Player, Config: Types.HitboxSettings): Types.H -- If the survivor/killer has a function callback for being hit, and they nullify the hit for one reason or another, skip them and move on. if CharacterModule and CharacterModule.OnHit ~= nil then - if CharacterModule:OnHit(Props, Damage) == "Ignored" then + if CharacterModule:OnHit(Props, Damage, Humanoid) == "Ignored" then continue end end diff --git a/src/ReplicatedStorage/Classes/Types.luau b/src/ReplicatedStorage/Classes/Types.luau index 4f317dbe..a73885d7 100644 --- a/src/ReplicatedStorage/Classes/Types.luau +++ b/src/ReplicatedStorage/Classes/Types.luau @@ -381,7 +381,7 @@ export type Survivor = { --- SERVER FUNCTION: An empty function that you can replace to execute custom behaviour when this character gets hit. --- --- If you want the character to not get affected by the hit, you can return `"Ignored"`. - OnHit: (self: Survivor, hitbox: HitboxDetails, damage: number) -> (string?)?, + OnHit: (self: Survivor, hitbox: HitboxDetails, damage: number, humanoid: Humanoid) -> (string?)?, --- SERVER FUNCTION: An empty function that you can replace to execute custom behaviour when this character takes damage from any source. --- --- If you want the character to not take damage, return `"Ignored"`. @@ -741,7 +741,7 @@ export type Killer = { --- SERVER FUNCTION: An empty function that you can replace to execute custom behaviour when this character gets hit. --- --- If you want the character to not get affected by the hit, you can return `"Ignored"`. - OnHit: (self: Killer, hitbox: any, damage: number) -> (string?)?, + OnHit: (self: Killer, hitbox: any, damage: number, humanoid: Humanoid) -> (string?)?, --- SERVER FUNCTION: An empty function that you can replace to execute custom behaviour when this character takes damage from any source. --- --- If you want the character to not take damage, return `"Ignored"`. From 0d37078ead6168c2173ed1d3041299e928185167 Mon Sep 17 00:00:00 2001 From: Norl4 Date: Sun, 1 Nov 2026 01:55:53 +0100 Subject: [PATCH 06/32] Add Instalacion greybox map with horror lighting A code-built first map (tools/maps/Instalacion.luau, run through the Studio MCP): yard, central building, sheds, containers, flickering lamps, two spawn layouts and per-map night lighting. Also fixes two engine issues found while testing it: with exactly two spawn layouts the killer and survivors could use different layouts, and a missing KillersAllowed attribute stopped rounds from starting. Lighting now defaults to Future in the Rojo project. --- default.project.json | 7 + .../Managers/MapManager.luau | 2 +- .../Managers/RoundManager.luau | 4 +- tools/maps/Instalacion.luau | 324 ++++++++++++++++++ 4 files changed, 334 insertions(+), 3 deletions(-) create mode 100644 tools/maps/Instalacion.luau diff --git a/default.project.json b/default.project.json index 47ab14e9..f33fe136 100644 --- a/default.project.json +++ b/default.project.json @@ -55,6 +55,13 @@ "SoundService": { "$className": "SoundService", "$path": "src/SoundService" + }, + + "Lighting": { + "$className": "Lighting", + "$properties": { + "Technology": "Future" + } } } } \ No newline at end of file diff --git a/src/ServerScriptService/Managers/MapManager.luau b/src/ServerScriptService/Managers/MapManager.luau index 8b96166b..db8db354 100644 --- a/src/ServerScriptService/Managers/MapManager.luau +++ b/src/ServerScriptService/Managers/MapManager.luau @@ -133,7 +133,7 @@ function MapManager.GetRandomMap(): Model | Folder --checks if there's more than 1 layout available local Spawnpoints = ActualMap:FindFirstChild("SpawnPoints") local AvailableLayouts = Spawnpoints:GetChildren() - if #AvailableLayouts > 2 then + if #AvailableLayouts > 1 then --chooses a random layout from the spawnpoint pool local ChosenLayout = AvailableLayouts[Rand:NextInteger(1, #AvailableLayouts)] --destroys the rest to ensure that the chosen one is the only one available diff --git a/src/ServerScriptService/Managers/RoundManager.luau b/src/ServerScriptService/Managers/RoundManager.luau index 8ab2e65f..b9532cd9 100644 --- a/src/ServerScriptService/Managers/RoundManager.luau +++ b/src/ServerScriptService/Managers/RoundManager.luau @@ -61,7 +61,7 @@ function RoundManager.StartRound() MapManager.GetRandomMap() --gets the killers for the round - for killerIndex = 1, workspace:GetAttribute("KillersAllowed") do + for killerIndex = 1, workspace:GetAttribute("KillersAllowed") or 1 do RoundManager.CurrentRound.Killers[killerIndex] = { Player = PlayersOrderedByMalice[killerIndex], Character = nil @@ -71,7 +71,7 @@ function RoundManager.StartRound() --sets up every player for index, Player: Player in PlayersOrderedByMalice do - local IsKiller = index <= workspace:GetAttribute("KillersAllowed") + local IsKiller = index <= (workspace:GetAttribute("KillersAllowed") or 1) RoundManager.Utils.SetupPlayer(Player, IsKiller and index or nil) --if it's a survivor then add it to the PlayersRemaining list diff --git a/tools/maps/Instalacion.luau b/tools/maps/Instalacion.luau new file mode 100644 index 00000000..65fcdfd8 --- /dev/null +++ b/tools/maps/Instalacion.luau @@ -0,0 +1,324 @@ +--!strict +local ServerStorage = game:GetService("ServerStorage") + +local ORIGIN = Vector3.new(0, 0, -2500) +local WALL_THICKNESS = 1.5 +local ONLY_THIS_MAP_IN_ROTATION = true + +local Maps = ServerStorage:WaitForChild("Maps") +local moved = {} +if ONLY_THIS_MAP_IN_ROTATION then + local Disabled = ServerStorage:FindFirstChild("MapsOff") or Instance.new("Folder") + Disabled.Name = "MapsOff" + Disabled.Parent = ServerStorage + for _, map in Maps:GetChildren() do + if map.Name ~= "Instalacion" and (map:IsA("Folder") or map:IsA("Model")) then + map.Parent = Disabled + table.insert(moved, map.Name) + end + end +end +local previous = Maps:FindFirstChild("Instalacion") +if previous then + warn("[Instalacion] Rebuilding: anything dressed inside the old Instalacion map is replaced") + previous:Destroy() +end + +local Root = Instance.new("Folder") +Root.Name = "Instalacion" +local MapModel = Instance.new("Model") +MapModel.Name = "Map" +MapModel.Parent = Root + +local function folder(name: string, parent: Instance): Folder + local f = Instance.new("Folder") + f.Name = name + f.Parent = parent + return f +end + +local Building = folder("Building", MapModel) +local Yard = folder("Yard", MapModel) +local Lamps = folder("Lamps", MapModel) + +local function part(parent: Instance, size: Vector3, pos: Vector3, color: Color3, material: Enum.Material, name: string?): Part + local p = Instance.new("Part") + p.Name = name or "Part" + p.Anchored = true + p.Size = size + p.CFrame = CFrame.new(ORIGIN + pos) + p.Color = color + p.Material = material + p.TopSurface = Enum.SurfaceType.Smooth + p.BottomSurface = Enum.SurfaceType.Smooth + p.Parent = parent + return p +end + +local CONCRETE = Color3.fromRGB(86, 88, 94) +local DARK_CONCRETE = Color3.fromRGB(66, 68, 75) +local RUST = Color3.fromRGB(104, 58, 38) +local METAL = Color3.fromRGB(70, 74, 80) +local WOOD = Color3.fromRGB(92, 70, 48) + +local function wall(parent: Instance, a: Vector2, b: Vector2, height: number, gaps: {{number}}?, color: Color3?) + local alongX = a.Y == b.Y + local from, to = if alongX then math.min(a.X, b.X) else math.min(a.Y, b.Y), if alongX then math.max(a.X, b.X) else math.max(a.Y, b.Y) + local cuts = {} + for _, gap in gaps or {} do + table.insert(cuts, { gap[1] - gap[2] / 2, gap[1] + gap[2] / 2 }) + end + table.sort(cuts, function(x, y) return x[1] < y[1] end) + local start = from + local function piece(s: number, e: number) + if e - s < 0.1 then + return + end + local mid, len = (s + e) / 2, e - s + local size = if alongX then Vector3.new(len, height, WALL_THICKNESS) else Vector3.new(WALL_THICKNESS, height, len) + local pos = if alongX then Vector3.new(mid, height / 2, a.Y) else Vector3.new(a.X, height / 2, mid) + part(parent, size, pos, color or CONCRETE, Enum.Material.Concrete, "Wall") + end + for _, cut in cuts do + piece(start, cut[1]) + start = cut[2] + end + piece(start, to) +end + +local function room(container: Instance, name: string, center: Vector2, size: Vector2, height: number, gaps: {[string]: {{number}}}) + local parent = Instance.new("Model") + parent.Name = name + parent.Parent = container + local x0, x1 = center.X - size.X / 2, center.X + size.X / 2 + local z0, z1 = center.Y - size.Y / 2, center.Y + size.Y / 2 + wall(parent, Vector2.new(x0, z0), Vector2.new(x1, z0), height, gaps.north) + wall(parent, Vector2.new(x0, z1), Vector2.new(x1, z1), height, gaps.south) + wall(parent, Vector2.new(x0, z0), Vector2.new(x0, z1), height, gaps.west) + wall(parent, Vector2.new(x1, z0), Vector2.new(x1, z1), height, gaps.east) + part(parent, Vector3.new(size.X + 2, 1, size.Y + 2), Vector3.new(center.X, height + 0.5, center.Y), METAL, Enum.Material.CorrodedMetal, "Roof") + part(parent, Vector3.new(size.X, 0.2, size.Y), Vector3.new(center.X, 0.1, center.Y), DARK_CONCRETE, Enum.Material.Concrete, "Floor") +end + +local function fillLight(parent: Instance, pos: Vector3) + local anchor = part(parent, Vector3.new(0.2, 0.2, 0.2), pos, CONCRETE, Enum.Material.SmoothPlastic, "FillLight") + anchor.Transparency = 1 + anchor.CanCollide = false + anchor.CanQuery = false + anchor.CanTouch = false + anchor.CastShadow = false + local light = Instance.new("PointLight") + light.Range = 26 + light.Brightness = 0.9 + light.Color = Color3.fromRGB(150, 165, 205) + light.Shadows = false + light.Parent = anchor +end + +local function lamp(parent: Instance, pos: Vector3, flicker: boolean, ceiling: boolean?, shadows: boolean?) + local model = Instance.new("Model") + model.Name = "Lamp" + model.Parent = parent + local headY = pos.Y + if not ceiling then + part(model, Vector3.new(0.6, pos.Y, 0.6), Vector3.new(pos.X, pos.Y / 2, pos.Z), METAL, Enum.Material.Metal, "Pole") + end + local head = part(model, Vector3.new(2.2, 0.5, 2.2), Vector3.new(pos.X, headY, pos.Z), Color3.fromRGB(255, 196, 140), Enum.Material.Neon, "Bulb") + head.CastShadow = false + local spot = Instance.new("SpotLight") + spot.Face = Enum.NormalId.Bottom + spot.Angle = 100 + spot.Range = if ceiling then 26 else 42 + spot.Brightness = 6 + spot.Color = Color3.fromRGB(255, 176, 110) + spot.Shadows = shadows == true + spot.Parent = head + local glow = Instance.new("PointLight") + glow.Range = 14 + glow.Brightness = 1 + glow.Color = spot.Color + glow.Parent = head + if flicker then + head:SetAttribute("Flicker", true) + end +end + +part(MapModel, Vector3.new(210, 2, 210), Vector3.new(0, -1, 0), Color3.fromRGB(74, 76, 82), Enum.Material.Asphalt, "Ground") + +wall(Yard, Vector2.new(-100, -100), Vector2.new(100, -100), 20, nil, DARK_CONCRETE) +wall(Yard, Vector2.new(-100, 100), Vector2.new(100, 100), 20, nil, DARK_CONCRETE) +wall(Yard, Vector2.new(-100, -100), Vector2.new(-100, 100), 20, nil, DARK_CONCRETE) +wall(Yard, Vector2.new(100, -100), Vector2.new(100, 100), 20, nil, DARK_CONCRETE) +for _, side in { Vector3.new(0, 45, -101), Vector3.new(0, 45, 101), Vector3.new(-101, 45, 0), Vector3.new(101, 45, 0) } do + local barrier = part(Yard, if side.X == 0 then Vector3.new(204, 50, 1) else Vector3.new(1, 50, 204), side, CONCRETE, Enum.Material.SmoothPlastic, "Barrier") + barrier.Transparency = 1 +end + +room(Building, "MainBuilding", Vector2.new(0, 0), Vector2.new(60, 50), 14, { + north = { { 0, 8 } }, + south = { { -15, 8 }, { 15, 8 } }, + west = { { 0, 8 } }, + east = { { 10, 8 } }, +}) +local InteriorWalls = Instance.new("Model") +InteriorWalls.Name = "InteriorWalls" +InteriorWalls.Parent = Building +wall(InteriorWalls, Vector2.new(-10, -25), Vector2.new(-10, 25), 14, { { -12, 6 }, { 12, 6 } }) +wall(InteriorWalls, Vector2.new(10, -25), Vector2.new(10, 25), 14, { { -5, 6 }, { 18, 6 } }) +wall(InteriorWalls, Vector2.new(10, 0), Vector2.new(30, 0), 14, { { 20, 6 } }) +for _, p in { Vector3.new(0, 8, -32), Vector3.new(-15, 8, 32), Vector3.new(15, 8, 32), Vector3.new(-37, 8, 0), Vector3.new(37, 8, 10), Vector3.new(-70, 7, -55), Vector3.new(55, 7, 65) } do + fillLight(Building, p) +end +for _, p in { Vector3.new(-20, 13.2, -12), Vector3.new(-20, 13.2, 12), Vector3.new(0, 13.2, -14), Vector3.new(0, 13.2, 14), Vector3.new(20, 13.2, -12), Vector3.new(20, 13.2, 12) } do + lamp(Building, p, p.X == 0 and p.Z > 0 or p.X == 20 and p.Z < 0, true) +end + +local Sheds = folder("Sheds", MapModel) +room(Sheds, "ShedNorthWest", Vector2.new(-70, -70), Vector2.new(24, 20), 10, { south = { { -70, 7 } }, east = { { -70, 6 } } }) +room(Sheds, "ShedSouthEast", Vector2.new(70, 65), Vector2.new(20, 24), 10, { west = { { 65, 7 } }, north = { { 70, 6 } } }) +lamp(Sheds, Vector3.new(-70, 9.2, -70), true, true) +lamp(Sheds, Vector3.new(70, 9.2, 65), false, true) + +local function container(pos: Vector3, rotY: number, color: Color3) + local c = part(Yard, Vector3.new(16, 8, 7), pos + Vector3.new(0, 4, 0), color, Enum.Material.CorrodedMetal, "Container") + c.CFrame = CFrame.new(ORIGIN + pos + Vector3.new(0, 4, 0)) * CFrame.Angles(0, math.rad(rotY), 0) +end +container(Vector3.new(55, 0, -40), 20, RUST) +container(Vector3.new(-65, 0, 35), 90, Color3.fromRGB(46, 66, 78)) +container(Vector3.new(-35, 0, 80), 5, RUST) +container(Vector3.new(-35, 8, 80), 0, Color3.fromRGB(70, 74, 52)) +container(Vector3.new(35, 0, -82), -10, Color3.fromRGB(46, 66, 78)) +container(Vector3.new(82, 0, 15), 80, RUST) + +for _, p in { Vector3.new(-45, 0, -45), Vector3.new(45, 0, 40), Vector3.new(-50, 0, 10), Vector3.new(30, 0, 60), Vector3.new(-20, 0, -60), Vector3.new(65, 0, -10) } do + local barrier = part(Yard, Vector3.new(9, 3, 1.5), p + Vector3.new(0, 1.5, 0), CONCRETE, Enum.Material.Concrete, "JerseyBarrier") + barrier.CFrame = CFrame.new(ORIGIN + p + Vector3.new(0, 1.5, 0)) * CFrame.Angles(0, math.rad((p.X * 7 + p.Z * 3) % 90), 0) +end + +for _, p in { Vector3.new(-82, 0, -30), Vector3.new(48, 0, 82), Vector3.new(-12, 0, 52), Vector3.new(84, 0, -60) } do + part(Yard, Vector3.new(4, 4, 4), p + Vector3.new(0, 2, 0), WOOD, Enum.Material.WoodPlanks, "Crate") + part(Yard, Vector3.new(4, 4, 4), p + Vector3.new(4.2, 2, 0.5), WOOD, Enum.Material.WoodPlanks, "Crate") + part(Yard, Vector3.new(4, 4, 4), p + Vector3.new(2, 6, 0.2), WOOD, Enum.Material.WoodPlanks, "Crate") +end + +for i, p in { Vector3.new(-50, 12, -45), Vector3.new(50, 12, 45), Vector3.new(-50, 12, 45), Vector3.new(50, 12, -45), Vector3.new(0, 12, 70), Vector3.new(0, 12, -70), Vector3.new(-85, 12, 0), Vector3.new(85, 12, -30) } do + lamp(Lamps, p, i % 3 == 0, false, i % 2 == 1) +end + +local SpawnPoints = folder("SpawnPoints", MapModel) +local function spawnPoint(parent: Instance, pos: Vector3) + local p = part(parent, Vector3.new(1, 1, 1), pos + Vector3.new(0, 3, 0), CONCRETE, Enum.Material.SmoothPlastic, if parent.Name == "Killers" then "KillerSpawn" else "SurvivorSpawn") + p.CFrame = CFrame.lookAt(ORIGIN + pos + Vector3.new(0, 3, 0), ORIGIN + Vector3.new(0, 3, 0)) + p.Transparency = 1 + p.CanCollide = false + p.CanQuery = false + p.CanTouch = false +end +for i, layout in { { survivors = Vector3.new(-80, 0, 80), killer = Vector3.new(80, 0, -80) }, { survivors = Vector3.new(80, 0, -80), killer = Vector3.new(-80, 0, 80) } } do + local l = folder("Layout" .. i, SpawnPoints) + local survivors, killers = folder("Survivors", l), folder("Killers", l) + for _, offset in { Vector3.new(-4, 0, -4), Vector3.new(4, 0, -4), Vector3.new(-4, 0, 4), Vector3.new(4, 0, 4) } do + spawnPoint(survivors, layout.survivors + offset) + end + spawnPoint(killers, layout.killer) +end + +local Config = Instance.new("ModuleScript") +Config.Name = "Config" +Config.Source = [[return { + Ambience = "rbxassetid://95706527553140", + AmbienceProperties = { + Volume = 0.3, + }, +}]] +Config.Parent = Root + +local LightingModule = Instance.new("ModuleScript") +LightingModule.Name = "Lighting" +LightingModule.Source = [[return { + ClockTime = 0.2, + Brightness = 1.4, + Ambient = Color3.fromRGB(80, 82, 102), + OutdoorAmbient = Color3.fromRGB(100, 106, 136), + EnvironmentDiffuseScale = 0.35, + EnvironmentSpecularScale = 0.6, + GlobalShadows = true, + ShadowSoftness = 0.35, + ExposureCompensation = 0.7, + ColorShift_Top = Color3.fromRGB(0, 0, 0), + ColorShift_Bottom = Color3.fromRGB(0, 0, 0), +}]] +LightingModule.Parent = Root + +local atmosphere = Instance.new("Atmosphere") +atmosphere.Density = 0.3 +atmosphere.Offset = 0.05 +atmosphere.Color = Color3.fromRGB(96, 104, 124) +atmosphere.Decay = Color3.fromRGB(36, 38, 52) +atmosphere.Glare = 0 +atmosphere.Haze = 1 +atmosphere.Parent = LightingModule + +local grade = Instance.new("ColorCorrectionEffect") +grade.Name = "Grade" +grade.Brightness = -0.02 +grade.Contrast = 0.2 +grade.Saturation = -0.35 +grade.TintColor = Color3.fromRGB(214, 224, 255) +grade.Parent = LightingModule + +local bloom = Instance.new("BloomEffect") +bloom.Intensity = 0.7 +bloom.Size = 28 +bloom.Threshold = 1.4 +bloom.Parent = LightingModule + +local sky = Instance.new("Sky") +sky.StarCount = 1200 +sky.CelestialBodiesShown = true +sky.Parent = LightingModule + +local Behaviour = Instance.new("ModuleScript") +Behaviour.Name = "Behaviour" +Behaviour.Source = [[--!strict +local RunService = game:GetService("RunService") + +local function flicker(bulb: BasePart) + local lights = bulb:GetChildren() + local rand = Random.new() + local function set(on: boolean) + bulb.Material = if on then Enum.Material.Neon else Enum.Material.SmoothPlastic + for _, light in lights do + if light:IsA("Light") then + light.Enabled = on + end + end + end + while bulb:IsDescendantOf(workspace) do + task.wait(rand:NextNumber(0.4, 4)) + for _ = 1, rand:NextInteger(1, 4) do + set(false) + task.wait(rand:NextNumber(0.03, 0.12)) + set(true) + task.wait(rand:NextNumber(0.03, 0.2)) + end + end +end + +return { + Init = function() + if not RunService:IsClient() then + return + end + for _, bulb in workspace:WaitForChild("Map"):GetDescendants() do + if bulb:IsA("BasePart") and bulb:GetAttribute("Flicker") then + task.spawn(flicker, bulb) + end + end + end, +}]] +Behaviour.Parent = Root + +Root.Parent = Maps +local _, size = MapModel:GetBoundingBox() +return "Instalacion built: " .. #MapModel:GetDescendants() .. " parts, size " .. tostring(size) .. ", moved out of rotation: " .. (if #moved > 0 then table.concat(moved, ", ") else "none") From 46a9386a458d8f205a497359d5240a0a31f10bfe Mon Sep 17 00:00:00 2001 From: Norl4 Date: Sun, 1 Nov 2026 11:14:51 +0100 Subject: [PATCH 07/32] Add skill-check generators with HUD counter and killer alerts --- .../Managers/GeneratorManager.luau | 305 ++++++++++++++++++ .../Miscellaneous/GeneratorClient.luau | 166 ++++++++++ .../Miscellaneous/SurvivorOnlyPrompts.luau | 18 ++ tools/maps/Instalacion.luau | 24 ++ 4 files changed, 513 insertions(+) create mode 100644 src/ServerScriptService/Managers/GeneratorManager.luau create mode 100644 src/StarterPlayerScripts/Miscellaneous/GeneratorClient.luau create mode 100644 src/StarterPlayerScripts/Miscellaneous/SurvivorOnlyPrompts.luau diff --git a/src/ServerScriptService/Managers/GeneratorManager.luau b/src/ServerScriptService/Managers/GeneratorManager.luau new file mode 100644 index 00000000..a9610492 --- /dev/null +++ b/src/ServerScriptService/Managers/GeneratorManager.luau @@ -0,0 +1,305 @@ +--!strict +local Debris = game:GetService("Debris") +local Players = game:GetService("Players") +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local SoundService = game:GetService("SoundService") + +local Janitor = require(ReplicatedStorage.Packages.Janitor) +local Network = require(ReplicatedStorage.Modules.Network) +local Utils = require(ReplicatedStorage.Modules.Utils) +local CommonFunctions = require(script.Parent.Parent.System.CommonFunctions) +local TimeManager = require(script.Parent.TimeManager) + +local RoundState: StringValue = ReplicatedStorage.RoundInfo.CurrentState + +local BROKEN_COLOR = Color3.fromRGB(200, 40, 30) +local FIXED_COLOR = Color3.fromRGB(70, 230, 110) +local FAIL_NOISE = "rbxasset://sounds/impact_explosion_03.mp3" + +local GeneratorManager = { + Config = { + Stages = 6, + TimeReduction = 15, + MalicePerStage = 0.2, + MinimumTimeLeft = 15, + HitLockout = 1.5, + FailLockout = 2, + SweepTime = 1.3, + ZoneWidth = 0.18, + CoopZoneBonus = 0.08, + LatencyTolerance = 0.08, + MaxDistance = 10, + }, +} + +type Session = { + generator: Model, + panel: BasePart, + startsAt: number, + zone: number, + width: number, + token: number, + hitConnection: RBXScriptConnection, +} + +local sessions: { [Player]: Session } = {} +local lockedUntil: { [Player]: number } = {} +local random = Random.new() + +local function survivorHumanoid(player: Player): Humanoid? + local character = player.Character + local role = character and character:FindFirstChild("Role") :: StringValue? + if not role or role.Value ~= "Survivor" then + return nil + end + local humanoid = character and character:FindFirstChildOfClass("Humanoid") + return if humanoid and humanoid.Health > 0 then humanoid else nil +end + +local function isNear(player: Player, session: Session): boolean + local root = player.Character and player.Character.PrimaryPart + return root ~= nil and (root.Position - session.panel.Position).Magnitude <= GeneratorManager.Config.MaxDistance +end + +local function paintStatus(generator: Model, progress: number) + local status = generator:FindFirstChild("Status") :: BasePart? + if not status then + return + end + local color = BROKEN_COLOR:Lerp(FIXED_COLOR, progress / GeneratorManager.Config.Stages) + status.Color = color + local light = status:FindFirstChildOfClass("PointLight") + if light then + light.Color = color + end +end + +local function endSession(player: Player, lockout: number?) + local session = sessions[player] + if not session then + return + end + sessions[player] = nil + session.hitConnection:Disconnect() + if lockout then + lockedUntil[player] = os.clock() + lockout + end + Network:FireClientConnection(player, "GeneratorCheckEnded", "REMOTE_EVENT") +end + +local function workersOn(generator: Model): number + local count = 0 + for _, session in sessions do + if session.generator == generator then + count += 1 + end + end + return count +end + +local function killers(): { Player } + local list = {} + for _, player in Players:GetPlayers() do + local role = player.Character and player.Character:FindFirstChild("Role") :: StringValue? + if role and role.Value == "Killer" then + table.insert(list, player) + end + end + return list +end + +local function fail(player: Player) + local session = sessions[player] + if not session then + return + end + local noise = Instance.new("Sound") + noise.SoundId = FAIL_NOISE + noise.Volume = 1.5 + noise.RollOffMode = Enum.RollOffMode.Linear + noise.RollOffMaxDistance = 300 + noise.SoundGroup = SoundService.SoundGroups.Master.SFX + noise.Parent = session.panel + noise:Play() + Debris:AddItem(noise, 4) + Network:FireClientConnection(killers(), "GeneratorAlert", "REMOTE_EVENT", session.generator) + endSession(player, GeneratorManager.Config.FailLockout) +end + +local function startCheck(player: Player, session: Session) + local config = GeneratorManager.Config + if not isNear(player, session) or not survivorHumanoid(player) or RoundState.Value ~= "InRound" then + endSession(player) + return + end + session.token += 1 + local token = session.token + local delay = random:NextNumber(0.4, 1) + session.width = config.ZoneWidth + (if workersOn(session.generator) > 1 then config.CoopZoneBonus else 0) + session.zone = random:NextNumber(0.4, 0.95 - session.width) + session.startsAt = os.clock() + delay + Network:FireClientConnection(player, "GeneratorCheck", "REMOTE_EVENT", delay, config.SweepTime, session.zone, session.width) + task.delay(delay + config.SweepTime + config.LatencyTolerance + player:GetNetworkPing(), function() + if sessions[player] ~= session or session.token ~= token then + return + end + if isNear(player, session) then + fail(player) + else + endSession(player) + end + end) +end + +local function completeStage(player: Player, session: Session) + local config = GeneratorManager.Config + local generator = session.generator + local progress = (generator:GetAttribute("Progress") :: number) + 1 + generator:SetAttribute("Progress", progress) + if not Utils.PlayerData.GetPlayerSetting(player, "Game.MaliceDisabled") then + CommonFunctions.UpdatePlayerStat(player, "General.Malice", config.MalicePerStage) + end + paintStatus(generator, progress) + local prompt = session.panel:FindFirstChildOfClass("ProximityPrompt") + if progress < config.Stages then + if prompt then + prompt.ObjectText = `Generator {progress}/{config.Stages}` + end + startCheck(player, session) + return + end + + if prompt then + prompt:Destroy() + end + for worker, other in sessions do + if other.generator == generator then + endSession(worker) + end + end + generator:SetAttribute("Repaired", true) + workspace:SetAttribute("GeneratorsRepaired", (workspace:GetAttribute("GeneratorsRepaired") or 0) + 1) + local removed = math.max(math.min(config.TimeReduction, TimeManager.CurrentTime - config.MinimumTimeLeft), 0) + if removed > 0 then + TimeManager.SetTime(TimeManager.CurrentTime - removed) + end + Network:FireAllClientConnection("GeneratorRepaired", "REMOTE_EVENT", removed) +end + +local function onCheckInput(player: Player) + local config = GeneratorManager.Config + local session = sessions[player] + if not session then + return + end + if not isNear(player, session) then + endSession(player) + return + end + local position = (os.clock() - player:GetNetworkPing() - session.startsAt) / config.SweepTime + local tolerance = config.LatencyTolerance / config.SweepTime + if position < session.zone - tolerance or position > session.zone + session.width + tolerance then + fail(player) + else + completeStage(player, session) + end +end + +local function setupGenerator(generator: Model, janitor: any): boolean + local config = GeneratorManager.Config + local panel = generator:FindFirstChild("Panel") :: BasePart? + if not panel then + warn(`[GeneratorManager] {generator:GetFullName()} has no "Panel" part, skipping it`) + return false + end + + generator:SetAttribute("Progress", 0) + paintStatus(generator, 0) + + local prompt = Instance.new("ProximityPrompt") + prompt.ActionText = "Repair" + prompt.ObjectText = `Generator 0/{config.Stages}` + prompt.HoldDuration = 0 + prompt.MaxActivationDistance = 8 + prompt.RequiresLineOfSight = false + prompt.KeyboardKeyCode = Enum.KeyCode.C + prompt.GamepadKeyCode = Enum.KeyCode.ButtonX + prompt:SetAttribute("SurvivorOnly", true) + prompt.Parent = panel + + janitor:Add(prompt.Triggered:Connect(function(player: Player) + local humanoid = survivorHumanoid(player) + if not humanoid or sessions[player] or os.clock() < (lockedUntil[player] or 0) then + return + end + if RoundState.Value ~= "InRound" or (generator:GetAttribute("Progress") :: number) >= config.Stages then + return + end + local lastHealth = humanoid.Health + local session: Session = { + generator = generator, + panel = panel, + startsAt = 0, + zone = 0, + width = 0, + token = 0, + hitConnection = humanoid.HealthChanged:Connect(function(health: number) + if health < lastHealth then + endSession(player, config.HitLockout) + end + lastHealth = health + end), + } + sessions[player] = session + startCheck(player, session) + end), "Disconnect") + return true +end + +local function onMapAdded(map: Instance) + if map.Name ~= "Map" then + return + end + workspace:SetAttribute("GeneratorsRepaired", 0) + local generators = map:FindFirstChild("Generators", true) + if not generators then + workspace:SetAttribute("GeneratorsTotal", 0) + return + end + + local janitor = Janitor.new() + janitor:LinkToInstance(map) + janitor:Add(function() + for player in sessions do + endSession(player) + end + end, true) + local total = 0 + for _, generator in generators:GetChildren() do + if generator:IsA("Model") and setupGenerator(generator, janitor) then + total += 1 + end + end + workspace:SetAttribute("GeneratorsTotal", total) +end + +function GeneratorManager:Init() + for _, name in { "GeneratorCheck", "GeneratorCheckEnded", "GeneratorRepaired", "GeneratorAlert" } do + Network:SetConnection(name, "REMOTE_EVENT", function() end) + end + Network:SetConnection("GeneratorCheckInput", "REMOTE_EVENT", onCheckInput) + Players.PlayerRemoving:Connect(function(player: Player) + endSession(player) + lockedUntil[player] = nil + end) + RoundState.Changed:Connect(function(state: string) + if state ~= "InRound" then + for player in sessions do + endSession(player) + end + end + end) + workspace.ChildAdded:Connect(onMapAdded) +end + +return GeneratorManager diff --git a/src/StarterPlayerScripts/Miscellaneous/GeneratorClient.luau b/src/StarterPlayerScripts/Miscellaneous/GeneratorClient.luau new file mode 100644 index 00000000..f4ad1bf6 --- /dev/null +++ b/src/StarterPlayerScripts/Miscellaneous/GeneratorClient.luau @@ -0,0 +1,166 @@ +--!strict +local Debris = game:GetService("Debris") +local Players = game:GetService("Players") +local ProximityPromptService = game:GetService("ProximityPromptService") +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local RunService = game:GetService("RunService") +local TweenService = game:GetService("TweenService") +local UserInputService = game:GetService("UserInputService") + +local Network = require(ReplicatedStorage.Modules.Network) +local Sounds = require(ReplicatedStorage.Modules.Sounds) + +local RoundState: StringValue = ReplicatedStorage.RoundInfo.CurrentState + +local FONT = Font.new("rbxasset://fonts/families/AccanthisADFStd.json", Enum.FontWeight.Bold) +local CHECK_KEYS = { [Enum.KeyCode.C] = true, [Enum.KeyCode.ButtonX] = true } + +local function frame(parent: Instance, size: UDim2, position: UDim2, color: Color3): Frame + local new = Instance.new("Frame") + new.Size = size + new.Position = position + new.BackgroundColor3 = color + new.BorderSizePixel = 0 + new.Parent = parent + return new +end + +local function label(parent: Instance, size: UDim2, position: UDim2, text: string): TextLabel + local new = Instance.new("TextLabel") + new.Size = size + new.Position = position + new.AnchorPoint = Vector2.new(0.5, 0) + new.BackgroundTransparency = 1 + new.FontFace = FONT + new.TextScaled = true + new.TextColor3 = Color3.new(1, 1, 1) + new.TextStrokeTransparency = 0.4 + new.Text = text + new.Parent = parent + return new +end + +return { + Init = function(_) + local gui = Instance.new("ScreenGui") + gui.Name = "Generators" + gui.ResetOnSpawn = false + gui.Parent = Players.LocalPlayer:WaitForChild("PlayerGui") + + local counter = label(gui, UDim2.new(0.16, 0, 0.035, 0), UDim2.new(0.5, 0, 0.1, 4), "") + local function updateCounter() + local total = workspace:GetAttribute("GeneratorsTotal") or 0 + counter.Text = `Generators {workspace:GetAttribute("GeneratorsRepaired") or 0}/{total}` + counter.Visible = RoundState.Value == "InRound" and total > 0 + end + workspace:GetAttributeChangedSignal("GeneratorsRepaired"):Connect(updateCounter) + workspace:GetAttributeChangedSignal("GeneratorsTotal"):Connect(updateCounter) + RoundState.Changed:Connect(updateCounter) + updateCounter() + + local bar = frame(gui, UDim2.new(0.28, 0, 0, 16), UDim2.new(0.5, 0, 0.74, 0), Color3.fromRGB(20, 22, 26)) + bar.AnchorPoint = Vector2.new(0.5, 0.5) + bar.Visible = false + Instance.new("UICorner").Parent = bar + local stroke = Instance.new("UIStroke") + stroke.Color = Color3.fromRGB(200, 200, 210) + stroke.Parent = bar + local zone = frame(bar, UDim2.new(), UDim2.new(), Color3.fromRGB(70, 230, 110)) + local needle = frame(bar, UDim2.new(0, 3, 1.6, 0), UDim2.new(), Color3.new(1, 1, 1)) + needle.AnchorPoint = Vector2.new(0.5, 0.5) + local hint = label(bar, UDim2.new(1, 0, 1.4, 0), UDim2.new(0.5, 0, -2, 0), "") + + local tapButton = Instance.new("TextButton") + tapButton.Size = UDim2.new(0, 96, 0, 96) + tapButton.Position = UDim2.new(0.82, 0, 0.6, 0) + tapButton.AnchorPoint = Vector2.new(0.5, 0.5) + tapButton.BackgroundColor3 = Color3.fromRGB(70, 230, 110) + tapButton.BackgroundTransparency = 0.3 + tapButton.FontFace = FONT + tapButton.TextScaled = true + tapButton.Text = "REPAIR" + tapButton.Visible = false + tapButton.Parent = gui + local corner = Instance.new("UICorner") + corner.CornerRadius = UDim.new(0.5, 0) + corner.Parent = tapButton + + local check: { startsAt: number, sweep: number }? = nil + local repairing = false + local renderConnection: RBXScriptConnection? = nil + + local function stopRender() + if renderConnection then + renderConnection:Disconnect() + renderConnection = nil + end + end + + local function press() + if not check then + return + end + check = nil + stopRender() + Network:FireServerConnection("GeneratorCheckInput", "REMOTE_EVENT") + end + + Network:SetConnection("GeneratorCheck", "REMOTE_EVENT", function(delay: number, sweep: number, zoneStart: number, width: number) + if repairing then + Sounds.PlaySound(Sounds.CommonlyUsedSounds.ButtonPress, { PlaybackSpeed = 1.3 }) + end + repairing = true + ProximityPromptService.Enabled = false + local touch = UserInputService:GetLastInputType() == Enum.UserInputType.Touch + local gamepad = UserInputService:GetLastInputType().Name:find("Gamepad") ~= nil + hint.Text = if touch then "Tap REPAIR in the green zone" elseif gamepad then "Press X in the green zone" else "Press C in the green zone" + tapButton.Visible = touch + zone.Position = UDim2.new(zoneStart, 0, 0, 0) + zone.Size = UDim2.new(width, 0, 1, 0) + needle.Position = UDim2.new(0, 0, 0.5, 0) + bar.Visible = true + local current = { startsAt = os.clock() + delay, sweep = sweep } + check = current + stopRender() + renderConnection = RunService.RenderStepped:Connect(function() + needle.Position = UDim2.new(math.clamp((os.clock() - current.startsAt) / current.sweep, 0, 1), 0, 0.5, 0) + end) + end) + + Network:SetConnection("GeneratorCheckEnded", "REMOTE_EVENT", function() + check = nil + repairing = false + stopRender() + bar.Visible = false + tapButton.Visible = false + ProximityPromptService.Enabled = true + end) + + Network:SetConnection("GeneratorRepaired", "REMOTE_EVENT", function(removed: number) + Sounds.PlaySound(Sounds.CommonlyUsedSounds.ButtonPress, { PlaybackSpeed = 0.6, Volume = 2 }) + if removed <= 0 then + return + end + local popup = label(gui, UDim2.new(0.1, 0, 0.04, 0), UDim2.new(0.5, 0, 0.14, 4), `-{removed} s`) + popup.TextColor3 = Color3.fromRGB(110, 240, 140) + TweenService:Create(popup, TweenInfo.new(1.6), { Position = UDim2.new(0.5, 0, 0.11, 4), TextTransparency = 1, TextStrokeTransparency = 1 }):Play() + Debris:AddItem(popup, 1.7) + end) + + Network:SetConnection("GeneratorAlert", "REMOTE_EVENT", function(generator: Model) + local highlight = Instance.new("Highlight") + highlight.FillTransparency = 1 + highlight.OutlineColor = Color3.fromRGB(255, 60, 40) + highlight.DepthMode = Enum.HighlightDepthMode.AlwaysOnTop + highlight.Parent = generator + Debris:AddItem(highlight, 2.5) + end) + + tapButton.Activated:Connect(press) + UserInputService.InputBegan:Connect(function(input: InputObject, gameProcessed: boolean) + if not gameProcessed and CHECK_KEYS[input.KeyCode] then + press() + end + end) + end, +} diff --git a/src/StarterPlayerScripts/Miscellaneous/SurvivorOnlyPrompts.luau b/src/StarterPlayerScripts/Miscellaneous/SurvivorOnlyPrompts.luau new file mode 100644 index 00000000..d7e7a06f --- /dev/null +++ b/src/StarterPlayerScripts/Miscellaneous/SurvivorOnlyPrompts.luau @@ -0,0 +1,18 @@ +--!strict +local Players = game:GetService("Players") +local ProximityPromptService = game:GetService("ProximityPromptService") + +return { + Init = function() + ProximityPromptService.PromptShown:Connect(function(prompt: ProximityPrompt) + if not prompt:GetAttribute("SurvivorOnly") then + return + end + local character = Players.LocalPlayer.Character + local role = character and character:FindFirstChild("Role") :: StringValue? + if not role or role.Value ~= "Survivor" then + prompt.Enabled = false + end + end) + end, +} diff --git a/tools/maps/Instalacion.luau b/tools/maps/Instalacion.luau index 65fcdfd8..aaa5aedc 100644 --- a/tools/maps/Instalacion.luau +++ b/tools/maps/Instalacion.luau @@ -205,6 +205,30 @@ for i, p in { Vector3.new(-50, 12, -45), Vector3.new(50, 12, 45), Vector3.new(-5 lamp(Lamps, p, i % 3 == 0, false, i % 2 == 1) end +local Generators = folder("Generators", MapModel) +local function generator(name: string, pos: Vector3, rotY: number) + local model = Instance.new("Model") + model.Name = name + model.Parent = Generators + local base = part(model, Vector3.new(5, 4, 3), Vector3.new(0, 2, 0), Color3.fromRGB(120, 104, 52), Enum.Material.DiamondPlate, "Base") + part(model, Vector3.new(3.2, 2.2, 0.3), Vector3.new(0, 2.4, -1.6), Color3.fromRGB(30, 32, 36), Enum.Material.Metal, "Panel") + part(model, Vector3.new(0.4, 2.5, 0.4), Vector3.new(1.8, 5.2, 0.8), METAL, Enum.Material.Metal, "Exhaust") + local status = part(model, Vector3.new(0.8, 0.8, 0.8), Vector3.new(-1.6, 4.4, -1), Color3.fromRGB(200, 40, 30), Enum.Material.Neon, "Status") + status.CastShadow = false + local light = Instance.new("PointLight") + light.Range = 12 + light.Brightness = 1.5 + light.Color = status.Color + light.Parent = status + model.PrimaryPart = base + model:PivotTo(CFrame.new(ORIGIN + pos + Vector3.new(0, 2, 0)) * CFrame.Angles(0, math.rad(rotY), 0)) +end +generator("WestRoom", Vector3.new(-22, 0, 16), 0) +generator("EastRoom", Vector3.new(22, 0, -16), 180) +generator("ShedNorthWest", Vector3.new(-73, 0, -74), 90) +generator("ShedSouthEast", Vector3.new(72, 0, 70), -90) +generator("Yard", Vector3.new(8, 0, 84), 0) + local SpawnPoints = folder("SpawnPoints", MapModel) local function spawnPoint(parent: Instance, pos: Vector3) local p = part(parent, Vector3.new(1, 1, 1), pos + Vector3.new(0, 3, 0), CONCRETE, Enum.Material.SmoothPlastic, if parent.Name == "Killers" then "KillerSpawn" else "SurvivorSpawn") From 323dd1736e131acd01d1717528f0b3921d4e196e Mon Sep 17 00:00:00 2001 From: Norl4 Date: Sun, 1 Nov 2026 11:35:07 +0100 Subject: [PATCH 08/32] Add reactive impacts, terror edge pulse and round tips Hits leave scratches, holes, dust, sparks and debris on the surface they strike and push tagged props. The survivor's screen edge pulses red as the killer nears, and the first rounds show device-aware tips. --- src/ReplicatedStorage/Classes/Hitbox.luau | 8 + .../Managers/ImpactManager.luau | 72 +++++++ .../Miscellaneous/ImpactEffects.luau | 194 ++++++++++++++++++ .../Miscellaneous/RoundTips.luau | 118 +++++++++++ .../Miscellaneous/TerrorPulse.luau | 98 +++++++++ tools/maps/Instalacion.luau | 21 ++ 6 files changed, 511 insertions(+) create mode 100644 src/ServerScriptService/Managers/ImpactManager.luau create mode 100644 src/StarterPlayerScripts/Miscellaneous/ImpactEffects.luau create mode 100644 src/StarterPlayerScripts/Miscellaneous/RoundTips.luau create mode 100644 src/StarterPlayerScripts/Miscellaneous/TerrorPulse.luau diff --git a/src/ReplicatedStorage/Classes/Hitbox.luau b/src/ReplicatedStorage/Classes/Hitbox.luau index 84fdf436..63e7738b 100644 --- a/src/ReplicatedStorage/Classes/Hitbox.luau +++ b/src/ReplicatedStorage/Classes/Hitbox.luau @@ -6,6 +6,7 @@ local ServerScriptService = game:GetService("ServerScriptService") local TimeManager = RunService:IsServer() and require(ServerScriptService.Managers.TimeManager) or nil local CommonFunctions = RunService:IsServer() and require(ServerScriptService.System.CommonFunctions) or nil +local ImpactManager = RunService:IsServer() and require(ServerScriptService.Managers.ImpactManager) or nil local Types = require(script.Parent.Types) local Network = require(ReplicatedStorage.Modules.Network) local Utils = require(ReplicatedStorage.Modules.Utils) @@ -150,6 +151,7 @@ function Hitbox.New(SourcePlayer: Player, Config: Types.HitboxSettings): Types.H Part.Size = Config.Size Part.Shape = Config.Shape + local SurfaceProbed = Config.IsProjectile while not Props.Cancelled and Props.TimePast < Config.Time do if not (Config.HitMultiple or #Props.HumanoidsHit <= 0) then break @@ -167,6 +169,11 @@ function Hitbox.New(SourcePlayer: Player, Config: Types.HitboxSettings): Types.H CheckPart.CFrame = (Config.CFrame and (typeof(Config.CFrame) == "function" and Config.CFrame() or Config.CFrame) or Root.CFrame) * Config.CFrameOffset CheckPart.Parent = HitboxFolder + if not SurfaceProbed then + SurfaceProbed = true + ImpactManager.ProbeHitbox(SourcePlayer, CheckPart.CFrame, CheckPart.Size, Config.Damage, RayParams) + end + -- The hitbox itself lingers for a little while, even after parsing it, so it can be seen for those who have hitboxes enabled in their client settings(?). Debris:AddItem(CheckPart, 0.75) @@ -360,6 +367,7 @@ function Hitbox.New(SourcePlayer: Player, Config: Types.HitboxSettings): Types.H -- If theres a function set to be executed on player hit for this hitbox, toss the humanoid to it. Hitbox._RunCallback(Config.Connections, "Hit", Config, Humanoid, Index == 1) + ImpactManager.Body(SourcePlayer, TargetCharacter, HRoot, Damage) -- Store the user as the last player whom hit the target, to give a readout of their killer if they are to die from a status effect or similar. if TargetCharacter:FindFirstChild("Killer") then diff --git a/src/ServerScriptService/Managers/ImpactManager.luau b/src/ServerScriptService/Managers/ImpactManager.luau new file mode 100644 index 00000000..1e81badf --- /dev/null +++ b/src/ServerScriptService/Managers/ImpactManager.luau @@ -0,0 +1,72 @@ +--!strict +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local Network = require(ReplicatedStorage.Modules.Network) + +local ImpactManager = { + Config = { + ShotLength = 10, + PropRadius = 7, + PropImpulse = 45, + DamageForFullStrength = 40, + }, +} + +local function strengthOf(damage: number): number + return math.clamp(damage / ImpactManager.Config.DamageForFullStrength, 0.3, 1) +end + +local function pushProps(position: Vector3, strength: number) + local config = ImpactManager.Config + for _, part in workspace:GetPartBoundsInRadius(position, config.PropRadius) do + if part.Anchored or not part:HasTag("Reactive") then + continue + end + local away = part.Position - position + local direction = (if away.Magnitude > 0.1 then away.Unit else Vector3.zero) + Vector3.new(0, 0.7, 0) + part:ApplyImpulse(direction.Unit * part.AssemblyMass * config.PropImpulse * strength) + end +end + +function ImpactManager.ProbeHitbox(source: Player, hitboxCFrame: CFrame, size: Vector3, damage: number, params: RaycastParams) + local root = source.Character and source.Character.PrimaryPart + if not root then + return + end + local kind = if size.Z >= ImpactManager.Config.ShotLength then "Shot" else "Melee" + local farEnd = hitboxCFrame.Position + hitboxCFrame.LookVector * size.Z / 2 + local hit = workspace:Raycast(root.Position, farEnd - root.Position, params) + if not hit and kind == "Melee" then + hit = workspace:Raycast(hitboxCFrame.Position, Vector3.new(0, -(size.Y / 2 + 4), 0), params) + end + if not hit or hit.Instance:IsDescendantOf(workspace.Players) then + return + end + local strength = strengthOf(damage) + Network:FireAllClientConnection("Impact", "REMOTE_EVENT", kind, hit.Position, hit.Normal, hit.Instance.Color, hit.Material, strength, source, nil, hit.Instance.Anchored) + pushProps(hit.Position, strength) +end + +function ImpactManager.Body(source: Player, target: Model, root: BasePart, damage: number) + local strength = strengthOf(damage) + Network:FireAllClientConnection("Impact", "REMOTE_EVENT", "Body", root.Position, Vector3.yAxis, Color3.fromRGB(150, 140, 130), Enum.Material.Concrete, strength, source, target) + pushProps(root.Position, strength) +end + +function ImpactManager:Init() + Network:SetConnection("Impact", "REMOTE_EVENT", function() end) + workspace.ChildAdded:Connect(function(map: Instance) + if map.Name ~= "Map" then + return + end + task.defer(function() + for _, part in map:GetDescendants() do + if part:IsA("BasePart") and part:HasTag("Reactive") and not part.Anchored then + part:SetNetworkOwner(nil) + end + end + end) + end) +end + +return ImpactManager diff --git a/src/StarterPlayerScripts/Miscellaneous/ImpactEffects.luau b/src/StarterPlayerScripts/Miscellaneous/ImpactEffects.luau new file mode 100644 index 00000000..da5e544d --- /dev/null +++ b/src/StarterPlayerScripts/Miscellaneous/ImpactEffects.luau @@ -0,0 +1,194 @@ +--!strict +local Debris = game:GetService("Debris") +local Lighting = game:GetService("Lighting") +local Players = game:GetService("Players") +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local TweenService = game:GetService("TweenService") + +local Network = require(ReplicatedStorage.Modules.Network) +local Utils = require(ReplicatedStorage.Modules.Utils) + +local LocalPlayer = Players.LocalPlayer + +local MAX_CHUNKS = 40 +local MAX_MARKS = 24 +local CULL_DISTANCE = 220 +local MARK_LIFETIME = 10 +local SMOKE = "rbxasset://textures/particles/smoke_main.dds" +local SPARK = "rbxasset://textures/particles/sparkles_main.dds" +local SPARKY = { + [Enum.Material.Metal] = true, + [Enum.Material.DiamondPlate] = true, + [Enum.Material.CorrodedMetal] = true, + [Enum.Material.Foil] = true, +} + +local random = Random.new() +local folder = Instance.new("Folder") +folder.Name = "LocalImpacts" +local chunks: { BasePart } = {} +local marks: { BasePart } = {} + +local function lowQuality(): boolean + local quality = UserSettings().GameSettings.SavedQualityLevel + return quality ~= Enum.SavedQualitySetting.Automatic and quality.Value <= 3 +end + +local function calmMode(): boolean + return Utils.PlayerData.GetPlayerSetting(LocalPlayer, "Accessibility.EpilepsyMode") == true +end + +local function track(list: { BasePart }, part: BasePart, limit: number) + table.insert(list, part) + if #list > limit then + local oldest = table.remove(list, 1) + if oldest then + oldest:Destroy() + end + end +end + +local function decor(size: Vector3, cframe: CFrame, color: Color3, material: Enum.Material): Part + local part = Instance.new("Part") + part.Size = size + part.CFrame = cframe + part.Color = color + part.Material = material + part.Anchored = true + part.CanCollide = false + part.CanQuery = false + part.CanTouch = false + part.CastShadow = false + part.Parent = folder + return part +end + +local function fadeOut(part: BasePart, after: number, duration: number) + task.delay(after, function() + if part.Parent then + TweenService:Create(part, TweenInfo.new(duration), { Transparency = 1 }):Play() + Debris:AddItem(part, duration) + end + end) +end + +local function burst(position: Vector3, normal: Vector3, texture: string, color: Color3, amount: number, speed: number, size: number, glow: boolean) + local anchor = decor(Vector3.one * 0.2, CFrame.lookAt(position, position + normal), color, Enum.Material.SmoothPlastic) + anchor.Transparency = 1 + local emitter = Instance.new("ParticleEmitter") + emitter.Texture = texture + emitter.Color = ColorSequence.new(color) + emitter.EmissionDirection = Enum.NormalId.Front + emitter.SpreadAngle = Vector2.new(60, 60) + emitter.Speed = NumberRange.new(speed * 0.5, speed) + emitter.Drag = 4 + emitter.Lifetime = NumberRange.new(0.4, 1) + emitter.Size = NumberSequence.new(size * 0.4, size) + emitter.Transparency = NumberSequence.new(0.35, 1) + emitter.LightEmission = if glow then 1 else 0.2 + emitter.Brightness = if glow then 3 else 1.5 + emitter.Rate = 0 + emitter.Parent = anchor + emitter:Emit(amount) + Debris:AddItem(anchor, 1.5) +end + +local function scatterChunks(position: Vector3, normal: Vector3, color: Color3, material: Enum.Material, count: number, strength: number) + for _ = 1, count do + local edge = random:NextNumber(0.2, 0.45) * (0.6 + strength) + local chunk = decor(Vector3.one * edge, CFrame.new(position + normal * 0.4) * CFrame.Angles(random:NextNumber(0, 6.3), random:NextNumber(0, 6.3), 0), color, material) + chunk.Anchored = false + chunk.CanCollide = true + chunk.CustomPhysicalProperties = PhysicalProperties.new(0.01, 0.5, 0.2) + local spread = Vector3.new(random:NextNumber(-1, 1), random:NextNumber(-0.2, 1), random:NextNumber(-1, 1)) * 0.7 + chunk.AssemblyLinearVelocity = (normal + spread).Unit * random:NextNumber(12, 24) * strength + track(chunks, chunk, MAX_CHUNKS) + fadeOut(chunk, 2, 0.8) + end +end + +local function markSurface(kind: string, position: Vector3, normal: Vector3, color: Color3, strength: number) + local base = CFrame.lookAt(position + normal * 0.03, position + normal * 2) * CFrame.Angles(0, 0, random:NextNumber(0, math.pi)) + local scarColor = if kind == "Shot" then color:Lerp(Color3.new(0, 0, 0), 0.75) else color:Lerp(Color3.fromRGB(215, 208, 196), 0.45) + if kind == "Shot" then + local hole = decor(Vector3.new(0.45, 0.45, 0.05), base, scarColor, Enum.Material.SmoothPlastic) + track(marks, hole, MAX_MARKS) + fadeOut(hole, MARK_LIFETIME, 2) + return + end + local length = 2.5 + 3 * strength + for offset = -1, 1 do + local tilt = CFrame.Angles(0, 0, math.rad(random:NextNumber(-7, 7))) + local scratch = decor(Vector3.new(0.13, length - math.abs(offset) * 0.7, 0.02), base * CFrame.new(offset * 0.5, 0, -0.02) * tilt, scarColor, Enum.Material.SmoothPlastic) + track(marks, scratch, MAX_MARKS) + fadeOut(scratch, MARK_LIFETIME, 2) + end +end + +local function hitStop(character: Model) + local animator = character:FindFirstChildWhichIsA("Animator", true) + if not animator then + return + end + local paused = {} + for _, track in animator:GetPlayingAnimationTracks() do + paused[track] = track.Speed + track:AdjustSpeed(0) + end + task.delay(0.07, function() + for track, speed in paused do + track:AdjustSpeed(speed) + end + end) +end + +local function hurtFlash() + local flash = Instance.new("ColorCorrectionEffect") + flash.TintColor = Color3.fromRGB(255, 190, 180) + flash.Brightness = 0.12 + flash.Parent = Lighting + TweenService:Create(flash, TweenInfo.new(0.25), { TintColor = Color3.new(1, 1, 1), Brightness = 0 }):Play() + Debris:AddItem(flash, 0.3) +end + +local function onImpact(kind: string, position: Vector3, normal: Vector3, color: Color3, material: Enum.Material, strength: number, source: Player?, target: Model?, surfaceAnchored: boolean?) + local camera = workspace.CurrentCamera + if not camera or (camera.CFrame.Position - position).Magnitude > CULL_DISTANCE then + return + end + local scale = if lowQuality() then 0.5 else 1 + local calm = calmMode() + local dust = color:Lerp(Color3.fromRGB(205, 198, 186), 0.6) + + if kind == "Body" then + burst(position, normal, SMOKE, dust, math.ceil(8 * scale), 6, 2.2, false) + if target and target == LocalPlayer.Character and not calm then + hurtFlash() + end + else + if surfaceAnchored then + markSurface(kind, position, normal, color, strength) + end + burst(position, normal, SMOKE, dust, math.ceil((if kind == "Shot" then 6 else 14) * scale), 9, 3 * strength + 1.2, false) + if SPARKY[material] then + burst(position, normal, SPARK, Color3.fromRGB(255, 190, 90), math.ceil(14 * scale), 26, 0.35, true) + else + scatterChunks(position, normal, color, material, math.ceil((if kind == "Shot" then 3 else 6) * strength * scale), strength) + end + end + + local character = LocalPlayer.Character + if source == LocalPlayer and character and not calm then + Utils.Player.ShakeCamera(0.35 * strength, 0.15) + if kind == "Body" then + hitStop(character) + end + end +end + +return { + Init = function(_) + folder.Parent = workspace + Network:SetConnection("Impact", "REMOTE_EVENT", onImpact) + end, +} diff --git a/src/StarterPlayerScripts/Miscellaneous/RoundTips.luau b/src/StarterPlayerScripts/Miscellaneous/RoundTips.luau new file mode 100644 index 00000000..68c60ef0 --- /dev/null +++ b/src/StarterPlayerScripts/Miscellaneous/RoundTips.luau @@ -0,0 +1,118 @@ +--!strict +local Players = game:GetService("Players") +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local TweenService = game:GetService("TweenService") +local UserInputService = game:GetService("UserInputService") + +local Utils = require(ReplicatedStorage.Modules.Utils) + +local LocalPlayer = Players.LocalPlayer +local RoundState: StringValue = ReplicatedStorage.RoundInfo.CurrentState + +local GUIDED_ROUNDS = 3 +local TIP_TIME = 5 + +local function keys(): { repair: string, sprint: string } + local input = UserInputService:GetLastInputType() + if input == Enum.UserInputType.Touch then + return { repair = "tap REPAIR", sprint = "tap the sprint button" } + elseif input.Name:find("Gamepad") then + return { repair = "press X", sprint = "hold L2" } + end + return { repair = "press C", sprint = "hold Shift" } +end + +local function tipsFor(role: string): { string } + local k = keys() + if role == "Killer" then + return { + "Hunt the survivors before the clock runs out. Every kill adds time.", + "A loud bang means a survivor failed a repair: that generator glows red for you.", + } + end + return { + `Repair generators: {k.repair} when the needle is in the green zone. Each one cuts 15 s off the clock.`, + `Run from the killer: {k.sprint}. Repair together to get a wider green zone.`, + "Survive until the clock hits zero. Medkits heal you.", + } +end + +return { + Init = function(_) + local gui = Instance.new("ScreenGui") + gui.Name = "RoundTips" + gui.ResetOnSpawn = false + gui.Parent = LocalPlayer:WaitForChild("PlayerGui") + local label = Instance.new("TextLabel") + label.AnchorPoint = Vector2.new(0.5, 0.5) + label.Position = UDim2.fromScale(0.5, 0.64) + label.Size = UDim2.fromScale(0.8, 0) + label.AutomaticSize = Enum.AutomaticSize.Y + local maxWidth = Instance.new("UISizeConstraint") + maxWidth.MaxSize = Vector2.new(620, math.huge) + maxWidth.Parent = label + label.BackgroundColor3 = Color3.fromRGB(10, 10, 14) + label.BackgroundTransparency = 1 + label.TextTransparency = 1 + label.TextColor3 = Color3.new(1, 1, 1) + label.FontFace = Font.new("rbxasset://fonts/families/AccanthisADFStd.json", Enum.FontWeight.Bold) + label.TextSize = 18 + label.TextWrapped = true + label.Parent = gui + Instance.new("UICorner").Parent = label + local padding = Instance.new("UIPadding") + padding.PaddingLeft = UDim.new(0, 10) + padding.PaddingRight = UDim.new(0, 10) + padding.PaddingTop = UDim.new(0, 4) + padding.PaddingBottom = UDim.new(0, 4) + padding.Parent = label + + local queue: { string } = {} + local showing = false + local function show(texts: { string }) + for _, text in texts do + table.insert(queue, text) + end + if showing then + return + end + showing = true + task.spawn(function() + while #queue > 0 do + label.Text = table.remove(queue, 1) :: string + TweenService:Create(label, TweenInfo.new(0.3), { TextTransparency = 0, BackgroundTransparency = 0.35 }):Play() + task.wait(TIP_TIME) + TweenService:Create(label, TweenInfo.new(0.3), { TextTransparency = 1, BackgroundTransparency = 1 }):Play() + task.wait(0.4) + end + showing = false + end) + end + + local function currentRole(): string? + local character = LocalPlayer.Character + local role = character and character:FindFirstChild("Role") :: StringValue? + return role and role.Value + end + + local roundsGuided = 0 + RoundState.Changed:Connect(function(state: string) + if state ~= "InRound" or roundsGuided >= GUIDED_ROUNDS then + return + end + task.wait(1) + local role = currentRole() + if role == "Survivor" or role == "Killer" then + roundsGuided += 1 + show(tipsFor(role)) + end + end) + + Utils.Character.ObserveCharacter(LocalPlayer, function(character: Model) + local role = character:WaitForChild("Role", 5) :: StringValue? + if role and role.Value == "Spectator" and RoundState.Value == "InRound" then + show({ "You're out. Spectate the others: the next round starts soon." }) + end + end) + end, +} diff --git a/src/StarterPlayerScripts/Miscellaneous/TerrorPulse.luau b/src/StarterPlayerScripts/Miscellaneous/TerrorPulse.luau new file mode 100644 index 00000000..b3ff18cf --- /dev/null +++ b/src/StarterPlayerScripts/Miscellaneous/TerrorPulse.luau @@ -0,0 +1,98 @@ +--!strict +local Players = game:GetService("Players") +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local RunService = game:GetService("RunService") + +local Types = require(ReplicatedStorage.Classes.Types) +local Utils = require(ReplicatedStorage.Modules.Utils) + +local LocalPlayer = Players.LocalPlayer +local RoundState: StringValue = ReplicatedStorage.RoundInfo.CurrentState + +local TERROR_RADIUS = 60 +local EDGE_COLOR = Color3.fromRGB(200, 20, 20) + +local function nearestKillerDistance(root: BasePart): number + local nearest = math.huge + for _, character in workspace.Players:GetChildren() do + local role = character:FindFirstChild("Role") :: StringValue? + local killerRoot = role and role.Value == "Killer" and Utils.Character.GetRootPart(character) + if killerRoot and not Utils.Instance.FindFirstChild(character, "Effects.Undetectable", 0) then + nearest = math.min(nearest, (killerRoot.Position - root.Position).Magnitude) + end + end + return nearest +end + +local function lowQuality(): boolean + local quality = UserSettings().GameSettings.SavedQualityLevel + return quality ~= Enum.SavedQualitySetting.Automatic and quality.Value <= 3 +end + +return { + Init = function(_) + local gui = Instance.new("ScreenGui") + gui.Name = "TerrorPulse" + gui.IgnoreGuiInset = true + gui.ResetOnSpawn = false + gui.Parent = LocalPlayer:WaitForChild("PlayerGui") + local edge = Instance.new("CanvasGroup") + edge.Size = UDim2.fromScale(1, 1) + edge.BackgroundTransparency = 1 + edge.GroupTransparency = 1 + edge.Parent = gui + for _, side in { + { UDim2.fromScale(1, 0.22), UDim2.fromScale(0, 0), 90 }, + { UDim2.fromScale(1, 0.22), UDim2.fromScale(0, 0.78), -90 }, + { UDim2.fromScale(0.16, 1), UDim2.fromScale(0, 0), 0 }, + { UDim2.fromScale(0.16, 1), UDim2.fromScale(0.84, 0), 180 }, + } do + local band = Instance.new("Frame") + band.Size = side[1] + band.Position = side[2] + band.BackgroundColor3 = EDGE_COLOR + band.BorderSizePixel = 0 + band.Parent = edge + local fade = Instance.new("UIGradient") + fade.Transparency = NumberSequence.new(0, 1) + fade.Rotation = side[3] + fade.Parent = band + end + + Utils.Character.ObserveCharacter(LocalPlayer, function(character: Model, janitor: Types.Janitor) + local role = character:WaitForChild("Role", 5) :: StringValue? + local root = Utils.Character.GetRootPart(character) + if not role or not root then + return + end + + if role.Value ~= "Spectator" then + local light = Instance.new("PointLight") + light.Name = "PresenceLight" + light.Range = 14 + light.Brightness = if lowQuality() then 0.8 else 0.35 + light.Color = Color3.fromRGB(190, 200, 230) + light.Shadows = false + light.Parent = root + end + + janitor:Add(RunService.PreRender:Connect(function() + if role.Value ~= "Survivor" or RoundState.Value ~= "InRound" then + edge.GroupTransparency = 1 + return + end + local closeness = 1 - math.clamp(nearestKillerDistance(root) / TERROR_RADIUS, 0, 1) + if closeness <= 0 then + edge.GroupTransparency = 1 + return + end + local calm = Utils.PlayerData.GetPlayerSetting(LocalPlayer, "Accessibility.EpilepsyMode") == true + local beat = if calm then 0 else (math.sin(os.clock() * math.pi * 2 * (0.9 + 1.6 * closeness)) + 1) / 2 + edge.GroupTransparency = 1 - closeness * (0.25 + 0.2 * beat) + end)) + janitor:Add(function() + edge.GroupTransparency = 1 + end, true) + end) + end, +} diff --git a/tools/maps/Instalacion.luau b/tools/maps/Instalacion.luau index aaa5aedc..cf72026f 100644 --- a/tools/maps/Instalacion.luau +++ b/tools/maps/Instalacion.luau @@ -229,6 +229,27 @@ generator("ShedNorthWest", Vector3.new(-73, 0, -74), 90) generator("ShedSouthEast", Vector3.new(72, 0, 70), -90) generator("Yard", Vector3.new(8, 0, 84), 0) +local Props = folder("ReactiveProps", MapModel) +local function looseProp(pos: Vector3, barrel: boolean) + local size = if barrel then Vector3.new(2.6, 2, 2) else Vector3.new(2, 2, 2) + local p = part(Props, size, pos + Vector3.new(0, size.Y / 2, 0), if barrel then RUST else WOOD, if barrel then Enum.Material.CorrodedMetal else Enum.Material.WoodPlanks, if barrel then "Barrel" else "Crate") + if barrel then + p.Shape = Enum.PartType.Cylinder + p.CFrame *= CFrame.Angles(0, 0, math.rad(90)) + end + p.Anchored = false + p:AddTag("Reactive") +end +for _, spot in { + {Vector3.new(-26, 0, 12), false}, {Vector3.new(-25, 0, 20), true}, + {Vector3.new(26, 0, -12), true}, {Vector3.new(18, 0, -20), false}, + {Vector3.new(-69, 0, -78), false}, {Vector3.new(68, 0, 66), true}, + {Vector3.new(4, 0, 88), true}, {Vector3.new(13, 0, 80), false}, + {Vector3.new(-40, 0, 60), false}, {Vector3.new(45, 0, -55), true}, +} do + looseProp(spot[1], spot[2]) +end + local SpawnPoints = folder("SpawnPoints", MapModel) local function spawnPoint(parent: Instance, pos: Vector3) local p = part(parent, Vector3.new(1, 1, 1), pos + Vector3.new(0, 3, 0), CONCRETE, Enum.Material.SmoothPlastic, if parent.Name == "Killers" then "KillerSpawn" else "SurvivorSpawn") From 7145680a66e1f78024e2763ba2d97831cb1910a7 Mon Sep 17 00:00:00 2001 From: Norl4 Date: Sun, 1 Nov 2026 11:54:23 +0100 Subject: [PATCH 09/32] Add Studio regression battery and wait for respawned GUIs tools/test.py syncs src into Studio, plays a survivor and a killer round and checks generators, shots, slashes, reactive props, the terror edge, round tips and script errors, then restores the place. The battery caught an intermittent race where character UI indexed ResetOnSpawn ScreenGuis before they were recloned. --- .../Classes/Ability/AbilityGUI.luau | 2 +- .../Classes/Item/ItemGUI.luau | 2 +- .../UI/CharacterStatsManager.luau | 2 +- src/StarterPlayerScripts/UI/EffectViewer.luau | 2 +- tools/test.py | 241 ++++++++++++++++++ 5 files changed, 245 insertions(+), 4 deletions(-) create mode 100644 tools/test.py diff --git a/src/ReplicatedStorage/Classes/Ability/AbilityGUI.luau b/src/ReplicatedStorage/Classes/Ability/AbilityGUI.luau index cdfb8456..4bd67473 100644 --- a/src/ReplicatedStorage/Classes/Ability/AbilityGUI.luau +++ b/src/ReplicatedStorage/Classes/Ability/AbilityGUI.luau @@ -45,7 +45,7 @@ function AbilityGUI.InitGUI() --creating the entire gui local Container = Prefabs.Container:Clone() Container.Name = Character:GetAttribute("CharacterName") - Container.Parent = Players.LocalPlayer.PlayerGui.CharacterGUI + Container.Parent = Players.LocalPlayer.PlayerGui:WaitForChild("CharacterGUI") local Abilities: {Ability} = {} diff --git a/src/ReplicatedStorage/Classes/Item/ItemGUI.luau b/src/ReplicatedStorage/Classes/Item/ItemGUI.luau index 5cd93c06..3c16694c 100644 --- a/src/ReplicatedStorage/Classes/Item/ItemGUI.luau +++ b/src/ReplicatedStorage/Classes/Item/ItemGUI.luau @@ -34,7 +34,7 @@ function ItemGUI.InitGUI() local Container = Prefabs.Container:Clone() Container.Name = "Backpack" - Container.Parent = Players.LocalPlayer.PlayerGui.CharacterGUI + Container.Parent = Players.LocalPlayer.PlayerGui:WaitForChild("CharacterGUI") local JanitorInstance = Janitor.new() JanitorInstance:LinkToInstances(Container, Character) diff --git a/src/StarterPlayerScripts/UI/CharacterStatsManager.luau b/src/StarterPlayerScripts/UI/CharacterStatsManager.luau index 35989289..a2e89317 100644 --- a/src/StarterPlayerScripts/UI/CharacterStatsManager.luau +++ b/src/StarterPlayerScripts/UI/CharacterStatsManager.luau @@ -27,7 +27,7 @@ function CharacterStatsManager._InitUI(Char: Model, UI: Frame) if Char:FindFirstChild("Role").Value == "Spectator" then return end CharacterStatsManager.UI = UI - UI.Parent = LocalPlayer.PlayerGui.PlayerStats + UI.Parent = LocalPlayer.PlayerGui:WaitForChild("PlayerStats") local Stamina = UI.Stamina local StaminaText = Stamina.TextLabel diff --git a/src/StarterPlayerScripts/UI/EffectViewer.luau b/src/StarterPlayerScripts/UI/EffectViewer.luau index 8b3a08c4..4435677b 100644 --- a/src/StarterPlayerScripts/UI/EffectViewer.luau +++ b/src/StarterPlayerScripts/UI/EffectViewer.luau @@ -23,7 +23,7 @@ function EffectViewer._SetupUI(UI: Frame, char: Model) return end - UI.Parent = LocalPlayer.PlayerGui.Effects + UI.Parent = LocalPlayer.PlayerGui:WaitForChild("Effects") local EffectManager = require(char.PlayerAttributeScripts.EffectManager) diff --git a/tools/test.py b/tools/test.py new file mode 100644 index 00000000..44ba832a --- /dev/null +++ b/tools/test.py @@ -0,0 +1,241 @@ +import json, os, pathlib, subprocess, sys, time + +ROOT = pathlib.Path(__file__).resolve().parent.parent +SERVICES = {"ReplicatedStorage": "ReplicatedStorage", "ReplicatedFirst": "ReplicatedFirst", "ServerScriptService": "ServerScriptService", + "StarterPlayerScripts": "StarterPlayer.StarterPlayerScripts", "StarterCharacterScripts": "StarterPlayer.StarterCharacterScripts", + "ServerStorage": "ServerStorage", "StarterGui": "StarterGui", "Workspace": "Workspace", "SoundService": "SoundService"} +proc = subprocess.Popen(["cmd.exe", "/c", os.path.expandvars(r"%LOCALAPPDATA%\Roblox\mcp.bat")], stdin=subprocess.PIPE, + stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True, encoding="utf-8") +next_id = 0 +results = [] + + +def call(method, params=None): + global next_id + next_id += 1 + proc.stdin.write(json.dumps({"jsonrpc": "2.0", "id": next_id, "method": method, "params": params or {}}) + "\n") + proc.stdin.flush() + while True: + msg = json.loads(proc.stdout.readline()) + if msg.get("id") == next_id: + return msg + + +def tool(name, **args): + result = call("tools/call", {"name": name, "arguments": args}).get("result", {}) + text = "\n".join(part.get("text", "") for part in result.get("content", [])) + return ("ERROR " if result.get("isError") else "") + text + + +call("initialize", {"protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": {"name": "anomalis-test", "version": "0"}}) +proc.stdin.write(json.dumps({"jsonrpc": "2.0", "method": "notifications/initialized"}) + "\n") +proc.stdin.flush() +SID = sys.argv[1] if len(sys.argv) > 1 else next(s["id"] for s in json.loads(tool("list_roblox_studios"))["studios"] if "Dysymmetrical" in s["name"]) + + +def luau(code, dm="Server"): + return tool("execute_luau", studio_id=SID, datamodel_type=dm, code=code).strip() + + +def lit(text): + n = 3 + while "]" + "=" * n + "]" in text: + n += 1 + return "[" + "=" * n + "[" + text + "]" + "=" * n + "]" + + +def sync(): + items = [] + for folder, service in SERVICES.items(): + for file in sorted((ROOT / "src" / folder).rglob("*.luau")): + parts = list(file.relative_to(ROOT / "src" / folder).parts) + name, cls = parts[-1][:-5], "ModuleScript" + for suffix, kind in ((".server", "Script"), (".client", "LocalScript")): + if name.endswith(suffix): + name, cls = name[: -len(suffix)], kind + path = service.split(".") + parts[:-1] + ([] if name == "init" else [name]) + items.append(f'go({{{",".join(chr(34) + p + chr(34) for p in path)}}}, "{cls}", {lit(file.read_text(encoding="utf-8"))})') + head = '''local changed = 0 +local function go(path, cls, src) + local node = game:GetService(path[1]) + for k = 2, #path do + node = node:FindFirstChild(path[k]) or Instance.new(k == #path and cls or "Folder", node) + node.Name = path[k] + end + if node:IsA("LuaSourceContainer") and node.Source ~= src then node.Source = src changed += 1 end +end +''' + total = 0 + for i in range(0, len(items), 20): + out = luau(head + "\n".join(items[i:i + 20]) + "\nreturn changed", "Edit") + if out.startswith("ERROR"): + sys.exit("sync failed: " + out[-300:]) + total += int(out) + return total + + +def check(name, ok, detail=""): + results.append((name, bool(ok), detail)) + print(("PASS " if ok else "FAIL ") + name + (f" ({detail})" if detail else ""), flush=True) + + +def wait_for(code, want, dm="Server", timeout=60): + end = time.time() + timeout + while time.time() < end: + out = luau(code, dm) + if want in out: + return out + time.sleep(1.5) + return out + + +def play(start): + tool("start_stop_play", studio_id=SID, is_start=start) + + +def debug_round(as_killer): + luau(f'''local m = game.ServerScriptService.Managers +m.TimeManager.Source = m.TimeManager.Source:gsub("LobbyTime = 45,", "LobbyTime = 5,"):gsub("MinimumPlayerAmount = 2,", "MinimumPlayerAmount = 1,") +m.RoundManager.Source = m.RoundManager.Source:gsub("CanSpawnKiller = %a+,","CanSpawnKiller = {str(as_killer).lower()},") +workspace:SetAttribute("KillersAllowed", {int(as_killer)})''', "Edit") + play(True) + role = "Killer" if as_killer else "Survivor" + state = wait_for('''local c = game.Players:GetPlayers()[1] and game.Players:GetPlayers()[1].Character +return game.ReplicatedStorage.RoundInfo.CurrentState.Value .. " " .. (c and c:FindFirstChild("Role") and c.Role.Value or "-")''', "InRound " + role) + check(f"round starts as {role}", "InRound " + role in state, state) + luau('workspace:SetAttribute("FreezeTime", true)') + luau('''_G.TestLog = {} +game.ReplicatedStorage.Modules.Network.REMOTE_EVENT.Impact.OnClientEvent:Connect(function(kind, _, _, _, _, _, _, target) + table.insert(_G.TestLog, kind .. (target and (":" .. target.Name) or "")) +end)''', "Client") + return "InRound " + role in state + + +def log(): + return luau('return table.concat(_G.TestLog, ",")', "Client") + + +def console_errors(): + lines = tool("get_console_output", studio_id=SID).splitlines() + return sorted({lines[i - 1].strip() + " @ " + lines[i + 1].strip() for i, line in enumerate(lines[:-1]) if i and "Stack Begin" in line}) + + +def survivor_phase(): + if not debug_round(False): + return + total = luau('return workspace:GetAttribute("GeneratorsTotal")') + check("map has 5 generators", total == "5", total) + time.sleep(1) + tip = luau('local l = game.Players.LocalPlayer.PlayerGui.RoundTips:FindFirstChildOfClass("TextLabel") return l.Text .. "|" .. tostring(l.TextFits)', "Client") + check("round tip shows and fits", tip.split("|")[0] != "" and tip.endswith("true"), tip[:60]) + + generators = luau('local n = {} for _, g in workspace.Map:FindFirstChild("Generators", true):GetChildren() do table.insert(n, g.Name) end return table.concat(n, ",")').split(",") + for name, mode in ((generators[0], "hit"), (generators[1], "miss")): + luau(f'''local panel = workspace.Map:FindFirstChild("Generators", true)["{name}"].Panel +local spot = panel.Position + panel.CFrame.LookVector * 4 +game.Players:GetPlayers()[1].Character:PivotTo(CFrame.lookAt(spot, panel.Position * Vector3.new(1, 0, 1) + Vector3.new(0, spot.Y, 0)))''') + time.sleep(0.5) + luau(f'''_G.GenEnded = false +local events = game.ReplicatedStorage.Modules.Network.REMOTE_EVENT +local input = events.GeneratorCheckInput +if _G.GenConn then _G.GenConn:Disconnect() _G.EndConn:Disconnect() end +_G.GenConn = events.GeneratorCheck.OnClientEvent:Connect(function(delay, sweep, zone, width) + task.delay(if "{mode}" == "hit" then delay + (zone + width / 2) * sweep else 0, function() input:FireServer() end) +end) +_G.EndConn = events.GeneratorCheckEnded.OnClientEvent:Connect(function() _G.GenEnded = true end) +local prompt = workspace.Map:FindFirstChild("Generators", true)["{name}"].Panel:FindFirstChildOfClass("ProximityPrompt") +prompt:InputHoldBegin() task.wait(0.1) prompt:InputHoldEnd()''', "Client") + if mode == "hit": + out = wait_for(f'local g = workspace.Map:FindFirstChild("Generators", true)["{name}"] return tostring(g:GetAttribute("Repaired")) .. " " .. tostring(g:GetAttribute("Progress")) .. " " .. tostring(workspace:GetAttribute("GeneratorsRepaired"))', "true", timeout=25) + check("generator repairs in 6 checks", out.startswith("true 6 1"), out) + else: + out = wait_for('return tostring(_G.GenEnded)', "true", "Client", timeout=6) + progress = luau(f'return tostring(workspace.Map:FindFirstChild("Generators", true)["{name}"]:GetAttribute("Progress"))') + check("missed check fails the repair", out == "true" and progress == "0", f"ended={out} progress={progress}") + + luau(f'''local panel = workspace.Map:FindFirstChild("Generators", true)["{generators[2]}"].Panel +local spot = panel.Position + panel.CFrame.LookVector * 12 +game.Players:GetPlayers()[1].Character:PivotTo(CFrame.lookAt(spot, panel.Position * Vector3.new(1, 0, 1) + Vector3.new(0, spot.Y, 0)))''') + reloading = 'return tostring(game.Players:GetPlayers()[1].Character:FindFirstChild("Slowness", true) ~= nil)' + for _ in range(2): + tool("user_keyboard_input", studio_id=SID, datamodel_type="Client", actions=[{"action": "keyPress", "key_code": "E"}, {"action": "wait", "wait_time_ms": 500}]) + if luau(reloading) == "true": + break + reloaded = luau(reloading) + tool("user_keyboard_input", studio_id=SID, datamodel_type="Client", actions=[{"action": "wait", "wait_time_ms": 5500}, {"action": "keyPress", "key_code": "R"}, {"action": "wait", "wait_time_ms": 2500}]) + check("Commander shot leaves a surface impact", "Shot" in log(), f"reload started={reloaded} log={log()}") + + luau('''local root = game.Players:GetPlayers()[1].Character.PrimaryPart +local fake = Instance.new("Model") fake.Name = "TestKiller" fake:SetAttribute("CharacterName", "NullexVoyd") +local part = Instance.new("Part") part.Name = "HumanoidRootPart" part.Anchored = true part.CanCollide = false part.Size = Vector3.new(2, 5, 2) +part.CFrame = root.CFrame * CFrame.new(0, 0, -15) part.Parent = fake fake.PrimaryPart = part +local role = Instance.new("StringValue") role.Name = "Role" role.Value = "Killer" role.Parent = fake +fake.Parent = workspace.Players''') + time.sleep(1) + near = luau('local e = game.Players.LocalPlayer.PlayerGui.TerrorPulse:FindFirstChildOfClass("CanvasGroup") local lo, hi = 1, 0 for _ = 1, 8 do lo = math.min(lo, e.GroupTransparency) hi = math.max(hi, e.GroupTransparency) task.wait(0.1) end return string.format("%.2f %.2f", lo, hi)', "Client") + luau('workspace.Players.TestKiller:Destroy()') + time.sleep(0.5) + far = luau('return string.format("%.2f", game.Players.LocalPlayer.PlayerGui.TerrorPulse:FindFirstChildOfClass("CanvasGroup").GroupTransparency)', "Client") + lo, hi = map(float, near.split()) + check("terror edge pulses near the killer", lo < 0.9 and hi - lo > 0.03 and far == "1.00", f"near {near}, gone {far}") + errors = console_errors() + check("no script errors (survivor)", not errors, "; ".join(errors)[:300]) + play(False) + + +def slash(): + tool("user_mouse_input", studio_id=SID, datamodel_type="Client", actions=[{"action": "moveTo", "x": 500, "y": 300}, {"action": "mouseButtonClick", "mouse_button": "left"}, {"action": "wait", "wait_time_ms": 1500}]) + + +def killer_phase(): + if not debug_round(True): + return + luau('''local panel = workspace.Map:FindFirstChild("Generators", true):GetChildren()[1].Panel +local base = panel.Position + panel.CFrame.LookVector * 14 +local char = game.Players:GetPlayers()[1].Character +char:PivotTo(CFrame.lookAt(base, base + Vector3.new(-5, 0, 0))) +local crate = workspace.Map:FindFirstChild("ReactiveProps", true):GetChildren()[1] +crate.AssemblyLinearVelocity = Vector3.zero +crate.CFrame = CFrame.new(char.PrimaryPart.Position + char.PrimaryPart.CFrame.LookVector * 3.5 + Vector3.new(0, -1.5, 0)) +_G.TestCrate = crate _G.CrateStart = crate.Position''') + time.sleep(1) + luau('_G.CrateStart = _G.TestCrate.Position') + slash() + moved = luau('return string.format("%.2f", (_G.TestCrate.Position - _G.CrateStart).Magnitude)') + check("slash pushes a reactive crate", float(moved) > 0.5, moved + " studs") + check("slash leaves a surface impact", "Melee" in log(), log()) + time.sleep(1.5) + health = luau('''local p = game.Players:GetPlayers()[1] +p.Character.Archivable = true +local dummy = p.Character:Clone() +p.Character.Archivable = false +dummy.Name = "TestSurvivor" +for _, s in dummy:GetDescendants() do if s:IsA("BaseScript") then s:Destroy() end end +dummy.Role.Value = "Survivor" +local root = p.Character.PrimaryPart +dummy:PivotTo(root.CFrame * CFrame.new(0, 0, -3) * CFrame.Angles(0, math.pi, 0)) +dummy.Parent = workspace.Players +_G.TestDummy = dummy +return tostring(dummy:FindFirstChildOfClass("Humanoid").Health)''') + slash() + after = luau('return tostring(_G.TestDummy:FindFirstChildOfClass("Humanoid").Health)') + check("slash damages a survivor", float(after) < float(health), f"{health} -> {after}") + check("body hit reaches clients", "Body:TestSurvivor" in log(), log()) + errors = console_errors() + check("no script errors (killer)", not errors, "; ".join(errors)[:300]) + play(False) + + +try: + play(False) + print("synced", sync(), "scripts", flush=True) + survivor_phase() + killer_phase() +finally: + play(False) + sync() + luau('workspace:SetAttribute("KillersAllowed", 1) workspace:SetAttribute("FreezeTime", nil)', "Edit") + proc.kill() +failed = [name for name, ok, _ in results if not ok] +print(f"\n{len(results) - len(failed)}/{len(results)} passed" + (": FAILED " + ", ".join(failed) if failed else "")) +sys.exit(1 if failed else 0) From 8c5bf2a3162c956380cdedcbf7c2f515f1782240 Mon Sep 17 00:00:00 2001 From: Norl4 Date: Sun, 1 Nov 2026 12:10:02 +0100 Subject: [PATCH 10/32] Give hit survivors a one-time escape burst and fix Resistance A survivor who lives through a killer hit gets Speed I for 2 s and Resistance II for 8 s. The protection is not refreshed by later hits and never lowers a stronger or longer effect. Resistance never worked in the engine: the hitbox looked for it in the wrong place and DamagePlayer did math on the instance. Both now share CommonFunctions.ResistedDamage, applied once after the character's OnHit so the kill check matches the real damage. The battery covers all of it (19 checks). --- src/ReplicatedStorage/Classes/Hitbox.luau | 22 +++++++++--- .../System/CommonFunctions.luau | 10 +++--- tools/test.py | 35 +++++++++++++++++++ 3 files changed, 58 insertions(+), 9 deletions(-) diff --git a/src/ReplicatedStorage/Classes/Hitbox.luau b/src/ReplicatedStorage/Classes/Hitbox.luau index 63e7738b..ce014fd6 100644 --- a/src/ReplicatedStorage/Classes/Hitbox.luau +++ b/src/ReplicatedStorage/Classes/Hitbox.luau @@ -33,6 +33,10 @@ local Hitbox = { EffectsToApply = {}, Connections = {}, }, + SurvivorEscapeEffects = { + { Name = "Speed", Level = 1, Duration = 2 }, + { Name = "Resistance", Level = 2, Duration = 8 }, + }, } --- Creates a new hitbox, assigning it to a specific player. @@ -326,16 +330,13 @@ function Hitbox.New(SourcePlayer: Player, Config: Types.HitboxSettings): Types.H -- Look for critical parts in the character in question, as well as the resistance effect, if applicable. local TargetCharacter = Humanoid.Parent local HRoot = TargetCharacter and TargetCharacter:IsA("Model") and Utils.Character.GetRootPart(TargetCharacter) - local Resistance = TargetCharacter and TargetCharacter:FindFirstChild("Resistance") and TargetCharacter.Effects:FindFirstChild("Resistance") and TargetCharacter.Effects["Resistance"].Value or 0 - - -- Configure damage to be affected by the Resistance effect. - local Damage = Config.Damage * (1 - Resistance / 5) + local Damage = CommonFunctions.ResistedDamage(TargetCharacter, Config.Damage) local function DamageHumanoid() CommonFunctions.DamagePlayer({ SourcePlayer = SourcePlayer, TargetHumanoid = Humanoid, - Damage = Damage, + Damage = Config.Damage, Reason = Config.Reason, KnockbackDirection = CheckPart.CFrame.LookVector, KnockbackMagnitude = Config.Knockback, @@ -364,6 +365,7 @@ function Hitbox.New(SourcePlayer: Player, Config: Types.HitboxSettings): Types.H end end + Damage = CommonFunctions.ResistedDamage(TargetCharacter, Config.Damage) -- If theres a function set to be executed on player hit for this hitbox, toss the humanoid to it. Hitbox._RunCallback(Config.Connections, "Hit", Config, Humanoid, Index == 1) @@ -531,6 +533,16 @@ function Hitbox.New(SourcePlayer: Player, Config: Types.HitboxSettings): Types.H -- Inflict the hitbox's damage if possible and do some knockback if existent. DamageHumanoid() + local TargetRole = TargetCharacter:FindFirstChild("Role") :: StringValue? + if Char.Role.Value == "Killer" and TargetRole and TargetRole.Value == "Survivor" and Humanoid.Health > 0 then + for _, Effect in Hitbox.SurvivorEscapeEffects do + local Existing = Utils.Instance.FindFirstChild(TargetCharacter, "Effects." .. Effect.Name, 0) :: NumberValue? + if not Existing or Existing.Value < Effect.Level or (Existing.Value == Effect.Level and (Existing:GetAttribute("Duration") or 0) < Effect.Duration) then + CommonFunctions.ApplyEffect({ TargetHumanoid = Humanoid, EffectSettings = Effect }) + end + end + end + -- If the hitbox applies effects, apply them. if Config.EffectsToApply then for _, Effect in Config.EffectsToApply do diff --git a/src/ServerScriptService/System/CommonFunctions.luau b/src/ServerScriptService/System/CommonFunctions.luau index d2d82d7a..59248b3c 100644 --- a/src/ServerScriptService/System/CommonFunctions.luau +++ b/src/ServerScriptService/System/CommonFunctions.luau @@ -88,6 +88,11 @@ type DamagePlayerSettings = { } --- Function that should be used to damage a player from another player's attack. --- Handles resistance, weakness... +function CommonFunctions.ResistedDamage(Char: Model, Damage: number): number + local Resistance = Utils.Instance.FindFirstChild(Char, "Effects.Resistance", 0) :: NumberValue? + return Damage * math.clamp(1 - (if Resistance then Resistance.Value else 0) / 5, 0, 1) +end + function CommonFunctions.DamagePlayer(DamageSettings: DamagePlayerSettings) local Char = DamageSettings.TargetHumanoid.Parent local HRP: BasePart = Utils.Character.GetRootPart(Char) @@ -109,10 +114,7 @@ function CommonFunctions.DamagePlayer(DamageSettings: DamagePlayerSettings) Char.Killer.Value = DamageSettings.SourcePlayer end - local ResistanceValue = Utils.Instance.FindFirstChild(Char, "Effects.Resistance", 0) - if ResistanceValue then - DamageSettings.Damage /= 0.1 * ResistanceValue + 1 --since lvl 1 should do the trick already - end + DamageSettings.Damage = CommonFunctions.ResistedDamage(Char, DamageSettings.Damage) -- "John Doe, scream if you love The Masquerade." "RAAGGGGGGHHHHHH" if Char:GetAttribute("CharacterName") then diff --git a/tools/test.py b/tools/test.py index 44ba832a..8c5b66c1 100644 --- a/tools/test.py +++ b/tools/test.py @@ -211,6 +211,7 @@ def killer_phase(): p.Character.Archivable = false dummy.Name = "TestSurvivor" for _, s in dummy:GetDescendants() do if s:IsA("BaseScript") then s:Destroy() end end +if dummy:FindFirstChild("Effects") then dummy.Effects:ClearAllChildren() end dummy.Role.Value = "Survivor" local root = p.Character.PrimaryPart dummy:PivotTo(root.CFrame * CFrame.new(0, 0, -3) * CFrame.Angles(0, math.pi, 0)) @@ -221,6 +222,40 @@ def killer_phase(): after = luau('return tostring(_G.TestDummy:FindFirstChildOfClass("Humanoid").Health)') check("slash damages a survivor", float(after) < float(health), f"{health} -> {after}") check("body hit reaches clients", "Body:TestSurvivor" in log(), log()) + effects = luau('local e = _G.TestDummy:FindFirstChild("Effects") _G.FirstResistance = e and e:FindFirstChild("Resistance") return e and tostring(e:FindFirstChild("Speed") ~= nil) .. " " .. tostring(e:FindFirstChild("Resistance") ~= nil) or "no folder"') + check("hit survivor gets escape speed and protection", effects == "true true", effects) + time.sleep(1.5) + slash() + second = luau('return tostring(_G.TestDummy:FindFirstChildOfClass("Humanoid").Health)') + first_hit, second_hit = float(health) - float(after), float(after) - float(second) + check("protected survivor takes 60% damage", first_hit > 0 and abs(second_hit - first_hit * 0.6) < 0.5, f"{first_hit:g} then {second_hit:g}") + same = luau('return tostring(_G.TestDummy.Effects:FindFirstChild("Resistance") == _G.FirstResistance)') + check("protection is not refreshed by later hits", same == "true", same) + time.sleep(1.5) + armored = luau('''_G.TestDummy:Destroy() +local p = game.Players:GetPlayers()[1] +p.Character.Archivable = true +local dummy = p.Character:Clone() +p.Character.Archivable = false +dummy.Name = "TestArmored" +for _, s in dummy:GetDescendants() do if s:IsA("BaseScript") then s:Destroy() end end +dummy.Effects:ClearAllChildren() +dummy.Role.Value = "Survivor" +if dummy:FindFirstChild("PlayerAbilities") then dummy.PlayerAbilities:Destroy() end +local abilities = Instance.new("Folder", dummy) abilities.Name = "PlayerAbilities" +local module = Instance.new("ModuleScript", abilities) module.Name = "CharacterAbilities" +module.Source = [[return { CharacterModule = { OnHit = function(_, _, _, humanoid) + local armor = Instance.new("NumberValue") armor.Name = "Resistance" armor.Value = 2 armor:SetAttribute("Duration", 1) armor.Parent = humanoid.Parent.Effects +end } }]] +dummy:FindFirstChildOfClass("Humanoid").Health = 15 +dummy:PivotTo(p.Character.PrimaryPart.CFrame * CFrame.new(0, 0, -3) * CFrame.Angles(0, math.pi, 0)) +dummy.Parent = workspace.Players +_G.TestDummy = dummy +return "ok"''') + slash() + survived = luau('local d = _G.TestDummy local r = d.Effects:FindFirstChild("Resistance") return string.format("%.0f %s %s", d:FindFirstChildOfClass("Humanoid").Health, tostring(d.Effects:FindFirstChild("Speed") ~= nil), tostring(r and r:GetAttribute("Duration")))') + check("on-hit armor counts before the kill check", armored == "ok" and survived.startswith("3 true"), survived) + check("short armor is upgraded to the 8 s escape protection", survived.endswith(" 8"), survived) errors = console_errors() check("no script errors (killer)", not errors, "; ".join(errors)[:300]) play(False) From 9274f2b3006ff82d18f3f704e671fa557f646e96 Mon Sep 17 00:00:00 2001 From: Norl4 Date: Sun, 1 Nov 2026 12:44:44 +0100 Subject: [PATCH 11/32] Expand Instalacion into a full research facility layout --- tools/maps/Instalacion.luau | 195 +++++++++++++++++++++++++++--------- tools/test.py | 4 +- 2 files changed, 147 insertions(+), 52 deletions(-) diff --git a/tools/maps/Instalacion.luau b/tools/maps/Instalacion.luau index cf72026f..e12ffe85 100644 --- a/tools/maps/Instalacion.luau +++ b/tools/maps/Instalacion.luau @@ -143,66 +143,158 @@ local function lamp(parent: Instance, pos: Vector3, flicker: boolean, ceiling: b end end -part(MapModel, Vector3.new(210, 2, 210), Vector3.new(0, -1, 0), Color3.fromRGB(74, 76, 82), Enum.Material.Asphalt, "Ground") - -wall(Yard, Vector2.new(-100, -100), Vector2.new(100, -100), 20, nil, DARK_CONCRETE) -wall(Yard, Vector2.new(-100, 100), Vector2.new(100, 100), 20, nil, DARK_CONCRETE) -wall(Yard, Vector2.new(-100, -100), Vector2.new(-100, 100), 20, nil, DARK_CONCRETE) -wall(Yard, Vector2.new(100, -100), Vector2.new(100, 100), 20, nil, DARK_CONCRETE) -for _, side in { Vector3.new(0, 45, -101), Vector3.new(0, 45, 101), Vector3.new(-101, 45, 0), Vector3.new(101, 45, 0) } do - local barrier = part(Yard, if side.X == 0 then Vector3.new(204, 50, 1) else Vector3.new(1, 50, 204), side, CONCRETE, Enum.Material.SmoothPlastic, "Barrier") +local HALF = Vector2.new(160, 140) +part(MapModel, Vector3.new(HALF.X * 2 + 20, 2, HALF.Y * 2 + 20), Vector3.new(0, -1, 0), Color3.fromRGB(74, 76, 82), Enum.Material.Asphalt, "Ground") + +wall(Yard, Vector2.new(-HALF.X, -HALF.Y), Vector2.new(HALF.X, -HALF.Y), 20, nil, DARK_CONCRETE) +wall(Yard, Vector2.new(-HALF.X, HALF.Y), Vector2.new(HALF.X, HALF.Y), 20, { { 0, 18 } }, DARK_CONCRETE) +wall(Yard, Vector2.new(-HALF.X, -HALF.Y), Vector2.new(-HALF.X, HALF.Y), 20, nil, DARK_CONCRETE) +wall(Yard, Vector2.new(HALF.X, -HALF.Y), Vector2.new(HALF.X, HALF.Y), 20, nil, DARK_CONCRETE) +part(Yard, Vector3.new(18, 12, 0.6), Vector3.new(0, 6, HALF.Y), METAL, Enum.Material.DiamondPlate, "MainGate") +for _, side in { Vector3.new(0, 45, -HALF.Y - 1), Vector3.new(0, 45, HALF.Y + 1), Vector3.new(-HALF.X - 1, 45, 0), Vector3.new(HALF.X + 1, 45, 0) } do + local barrier = part(Yard, if side.X == 0 then Vector3.new(HALF.X * 2 + 4, 50, 1) else Vector3.new(1, 50, HALF.Y * 2 + 4), side, CONCRETE, Enum.Material.SmoothPlastic, "Barrier") barrier.Transparency = 1 end -room(Building, "MainBuilding", Vector2.new(0, 0), Vector2.new(60, 50), 14, { - north = { { 0, 8 } }, - south = { { -15, 8 }, { 15, 8 } }, - west = { { 0, 8 } }, - east = { { 10, 8 } }, +local Lab = folder("LabComplex", Building) +room(Lab, "Shell", Vector2.new(-35, -80), Vector2.new(150, 80), 16, { + north = { { -85, 8 } }, + south = { { -95, 8 }, { -45, 12 }, { 15, 8 } }, + west = { { -80, 10 } }, + east = { { -80, 10 } }, }) -local InteriorWalls = Instance.new("Model") -InteriorWalls.Name = "InteriorWalls" -InteriorWalls.Parent = Building -wall(InteriorWalls, Vector2.new(-10, -25), Vector2.new(-10, 25), 14, { { -12, 6 }, { 12, 6 } }) -wall(InteriorWalls, Vector2.new(10, -25), Vector2.new(10, 25), 14, { { -5, 6 }, { 18, 6 } }) -wall(InteriorWalls, Vector2.new(10, 0), Vector2.new(30, 0), 14, { { 20, 6 } }) -for _, p in { Vector3.new(0, 8, -32), Vector3.new(-15, 8, 32), Vector3.new(15, 8, 32), Vector3.new(-37, 8, 0), Vector3.new(37, 8, 10), Vector3.new(-70, 7, -55), Vector3.new(55, 7, 65) } do - fillLight(Building, p) +wall(Lab, Vector2.new(-110, -85), Vector2.new(40, -85), 16, { { -85, 8 }, { -35, 10 }, { 15, 8 } }) +wall(Lab, Vector2.new(-110, -75), Vector2.new(40, -75), 16, { { -92, 8 }, { -45, 12 }, { 12, 8 } }) +wall(Lab, Vector2.new(-60, -120), Vector2.new(-60, -85), 16, { { -100, 7 } }) +wall(Lab, Vector2.new(-10, -120), Vector2.new(-10, -85), 16, { { -110, 7 } }) +wall(Lab, Vector2.new(-75, -75), Vector2.new(-75, -40), 16, { { -58, 7 } }) +wall(Lab, Vector2.new(-15, -75), Vector2.new(-15, -40), 16, { { -50, 7 } }) +local cell = Instance.new("Model") +cell.Name = "ContainmentCell" +cell.Parent = Lab +for _, side in { { Vector2.new(-43, -112), Vector2.new(-27, -112), nil }, { Vector2.new(-43, -98), Vector2.new(-27, -98), { { -35, 6 } } }, { Vector2.new(-43, -112), Vector2.new(-43, -98), nil }, { Vector2.new(-27, -112), Vector2.new(-27, -98), nil } } do + wall(cell, side[1], side[2], 10, side[3], Color3.fromRGB(150, 190, 200)) +end +for _, glass in cell:GetChildren() do + if glass:IsA("BasePart") then + glass.Material = Enum.Material.Glass + glass.Transparency = 0.8 + glass.Color = Color3.fromRGB(190, 225, 235) + end +end +local alarm = part(cell, Vector3.new(1.2, 1.2, 1.2), Vector3.new(-35, 10.6, -105), Color3.fromRGB(255, 40, 30), Enum.Material.Neon, "Alarm") +alarm.CastShadow = false +local alarmLight = Instance.new("PointLight") +alarmLight.Color = alarm.Color +alarmLight.Range = 24 +alarmLight.Brightness = 2 +alarmLight.Parent = alarm +for _, p in { Vector3.new(-85, 15.2, -102), Vector3.new(15, 15.2, -102), Vector3.new(-92, 15.2, -58), Vector3.new(-45, 15.2, -58), Vector3.new(12, 15.2, -58), Vector3.new(-80, 15.2, -80), Vector3.new(-20, 15.2, -80), Vector3.new(25, 15.2, -80) } do + lamp(Lab, p, p.X == -20 or p.X == 15, true) +end + +local Reactor = folder("ReactorHall", Building) +room(Reactor, "Shell", Vector2.new(105, -85), Vector2.new(90, 70), 22, { + west = { { -80, 12 } }, + south = { { 85, 10 }, { 130, 8 } }, +}) +local core = part(Reactor, Vector3.new(18, 14, 14), Vector3.new(105, 9, -92), METAL, Enum.Material.DiamondPlate, "ReactorCore") +core.Shape = Enum.PartType.Cylinder +core.CFrame = CFrame.new(ORIGIN + Vector3.new(105, 9, -92)) * CFrame.Angles(0, 0, math.rad(90)) +local ring = part(Reactor, Vector3.new(1, 15, 15), Vector3.new(105, 12, -92), Color3.fromRGB(70, 170, 255), Enum.Material.Neon, "CoreRing") +ring.Shape = Enum.PartType.Cylinder +ring.CFrame = CFrame.new(ORIGIN + Vector3.new(105, 12, -92)) * CFrame.Angles(0, 0, math.rad(90)) +ring.CanCollide = false +local coreLight = Instance.new("PointLight") +coreLight.Color = ring.Color +coreLight.Range = 45 +coreLight.Brightness = 2.2 +for _, p in { Vector3.new(75, 10, -95), Vector3.new(135, 10, -95), Vector3.new(105, 10, -60) } do + fillLight(Reactor, p) +end +coreLight.Parent = ring +local chimney = part(Reactor, Vector3.new(60, 9, 9), Vector3.new(100, 30, -131), DARK_CONCRETE, Enum.Material.Concrete, "Chimney") +chimney.Shape = Enum.PartType.Cylinder +chimney.CFrame = CFrame.new(ORIGIN + Vector3.new(100, 30, -131)) * CFrame.Angles(0, 0, math.rad(90)) +local beacon = part(Reactor, Vector3.new(1.5, 1.5, 1.5), Vector3.new(100, 61, -131), Color3.fromRGB(255, 40, 30), Enum.Material.Neon, "Beacon") +beacon.CastShadow = false +beacon:SetAttribute("Flicker", true) +part(Reactor, Vector3.new(20, 1, 14), Vector3.new(50, 10.5, -80), METAL, Enum.Material.CorrodedMetal, "WalkwayRoof") +for _, p in { Vector3.new(80, 21.2, -70), Vector3.new(130, 21.2, -70), Vector3.new(80, 21.2, -105), Vector3.new(130, 21.2, -105) } do + lamp(Reactor, p, p.X == 130 and p.Z == -105, true) +end + +local Warehouse = folder("Warehouse", Building) +room(Warehouse, "Shell", Vector2.new(-110, 100), Vector2.new(80, 60), 18, { + north = { { -125, 10 }, { -90, 14 } }, + east = { { 110, 8 } }, +}) +for _, z in { 85, 100, 115 } do + wall(Warehouse, Vector2.new(-142, z), Vector2.new(-104, z), 9, { { -123, 5 } }, WOOD) +end +for _, p in { Vector3.new(-130, 17.2, 92), Vector3.new(-95, 17.2, 92), Vector3.new(-130, 17.2, 120), Vector3.new(-95, 17.2, 120) } do + lamp(Warehouse, p, p.X == -130 and p.Z == 120, true) +end + +local Barracks = folder("Barracks", Building) +for i, c in { Vector2.new(70, 90), Vector2.new(115, 90), Vector2.new(70, 122), Vector2.new(115, 122) } do + local door = if c.Y < 100 then { north = { { c.X, 6 } }, south = { { c.X + 6, 6 } } } else { north = { { c.X - 5, 6 } }, east = { { c.Y, 6 } } } + room(Barracks, "Hut" .. i, c, Vector2.new(24, 16), 10, door) + lamp(Barracks, Vector3.new(c.X, 9.2, c.Y), i == 3, true) +end +room(Barracks, "GuardBooth", Vector2.new(24, 126), Vector2.new(12, 10), 9, { north = { { 24, 5 } }, west = { { 126, 4 } } }) + +local Loops = folder("Loops", Yard) +local function lshape(center: Vector3, rotY: number) + local model = Instance.new("Model") + model.Name = "LWall" + model.Parent = Loops + local long = part(model, Vector3.new(14, 8, WALL_THICKNESS), center + Vector3.new(0, 4, -6), CONCRETE, Enum.Material.Concrete, "Wall") + part(model, Vector3.new(WALL_THICKNESS, 8, 8), center + Vector3.new(-6.25, 4, -1.25), CONCRETE, Enum.Material.Concrete, "Wall") + part(model, Vector3.new(WALL_THICKNESS, 3, 4), center + Vector3.new(6.25, 1.5, -3.25), CONCRETE, Enum.Material.Concrete, "LowWall") + model.PrimaryPart = long + model:PivotTo(long.CFrame * CFrame.Angles(0, math.rad(rotY), 0)) end -for _, p in { Vector3.new(-20, 13.2, -12), Vector3.new(-20, 13.2, 12), Vector3.new(0, 13.2, -14), Vector3.new(0, 13.2, 14), Vector3.new(20, 13.2, -12), Vector3.new(20, 13.2, 12) } do - lamp(Building, p, p.X == 0 and p.Z > 0 or p.X == 20 and p.Z < 0, true) +for _, l in { { Vector3.new(-120, 0, 5), 0 }, { Vector3.new(60, 0, 0), 90 }, { Vector3.new(-60, 0, 45), 180 }, { Vector3.new(120, 0, 30), -90 }, { Vector3.new(5, 0, 60), 45 } } do + lshape(l[1], l[2]) end -local Sheds = folder("Sheds", MapModel) -room(Sheds, "ShedNorthWest", Vector2.new(-70, -70), Vector2.new(24, 20), 10, { south = { { -70, 7 } }, east = { { -70, 6 } } }) -room(Sheds, "ShedSouthEast", Vector2.new(70, 65), Vector2.new(20, 24), 10, { west = { { 65, 7 } }, north = { { 70, 6 } } }) -lamp(Sheds, Vector3.new(-70, 9.2, -70), true, true) -lamp(Sheds, Vector3.new(70, 9.2, 65), false, true) +local tower = folder("WaterTower", Yard) +for _, o in { Vector3.new(-5, 0, -5), Vector3.new(5, 0, -5), Vector3.new(-5, 0, 5), Vector3.new(5, 0, 5) } do + part(tower, Vector3.new(1.2, 30, 1.2), Vector3.new(-20, 15, 10) + o, METAL, Enum.Material.CorrodedMetal, "Leg") +end +local tank = part(tower, Vector3.new(12, 16, 16), Vector3.new(-20, 36, 10), RUST, Enum.Material.CorrodedMetal, "Tank") +tank.Shape = Enum.PartType.Cylinder +tank.CFrame = CFrame.new(ORIGIN + Vector3.new(-20, 36, 10)) * CFrame.Angles(0, 0, math.rad(90)) local function container(pos: Vector3, rotY: number, color: Color3) local c = part(Yard, Vector3.new(16, 8, 7), pos + Vector3.new(0, 4, 0), color, Enum.Material.CorrodedMetal, "Container") c.CFrame = CFrame.new(ORIGIN + pos + Vector3.new(0, 4, 0)) * CFrame.Angles(0, math.rad(rotY), 0) end -container(Vector3.new(55, 0, -40), 20, RUST) -container(Vector3.new(-65, 0, 35), 90, Color3.fromRGB(46, 66, 78)) -container(Vector3.new(-35, 0, 80), 5, RUST) -container(Vector3.new(-35, 8, 80), 0, Color3.fromRGB(70, 74, 52)) -container(Vector3.new(35, 0, -82), -10, Color3.fromRGB(46, 66, 78)) -container(Vector3.new(82, 0, 15), 80, RUST) - -for _, p in { Vector3.new(-45, 0, -45), Vector3.new(45, 0, 40), Vector3.new(-50, 0, 10), Vector3.new(30, 0, 60), Vector3.new(-20, 0, -60), Vector3.new(65, 0, -10) } do +container(Vector3.new(-90, 0, -20), 20, RUST) +container(Vector3.new(-140, 0, 40), 90, Color3.fromRGB(46, 66, 78)) +container(Vector3.new(30, 0, 25), 5, RUST) +container(Vector3.new(30, 8, 25), 0, Color3.fromRGB(70, 74, 52)) +container(Vector3.new(95, 0, -25), -10, Color3.fromRGB(46, 66, 78)) +container(Vector3.new(145, 0, 60), 80, RUST) +container(Vector3.new(-40, 0, 110), 90, Color3.fromRGB(70, 74, 52)) + +for _, p in { Vector3.new(-100, 0, 25), Vector3.new(80, 0, 40), Vector3.new(-30, 0, -20), Vector3.new(40, 0, -30), Vector3.new(-60, 0, 85), Vector3.new(20, 0, 95), Vector3.new(140, 0, -10), Vector3.new(-145, 0, -20) } do local barrier = part(Yard, Vector3.new(9, 3, 1.5), p + Vector3.new(0, 1.5, 0), CONCRETE, Enum.Material.Concrete, "JerseyBarrier") barrier.CFrame = CFrame.new(ORIGIN + p + Vector3.new(0, 1.5, 0)) * CFrame.Angles(0, math.rad((p.X * 7 + p.Z * 3) % 90), 0) end -for _, p in { Vector3.new(-82, 0, -30), Vector3.new(48, 0, 82), Vector3.new(-12, 0, 52), Vector3.new(84, 0, -60) } do +for _, p in { Vector3.new(-150, 0, -60), Vector3.new(146, 0, 125), Vector3.new(-12, 0, 130), Vector3.new(60, 0, 60) } do part(Yard, Vector3.new(4, 4, 4), p + Vector3.new(0, 2, 0), WOOD, Enum.Material.WoodPlanks, "Crate") part(Yard, Vector3.new(4, 4, 4), p + Vector3.new(4.2, 2, 0.5), WOOD, Enum.Material.WoodPlanks, "Crate") part(Yard, Vector3.new(4, 4, 4), p + Vector3.new(2, 6, 0.2), WOOD, Enum.Material.WoodPlanks, "Crate") end -for i, p in { Vector3.new(-50, 12, -45), Vector3.new(50, 12, 45), Vector3.new(-50, 12, 45), Vector3.new(50, 12, -45), Vector3.new(0, 12, 70), Vector3.new(0, 12, -70), Vector3.new(-85, 12, 0), Vector3.new(85, 12, -30) } do - lamp(Lamps, p, i % 3 == 0, false, i % 2 == 1) +for i, p in { Vector3.new(-130, 12, -20), Vector3.new(-60, 12, -20), Vector3.new(10, 12, -25), Vector3.new(75, 12, -30), Vector3.new(140, 12, -30), Vector3.new(-120, 12, 55), Vector3.new(-50, 12, 70), Vector3.new(15, 12, 40), Vector3.new(90, 12, 60), Vector3.new(-10, 12, 115), Vector3.new(140, 12, 105), Vector3.new(-150, 12, -125) } do + lamp(Lamps, p, i % 3 == 0, false, i % 3 == 1) +end +for _, p in { Vector3.new(-45, 8, -33), Vector3.new(-95, 8, -33), Vector3.new(15, 8, -33), Vector3.new(-117, 8, -80), Vector3.new(50, 8, -80), Vector3.new(85, 8, -43), Vector3.new(-90, 8, 63), Vector3.new(-125, 8, 63), Vector3.new(92, 8, 100) } do + fillLight(Building, p) end local Generators = folder("Generators", MapModel) @@ -223,11 +315,11 @@ local function generator(name: string, pos: Vector3, rotY: number) model.PrimaryPart = base model:PivotTo(CFrame.new(ORIGIN + pos + Vector3.new(0, 2, 0)) * CFrame.Angles(0, math.rad(rotY), 0)) end -generator("WestRoom", Vector3.new(-22, 0, 16), 0) -generator("EastRoom", Vector3.new(22, 0, -16), 180) -generator("ShedNorthWest", Vector3.new(-73, 0, -74), 90) -generator("ShedSouthEast", Vector3.new(72, 0, 70), -90) -generator("Yard", Vector3.new(8, 0, 84), 0) +generator("LabA", Vector3.new(-100, 0, -102), -90) +generator("Reactor", Vector3.new(85, 0, -70), 180) +generator("Warehouse", Vector3.new(-85, 0, 120), 0) +generator("Barracks", Vector3.new(92, 0, 106), 0) +generator("WaterTower", Vector3.new(-20, 0, 20), 180) local Props = folder("ReactiveProps", MapModel) local function looseProp(pos: Vector3, barrel: boolean) @@ -241,11 +333,14 @@ local function looseProp(pos: Vector3, barrel: boolean) p:AddTag("Reactive") end for _, spot in { - {Vector3.new(-26, 0, 12), false}, {Vector3.new(-25, 0, 20), true}, - {Vector3.new(26, 0, -12), true}, {Vector3.new(18, 0, -20), false}, - {Vector3.new(-69, 0, -78), false}, {Vector3.new(68, 0, 66), true}, - {Vector3.new(4, 0, 88), true}, {Vector3.new(13, 0, 80), false}, - {Vector3.new(-40, 0, 60), false}, {Vector3.new(45, 0, -55), true}, + {Vector3.new(-104, 0, -95), false}, {Vector3.new(-103, 0, -110), true}, + {Vector3.new(78, 0, -76), true}, {Vector3.new(92, 0, -76), false}, + {Vector3.new(-80, 0, 126), false}, {Vector3.new(-92, 0, 126), true}, + {Vector3.new(86, 0, 110), true}, {Vector3.new(98, 0, 112), false}, + {Vector3.new(-26, 0, 26), false}, {Vector3.new(-14, 0, 27), true}, + {Vector3.new(-45, 0, -80), true}, {Vector3.new(-5, 0, -80), false}, + {Vector3.new(-40, 0, -55), false}, {Vector3.new(130, 0, 70), true}, + {Vector3.new(-125, 0, 108), false}, {Vector3.new(25, 0, 112), true}, } do looseProp(spot[1], spot[2]) end @@ -259,7 +354,7 @@ local function spawnPoint(parent: Instance, pos: Vector3) p.CanQuery = false p.CanTouch = false end -for i, layout in { { survivors = Vector3.new(-80, 0, 80), killer = Vector3.new(80, 0, -80) }, { survivors = Vector3.new(80, 0, -80), killer = Vector3.new(-80, 0, 80) } } do +for i, layout in { { survivors = Vector3.new(0, 0, 116), killer = Vector3.new(-35, 0, -105) }, { survivors = Vector3.new(-120, 0, 50), killer = Vector3.new(130, 0, -60) } } do local l = folder("Layout" .. i, SpawnPoints) local survivors, killers = folder("Survivors", l), folder("Killers", l) for _, offset in { Vector3.new(-4, 0, -4), Vector3.new(4, 0, -4), Vector3.new(-4, 0, 4), Vector3.new(4, 0, 4) } do diff --git a/tools/test.py b/tools/test.py index 8c5b66c1..03f94ce2 100644 --- a/tools/test.py +++ b/tools/test.py @@ -147,11 +147,11 @@ def survivor_phase(): prompt:InputHoldBegin() task.wait(0.1) prompt:InputHoldEnd()''', "Client") if mode == "hit": out = wait_for(f'local g = workspace.Map:FindFirstChild("Generators", true)["{name}"] return tostring(g:GetAttribute("Repaired")) .. " " .. tostring(g:GetAttribute("Progress")) .. " " .. tostring(workspace:GetAttribute("GeneratorsRepaired"))', "true", timeout=25) - check("generator repairs in 6 checks", out.startswith("true 6 1"), out) + check("generator repairs in 6 checks", out.startswith("true 6 1"), f"{name} {out}") else: out = wait_for('return tostring(_G.GenEnded)', "true", "Client", timeout=6) progress = luau(f'return tostring(workspace.Map:FindFirstChild("Generators", true)["{name}"]:GetAttribute("Progress"))') - check("missed check fails the repair", out == "true" and progress == "0", f"ended={out} progress={progress}") + check("missed check fails the repair", out == "true" and progress == "0", f"{name} ended={out} progress={progress}") luau(f'''local panel = workspace.Map:FindFirstChild("Generators", true)["{generators[2]}"].Panel local spot = panel.Position + panel.CFrame.LookVector * 12 From 0ba9531974494f0278a10f46cb914ce0f7ee2faa Mon Sep 17 00:00:00 2001 From: Norl4 Date: Sun, 1 Nov 2026 12:45:29 +0100 Subject: [PATCH 12/32] Add chase structures to the south yard and lengthen L walls --- tools/maps/Instalacion.luau | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/tools/maps/Instalacion.luau b/tools/maps/Instalacion.luau index e12ffe85..b44741bd 100644 --- a/tools/maps/Instalacion.luau +++ b/tools/maps/Instalacion.luau @@ -249,15 +249,27 @@ local function lshape(center: Vector3, rotY: number) local model = Instance.new("Model") model.Name = "LWall" model.Parent = Loops - local long = part(model, Vector3.new(14, 8, WALL_THICKNESS), center + Vector3.new(0, 4, -6), CONCRETE, Enum.Material.Concrete, "Wall") - part(model, Vector3.new(WALL_THICKNESS, 8, 8), center + Vector3.new(-6.25, 4, -1.25), CONCRETE, Enum.Material.Concrete, "Wall") - part(model, Vector3.new(WALL_THICKNESS, 3, 4), center + Vector3.new(6.25, 1.5, -3.25), CONCRETE, Enum.Material.Concrete, "LowWall") + local long = part(model, Vector3.new(20, 8, WALL_THICKNESS), center + Vector3.new(0, 4, -6), CONCRETE, Enum.Material.Concrete, "Wall") + part(model, Vector3.new(WALL_THICKNESS, 8, 10), center + Vector3.new(-9.25, 4, -0.25), CONCRETE, Enum.Material.Concrete, "Wall") + part(model, Vector3.new(WALL_THICKNESS, 3, 4), center + Vector3.new(9.25, 1.5, -3.25), CONCRETE, Enum.Material.Concrete, "LowWall") model.PrimaryPart = long model:PivotTo(long.CFrame * CFrame.Angles(0, math.rad(rotY), 0)) end for _, l in { { Vector3.new(-120, 0, 5), 0 }, { Vector3.new(60, 0, 0), 90 }, { Vector3.new(-60, 0, 45), 180 }, { Vector3.new(120, 0, 30), -90 }, { Vector3.new(5, 0, 60), 45 } } do lshape(l[1], l[2]) end +local function tshape(center: Vector3, rotY: number) + local model = Instance.new("Model") + model.Name = "TWall" + model.Parent = Loops + local bar = part(model, Vector3.new(22, 8, WALL_THICKNESS), center + Vector3.new(0, 4, -6), CONCRETE, Enum.Material.Concrete, "Wall") + part(model, Vector3.new(WALL_THICKNESS, 8, 12), center + Vector3.new(0, 4, 0.5), CONCRETE, Enum.Material.Concrete, "Wall") + model.PrimaryPart = bar + model:PivotTo(bar.CFrame * CFrame.Angles(0, math.rad(rotY), 0)) +end +tshape(Vector3.new(-25, 0, 82), 0) +tshape(Vector3.new(38, 0, 120), 90) +lshape(Vector3.new(-55, 0, 128), 0) local tower = folder("WaterTower", Yard) for _, o in { Vector3.new(-5, 0, -5), Vector3.new(5, 0, -5), Vector3.new(-5, 0, 5), Vector3.new(5, 0, 5) } do @@ -278,6 +290,8 @@ container(Vector3.new(30, 8, 25), 0, Color3.fromRGB(70, 74, 52)) container(Vector3.new(95, 0, -25), -10, Color3.fromRGB(46, 66, 78)) container(Vector3.new(145, 0, 60), 80, RUST) container(Vector3.new(-40, 0, 110), 90, Color3.fromRGB(70, 74, 52)) +container(Vector3.new(48, 0, 70), 0, RUST) +container(Vector3.new(48, 0, 86), 0, Color3.fromRGB(46, 66, 78)) for _, p in { Vector3.new(-100, 0, 25), Vector3.new(80, 0, 40), Vector3.new(-30, 0, -20), Vector3.new(40, 0, -30), Vector3.new(-60, 0, 85), Vector3.new(20, 0, 95), Vector3.new(140, 0, -10), Vector3.new(-145, 0, -20) } do local barrier = part(Yard, Vector3.new(9, 3, 1.5), p + Vector3.new(0, 1.5, 0), CONCRETE, Enum.Material.Concrete, "JerseyBarrier") @@ -354,7 +368,7 @@ local function spawnPoint(parent: Instance, pos: Vector3) p.CanQuery = false p.CanTouch = false end -for i, layout in { { survivors = Vector3.new(0, 0, 116), killer = Vector3.new(-35, 0, -105) }, { survivors = Vector3.new(-120, 0, 50), killer = Vector3.new(130, 0, -60) } } do +for i, layout in { { survivors = Vector3.new(-12, 0, 104), killer = Vector3.new(-35, 0, -105) }, { survivors = Vector3.new(-120, 0, 50), killer = Vector3.new(130, 0, -60) } } do local l = folder("Layout" .. i, SpawnPoints) local survivors, killers = folder("Survivors", l), folder("Killers", l) for _, offset in { Vector3.new(-4, 0, -4), Vector3.new(4, 0, -4), Vector3.new(-4, 0, 4), Vector3.new(4, 0, 4) } do From 9bbfe78846f42e61f4cf6fc1437d09ca9065a3e1 Mon Sep 17 00:00:00 2001 From: Norl4 Date: Sun, 1 Nov 2026 12:48:22 +0100 Subject: [PATCH 13/32] Dress Instalacion with custom materials, signs, pipes and puddles --- tools/maps/Instalacion.luau | 60 ++++++++++++++++++++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/tools/maps/Instalacion.luau b/tools/maps/Instalacion.luau index b44741bd..0d1e472c 100644 --- a/tools/maps/Instalacion.luau +++ b/tools/maps/Instalacion.luau @@ -41,6 +41,13 @@ local Building = folder("Building", MapModel) local Yard = folder("Yard", MapModel) local Lamps = folder("Lamps", MapModel) +local VARIANTS = { + [Enum.Material.Concrete] = "AnomalisConcrete", + [Enum.Material.CorrodedMetal] = "AnomalisRustMetal", + [Enum.Material.Asphalt] = "AnomalisAsphalt", + [Enum.Material.CeramicTiles] = "AnomalisLabTile", +} + local function part(parent: Instance, size: Vector3, pos: Vector3, color: Color3, material: Enum.Material, name: string?): Part local p = Instance.new("Part") p.Name = name or "Part" @@ -49,6 +56,7 @@ local function part(parent: Instance, size: Vector3, pos: Vector3, color: Color3 p.CFrame = CFrame.new(ORIGIN + pos) p.Color = color p.Material = material + p.MaterialVariant = VARIANTS[material] or "" p.TopSurface = Enum.SurfaceType.Smooth p.BottomSurface = Enum.SurfaceType.Smooth p.Parent = parent @@ -97,7 +105,8 @@ local function room(container: Instance, name: string, center: Vector2, size: Ve wall(parent, Vector2.new(x0, z0), Vector2.new(x0, z1), height, gaps.west) wall(parent, Vector2.new(x1, z0), Vector2.new(x1, z1), height, gaps.east) part(parent, Vector3.new(size.X + 2, 1, size.Y + 2), Vector3.new(center.X, height + 0.5, center.Y), METAL, Enum.Material.CorrodedMetal, "Roof") - part(parent, Vector3.new(size.X, 0.2, size.Y), Vector3.new(center.X, 0.1, center.Y), DARK_CONCRETE, Enum.Material.Concrete, "Floor") + local lab = container.Name == "LabComplex" + part(parent, Vector3.new(size.X, 0.2, size.Y), Vector3.new(center.X, 0.1, center.Y), if lab then Color3.fromRGB(124, 126, 120) else DARK_CONCRETE, if lab then Enum.Material.CeramicTiles else Enum.Material.Concrete, "Floor") end local function fillLight(parent: Instance, pos: Vector3) @@ -359,6 +368,55 @@ for _, spot in { looseProp(spot[1], spot[2]) end +local Details = folder("Details", MapModel) +local function detail(p: BasePart): BasePart + p.CanCollide = false + p.CanQuery = false + p.CastShadow = false + p:AddTag("Detail") + return p +end + +local function sign(text: string, pos: Vector3, face: Enum.NormalId, color: Color3) + local s = detail(part(Details, Vector3.new(#text * 0.8 + 2, 2.4, 0.2), pos, Color3.fromRGB(28, 30, 34), Enum.Material.Metal, "Sign")) + local gui = Instance.new("SurfaceGui") + gui.Face = face + gui.SizingMode = Enum.SurfaceGuiSizingMode.PixelsPerStud + gui.PixelsPerStud = 40 + gui.LightInfluence = 1 + gui.Parent = s + local label = Instance.new("TextLabel") + label.Size = UDim2.fromScale(1, 1) + label.BackgroundTransparency = 1 + label.Text = text + label.TextScaled = true + label.Font = Enum.Font.GothamBlack + label.TextColor3 = color + label.Parent = gui +end +local WHITE, RED, AMBER = Color3.fromRGB(220, 224, 230), Color3.fromRGB(210, 40, 36), Color3.fromRGB(230, 170, 40) +sign("ANOMALIS RESEARCH SITE 7", Vector3.new(0, 16, 138.6), Enum.NormalId.Front, WHITE) +sign("SECTOR C LABORATORIES", Vector3.new(-35, 12, -39.1), Enum.NormalId.Back, WHITE) +sign("CONTAINMENT AUTHORIZED ONLY", Vector3.new(-35, 10, -84.1), Enum.NormalId.Back, RED) +sign("REACTOR 02 RADIATION", Vector3.new(105, 16, -49.1), Enum.NormalId.Back, AMBER) +sign("STORAGE B", Vector3.new(-110, 10, 69.1), Enum.NormalId.Front, WHITE) +sign("BARRACKS", Vector3.new(92, 9, 81.9), Enum.NormalId.Front, WHITE) + +for _, z in { -83.4, -76.6 } do + detail(part(Details, Vector3.new(148, 1.1, 1.1), Vector3.new(-35, 14.6, z), METAL, Enum.Material.Metal, "Pipe")).Shape = Enum.PartType.Cylinder +end +detail(part(Details, Vector3.new(88, 1.6, 1.6), Vector3.new(105, 19, -118), RUST, Enum.Material.CorrodedMetal, "Pipe")).Shape = Enum.PartType.Cylinder + +for _, p in { Vector3.new(-70, 0, 20), Vector3.new(15, 0, 50), Vector3.new(-15, 0, 95), Vector3.new(70, 0, 10), Vector3.new(-125, 0, -45), Vector3.new(125, 0, 80), Vector3.new(40, 0, 105) } do + for i, o in { Vector3.zero, Vector3.new(3, 0, 1.5), Vector3.new(-2, 0, 2) } do + local d = 6 - i + local puddle = detail(part(Details, Vector3.new(0.05, d, d * 1.3), p + o + Vector3.new(0, 0.03, 0), Color3.fromRGB(58, 64, 72), Enum.Material.Glass, "Puddle")) + puddle.Shape = Enum.PartType.Cylinder + puddle.CFrame *= CFrame.Angles(0, 0, math.rad(90)) + puddle.Transparency = 0.45 + end +end + local SpawnPoints = folder("SpawnPoints", MapModel) local function spawnPoint(parent: Instance, pos: Vector3) local p = part(parent, Vector3.new(1, 1, 1), pos + Vector3.new(0, 3, 0), CONCRETE, Enum.Material.SmoothPlastic, if parent.Name == "Killers" then "KillerSpawn" else "SurvivorSpawn") From 881dc6251bfcf82f8aa0e9ca8ef203747ca75f39 Mon Sep 17 00:00:00 2001 From: Norl4 Date: Sun, 1 Nov 2026 12:53:51 +0100 Subject: [PATCH 14/32] Translate all player-facing text to Spanish --- src/ReplicatedFirst/Loading.client.luau | 2 +- .../Assets/Achievements.luau | 4 +- .../Assets/Emotes/TPose.luau | 2 +- .../Assets/KillerIntros/KillerNameToast.luau | 2 +- .../Characters/Killers/NullexVoyd.luau | 44 ++++++------- .../Commander/CommanderYourself.luau | 2 +- .../Characters/Survivors/Commander.luau | 66 +++++++++---------- .../Classes/Ability/Slash.luau | 2 +- src/ReplicatedStorage/Classes/Hitbox.luau | 4 +- src/ReplicatedStorage/Effects/Blindness.luau | 2 +- .../Effects/Disoriented.luau | 2 +- src/ReplicatedStorage/Effects/Helpless.luau | 2 +- .../Effects/Invisibility.luau | 2 +- .../KillerSpecific/SpawnProtection.luau | 2 +- .../Effects/KillerSpecific/Stunned.luau | 2 +- src/ReplicatedStorage/Effects/Resistance.luau | 2 +- src/ReplicatedStorage/Effects/Slowness.luau | 2 +- src/ReplicatedStorage/Effects/Speed.luau | 2 +- .../Effects/Undetectable.luau | 2 +- .../ModeratorCommands/KillPlayer.luau | 2 +- .../ModeratorCommands/TurnPlayerInto.luau | 4 +- .../Managers/GeneratorManager.luau | 6 +- .../Managers/PlayerManager/init.luau | 4 +- .../Managers/RoundManager.luau | 4 +- .../System/CommonFunctions.luau | 6 +- .../Miscellaneous/GeneratorClient.luau | 6 +- .../Miscellaneous/RoundTips.luau | 18 ++--- .../UI/AchievementsMenu.luau | 8 +-- src/StarterPlayerScripts/UI/Changelog.luau | 24 +++---- src/StarterPlayerScripts/UI/Credits.luau | 4 +- .../UI/Inventory/init.luau | 6 +- .../UI/ModPanel/Bool.luau | 4 +- .../UI/ModPanel/Enum.luau | 4 +- .../UI/ModPanel/init.luau | 2 +- .../UI/MoneyXPRewards.luau | 6 +- .../UI/PlayerList/PlayerInfoWindow.luau | 4 +- .../UI/RoundPlayerList.luau | 4 +- src/StarterPlayerScripts/UI/RulesMenu.luau | 24 +++---- .../UI/Shop/BuyWindow.luau | 6 +- src/StarterPlayerScripts/UI/Shop/init.luau | 14 ++-- src/StarterPlayerScripts/UI/Time.luau | 8 +-- tools/maps/Instalacion.luau | 12 ++-- 42 files changed, 164 insertions(+), 164 deletions(-) diff --git a/src/ReplicatedFirst/Loading.client.luau b/src/ReplicatedFirst/Loading.client.luau index 2d416fcf..833da15c 100644 --- a/src/ReplicatedFirst/Loading.client.luau +++ b/src/ReplicatedFirst/Loading.client.luau @@ -125,7 +125,7 @@ local function InitScripts(module: ModuleScript) while not ModuleCode.Loaded do if ModuleCode.CurrentlyInitting then - CurrentlyInitting.Text = "Initializing "..ModuleCode.CurrentlyInitting.."..." + CurrentlyInitting.Text = "Iniciando "..ModuleCode.CurrentlyInitting.."..." else CurrentlyInitting.Text = "" end diff --git a/src/ReplicatedStorage/Assets/Achievements.luau b/src/ReplicatedStorage/Assets/Achievements.luau index c64e1f63..3645d20d 100644 --- a/src/ReplicatedStorage/Assets/Achievements.luau +++ b/src/ReplicatedStorage/Assets/Achievements.luau @@ -14,8 +14,8 @@ return { LayoutOrder = 1, Achievements = { Welcome = { - Title = "Welcome To [INSERT GAME NAME]", - Description = "Play [INSERT GAME NAME] for the first time", + Title = "Bienvenido a Anomalis", + Description = "Juega a Anomalis por primera vez", RewardType = "Currency", Amount = 300, LayoutOrder = 1, diff --git a/src/ReplicatedStorage/Assets/Emotes/TPose.luau b/src/ReplicatedStorage/Assets/Emotes/TPose.luau index c50247ef..cc50d4fb 100644 --- a/src/ReplicatedStorage/Assets/Emotes/TPose.luau +++ b/src/ReplicatedStorage/Assets/Emotes/TPose.luau @@ -4,7 +4,7 @@ local Emote = require(ReplicatedStorage.Classes.Emote) return Emote.New({ Config = { Name = "T-Pose", - Quote = "be not afraid.", + Quote = "no temas.", }, AnimationIds = "rbxassetid://87300475457340", SoundIds = "rbxassetid://1839549655", diff --git a/src/ReplicatedStorage/Assets/KillerIntros/KillerNameToast.luau b/src/ReplicatedStorage/Assets/KillerIntros/KillerNameToast.luau index 4af599ec..e4811901 100644 --- a/src/ReplicatedStorage/Assets/KillerIntros/KillerNameToast.luau +++ b/src/ReplicatedStorage/Assets/KillerIntros/KillerNameToast.luau @@ -41,7 +41,7 @@ return function(Parent: ScreenGui, plrNames: {string}) end if #plrNames > 1 then - Group.Container.Label.Text = "The killers are..." + Group.Container.Label.Text = "Los asesinos son..." end task.wait(1) diff --git a/src/ReplicatedStorage/Characters/Killers/NullexVoyd.luau b/src/ReplicatedStorage/Characters/Killers/NullexVoyd.luau index 3c532303..45248dac 100644 --- a/src/ReplicatedStorage/Characters/Killers/NullexVoyd.luau +++ b/src/ReplicatedStorage/Characters/Killers/NullexVoyd.luau @@ -154,12 +154,12 @@ end local NullexVoyd: Types.Killer = Character.CreateKiller({ Config = { Name = "Nullex Voyd", - Quote = "Order was the first lie.", + Quote = "El orden fue la primera mentira.", Render = "rbxassetid://139883756028491", Price = 1308, Origin = { - TooltipText = "Originates from Project Terminus!", + TooltipText = "¡Viene del Proyecto Terminus!", Icon = "rbxasset://textures/ui/GuiImagePlaceholder.png", }, @@ -224,7 +224,7 @@ local NullexVoydNameLabel = ""..NullexVoyd.Co NullexVoyd.Config.Description = { { Type = "Separator", - Text = "GENERAL INFO", + Text = "INFORMACIÓN GENERAL", }, { Type = "Header", @@ -236,31 +236,31 @@ NullexVoyd.Config.Description = { }, { Type = "Text", - Text = "Once a law enforcement automaton built to enforce order, now a void-born executioner of silence. ".. - NullexVoydNameLabel.." hunts those who trespass forbidden truths, warping space to pursue them through shadows. ".. - "With a surge of corrupted energy, he blinks across distance, accelerates beyond human reach, and rends with anomalous force. ".. - "None can hide — his gaze pierces walls, his touch unravels existence itself.", + Text = "Antes era un autómata policial creado para imponer el orden; ahora es un verdugo del silencio nacido del vacío. ".. + NullexVoydNameLabel.." caza a quienes cruzan verdades prohibidas y deforma el espacio para perseguirlos entre las sombras. ".. + "Con una descarga de energía corrupta salta distancias, acelera más allá de lo humano y desgarra con fuerza anómala. ".. + "Nadie puede esconderse: su mirada atraviesa paredes y su toque deshace la existencia misma.", }, { Type = "Header", - Text = "STATS", + Text = "ESTADÍSTICAS", }, { Type = "Text", Text = { - "Difficulty: ★★★☆☆", + "Dificultad: ★★★☆☆", -- "Difficulty: 6 7", - "Health: "..tostring(NullexVoyd.GameplayConfig.Health), - "Base Speed: "..tostring(NullexVoyd.GameplayConfig.BaseSpeed), - "Sprint Speed: "..tostring(math.round(NullexVoyd.GameplayConfig.BaseSpeed * NullexVoyd.GameplayConfig.SprintSpeedMultiplier * 10) / 10), - "Max Stamina: "..tostring(NullexVoyd.GameplayConfig.StaminaProperties.MaxStamina), - "Stamina Loss per second: "..tostring(NullexVoyd.GameplayConfig.StaminaProperties.StaminaDrain), - "Stamina Gain per second: "..tostring(NullexVoyd.GameplayConfig.StaminaProperties.StaminaGain), + "Vida: "..tostring(NullexVoyd.GameplayConfig.Health), + "Velocidad base: "..tostring(NullexVoyd.GameplayConfig.BaseSpeed), + "Velocidad al correr: "..tostring(math.round(NullexVoyd.GameplayConfig.BaseSpeed * NullexVoyd.GameplayConfig.SprintSpeedMultiplier * 10) / 10), + "Aguante máximo: "..tostring(NullexVoyd.GameplayConfig.StaminaProperties.MaxStamina), + "Aguante perdido por segundo: "..tostring(NullexVoyd.GameplayConfig.StaminaProperties.StaminaDrain), + "Aguante recuperado por segundo: "..tostring(NullexVoyd.GameplayConfig.StaminaProperties.StaminaGain), }, }, { Type = "Separator", - Text = "ABILITIES", + Text = "HABILIDADES", }, { Type = "Header", @@ -268,7 +268,7 @@ NullexVoyd.Config.Description = { }, { Type = "Text", - Text = NullexVoydNameLabel.." performs a basic slash, dealing "..tostring(NullexVoyd.GameplayConfig.Abilities.Slash.Damage).." damage.", + Text = NullexVoydNameLabel.." lanza un tajo básico que hace "..tostring(NullexVoyd.GameplayConfig.Abilities.Slash.Damage).." de daño.", Image = "rbxassetid://9759886280", }, { @@ -277,7 +277,7 @@ NullexVoyd.Config.Description = { }, { Type = "Text", - Text = NullexVoydNameLabel.." slows down for "..tostring(NullexVoyd.GameplayConfig.Abilities.CallbackPing.Duration).." seconds, revealing to him the aura of every single visible & alive survivor in the match that doesn't have Undetectable.", + Text = NullexVoydNameLabel.." se ralentiza durante "..tostring(NullexVoyd.GameplayConfig.Abilities.CallbackPing.Duration).." segundos y ve el aura de cada superviviente vivo y visible de la partida que no tenga Indetectable.", }, { Type = "Header", @@ -286,8 +286,8 @@ NullexVoyd.Config.Description = { { Type = "Text", Text = { - NullexVoydNameLabel.." places a device on the floor, visible to everyone clearly but undestructible.", - "If this ability is used a second time, "..NullexVoydNameLabel.." will be teleported to this device instantly, destroying it in the process.", + NullexVoydNameLabel.." coloca un dispositivo en el suelo que todos ven con claridad y que no se puede destruir.", + "Si usa esta habilidad otra vez, "..NullexVoydNameLabel.." se teletransporta al instante al dispositivo y lo destruye.", }, }, { @@ -296,8 +296,8 @@ NullexVoyd.Config.Description = { }, { Type = "Text", - Text = NullexVoydNameLabel.." overloads his systems with electricity, giving him Speed "..Utils.Math.IntToRoman(NullexVoyd.GameplayConfig.Abilities.Overcharge.SpeedLevel).." for "..tostring(NullexVoyd.GameplayConfig.Abilities.Overcharge.SpeedDuration).." seconds. ".. - "When the effect wears off, his systems will exhaust briefly, inflicting him with Slowness "..Utils.Math.IntToRoman(NullexVoyd.GameplayConfig.Abilities.Overcharge.SlownessLevel).." for "..tostring(NullexVoyd.GameplayConfig.Abilities.Overcharge.SlownessDuration).." seconds.", + Text = NullexVoydNameLabel.." sobrecarga sus sistemas con electricidad y gana Velocidad "..Utils.Math.IntToRoman(NullexVoyd.GameplayConfig.Abilities.Overcharge.SpeedLevel).." durante "..tostring(NullexVoyd.GameplayConfig.Abilities.Overcharge.SpeedDuration).." segundos. ".. + "Cuando se acaba, sus sistemas se agotan un momento y sufre Lentitud "..Utils.Math.IntToRoman(NullexVoyd.GameplayConfig.Abilities.Overcharge.SlownessLevel).." durante "..tostring(NullexVoyd.GameplayConfig.Abilities.Overcharge.SlownessDuration).." segundos.", }, } diff --git a/src/ReplicatedStorage/Characters/Skins/Survivors/Commander/CommanderYourself.luau b/src/ReplicatedStorage/Characters/Skins/Survivors/Commander/CommanderYourself.luau index 95f2cf6c..9c104250 100644 --- a/src/ReplicatedStorage/Characters/Skins/Survivors/Commander/CommanderYourself.luau +++ b/src/ReplicatedStorage/Characters/Skins/Survivors/Commander/CommanderYourself.luau @@ -16,7 +16,7 @@ end return Character.CreateSkin("Survivor", "Commander", { Config = { Name = "Yourself", - Quote = "Hey, it's you!", + Quote = "¡Eh, si eres tú!", Price = 99999999, Origin = {}, diff --git a/src/ReplicatedStorage/Characters/Survivors/Commander.luau b/src/ReplicatedStorage/Characters/Survivors/Commander.luau index 7eb81ed5..1bc3b117 100644 --- a/src/ReplicatedStorage/Characters/Survivors/Commander.luau +++ b/src/ReplicatedStorage/Characters/Survivors/Commander.luau @@ -48,7 +48,7 @@ local function BlindsideBehaviour(self: Types.Ability) }, Connections = { PostHit = function(Config: Types.HitboxSettings, Hum: Humanoid) - CommonFunctions.GrantRewardToPlayer(self.Owner, {Money = 10, EXP = 20, Reason = "blinding the killer with Blindside"}) + CommonFunctions.GrantRewardToPlayer(self.Owner, {Money = 10, EXP = 20, Reason = "cegar al asesino con Blindside"}) end, } }) @@ -108,7 +108,7 @@ local function JusticeBehaviour(self: Types.Ability) CFrameOffset = CFrame.new(0, 0, -(self.HitboxLength / 2)), Size = Vector3.new(3, 1.5, self.HitboxLength), Time = self.Duration, - Reason = "The Gun Shot", + Reason = "el disparo", Damage = self.Damage, EffectsToApply = { { @@ -120,7 +120,7 @@ local function JusticeBehaviour(self: Types.Ability) }, Connections = { PostHit = function(Config: Types.HitboxSettings, Hum: Humanoid) - CommonFunctions.GrantRewardToPlayer(self.Owner, {Money = 6, EXP = 14, Reason = "stunning the killer with Justice"}) + CommonFunctions.GrantRewardToPlayer(self.Owner, {Money = 6, EXP = 14, Reason = "aturdir al asesino con Justice"}) end, } }) @@ -178,7 +178,7 @@ local Commander: Types.Survivor = Character.CreateSurvivor({ Render = "rbxassetid://108440668883002", Origin = { - TooltipText = "Originates from Project Terminus!", + TooltipText = "¡Viene del Proyecto Terminus!", Icon = "rbxasset://textures/ui/GuiImagePlaceholder.png", }, }, @@ -221,7 +221,7 @@ local Commander: Types.Survivor = Character.CreateSurvivor({ Justice = Ability.New({ Name = "Justice", - Description = "Commander pulls out a gun and shoots forward while in low speed. Does 50 damage to the killer and stuns them for 4.2 seconds.", + Description = "Commander saca la pistola y dispara al frente mientras va despacio. Hace 50 de daño al asesino y lo aturde 4,2 segundos.", UseAnimation = "", ShotSound = "", Cooldown = 45, @@ -249,7 +249,7 @@ local Commander: Types.Survivor = Character.CreateSurvivor({ Commander.Config.Description = { { Type = "Separator", - Text = "GENERAL INFO", + Text = "INFORMACIÓN GENERAL", }, { Type = "Header", @@ -261,30 +261,30 @@ Commander.Config.Description = { }, { Type = "Text", - Text = "Initially a simple soldier that managed to turn the military upside-down with his leadership. " - .."After some altercations in a company he got hired in as a guard against anomalies, he managed to survive a fatal accident and become one of the peak soldiers in the army. " - .."He's been intensely trained for years and has experience with handguns and flash grenades, using them as efficiently as possible and purchasing the tools with most quality.", + Text = "Empezó como un simple soldado que puso el ejército patas arriba con su liderazgo. " + .."Tras varios altercados en la empresa que lo contrató como guardia contra anomalías, sobrevivió a un accidente mortal y se convirtió en uno de los mejores soldados del ejército. " + .."Lleva años de entrenamiento intenso, domina las pistolas y las granadas cegadoras, las usa con la máxima eficacia y compra siempre el mejor equipo.", }, { Type = "Header", - Text = "STATS", + Text = "ESTADÍSTICAS", }, { Type = "Text", Text = { - "Difficulty: ★★★☆☆", + "Dificultad: ★★★☆☆", -- "Difficulty: 6 7", - "Health: "..tostring(Commander.GameplayConfig.Health), - "Base Speed: "..tostring(Commander.GameplayConfig.BaseSpeed), - "Sprint Speed: "..tostring(math.round(Commander.GameplayConfig.BaseSpeed * Commander.GameplayConfig.SprintSpeedMultiplier * 10) / 10), - "Max Stamina: "..tostring(Commander.GameplayConfig.StaminaProperties.MaxStamina), - "Stamina Loss per second: "..tostring(Commander.GameplayConfig.StaminaProperties.StaminaDrain), - "Stamina Gain per second: "..tostring(Commander.GameplayConfig.StaminaProperties.StaminaGain), + "Vida: "..tostring(Commander.GameplayConfig.Health), + "Velocidad base: "..tostring(Commander.GameplayConfig.BaseSpeed), + "Velocidad al correr: "..tostring(math.round(Commander.GameplayConfig.BaseSpeed * Commander.GameplayConfig.SprintSpeedMultiplier * 10) / 10), + "Aguante máximo: "..tostring(Commander.GameplayConfig.StaminaProperties.MaxStamina), + "Aguante perdido por segundo: "..tostring(Commander.GameplayConfig.StaminaProperties.StaminaDrain), + "Aguante recuperado por segundo: "..tostring(Commander.GameplayConfig.StaminaProperties.StaminaGain), }, }, { Type = "Separator", - Text = "PASSIVES", + Text = "PASIVAS", }, { Type = "Header", @@ -292,16 +292,16 @@ Commander.Config.Description = { }, { Type = "Quote", - Text = "\"Diamond armor, full set!\" -Minecraft Steve", + Text = "\"¡Armadura de diamante, set completo!\" -Steve de Minecraft", }, { Type = "Text", - Text = "Every time Commander gets hit, he gets Resistance V for 1 second, acting as invincibility frames.", + Text = "Cada vez que golpean a Commander, gana Resistencia V durante 1 segundo, como fotogramas de invencibilidad.", Image = "http://www.roblox.com/asset/?id=113412520", }, { Type = "Separator", - Text = "ABILITIES", + Text = "HABILIDADES", }, { Type = "Header", @@ -309,11 +309,11 @@ Commander.Config.Description = { }, { Type = "Text", - Text = "Commander picks up a flashbang grenade and throws it after "..tostring(Commander.GameplayConfig.Abilities.Blindside.ThrowDelay).. - " seconds, triggering it after "..tostring(Commander.GameplayConfig.Abilities.Blindside.TriggerDelay).. - " seconds and inflicting the killer Blindness for "..tostring(Commander.GameplayConfig.Abilities.Blindside.FlashTime).. - " seconds if they're in a "..tostring(Commander.GameplayConfig.Abilities.Blindside.FlashRadius).. - " stud radius.", + Text = "Commander saca una granada cegadora y la lanza tras "..tostring(Commander.GameplayConfig.Abilities.Blindside.ThrowDelay).. + " segundos; estalla tras "..tostring(Commander.GameplayConfig.Abilities.Blindside.TriggerDelay).. + " segundos y causa Ceguera al asesino durante "..tostring(Commander.GameplayConfig.Abilities.Blindside.FlashTime).. + " segundos si está a menos de "..tostring(Commander.GameplayConfig.Abilities.Blindside.FlashRadius).. + " studs.", }, { Type = "Header", @@ -321,8 +321,8 @@ Commander.Config.Description = { }, { Type = "Text", - Text = "Commander reloads his gun, getting Slowness "..Utils.Math.IntToRoman(Commander.GameplayConfig.Abilities.Reload.SlownessLevel).. - " for "..tostring(Commander.GameplayConfig.Abilities.Reload.Duration).." seconds.", + Text = "Commander recarga la pistola y sufre Lentitud "..Utils.Math.IntToRoman(Commander.GameplayConfig.Abilities.Reload.SlownessLevel).. + " durante "..tostring(Commander.GameplayConfig.Abilities.Reload.Duration).." segundos.", }, { Type = "Header", @@ -331,11 +331,11 @@ Commander.Config.Description = { { Type = "Text", Text = { - "Commander shoots forward " - ..tostring(Commander.GameplayConfig.Abilities.Justice.HitboxLength).." studs. If it hits the killer, they get stunned for " - ..tostring(Commander.GameplayConfig.Abilities.Justice.StunTime).." seconds and receive " - ..tostring(Commander.GameplayConfig.Abilities.Justice.Damage).." damage.", - "This ability's charge is obtained through Reload." + "Commander dispara al frente a " + ..tostring(Commander.GameplayConfig.Abilities.Justice.HitboxLength).." studs. Si acierta al asesino, lo aturde durante " + ..tostring(Commander.GameplayConfig.Abilities.Justice.StunTime).." segundos y le hace " + ..tostring(Commander.GameplayConfig.Abilities.Justice.Damage).." de daño.", + "La carga de esta habilidad se consigue con Reload." }, }, } diff --git a/src/ReplicatedStorage/Classes/Ability/Slash.luau b/src/ReplicatedStorage/Classes/Ability/Slash.luau index c337e460..95cbd637 100644 --- a/src/ReplicatedStorage/Classes/Ability/Slash.luau +++ b/src/ReplicatedStorage/Classes/Ability/Slash.luau @@ -18,7 +18,7 @@ local function DefaultSlashBehaviour(self: Types.Ability) Size = self.HitboxSize, Time = self.Duration, Damage = self.Damage, - Reason = "Slash Attack", + Reason = "un tajo", ExecuteOnKill = true, Connections = self.Connections, }) diff --git a/src/ReplicatedStorage/Classes/Hitbox.luau b/src/ReplicatedStorage/Classes/Hitbox.luau index ce014fd6..51214554 100644 --- a/src/ReplicatedStorage/Classes/Hitbox.luau +++ b/src/ReplicatedStorage/Classes/Hitbox.luau @@ -26,7 +26,7 @@ local Hitbox = { ExecuteOnKill = false, IsProjectile = false, FriendlyFire = false, - Reason = "Slash Attack", + Reason = "un tajo", Shape = Enum.PartType.Block, MinBlockingWallHeight = 3, IgnoreWalls = false, @@ -441,7 +441,7 @@ function Hitbox.New(SourcePlayer: Player, Config: Types.HitboxSettings): Types.H Network:FireConnection("KilledPlayer", SourcePlayer, TargetPlayer) CommonFunctions.UpdatePlayerStat(SourcePlayer, "KillerStats.Kills", 1) - CommonFunctions.GrantRewardToPlayer(SourcePlayer, {Money = 20, EXP = 60, Reason = "killing a Survivor"}) + CommonFunctions.GrantRewardToPlayer(SourcePlayer, {Money = 20, EXP = 60, Reason = "matar a un superviviente"}) TimeManager.SetTime(TimeManager.CurrentTime + 35) end) diff --git a/src/ReplicatedStorage/Effects/Blindness.luau b/src/ReplicatedStorage/Effects/Blindness.luau index 0d6c5f8d..3dc6ce93 100644 --- a/src/ReplicatedStorage/Effects/Blindness.luau +++ b/src/ReplicatedStorage/Effects/Blindness.luau @@ -12,7 +12,7 @@ local DisappearThread return Effect.New({ Name = "Blindness", - Description = "Blinds the affected player for a set time. This effect will flash white if the Photosensitive setting is off, and black if it's on.", + Description = "Ciega al jugador durante un tiempo. Destella en blanco si el ajuste Fotosensible está apagado y en negro si está encendido.", ApplyEffect = function(own: Types.Effect, char: Model, level: number, duration: number) if not RunService:IsServer() then diff --git a/src/ReplicatedStorage/Effects/Disoriented.luau b/src/ReplicatedStorage/Effects/Disoriented.luau index 45ee9726..6b14f9bc 100644 --- a/src/ReplicatedStorage/Effects/Disoriented.luau +++ b/src/ReplicatedStorage/Effects/Disoriented.luau @@ -8,7 +8,7 @@ local PlayerModule = not RunService:IsServer() and require(Players.LocalPlayer.P return Effect.New({ Name = "Disoriented", - Description = "Inverts the player's contorls for the duration of the effect.", + Description = "Invierte los controles del jugador mientras dura el efecto.", Duration = 4.2, ShowInGUI = false, ApplyEffect = function() diff --git a/src/ReplicatedStorage/Effects/Helpless.luau b/src/ReplicatedStorage/Effects/Helpless.luau index dee33ac0..7d21f343 100644 --- a/src/ReplicatedStorage/Effects/Helpless.luau +++ b/src/ReplicatedStorage/Effects/Helpless.luau @@ -4,6 +4,6 @@ local Effect = require(ReplicatedStorage.Classes.Effect) return Effect.New({ Name = "Helpless", - Description = "Disables a character's abilities during its presence.", + Description = "Desactiva las habilidades del personaje mientras dura.", Duration = 1, }) diff --git a/src/ReplicatedStorage/Effects/Invisibility.luau b/src/ReplicatedStorage/Effects/Invisibility.luau index dd9e64a3..9119c4a8 100644 --- a/src/ReplicatedStorage/Effects/Invisibility.luau +++ b/src/ReplicatedStorage/Effects/Invisibility.luau @@ -29,7 +29,7 @@ end return Effect.New({ Name = "Invisibility", - Description = "Makes the player fairly transparent (but never fully invisible) the higher the level.", + Description = "Vuelve al jugador más transparente cuanto más alto es el nivel (nunca del todo invisible).", Duration = 10, TransparentInstances = {}, diff --git a/src/ReplicatedStorage/Effects/KillerSpecific/SpawnProtection.luau b/src/ReplicatedStorage/Effects/KillerSpecific/SpawnProtection.luau index e8e9d960..8bab2d40 100644 --- a/src/ReplicatedStorage/Effects/KillerSpecific/SpawnProtection.luau +++ b/src/ReplicatedStorage/Effects/KillerSpecific/SpawnProtection.luau @@ -6,7 +6,7 @@ local Types = require(ReplicatedStorage.Classes.Types) return Effect.New({ Name = "Spawn Protection", - Description = "Prevents a killer from being stunned for a set time, generally 12 seconds.", + Description = "Impide que aturdan al asesino durante un tiempo, normalmente 12 segundos.", Duration = 12, ShowInGUI = false, ApplyEffect = function(self: Types.Effect, level: number, char: Model, duration: number) diff --git a/src/ReplicatedStorage/Effects/KillerSpecific/Stunned.luau b/src/ReplicatedStorage/Effects/KillerSpecific/Stunned.luau index 528cde12..761d10d6 100644 --- a/src/ReplicatedStorage/Effects/KillerSpecific/Stunned.luau +++ b/src/ReplicatedStorage/Effects/KillerSpecific/Stunned.luau @@ -10,7 +10,7 @@ local PlayerSpeedManager = RunService:IsServer() and require(game:GetService("Se return Effect.New({ Name = "Stunned", - Description = "Keeps the player still and unable to use their abilities for a specific amount of time.", + Description = "Deja al jugador quieto y sin poder usar habilidades durante un tiempo.", Duration = 3, ShowInGUI = true, ApplyEffect = function(_own: Types.Effect, _level: number, char: Model) diff --git a/src/ReplicatedStorage/Effects/Resistance.luau b/src/ReplicatedStorage/Effects/Resistance.luau index a8539597..4b4ca4bc 100644 --- a/src/ReplicatedStorage/Effects/Resistance.luau +++ b/src/ReplicatedStorage/Effects/Resistance.luau @@ -2,6 +2,6 @@ local Effect = require(game:GetService("ReplicatedStorage").Classes.Effect) return Effect.New({ Name = "Resistance", - Description = "Reduces the damage inflicted to the player (produced by the killer, not by the environment or himself).", + Description = "Reduce el daño que recibe el jugador del asesino (no el del entorno ni el propio).", Duration = 15, }) diff --git a/src/ReplicatedStorage/Effects/Slowness.luau b/src/ReplicatedStorage/Effects/Slowness.luau index 5d2314df..ec184d9e 100644 --- a/src/ReplicatedStorage/Effects/Slowness.luau +++ b/src/ReplicatedStorage/Effects/Slowness.luau @@ -8,7 +8,7 @@ local PlayerSpeedManager = RunService:IsServer() and require(game:GetService("Se return Effect.New({ Name = "Slowness", - Description = "Makes the player slower the higher the level.", + Description = "Vuelve al jugador más lento cuanto más alto es el nivel.", ApplyEffect = function(_own: Types.Effect, level: number, char: Model) if RunService:IsServer() then PlayerSpeedManager.AddSpeedFactor(Players:GetPlayerFromCharacter(char), "SlownessEffect", 1 - 0.26 * level) diff --git a/src/ReplicatedStorage/Effects/Speed.luau b/src/ReplicatedStorage/Effects/Speed.luau index fb3f9223..1a2e20c9 100644 --- a/src/ReplicatedStorage/Effects/Speed.luau +++ b/src/ReplicatedStorage/Effects/Speed.luau @@ -8,7 +8,7 @@ local PlayerSpeedManager = RunService:IsServer() and require(game:GetService("Se return Effect.New({ Name = "Speed", - Description = "Makes the player faster the higher the level.", + Description = "Vuelve al jugador más rápido cuanto más alto es el nivel.", ApplyEffect = function(_own: Types.Effect, level: number, char: Model) if RunService:IsServer() then PlayerSpeedManager.AddSpeedFactor(Players:GetPlayerFromCharacter(char), "SpeedEffect", 1 + 0.26 * level) diff --git a/src/ReplicatedStorage/Effects/Undetectable.luau b/src/ReplicatedStorage/Effects/Undetectable.luau index 5ec65eaf..98359fc6 100644 --- a/src/ReplicatedStorage/Effects/Undetectable.luau +++ b/src/ReplicatedStorage/Effects/Undetectable.luau @@ -2,6 +2,6 @@ local Effect = require(game:GetService("ReplicatedStorage").Classes.Effect) return Effect.New({ Name = "Undetectable", - Description = "Makes the player's aura invisible for the duration of the effect.", + Description = "Hace invisible el aura del jugador mientras dura el efecto.", Duration = 15, }) diff --git a/src/ReplicatedStorage/Security/ModeratorCommands/KillPlayer.luau b/src/ReplicatedStorage/Security/ModeratorCommands/KillPlayer.luau index 0d900d86..081cc6ec 100644 --- a/src/ReplicatedStorage/Security/ModeratorCommands/KillPlayer.luau +++ b/src/ReplicatedStorage/Security/ModeratorCommands/KillPlayer.luau @@ -3,7 +3,7 @@ return { Executable = true, Params = { { - Title = "Chosen Player", + Title = "Jugador elegido", Type = "String", Default = "...", }, diff --git a/src/ReplicatedStorage/Security/ModeratorCommands/TurnPlayerInto.luau b/src/ReplicatedStorage/Security/ModeratorCommands/TurnPlayerInto.luau index d3284147..9cc17bbe 100644 --- a/src/ReplicatedStorage/Security/ModeratorCommands/TurnPlayerInto.luau +++ b/src/ReplicatedStorage/Security/ModeratorCommands/TurnPlayerInto.luau @@ -11,12 +11,12 @@ return { Executable = true, Params = { { - Title = "Chosen Player", + Title = "Jugador elegido", Type = "Player", Default = "...", }, { - Title = "Character Type", + Title = "Tipo de personaje", Default = "...", Cache = { Options = { diff --git a/src/ServerScriptService/Managers/GeneratorManager.luau b/src/ServerScriptService/Managers/GeneratorManager.luau index a9610492..355993dd 100644 --- a/src/ServerScriptService/Managers/GeneratorManager.luau +++ b/src/ServerScriptService/Managers/GeneratorManager.luau @@ -163,7 +163,7 @@ local function completeStage(player: Player, session: Session) local prompt = session.panel:FindFirstChildOfClass("ProximityPrompt") if progress < config.Stages then if prompt then - prompt.ObjectText = `Generator {progress}/{config.Stages}` + prompt.ObjectText = `Generador {progress}/{config.Stages}` end startCheck(player, session) return @@ -217,8 +217,8 @@ local function setupGenerator(generator: Model, janitor: any): boolean paintStatus(generator, 0) local prompt = Instance.new("ProximityPrompt") - prompt.ActionText = "Repair" - prompt.ObjectText = `Generator 0/{config.Stages}` + prompt.ActionText = "Reparar" + prompt.ObjectText = `Generador 0/{config.Stages}` prompt.HoldDuration = 0 prompt.MaxActivationDistance = 8 prompt.RequiresLineOfSight = false diff --git a/src/ServerScriptService/Managers/PlayerManager/init.luau b/src/ServerScriptService/Managers/PlayerManager/init.luau index cd71c036..2b486203 100644 --- a/src/ServerScriptService/Managers/PlayerManager/init.luau +++ b/src/ServerScriptService/Managers/PlayerManager/init.luau @@ -155,7 +155,7 @@ function PlayerManager:Init() Network:FireConnection("KilledPlayer", KillerValue.Value, plr) end - CommonFunctions.GrantRewardToPlayer(KillerValue.Value, {Money = 7, EXP = 20, Reason = "making a Survivor ragequit"}) + CommonFunctions.GrantRewardToPlayer(KillerValue.Value, {Money = 7, EXP = 20, Reason = "hacer que un superviviente abandone"}) --i'm leaving this because it's funny to have double the extra time when someone ragequits lol -Dyscarn TimeManager.SetTime(TimeManager.CurrentTime + 35) @@ -175,7 +175,7 @@ function PlayerManager:Init() Network:FireConnection("KilledPlayer", KillerValue.Value, plr) CommonFunctions.UpdatePlayerStat(KillerValue.Value, "KillerStats.Kills", 1) - CommonFunctions.GrantRewardToPlayer(KillerValue.Value, {Money = 20, EXP = 60, Reason = "killing a Survivor"}) + CommonFunctions.GrantRewardToPlayer(KillerValue.Value, {Money = 20, EXP = 60, Reason = "matar a un superviviente"}) TimeManager.SetTime(TimeManager.CurrentTime + 35) end diff --git a/src/ServerScriptService/Managers/RoundManager.luau b/src/ServerScriptService/Managers/RoundManager.luau index b9532cd9..c81f6810 100644 --- a/src/ServerScriptService/Managers/RoundManager.luau +++ b/src/ServerScriptService/Managers/RoundManager.luau @@ -187,7 +187,7 @@ function RoundManager.Callables.EndRound(winner: "Survivors" | "Killer") if winner == "Killer" then for _, Killer in RoundManager.CurrentRound.Killers do CommonFunctions.UpdatePlayerStat(Killer.Player, "KillerStats.KillerWins", 1) - CommonFunctions.GrantRewardToPlayer(Killer.Player, {Money = 120, EXP = 180, Reason = "winning a match as Killer"}) + CommonFunctions.GrantRewardToPlayer(Killer.Player, {Money = 120, EXP = 180, Reason = "ganar una partida como asesino"}) end --survivor losses aren't handled here since when a player dies it directly counts Utils.Misc.Print("killer wins") @@ -197,7 +197,7 @@ function RoundManager.Callables.EndRound(winner: "Survivors" | "Killer") end for _, Player: Player in RoundManager.CurrentRound.PlayersRemaining do CommonFunctions.UpdatePlayerStat(Player, "SurvivorStats.SurvivorWins", 1) - CommonFunctions.GrantRewardToPlayer(Player, {Money = 70, EXP = 110, Reason = "winning a match as Survivor"}) + CommonFunctions.GrantRewardToPlayer(Player, {Money = 70, EXP = 110, Reason = "ganar una partida como superviviente"}) end Utils.Misc.Print("survivors win") diff --git a/src/ServerScriptService/System/CommonFunctions.luau b/src/ServerScriptService/System/CommonFunctions.luau index 59248b3c..a7f29db2 100644 --- a/src/ServerScriptService/System/CommonFunctions.luau +++ b/src/ServerScriptService/System/CommonFunctions.luau @@ -3,15 +3,15 @@ local CommonFunctions = { KickIDs = { Noclipping = { ID = "0001", - Message = "Player assumed to be noclipping.", + Message = "Se ha detectado que atraviesas paredes.", }, Flying = { ID = "0002", - Message = "Player assumed to be flying.", + Message = "Se ha detectado que vuelas.", }, TPing = { ID = "0003", - Message = "Player assumed to be teleporting across great distances.", + Message = "Se ha detectado un teletransporte a gran distancia.", }, }, AdminPerms = { diff --git a/src/StarterPlayerScripts/Miscellaneous/GeneratorClient.luau b/src/StarterPlayerScripts/Miscellaneous/GeneratorClient.luau index f4ad1bf6..dff289f1 100644 --- a/src/StarterPlayerScripts/Miscellaneous/GeneratorClient.luau +++ b/src/StarterPlayerScripts/Miscellaneous/GeneratorClient.luau @@ -50,7 +50,7 @@ return { local counter = label(gui, UDim2.new(0.16, 0, 0.035, 0), UDim2.new(0.5, 0, 0.1, 4), "") local function updateCounter() local total = workspace:GetAttribute("GeneratorsTotal") or 0 - counter.Text = `Generators {workspace:GetAttribute("GeneratorsRepaired") or 0}/{total}` + counter.Text = `Generadores {workspace:GetAttribute("GeneratorsRepaired") or 0}/{total}` counter.Visible = RoundState.Value == "InRound" and total > 0 end workspace:GetAttributeChangedSignal("GeneratorsRepaired"):Connect(updateCounter) @@ -78,7 +78,7 @@ return { tapButton.BackgroundTransparency = 0.3 tapButton.FontFace = FONT tapButton.TextScaled = true - tapButton.Text = "REPAIR" + tapButton.Text = "REPARAR" tapButton.Visible = false tapButton.Parent = gui local corner = Instance.new("UICorner") @@ -113,7 +113,7 @@ return { ProximityPromptService.Enabled = false local touch = UserInputService:GetLastInputType() == Enum.UserInputType.Touch local gamepad = UserInputService:GetLastInputType().Name:find("Gamepad") ~= nil - hint.Text = if touch then "Tap REPAIR in the green zone" elseif gamepad then "Press X in the green zone" else "Press C in the green zone" + hint.Text = if touch then "Toca REPARAR en la zona verde" elseif gamepad then "Pulsa X en la zona verde" else "Pulsa C en la zona verde" tapButton.Visible = touch zone.Position = UDim2.new(zoneStart, 0, 0, 0) zone.Size = UDim2.new(width, 0, 1, 0) diff --git a/src/StarterPlayerScripts/Miscellaneous/RoundTips.luau b/src/StarterPlayerScripts/Miscellaneous/RoundTips.luau index 68c60ef0..d1d88356 100644 --- a/src/StarterPlayerScripts/Miscellaneous/RoundTips.luau +++ b/src/StarterPlayerScripts/Miscellaneous/RoundTips.luau @@ -15,25 +15,25 @@ local TIP_TIME = 5 local function keys(): { repair: string, sprint: string } local input = UserInputService:GetLastInputType() if input == Enum.UserInputType.Touch then - return { repair = "tap REPAIR", sprint = "tap the sprint button" } + return { repair = "toca REPARAR", sprint = "toca el botón de correr" } elseif input.Name:find("Gamepad") then - return { repair = "press X", sprint = "hold L2" } + return { repair = "pulsa X", sprint = "mantén L2" } end - return { repair = "press C", sprint = "hold Shift" } + return { repair = "pulsa C", sprint = "mantén Shift" } end local function tipsFor(role: string): { string } local k = keys() if role == "Killer" then return { - "Hunt the survivors before the clock runs out. Every kill adds time.", - "A loud bang means a survivor failed a repair: that generator glows red for you.", + "Caza a los supervivientes antes de que se acabe el reloj. Cada muerte suma tiempo.", + "Un estallido fuerte significa que un superviviente falló una reparación: ese generador se ilumina en rojo para ti.", } end return { - `Repair generators: {k.repair} when the needle is in the green zone. Each one cuts 15 s off the clock.`, - `Run from the killer: {k.sprint}. Repair together to get a wider green zone.`, - "Survive until the clock hits zero. Medkits heal you.", + `Repara generadores: {k.repair} cuando la aguja esté en la zona verde. Cada uno quita 15 s al reloj.`, + `Huye del asesino: {k.sprint}. Reparando juntos la zona verde es más ancha.`, + "Sobrevive hasta que el reloj llegue a cero. Los botiquines te curan.", } end @@ -111,7 +111,7 @@ return { Utils.Character.ObserveCharacter(LocalPlayer, function(character: Model) local role = character:WaitForChild("Role", 5) :: StringValue? if role and role.Value == "Spectator" and RoundState.Value == "InRound" then - show({ "You're out. Spectate the others: the next round starts soon." }) + show({ "Has caído. Mira a los demás: la siguiente ronda empieza pronto." }) end end) end, diff --git a/src/StarterPlayerScripts/UI/AchievementsMenu.luau b/src/StarterPlayerScripts/UI/AchievementsMenu.luau index 5dc7f8e6..0ba9aca8 100644 --- a/src/StarterPlayerScripts/UI/AchievementsMenu.luau +++ b/src/StarterPlayerScripts/UI/AchievementsMenu.luau @@ -19,8 +19,8 @@ local AchievementsMenu = { Janitor = nil, Defaults = { Shown = { - Title = "Achievement Name", - Description = "Achievement Description", + Title = "Nombre del logro", + Description = "Descripción del logro", Icon = "rbxasset://textures/ui/GuiImagePlaceholder.png", }, Hidden = { @@ -135,7 +135,7 @@ function AchievementsMenu._SetupAchievements() end end - Counter.Text = `Achievements ({Unlocked}/{Total} Unlocked)` + Counter.Text = `Logros ({Unlocked}/{Total} desbloqueados)` end local function MakeCategoryButton(Name: string, Title: string?, Category: Frame?) @@ -269,7 +269,7 @@ function AchievementsMenu._SetupAchievements() RewardText = RewardText..Suffix end - Achievement.Reward.Text = "Reward: "..RewardText + Achievement.Reward.Text = "Recompensa: "..RewardText Achievement.MouseEnter:Connect(function() if AchievementEquivalent.HideRewardIfLocked and not IsAchievementUnlocked(AchievementValue, AchievementEquivalent) then diff --git a/src/StarterPlayerScripts/UI/Changelog.luau b/src/StarterPlayerScripts/UI/Changelog.luau index 19e60bc2..714e3169 100644 --- a/src/StarterPlayerScripts/UI/Changelog.luau +++ b/src/StarterPlayerScripts/UI/Changelog.luau @@ -44,51 +44,51 @@ local CurrentChangelog: ChangelogFormat = { Entries = { { Type = "Separator", - Text = "Section N.º 1", + Text = "Sección N.º 1", }, { Type = "SectionEntry", - Text = "Pretty cool feature N.º 1!", + Text = "¡Novedad N.º 1!", }, { Type = "SectionEntry", - Text = "Pretty cool feature N.º 2!", + Text = "¡Novedad N.º 2!", }, { Type = "Text", - Text = "Quick note N.º 1!", + Text = "¡Nota N.º 1!", }, { Type = "SectionEntry", - Text = "Pretty cool feature N.º 3!", + Text = "¡Novedad N.º 3!", }, { Type = "Text", - Text = "Quick note N.º 2!", + Text = "¡Nota N.º 2!", }, { Type = "Separator", - Text = "Section N.º 2", + Text = "Sección N.º 2", }, { Type = "SectionEntry", - Text = "Pretty cool feature N.º 1!", + Text = "¡Novedad N.º 1!", }, { Type = "Text", - Text = "Quick note N.º 1!", + Text = "¡Nota N.º 1!", }, { Type = "SectionEntry", - Text = "Pretty cool feature N.º 2!", + Text = "¡Novedad N.º 2!", }, { Type = "SectionEntry", - Text = "Pretty cool feature N.º 3!", + Text = "¡Novedad N.º 3!", }, { Type = "Text", - Text = "Quick note N.º 2!", + Text = "¡Nota N.º 2!", }, }, } diff --git a/src/StarterPlayerScripts/UI/Credits.luau b/src/StarterPlayerScripts/UI/Credits.luau index 02ab87a6..2949dd5c 100644 --- a/src/StarterPlayerScripts/UI/Credits.luau +++ b/src/StarterPlayerScripts/UI/Credits.luau @@ -27,13 +27,13 @@ local Credits: {{ Name: string, Credits: {Credit} }} = { Render = "rbxassetid://112215528635901", DisplayName = "Dyscarn", Username = "@LrgeAlvaro8882", - Description = "Made the engine used in this game: Dysymmetrical.", + Description = "Hizo el motor de este juego: Dysymmetrical.", }, { Render = "", DisplayName = "heromax149807", Username = "@heromax149807", - Description = "Made the original engine art like the sidebar icons, the buttons, etc.", + Description = "Hizo el arte original del motor: iconos de la barra lateral, botones, etc.", }, }, }, diff --git a/src/StarterPlayerScripts/UI/Inventory/init.luau b/src/StarterPlayerScripts/UI/Inventory/init.luau index 699154c5..4f4d39cb 100644 --- a/src/StarterPlayerScripts/UI/Inventory/init.luau +++ b/src/StarterPlayerScripts/UI/Inventory/init.luau @@ -148,7 +148,7 @@ function Inventory.EquipItem() end local lastEquipped = Equipped["Equipped"..Inventory.SelectedItem.Type].Value - Buttons.Equip.Title.Text = "Equipped" + Buttons.Equip.Title.Text = "Equipado" Network:FireServerConnection("EquipItem", "REMOTE_EVENT", Inventory.SelectedItem.Name, Inventory.SelectedItem.Type) if Equipped["Equipped"..Inventory.SelectedItem.Type].Value == lastEquipped then Equipped["Equipped"..Inventory.SelectedItem.Type].Changed:Wait() @@ -715,9 +715,9 @@ function Inventory.OnCharButtonPress(codeName: string, info: Types.Killer | Type PreviewInstances.Title.Text = info.Config.Name if Equipped["Equipped"..Type].Value == codeName then - Buttons.Equip.Title.Text = "Equipped" + Buttons.Equip.Title.Text = "Equipado" else - Buttons.Equip.Title.Text = "Equip" + Buttons.Equip.Title.Text = "Equipar" end Inventory.SelectedItem.Name = codeName diff --git a/src/StarterPlayerScripts/UI/ModPanel/Bool.luau b/src/StarterPlayerScripts/UI/ModPanel/Bool.luau index 8a1dd284..78dccfa4 100644 --- a/src/StarterPlayerScripts/UI/ModPanel/Bool.luau +++ b/src/StarterPlayerScripts/UI/ModPanel/Bool.luau @@ -12,7 +12,7 @@ function BoolCommand.New(CommandModule: ModuleScript, UI): ImageLabel CommandFrame.Parent = UI.Content CommandFrame.Label.Text = Config.Name - CommandFrame.Value.Text = "Current value: "..tostring(workspace:GetAttribute(Config.DisplayedAttribute)) + CommandFrame.Value.Text = "Valor actual: "..tostring(workspace:GetAttribute(Config.DisplayedAttribute)) CommandFrame.Execute.MouseEnter:Connect(function() Sounds.PlaySound(Sounds.CommonlyUsedSounds.ButtonHover, {Volume = 0.2}) end) @@ -22,7 +22,7 @@ function BoolCommand.New(CommandModule: ModuleScript, UI): ImageLabel CommandFrame.Execute.MouseButton1Click:Connect(function() Sounds.PlaySound(Sounds.CommonlyUsedSounds.ButtonPress, {Volume = 0.8}) local Value = Network:FireServerConnection("ExecuteCommand", "REMOTE_FUNCTION", CommandModule.Name) - CommandFrame.Value.Text = "Current value: "..tostring(Value or workspace:GetAttribute(Config.DisplayedAttribute)) + CommandFrame.Value.Text = "Valor actual: "..tostring(Value or workspace:GetAttribute(Config.DisplayedAttribute)) end) return CommandFrame diff --git a/src/StarterPlayerScripts/UI/ModPanel/Enum.luau b/src/StarterPlayerScripts/UI/ModPanel/Enum.luau index 6ac0a160..0cdf9fe0 100644 --- a/src/StarterPlayerScripts/UI/ModPanel/Enum.luau +++ b/src/StarterPlayerScripts/UI/ModPanel/Enum.luau @@ -17,7 +17,7 @@ function Dropdown.New(CommandModule: ModuleScript, UI): ImageLabel CommandFrame.Parent = UI.Content CommandFrame.Label.Text = Config.Name - CommandFrame.Value.Text = "Current value: "..tostring(workspace:GetAttribute(Config.DisplayedAttribute)) + CommandFrame.Value.Text = "Valor actual: "..tostring(workspace:GetAttribute(Config.DisplayedAttribute)) for lo, param in Config.Params do local Value = ValuePrefab:Clone() @@ -35,7 +35,7 @@ function Dropdown.New(CommandModule: ModuleScript, UI): ImageLabel CommandFrame.Execute.MouseButton1Click:Connect(function() Sounds.PlaySound(Sounds.CommonlyUsedSounds.ButtonPress, {Volume = 0.8}) local Value = Network:FireServerConnection("ExecuteCommand", "REMOTE_FUNCTION", CommandModule.Name) - CommandFrame.Value.Text = "Current value: "..tostring(Value or workspace:GetAttribute(Config.DisplayedAttribute)) + CommandFrame.Value.Text = "Valor actual: "..tostring(Value or workspace:GetAttribute(Config.DisplayedAttribute)) end) return CommandFrame diff --git a/src/StarterPlayerScripts/UI/ModPanel/init.luau b/src/StarterPlayerScripts/UI/ModPanel/init.luau index 553bb2bc..efc28ebf 100644 --- a/src/StarterPlayerScripts/UI/ModPanel/init.luau +++ b/src/StarterPlayerScripts/UI/ModPanel/init.luau @@ -56,7 +56,7 @@ function ModPanel.Start() local TopbarButton = TopbarPlus.new() TopbarButton:setOrder(0) TopbarButton:oneClick(true) - TopbarButton:setLabel("Command Panel") + TopbarButton:setLabel("Panel de comandos") TopbarButton:setTextFont(Enum.Font.Bodoni) TopbarButton:bindEvent("deselected", function() Sounds.PlaySound(Sounds.CommonlyUsedSounds.ButtonPress, {Volume = 0.8}) diff --git a/src/StarterPlayerScripts/UI/MoneyXPRewards.luau b/src/StarterPlayerScripts/UI/MoneyXPRewards.luau index 1287286a..25cf74e9 100644 --- a/src/StarterPlayerScripts/UI/MoneyXPRewards.luau +++ b/src/StarterPlayerScripts/UI/MoneyXPRewards.luau @@ -23,9 +23,9 @@ function MoneyXPRewards:Init() local Size = Notification.Size Notification.Size = Utils.UDim.Zero - local Text = "Granted " + local Text = "Has ganado " if Money and EXP then - Text = Text..tostring(Money).."$ and "..tostring(EXP).." EXP" + Text = Text..tostring(Money).."$ y "..tostring(EXP).." EXP" elseif Money then Text = Text..tostring(Money).."$" elseif EXP then @@ -36,7 +36,7 @@ function MoneyXPRewards:Init() end if Reason then - Text = Text.." for "..Reason + Text = Text.." por "..Reason end Text = Text.."." diff --git a/src/StarterPlayerScripts/UI/PlayerList/PlayerInfoWindow.luau b/src/StarterPlayerScripts/UI/PlayerList/PlayerInfoWindow.luau index 67f56543..63d17266 100644 --- a/src/StarterPlayerScripts/UI/PlayerList/PlayerInfoWindow.luau +++ b/src/StarterPlayerScripts/UI/PlayerList/PlayerInfoWindow.luau @@ -100,7 +100,7 @@ function PlayerInfoWindow.New(Player: Player, Parent: ScreenGui) if #Skin <= 0 then local Default = require(Utils.Instance.GetCharacterModule("Survivor", Value)) - Window.Instance.CharactersUsing.Survivor.PortraitLabel.SkinLabel.Text = "Default" + Window.Instance.CharactersUsing.Survivor.PortraitLabel.SkinLabel.Text = "Por defecto" Window.Instance.CharactersUsing.Survivor.PortraitLabel.PortraitImage.Image = Default.Config.Render return @@ -142,7 +142,7 @@ function PlayerInfoWindow.New(Player: Player, Parent: ScreenGui) if #Skin <= 0 then local Default = require(Utils.Instance.GetCharacterModule("Killer", Value)) - Window.Instance.CharactersUsing.Killer.PortraitLabel.SkinLabel.Text = "Default" + Window.Instance.CharactersUsing.Killer.PortraitLabel.SkinLabel.Text = "Por defecto" Window.Instance.CharactersUsing.Killer.PortraitLabel.PortraitImage.Image = Default.Config.Render return diff --git a/src/StarterPlayerScripts/UI/RoundPlayerList.luau b/src/StarterPlayerScripts/UI/RoundPlayerList.luau index bd100237..a4b6f518 100644 --- a/src/StarterPlayerScripts/UI/RoundPlayerList.luau +++ b/src/StarterPlayerScripts/UI/RoundPlayerList.luau @@ -80,7 +80,7 @@ function RoundPlayerList._InitUI(Char: Model) Card.PortraitOutline.ImageColor3 = Color3.fromRGB(255, 0, 0) Card.PlayerState.TextColor3 = Color3.fromRGB(255, 0, 0) - Card.PlayerState.Text = "DEAD" + Card.PlayerState.Text = "MUERTO" Card.PlayerState.TextTransparency = 1 Card.PlayerHealth.TextTransparency = 1 @@ -116,7 +116,7 @@ function RoundPlayerList._InitUI(Char: Model) Card.PortraitOutline.ImageColor3 = Color3.fromRGB(150, 150, 150) Card.PlayerState.TextColor3 = Color3.fromRGB(170, 170, 170) - Card.PlayerState.Text = "LOST" + Card.PlayerState.Text = "SE FUE" Card.PlayerState.TextTransparency = 0 Card.PlayerHealth.TextTransparency = 1 diff --git a/src/StarterPlayerScripts/UI/RulesMenu.luau b/src/StarterPlayerScripts/UI/RulesMenu.luau index e6607e5e..0b2f73a8 100644 --- a/src/StarterPlayerScripts/UI/RulesMenu.luau +++ b/src/StarterPlayerScripts/UI/RulesMenu.luau @@ -19,51 +19,51 @@ local Ruleset: {{ }} = { { Type = "Separator", - Text = "Rule section N.º 1", + Text = "Sección de normas N.º 1", }, { Type = "SectionEntry", - Text = "Pretty cool rule N.º 1!", + Text = "¡Norma N.º 1!", }, { Type = "SectionEntry", - Text = "Pretty cool feature N.º 2!", + Text = "¡Norma N.º 2!", }, { Type = "Text", - Text = "Quick note N.º 1!", + Text = "¡Nota N.º 1!", }, { Type = "SectionEntry", - Text = "Pretty cool rule N.º 3!", + Text = "¡Norma N.º 3!", }, { Type = "Text", - Text = "Quick note N.º 2!", + Text = "¡Nota N.º 2!", }, { Type = "Separator", - Text = "Rule section N.º 2", + Text = "Sección de normas N.º 2", }, { Type = "SectionEntry", - Text = "Pretty cool rule N.º 1!", + Text = "¡Norma N.º 1!", }, { Type = "Text", - Text = "Quick note N.º 1!", + Text = "¡Nota N.º 1!", }, { Type = "SectionEntry", - Text = "Pretty cool rule N.º 2!", + Text = "¡Norma N.º 2!", }, { Type = "SectionEntry", - Text = "Pretty cool rule N.º 3!", + Text = "¡Norma N.º 3!", }, { Type = "Text", - Text = "Quick note N.º 2!", + Text = "¡Nota N.º 2!", }, } diff --git a/src/StarterPlayerScripts/UI/Shop/BuyWindow.luau b/src/StarterPlayerScripts/UI/Shop/BuyWindow.luau index 05bc066d..61f41810 100644 --- a/src/StarterPlayerScripts/UI/Shop/BuyWindow.luau +++ b/src/StarterPlayerScripts/UI/Shop/BuyWindow.luau @@ -21,7 +21,7 @@ function BuyWindow.New(Shop, Module: ModuleScript, skin: boolean?, Parent: Frame skin = false end local CharInfo = require(Module) - Window.Instance.PriceTag.Text = "Purchase "..CharInfo.Config.Name.." for "..(CharInfo.Config.Price > 0 and CharInfo.Config.Price.."$" or "FREE").."?" + Window.Instance.PriceTag.Text = "¿Comprar "..CharInfo.Config.Name.." por "..(CharInfo.Config.Price > 0 and CharInfo.Config.Price.." $" or "GRATIS").."?" Window.Instance.Parent = Parent Window.Instance.Size = Utils.UDim.Zero @@ -33,7 +33,7 @@ function BuyWindow.New(Shop, Module: ModuleScript, skin: boolean?, Parent: Frame Window.Instance.Buy.TextLabel.Text = "..." local Success = Network:FireServerConnection("BuyItem", "REMOTE_FUNCTION", Module) if Success then - Window.Instance.Buy.TextLabel.Text = "Purchased!" + Window.Instance.Buy.TextLabel.Text = "¡Comprado!" Card.Container.Owned.BackgroundTransparency = 0.55 Card.Container.Owned.TextLabel.TextTransparency = 0 Window.Purchased:Fire() @@ -41,7 +41,7 @@ function BuyWindow.New(Shop, Module: ModuleScript, skin: boolean?, Parent: Frame Window:Hide() end)) else - Window.Instance.Buy.TextLabel.Text = "you're too poor." + Window.Instance.Buy.TextLabel.Text = "no te llega el dinero." Window.Checking = false end end)) diff --git a/src/StarterPlayerScripts/UI/Shop/init.luau b/src/StarterPlayerScripts/UI/Shop/init.luau index 22dc0ec0..66ee9c3c 100644 --- a/src/StarterPlayerScripts/UI/Shop/init.luau +++ b/src/StarterPlayerScripts/UI/Shop/init.luau @@ -208,7 +208,7 @@ function Shop.ToggleSkinsMenu(Open: boolean?) Card.Parent = SkinContainer.ScrollingFrame Container.Title.Text = Info.Config.Name Container.CharacterRender.Image = Info.Config.Render - Container.Price.Text = Info.Config.Price > 0 and tostring(Info.Config.Price).."$" or "FREE" + Container.Price.Text = Info.Config.Price > 0 and tostring(Info.Config.Price).." $" or "GRATIS" if Info.Config.CardFrame then Container.Outline.Image = Info.Config.CardFrame.Image or Container.Outline.Image @@ -760,12 +760,12 @@ function Shop.OnCharButtonPress(codeName: string, info: Types.Killer | Types.Sur PreviewInstances.Title.Text = info.Config.Name if Utils.PlayerData.GetPlayerOwned(LocalPlayer, Type.."s."..codeName) then - Buttons.Purchase.Title.Text = "Purchased" + Buttons.Purchase.Title.Text = "Comprado" else - Buttons.Purchase.Title.Text = "Purchase" + Buttons.Purchase.Title.Text = "Comprar" end - ButtonsContainer.CurrentItemPrice.Text = info.Config.Price > 0 and tostring(info.Config.Price).."$" or "FREE" + ButtonsContainer.CurrentItemPrice.Text = info.Config.Price > 0 and tostring(info.Config.Price).." $" or "GRATIS" Shop.SelectedItem.Name = codeName Shop.SelectedItem.Type = Type @@ -889,7 +889,7 @@ function Shop.ReloadContent(CreateCards: boolean?) Card.Parent = Parent Container.Title.Text = Info.Config.Name Container.CharacterRender.Image = Info.Config.Render - Container.Price.Text = Info.Config.Price > 0 and tostring(Info.Config.Price).."$" or "FREE" + Container.Price.Text = Info.Config.Price > 0 and tostring(Info.Config.Price).." $" or "GRATIS" if Info.Config.CardFrame then Container.Outline.Image = Info.Config.CardFrame.Image or Container.Outline.Image @@ -1057,9 +1057,9 @@ function Shop.CreateBuyWindow(Module: ModuleScript, skin: boolean?, Parent: Fram local Window = BuyWindow.New(Shop, Module, skin, Parent, Card) Window.Purchased:Connect(function() if Utils.PlayerData.GetPlayerOwned(LocalPlayer, Shop.SelectedItem.Type.."s."..Shop.SelectedItem.Name) then - Buttons.Purchase.Title.Text = "Purchased" + Buttons.Purchase.Title.Text = "Comprado" else - Buttons.Purchase.Title.Text = "Purchase" + Buttons.Purchase.Title.Text = "Comprar" end Shop.ReloadContent() Shop.ReorderSkins() diff --git a/src/StarterPlayerScripts/UI/Time.luau b/src/StarterPlayerScripts/UI/Time.luau index 2b808499..ae45df96 100644 --- a/src/StarterPlayerScripts/UI/Time.luau +++ b/src/StarterPlayerScripts/UI/Time.luau @@ -17,10 +17,10 @@ return { local EnoughPlayers = ReplicatedStorage.RoundInfo.EnoughPlayers local LabelTexts = { - Lobby = "Round begins in...", - InRound = "Round ends in...", - NotEnough = "Waiting for players...", - Frozen = "Time frozen!", + Lobby = "La ronda empieza en...", + InRound = "La ronda acaba en...", + NotEnough = "Esperando jugadores...", + Frozen = "¡Tiempo congelado!", } TimeLabel.Text = Utils.Math.ConvertToMinSec(math.ceil(TimeRemaining.Value)) diff --git a/tools/maps/Instalacion.luau b/tools/maps/Instalacion.luau index 0d1e472c..28f70c61 100644 --- a/tools/maps/Instalacion.luau +++ b/tools/maps/Instalacion.luau @@ -395,12 +395,12 @@ local function sign(text: string, pos: Vector3, face: Enum.NormalId, color: Colo label.Parent = gui end local WHITE, RED, AMBER = Color3.fromRGB(220, 224, 230), Color3.fromRGB(210, 40, 36), Color3.fromRGB(230, 170, 40) -sign("ANOMALIS RESEARCH SITE 7", Vector3.new(0, 16, 138.6), Enum.NormalId.Front, WHITE) -sign("SECTOR C LABORATORIES", Vector3.new(-35, 12, -39.1), Enum.NormalId.Back, WHITE) -sign("CONTAINMENT AUTHORIZED ONLY", Vector3.new(-35, 10, -84.1), Enum.NormalId.Back, RED) -sign("REACTOR 02 RADIATION", Vector3.new(105, 16, -49.1), Enum.NormalId.Back, AMBER) -sign("STORAGE B", Vector3.new(-110, 10, 69.1), Enum.NormalId.Front, WHITE) -sign("BARRACKS", Vector3.new(92, 9, 81.9), Enum.NormalId.Front, WHITE) +sign("ANOMALIS CENTRO DE INVESTIGACIÓN 7", Vector3.new(0, 16, 138.6), Enum.NormalId.Front, WHITE) +sign("SECTOR C LABORATORIOS", Vector3.new(-35, 12, -39.1), Enum.NormalId.Back, WHITE) +sign("CONTENCIÓN SOLO AUTORIZADOS", Vector3.new(-35, 10, -84.1), Enum.NormalId.Back, RED) +sign("REACTOR 02 RADIACIÓN", Vector3.new(105, 16, -49.1), Enum.NormalId.Back, AMBER) +sign("ALMACÉN B", Vector3.new(-110, 10, 69.1), Enum.NormalId.Front, WHITE) +sign("BARRACONES", Vector3.new(92, 9, 81.9), Enum.NormalId.Front, WHITE) for _, z in { -83.4, -76.6 } do detail(part(Details, Vector3.new(148, 1.1, 1.1), Vector3.new(-35, 14.6, z), METAL, Enum.Material.Metal, "Pipe")).Shape = Enum.PartType.Cylinder From a8b82bdd9e7d1d712341107a719e5ceffa4ca17d Mon Sep 17 00:00:00 2001 From: Norl4 Date: Sun, 1 Nov 2026 12:59:45 +0100 Subject: [PATCH 15/32] Light Instalacion with fog, lamp cones and lamps that die near the killer, plus potato mode --- .../Miscellaneous/PotatoMode.luau | 36 +++++ tools/maps/Instalacion.luau | 134 +++++++++++++++--- 2 files changed, 153 insertions(+), 17 deletions(-) create mode 100644 src/StarterPlayerScripts/Miscellaneous/PotatoMode.luau diff --git a/src/StarterPlayerScripts/Miscellaneous/PotatoMode.luau b/src/StarterPlayerScripts/Miscellaneous/PotatoMode.luau new file mode 100644 index 00000000..2baa3ebe --- /dev/null +++ b/src/StarterPlayerScripts/Miscellaneous/PotatoMode.luau @@ -0,0 +1,36 @@ +--!strict +local CollectionService = game:GetService("CollectionService") +local Players = game:GetService("Players") + +local GameSettings = UserSettings():GetService("UserGameSettings") + +local POTATO_MAX_QUALITY = 3 + +local function isPotato(): boolean + local level = GameSettings.SavedQualityLevel.Value + return level > 0 and level <= POTATO_MAX_QUALITY +end + +local function apply(detail: Instance) + local on = not isPotato() + if detail:IsA("BasePart") then + detail.LocalTransparencyModifier = if on then 0 else 1 + elseif detail:IsA("ParticleEmitter") or detail:IsA("Beam") or detail:IsA("PostEffect") then + detail.Enabled = on + end +end + +local function applyAll() + Players.LocalPlayer:SetAttribute("Potato", isPotato()) + for _, detail in CollectionService:GetTagged("Detail") do + apply(detail) + end +end + +return { + Init = function(_) + CollectionService:GetInstanceAddedSignal("Detail"):Connect(apply) + GameSettings:GetPropertyChangedSignal("SavedQualityLevel"):Connect(applyAll) + applyAll() + end, +} diff --git a/tools/maps/Instalacion.luau b/tools/maps/Instalacion.luau index 28f70c61..643ab779 100644 --- a/tools/maps/Instalacion.luau +++ b/tools/maps/Instalacion.luau @@ -106,7 +106,7 @@ local function room(container: Instance, name: string, center: Vector2, size: Ve wall(parent, Vector2.new(x1, z0), Vector2.new(x1, z1), height, gaps.east) part(parent, Vector3.new(size.X + 2, 1, size.Y + 2), Vector3.new(center.X, height + 0.5, center.Y), METAL, Enum.Material.CorrodedMetal, "Roof") local lab = container.Name == "LabComplex" - part(parent, Vector3.new(size.X, 0.2, size.Y), Vector3.new(center.X, 0.1, center.Y), if lab then Color3.fromRGB(124, 126, 120) else DARK_CONCRETE, if lab then Enum.Material.CeramicTiles else Enum.Material.Concrete, "Floor") + part(parent, Vector3.new(size.X, 0.2, size.Y), Vector3.new(center.X, 0.1, center.Y), if lab then Color3.fromRGB(96, 98, 94) else DARK_CONCRETE, if lab then Enum.Material.CeramicTiles else Enum.Material.Concrete, "Floor") end local function fillLight(parent: Instance, pos: Vector3) @@ -138,7 +138,7 @@ local function lamp(parent: Instance, pos: Vector3, flicker: boolean, ceiling: b spot.Face = Enum.NormalId.Bottom spot.Angle = 100 spot.Range = if ceiling then 26 else 42 - spot.Brightness = 6 + spot.Brightness = if ceiling then 3.5 else 6 spot.Color = Color3.fromRGB(255, 176, 110) spot.Shadows = shadows == true spot.Parent = head @@ -147,6 +147,19 @@ local function lamp(parent: Instance, pos: Vector3, flicker: boolean, ceiling: b glow.Brightness = 1 glow.Color = spot.Color glow.Parent = head + local top, bottom = Instance.new("Attachment"), Instance.new("Attachment") + bottom.Position = Vector3.new(0, -headY + 0.2, 0) + top.Parent, bottom.Parent = head, head + local cone = Instance.new("Beam") + cone.Attachment0, cone.Attachment1 = top, bottom + cone.FaceCamera = true + cone.Width0, cone.Width1 = 1.8, if ceiling then 9 else 16 + cone.LightEmission = 1 + cone.Color = ColorSequence.new(spot.Color) + cone.Transparency = NumberSequence.new(0.92, 1) + cone.Segments = 1 + cone:AddTag("Detail") + cone.Parent = head if flicker then head:SetAttribute("Flicker", true) end @@ -379,6 +392,7 @@ end local function sign(text: string, pos: Vector3, face: Enum.NormalId, color: Color3) local s = detail(part(Details, Vector3.new(#text * 0.8 + 2, 2.4, 0.2), pos, Color3.fromRGB(28, 30, 34), Enum.Material.Metal, "Sign")) + s:RemoveTag("Detail") local gui = Instance.new("SurfaceGui") gui.Face = face gui.SizingMode = Enum.SurfaceGuiSizingMode.PixelsPerStud @@ -417,6 +431,29 @@ for _, p in { Vector3.new(-70, 0, 20), Vector3.new(15, 0, 50), Vector3.new(-15, end end +local Fog = folder("GroundFog", Details) +for _, c in { Vector2.new(-90, 30), Vector2.new(0, 50), Vector2.new(90, 40), Vector2.new(-30, 110), Vector2.new(110, -15), Vector2.new(-120, -30) } do + local area = detail(part(Fog, Vector3.new(60, 1, 50), Vector3.new(c.X, 0.6, c.Y), CONCRETE, Enum.Material.SmoothPlastic, "FogArea")) + area.Transparency = 1 + local mist = Instance.new("ParticleEmitter") + mist.Texture = "rbxasset://textures/particles/smoke_main.dds" + mist.Shape = Enum.ParticleEmitterShape.Box + mist.EmissionDirection = Enum.NormalId.Top + mist.Rate = 3 + mist.Lifetime = NumberRange.new(10, 14) + mist.Speed = NumberRange.new(0.3, 1) + mist.SpreadAngle = Vector2.new(70, 70) + mist.Size = NumberSequence.new(14, 24) + mist.Transparency = NumberSequence.new({ NumberSequenceKeypoint.new(0, 1), NumberSequenceKeypoint.new(0.3, 0.76), NumberSequenceKeypoint.new(1, 1) }) + mist.Color = ColorSequence.new(Color3.fromRGB(150, 160, 185)) + mist.LightInfluence = 1 + mist.Rotation = NumberRange.new(0, 360) + mist.RotSpeed = NumberRange.new(-6, 6) + mist.Acceleration = Vector3.new(0, -0.05, 0) + mist:AddTag("Detail") + mist.Parent = area +end + local SpawnPoints = folder("SpawnPoints", MapModel) local function spawnPoint(parent: Instance, pos: Vector3) local p = part(parent, Vector3.new(1, 1, 1), pos + Vector3.new(0, 3, 0), CONCRETE, Enum.Material.SmoothPlastic, if parent.Name == "Killers" then "KillerSpawn" else "SurvivorSpawn") @@ -463,7 +500,7 @@ LightingModule.Source = [[return { LightingModule.Parent = Root local atmosphere = Instance.new("Atmosphere") -atmosphere.Density = 0.3 +atmosphere.Density = 0.36 atmosphere.Offset = 0.05 atmosphere.Color = Color3.fromRGB(96, 104, 124) atmosphere.Decay = Color3.fromRGB(36, 38, 52) @@ -488,45 +525,108 @@ bloom.Parent = LightingModule local sky = Instance.new("Sky") sky.StarCount = 1200 sky.CelestialBodiesShown = true +sky.MoonAngularSize = 16 sky.Parent = LightingModule +local depth = Instance.new("DepthOfFieldEffect") +depth.FarIntensity = 0.18 +depth.FocusDistance = 40 +depth.InFocusRadius = 60 +depth.NearIntensity = 0 +depth:AddTag("Detail") +depth.Parent = LightingModule + local Behaviour = Instance.new("ModuleScript") Behaviour.Name = "Behaviour" Behaviour.Source = [[--!strict +local Players = game:GetService("Players") local RunService = game:GetService("RunService") -local function flicker(bulb: BasePart) - local lights = bulb:GetChildren() - local rand = Random.new() - local function set(on: boolean) - bulb.Material = if on then Enum.Material.Neon else Enum.Material.SmoothPlastic - for _, light in lights do - if light:IsA("Light") then - light.Enabled = on - end +local DRAIN_RADIUS = 18 +local STUTTER_RADIUS = 40 + +local function setLit(bulb: BasePart, on: boolean) + bulb.Material = if on then Enum.Material.Neon else Enum.Material.SmoothPlastic + for _, light in bulb:GetChildren() do + if light:IsA("Light") then + light.Enabled = on + elseif light:IsA("Beam") then + light.Enabled = on and not Players.LocalPlayer:GetAttribute("Potato") end end +end + +local function blink(bulb: BasePart, rand: Random) + setLit(bulb, false) + task.wait(rand:NextNumber(0.03, 0.12)) + if not bulb:GetAttribute("Drained") then + setLit(bulb, true) + end +end + +local function flicker(bulb: BasePart) + local rand = Random.new() while bulb:IsDescendantOf(workspace) do task.wait(rand:NextNumber(0.4, 4)) for _ = 1, rand:NextInteger(1, 4) do - set(false) - task.wait(rand:NextNumber(0.03, 0.12)) - set(true) + if not bulb:GetAttribute("Drained") then + blink(bulb, rand) + end task.wait(rand:NextNumber(0.03, 0.2)) end end end +local function killerPositions(): { Vector3 } + local list = {} + for _, player in Players:GetPlayers() do + local char = player.Character + local role = char and char:FindFirstChild("Role") :: StringValue? + local root = char and char:FindFirstChild("HumanoidRootPart") :: BasePart? + local effects = char and char:FindFirstChild("Effects") + if role and root and role.Value == "Killer" and not (effects and effects:FindFirstChild("Undetectable")) then + table.insert(list, root.Position) + end + end + return list +end + +local function drainNearKiller(bulbs: { BasePart }) + local rand = Random.new() + while #bulbs > 0 and bulbs[1]:IsDescendantOf(workspace) do + local killers = killerPositions() + for _, bulb in bulbs do + local near = math.huge + for _, pos in killers do + near = math.min(near, (pos - bulb.Position).Magnitude) + end + local drained = near < DRAIN_RADIUS + if drained ~= (bulb:GetAttribute("Drained") == true) then + bulb:SetAttribute("Drained", drained) + setLit(bulb, not drained) + elseif not drained and near < STUTTER_RADIUS and rand:NextNumber() < 0.3 then + task.spawn(blink, bulb, rand) + end + end + task.wait(0.25) + end +end + return { Init = function() if not RunService:IsClient() then return end + local bulbs = {} for _, bulb in workspace:WaitForChild("Map"):GetDescendants() do - if bulb:IsA("BasePart") and bulb:GetAttribute("Flicker") then - task.spawn(flicker, bulb) + if bulb:IsA("BasePart") and bulb.Name == "Bulb" then + table.insert(bulbs, bulb) + if bulb:GetAttribute("Flicker") then + task.spawn(flicker, bulb) + end end end + task.spawn(drainNearKiller, bulbs) end, }]] Behaviour.Parent = Root From 530dc4d0618298b647c71d324ee2496244cb1957 Mon Sep 17 00:00:00 2001 From: Norl4 Date: Sun, 1 Nov 2026 13:13:59 +0100 Subject: [PATCH 16/32] Recreate custom materials from the map script and keep the UI translation as a tool --- tools/maps/Instalacion.luau | 18 +++++++++++++ tools/ui_es.luau | 52 +++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 tools/ui_es.luau diff --git a/tools/maps/Instalacion.luau b/tools/maps/Instalacion.luau index 643ab779..03352abe 100644 --- a/tools/maps/Instalacion.luau +++ b/tools/maps/Instalacion.luau @@ -41,6 +41,24 @@ local Building = folder("Building", MapModel) local Yard = folder("Yard", MapModel) local Lamps = folder("Lamps", MapModel) +local MaterialService = game:GetService("MaterialService") +for _, m in { + { "AnomalisLabTile", "CeramicTiles", "rbxassetid://81243399359695", "rbxassetid://93392214421283", "rbxassetid://109790649107314", "rbxassetid://87299537724194", "Regular" }, + { "AnomalisRustMetal", "CorrodedMetal", "rbxassetid://92472211671271", "rbxassetid://103811817023703", "rbxassetid://81878616278695", "rbxassetid://114269752282446", "Organic" }, + { "AnomalisConcrete", "Concrete", "rbxassetid://72409752443113", "rbxassetid://97656916839878", "rbxassetid://122561031610057", "rbxassetid://91752265013131", "Organic" }, + { "AnomalisAsphalt", "Asphalt", "rbxassetid://115052350389321", "rbxassetid://126753607852078", "rbxassetid://117477470501608", "rbxassetid://139318763711632", "Organic" }, +} do + if not MaterialService:FindFirstChild(m[1], true) then + local variant = Instance.new("MaterialVariant") + variant.Name = m[1] + variant.BaseMaterial = (Enum.Material :: any)[m[2]] + variant.ColorMap, variant.NormalMap, variant.RoughnessMap, variant.MetalnessMap = m[3], m[4], m[5], m[6] + variant.StudsPerTile = 10 + variant.MaterialPattern = (Enum.MaterialPattern :: any)[m[7]] + variant.Parent = MaterialService + end +end + local VARIANTS = { [Enum.Material.Concrete] = "AnomalisConcrete", [Enum.Material.CorrodedMetal] = "AnomalisRustMetal", diff --git a/tools/ui_es.luau b/tools/ui_es.luau new file mode 100644 index 00000000..8d1e8e69 --- /dev/null +++ b/tools/ui_es.luau @@ -0,0 +1,52 @@ +local map = { + ["AFK mode is on. You won't be put in new matches while it's on. It can be turned off from the top of the settings menu."] = "El modo AFK está activado. No entrarás en partidas nuevas mientras siga así. Se quita arriba del todo en Ajustes.", + ["Achievement granted!"] = "¡Logro conseguido!", + ["Achievements"] = "Logros", + ["Achievements (0/0 Unlocked)"] = "Logros (0/0 desbloqueados)", + ["Cancel"] = "Cancelar", + ["Category"] = "Categoría", + ["Changelog"] = "Novedades", + ["Character"] = "Personaje", + ["Command Panel"] = "Panel de comandos", + ["Credits"] = "Créditos", + ["Current value: false"] = "Valor actual: false", + ["DEAD"] = "MUERTO", + ["Emote"] = "Gesto", + ["Emotes"] = "Gestos", + ["Equip"] = "Equipar", + ["Granted 0$ and 0 EXP for something."] = "Has ganado 0$ y 0 EXP por algo.", + ["Initializing"] = "Iniciando", + ["Killer Wins"] = "Victorias de asesino", + ["Killers"] = "Asesinos", + ["Money"] = "Dinero", + ["More Info"] = "Más información", + ["Owned"] = "Tuyo", + ["Purchase"] = "Comprar", + ["Reward: ???????????"] = "Recompensa: ???????????", + ["Round begins in..."] = "La ronda empieza en...", + ["Rules"] = "Normas", + ["Section"] = "Sección", + ["Skins"] = "Aspectos", + ["Survivor Wins"] = "Victorias de superviviente", + ["Survivors"] = "Supervivientes", + ["The killer is..."] = "El asesino es...", + ["Time Played"] = "Tiempo jugado", + ["Toggle"] = "Cambiar", + ["Unequip"] = "Quitar", + ["Using:"] = "Usando:", + ["X Skins"] = "X aspectos", + ["you won a match yayyy"] = "¡has ganado una partida!", +} +local n = 0 +for _, root in { game.StarterGui, game.ReplicatedStorage, game.StarterPlayer } do + for _, d in root:GetDescendants() do + if d:IsA("TextLabel") or d:IsA("TextButton") or d:IsA("TextBox") then + local es = map[(d.Text:gsub("%c", " "))] + if es then + d.Text = es + n += 1 + end + end + end +end +return n .. " textos traducidos" From 33a5c4b8512c402ed19d62bb431607a5c3a30d70 Mon Sep 17 00:00:00 2001 From: Norl4 Date: Sun, 1 Nov 2026 13:25:25 +0100 Subject: [PATCH 17/32] Let rounds start solo in Studio and stop the terror edge showing in the lobby --- .../Managers/TimeManager.luau | 2 +- .../Miscellaneous/TerrorPulse.luau | 17 +++++++++++------ tools/test.py | 6 +++--- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/ServerScriptService/Managers/TimeManager.luau b/src/ServerScriptService/Managers/TimeManager.luau index 91fcc2ce..44dc0918 100644 --- a/src/ServerScriptService/Managers/TimeManager.luau +++ b/src/ServerScriptService/Managers/TimeManager.luau @@ -8,7 +8,7 @@ local TimeManager = { --- If true, `TimeManager` will wait until `TimeManager.Config.MinimumPlayerAmount` players have loaded their game to tick down. WaitForEnoughPlayers = true, - MinimumPlayerAmount = 2, --change this to 1 to test rounds alone instead of using `WaitForEnoughPlayers` + MinimumPlayerAmount = if game:GetService("RunService"):IsStudio() then 1 else 2, }, --- Function that'll execute whenever the timer reaches 0. OnTimerEnd = function() end, --func diff --git a/src/StarterPlayerScripts/Miscellaneous/TerrorPulse.luau b/src/StarterPlayerScripts/Miscellaneous/TerrorPulse.luau index b3ff18cf..26fb275d 100644 --- a/src/StarterPlayerScripts/Miscellaneous/TerrorPulse.luau +++ b/src/StarterPlayerScripts/Miscellaneous/TerrorPulse.luau @@ -36,11 +36,12 @@ return { gui.IgnoreGuiInset = true gui.ResetOnSpawn = false gui.Parent = LocalPlayer:WaitForChild("PlayerGui") - local edge = Instance.new("CanvasGroup") + local edge = Instance.new("Frame") edge.Size = UDim2.fromScale(1, 1) edge.BackgroundTransparency = 1 - edge.GroupTransparency = 1 + edge.Visible = false edge.Parent = gui + local bands = {} for _, side in { { UDim2.fromScale(1, 0.22), UDim2.fromScale(0, 0), 90 }, { UDim2.fromScale(1, 0.22), UDim2.fromScale(0, 0.78), -90 }, @@ -48,6 +49,7 @@ return { { UDim2.fromScale(0.16, 1), UDim2.fromScale(0.84, 0), 180 }, } do local band = Instance.new("Frame") + table.insert(bands, band) band.Size = side[1] band.Position = side[2] band.BackgroundColor3 = EDGE_COLOR @@ -78,20 +80,23 @@ return { janitor:Add(RunService.PreRender:Connect(function() if role.Value ~= "Survivor" or RoundState.Value ~= "InRound" then - edge.GroupTransparency = 1 + edge.Visible = false return end local closeness = 1 - math.clamp(nearestKillerDistance(root) / TERROR_RADIUS, 0, 1) if closeness <= 0 then - edge.GroupTransparency = 1 + edge.Visible = false return end local calm = Utils.PlayerData.GetPlayerSetting(LocalPlayer, "Accessibility.EpilepsyMode") == true local beat = if calm then 0 else (math.sin(os.clock() * math.pi * 2 * (0.9 + 1.6 * closeness)) + 1) / 2 - edge.GroupTransparency = 1 - closeness * (0.25 + 0.2 * beat) + edge.Visible = true + for _, band in bands do + band.BackgroundTransparency = 1 - closeness * (0.25 + 0.2 * beat) + end end)) janitor:Add(function() - edge.GroupTransparency = 1 + edge.Visible = false end, true) end) end, diff --git a/tools/test.py b/tools/test.py index 03f94ce2..545fa17c 100644 --- a/tools/test.py +++ b/tools/test.py @@ -95,7 +95,7 @@ def play(start): def debug_round(as_killer): luau(f'''local m = game.ServerScriptService.Managers -m.TimeManager.Source = m.TimeManager.Source:gsub("LobbyTime = 45,", "LobbyTime = 5,"):gsub("MinimumPlayerAmount = 2,", "MinimumPlayerAmount = 1,") +m.TimeManager.Source = m.TimeManager.Source:gsub("LobbyTime = 45,", "LobbyTime = 5,") m.RoundManager.Source = m.RoundManager.Source:gsub("CanSpawnKiller = %a+,","CanSpawnKiller = {str(as_killer).lower()},") workspace:SetAttribute("KillersAllowed", {int(as_killer)})''', "Edit") play(True) @@ -172,10 +172,10 @@ def survivor_phase(): local role = Instance.new("StringValue") role.Name = "Role" role.Value = "Killer" role.Parent = fake fake.Parent = workspace.Players''') time.sleep(1) - near = luau('local e = game.Players.LocalPlayer.PlayerGui.TerrorPulse:FindFirstChildOfClass("CanvasGroup") local lo, hi = 1, 0 for _ = 1, 8 do lo = math.min(lo, e.GroupTransparency) hi = math.max(hi, e.GroupTransparency) task.wait(0.1) end return string.format("%.2f %.2f", lo, hi)', "Client") + near = luau('local e = game.Players.LocalPlayer.PlayerGui.TerrorPulse.Frame local lo, hi = 1, 0 for _ = 1, 8 do local t = if e.Visible then e.Frame.BackgroundTransparency else 1 lo = math.min(lo, t) hi = math.max(hi, t) task.wait(0.1) end return string.format("%.2f %.2f", lo, hi)', "Client") luau('workspace.Players.TestKiller:Destroy()') time.sleep(0.5) - far = luau('return string.format("%.2f", game.Players.LocalPlayer.PlayerGui.TerrorPulse:FindFirstChildOfClass("CanvasGroup").GroupTransparency)', "Client") + far = luau('local e = game.Players.LocalPlayer.PlayerGui.TerrorPulse.Frame return if e.Visible then string.format("%.2f", e.Frame.BackgroundTransparency) else "1.00"', "Client") lo, hi = map(float, near.split()) check("terror edge pulses near the killer", lo < 0.9 and hi - lo > 0.03 and far == "1.00", f"near {near}, gone {far}") errors = console_errors() From 044fc4a0c0fadd33097cf9c7ff2e4630c480dcdd Mon Sep 17 00:00:00 2001 From: Norl4 Date: Sun, 1 Nov 2026 15:37:25 +0100 Subject: [PATCH 18/32] Darken Instalacion with cold tube lights, glossy lab floors and a screen vignette --- .../Miscellaneous/Vignette.luau | 33 ++++++++++++++ tools/maps/Instalacion.luau | 45 ++++++++++--------- 2 files changed, 56 insertions(+), 22 deletions(-) create mode 100644 src/StarterPlayerScripts/Miscellaneous/Vignette.luau diff --git a/src/StarterPlayerScripts/Miscellaneous/Vignette.luau b/src/StarterPlayerScripts/Miscellaneous/Vignette.luau new file mode 100644 index 00000000..12250338 --- /dev/null +++ b/src/StarterPlayerScripts/Miscellaneous/Vignette.luau @@ -0,0 +1,33 @@ +--!strict +local Players = game:GetService("Players") + +local STRENGTH = 0.35 + +return { + Init = function(_) + local gui = Instance.new("ScreenGui") + gui.Name = "Vignette" + gui.IgnoreGuiInset = true + gui.ResetOnSpawn = false + gui.DisplayOrder = -1 + for _, side in { + { UDim2.fromScale(1, 0.3), UDim2.fromScale(0, 0), 90 }, + { UDim2.fromScale(1, 0.3), UDim2.fromScale(0, 0.7), -90 }, + { UDim2.fromScale(0.25, 1), UDim2.fromScale(0, 0), 0 }, + { UDim2.fromScale(0.25, 1), UDim2.fromScale(0.75, 0), 180 }, + } do + local band = Instance.new("Frame") + band.Size = side[1] + band.Position = side[2] + band.BackgroundColor3 = Color3.new(0, 0, 0) + band.BackgroundTransparency = 1 - STRENGTH + band.BorderSizePixel = 0 + band.Parent = gui + local fade = Instance.new("UIGradient") + fade.Transparency = NumberSequence.new(0, 1) + fade.Rotation = side[3] + fade.Parent = band + end + gui.Parent = Players.LocalPlayer:WaitForChild("PlayerGui") + end, +} diff --git a/tools/maps/Instalacion.luau b/tools/maps/Instalacion.luau index 03352abe..c86e0f24 100644 --- a/tools/maps/Instalacion.luau +++ b/tools/maps/Instalacion.luau @@ -124,7 +124,7 @@ local function room(container: Instance, name: string, center: Vector2, size: Ve wall(parent, Vector2.new(x1, z0), Vector2.new(x1, z1), height, gaps.east) part(parent, Vector3.new(size.X + 2, 1, size.Y + 2), Vector3.new(center.X, height + 0.5, center.Y), METAL, Enum.Material.CorrodedMetal, "Roof") local lab = container.Name == "LabComplex" - part(parent, Vector3.new(size.X, 0.2, size.Y), Vector3.new(center.X, 0.1, center.Y), if lab then Color3.fromRGB(96, 98, 94) else DARK_CONCRETE, if lab then Enum.Material.CeramicTiles else Enum.Material.Concrete, "Floor") + part(parent, Vector3.new(size.X, 0.2, size.Y), Vector3.new(center.X, 0.1, center.Y), if lab then Color3.fromRGB(96, 98, 94) else DARK_CONCRETE, if lab then Enum.Material.CeramicTiles else Enum.Material.Concrete, "Floor").Reflectance = if lab then 0.12 else 0 end local function fillLight(parent: Instance, pos: Vector3) @@ -150,14 +150,15 @@ local function lamp(parent: Instance, pos: Vector3, flicker: boolean, ceiling: b if not ceiling then part(model, Vector3.new(0.6, pos.Y, 0.6), Vector3.new(pos.X, pos.Y / 2, pos.Z), METAL, Enum.Material.Metal, "Pole") end - local head = part(model, Vector3.new(2.2, 0.5, 2.2), Vector3.new(pos.X, headY, pos.Z), Color3.fromRGB(255, 196, 140), Enum.Material.Neon, "Bulb") + local tint = if ceiling then Color3.fromRGB(150, 205, 255) else Color3.fromRGB(205, 215, 255) + local head = part(model, if ceiling then Vector3.new(6, 0.35, 0.8) else Vector3.new(2.2, 0.5, 2.2), Vector3.new(pos.X, headY, pos.Z), tint, Enum.Material.Neon, "Bulb") head.CastShadow = false local spot = Instance.new("SpotLight") spot.Face = Enum.NormalId.Bottom spot.Angle = 100 spot.Range = if ceiling then 26 else 42 spot.Brightness = if ceiling then 3.5 else 6 - spot.Color = Color3.fromRGB(255, 176, 110) + spot.Color = tint spot.Shadows = shadows == true spot.Parent = head local glow = Instance.new("PointLight") @@ -504,40 +505,40 @@ local LightingModule = Instance.new("ModuleScript") LightingModule.Name = "Lighting" LightingModule.Source = [[return { ClockTime = 0.2, - Brightness = 1.4, - Ambient = Color3.fromRGB(80, 82, 102), - OutdoorAmbient = Color3.fromRGB(100, 106, 136), - EnvironmentDiffuseScale = 0.35, - EnvironmentSpecularScale = 0.6, + Brightness = 0.8, + Ambient = Color3.fromRGB(14, 14, 24), + OutdoorAmbient = Color3.fromRGB(40, 44, 74), + EnvironmentDiffuseScale = 0.12, + EnvironmentSpecularScale = 1, GlobalShadows = true, - ShadowSoftness = 0.35, - ExposureCompensation = 0.7, + ShadowSoftness = 0.2, + ExposureCompensation = 0.1, ColorShift_Top = Color3.fromRGB(0, 0, 0), ColorShift_Bottom = Color3.fromRGB(0, 0, 0), }]] LightingModule.Parent = Root local atmosphere = Instance.new("Atmosphere") -atmosphere.Density = 0.36 -atmosphere.Offset = 0.05 -atmosphere.Color = Color3.fromRGB(96, 104, 124) -atmosphere.Decay = Color3.fromRGB(36, 38, 52) +atmosphere.Density = 0.42 +atmosphere.Offset = 0 +atmosphere.Color = Color3.fromRGB(34, 38, 66) +atmosphere.Decay = Color3.fromRGB(8, 8, 18) atmosphere.Glare = 0 -atmosphere.Haze = 1 +atmosphere.Haze = 2 atmosphere.Parent = LightingModule local grade = Instance.new("ColorCorrectionEffect") grade.Name = "Grade" -grade.Brightness = -0.02 -grade.Contrast = 0.2 -grade.Saturation = -0.35 -grade.TintColor = Color3.fromRGB(214, 224, 255) +grade.Brightness = -0.03 +grade.Contrast = 0.38 +grade.Saturation = 0.12 +grade.TintColor = Color3.fromRGB(206, 212, 255) grade.Parent = LightingModule local bloom = Instance.new("BloomEffect") -bloom.Intensity = 0.7 -bloom.Size = 28 -bloom.Threshold = 1.4 +bloom.Intensity = 1.2 +bloom.Size = 24 +bloom.Threshold = 0.9 bloom.Parent = LightingModule local sky = Instance.new("Sky") From d90bfdf230e2ddf12f98913c83f15b98df2e5759 Mon Sep 17 00:00:00 2001 From: Norl4 Date: Sun, 1 Nov 2026 16:23:19 +0100 Subject: [PATCH 19/32] Grow Instalacion into a fenced compound with watchtowers, forest and the LAB-07 facade with its research dome --- tools/maps/Instalacion.luau | 367 +++++++++++++++++++++++++++++++++--- 1 file changed, 345 insertions(+), 22 deletions(-) diff --git a/tools/maps/Instalacion.luau b/tools/maps/Instalacion.luau index c86e0f24..f67e6bd5 100644 --- a/tools/maps/Instalacion.luau +++ b/tools/maps/Instalacion.luau @@ -184,14 +184,205 @@ local function lamp(parent: Instance, pos: Vector3, flicker: boolean, ceiling: b end end -local HALF = Vector2.new(160, 140) -part(MapModel, Vector3.new(HALF.X * 2 + 20, 2, HALF.Y * 2 + 20), Vector3.new(0, -1, 0), Color3.fromRGB(74, 76, 82), Enum.Material.Asphalt, "Ground") - -wall(Yard, Vector2.new(-HALF.X, -HALF.Y), Vector2.new(HALF.X, -HALF.Y), 20, nil, DARK_CONCRETE) -wall(Yard, Vector2.new(-HALF.X, HALF.Y), Vector2.new(HALF.X, HALF.Y), 20, { { 0, 18 } }, DARK_CONCRETE) -wall(Yard, Vector2.new(-HALF.X, -HALF.Y), Vector2.new(-HALF.X, HALF.Y), 20, nil, DARK_CONCRETE) -wall(Yard, Vector2.new(HALF.X, -HALF.Y), Vector2.new(HALF.X, HALF.Y), 20, nil, DARK_CONCRETE) -part(Yard, Vector3.new(18, 12, 0.6), Vector3.new(0, 6, HALF.Y), METAL, Enum.Material.DiamondPlate, "MainGate") +local HALF = Vector2.new(230, 210) +local CORE = Vector2.new(160, 140) +local BEACON_RED = Color3.fromRGB(255, 40, 30) +part(MapModel, Vector3.new(CORE.X * 2 + 20, 2, CORE.Y * 2 + 20), Vector3.new(0, -1, 0), Color3.fromRGB(74, 76, 82), Enum.Material.Asphalt, "Ground") +part(MapModel, Vector3.new(HALF.X * 2 + 500, 2, HALF.Y * 2 + 500), Vector3.new(0, -1.15, 0), Color3.fromRGB(64, 72, 52), Enum.Material.Grass, "Grass") +part(MapModel, Vector3.new(20, 2, 300), Vector3.new(0, -0.98, CORE.Y + 150), Color3.fromRGB(58, 60, 66), Enum.Material.Asphalt, "Road") +for z = CORE.Y + 6, CORE.Y + 296, 10 do + local stripe = part(MapModel, Vector3.new(0.5, 0.05, 5), Vector3.new(0, 0.05, z), Color3.fromRGB(220, 180, 50), Enum.Material.SmoothPlastic, "RoadLine") + stripe.CanCollide = false + stripe.CastShadow = false +end +for _, z in { CORE.Y + 20, CORE.Y + 45, HALF.Y + 30, HALF.Y + 70 } do + for _, x in { -12, 12 } do + local post = part(MapModel, Vector3.new(0.6, 14, 0.6), Vector3.new(x, 7, z), METAL, Enum.Material.Metal, "StreetLamp") + local head = part(MapModel, Vector3.new(2.4, 0.5, 1.2), Vector3.new(x - math.sign(x) * 1.2, 14, z), Color3.fromRGB(255, 205, 140), Enum.Material.Neon, "StreetLampHead") + head.CastShadow = false + local warm = Instance.new("SpotLight") + warm.Face = Enum.NormalId.Bottom + warm.Angle = 110 + warm.Range = 26 + warm.Brightness = 3 + warm.Color = Color3.fromRGB(255, 190, 120) + warm.Parent = head + post.CastShadow = true + end +end + +local function beacon(parent: Instance, pos: Vector3) + local bulb = part(parent, Vector3.new(0.9, 0.9, 0.9), pos, BEACON_RED, Enum.Material.Neon, "Beacon") + bulb.Shape = Enum.PartType.Ball + bulb.CastShadow = false + local glow = Instance.new("PointLight") + glow.Color = BEACON_RED + glow.Range = 12 + glow.Brightness = 2.6 + glow.Parent = bulb +end + +local Fence = folder("Fence", Yard) +local function fenceSide(alongX: boolean, fixed: number, from: number, to: number, gap: number?) + local function at(t: number, y: number): Vector3 + return if alongX then Vector3.new(t, y, fixed) else Vector3.new(fixed, y, t) + end + local runs = if gap then { { from, -gap / 2 }, { gap / 2, to } } else { { from, to } } + for _, run in runs do + local len = run[2] - run[1] + local mid = (run[1] + run[2]) / 2 + local function span(height: number, y: number, thick: number, name: string, material: Enum.Material): Part + return part(Fence, if alongX then Vector3.new(len, height, thick) else Vector3.new(thick, height, len), at(mid, y), METAL, material, name) + end + local mesh = span(10, 5.4, 0.12, "Mesh", Enum.Material.DiamondPlate) + mesh.Color = Color3.fromRGB(130, 136, 148) + mesh.Transparency = 0.62 + mesh.CastShadow = false + span(0.35, 10.6, 0.35, "Rail", Enum.Material.Metal) + span(0.25, 0.6, 0.25, "Rail", Enum.Material.Metal) + local wire = span(0.9, 11.6, 0.9, "BarbedWire", Enum.Material.Metal) + wire.Shape = Enum.PartType.Cylinder + wire.CFrame *= if alongX then CFrame.identity else CFrame.Angles(0, math.rad(90), 0) + wire.Color = Color3.fromRGB(150, 155, 165) + wire.Transparency = 0.2 + local posts = math.ceil(len / 12) + for i = 0, posts do + local t = run[1] + len * i / posts + part(Fence, Vector3.new(0.6, 12, 0.6), at(t, 6), METAL, Enum.Material.Metal, "Post") + if i % 5 == 0 then + beacon(Fence, at(t, 12.5)) + end + end + end +end +fenceSide(true, -HALF.Y, -HALF.X, HALF.X) +fenceSide(true, HALF.Y, -HALF.X, HALF.X, 26) +fenceSide(false, -HALF.X, -HALF.Y, HALF.Y) +fenceSide(false, HALF.X, -HALF.Y, HALF.Y) + +local Gate = folder("Gate", Yard) +for _, x in { -14, 14 } do + part(Gate, Vector3.new(4, 16, 4), Vector3.new(x, 8, HALF.Y), DARK_CONCRETE, Enum.Material.Concrete, "Pillar") + beacon(Gate, Vector3.new(x, 16.6, HALF.Y)) + lamp(Gate, Vector3.new(x, 0, HALF.Y - 3), false) +end +for _, x in { -6, 6 } do + local leaf = part(Gate, Vector3.new(12, 12, 0.3), Vector3.new(x, 6.2, HALF.Y), METAL, Enum.Material.DiamondPlate, "GateLeaf") + leaf.Transparency = 0.55 + part(Gate, Vector3.new(12, 0.5, 0.5), Vector3.new(x, 12.2, HALF.Y), RUST, Enum.Material.CorrodedMetal, "GateFrame") + part(Gate, Vector3.new(12, 0.5, 0.5), Vector3.new(x, 0.4, HALF.Y), RUST, Enum.Material.CorrodedMetal, "GateFrame") + part(Gate, Vector3.new(0.5, 12, 0.5), Vector3.new(x + (if x < 0 then -5.8 else 5.8), 6.2, HALF.Y), RUST, Enum.Material.CorrodedMetal, "GateFrame") +end +part(Gate, Vector3.new(32, 4, 1), Vector3.new(0, 18, HALF.Y), DARK_CONCRETE, Enum.Material.Concrete, "Lintel") + +local function watchtower(c: Vector2, faceTo: Vector2) + local m = Instance.new("Model") + m.Name = "Watchtower" + m.Parent = Yard + for _, o in { Vector2.new(-4, -4), Vector2.new(4, -4), Vector2.new(-4, 4), Vector2.new(4, 4) } do + part(m, Vector3.new(0.9, 24, 0.9), Vector3.new(c.X + o.X, 12, c.Y + o.Y), WOOD, Enum.Material.Wood, "Leg") + end + for y = 7, 19, 6 do + part(m, Vector3.new(9, 0.4, 0.4), Vector3.new(c.X, y, c.Y - 4), WOOD, Enum.Material.Wood, "Brace") + part(m, Vector3.new(9, 0.4, 0.4), Vector3.new(c.X, y, c.Y + 4), WOOD, Enum.Material.Wood, "Brace") + part(m, Vector3.new(0.4, 0.4, 9), Vector3.new(c.X - 4, y, c.Y), WOOD, Enum.Material.Wood, "Brace") + part(m, Vector3.new(0.4, 0.4, 9), Vector3.new(c.X + 4, y, c.Y), WOOD, Enum.Material.Wood, "Brace") + end + part(m, Vector3.new(11, 0.6, 11), Vector3.new(c.X, 24.3, c.Y), WOOD, Enum.Material.WoodPlanks, "Deck") + for _, s in { { Vector3.new(10, 2.4, 0.4), Vector2.new(0, -5) }, { Vector3.new(10, 2.4, 0.4), Vector2.new(0, 5) }, { Vector3.new(0.4, 2.4, 10), Vector2.new(-5, 0) }, { Vector3.new(0.4, 2.4, 10), Vector2.new(5, 0) } } do + part(m, s[1], Vector3.new(c.X + s[2].X, 25.8, c.Y + s[2].Y), WOOD, Enum.Material.WoodPlanks, "Cabin") + local window = part(m, s[1] + Vector3.new(0, 0.2, 0), Vector3.new(c.X + s[2].X, 28.3, c.Y + s[2].Y), Color3.fromRGB(255, 196, 120), Enum.Material.Glass, "Window") + window.Transparency = 0.45 + end + part(m, Vector3.new(12.5, 0.8, 12.5), Vector3.new(c.X, 30, c.Y), METAL, Enum.Material.CorrodedMetal, "Roof") + local warm = Instance.new("PointLight") + warm.Color = Color3.fromRGB(255, 180, 100) + warm.Range = 14 + warm.Brightness = 1.5 + warm.Parent = part(m, Vector3.new(0.6, 0.6, 0.6), Vector3.new(c.X, 28.5, c.Y), Color3.fromRGB(255, 200, 130), Enum.Material.Neon, "CabinLamp") + beacon(m, Vector3.new(c.X, 30.9, c.Y)) + local head = part(m, Vector3.new(1.4, 1.4, 1.8), Vector3.new(c.X, 27.5, c.Y), METAL, Enum.Material.Metal, "Searchlight") + head.CFrame = CFrame.lookAt(head.Position, ORIGIN + Vector3.new(faceTo.X, 0, faceTo.Y)) + local beam = Instance.new("SpotLight") + beam.Face = Enum.NormalId.Front + beam.Angle = 28 + beam.Range = 60 + beam.Brightness = 3 + beam.Color = Color3.fromRGB(220, 230, 255) + beam.Parent = head + local from, to = Instance.new("Attachment"), Instance.new("Attachment") + to.Position = Vector3.new(0, 0, -55) + from.Parent, to.Parent = head, head + local cone = Instance.new("Beam") + cone.Attachment0, cone.Attachment1 = from, to + cone.Width0, cone.Width1 = 1.4, 26 + cone.FaceCamera = true + cone.LightEmission = 1 + cone.Color = ColorSequence.new(Color3.fromRGB(210, 225, 255)) + cone.Transparency = NumberSequence.new(0.8, 1) + cone.Segments = 1 + cone:AddTag("Detail") + cone.Parent = head + part(m, Vector3.new(2, 24, 0.3), Vector3.new(c.X, 12, c.Y + 4.8), RUST, Enum.Material.CorrodedMetal, "Ladder") +end +for _, s in { -1, 1 } do + watchtower(Vector2.new(s * (HALF.X - 12), HALF.Y - 12), Vector2.new(s * (HALF.X - 50), HALF.Y - 50)) + watchtower(Vector2.new(s * (HALF.X - 12), -HALF.Y + 12), Vector2.new(s * (HALF.X - 50), -HALF.Y + 50)) +end + +local Outside = folder("Outside", MapModel) +local function pine(parent: Instance, base: Vector3, h: number, solid: boolean) + local trunk = part(parent, Vector3.new(h * 0.3, h * 0.05, h * 0.05), base + Vector3.new(0, h * 0.15, 0), Color3.fromRGB(46, 34, 26), Enum.Material.Wood, "Trunk") + trunk.Shape = Enum.PartType.Cylinder + trunk.CFrame *= CFrame.Angles(0, 0, math.rad(90)) + trunk.CanCollide = solid + for tier = 0, 1 do + local w, th = h * (0.5 - tier * 0.17), h * (0.5 - tier * 0.08) + local center = ORIGIN + base + Vector3.new(0, h * (0.2 + tier * 0.3) + th / 2, 0) + for q = 0, 3 do + local leaf = Instance.new("CornerWedgePart") + leaf.Name = "Needles" + leaf.Anchored = true + leaf.CanCollide = false + leaf.CastShadow = solid + leaf.Size = Vector3.new(w / 2, th, w / 2) + leaf.CFrame = CFrame.new(center) * CFrame.Angles(0, math.rad(90 * q), 0) * CFrame.new(-w / 4, 0, w / 4) + leaf.Color = if tier == 0 then Color3.fromRGB(40, 58, 46) else Color3.fromRGB(58, 80, 64) + leaf.Material = Enum.Material.Grass + leaf.Parent = parent + end + end +end +local forestRand = Random.new(7) +for x = -HALF.X - 80, HALF.X + 80, 22 do + for z = -HALF.Y - 80, HALF.Y + 80, 22 do + local p = Vector2.new(x + forestRand:NextNumber(-8, 8), z + forestRand:NextNumber(-8, 8)) + local inside = math.abs(p.X) < HALF.X + 8 and math.abs(p.Y) < HALF.Y + 8 + local onRoad = math.abs(p.X) < 18 and p.Y > HALF.Y + if not inside and not onRoad then + local tree = Instance.new("Model") + tree.Name = "Pine" + tree.Parent = Outside + pine(tree, Vector3.new(p.X, 0, p.Y), forestRand:NextNumber(26, 44), false) + if math.abs(p.X) > HALF.X + 40 or math.abs(p.Y) > HALF.Y + 40 then + tree:AddTag("Detail") + for _, d in tree:GetChildren() do + d:AddTag("Detail") + end + end + end + end +end +for i = 0, 17 do + local angle = math.rad(i * 20 + forestRand:NextNumber(-6, 6)) + local peak = forestRand:NextNumber(90, 170) + local at = Vector3.new(math.cos(angle) * 540, -peak * 0.2, math.sin(angle) * 500) + local ridge = part(Outside, Vector3.new(peak, peak, 200), at, Color3.fromRGB(22, 25, 34), Enum.Material.Slate, "Mountain") + ridge.CFrame = CFrame.lookAt(ORIGIN + at, ORIGIN + Vector3.new(0, at.Y, 0)) * CFrame.Angles(0, 0, math.rad(45)) + ridge.CanCollide = false + ridge.CastShadow = false +end + for _, side in { Vector3.new(0, 45, -HALF.Y - 1), Vector3.new(0, 45, HALF.Y + 1), Vector3.new(-HALF.X - 1, 45, 0), Vector3.new(HALF.X + 1, 45, 0) } do local barrier = part(Yard, if side.X == 0 then Vector3.new(HALF.X * 2 + 4, 50, 1) else Vector3.new(1, 50, HALF.Y * 2 + 4), side, CONCRETE, Enum.Material.SmoothPlastic, "Barrier") barrier.Transparency = 1 @@ -257,9 +448,9 @@ coreLight.Parent = ring local chimney = part(Reactor, Vector3.new(60, 9, 9), Vector3.new(100, 30, -131), DARK_CONCRETE, Enum.Material.Concrete, "Chimney") chimney.Shape = Enum.PartType.Cylinder chimney.CFrame = CFrame.new(ORIGIN + Vector3.new(100, 30, -131)) * CFrame.Angles(0, 0, math.rad(90)) -local beacon = part(Reactor, Vector3.new(1.5, 1.5, 1.5), Vector3.new(100, 61, -131), Color3.fromRGB(255, 40, 30), Enum.Material.Neon, "Beacon") -beacon.CastShadow = false -beacon:SetAttribute("Flicker", true) +local chimneyBeacon = part(Reactor, Vector3.new(1.5, 1.5, 1.5), Vector3.new(100, 61, -131), Color3.fromRGB(255, 40, 30), Enum.Material.Neon, "Beacon") +chimneyBeacon.CastShadow = false +chimneyBeacon:SetAttribute("Flicker", true) part(Reactor, Vector3.new(20, 1, 14), Vector3.new(50, 10.5, -80), METAL, Enum.Material.CorrodedMetal, "WalkwayRoof") for _, p in { Vector3.new(80, 21.2, -70), Vector3.new(130, 21.2, -70), Vector3.new(80, 21.2, -105), Vector3.new(130, 21.2, -105) } do lamp(Reactor, p, p.X == 130 and p.Z == -105, true) @@ -428,7 +619,8 @@ local function sign(text: string, pos: Vector3, face: Enum.NormalId, color: Colo label.Parent = gui end local WHITE, RED, AMBER = Color3.fromRGB(220, 224, 230), Color3.fromRGB(210, 40, 36), Color3.fromRGB(230, 170, 40) -sign("ANOMALIS CENTRO DE INVESTIGACIÓN 7", Vector3.new(0, 16, 138.6), Enum.NormalId.Front, WHITE) +sign("ANOMALIS LAB-07", Vector3.new(0, 18, HALF.Y - 0.6), Enum.NormalId.Front, WHITE) +sign("ANOMALIS SECTOR A PUERTA", Vector3.new(0, 18, HALF.Y + 0.6), Enum.NormalId.Back, RED) sign("SECTOR C LABORATORIOS", Vector3.new(-35, 12, -39.1), Enum.NormalId.Back, WHITE) sign("CONTENCIÓN SOLO AUTORIZADOS", Vector3.new(-35, 10, -84.1), Enum.NormalId.Back, RED) sign("REACTOR 02 RADIACIÓN", Vector3.new(105, 16, -49.1), Enum.NormalId.Back, AMBER) @@ -450,6 +642,136 @@ for _, p in { Vector3.new(-70, 0, 20), Vector3.new(15, 0, 50), Vector3.new(-15, end end +local Landmarks = folder("Landmarks", MapModel) +local PURPLE = Color3.fromRGB(170, 70, 230) +local function glowLight(parent: BasePart, kind: string, color: Color3, range: number, brightness: number) + local light = Instance.new(kind) :: any + light.Color = color + light.Range = range + light.Brightness = brightness + light.Parent = parent + return light +end +local function bigText(parent: BasePart, text: string, face: Enum.NormalId, color: Color3, font: Enum.Font) + local gui = Instance.new("SurfaceGui") + gui.Face = face + gui.SizingMode = Enum.SurfaceGuiSizingMode.PixelsPerStud + gui.PixelsPerStud = 30 + gui.LightInfluence = 0.4 + gui.Parent = parent + local label = Instance.new("TextLabel") + label.Size = UDim2.fromScale(1, 1) + label.BackgroundTransparency = 1 + label.Text = text + label.TextScaled = true + label.Font = font + label.TextColor3 = color + label.Parent = gui +end + +local upper = part(Landmarks, Vector3.new(80, 14, 40), Vector3.new(-35, 24, -62), DARK_CONCRETE, Enum.Material.Concrete, "UpperFloor") +local WINDOW_TINTS = { Color3.fromRGB(120, 170, 255), Color3.fromRGB(255, 190, 120), nil, Color3.fromRGB(120, 170, 255) } +for i = 0, 6 do + local x = -70 + i * 6.5 + local tint = WINDOW_TINTS[i % 4 + 1] + local pane = part(Landmarks, Vector3.new(4.4, 3.4, 0.3), Vector3.new(x, 24, -41.7), tint or Color3.fromRGB(16, 18, 24), if tint then Enum.Material.Neon else Enum.Material.Glass, "Window") + pane.Transparency = if tint then 0.3 else 0.1 + pane.CastShadow = false + part(Landmarks, Vector3.new(5.2, 4.2, 0.2), Vector3.new(x, 24, -41.85), Color3.fromRGB(30, 32, 38), Enum.Material.Metal, "WindowFrame") + part(Landmarks, Vector3.new(0.25, 3.4, 0.4), Vector3.new(x, 24, -41.6), Color3.fromRGB(30, 32, 38), Enum.Material.Metal, "Mullion") +end +local plate = part(Landmarks, Vector3.new(26, 9, 0.4), Vector3.new(-8, 24, -41.6), Color3.fromRGB(26, 28, 34), Enum.Material.Metal, "LabSign") +bigText(plate, "LAB-07", Enum.NormalId.Back, Color3.fromRGB(235, 240, 250), Enum.Font.GothamBlack) +glowLight(plate, "SurfaceLight", Color3.fromRGB(200, 215, 255), 10, 1.5).Face = Enum.NormalId.Back +for _, edge in { { Vector3.new(26.6, 0.35, 0.3), Vector3.new(-8, 28.65, -41.9) }, { Vector3.new(26.6, 0.35, 0.3), Vector3.new(-8, 19.35, -41.9) }, { Vector3.new(0.35, 9.6, 0.3), Vector3.new(-21.15, 24, -41.9) }, { Vector3.new(0.35, 9.6, 0.3), Vector3.new(5.15, 24, -41.9) } } do + part(Landmarks, edge[1], edge[2], Color3.fromRGB(150, 190, 255), Enum.Material.Neon, "SignBorder").CastShadow = false +end +local hazard = part(Landmarks, Vector3.new(8, 13, 0.4), Vector3.new(-26, 24, -41.6), Color3.fromRGB(170, 22, 24), Enum.Material.SmoothPlastic, "HazardBanner") +bigText(hazard, "☣", Enum.NormalId.Back, Color3.fromRGB(16, 4, 4), Enum.Font.GothamBlack) +glowLight(hazard, "SurfaceLight", Color3.fromRGB(255, 40, 40), 16, 1.6).Face = Enum.NormalId.Back +local washer = part(Landmarks, Vector3.new(1.5, 0.6, 1.5), Vector3.new(-26, 0.4, -36), Color3.fromRGB(255, 50, 40), Enum.Material.Neon, "FacadeWash") +washer.CFrame = CFrame.lookAt(washer.Position, ORIGIN + Vector3.new(-26, 26, -42)) +glowLight(washer, "SpotLight", Color3.fromRGB(255, 40, 30), 34, 4).Angle = 40 +for _, x in { -45, -25 } do + local sconce = part(Landmarks, Vector3.new(1.6, 0.8, 1), Vector3.new(x, 13, -39.2), Color3.fromRGB(255, 200, 130), Enum.Material.Neon, "Sconce") + sconce.CastShadow = false + local down = glowLight(sconce, "SpotLight", Color3.fromRGB(255, 185, 110), 22, 3) + down.Face = Enum.NormalId.Bottom + down.Angle = 90 +end +for _, edge in { { Vector3.new(81, 1.6, 0.8), Vector3.new(-35, 31.8, -42.4) }, { Vector3.new(81, 1.6, 0.8), Vector3.new(-35, 31.8, -81.6) }, { Vector3.new(0.8, 1.6, 40), Vector3.new(-75.1, 31.8, -62) }, { Vector3.new(0.8, 1.6, 40), Vector3.new(5.1, 31.8, -62) } } do + part(Landmarks, edge[1], edge[2], DARK_CONCRETE, Enum.Material.Concrete, "Parapet") +end + +local DOME_GLOW = Color3.fromRGB(205, 90, 255) +local dome = part(Landmarks, Vector3.new(34, 34, 34), Vector3.new(-35, 31, -62), DOME_GLOW, Enum.Material.Glass, "ResearchDome") +dome.Shape = Enum.PartType.Ball +dome.Transparency = 0.58 +dome.CastShadow = false +local core = part(Landmarks, Vector3.new(6, 6, 6), Vector3.new(-35, 34, -62), DOME_GLOW, Enum.Material.Neon, "DomeCore") +core.Shape = Enum.PartType.Ball +glowLight(core, "PointLight", DOME_GLOW, 60, 5) +glowLight(part(Landmarks, Vector3.new(0.4, 0.4, 0.4), Vector3.new(-35, 50, -62), DOME_GLOW, Enum.Material.Neon, "DomeTop"), "PointLight", DOME_GLOW, 40, 3) +for _, angle in { 0, 45, 90, 135 } do + local rib = part(Landmarks, Vector3.new(0.7, 34.6, 34.6), Vector3.new(-35, 31, -62), Color3.fromRGB(255, 190, 255), Enum.Material.Neon, "DomeRib") + rib.Shape = Enum.PartType.Cylinder + rib.CFrame = CFrame.new(ORIGIN + Vector3.new(-35, 31, -62)) * CFrame.Angles(0, math.rad(angle), 0) + rib.Transparency = 0.7 + rib.CastShadow = false +end +local baseRing = part(Landmarks, Vector3.new(0.8, 35, 35), Vector3.new(-35, 31.4, -62), DOME_GLOW, Enum.Material.Neon, "DomeRing") +baseRing.Shape = Enum.PartType.Cylinder +baseRing.CFrame = CFrame.new(ORIGIN + Vector3.new(-35, 31.4, -62)) * CFrame.Angles(0, 0, math.rad(90)) +baseRing.CastShadow = false + +local mast = part(Landmarks, Vector3.new(1.2, 6, 1.2), Vector3.new(10, 20, -105), METAL, Enum.Material.Metal, "DishMast") +local dish = part(Landmarks, Vector3.new(1, 16, 16), Vector3.new(10, 26, -105), Color3.fromRGB(150, 152, 158), Enum.Material.Metal, "SatelliteDish") +dish.Shape = Enum.PartType.Cylinder +dish.CFrame = CFrame.new(ORIGIN + Vector3.new(10, 26, -105)) * CFrame.Angles(0, math.rad(-30), math.rad(-35)) +mast.CastShadow = true + +local tower = Instance.new("Model") +tower.Name = "CommsTower" +tower.Parent = Landmarks +local TOWER_AT, TOWER_H = Vector2.new(-185, -150), 60 +for _, o in { Vector2.new(-5, -5), Vector2.new(5, -5), Vector2.new(-5, 5), Vector2.new(5, 5) } do + local leg = part(tower, Vector3.new(0.8, TOWER_H, 0.8), Vector3.new(TOWER_AT.X + o.X * 0.6, TOWER_H / 2, TOWER_AT.Y + o.Y * 0.6), RUST, Enum.Material.CorrodedMetal, "Leg") + leg.CFrame = CFrame.lookAt(ORIGIN + Vector3.new(TOWER_AT.X + o.X, 0, TOWER_AT.Y + o.Y), ORIGIN + Vector3.new(TOWER_AT.X + o.X * 0.2, TOWER_H, TOWER_AT.Y + o.Y * 0.2)) * CFrame.new(0, 0, -TOWER_H / 2) * CFrame.Angles(math.rad(90), 0, 0) +end +for y = 8, TOWER_H - 6, 10 do + local w = 10 - y / TOWER_H * 8 + for _, s in { { Vector3.new(w, 0.4, 0.4), Vector2.new(0, -w / 2) }, { Vector3.new(w, 0.4, 0.4), Vector2.new(0, w / 2) }, { Vector3.new(0.4, 0.4, w), Vector2.new(-w / 2, 0) }, { Vector3.new(0.4, 0.4, w), Vector2.new(w / 2, 0) } } do + part(tower, s[1], Vector3.new(TOWER_AT.X + s[2].X, y, TOWER_AT.Y + s[2].Y), RUST, Enum.Material.CorrodedMetal, "Brace") + end + if y % 20 == 8 then + beacon(tower, Vector3.new(TOWER_AT.X + w / 2, y + 0.8, TOWER_AT.Y)) + end +end +part(tower, Vector3.new(0.4, 10, 0.4), Vector3.new(TOWER_AT.X, TOWER_H + 5, TOWER_AT.Y), METAL, Enum.Material.Metal, "Antenna") +beacon(tower, Vector3.new(TOWER_AT.X, TOWER_H + 10.5, TOWER_AT.Y)) + +local HELIPAD_AT = Vector3.new(195, 0, -40) +local pad = part(Landmarks, Vector3.new(0.4, 36, 36), HELIPAD_AT + Vector3.new(0, 0.1, 0), Color3.fromRGB(52, 56, 62), Enum.Material.Concrete, "Helipad") +pad.Shape = Enum.PartType.Cylinder +pad.CFrame *= CFrame.Angles(0, 0, math.rad(90)) +local ring = part(Landmarks, Vector3.new(0.42, 30, 30), HELIPAD_AT + Vector3.new(0, 0.11, 0), Color3.fromRGB(200, 200, 190), Enum.Material.SmoothPlastic, "HelipadRing") +ring.Shape = Enum.PartType.Cylinder +ring.CFrame *= CFrame.Angles(0, 0, math.rad(90)) +local inner = part(Landmarks, Vector3.new(0.44, 27, 27), HELIPAD_AT + Vector3.new(0, 0.12, 0), Color3.fromRGB(52, 56, 62), Enum.Material.Concrete, "HelipadInner") +inner.Shape = Enum.PartType.Cylinder +inner.CFrame *= CFrame.Angles(0, 0, math.rad(90)) +for _, s in { { Vector3.new(1.6, 0.1, 11), Vector3.new(-4, 0, 0) }, { Vector3.new(1.6, 0.1, 11), Vector3.new(4, 0, 0) }, { Vector3.new(8, 0.1, 1.6), Vector3.zero } } do + part(Landmarks, s[1], HELIPAD_AT + s[2] + Vector3.new(0, 0.36, 0), Color3.fromRGB(210, 210, 200), Enum.Material.SmoothPlastic, "HelipadMark").CanCollide = false +end +for i = 0, 11 do + local a = math.rad(i * 30) + local stud = part(Landmarks, Vector3.new(0.6, 0.4, 0.6), HELIPAD_AT + Vector3.new(math.cos(a) * 17, 0.4, math.sin(a) * 17), Color3.fromRGB(120, 200, 255), Enum.Material.Neon, "PadLight") + stud.CastShadow = false + if i % 3 == 0 then + glowLight(stud, "PointLight", Color3.fromRGB(120, 200, 255), 8, 1) + end +end + local Fog = folder("GroundFog", Details) for _, c in { Vector2.new(-90, 30), Vector2.new(0, 50), Vector2.new(90, 40), Vector2.new(-30, 110), Vector2.new(110, -15), Vector2.new(-120, -30) } do local area = detail(part(Fog, Vector3.new(60, 1, 50), Vector3.new(c.X, 0.6, c.Y), CONCRETE, Enum.Material.SmoothPlastic, "FogArea")) @@ -504,11 +826,12 @@ Config.Parent = Root local LightingModule = Instance.new("ModuleScript") LightingModule.Name = "Lighting" LightingModule.Source = [[return { - ClockTime = 0.2, - Brightness = 0.8, - Ambient = Color3.fromRGB(14, 14, 24), - OutdoorAmbient = Color3.fromRGB(40, 44, 74), - EnvironmentDiffuseScale = 0.12, + ClockTime = 20, + GeographicLatitude = 55, + Brightness = 2, + Ambient = Color3.fromRGB(22, 22, 34), + OutdoorAmbient = Color3.fromRGB(105, 115, 155), + EnvironmentDiffuseScale = 0.4, EnvironmentSpecularScale = 1, GlobalShadows = true, ShadowSoftness = 0.2, @@ -519,9 +842,9 @@ LightingModule.Source = [[return { LightingModule.Parent = Root local atmosphere = Instance.new("Atmosphere") -atmosphere.Density = 0.42 -atmosphere.Offset = 0 -atmosphere.Color = Color3.fromRGB(34, 38, 66) +atmosphere.Density = 0.32 +atmosphere.Offset = 0.1 +atmosphere.Color = Color3.fromRGB(84, 92, 124) atmosphere.Decay = Color3.fromRGB(8, 8, 18) atmosphere.Glare = 0 atmosphere.Haze = 2 @@ -529,8 +852,8 @@ atmosphere.Parent = LightingModule local grade = Instance.new("ColorCorrectionEffect") grade.Name = "Grade" -grade.Brightness = -0.03 -grade.Contrast = 0.38 +grade.Brightness = 0 +grade.Contrast = 0.08 grade.Saturation = 0.12 grade.TintColor = Color3.fromRGB(206, 212, 255) grade.Parent = LightingModule From 95508c18210eb35a8835fba129cd0a3ef789889c Mon Sep 17 00:00:00 2001 From: Norl4 Date: Sun, 1 Nov 2026 16:26:56 +0100 Subject: [PATCH 20/32] Add Instalacion experiment capsules, red reactor core, catwalk with two ramps and a reception desk --- tools/maps/Instalacion.luau | 92 +++++++++++++++++++++++++++++++++++-- 1 file changed, 88 insertions(+), 4 deletions(-) diff --git a/tools/maps/Instalacion.luau b/tools/maps/Instalacion.luau index f67e6bd5..497115f4 100644 --- a/tools/maps/Instalacion.luau +++ b/tools/maps/Instalacion.luau @@ -433,14 +433,14 @@ room(Reactor, "Shell", Vector2.new(105, -85), Vector2.new(90, 70), 22, { local core = part(Reactor, Vector3.new(18, 14, 14), Vector3.new(105, 9, -92), METAL, Enum.Material.DiamondPlate, "ReactorCore") core.Shape = Enum.PartType.Cylinder core.CFrame = CFrame.new(ORIGIN + Vector3.new(105, 9, -92)) * CFrame.Angles(0, 0, math.rad(90)) -local ring = part(Reactor, Vector3.new(1, 15, 15), Vector3.new(105, 12, -92), Color3.fromRGB(70, 170, 255), Enum.Material.Neon, "CoreRing") +local ring = part(Reactor, Vector3.new(1, 15, 15), Vector3.new(105, 12, -92), Color3.fromRGB(255, 45, 35), Enum.Material.Neon, "CoreRing") ring.Shape = Enum.PartType.Cylinder ring.CFrame = CFrame.new(ORIGIN + Vector3.new(105, 12, -92)) * CFrame.Angles(0, 0, math.rad(90)) ring.CanCollide = false local coreLight = Instance.new("PointLight") coreLight.Color = ring.Color coreLight.Range = 45 -coreLight.Brightness = 2.2 +coreLight.Brightness = 3.2 for _, p in { Vector3.new(75, 10, -95), Vector3.new(135, 10, -95), Vector3.new(105, 10, -60) } do fillLight(Reactor, p) end @@ -710,8 +710,8 @@ dome.Transparency = 0.58 dome.CastShadow = false local core = part(Landmarks, Vector3.new(6, 6, 6), Vector3.new(-35, 34, -62), DOME_GLOW, Enum.Material.Neon, "DomeCore") core.Shape = Enum.PartType.Ball -glowLight(core, "PointLight", DOME_GLOW, 60, 5) -glowLight(part(Landmarks, Vector3.new(0.4, 0.4, 0.4), Vector3.new(-35, 50, -62), DOME_GLOW, Enum.Material.Neon, "DomeTop"), "PointLight", DOME_GLOW, 40, 3) +glowLight(core, "PointLight", DOME_GLOW, 60, 5).Shadows = true +glowLight(part(Landmarks, Vector3.new(0.4, 0.4, 0.4), Vector3.new(-35, 50, -62), DOME_GLOW, Enum.Material.Neon, "DomeTop"), "PointLight", DOME_GLOW, 40, 3).Shadows = true for _, angle in { 0, 45, 90, 135 } do local rib = part(Landmarks, Vector3.new(0.7, 34.6, 34.6), Vector3.new(-35, 31, -62), Color3.fromRGB(255, 190, 255), Enum.Material.Neon, "DomeRib") rib.Shape = Enum.PartType.Cylinder @@ -772,6 +772,90 @@ for i = 0, 11 do end end +local Interiors = folder("Interiors", MapModel) +local LIQUID = Color3.fromRGB(150, 60, 220) +local function capsule(x: number, z: number, broken: boolean) + local m = Instance.new("Model") + m.Name = "ContainmentCapsule" + m.Parent = Interiors + local function disc(size: Vector3, y: number, color: Color3, material: Enum.Material, name: string): Part + local d = part(m, size, Vector3.new(x, y, z), color, material, name) + d.Shape = Enum.PartType.Cylinder + d.CFrame *= CFrame.Angles(0, 0, math.rad(90)) + return d + end + disc(Vector3.new(1.4, 8, 8), 0.7, METAL, Enum.Material.DiamondPlate, "Base") + disc(Vector3.new(1.2, 8, 8), 13.4, METAL, Enum.Material.Metal, "Cap") + disc(Vector3.new(0.3, 7.6, 7.6), 1.5, LIQUID, Enum.Material.Neon, "BaseGlow").CastShadow = false + if broken then + for i = 0, 4 do + local a = math.rad(i * 72 + 20) + local shard = part(m, Vector3.new(1.6, 0.15, 1), Vector3.new(x + math.cos(a) * (5 + i % 2 * 2), 0.1, z + math.sin(a) * (5 + i % 2 * 2)), Color3.fromRGB(200, 170, 230), Enum.Material.Glass, "Shard") + shard.Transparency = 0.4 + shard.CanCollide = false + shard.CFrame *= CFrame.Angles(0, a, 0) + end + local spill = disc(Vector3.new(0.05, 12, 14), 0.06, LIQUID, Enum.Material.Glass, "Spill") + spill.Transparency = 0.35 + spill.CanCollide = false + glowLight(disc(Vector3.new(0.3, 2, 2), 12.5, LIQUID, Enum.Material.Neon, "Spark"), "PointLight", LIQUID, 18, 1.5) + else + local liquid = disc(Vector3.new(11, 6.4, 6.4), 7, LIQUID, Enum.Material.Neon, "Liquid") + liquid.Transparency = 0.55 + liquid.CanCollide = false + liquid.CastShadow = false + local glass = disc(Vector3.new(11.6, 7.2, 7.2), 7.2, Color3.fromRGB(210, 190, 240), Enum.Material.Glass, "Glass") + glass.Transparency = 0.6 + part(m, Vector3.new(1.4, 3, 0.8), Vector3.new(x, 7.5, z), Color3.fromRGB(14, 10, 18), Enum.Material.SmoothPlastic, "Specimen").CanCollide = false + local head = part(m, Vector3.new(1.2, 1.2, 1.2), Vector3.new(x, 9.7, z), Color3.fromRGB(14, 10, 18), Enum.Material.SmoothPlastic, "SpecimenHead") + head.Shape = Enum.PartType.Ball + head.CanCollide = false + glowLight(liquid, "PointLight", LIQUID, 16, 1.4) + end +end +capsule(0, -113, false) +capsule(13, -113, false) +capsule(26, -113, true) +capsule(36, -96, false) +local table_ = part(Interiors, Vector3.new(11, 0.6, 5), Vector3.new(10, 3.2, -98), Color3.fromRGB(150, 155, 160), Enum.Material.Metal, "ExperimentTable") +for _, o in { Vector3.new(-5, 0, -2), Vector3.new(5, 0, -2), Vector3.new(-5, 0, 2), Vector3.new(5, 0, 2) } do + part(Interiors, Vector3.new(0.4, 3, 0.4), Vector3.new(10, 1.5, -98) + o, METAL, Enum.Material.Metal, "TableLeg") +end +local stain = part(Interiors, Vector3.new(4, 0.05, 2.2), Vector3.new(11, 3.53, -98), Color3.fromRGB(90, 10, 14), Enum.Material.SmoothPlastic, "Stain") +stain.CanCollide = false +table_.CastShadow = true + +local DESK = Color3.fromRGB(46, 48, 54) +part(Interiors, Vector3.new(18, 3.6, 3), Vector3.new(-45, 1.8, -62), DESK, Enum.Material.Metal, "ReceptionDesk") +part(Interiors, Vector3.new(18.6, 0.3, 3.6), Vector3.new(-45, 3.75, -62), Color3.fromRGB(120, 124, 130), Enum.Material.Marble, "DeskTop") +for _, x in { -51, -45, -39 } do + local screen = part(Interiors, Vector3.new(2.4, 1.6, 0.2), Vector3.new(x, 4.9, -62.6), Color3.fromRGB(60, 150, 255), Enum.Material.Neon, "Monitor") + screen.CastShadow = false + glowLight(screen, "SurfaceLight", Color3.fromRGB(80, 160, 255), 8, 1).Face = Enum.NormalId.Back +end +for _, x in { -68, -22 } do + part(Interiors, Vector3.new(8, 1.6, 2.4), Vector3.new(x, 0.8, -46), Color3.fromRGB(70, 40, 40), Enum.Material.Fabric, "WaitingBench") +end + +local Catwalk = folder("Catwalk", Interiors) +local WALK_Y, WALK_Z = 10, -114 +part(Catwalk, Vector3.new(82, 0.6, 6), Vector3.new(105, WALK_Y, WALK_Z), METAL, Enum.Material.DiamondPlate, "Walkway") +part(Catwalk, Vector3.new(70, 3, 0.3), Vector3.new(105, WALK_Y + 1.8, WALK_Z + 3), RUST, Enum.Material.CorrodedMetal, "Railing").Transparency = 0.3 +for x = 66, 144, 13 do + part(Catwalk, Vector3.new(0.6, WALK_Y, 0.6), Vector3.new(x, WALK_Y / 2, WALK_Z + 2.7), RUST, Enum.Material.CorrodedMetal, "Strut") +end +local RAMP_RUN = 20 +for _, x in { 67, 143 } do + local ramp = part(Catwalk, Vector3.new(6, 0.6, math.sqrt(RAMP_RUN ^ 2 + WALK_Y ^ 2)), Vector3.new(x, WALK_Y / 2, WALK_Z + 3 + RAMP_RUN / 2), METAL, Enum.Material.DiamondPlate, "Ramp") + ramp.CFrame *= CFrame.Angles(math.atan2(WALK_Y, RAMP_RUN), 0, 0) +end +local hazardRing = part(Interiors, Vector3.new(0.05, 26, 26), Vector3.new(105, 0.04, -92), Color3.fromRGB(255, 40, 30), Enum.Material.Neon, "HazardRing") +hazardRing.Shape = Enum.PartType.Cylinder +hazardRing.CFrame *= CFrame.Angles(0, 0, math.rad(90)) +hazardRing.Transparency = 0.75 +hazardRing.CanCollide = false +hazardRing.CastShadow = false + local Fog = folder("GroundFog", Details) for _, c in { Vector2.new(-90, 30), Vector2.new(0, 50), Vector2.new(90, 40), Vector2.new(-30, 110), Vector2.new(110, -15), Vector2.new(-120, -30) } do local area = detail(part(Fog, Vector3.new(60, 1, 50), Vector3.new(c.X, 0.6, c.Y), CONCRETE, Enum.Material.SmoothPlastic, "FogArea")) From c032a329bfd226378310aae39cdf3d0397df8658 Mon Sep 17 00:00:00 2001 From: Norl4 Date: Sun, 1 Nov 2026 17:10:07 +0100 Subject: [PATCH 21/32] Wait for the killer intro to finish before the battery slashes the reactive crate --- tools/test.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/test.py b/tools/test.py index 545fa17c..d87ecc87 100644 --- a/tools/test.py +++ b/tools/test.py @@ -190,6 +190,7 @@ def slash(): def killer_phase(): if not debug_round(True): return + wait_for('return tostring(#game.Players.LocalPlayer.PlayerGui.KillerIntros:GetChildren() == 0)', "true", "Client", 15) luau('''local panel = workspace.Map:FindFirstChild("Generators", true):GetChildren()[1].Panel local base = panel.Position + panel.CFrame.LookVector * 14 local char = game.Players:GetPlayers()[1].Character From 41bdd8ac51ce8a58f734a8019caab70899e25955 Mon Sep 17 00:00:00 2001 From: Norl4 Date: Tue, 3 Nov 2026 20:34:12 +0100 Subject: [PATCH 22/32] Switch the UI to the bloodless engraved textures --- .../Characters/Killers/NullexVoyd.luau | 4 +-- .../Characters/Survivors/Commander.luau | 2 +- .../Classes/Ability/Slash.luau | 2 +- .../Classes/Ability/init.luau | 2 +- .../DamageVignette/DamageVignette.model.json | 2 +- .../RemoteHandler/ShowJokeSettingImage.luau | 8 ++--- .../UI/AchievementsMenu.luau | 2 +- src/StarterPlayerScripts/UI/Credits.luau | 2 +- src/StarterPlayerScripts/UI/SideBar.luau | 36 +++++++++---------- tools/texturas_v3.luau | 12 +++++++ 10 files changed, 42 insertions(+), 30 deletions(-) create mode 100644 tools/texturas_v3.luau diff --git a/src/ReplicatedStorage/Characters/Killers/NullexVoyd.luau b/src/ReplicatedStorage/Characters/Killers/NullexVoyd.luau index 45248dac..f6021b9d 100644 --- a/src/ReplicatedStorage/Characters/Killers/NullexVoyd.luau +++ b/src/ReplicatedStorage/Characters/Killers/NullexVoyd.luau @@ -155,7 +155,7 @@ local NullexVoyd: Types.Killer = Character.CreateKiller({ Config = { Name = "Nullex Voyd", Quote = "El orden fue la primera mentira.", - Render = "rbxassetid://139883756028491", + Render = "rbxassetid://100323356058904", Price = 1308, Origin = { @@ -269,7 +269,7 @@ NullexVoyd.Config.Description = { { Type = "Text", Text = NullexVoydNameLabel.." lanza un tajo básico que hace "..tostring(NullexVoyd.GameplayConfig.Abilities.Slash.Damage).." de daño.", - Image = "rbxassetid://9759886280", + Image = "rbxassetid://133006324317341", }, { Type = "Header", diff --git a/src/ReplicatedStorage/Characters/Survivors/Commander.luau b/src/ReplicatedStorage/Characters/Survivors/Commander.luau index 1bc3b117..62ac7316 100644 --- a/src/ReplicatedStorage/Characters/Survivors/Commander.luau +++ b/src/ReplicatedStorage/Characters/Survivors/Commander.luau @@ -175,7 +175,7 @@ local Commander: Types.Survivor = Character.CreateSurvivor({ Config = { Name = "Commander", Quote = "*pyro noises*", - Render = "rbxassetid://108440668883002", + Render = "rbxassetid://80481019529425", Origin = { TooltipText = "¡Viene del Proyecto Terminus!", diff --git a/src/ReplicatedStorage/Classes/Ability/Slash.luau b/src/ReplicatedStorage/Classes/Ability/Slash.luau index 95cbd637..3df6d643 100644 --- a/src/ReplicatedStorage/Classes/Ability/Slash.luau +++ b/src/ReplicatedStorage/Classes/Ability/Slash.luau @@ -45,7 +45,7 @@ function SlashModule.New(Props: Types.Ability?): Types.Ability Cooldown = 2, Duration = 0.4, Damage = 20, - RenderImage = "rbxassetid://11218451110", + RenderImage = "rbxassetid://114455340704913", UseSound = "rbxassetid://12222200", UseAnimation = "rbxassetid://133504250023139", UICorner = true, diff --git a/src/ReplicatedStorage/Classes/Ability/init.luau b/src/ReplicatedStorage/Classes/Ability/init.luau index 0d2c5539..c863eba2 100644 --- a/src/ReplicatedStorage/Classes/Ability/init.luau +++ b/src/ReplicatedStorage/Classes/Ability/init.luau @@ -14,7 +14,7 @@ AbilityType.__index = AbilityType function AbilityType.GetDefaultSettings(): Types.Ability return setmetatable({ Name = "Ability", - RenderImage = "rbxassetid://9759886280", + RenderImage = "rbxassetid://133006324317341", UICorner = true, Passive = false, diff --git a/src/StarterPlayerScripts/Miscellaneous/CharacterExclusive/DamageVignette/DamageVignette.model.json b/src/StarterPlayerScripts/Miscellaneous/CharacterExclusive/DamageVignette/DamageVignette.model.json index 305ef876..6088c635 100644 --- a/src/StarterPlayerScripts/Miscellaneous/CharacterExclusive/DamageVignette/DamageVignette.model.json +++ b/src/StarterPlayerScripts/Miscellaneous/CharacterExclusive/DamageVignette/DamageVignette.model.json @@ -3,7 +3,7 @@ "properties": { "AnchorPoint": [0.5, 0.5], "BackgroundTransparency": 1, - "Image": "rbxassetid://106241917931649", + "Image": "rbxassetid://74959684527828", "ImageTransparency": 0.999, "Size": {"UDim2": [[1, 0], [1, 0]]} } diff --git a/src/StarterPlayerScripts/RemoteHandler/ShowJokeSettingImage.luau b/src/StarterPlayerScripts/RemoteHandler/ShowJokeSettingImage.luau index 5d89cdb2..b1d87e35 100644 --- a/src/StarterPlayerScripts/RemoteHandler/ShowJokeSettingImage.luau +++ b/src/StarterPlayerScripts/RemoteHandler/ShowJokeSettingImage.luau @@ -12,18 +12,18 @@ local LocalPlayer = Players.LocalPlayer local Images = { Victim = { Killer = { - "rbxassetid://125952385779817", + "rbxassetid://119277567001272", }, Survivor = { - "rbxassetid://125952385779817", + "rbxassetid://119277567001272", }, }, Source = { Killer = { - "rbxassetid://122348374983136", + "rbxassetid://90001887337594", }, Survivor = { - "rbxassetid://122348374983136", + "rbxassetid://90001887337594", }, }, } diff --git a/src/StarterPlayerScripts/UI/AchievementsMenu.luau b/src/StarterPlayerScripts/UI/AchievementsMenu.luau index 0ba9aca8..4c18083a 100644 --- a/src/StarterPlayerScripts/UI/AchievementsMenu.luau +++ b/src/StarterPlayerScripts/UI/AchievementsMenu.luau @@ -26,7 +26,7 @@ local AchievementsMenu = { Hidden = { Title = "?????", Description = "????????????????????", - Icon = "rbxassetid://15117261700", + Icon = "rbxassetid://108919288576085", }, }, } diff --git a/src/StarterPlayerScripts/UI/Credits.luau b/src/StarterPlayerScripts/UI/Credits.luau index 2949dd5c..fa127de5 100644 --- a/src/StarterPlayerScripts/UI/Credits.luau +++ b/src/StarterPlayerScripts/UI/Credits.luau @@ -24,7 +24,7 @@ local Credits: {{ Name: string, Credits: {Credit} }} = { Name = "Dysymmetrical", Credits = { { - Render = "rbxassetid://112215528635901", + Render = "rbxassetid://132085691188764", DisplayName = "Dyscarn", Username = "@LrgeAlvaro8882", Description = "Hizo el motor de este juego: Dysymmetrical.", diff --git a/src/StarterPlayerScripts/UI/SideBar.luau b/src/StarterPlayerScripts/UI/SideBar.luau index 33f69074..157e4cd5 100644 --- a/src/StarterPlayerScripts/UI/SideBar.luau +++ b/src/StarterPlayerScripts/UI/SideBar.luau @@ -118,81 +118,81 @@ function SideBar:Init() local Buttons: {[string]: Button} = { Shop = { - Render = "rbxassetid://140391842164127", + Render = "rbxassetid://92125848707503", Type = "Button", - InvertedRender = "rbxassetid://106935872381429", + InvertedRender = "rbxassetid://88124154416159", MenuModule = PlayerUIModules.Shop, LayoutOrder = 0, CloseOnOtherOpen = true, CloseOthersOnOpen = true, }, Inventory = { - Render = "rbxassetid://124486892591913", + Render = "rbxassetid://133773587567288", Type = "Button", - InvertedRender = "rbxassetid://97795375278271", + InvertedRender = "rbxassetid://135032912792327", MenuModule = PlayerUIModules.Inventory, LayoutOrder = 1, CloseOnOtherOpen = true, CloseOthersOnOpen = true, }, Achievements = { - Render = "rbxassetid://113899511427023", + Render = "rbxassetid://116072020411630", Type = "Button", - InvertedRender = "rbxassetid://70778277265275", + InvertedRender = "rbxassetid://132952592975074", MenuModule = PlayerUIModules.AchievementsMenu, LayoutOrder = 2, CloseOnOtherOpen = true, CloseOthersOnOpen = true, }, Stats = { - Render = "rbxassetid://130488549713780", + Render = "rbxassetid://136234713338136", Type = "Button", - InvertedRender = "rbxassetid://72911514685580", + InvertedRender = "rbxassetid://126222746123066", MenuModule = PlayerUIModules.StatsMenu, LayoutOrder = 3, CloseOnOtherOpen = true, CloseOthersOnOpen = true, }, Spectate = { - Render = "rbxassetid://89862280432932", + Render = "rbxassetid://98665327829798", Type = "Button", - InvertedRender = "rbxassetid://134398825115015", + InvertedRender = "rbxassetid://99614397357911", MenuModule = PlayerUIModules.Spectate, LayoutOrder = 4, CloseOnOtherOpen = false, CloseOthersOnOpen = false, }, Settings = { - Render = "rbxassetid://95524701319726", + Render = "rbxassetid://115123853519355", Type = "Button", - InvertedRender = "rbxassetid://113951492478970", + InvertedRender = "rbxassetid://112043618362631", MenuModule = PlayerUIModules.Settings, LayoutOrder = 5, CloseOnOtherOpen = true, CloseOthersOnOpen = true, }, Credits = { - Render = "rbxassetid://82434587437333", + Render = "rbxassetid://102863849269730", Type = "Button", - InvertedRender = "rbxassetid://113410904986274", + InvertedRender = "rbxassetid://82305913763140", MenuModule = PlayerUIModules.Credits, LayoutOrder = 6, CloseOnOtherOpen = true, CloseOthersOnOpen = true, }, Rules = { - Render = "rbxassetid://83147016587963", + Render = "rbxassetid://88200919300377", Type = "SmallButton", - InvertedRender = "rbxassetid://90249013398230", + InvertedRender = "rbxassetid://105621437001449", MenuModule = PlayerUIModules.RulesMenu, LayoutOrder = 7, CloseOnOtherOpen = true, CloseOthersOnOpen = true, }, Changelog = { - Render = "rbxassetid://82434587437333", + Render = "rbxassetid://102863849269730", Type = "SmallButton", - InvertedRender = "rbxassetid://113410904986274", + InvertedRender = "rbxassetid://82305913763140", MenuModule = PlayerUIModules.Changelog, LayoutOrder = 8, CloseOnOtherOpen = true, diff --git a/tools/texturas_v3.luau b/tools/texturas_v3.luau new file mode 100644 index 00000000..e44a6472 --- /dev/null +++ b/tools/texturas_v3.luau @@ -0,0 +1,12 @@ +local map = {["139883756028491"]="100323356058904",["100921752780361"]="100323356058904",["106241917931649"]="74959684527828",["117010715844501"]="74959684527828",["18831151594"]="128181696744363",["119610261645437"]="128181696744363",["113899511427023"]="116072020411630",["83687584504780"]="116072020411630",["76678266629187"]="139825927020130",["117274474885324"]="139825927020130",["97795375278271"]="135032912792327",["72011786600695"]="135032912792327",["11218451110"]="114455340704913",["130375414145807"]="114455340704913",["81173680651802"]="71513422232998",["117189079397470"]="71513422232998",["82434587437333"]="102863849269730",["82314892877351"]="102863849269730",["113951492478970"]="112043618362631",["81982328714347"]="112043618362631",["106935872381429"]="88124154416159",["76990192953218"]="88124154416159",["15117261700"]="108919288576085",["114628211808952"]="108919288576085",["9759886280"]="133006324317341",["110592908311818"]="133006324317341",["112215528635901"]="132085691188764",["90992446500322"]="132085691188764",["122348374983136"]="90001887337594",["85345158778229"]="92745076441825",["122529795299668"]="92745076441825",["70778277265275"]="132952592975074",["84394094794003"]="132952592975074",["95524701319726"]="115123853519355",["121233760883636"]="115123853519355",["72911514685580"]="126222746123066",["120821340324633"]="126222746123066",["90249013398230"]="105621437001449",["96047450858874"]="105621437001449",["78551669134263"]="89672958353922",["118220108698495"]="89672958353922",["134398825115015"]="99614397357911",["111996292073761"]="99614397357911",["113410904986274"]="82305913763140",["130246897364015"]="82305913763140",["125952385779817"]="119277567001272",["140391842164127"]="92125848707503",["74804042483805"]="92125848707503",["130488549713780"]="136234713338136",["99694070436802"]="136234713338136",["19000746528"]="122817981158609",["138393274749188"]="122817981158609",["122785356471162"]="71154417238430",["106776058700110"]="71154417238430",["108440668883002"]="80481019529425",["86062239822810"]="80481019529425",["124486892591913"]="133773587567288",["108434145732421"]="133773587567288",["83147016587963"]="88200919300377",["119743866893925"]="88200919300377",["89862280432932"]="98665327829798",["91180813843795"]="98665327829798"} +local images = 0 +for _, inst in game:GetDescendants() do + if inst:IsA("ImageLabel") or inst:IsA("ImageButton") then + local id = inst.Image:match("(%d+)") + if id and map[id] then + inst.Image = "rbxassetid://" .. map[id] + images += 1 + end + end +end +return images .. " images switched to the v3 (sin sangre) textures" From 758bb47ac4d2edb02cf843a8d4af0616ed1b9787 Mon Sep 17 00:00:00 2001 From: Norl4 Date: Tue, 3 Nov 2026 20:44:20 +0100 Subject: [PATCH 23/32] Animate the side bar and menus with breathing icons, hand-inked ripples and an ink veil reveal --- src/StarterPlayerScripts/UI/SideBar.luau | 101 ++++++++++++++++++++++- tools/tinta_ui.py | 51 ++++++++++++ 2 files changed, 150 insertions(+), 2 deletions(-) create mode 100644 tools/tinta_ui.py diff --git a/src/StarterPlayerScripts/UI/SideBar.luau b/src/StarterPlayerScripts/UI/SideBar.luau index 157e4cd5..b9f3626c 100644 --- a/src/StarterPlayerScripts/UI/SideBar.luau +++ b/src/StarterPlayerScripts/UI/SideBar.luau @@ -25,6 +25,90 @@ type Button = { } local InvertedInfo = TweenInfo.new(0.25, Enum.EasingStyle.Sine, Enum.EasingDirection.InOut) +local InkVeilImage = "rbxassetid://94299335585756" +local InkRingImage = "rbxassetid://106770110603452" + +local function CalmMotion(): boolean + return Utils.PlayerData.GetPlayerSetting(LocalPlayer, "Customization.ScreenShakeEnabled") == false +end + +local function InkRipple(Host: GuiObject, Grow: number, Duration: number) + if CalmMotion() then + return + end + for Index, Delay in {0, 0.07} do + local Ring = Instance.new("ImageLabel") + Ring.Image = InkRingImage + Ring.AnchorPoint = Vector2.new(0.5, 0.5) + Ring.Position = UDim2.fromScale(0.5, 0.5) + Ring.Size = UDim2.fromScale(0.95, 0.95) + Ring.Rotation = math.random(0, 360) + Ring.BackgroundTransparency = 1 + Ring.ImageTransparency = 0.1 + Index * 0.25 + Ring.ZIndex = 6 + Ring.Parent = Host + local Info = TweenInfo.new(Duration, Enum.EasingStyle.Quint, Enum.EasingDirection.Out, 0, false, Delay) + TweenService:Create(Ring, Info, {Size = UDim2.fromScale(Grow, Grow), Rotation = Ring.Rotation + 40, ImageTransparency = 1}):Play() + task.delay(Duration + Delay, Ring.Destroy, Ring) + end +end + +local function InkReveal(Menu: GuiObject?, Closing: boolean) + if not Menu or not Menu:IsA("GuiObject") or CalmMotion() then + return + end + local Clip = Instance.new("Frame") + Clip.Name = "InkVeil" + Clip.Size = UDim2.fromScale(1, 1) + Clip.BackgroundTransparency = 1 + Clip.ClipsDescendants = true + Clip.ZIndex = 50 + local Veil = Instance.new("ImageLabel") + Veil.Image = InkVeilImage + Veil.BackgroundTransparency = 1 + Veil.ScaleType = Enum.ScaleType.Stretch + Veil.Size = UDim2.fromScale(2, 1) + Veil.Position = UDim2.fromScale(if Closing then 0 else -1, 0) + Veil.ZIndex = 50 + Veil.Parent = Clip + Clip.Parent = Menu + local Duration = if Closing then 0.3 else 0.5 + local Style = if Closing then Enum.EasingDirection.In else Enum.EasingDirection.Out + TweenService:Create(Veil, TweenInfo.new(Duration, Enum.EasingStyle.Sine, Style), {Position = UDim2.fromScale(if Closing then -1 else 0, 0)}):Play() + task.delay(Duration, Clip.Destroy, Clip) + if not Closing and not Menu:FindFirstChildOfClass("UIScale") then + local Settle = Instance.new("UIScale") + Settle.Scale = 0.96 + Settle.Parent = Menu + TweenService:Create(Settle, TweenInfo.new(0.55, Enum.EasingStyle.Back, Enum.EasingDirection.Out), {Scale = 1}):Play() + task.delay(0.55, Settle.Destroy, Settle) + end +end + +local function Sway(BarButton: GuiObject) + for _, Name in {"Button", "Inverted"} do + local Image = BarButton:FindFirstChild(Name) :: GuiObject + local Lean = TweenService:Create(Image, TweenInfo.new(0.08, Enum.EasingStyle.Quad, Enum.EasingDirection.Out), {Rotation = -5}) + Lean.Completed:Once(function(State: Enum.PlaybackState) + if State ~= Enum.PlaybackState.Completed then + return + end + TweenService:Create(Image, TweenInfo.new(0.4, Enum.EasingStyle.Back, Enum.EasingDirection.Out), {Rotation = 0}):Play() + end) + Lean:Play() + end +end + +local function Breathe(Buttons: {[GuiObject]: number}) + local Clock = 0 + game:GetService("RunService").RenderStepped:Connect(function(dt: number) + Clock += dt + local Calm = CalmMotion() + for Scale, Phase in Buttons do + Scale.Scale = if Calm then 1 else 1 + 0.025 * math.sin(Clock * 1.6 + Phase) + end + end) +end local UI local BarButtonPrefabs = { @@ -89,10 +173,12 @@ function SideBar.OpenMenu(Menu) if Menu.Button then TweenInverted(Menu.Button, true) end + InkReveal(Menu.UIInstance, false) if Menu.CloseOthersOnOpen then SideBar.CloseAll(Menu) end else + InkReveal(Menu.UIInstance, true) Menu.Close(SideBar.ToggleDuration) if Menu.Button then TweenInverted(Menu.Button, false) @@ -116,6 +202,9 @@ function SideBar:Init() local PlayerUIModules = LocalPlayer.PlayerScripts.UI + local Breathing = {} + Breathe(Breathing) + local Buttons: {[string]: Button} = { Shop = { Render = "rbxassetid://92125848707503", @@ -213,8 +302,10 @@ function SideBar:Init() Menu.CloseOnOtherOpen = button.CloseOnOtherOpen Menu.CloseOthersOnOpen = button.CloseOthersOnOpen BarButton.Button.MouseButton1Click:Connect(function() + InkRipple(BarButton, 1.75, 0.6) SideBar.OpenMenu(Menu) end) + Breathing[Instance.new("UIScale", BarButton)] = button.LayoutOrder * 0.7 Menu.Button = BarButton --TODO: replace these variables only if the size is the hidden one to handle resolution changes @@ -225,7 +316,11 @@ function SideBar:Init() BarButton.Parent = ButtonContainer local OriginalYLabelBound = BarButton.Title.Holder.Label.AbsoluteSize.Y - BarButton.MouseEnter:Connect(function() + local function Hover() + if not CalmMotion() then + InkRipple(BarButton, 1.45, 0.45) + Sway(BarButton) + end Sounds.PlaySound(Sounds.CommonlyUsedSounds.ButtonHover, {Volume = 0.2}) TweenService:Create(BarButton, TweenInfo.new(0.3, Enum.EasingStyle.Sine, Enum.EasingDirection.Out), {Size = HoverSize}):Play() -- Why does `TextLabel.AutomaticSize` not fucking work with `TextLabel.TextScaled`? No idea! I'm mentally insane! :D @@ -236,7 +331,9 @@ function SideBar:Init() TweenService:Create(BarButton.Title.Holder, TweenInfo.new(0.3, Enum.EasingStyle.Sine, Enum.EasingDirection.Out), { Position = UDim2.new(0.15, Size.X + 56, 0.5, 0) }):Play() - end) + end + BarButton.MouseEnter:Connect(Hover) + BarButton.Button.SelectionGained:Connect(Hover) BarButton.MouseLeave:Connect(function() Sounds.PlaySound(Sounds.CommonlyUsedSounds.ButtonHoverStop, {Volume = 0.2}) TweenService:Create(BarButton, TweenInfo.new(0.3, Enum.EasingStyle.Sine, Enum.EasingDirection.Out), {Size = OriginalSize}):Play() diff --git a/tools/tinta_ui.py b/tools/tinta_ui.py new file mode 100644 index 00000000..0b09f451 --- /dev/null +++ b/tools/tinta_ui.py @@ -0,0 +1,51 @@ +import numpy as np +from PIL import Image, ImageFilter +rng = np.random.default_rng(7) + +def smooth_noise(n, scale, amp): + g = np.exp(-0.5 * (np.arange(-3 * scale, 3 * scale + 1) / scale) ** 2) + k = np.convolve(rng.normal(0, 1, n + 6 * scale), g / g.sum(), "valid")[:n] + return k / k.std() * amp + +W, H = 2048, 512 +x = np.arange(W)[None, :].repeat(H, 0).astype(float) +y = np.arange(H)[:, None].repeat(W, 1).astype(float) +edge = 1024 + smooth_noise(H, 64, 46)[:, None] + smooth_noise(H, 16, 5)[:, None] +d = x - edge +alpha = np.clip(d / 60 + 0.5, 0, 1) +dry = rng.random((H, W)) < np.clip(0.55 - d / 70, 0, 0.55) +alpha[dry & (d < 30)] *= 0.25 +hatch = (np.sin((x + y * 0.9) * 0.22) > 0.86) & (rng.random((H, W)) > 0.35) +ink = np.stack([np.full((H, W), 18.0), np.full((H, W), 17.0), np.full((H, W), 15.0)], -1) +ivory = np.array([236, 228, 210], float) +mix = np.where(hatch, 0.09, 0.0)[..., None] +lead = (np.abs(d - 4) < 2.2) & (rng.random((H, W)) > 0.25) +mix = np.where(lead[..., None], 0.75, mix) +rgb = ink * (1 - mix) + ivory * mix +a = np.where(lead, np.maximum(alpha, 0.85), alpha) +img = Image.fromarray(np.dstack([rgb, a * 255]).clip(0, 255).astype(np.uint8), "RGBA").filter(ImageFilter.GaussianBlur(0.6)) +img.save("UI texturas/efectos/velo_tinta.png") + +S = 512 +yy, xx = np.mgrid[0:S, 0:S].astype(float) - S / 2 +r = np.hypot(xx, yy) +th = np.arctan2(yy, xx) +canvas = np.zeros((S, S)) +for k in range(3): + phase = rng.uniform(0, 6.28) + rad = 196 + k * 7 + 8 * np.sin(th * 3 + phase) + 4 * np.sin(th * 7 + phase * 2) + width = 5 + 4 * (0.5 + 0.5 * np.sin(th * 2 + phase)) + stroke = np.clip(1 - np.abs(r - rad) / width, 0, 1) + gaps = 0.5 + 0.5 * np.sin(th * 11 + phase * 3) + canvas = np.maximum(canvas, stroke * (0.55 + 0.45 * gaps) * (1 - 0.35 * k)) +canvas *= (rng.random((S, S)) > 0.18) +ticks = (np.abs(r - 228) < 14) & (np.abs(np.sin(th * 36)) < 0.08) +pass +ring = np.zeros((S, S, 4), np.uint8) +ring[..., :3] = [236, 228, 210] +ring[..., 3] = (canvas.clip(0, 1) * 255).astype(np.uint8) +Image.fromarray(ring, "RGBA").filter(ImageFilter.GaussianBlur(0.7)).save("UI texturas/efectos/anillo_tinta.png") +prev = Image.new("RGBA", (W // 2 + S // 2, H // 2), (60, 70, 60, 255)) +prev.alpha_composite(img.resize((W // 2, H // 2))) +prev.alpha_composite(Image.open("UI texturas/efectos/anillo_tinta.png").resize((S // 2, S // 2)), (W // 2, 0)) +prev.save("UI texturas/efectos/vista_previa.png") From 6a10dc403d0b6288a99312f432b88c754f84b5f2 Mon Sep 17 00:00:00 2001 From: Norl4 Date: Tue, 3 Nov 2026 20:53:25 +0100 Subject: [PATCH 24/32] Animate the round HUD with ink: staggered ability entrance, ink-disc cooldowns, ready pop and a ticking clock in the last ten seconds --- .../Classes/Ability/AbilityGUI.luau | 9 +- src/ReplicatedStorage/Modules/InkFX.luau | 119 ++++++++++++++++++ src/StarterPlayerScripts/UI/SideBar.luau | 73 ++--------- src/StarterPlayerScripts/UI/Time.luau | 13 +- tools/tinta_ui.py | 13 ++ 5 files changed, 159 insertions(+), 68 deletions(-) create mode 100644 src/ReplicatedStorage/Modules/InkFX.luau diff --git a/src/ReplicatedStorage/Classes/Ability/AbilityGUI.luau b/src/ReplicatedStorage/Classes/Ability/AbilityGUI.luau index 4bd67473..7f849fa3 100644 --- a/src/ReplicatedStorage/Classes/Ability/AbilityGUI.luau +++ b/src/ReplicatedStorage/Classes/Ability/AbilityGUI.luau @@ -4,6 +4,7 @@ local Players = game:GetService("Players") local ReplicatedStorage = game:GetService("ReplicatedStorage") local Types = require(ReplicatedStorage.Classes.Types) local Janitor = require(ReplicatedStorage.Packages.Janitor) +local InkFX = require(ReplicatedStorage.Modules.InkFX) type Ability = { ModuleCode: Types.Ability, @@ -73,8 +74,11 @@ function AbilityGUI.InitGUI() end end), "Disconnect") - for _, Ability: Ability in Abilities do + for Index, Ability: Ability in Abilities do AbilityGUI._UpdateAbilityFrameInput(Ability, InputManager, InputManager.CurrentControlScheme) + local Frame = Ability.Interactor.Parent :: Frame + InkFX.Pop(Frame, 0, 0.5, Index * 0.08) + task.delay(Index * 0.08, InkFX.Ripple, Frame, 1.5, 0.5) --it would be stupid to not add this (again). -Dyscarn -- uses label @@ -90,6 +94,9 @@ function AbilityGUI.InitGUI() JanitorInstance:Add(Ability.ModuleCode.Signals.CooldownSet:Connect(function(_cooldownSet: number) Ability.Timer.TextTransparency = 0 + InkFX.Pop(Frame, 0.82, 0.4) + InkFX.Ripple(Frame, 1.7, 0.5) + InkFX.Drain(Frame, Ability.ModuleCode.CooldownTimer) while Ability.Timer and Ability.ModuleCode.CooldownTimer > 0 do -- failsafe if the ui gets destroyed for any reason if not Ability.Timer then diff --git a/src/ReplicatedStorage/Modules/InkFX.luau b/src/ReplicatedStorage/Modules/InkFX.luau new file mode 100644 index 00000000..d46b5ba6 --- /dev/null +++ b/src/ReplicatedStorage/Modules/InkFX.luau @@ -0,0 +1,119 @@ +--!strict +local Players = game:GetService("Players") +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local TweenService = game:GetService("TweenService") + +local Utils = require(ReplicatedStorage.Modules.Utils) + +local InkFX = {} + +local InkVeilImage = "rbxassetid://94299335585756" +local InkRingImage = "rbxassetid://106770110603452" +local InkDiscImage = "rbxassetid://77200941814063" + +function InkFX.Calm(): boolean + return Utils.PlayerData.GetPlayerSetting(Players.LocalPlayer, "Customization.ScreenShakeEnabled") == false +end + +function InkFX.Ripple(Host: GuiObject, Grow: number, Duration: number, ZIndex: number?) + if InkFX.Calm() then + return + end + for Index, Delay in {0, 0.07} do + local Ring = Instance.new("ImageLabel") + Ring.Image = InkRingImage + Ring.AnchorPoint = Vector2.new(0.5, 0.5) + Ring.Position = UDim2.fromScale(0.5, 0.5) + Ring.Size = UDim2.fromScale(0.95, 0.95) + Ring.Rotation = math.random(0, 360) + Ring.BackgroundTransparency = 1 + Ring.ImageTransparency = 0.1 + Index * 0.25 + Ring.ZIndex = ZIndex or 6 + Ring.SizeConstraint = Enum.SizeConstraint.RelativeYY + Ring.Parent = Host + local Info = TweenInfo.new(Duration, Enum.EasingStyle.Quint, Enum.EasingDirection.Out, 0, false, Delay) + TweenService:Create(Ring, Info, {Size = UDim2.fromScale(Grow, Grow), Rotation = Ring.Rotation + 40, ImageTransparency = 1}):Play() + task.delay(Duration + Delay, Ring.Destroy, Ring) + end +end + +function InkFX.Reveal(Menu: GuiObject?, Closing: boolean) + if not Menu or not Menu:IsA("GuiObject") or InkFX.Calm() then + return + end + local Clip = Instance.new("Frame") + Clip.Name = "InkVeil" + Clip.Size = UDim2.fromScale(1, 1) + Clip.BackgroundTransparency = 1 + Clip.ClipsDescendants = true + Clip.ZIndex = 50 + local Veil = Instance.new("ImageLabel") + Veil.Image = InkVeilImage + Veil.BackgroundTransparency = 1 + Veil.ScaleType = Enum.ScaleType.Stretch + Veil.Size = UDim2.fromScale(2, 1) + Veil.Position = UDim2.fromScale(if Closing then 0 else -1, 0) + Veil.ZIndex = 50 + Veil.Parent = Clip + Clip.Parent = Menu + local Duration = if Closing then 0.3 else 0.5 + local Style = if Closing then Enum.EasingDirection.In else Enum.EasingDirection.Out + TweenService:Create(Veil, TweenInfo.new(Duration, Enum.EasingStyle.Sine, Style), {Position = UDim2.fromScale(if Closing then -1 else 0, 0)}):Play() + task.delay(Duration, Clip.Destroy, Clip) + if not Closing and not Menu:FindFirstChildOfClass("UIScale") then + local Settle = Instance.new("UIScale") + Settle.Scale = 0.96 + Settle.Parent = Menu + TweenService:Create(Settle, TweenInfo.new(0.55, Enum.EasingStyle.Back, Enum.EasingDirection.Out), {Scale = 1}):Play() + task.delay(0.55, Settle.Destroy, Settle) + end +end + +function InkFX.Pop(Host: GuiObject, From: number, Duration: number, Delay: number?) + if InkFX.Calm() then + return + end + local Scale = Host:FindFirstChildOfClass("UIScale") + if not Scale then + Scale = Instance.new("UIScale") + Scale:SetAttribute("Rest", 1) + Scale.Parent = Host + end + local Rest = Scale:GetAttribute("Rest") or Scale.Scale + Scale:SetAttribute("Rest", Rest) + Scale.Scale = From * Rest + TweenService:Create(Scale, TweenInfo.new(Duration, Enum.EasingStyle.Back, Enum.EasingDirection.Out, 0, false, Delay or 0), {Scale = Rest}):Play() +end + +function InkFX.Drain(Host: GuiObject, Seconds: number) + local Icon = Host:FindFirstChild("ImageLabel") + if not Icon or not Icon:IsA("ImageLabel") then + return + end + local Old = Icon:FindFirstChild("InkDrain") + if Old then + Old:Destroy() + end + local Ink = Instance.new("ImageLabel") + Ink.Name = "InkDrain" + Ink.Image = InkDiscImage + Ink.ImageTransparency = 0.25 + Ink.BackgroundTransparency = 1 + Ink.AnchorPoint = Vector2.new(0.5, 0.5) + Ink.Position = UDim2.fromScale(0.5, 0.5) + Ink.Size = UDim2.fromScale(1, 1) + Ink.SizeConstraint = Enum.SizeConstraint.RelativeYY + Ink.ZIndex = Icon.ZIndex + 1 + Ink.Parent = Icon + local Tween = TweenService:Create(Ink, TweenInfo.new(Seconds, Enum.EasingStyle.Linear), {Size = UDim2.fromScale(0, 0), Rotation = 90}) + Tween.Completed:Once(function(State: Enum.PlaybackState) + Ink:Destroy() + if State == Enum.PlaybackState.Completed and Host.Parent then + InkFX.Ripple(Host, 1.5, 0.5) + InkFX.Pop(Host, 1.12, 0.35) + end + end) + Tween:Play() +end + +return InkFX diff --git a/src/StarterPlayerScripts/UI/SideBar.luau b/src/StarterPlayerScripts/UI/SideBar.luau index b9f3626c..a578c056 100644 --- a/src/StarterPlayerScripts/UI/SideBar.luau +++ b/src/StarterPlayerScripts/UI/SideBar.luau @@ -5,6 +5,7 @@ local TweenService = game:GetService("TweenService") local Utils = require(ReplicatedStorage.Modules.Utils) local Sounds = require(ReplicatedStorage.Modules.Sounds) +local InkFX = require(ReplicatedStorage.Modules.InkFX) local LocalPlayer = Players.LocalPlayer @@ -25,66 +26,6 @@ type Button = { } local InvertedInfo = TweenInfo.new(0.25, Enum.EasingStyle.Sine, Enum.EasingDirection.InOut) -local InkVeilImage = "rbxassetid://94299335585756" -local InkRingImage = "rbxassetid://106770110603452" - -local function CalmMotion(): boolean - return Utils.PlayerData.GetPlayerSetting(LocalPlayer, "Customization.ScreenShakeEnabled") == false -end - -local function InkRipple(Host: GuiObject, Grow: number, Duration: number) - if CalmMotion() then - return - end - for Index, Delay in {0, 0.07} do - local Ring = Instance.new("ImageLabel") - Ring.Image = InkRingImage - Ring.AnchorPoint = Vector2.new(0.5, 0.5) - Ring.Position = UDim2.fromScale(0.5, 0.5) - Ring.Size = UDim2.fromScale(0.95, 0.95) - Ring.Rotation = math.random(0, 360) - Ring.BackgroundTransparency = 1 - Ring.ImageTransparency = 0.1 + Index * 0.25 - Ring.ZIndex = 6 - Ring.Parent = Host - local Info = TweenInfo.new(Duration, Enum.EasingStyle.Quint, Enum.EasingDirection.Out, 0, false, Delay) - TweenService:Create(Ring, Info, {Size = UDim2.fromScale(Grow, Grow), Rotation = Ring.Rotation + 40, ImageTransparency = 1}):Play() - task.delay(Duration + Delay, Ring.Destroy, Ring) - end -end - -local function InkReveal(Menu: GuiObject?, Closing: boolean) - if not Menu or not Menu:IsA("GuiObject") or CalmMotion() then - return - end - local Clip = Instance.new("Frame") - Clip.Name = "InkVeil" - Clip.Size = UDim2.fromScale(1, 1) - Clip.BackgroundTransparency = 1 - Clip.ClipsDescendants = true - Clip.ZIndex = 50 - local Veil = Instance.new("ImageLabel") - Veil.Image = InkVeilImage - Veil.BackgroundTransparency = 1 - Veil.ScaleType = Enum.ScaleType.Stretch - Veil.Size = UDim2.fromScale(2, 1) - Veil.Position = UDim2.fromScale(if Closing then 0 else -1, 0) - Veil.ZIndex = 50 - Veil.Parent = Clip - Clip.Parent = Menu - local Duration = if Closing then 0.3 else 0.5 - local Style = if Closing then Enum.EasingDirection.In else Enum.EasingDirection.Out - TweenService:Create(Veil, TweenInfo.new(Duration, Enum.EasingStyle.Sine, Style), {Position = UDim2.fromScale(if Closing then -1 else 0, 0)}):Play() - task.delay(Duration, Clip.Destroy, Clip) - if not Closing and not Menu:FindFirstChildOfClass("UIScale") then - local Settle = Instance.new("UIScale") - Settle.Scale = 0.96 - Settle.Parent = Menu - TweenService:Create(Settle, TweenInfo.new(0.55, Enum.EasingStyle.Back, Enum.EasingDirection.Out), {Scale = 1}):Play() - task.delay(0.55, Settle.Destroy, Settle) - end -end - local function Sway(BarButton: GuiObject) for _, Name in {"Button", "Inverted"} do local Image = BarButton:FindFirstChild(Name) :: GuiObject @@ -103,7 +44,7 @@ local function Breathe(Buttons: {[GuiObject]: number}) local Clock = 0 game:GetService("RunService").RenderStepped:Connect(function(dt: number) Clock += dt - local Calm = CalmMotion() + local Calm = InkFX.Calm() for Scale, Phase in Buttons do Scale.Scale = if Calm then 1 else 1 + 0.025 * math.sin(Clock * 1.6 + Phase) end @@ -173,12 +114,12 @@ function SideBar.OpenMenu(Menu) if Menu.Button then TweenInverted(Menu.Button, true) end - InkReveal(Menu.UIInstance, false) + InkFX.Reveal(Menu.UIInstance, false) if Menu.CloseOthersOnOpen then SideBar.CloseAll(Menu) end else - InkReveal(Menu.UIInstance, true) + InkFX.Reveal(Menu.UIInstance, true) Menu.Close(SideBar.ToggleDuration) if Menu.Button then TweenInverted(Menu.Button, false) @@ -302,7 +243,7 @@ function SideBar:Init() Menu.CloseOnOtherOpen = button.CloseOnOtherOpen Menu.CloseOthersOnOpen = button.CloseOthersOnOpen BarButton.Button.MouseButton1Click:Connect(function() - InkRipple(BarButton, 1.75, 0.6) + InkFX.Ripple(BarButton, 1.75, 0.6) SideBar.OpenMenu(Menu) end) Breathing[Instance.new("UIScale", BarButton)] = button.LayoutOrder * 0.7 @@ -317,8 +258,8 @@ function SideBar:Init() local OriginalYLabelBound = BarButton.Title.Holder.Label.AbsoluteSize.Y local function Hover() - if not CalmMotion() then - InkRipple(BarButton, 1.45, 0.45) + if not InkFX.Calm() then + InkFX.Ripple(BarButton, 1.45, 0.45) Sway(BarButton) end Sounds.PlaySound(Sounds.CommonlyUsedSounds.ButtonHover, {Volume = 0.2}) diff --git a/src/StarterPlayerScripts/UI/Time.luau b/src/StarterPlayerScripts/UI/Time.luau index ae45df96..d9da2d0a 100644 --- a/src/StarterPlayerScripts/UI/Time.luau +++ b/src/StarterPlayerScripts/UI/Time.luau @@ -1,6 +1,7 @@ local ReplicatedStorage = game:GetService("ReplicatedStorage") local Utils = require(ReplicatedStorage.Modules.Utils) +local InkFX = require(ReplicatedStorage.Modules.InkFX) return { Init = function(_) @@ -26,8 +27,15 @@ return { TimeLabel.Text = Utils.Math.ConvertToMinSec(math.ceil(TimeRemaining.Value)) Label.Text = LabelTexts[State.Value] + local LastSecond = math.ceil(TimeRemaining.Value) TimeRemaining.Changed:Connect(function(value: number) TimeLabel.Text = Utils.Math.ConvertToMinSec(math.ceil(value)) + local Second = math.ceil(value) + if Second ~= LastSecond and State.Value == "InRound" and Second <= 10 and Second > 0 then + InkFX.Pop(TimeLabel, 1.22, 0.35) + InkFX.Ripple(Frame, 1.25, 0.6, 0) + end + LastSecond = Second end) local function Update(value: string) @@ -40,7 +48,10 @@ return { end end - State.Changed:Connect(Update) + State.Changed:Connect(function(value: string) + Update(value) + InkFX.Ripple(Frame, 1.6, 0.7, 0) + end) EnoughPlayers.Changed:Connect(function() Update(State.Value) end) diff --git a/tools/tinta_ui.py b/tools/tinta_ui.py index 0b09f451..d4e2be62 100644 --- a/tools/tinta_ui.py +++ b/tools/tinta_ui.py @@ -49,3 +49,16 @@ def smooth_noise(n, scale, amp): prev.alpha_composite(img.resize((W // 2, H // 2))) prev.alpha_composite(Image.open("UI texturas/efectos/anillo_tinta.png").resize((S // 2, S // 2)), (W // 2, 0)) prev.save("UI texturas/efectos/vista_previa.png") + +D = 256 +yy, xx = np.mgrid[0:D, 0:D].astype(float) - D / 2 +r, th = np.hypot(xx, yy), np.arctan2(yy, xx) +rim = 118 + 3 * np.sin(th * 3 + 1.3) + 2 * np.sin(th * 5 + 0.4) +body = np.clip((rim - r) / 4, 0, 1) +hatch = (np.sin((xx + yy * 0.9) * 0.45) > 0.8) & (rng.random((D, D)) > 0.3) +disc = np.zeros((D, D, 4), np.uint8) +disc[..., :3] = np.where(hatch[..., None], [44, 42, 38], [16, 15, 13]) +edge = np.clip(1 - np.abs(r - rim + 2) / 2.2, 0, 1) * 0.6 +disc[..., :3] = (disc[..., :3] * (1 - edge[..., None]) + np.array([236, 228, 210]) * edge[..., None]).astype(np.uint8) +disc[..., 3] = (body * 255).astype(np.uint8) +Image.fromarray(disc, "RGBA").filter(ImageFilter.GaussianBlur(0.5)).save("UI texturas/efectos/disco_tinta.png") From 87bcfef2bd42b138988490ef0d38fa6d561a2483 Mon Sep 17 00:00:00 2001 From: Norl4 Date: Tue, 3 Nov 2026 21:00:44 +0100 Subject: [PATCH 25/32] Animate the health, stamina and generator HUD with ink: ivory damage trail, hit and heal pops, low-stamina flicker, generator counter pop and minigame ripples --- .../Miscellaneous/GeneratorClient.luau | 11 +++++++++ .../UI/CharacterStatsManager.luau | 23 +++++++++++++++++-- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/StarterPlayerScripts/Miscellaneous/GeneratorClient.luau b/src/StarterPlayerScripts/Miscellaneous/GeneratorClient.luau index dff289f1..eb3e4126 100644 --- a/src/StarterPlayerScripts/Miscellaneous/GeneratorClient.luau +++ b/src/StarterPlayerScripts/Miscellaneous/GeneratorClient.luau @@ -9,6 +9,7 @@ local UserInputService = game:GetService("UserInputService") local Network = require(ReplicatedStorage.Modules.Network) local Sounds = require(ReplicatedStorage.Modules.Sounds) +local InkFX = require(ReplicatedStorage.Modules.InkFX) local RoundState: StringValue = ReplicatedStorage.RoundInfo.CurrentState @@ -48,7 +49,14 @@ return { gui.Parent = Players.LocalPlayer:WaitForChild("PlayerGui") local counter = label(gui, UDim2.new(0.16, 0, 0.035, 0), UDim2.new(0.5, 0, 0.1, 4), "") + local lastRepaired = workspace:GetAttribute("GeneratorsRepaired") or 0 local function updateCounter() + local repaired = workspace:GetAttribute("GeneratorsRepaired") or 0 + if repaired > lastRepaired and counter.Visible then + InkFX.Pop(counter, 1.45, 0.55) + InkFX.Ripple(counter, 2.6, 0.9) + end + lastRepaired = repaired local total = workspace:GetAttribute("GeneratorsTotal") or 0 counter.Text = `Generadores {workspace:GetAttribute("GeneratorsRepaired") or 0}/{total}` counter.Visible = RoundState.Value == "InRound" and total > 0 @@ -108,6 +116,9 @@ return { Network:SetConnection("GeneratorCheck", "REMOTE_EVENT", function(delay: number, sweep: number, zoneStart: number, width: number) if repairing then Sounds.PlaySound(Sounds.CommonlyUsedSounds.ButtonPress, { PlaybackSpeed = 1.3 }) + InkFX.Ripple(needle, 2.4, 0.5) + else + InkFX.Pop(bar, 0.6, 0.35) end repairing = true ProximityPromptService.Enabled = false diff --git a/src/StarterPlayerScripts/UI/CharacterStatsManager.luau b/src/StarterPlayerScripts/UI/CharacterStatsManager.luau index a2e89317..1b0cf79d 100644 --- a/src/StarterPlayerScripts/UI/CharacterStatsManager.luau +++ b/src/StarterPlayerScripts/UI/CharacterStatsManager.luau @@ -7,6 +7,7 @@ local LocalPlayer = game:GetService("Players").LocalPlayer local ReplicatedStorage = game:GetService("ReplicatedStorage") local RunService = game:GetService("RunService") local Utils = require(ReplicatedStorage.Modules.Utils) +local InkFX = require(ReplicatedStorage.Modules.InkFX) function CharacterStatsManager:Init() local UIPrefab = Utils.Instance.FindFirstChild(script, "Stats") @@ -36,6 +37,12 @@ function CharacterStatsManager._InitUI(Char: Model, UI: Frame) local Health = UI.Health local HealthText = Health.TextLabel local HealthBar = Health.BarBackground.Bar + local HealthTrail = HealthBar:Clone() + HealthTrail.Name = "Trail" + HealthTrail.BackgroundColor3 = Color3.fromRGB(236, 228, 210) + HealthTrail.ZIndex = HealthBar.ZIndex + HealthBar.ZIndex += 1 + HealthTrail.Parent = Health.BarBackground local Humanoid = Char:FindFirstChildWhichIsA("Humanoid") local StaminaAttributes = { @@ -49,12 +56,24 @@ function CharacterStatsManager._InitUI(Char: Model, UI: Frame) return end - HealthBar.Size = UDim2.fromScale(math.lerp(HealthBar.Size.X.Scale, Humanoid.Health / Humanoid.MaxHealth, delta * 9), 1) + local HealthRatio = Humanoid.Health / Humanoid.MaxHealth + HealthBar.Size = UDim2.fromScale(math.lerp(HealthBar.Size.X.Scale, HealthRatio, delta * 9), 1) + HealthTrail.Size = UDim2.fromScale(math.max(HealthRatio, math.lerp(HealthTrail.Size.X.Scale, HealthRatio, delta * 1.6)), 1) -- - StaminaBar.Size = UDim2.fromScale(math.lerp(StaminaBar.Size.X.Scale, StaminaAttributes.Stamina.Value / StaminaAttributes.MaxStamina.Value, delta * 9), 1) + local StaminaRatio = StaminaAttributes.Stamina.Value / StaminaAttributes.MaxStamina.Value + StaminaBar.Size = UDim2.fromScale(math.lerp(StaminaBar.Size.X.Scale, StaminaRatio, delta * 9), 1) + StaminaBar.BackgroundTransparency = if StaminaRatio < 0.25 and not InkFX.Calm() then 0.35 + 0.35 * math.sin(os.clock() * 9) else 0 end) + local LastHealth = Humanoid.Health Utils.Instance.ObserveProperty(Humanoid, "Health", function(value: number) + if value < LastHealth - 0.5 then + InkFX.Pop(HealthText, 1.35, 0.45) + InkFX.Ripple(HealthText, 1.8, 0.6) + elseif value > LastHealth + 0.5 then + InkFX.Pop(HealthText, 1.15, 0.4) + end + LastHealth = value HealthText.Text = tostring(math.round(Humanoid.Health)).."/"..tostring(Humanoid.MaxHealth) end) From 583f91faa07f2b60ed16e6138328dcad366537a4 Mon Sep 17 00:00:00 2001 From: Norl4 Date: Tue, 3 Nov 2026 21:50:56 +0100 Subject: [PATCH 26/32] Limit health pops and ripples to one every 0.3 s so tick damage does not pulse nonstop --- src/StarterPlayerScripts/UI/CharacterStatsManager.luau | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/StarterPlayerScripts/UI/CharacterStatsManager.luau b/src/StarterPlayerScripts/UI/CharacterStatsManager.luau index 1b0cf79d..f3894502 100644 --- a/src/StarterPlayerScripts/UI/CharacterStatsManager.luau +++ b/src/StarterPlayerScripts/UI/CharacterStatsManager.luau @@ -66,11 +66,15 @@ function CharacterStatsManager._InitUI(Char: Model, UI: Frame) end) local LastHealth = Humanoid.Health + local LastPop = 0 Utils.Instance.ObserveProperty(Humanoid, "Health", function(value: number) - if value < LastHealth - 0.5 then + local Ready = os.clock() - LastPop > 0.3 + if Ready and value < LastHealth - 0.5 then + LastPop = os.clock() InkFX.Pop(HealthText, 1.35, 0.45) InkFX.Ripple(HealthText, 1.8, 0.6) - elseif value > LastHealth + 0.5 then + elseif Ready and value > LastHealth + 0.5 then + LastPop = os.clock() InkFX.Pop(HealthText, 1.15, 0.4) end LastHealth = value From 11a06f137936c0f3a44fcbf086de20bcbb6bffd5 Mon Sep 17 00:00:00 2001 From: Norl4 Date: Tue, 3 Nov 2026 23:17:22 +0100 Subject: [PATCH 27/32] Replace the menu size tweens with an ink veil transition, draw ripples behind the UI with an ink stroke under wide text, and wire the new hand-drawn icons, renders and ink textures --- .../Assets/Achievements.luau | 1 + .../Assets/Emotes/TPose.luau | 1 + src/ReplicatedStorage/Assets/Items/Cola.luau | 1 + .../Assets/Items/Medkit.luau | 1 + .../Characters/Killers/NullexVoyd.luau | 5 +- .../Characters/Survivors/Commander.luau | 4 + .../Classes/Ability/Slash.luau | 2 +- src/ReplicatedStorage/Modules/InkFX.luau | 146 ++++++++++++++---- .../UI/AchievementsMenu.luau | 7 +- src/StarterPlayerScripts/UI/Changelog.luau | 5 +- src/StarterPlayerScripts/UI/Credits.luau | 5 +- .../UI/Inventory/init.luau | 5 +- .../UI/ModPanel/init.luau | 5 +- src/StarterPlayerScripts/UI/RulesMenu.luau | 5 +- .../UI/Settings/init.luau | 5 +- src/StarterPlayerScripts/UI/Shop/init.luau | 5 +- src/StarterPlayerScripts/UI/SideBar.luau | 2 - src/StarterPlayerScripts/UI/StatsMenu.luau | 5 +- src/StarterPlayerScripts/UI/Time.luau | 4 +- 19 files changed, 159 insertions(+), 55 deletions(-) diff --git a/src/ReplicatedStorage/Assets/Achievements.luau b/src/ReplicatedStorage/Assets/Achievements.luau index 3645d20d..e305a0b3 100644 --- a/src/ReplicatedStorage/Assets/Achievements.luau +++ b/src/ReplicatedStorage/Assets/Achievements.luau @@ -15,6 +15,7 @@ return { Achievements = { Welcome = { Title = "Bienvenido a Anomalis", + Icon = "rbxassetid://108807232649489", Description = "Juega a Anomalis por primera vez", RewardType = "Currency", Amount = 300, diff --git a/src/ReplicatedStorage/Assets/Emotes/TPose.luau b/src/ReplicatedStorage/Assets/Emotes/TPose.luau index cc50d4fb..8d6ab97f 100644 --- a/src/ReplicatedStorage/Assets/Emotes/TPose.luau +++ b/src/ReplicatedStorage/Assets/Emotes/TPose.luau @@ -4,6 +4,7 @@ local Emote = require(ReplicatedStorage.Classes.Emote) return Emote.New({ Config = { Name = "T-Pose", + Render = "rbxassetid://134166673644911", Quote = "no temas.", }, AnimationIds = "rbxassetid://87300475457340", diff --git a/src/ReplicatedStorage/Assets/Items/Cola.luau b/src/ReplicatedStorage/Assets/Items/Cola.luau index 69f4665d..d7be697f 100644 --- a/src/ReplicatedStorage/Assets/Items/Cola.luau +++ b/src/ReplicatedStorage/Assets/Items/Cola.luau @@ -8,6 +8,7 @@ local PlayerSpeedManager = RunService:IsServer() and require(game:GetService("Se return Item.New({ Name = "Bloxy Cola", + Icon = "rbxassetid://76600054012804", ToolPrefab = script:FindFirstChild("Cola"), Behaviour = function(self: Types.Item) diff --git a/src/ReplicatedStorage/Assets/Items/Medkit.luau b/src/ReplicatedStorage/Assets/Items/Medkit.luau index b4db9890..2ad216ab 100644 --- a/src/ReplicatedStorage/Assets/Items/Medkit.luau +++ b/src/ReplicatedStorage/Assets/Items/Medkit.luau @@ -7,6 +7,7 @@ local PlayerSpeedManager = RunService:IsServer() and require(game:GetService("Se return Item.New({ Name = "Medkit", + Icon = "rbxassetid://113163533935584", ToolPrefab = script:FindFirstChild("Medkit"), Behaviour = function(self: Types.Item) diff --git a/src/ReplicatedStorage/Characters/Killers/NullexVoyd.luau b/src/ReplicatedStorage/Characters/Killers/NullexVoyd.luau index f6021b9d..1ebf4fc3 100644 --- a/src/ReplicatedStorage/Characters/Killers/NullexVoyd.luau +++ b/src/ReplicatedStorage/Characters/Killers/NullexVoyd.luau @@ -176,6 +176,7 @@ local NullexVoyd: Types.Killer = Character.CreateKiller({ DataAnchor = Ability.New({ Name = "Data Anchor", + RenderImage = "rbxassetid://82042307155133", FirstCooldown = 3, SecondCooldown = 20, @@ -197,6 +198,7 @@ local NullexVoyd: Types.Killer = Character.CreateKiller({ CallbackPing = Ability.New({ Name = "Callback Ping", + RenderImage = "rbxassetid://123063919281024", Duration = 3, InputName = "ThirdAbility", Cooldown = 32.5, @@ -206,6 +208,7 @@ local NullexVoyd: Types.Killer = Character.CreateKiller({ Overcharge = Ability.New({ Name = "Overcharge", + RenderImage = "rbxassetid://114480081841217", Cooldown = 45, InputName = "FourthAbility", SpeedDuration = 3.2, @@ -269,7 +272,7 @@ NullexVoyd.Config.Description = { { Type = "Text", Text = NullexVoydNameLabel.." lanza un tajo básico que hace "..tostring(NullexVoyd.GameplayConfig.Abilities.Slash.Damage).." de daño.", - Image = "rbxassetid://133006324317341", + Image = "rbxassetid://82563286387450", }, { Type = "Header", diff --git a/src/ReplicatedStorage/Characters/Survivors/Commander.luau b/src/ReplicatedStorage/Characters/Survivors/Commander.luau index 62ac7316..73626457 100644 --- a/src/ReplicatedStorage/Characters/Survivors/Commander.luau +++ b/src/ReplicatedStorage/Characters/Survivors/Commander.luau @@ -187,12 +187,14 @@ local Commander: Types.Survivor = Character.CreateSurvivor({ Abilities = { BodyArmor = Ability.New({ Name = "Body Armor", + RenderImage = "rbxassetid://120611547615079", -- Description = "Every time Commander gets damaged, he receives Resistance V for 1 second, acting as invincibility frames.", Passive = true, }), Blindside = Ability.New({ Name = "Blindside", + RenderImage = "rbxassetid://118477647585035", ThrowDelay = 1, TriggerDelay = 3.5, @@ -211,6 +213,7 @@ local Commander: Types.Survivor = Character.CreateSurvivor({ Reload = Ability.New({ Name = "Reload", + RenderImage = "rbxassetid://129836808884946", Duration = 5, Cooldown = 40, InputName = "SecondAbility", @@ -221,6 +224,7 @@ local Commander: Types.Survivor = Character.CreateSurvivor({ Justice = Ability.New({ Name = "Justice", + RenderImage = "rbxassetid://77286835155262", Description = "Commander saca la pistola y dispara al frente mientras va despacio. Hace 50 de daño al asesino y lo aturde 4,2 segundos.", UseAnimation = "", ShotSound = "", diff --git a/src/ReplicatedStorage/Classes/Ability/Slash.luau b/src/ReplicatedStorage/Classes/Ability/Slash.luau index 3df6d643..7f2834ca 100644 --- a/src/ReplicatedStorage/Classes/Ability/Slash.luau +++ b/src/ReplicatedStorage/Classes/Ability/Slash.luau @@ -45,7 +45,7 @@ function SlashModule.New(Props: Types.Ability?): Types.Ability Cooldown = 2, Duration = 0.4, Damage = 20, - RenderImage = "rbxassetid://114455340704913", + RenderImage = "rbxassetid://82563286387450", UseSound = "rbxassetid://12222200", UseAnimation = "rbxassetid://133504250023139", UICorner = true, diff --git a/src/ReplicatedStorage/Modules/InkFX.luau b/src/ReplicatedStorage/Modules/InkFX.luau index d46b5ba6..f3eecb83 100644 --- a/src/ReplicatedStorage/Modules/InkFX.luau +++ b/src/ReplicatedStorage/Modules/InkFX.luau @@ -7,66 +7,152 @@ local Utils = require(ReplicatedStorage.Modules.Utils) local InkFX = {} -local InkVeilImage = "rbxassetid://94299335585756" -local InkRingImage = "rbxassetid://106770110603452" -local InkDiscImage = "rbxassetid://77200941814063" +local InkVeilImage = "rbxassetid://118511292501872" +local InkRingImage = "rbxassetid://116404627712698" +local InkDiscImage = "rbxassetid://121024594981035" +local InkStrokeImage = "rbxassetid://125872915180924" function InkFX.Calm(): boolean return Utils.PlayerData.GetPlayerSetting(Players.LocalPlayer, "Customization.ScreenShakeEnabled") == false end -function InkFX.Ripple(Host: GuiObject, Grow: number, Duration: number, ZIndex: number?) - if InkFX.Calm() then +local Layers: {[string]: Frame} = {} +local function Layer(Host: GuiObject, Name: string, ZIndex: number): Frame? + local Gui = Host:FindFirstAncestorWhichIsA("ScreenGui") + if not Gui then + return nil + end + local Key = Gui:GetFullName() .. Name + local Found = Layers[Key] + if not Found or not Found.Parent then + Found = Instance.new("Frame") + Found.Name = Name + Found.Size = UDim2.fromScale(1, 1) + Found.BackgroundTransparency = 1 + Found.ZIndex = ZIndex + Found.Parent = Gui + Layers[Key] = Found + end + return Found +end + +function InkFX.Ripple(Host: GuiObject, Grow: number, Duration: number) + local Layer = Layer(Host, "InkLayer", 0) + if InkFX.Calm() or not Layer then + return + end + local Size = Host.AbsoluteSize + local Center = Host.AbsolutePosition + Size / 2 - Layer.AbsolutePosition + if Size.X > Size.Y * 1.6 then + local Below = if Host.BackgroundTransparency < 1 then Size.Y * 0.5 + 6 else Size.Y * 0.42 + local Stroke = Instance.new("ImageLabel") + Stroke.Image = InkStrokeImage + Stroke.BackgroundTransparency = 1 + Stroke.ImageTransparency = 0.15 + Stroke.AnchorPoint = Vector2.new(0, 0.5) + Stroke.Position = UDim2.fromOffset(Center.X - Size.X * 0.55, Center.Y + Below) + Stroke.Size = UDim2.fromOffset(0, math.min(Size.Y * 0.3, 14)) + Stroke.Parent = Layer + TweenService:Create(Stroke, TweenInfo.new(Duration * 0.5, Enum.EasingStyle.Quint, Enum.EasingDirection.Out), {Size = UDim2.fromOffset(Size.X * 1.1, Stroke.Size.Y.Offset)}):Play() + TweenService:Create(Stroke, TweenInfo.new(Duration * 0.6, Enum.EasingStyle.Sine, Enum.EasingDirection.In, 0, false, Duration * 0.4), {ImageTransparency = 1}):Play() + task.delay(Duration, Stroke.Destroy, Stroke) return end for Index, Delay in {0, 0.07} do local Ring = Instance.new("ImageLabel") Ring.Image = InkRingImage - Ring.AnchorPoint = Vector2.new(0.5, 0.5) - Ring.Position = UDim2.fromScale(0.5, 0.5) - Ring.Size = UDim2.fromScale(0.95, 0.95) - Ring.Rotation = math.random(0, 360) Ring.BackgroundTransparency = 1 Ring.ImageTransparency = 0.1 + Index * 0.25 - Ring.ZIndex = ZIndex or 6 - Ring.SizeConstraint = Enum.SizeConstraint.RelativeYY - Ring.Parent = Host + Ring.AnchorPoint = Vector2.new(0.5, 0.5) + Ring.Position = UDim2.fromOffset(Center.X, Center.Y) + Ring.Size = UDim2.fromOffset(Size.Y * 0.95, Size.Y * 0.95) + Ring.Rotation = math.random(0, 360) + Ring.Parent = Layer local Info = TweenInfo.new(Duration, Enum.EasingStyle.Quint, Enum.EasingDirection.Out, 0, false, Delay) - TweenService:Create(Ring, Info, {Size = UDim2.fromScale(Grow, Grow), Rotation = Ring.Rotation + 40, ImageTransparency = 1}):Play() + TweenService:Create(Ring, Info, {Size = UDim2.fromOffset(Size.Y * Grow, Size.Y * Grow), Rotation = Ring.Rotation + 40, ImageTransparency = 1}):Play() task.delay(Duration + Delay, Ring.Destroy, Ring) end end -function InkFX.Reveal(Menu: GuiObject?, Closing: boolean) - if not Menu or not Menu:IsA("GuiObject") or InkFX.Calm() then +local function Bounds(Menu: GuiObject): (Vector2, Vector2) + local Min, Max = Menu.AbsolutePosition + Menu.AbsoluteSize, Menu.AbsolutePosition + local Items: {GuiObject} = if Menu:FindFirstChildOfClass("UIAspectRatioConstraint") then {Menu} else {} + for _, Child in Menu:GetChildren() do + if Child:IsA("GuiObject") and Child.Visible and Child.AbsoluteSize.X > 30 and Child.AbsoluteSize.Y > 30 then + table.insert(Items, Child) + end + end + for _, Item in Items do + Min = Min:Min(Item.AbsolutePosition) + Max = Max:Max(Item.AbsolutePosition + Item.AbsoluteSize) + end + return Min, Max - Min +end + +local Versions: {[GuiObject]: number} = {} +local Veils: {[GuiObject]: Frame} = {} +function InkFX.Transition(Menu: GuiObject, Opening: boolean, Duration: number, Full: UDim2) + local Version = (Versions[Menu] or 0) + 1 + Versions[Menu] = Version + local Previous = Veils[Menu] and Veils[Menu]:FindFirstChildOfClass("ImageLabel") + local From = Previous and Previous.Position + if Veils[Menu] then + Veils[Menu]:Destroy() + Veils[Menu] = nil + end + local Top = Layer(Menu, "InkTop", 100) + if InkFX.Calm() or Duration <= 0 or not Top then + Menu.Size = if Opening then Full else UDim2.new() return end + Menu.Size = Full + local Scale = Menu:FindFirstChild("InkScale") :: UIScale? or Instance.new("UIScale") + Scale.Name = "InkScale" + Scale.Scale = 1 + Scale.Parent = Menu + local Position, Size = Bounds(Menu) local Clip = Instance.new("Frame") Clip.Name = "InkVeil" - Clip.Size = UDim2.fromScale(1, 1) + Clip.Position = UDim2.fromOffset(Position.X - Top.AbsolutePosition.X, Position.Y - Top.AbsolutePosition.Y) + Clip.Size = UDim2.fromOffset(Size.X, Size.Y) Clip.BackgroundTransparency = 1 Clip.ClipsDescendants = true - Clip.ZIndex = 50 local Veil = Instance.new("ImageLabel") Veil.Image = InkVeilImage Veil.BackgroundTransparency = 1 Veil.ScaleType = Enum.ScaleType.Stretch Veil.Size = UDim2.fromScale(2, 1) - Veil.Position = UDim2.fromScale(if Closing then 0 else -1, 0) - Veil.ZIndex = 50 + Veil.Position = From or UDim2.fromScale(if Opening then 0 else -1.15, 0) + Veil.ImageTransparency = if Opening and not From then 1 else 0 Veil.Parent = Clip - Clip.Parent = Menu - local Duration = if Closing then 0.3 else 0.5 - local Style = if Closing then Enum.EasingDirection.In else Enum.EasingDirection.Out - TweenService:Create(Veil, TweenInfo.new(Duration, Enum.EasingStyle.Sine, Style), {Position = UDim2.fromScale(if Closing then -1 else 0, 0)}):Play() - task.delay(Duration, Clip.Destroy, Clip) - if not Closing and not Menu:FindFirstChildOfClass("UIScale") then - local Settle = Instance.new("UIScale") - Settle.Scale = 0.96 - Settle.Parent = Menu - TweenService:Create(Settle, TweenInfo.new(0.55, Enum.EasingStyle.Back, Enum.EasingDirection.Out), {Scale = 1}):Play() - task.delay(0.55, Settle.Destroy, Settle) + Clip.Parent = Top + Veils[Menu] = Clip + if Opening and not From then + Scale.Scale = 0.94 + TweenService:Create(Veil, TweenInfo.new(0.08, Enum.EasingStyle.Linear), {ImageTransparency = 0}):Play() end + local Info = TweenInfo.new(Duration, Enum.EasingStyle.Quint, if Opening then Enum.EasingDirection.Out else Enum.EasingDirection.InOut) + TweenService:Create(Scale, Info, {Scale = if Opening then 1 else 0.97}):Play() + TweenService:Create(Veil, Info, {Position = UDim2.fromScale(if Opening then -1.15 else 0, 0)}):Play() + task.delay(Duration, function() + if Versions[Menu] ~= Version then + return + end + if Opening then + Clip:Destroy() + Veils[Menu] = nil + return + end + Menu.Size = UDim2.new() + Scale.Scale = 1 + TweenService:Create(Veil, TweenInfo.new(0.15, Enum.EasingStyle.Sine), {ImageTransparency = 1}):Play() + task.delay(0.15, function() + if Veils[Menu] == Clip then + Veils[Menu] = nil + end + Clip:Destroy() + end) + end) end function InkFX.Pop(Host: GuiObject, From: number, Duration: number, Delay: number?) diff --git a/src/StarterPlayerScripts/UI/AchievementsMenu.luau b/src/StarterPlayerScripts/UI/AchievementsMenu.luau index 4c18083a..87bbe2cb 100644 --- a/src/StarterPlayerScripts/UI/AchievementsMenu.luau +++ b/src/StarterPlayerScripts/UI/AchievementsMenu.luau @@ -6,6 +6,7 @@ local Achievements = require(ReplicatedStorage.Assets.Achievements) local Types = require(ReplicatedStorage.Classes.Types) local Sounds = require(ReplicatedStorage.Modules.Sounds) local Utils = require(ReplicatedStorage.Modules.Utils) +local InkFX = require(ReplicatedStorage.Modules.InkFX) local Janitor = require(ReplicatedStorage.Packages.Janitor) local SideBar = require(script.Parent.SideBar) @@ -26,7 +27,7 @@ local AchievementsMenu = { Hidden = { Title = "?????", Description = "????????????????????", - Icon = "rbxassetid://108919288576085", + Icon = "rbxassetid://130627942827530", }, }, } @@ -68,7 +69,7 @@ function AchievementsMenu.Open(Time: number) end if Time > 0 then - TweenService:Create(AchievementsMenu.UIInstance, TweenInfo.new(Time, Enum.EasingStyle.Cubic, Enum.EasingDirection.Out), {Size = UDim2.fromScale(0.5, 0.7)}):Play() + InkFX.Transition(AchievementsMenu.UIInstance, true, Time, UDim2.fromScale(0.5, 0.7)) else AchievementsMenu.UIInstance.Size = UDim2.fromScale(0.4, 0.7) end @@ -79,7 +80,7 @@ end function AchievementsMenu.Close(Time: number) if Time > 0 then - TweenService:Create(AchievementsMenu.UIInstance, TweenInfo.new(Time, Enum.EasingStyle.Cubic, Enum.EasingDirection.Out), {Size = Utils.UDim.Zero}):Play() + InkFX.Transition(AchievementsMenu.UIInstance, false, Time, AchievementsMenu.UIInstance.Size) else AchievementsMenu.UIInstance.Size = Utils.UDim.Zero end diff --git a/src/StarterPlayerScripts/UI/Changelog.luau b/src/StarterPlayerScripts/UI/Changelog.luau index 714e3169..262115b3 100644 --- a/src/StarterPlayerScripts/UI/Changelog.luau +++ b/src/StarterPlayerScripts/UI/Changelog.luau @@ -4,6 +4,7 @@ local TweenService = game:GetService("TweenService") local SideBar = require(script.Parent.SideBar) local Utils = require(ReplicatedStorage.Modules.Utils) +local InkFX = require(ReplicatedStorage.Modules.InkFX) local LocalPlayer = Players.LocalPlayer @@ -146,7 +147,7 @@ function Changelog.Open(Time: number) end if Time > 0 then - TweenService:Create(Changelog.UIInstance, TweenInfo.new(Time, Enum.EasingStyle.Cubic, Enum.EasingDirection.Out), {Size = UDim2.fromScale(0.4, 0.7)}):Play() + InkFX.Transition(Changelog.UIInstance, true, Time, UDim2.fromScale(0.4, 0.7)) else Changelog.UIInstance.Size = UDim2.fromScale(0.4, 0.7) end @@ -157,7 +158,7 @@ end function Changelog.Close(Time: number) if Time > 0 then - TweenService:Create(Changelog.UIInstance, TweenInfo.new(Time, Enum.EasingStyle.Cubic, Enum.EasingDirection.Out), {Size = Utils.UDim.Zero}):Play() + InkFX.Transition(Changelog.UIInstance, false, Time, Changelog.UIInstance.Size) else Changelog.UIInstance.Size = Utils.UDim.Zero end diff --git a/src/StarterPlayerScripts/UI/Credits.luau b/src/StarterPlayerScripts/UI/Credits.luau index fa127de5..7d1439b6 100644 --- a/src/StarterPlayerScripts/UI/Credits.luau +++ b/src/StarterPlayerScripts/UI/Credits.luau @@ -4,6 +4,7 @@ local TweenService = game:GetService("TweenService") local SideBar = require(script.Parent.SideBar) local Utils = require(ReplicatedStorage.Modules.Utils) +local InkFX = require(ReplicatedStorage.Modules.InkFX) local LocalPlayer = Players.LocalPlayer @@ -95,7 +96,7 @@ function CreditsMenu.Open(Time: number) return "Prevented" end - TweenService:Create(CreditsMenu.UIInstance, TweenInfo.new(Time, Enum.EasingStyle.Cubic, Enum.EasingDirection.Out), {Size = UDim2.fromScale(0.475, 0.599)}):Play() + InkFX.Transition(CreditsMenu.UIInstance, true, Time, UDim2.fromScale(0.475, 0.599)) CreditsMenu.Opened = true return @@ -103,7 +104,7 @@ end function CreditsMenu.Close(Time: number) if Time > 0 then - TweenService:Create(CreditsMenu.UIInstance, TweenInfo.new(Time, Enum.EasingStyle.Cubic, Enum.EasingDirection.Out), {Size = Utils.UDim.Zero}):Play() + InkFX.Transition(CreditsMenu.UIInstance, false, Time, CreditsMenu.UIInstance.Size) else CreditsMenu.UIInstance.Size = Utils.UDim.Zero end diff --git a/src/StarterPlayerScripts/UI/Inventory/init.luau b/src/StarterPlayerScripts/UI/Inventory/init.luau index 4f4d39cb..2d8abec6 100644 --- a/src/StarterPlayerScripts/UI/Inventory/init.luau +++ b/src/StarterPlayerScripts/UI/Inventory/init.luau @@ -11,6 +11,7 @@ local Network = require(ReplicatedStorage.Modules.Network) local Types = require(ReplicatedStorage.Classes.Types) local Tooltip = require(ReplicatedStorage.Classes.Tooltip) local Utils = require(ReplicatedStorage.Modules.Utils) +local InkFX = require(ReplicatedStorage.Modules.InkFX) local Sounds = require(ReplicatedStorage.Modules.Sounds) local Janitor = require(ReplicatedStorage.Packages.Janitor) @@ -661,7 +662,7 @@ function Inventory.Open(Time: number) Inventory.EmoteWindow = nil end - TweenService:Create(UI, TweenInfo.new(Time, Enum.EasingStyle.Cubic, Enum.EasingDirection.Out), {Size = Utils.UDim.Full20Offset}):Play() + InkFX.Transition(UI, true, Time, Utils.UDim.Full20Offset) Inventory.Opened = true return @@ -672,7 +673,7 @@ function Inventory.Close(Time: number) Inventory.EmoteWindow:Hide() end if Time > 0 then - TweenService:Create(UI, TweenInfo.new(Time, Enum.EasingStyle.Cubic, Enum.EasingDirection.Out), {Size = Utils.UDim.Zero}):Play() + InkFX.Transition(UI, false, Time, UI.Size) else UI.Size = Utils.UDim.Zero UI.Visible = false diff --git a/src/StarterPlayerScripts/UI/ModPanel/init.luau b/src/StarterPlayerScripts/UI/ModPanel/init.luau index efc28ebf..140d2e34 100644 --- a/src/StarterPlayerScripts/UI/ModPanel/init.luau +++ b/src/StarterPlayerScripts/UI/ModPanel/init.luau @@ -6,6 +6,7 @@ local SideBar = require(script.Parent.SideBar) local Network = require(ReplicatedStorage.Modules.Network) local Sounds = require(ReplicatedStorage.Modules.Sounds) local Utils = require(ReplicatedStorage.Modules.Utils) +local InkFX = require(ReplicatedStorage.Modules.InkFX) local TopbarPlus = require(ReplicatedStorage.Packages.TopbarPlus) local LocalPlayer = Players.LocalPlayer @@ -113,7 +114,7 @@ function ModPanel.Open(Time: number) SideBar.CloseAll(ModPanel) if Time > 0 then - TweenService:Create(ModPanel.UIInstance, TweenInfo.new(Time, Enum.EasingStyle.Cubic, Enum.EasingDirection.Out), {Size = UDim2.fromScale(0.542, 0.741)}):Play() + InkFX.Transition(ModPanel.UIInstance, true, Time, UDim2.fromScale(0.542, 0.741)) else ModPanel.UIInstance.Size = UDim2.fromScale(0.4, 0.7) end @@ -124,7 +125,7 @@ end function ModPanel.Close(Time: number) if Time > 0 then - TweenService:Create(ModPanel.UIInstance, TweenInfo.new(Time, Enum.EasingStyle.Cubic, Enum.EasingDirection.Out), {Size = Utils.UDim.Zero}):Play() + InkFX.Transition(ModPanel.UIInstance, false, Time, ModPanel.UIInstance.Size) else ModPanel.UIInstance.Size = Utils.UDim.Zero end diff --git a/src/StarterPlayerScripts/UI/RulesMenu.luau b/src/StarterPlayerScripts/UI/RulesMenu.luau index 0b2f73a8..3f46a7a0 100644 --- a/src/StarterPlayerScripts/UI/RulesMenu.luau +++ b/src/StarterPlayerScripts/UI/RulesMenu.luau @@ -4,6 +4,7 @@ local TweenService = game:GetService("TweenService") local SideBar = require(script.Parent.SideBar) local Utils = require(ReplicatedStorage.Modules.Utils) +local InkFX = require(ReplicatedStorage.Modules.InkFX) local LocalPlayer = Players.LocalPlayer @@ -109,7 +110,7 @@ function Rules.Open(Time: number) end if Time > 0 then - TweenService:Create(Rules.UIInstance, TweenInfo.new(Time, Enum.EasingStyle.Cubic, Enum.EasingDirection.Out), {Size = UDim2.fromScale(0.4, 0.7)}):Play() + InkFX.Transition(Rules.UIInstance, true, Time, UDim2.fromScale(0.4, 0.7)) else Rules.UIInstance.Size = UDim2.fromScale(0.4, 0.7) end @@ -120,7 +121,7 @@ end function Rules.Close(Time: number) if Time > 0 then - TweenService:Create(Rules.UIInstance, TweenInfo.new(Time, Enum.EasingStyle.Cubic, Enum.EasingDirection.Out), {Size = Utils.UDim.Zero}):Play() + InkFX.Transition(Rules.UIInstance, false, Time, Rules.UIInstance.Size) else Rules.UIInstance.Size = Utils.UDim.Zero end diff --git a/src/StarterPlayerScripts/UI/Settings/init.luau b/src/StarterPlayerScripts/UI/Settings/init.luau index ed957e44..28af0a1b 100644 --- a/src/StarterPlayerScripts/UI/Settings/init.luau +++ b/src/StarterPlayerScripts/UI/Settings/init.luau @@ -4,6 +4,7 @@ local TweenService = game:GetService("TweenService") local SideBar = require(script.Parent.SideBar) local Utils = require(ReplicatedStorage.Modules.Utils) +local InkFX = require(ReplicatedStorage.Modules.InkFX) local LocalPlayer = game:GetService("Players").LocalPlayer @@ -116,7 +117,7 @@ function Settings.Open(Time: number) return "Prevented" end - TweenService:Create(Settings.UIInstance, TweenInfo.new(Time, Enum.EasingStyle.Cubic, Enum.EasingDirection.Out), {Size = Utils.UDim.Full20Offset}):Play() + InkFX.Transition(Settings.UIInstance, true, Time, Utils.UDim.Full20Offset) Settings.Opened = true return @@ -124,7 +125,7 @@ end function Settings.Close(Time: number) if Time > 0 then - TweenService:Create(Settings.UIInstance, TweenInfo.new(Time, Enum.EasingStyle.Cubic, Enum.EasingDirection.Out), {Size = Utils.UDim.Zero}):Play() + InkFX.Transition(Settings.UIInstance, false, Time, Settings.UIInstance.Size) else Settings.UIInstance.Size = Utils.UDim.Zero Settings.UIInstance.Visible = false diff --git a/src/StarterPlayerScripts/UI/Shop/init.luau b/src/StarterPlayerScripts/UI/Shop/init.luau index 66ee9c3c..480e3d58 100644 --- a/src/StarterPlayerScripts/UI/Shop/init.luau +++ b/src/StarterPlayerScripts/UI/Shop/init.luau @@ -9,6 +9,7 @@ local SideBar = require(script.Parent.SideBar) local Types = require(ReplicatedStorage.Classes.Types) local Tooltip = require(ReplicatedStorage.Classes.Tooltip) local Utils = require(ReplicatedStorage.Modules.Utils) +local InkFX = require(ReplicatedStorage.Modules.InkFX) local Sounds = require(ReplicatedStorage.Modules.Sounds) local Janitor = require(ReplicatedStorage.Packages.Janitor) @@ -697,7 +698,7 @@ function Shop.Open(Time: number) ShopContainer.Parent.Size = UDim2.fromScale(0.7, 1) Shop.SkinMenu.Open = false - TweenService:Create(UI, TweenInfo.new(Time, Enum.EasingStyle.Cubic, Enum.EasingDirection.Out), {Size = Utils.UDim.Full20Offset}):Play() + InkFX.Transition(UI, true, Time, Utils.UDim.Full20Offset) Shop.Opened = true return @@ -711,7 +712,7 @@ function Shop.Close(Time: number) workspace.Sounds.EmotePreview:Destroy() end if Time > 0 then - TweenService:Create(UI, TweenInfo.new(Time, Enum.EasingStyle.Cubic, Enum.EasingDirection.Out), {Size = Utils.UDim.Zero}):Play() + InkFX.Transition(UI, false, Time, UI.Size) else UI.Size = Utils.UDim.Zero UI.Visible = false diff --git a/src/StarterPlayerScripts/UI/SideBar.luau b/src/StarterPlayerScripts/UI/SideBar.luau index a578c056..20851c07 100644 --- a/src/StarterPlayerScripts/UI/SideBar.luau +++ b/src/StarterPlayerScripts/UI/SideBar.luau @@ -114,12 +114,10 @@ function SideBar.OpenMenu(Menu) if Menu.Button then TweenInverted(Menu.Button, true) end - InkFX.Reveal(Menu.UIInstance, false) if Menu.CloseOthersOnOpen then SideBar.CloseAll(Menu) end else - InkFX.Reveal(Menu.UIInstance, true) Menu.Close(SideBar.ToggleDuration) if Menu.Button then TweenInverted(Menu.Button, false) diff --git a/src/StarterPlayerScripts/UI/StatsMenu.luau b/src/StarterPlayerScripts/UI/StatsMenu.luau index f3679b1c..f9928a59 100644 --- a/src/StarterPlayerScripts/UI/StatsMenu.luau +++ b/src/StarterPlayerScripts/UI/StatsMenu.luau @@ -4,6 +4,7 @@ local TweenService = game:GetService("TweenService") local SideBar = require(script.Parent.SideBar) local Utils = require(ReplicatedStorage.Modules.Utils) +local InkFX = require(ReplicatedStorage.Modules.InkFX) local LocalPlayer = Players.LocalPlayer @@ -102,14 +103,14 @@ function StatsMenu.Open(Time: number) return "Prevented" end - TweenService:Create(StatsMenu.UIInstance, TweenInfo.new(Time, Enum.EasingStyle.Cubic, Enum.EasingDirection.Out), {Size = Utils.UDim.Full20Offset}):Play() + InkFX.Transition(StatsMenu.UIInstance, true, Time, Utils.UDim.Full20Offset) StatsMenu.Opened = true return end function StatsMenu.Close(Time: number) if Time > 0 then - TweenService:Create(StatsMenu.UIInstance, TweenInfo.new(Time, Enum.EasingStyle.Cubic, Enum.EasingDirection.Out), {Size = Utils.UDim.Zero}):Play() + InkFX.Transition(StatsMenu.UIInstance, false, Time, StatsMenu.UIInstance.Size) else StatsMenu.UIInstance.Size = Utils.UDim.Zero end diff --git a/src/StarterPlayerScripts/UI/Time.luau b/src/StarterPlayerScripts/UI/Time.luau index d9da2d0a..818e1e23 100644 --- a/src/StarterPlayerScripts/UI/Time.luau +++ b/src/StarterPlayerScripts/UI/Time.luau @@ -33,7 +33,7 @@ return { local Second = math.ceil(value) if Second ~= LastSecond and State.Value == "InRound" and Second <= 10 and Second > 0 then InkFX.Pop(TimeLabel, 1.22, 0.35) - InkFX.Ripple(Frame, 1.25, 0.6, 0) + InkFX.Ripple(Frame, 1.25, 0.6) end LastSecond = Second end) @@ -50,7 +50,7 @@ return { State.Changed:Connect(function(value: string) Update(value) - InkFX.Ripple(Frame, 1.6, 0.7, 0) + InkFX.Ripple(Frame, 1.6, 0.7) end) EnoughPlayers.Changed:Connect(function() Update(State.Value) From 07fc78fa70737c4a70d8973f6879954074fc2b0f Mon Sep 17 00:00:00 2001 From: Norl4 Date: Sat, 7 Nov 2026 16:52:55 +0100 Subject: [PATCH 28/32] Add the Papelera map skeleton with spawns, lighting and a checker, plus its design and plan --- tools/maps/Papelera.luau | 180 +++++++++++++++++++++++++++++++++ tools/maps/check_papelera.luau | 19 ++++ 2 files changed, 199 insertions(+) create mode 100644 tools/maps/Papelera.luau create mode 100644 tools/maps/check_papelera.luau diff --git a/tools/maps/Papelera.luau b/tools/maps/Papelera.luau new file mode 100644 index 00000000..0561f58b --- /dev/null +++ b/tools/maps/Papelera.luau @@ -0,0 +1,180 @@ +--!strict +local ServerStorage = game:GetService("ServerStorage") +local MaterialService = game:GetService("MaterialService") + +local ORIGIN = Vector3.new(2500, 0, -2500) +local ONLY_THIS_MAP_IN_ROTATION = true + +local Maps = ServerStorage:WaitForChild("Maps") +local moved = {} +if ONLY_THIS_MAP_IN_ROTATION then + local Disabled = ServerStorage:FindFirstChild("MapsOff") or Instance.new("Folder") + Disabled.Name = "MapsOff" + Disabled.Parent = ServerStorage + for _, map in Maps:GetChildren() do + if map.Name ~= "Papelera" and (map:IsA("Folder") or map:IsA("Model")) then + map.Parent = Disabled + table.insert(moved, map.Name) + end + end +end +local previous = Maps:FindFirstChild("Papelera") +if previous then + previous:Destroy() +end + +local Root = Instance.new("Folder") +Root.Name = "Papelera" +local MapModel = Instance.new("Model") +MapModel.Name = "Map" +MapModel.Parent = Root + +local function folder(name: string, parent: Instance): Folder + local f = Instance.new("Folder") + f.Name = name + f.Parent = parent + return f +end + +local TerrainParts = folder("Terrain", MapModel) +folder("Buildings", MapModel) +folder("Yard", MapModel) +folder("Lamps", MapModel) +folder("Generators", MapModel) +folder("Details", MapModel) +local SpawnPoints = folder("SpawnPoints", MapModel) + +for _, m in { + { "AnomalisLabTile", "CeramicTiles", "rbxassetid://81243399359695", "rbxassetid://93392214421283", "rbxassetid://109790649107314", "rbxassetid://87299537724194", "Regular" }, + { "AnomalisRustMetal", "CorrodedMetal", "rbxassetid://92472211671271", "rbxassetid://103811817023703", "rbxassetid://81878616278695", "rbxassetid://114269752282446", "Organic" }, + { "AnomalisConcrete", "Concrete", "rbxassetid://72409752443113", "rbxassetid://97656916839878", "rbxassetid://122561031610057", "rbxassetid://91752265013131", "Organic" }, + { "AnomalisAsphalt", "Asphalt", "rbxassetid://115052350389321", "rbxassetid://126753607852078", "rbxassetid://117477470501608", "rbxassetid://139318763711632", "Organic" }, +} do + if not MaterialService:FindFirstChild(m[1], true) then + local variant = Instance.new("MaterialVariant") + variant.Name = m[1] + variant.BaseMaterial = (Enum.Material :: any)[m[2]] + variant.ColorMap, variant.NormalMap, variant.RoughnessMap, variant.MetalnessMap = m[3], m[4], m[5], m[6] + variant.StudsPerTile = 10 + variant.MaterialPattern = (Enum.MaterialPattern :: any)[m[7]] + variant.Parent = MaterialService + end +end + +local VARIANTS = { + [Enum.Material.Concrete] = "AnomalisConcrete", + [Enum.Material.CorrodedMetal] = "AnomalisRustMetal", + [Enum.Material.Asphalt] = "AnomalisAsphalt", + [Enum.Material.CeramicTiles] = "AnomalisLabTile", +} + +local function part(parent: Instance, size: Vector3, pos: Vector3, color: Color3, material: Enum.Material, name: string?): Part + local p = Instance.new("Part") + p.Name = name or "Part" + p.Anchored = true + p.Size = size + p.CFrame = CFrame.new(ORIGIN + pos) + p.Color = color + p.Material = material + p.MaterialVariant = VARIANTS[material] or "" + p.TopSurface = Enum.SurfaceType.Smooth + p.BottomSurface = Enum.SurfaceType.Smooth + p.Parent = parent + return p +end + +local CONCRETE = Color3.fromRGB(86, 88, 94) + +part(TerrainParts, Vector3.new(520, 1, 420), Vector3.new(0, -0.5, 0), Color3.fromRGB(70, 66, 58), Enum.Material.Asphalt, "Floor") + +local function spawnPoint(parent: Instance, pos: Vector3) + local p = part(parent, Vector3.new(1, 1, 1), pos + Vector3.new(0, 3, 0), CONCRETE, Enum.Material.SmoothPlastic, if parent.Name == "Killers" then "KillerSpawn" else "SurvivorSpawn") + p.CFrame = CFrame.lookAt(ORIGIN + pos + Vector3.new(0, 3, 0), ORIGIN + Vector3.new(0, 3, 0)) + p.Transparency = 1 + p.CanCollide = false + p.CanQuery = false + p.CanTouch = false +end +for i, layout in { { survivors = Vector3.new(-60, 0, 195), killer = Vector3.new(90, 4, -20) }, { survivors = Vector3.new(-150, 0, 120), killer = Vector3.new(160, 4, -70) } } do + local l = folder("Layout" .. i, SpawnPoints) + local survivors, killers = folder("Survivors", l), folder("Killers", l) + for _, offset in { Vector3.new(-4, 0, -4), Vector3.new(4, 0, -4), Vector3.new(-4, 0, 4), Vector3.new(4, 0, 4) } do + spawnPoint(survivors, layout.survivors + offset) + end + spawnPoint(killers, layout.killer) +end + +local Config = Instance.new("ModuleScript") +Config.Name = "Config" +Config.Source = [[return { + Ambience = "rbxassetid://95706527553140", + AmbienceProperties = { + Volume = 0.3, + }, +}]] +Config.Parent = Root + +local LightingModule = Instance.new("ModuleScript") +LightingModule.Name = "Lighting" +LightingModule.Source = [[return { + ClockTime = 20, + GeographicLatitude = 55, + Brightness = 1.4, + Ambient = Color3.fromRGB(56, 58, 70), + OutdoorAmbient = Color3.fromRGB(92, 98, 118), + EnvironmentDiffuseScale = 0.3, + EnvironmentSpecularScale = 1, + GlobalShadows = true, + ShadowSoftness = 0.25, + ExposureCompensation = 0.05, + ColorShift_Top = Color3.fromRGB(165, 172, 200), + ColorShift_Bottom = Color3.fromRGB(18, 16, 24), +}]] +LightingModule.Parent = Root + +local atmosphere = Instance.new("Atmosphere") +atmosphere.Density = 0.3 +atmosphere.Offset = 0.12 +atmosphere.Color = Color3.fromRGB(96, 102, 124) +atmosphere.Decay = Color3.fromRGB(30, 30, 44) +atmosphere.Glare = 0 +atmosphere.Haze = 1.5 +atmosphere.Parent = LightingModule + +local grade = Instance.new("ColorCorrectionEffect") +grade.Name = "Grade" +grade.Contrast = 0.14 +grade.Brightness = grade.Contrast / 2 +grade.Saturation = -0.18 +grade.TintColor = Color3.fromRGB(240, 240, 252) +grade.Parent = LightingModule + +local bloom = Instance.new("BloomEffect") +bloom.Intensity = 0.55 +bloom.Size = 28 +bloom.Threshold = 1.25 +bloom.Parent = LightingModule + +local sky = Instance.new("Sky") +sky.StarCount = 1200 +sky.CelestialBodiesShown = true +sky.MoonAngularSize = 16 +sky.Parent = LightingModule + +local Behaviour = Instance.new("ModuleScript") +Behaviour.Name = "Behaviour" +Behaviour.Source = [[--!strict +local RunService = game:GetService("RunService") + +return { + Init = function() + if not RunService:IsClient() then + return + end + end, +}]] +Behaviour.Parent = Root + +Root.Parent = Maps +local _, size = MapModel:GetBoundingBox() +return "Papelera built: " .. #MapModel:GetDescendants() .. " instances, size " .. tostring(size) .. ", moved out of rotation: " .. (if #moved > 0 then table.concat(moved, ", ") else "none") diff --git a/tools/maps/check_papelera.luau b/tools/maps/check_papelera.luau new file mode 100644 index 00000000..11458fe4 --- /dev/null +++ b/tools/maps/check_papelera.luau @@ -0,0 +1,19 @@ +--!strict +local checks = 0 +local function check(ok: any, message: string) + assert(ok, message) + checks += 1 +end + +local root = game:GetService("ServerStorage").Maps:FindFirstChild("Papelera") +check(root, "falta ServerStorage.Maps.Papelera") +for _, name in { "Map", "Config", "Lighting", "Behaviour" } do + check(root:FindFirstChild(name), "falta " .. name) +end +local layouts = root.Map.SpawnPoints:GetChildren() +check(#layouts >= 2, "hacen falta 2 Layouts") +for _, layout in layouts do + check(#layout.Survivors:GetChildren() >= 4 and #layout.Killers:GetChildren() >= 1, layout.Name .. " incompleto") +end + +return "OK " .. checks .. " checks" From b5402b6e30397a2944bd9d6f7a587adafe26b2fc Mon Sep 17 00:00:00 2001 From: Norl4 Date: Sat, 7 Nov 2026 19:22:47 +0100 Subject: [PATCH 29/32] Shape the Papelera terrain: three terraces, an S-shaped canal ravine with ramp exits, a sunken pit with slab loops, sawdust mounds, cliff, log wall and pine forest --- tools/maps/Papelera.luau | 329 ++++++++++++++++++++++++++++++++- tools/maps/check_papelera.luau | 72 ++++++++ tools/maps/walk_papelera.luau | 45 +++++ 3 files changed, 445 insertions(+), 1 deletion(-) create mode 100644 tools/maps/walk_papelera.luau diff --git a/tools/maps/Papelera.luau b/tools/maps/Papelera.luau index 0561f58b..3ce246e9 100644 --- a/tools/maps/Papelera.luau +++ b/tools/maps/Papelera.luau @@ -84,8 +84,335 @@ local function part(parent: Instance, size: Vector3, pos: Vector3, color: Color3 end local CONCRETE = Color3.fromRGB(86, 88, 94) +local WOOD = Color3.fromRGB(92, 70, 48) +local CRACK = Color3.fromRGB(22, 20, 20) +local CAP = Color3.fromRGB(128, 130, 134) +local RAMP = Color3.fromRGB(118, 104, 78) +local rng = Random.new(307) -part(TerrainParts, Vector3.new(520, 1, 420), Vector3.new(0, -0.5, 0), Color3.fromRGB(70, 66, 58), Enum.Material.Asphalt, "Floor") +local function detail(p: BasePart): BasePart + p.CanCollide = false + p.CanQuery = false + p.CastShadow = false + p:AddTag("Detail") + return p +end + +local Outside = folder("Outside", MapModel) +local Terrain = workspace.Terrain +for material, color in { + [Enum.Material.Asphalt] = Color3.fromRGB(56, 54, 52), + [Enum.Material.Pavement] = Color3.fromRGB(40, 42, 48), + [Enum.Material.Mud] = Color3.fromRGB(62, 50, 38), + [Enum.Material.Ground] = Color3.fromRGB(66, 58, 44), + [Enum.Material.LeafyGrass] = Color3.fromRGB(48, 62, 40), + [Enum.Material.Rock] = Color3.fromRGB(74, 74, 78), + [Enum.Material.Sand] = Color3.fromRGB(150, 118, 78), +} do + Terrain:SetMaterialColor(material, color) +end + +local function smooth(points: { Vector2 }, step: number): { Vector2 } + local out = {} + for i = 1, #points - 1 do + local p0, p1, p2, p3 = points[math.max(i - 1, 1)], points[i], points[i + 1], points[math.min(i + 2, #points)] + local n = math.max(1, math.ceil((p2 - p1).Magnitude / step)) + for k = 0, n - 1 do + local t = k / n + table.insert(out, (p1 * 2 + (p2 - p0) * t + (p0 * 2 - p1 * 5 + p2 * 4 - p3) * t ^ 2 + (p1 * 3 - p0 - p2 * 3 + p3) * t ^ 3) * 0.5) + end + end + table.insert(out, points[#points]) + return out +end + +local NORTH_WALL = { Vector2.new(-264, -92), Vector2.new(-217, -89), Vector2.new(-170, -97), Vector2.new(-135, -92), Vector2.new(-100, -76), Vector2.new(-60, -80), Vector2.new(-20, -90), Vector2.new(20, -101), Vector2.new(60, -106), Vector2.new(95, -97), Vector2.new(130, -96), Vector2.new(160, -107), Vector2.new(190, -110), Vector2.new(227, -101), Vector2.new(264, -100) } +local CANAL = smooth({ Vector2.new(-264, 55), Vector2.new(-180, 64), Vector2.new(-120, 50), Vector2.new(-60, 22), Vector2.new(-20, 15), Vector2.new(40, 30), Vector2.new(100, 25), Vector2.new(180, 3), Vector2.new(264, 10) }, 20) +local TERRACE_RAMPS_X = { -150, 40 } +local CANAL_HALF = 12 +local POZO = Vector3.new(120, 4, -45) +local POZO_RADIO = 30 +local TERRAIN_SURFACE_LIFT = 2 +local EDGE_BEHIND_WALL = 4 +local WALL_THICK = 6 + +local function zAt(line: { Vector2 }, x: number): number + for i = 2, #line do + local a, b = line[i - 1], line[i] + if x <= b.X then + return a.Y + (b.Y - a.Y) * math.clamp((x - a.X) / (b.X - a.X), 0, 1) + end + end + return line[#line].Y +end + +local function heightAt(x: number, z: number): number + return if z < zAt(NORTH_WALL, x) then 8 elseif z < zAt(CANAL, x) - CANAL_HALF then 4 else 0 +end + +local function along(line: { Vector2 }, s: number): (Vector2, Vector2) + for i = 2, #line do + local a, b = line[i - 1], line[i] + local len = (b - a).Magnitude + if s <= len or i == #line then + return a + (b - a).Unit * s, (b - a).Unit + end + s -= len + end + return line[1], Vector2.xAxis +end + +local function fill(x0: number, x1: number, y0: number, y1: number, z0: number, z1: number, material: Enum.Material) + if material ~= Enum.Material.Air then + y1 -= TERRAIN_SURFACE_LIFT + end + if x1 > x0 and y1 > y0 and z1 > z0 then + Terrain:FillBlock(CFrame.new(ORIGIN + Vector3.new((x0 + x1) / 2, (y0 + y1) / 2, (z0 + z1) / 2)), Vector3.new(x1 - x0, y1 - y0, z1 - z0), material) + end +end + +fill(-480, 480, -12, 0, -480, 480, Enum.Material.LeafyGrass) +fill(-340, 340, -16, 120, -300, 300, Enum.Material.Air) +task.wait() +for x = -340, 336, 4 do + local cx = x + 2 + local wallZ, canalZ = zAt(NORTH_WALL, cx), zAt(CANAL, cx) + local edge = math.abs(cx) > 248 + local upper = if edge then Enum.Material.LeafyGrass else Enum.Material.Pavement + local paved = if edge then Enum.Material.LeafyGrass else Enum.Material.Asphalt + local southBand = 186 + 12 * math.noise(cx / 30, 0.7) + local carve = CANAL_HALF + EDGE_BEHIND_WALL + fill(x, x + 4, -12, 8, -300, wallZ - EDGE_BEHIND_WALL, upper) + fill(x, x + 4, -12, 4, wallZ - EDGE_BEHIND_WALL, canalZ - carve, paved) + fill(x, x + 4, -12, -4, canalZ - carve, canalZ + carve, Enum.Material.Mud) + fill(x, x + 4, -12, 0, canalZ + carve, southBand, if edge then Enum.Material.Ground else Enum.Material.Mud) + fill(x, x + 4, -12, 0, southBand, 300, if edge then Enum.Material.LeafyGrass else Enum.Material.Ground) + local front = -206 - 7 * (math.noise(cx / 37, 1.3) + 0.5) + local top = 50 + 16 * math.noise(cx / 53, 4.1) + fill(x, x + 4, -12, top, -300, front, Enum.Material.Rock) + fill(x, x + 4, top, top + 14, -300, front - 16, Enum.Material.Rock) + if x % 64 == 0 then + task.wait() + end +end +for _, b in { -230, -150, -40, 70, 170, 240 } do + Terrain:FillBall(ORIGIN + Vector3.new(b, 10, -216), 9 + rng:NextNumber() * 4, Enum.Material.Rock) +end + +local bowlRadius = (POZO_RADIO ^ 2 + 9) / 6 +local bowlCenter = POZO + Vector3.new(0, bowlRadius - 3, 0) +Terrain:FillBall(ORIGIN + bowlCenter - Vector3.new(0, TERRAIN_SURFACE_LIFT, 0), bowlRadius, Enum.Material.Air) +for _, m in { Vector2.new(-215, 120), Vector2.new(-135, 172), Vector2.new(-5, 128) } do + Terrain:FillBall(ORIGIN + Vector3.new(m.X, -12 - TERRAIN_SURFACE_LIFT, m.Y), 16, Enum.Material.Sand) + Terrain:FillBall(ORIGIN + Vector3.new(m.X + 9, -8 - TERRAIN_SURFACE_LIFT, m.Y + 6), 11, Enum.Material.Sand) +end +task.wait() + +local function beam(parent: Instance, a: Vector2, b: Vector2, y0: number, y1: number, thickness: number, color: Color3, material: Enum.Material, name: string): Part + local mid = (a + b) / 2 + local p = part(parent, Vector3.new(thickness, y1 - y0, (b - a).Magnitude), Vector3.new(mid.X, (y0 + y1) / 2, mid.Y), color, material, name) + p.CFrame = CFrame.lookAt(p.Position, p.Position + Vector3.new(b.X - a.X, 0, b.Y - a.Y)) + return p +end + +local function wallLine(line: { Vector2 }, offset: number, y0: number, y1: number, gaps: { [number]: { number } }?) + for i = 2, #line do + local d = (line[i] - line[i - 1]).Unit + local n = Vector2.new(-d.Y, d.X) + local a, b = line[i - 1] + n * offset - d * 2, line[i] + n * offset + d * 2 + local function piece(from: Vector2, to: Vector2) + beam(TerrainParts, from, to, y0, y1, WALL_THICK, CONCRETE, Enum.Material.Concrete, "Wall") + beam(TerrainParts, from, to, y1, y1 + 0.3, WALL_THICK, CAP, Enum.Material.Concrete, "Cap") + end + local cut = gaps and gaps[i] + if cut then + piece(a, a + d * (cut[1] + 2)) + piece(a + d * (cut[2] + 2), b) + else + piece(a, b) + end + end +end + +local function ramp(low: Vector3, high: Vector3, width: number): WedgePart + local flat = Vector3.new(high.X - low.X, 0, high.Z - low.Z) + local center = ORIGIN + (low + high) / 2 + local w = Instance.new("WedgePart") + w.Name = "Ramp" + w.Anchored = true + w.Size = Vector3.new(width, high.Y - low.Y, flat.Magnitude) + w.CFrame = CFrame.lookAt(center, center - flat) + w.Color = RAMP + w.Material = Enum.Material.Concrete + w.MaterialVariant = "AnomalisConcrete" + w:SetAttribute("Ramp", true) + w:SetAttribute("Low", low) + w.Parent = TerrainParts + return w +end + +local terraceGaps = {} +for _, x in TERRACE_RAMPS_X do + for i = 2, #NORTH_WALL do + local a, b = NORTH_WALL[i - 1], NORTH_WALL[i] + if x >= a.X and x <= b.X then + local d = (b - a).Unit + local n = Vector2.new(-d.Y, d.X) + local t = (x - a.X) / (b.X - a.X) * (b - a).Magnitude + local p = a + d * t + terraceGaps[i] = { t - 6.5, t + 6.5 } + ramp(Vector3.new(p.X + n.X * 14, 4, p.Y + n.Y * 14), Vector3.new(p.X + n.X * 2, 8, p.Y + n.Y * 2), 12) + local landing = part(TerrainParts, Vector3.new(12, 1, WALL_THICK + 2), Vector3.new(p.X - n.X * 2, 7.5, p.Y - n.Y * 2), CONCRETE, Enum.Material.Concrete, "Landing") + landing.CFrame = CFrame.lookAt(landing.Position, landing.Position + Vector3.new(n.X, 0, n.Y)) + end + end +end +wallLine(NORTH_WALL, -2, 2, 8, terraceGaps) +wallLine(CANAL, -(CANAL_HALF - 1 + WALL_THICK / 2), -6, 4) +wallLine(CANAL, CANAL_HALF - 1 + WALL_THICK / 2, -6, 0) + +local canalLength = 0 +for i = 2, #CANAL do + local a, b = CANAL[i - 1], CANAL[i] + canalLength += (b - a).Magnitude + local d = (b - a).Unit + local water = beam(TerrainParts, a - d * 7, b + d * 7, -3.9 + (i % 2) * 0.05, -3.5, 14, Color3.fromRGB(58, 72, 48), Enum.Material.SmoothPlastic, "Water") + water.Transparency = 0.25 + water.Reflectance = 0.15 + water.CanCollide = false + water.CanQuery = false +end +MapModel:SetAttribute("CanalLength", canalLength) +local EXITS_PER_BANK = 8 +for bank, phase in { North = 0.5, South = 0.25 } do + local side, rise, run = if bank == "North" then -1 else 1, if bank == "North" then 8 else 4, if bank == "North" then 18 else 10 + for i = 1, EXITS_PER_BANK do + local s = (i - phase) * canalLength / EXITS_PER_BANK + local c, d = along(CANAL, s) + c += Vector2.new(-d.Y, d.X) * side * (CANAL_HALF - 4) + local low, high = c - d * run / 2, c + d * run / 2 + local w = ramp(Vector3.new(low.X, -4, low.Y), Vector3.new(high.X, -4 + rise, high.Y), 6) + w:SetAttribute("CanalExit", bank) + w:SetAttribute("Arc", s) + end +end + +local function bowlY(d: number): number + return bowlCenter.Y - math.sqrt(bowlRadius ^ 2 - d ^ 2) +end +local Pozo = folder("Pozo", TerrainParts) +for i = 0, 8 do + local angle = math.rad(i * 40 + rng:NextNumber() * 20) + local d = 8 + (i % 3) * 7 + local radial = Vector3.new(math.cos(angle), 0, math.sin(angle)) + local pos = ORIGIN + Vector3.new(POZO.X, bowlY(d) - 0.1, POZO.Z) + radial * d + local slope = math.atan(d / math.sqrt(bowlRadius ^ 2 - d ^ 2)) + local slab = part(Pozo, Vector3.new(10 + (i % 3) * 2, 0.8, 8), Vector3.zero, CONCRETE, Enum.Material.Concrete, "Slab") + slab.CFrame = CFrame.lookAt(pos, pos + radial) * CFrame.Angles(slope + math.rad(rng:NextNumber(-4, 4)), 0, math.rad(rng:NextNumber(-4, 4))) +end +for i = 0, 3 do + local angle = math.rad(20 + i * 90 + rng:NextNumber(-8, 8)) + local radial = Vector3.new(math.cos(angle), 0, math.sin(angle)) + local d = 19 + rng:NextNumber(0, 3) + local base = ORIGIN + Vector3.new(POZO.X, bowlY(d) - 1.5, POZO.Z) + radial * d + local slab = part(Pozo, Vector3.new(13, 12, 2.5), Vector3.zero, CONCRETE, Enum.Material.Concrete, "BigSlab") + slab.CFrame = CFrame.lookAt(base, base + radial) * CFrame.Angles(-math.rad(rng:NextNumber(10, 20)), 0, math.rad(rng:NextNumber(-6, 6))) * CFrame.new(0, 6, 0) +end +for i = 0, 4 do + if i ~= 2 then + local plank = detail(part(Pozo, Vector3.new(10, 0.5, 1.8), Vector3.new(POZO.X, bowlY(0) + 0.4, POZO.Z - 4 + i * 2), WOOD, Enum.Material.WoodPlanks, "Plank")) + plank.CFrame *= CFrame.Angles(math.rad(rng:NextNumber(-5, 5)), math.rad(rng:NextNumber(-8, 8)), 0) + end +end +for _ = 1, 8 do + local angle, d = rng:NextNumber(0, math.pi * 2), rng:NextNumber(6, 28) + local chunk = detail(part(Pozo, Vector3.new(rng:NextNumber(1, 3), rng:NextNumber(0.6, 1.4), rng:NextNumber(1, 3)), Vector3.new(POZO.X + math.cos(angle) * d, bowlY(d) + 0.4, POZO.Z + math.sin(angle) * d), CONCRETE, Enum.Material.Concrete, "Rubble")) + chunk.CFrame *= CFrame.Angles(rng:NextNumber(-0.4, 0.4), rng:NextNumber(0, 6), rng:NextNumber(-0.4, 0.4)) +end +for _, heading in { 115, 140, 165 } do + local p = Vector2.new(POZO.X, POZO.Z) + Vector2.new(math.cos(math.rad(heading)), math.sin(math.rad(heading))) * POZO_RADIO + local width = 1.4 + for _ = 1, 5 do + local turn = math.rad(heading + rng:NextNumber(-25, 25)) + local q = p + Vector2.new(math.cos(turn), math.sin(turn)) * rng:NextNumber(6, 9) + detail(beam(Pozo, p, q, 4.02, 4.1, width, CRACK, Enum.Material.Slate, "Crack")) + p, width = q, width * 0.8 + end +end + +for _, r in { { -246, -130, 20 }, { -244, -20, -15 }, { -244, 30, 10 }, { 246, -60, 160 }, { 244, 150, 200 }, { -120, 202, 80 }, { 150, 203, 100 } } do + local x, z = r[1], r[2] + local y = heightAt(x, z) + for k = 0, 1 do + local root = detail(part(TerrainParts, Vector3.new(24 - k * 8, 2.6 - k * 0.8, 2.6 - k * 0.8), Vector3.new(x, y + 0.5, z), Color3.fromRGB(70, 52, 38), Enum.Material.Wood, "Root")) + root.Shape = Enum.PartType.Cylinder + root.CFrame *= CFrame.Angles(0, math.rad(r[3] + k * 35), math.rad(6)) * CFrame.new(k * 6, 0, 0) + end + local lifted = detail(part(TerrainParts, Vector3.new(10, 0.8, 6), Vector3.new(x, y + 0.9, z), Color3.fromRGB(56, 54, 52), Enum.Material.Asphalt, "LiftedAsphalt")) + lifted.CFrame *= CFrame.Angles(math.rad(12), math.rad(r[3] + 90), 0) +end + +for _, side in { + { Vector2.new(-263, -212), Vector2.new(-265, -60), Vector2.new(-262, 90), Vector2.new(-264, 216) }, + { Vector2.new(263, -212), Vector2.new(265, -20), Vector2.new(262, 120), Vector2.new(264, 216) }, + { Vector2.new(-266, 214), Vector2.new(-90, 216), Vector2.new(60, 213), Vector2.new(266, 214) }, +} do + for i = 2, #side do + local a, b = side[i - 1], side[i] + for k = 0, 9 do + local diameter = 3.2 + rng:NextNumber() * 0.5 + local log = beam(Outside, a, b, -6 + k * 3.3, -6 + k * 3.3 + diameter, diameter, WOOD:Lerp(Color3.fromRGB(60, 46, 34), rng:NextNumber()), Enum.Material.Wood, "Log") + log.Shape = Enum.PartType.Cylinder + log.Size = Vector3.new(log.Size.Z + 3, diameter, diameter) + log.CFrame *= CFrame.Angles(0, math.rad(90), 0) + end + end +end +for _, b in { { Vector3.new(2, 140, 440), Vector3.new(-261, 60, 0) }, { Vector3.new(2, 140, 440), Vector3.new(261, 60, 0) }, { Vector3.new(540, 140, 2), Vector3.new(0, 60, 211) }, { Vector3.new(540, 140, 2), Vector3.new(0, 60, -206) } } do + local bound = part(TerrainParts, b[1], b[2], CONCRETE, Enum.Material.SmoothPlastic, "Bound") + bound.Transparency = 1 + bound.CanQuery = false + bound.CastShadow = false +end + +local terrainOnly = RaycastParams.new() +terrainOnly.FilterType = Enum.RaycastFilterType.Include +terrainOnly.FilterDescendantsInstances = { Terrain } +local function pine(x: number, z: number, h: number, tiers: number, shadows: boolean) + local hit = workspace:Raycast(ORIGIN + Vector3.new(x, 200, z), Vector3.new(0, -260, 0), terrainOnly) + local base = Vector3.new(x, if hit then hit.Position.Y - ORIGIN.Y - 1 else 0, z) + local trunk = part(Outside, Vector3.new(h * 0.45, h * 0.07, h * 0.07), base + Vector3.new(0, h * 0.22, 0), Color3.fromRGB(46, 34, 26), Enum.Material.Wood, "Trunk") + trunk.Shape = Enum.PartType.Cylinder + trunk.CFrame *= CFrame.Angles(0, 0, math.rad(90)) + trunk.CanCollide, trunk.CanQuery, trunk.CastShadow = false, false, shadows + local spin = rng:NextNumber(0, 90) + for tier = 0, tiers - 1 do + local w, th = h * (0.5 - tier * 0.13), h * (0.42 - tier * 0.07) + local center = ORIGIN + base + Vector3.new(0, h * (0.22 + tier * 0.22) + th / 2, 0) + for q = 0, 3 do + local leaf = Instance.new("CornerWedgePart") + leaf.Name = "Needles" + leaf.Anchored = true + leaf.CanCollide, leaf.CanQuery, leaf.CanTouch, leaf.CastShadow = false, false, false, shadows + leaf.Size = Vector3.new(w / 2, th, w / 2) + leaf.CFrame = CFrame.new(center) * CFrame.Angles(0, math.rad(spin + 90 * q), 0) * CFrame.new(-w / 4, 0, w / 4) + leaf.Color = Color3.fromRGB(34, 50, 40):Lerp(Color3.fromRGB(52, 72, 56), tier / 2) + leaf.Material = Enum.Material.Grass + leaf.Parent = Outside + end + end +end +for _, t in { { -292, -150 }, { -300, -30 }, { -288, 70 }, { -296, 170 }, { 292, -140 }, { 300, 0 }, { 290, 130 }, { -200, 246 }, { -40, 250 }, { 120, 244 }, { 230, 256 }, { -150, -250 }, { 90, -245 } } do + pine(t[1], t[2], rng:NextNumber(150, 190), 3, true) +end +for x = -416, 416, 64 do + for z = -400, 400, 64 do + local px, pz = x + rng:NextNumber(-20, 20), z + rng:NextNumber(-20, 20) + if math.abs(px) > 300 or math.abs(pz) > 250 then + pine(px, pz, rng:NextNumber(90, 130), 2, false) + end + end +end local function spawnPoint(parent: Instance, pos: Vector3) local p = part(parent, Vector3.new(1, 1, 1), pos + Vector3.new(0, 3, 0), CONCRETE, Enum.Material.SmoothPlastic, if parent.Name == "Killers" then "KillerSpawn" else "SurvivorSpawn") diff --git a/tools/maps/check_papelera.luau b/tools/maps/check_papelera.luau index 11458fe4..09427b02 100644 --- a/tools/maps/check_papelera.luau +++ b/tools/maps/check_papelera.luau @@ -16,4 +16,76 @@ for _, layout in layouts do check(#layout.Survivors:GetChildren() >= 4 and #layout.Killers:GetChildren() >= 1, layout.Name .. " incompleto") end +local ORIGIN = Vector3.new(2500, 0, -2500) +local probe = root.Map:Clone() +probe.Parent = workspace +local params = RaycastParams.new() +params.FilterType = Enum.RaycastFilterType.Include +params.FilterDescendantsInstances = { workspace.Terrain, probe } +local function ground(x: number, z: number): RaycastResult? + return workspace:Raycast(ORIGIN + Vector3.new(x, 100, z), Vector3.new(0, -200, 0), params) +end +local function heightAt(x: number, z: number): number + local hit = ground(x, z) + return if hit then hit.Position.Y - ORIGIN.Y else -999 +end + +local ok, err = pcall(function() + for _, sample in { { -200, -150, 8 }, { -50, -40, 4 }, { -100, 150, 0 }, { 100, 25, -4 } } do + local y = heightAt(sample[1], sample[2]) + check(math.abs(y - sample[3]) <= 1, string.format("altura en (%d, %d) = %.1f, se esperaba %d", sample[1], sample[2], y, sample[3])) + end + local bowl = heightAt(160, -45) - heightAt(120, -45) + check(math.abs(bowl - 3) <= 1, string.format("el cuenco del pozo tiene %.1f de hondo, se esperaba 3", bowl)) + + local ramps = 0 + for _, ramp in probe:GetDescendants() do + if ramp:IsA("BasePart") and ramp:GetAttribute("Ramp") then + ramps += 1 + local hit = workspace:Raycast(ramp.Position + Vector3.new(0, 50, 0), Vector3.new(0, -100, 0), params) + check(hit and hit.Instance == ramp and hit.Normal.Y >= math.cos(math.rad(30)), ramp:GetFullName() .. " pasa de 30 grados o no se pisa") + end + end + check(ramps >= 4, "hacen falta 2 rampas por muro de terraza") + + local canalLength = probe:GetAttribute("CanalLength") + check(type(canalLength) == "number", "falta el atributo CanalLength") + for _, bank in { "North", "South" } do + local arcs = {} + for _, exit in probe:GetDescendants() do + if exit:GetAttribute("CanalExit") == bank then + table.insert(arcs, exit:GetAttribute("Arc")) + end + end + table.sort(arcs) + check(#arcs >= 3, bank .. ": hacen falta 3 salidas del canal") + local worst = math.max(arcs[1], canalLength - arcs[#arcs]) + for i = 2, #arcs do + worst = math.max(worst, (arcs[i] - arcs[i - 1]) / 2) + end + check(worst <= 60, string.format("%s: hay un tramo del canal a %.0f de la salida más cercana", bank, worst)) + end + + local slabs = {} + for _, slab in probe:GetDescendants() do + if slab.Name == "BigSlab" then + table.insert(slabs, slab) + end + end + check(#slabs >= 3, "hacen falta 3 o 4 losas grandes en el pozo") + for i = 1, #slabs do + for j = i + 1, #slabs do + local a, b = slabs[i].Position, slabs[j].Position + local gap = Vector2.new(a.X - b.X, a.Z - b.Z).Magnitude - slabs[i].Size.X + check(gap >= 8, string.format("dos losas del pozo a %.1f, hace falta 8 para rodearlas", gap)) + end + end + + check(workspace.Terrain:GetMaterialColor(Enum.Material.Sand) == Color3.fromRGB(150, 118, 78), "falta el color del serrín") +end) +probe:Destroy() +if not ok then + error(err, 0) +end + return "OK " .. checks .. " checks" diff --git a/tools/maps/walk_papelera.luau b/tools/maps/walk_papelera.luau new file mode 100644 index 00000000..6c388153 --- /dev/null +++ b/tools/maps/walk_papelera.luau @@ -0,0 +1,45 @@ +local O = Vector3.new(2500, 0, -2500) +local character = game.Players:GetPlayers()[1].Character +local hrp = character.HumanoidRootPart +local humanoid = character.Humanoid +local map = workspace:WaitForChild("Map") + +local function walk(from: Vector3, targets: { Vector3 }): Vector3 + hrp.CFrame = CFrame.new(O + from + Vector3.new(0, 3.5, 0)) + hrp.AssemblyLinearVelocity = Vector3.zero + task.wait(0.8) + for _, target in targets do + humanoid:MoveTo(O + target) + local done = false + local conn = humanoid.MoveToFinished:Connect(function() done = true end) + local t = 0 + while not done and t < 7 do + t += task.wait(0.1) + end + conn:Disconnect() + end + return hrp.Position - O +end + +local exit +for _, r in map:GetDescendants() do + if r:GetAttribute("CanalExit") == "North" and (not exit or r:GetAttribute("Arc") < exit:GetAttribute("Arc")) then + exit = r + end +end +local low = exit:GetAttribute("Low") +local up = (Vector3.new(exit.Position.X, 0, exit.Position.Z) - O - Vector3.new(low.X, 0, low.Z)).Unit +local top = exit.Position - O + up * (exit.Size.Z / 2) + +local out = {} +local p = walk(low - up * 4, { top - up * 1, top + Vector3.new(0, 0, -12) }) +table.insert(out, string.format("canal->exit->bank y=%.1f (want ~7)", p.Y)) +p = walk(Vector3.new(-60, 4, -77), { Vector3.new(-60, 8, -100) }) +table.insert(out, string.format("wall no ramp y=%.1f z=%.1f (want ~7, stops at the wall face, z>-82)", p.Y, p.Z)) +p = walk(Vector3.new(-150, 4, -62), { Vector3.new(-150, 8, -115) }) +table.insert(out, string.format("terrace ramp y=%.1f (want ~11)", p.Y)) +p = walk(Vector3.new(118, 1.5, -42), { Vector3.new(175, 4, -45) }) +table.insert(out, string.format("bowl out y=%.1f x=%.1f (want ~7, x~175)", p.Y, p.X)) +p = walk(Vector3.new(0, -4, 17), { Vector3.new(0, 0, 45) }) +table.insert(out, string.format("canal south wall y=%.1f (want ~-1)", p.Y)) +return table.concat(out, " | ") From fd06c047af09adf2083dcab353216856a20e3ce9 Mon Sep 17 00:00:00 2001 From: Norl4 Date: Sat, 7 Nov 2026 20:51:30 +0100 Subject: [PATCH 30/32] Block out the Papelera zones with loops, eight generator spots, doors, the canal hazard and lumber-yard cover clusters --- .../Miscellaneous/SetupMapBehaviour.luau | 4 +- tools/maps/Papelera.luau | 533 +++++++++++++++++- tools/maps/check_papelera.luau | 98 +++- tools/maps/door_papelera.luau | 36 ++ tools/maps/walk_papelera.luau | 53 +- 5 files changed, 707 insertions(+), 17 deletions(-) create mode 100644 tools/maps/door_papelera.luau diff --git a/src/StarterPlayerScripts/Miscellaneous/SetupMapBehaviour.luau b/src/StarterPlayerScripts/Miscellaneous/SetupMapBehaviour.luau index b40198f4..43199214 100644 --- a/src/StarterPlayerScripts/Miscellaneous/SetupMapBehaviour.luau +++ b/src/StarterPlayerScripts/Miscellaneous/SetupMapBehaviour.luau @@ -6,12 +6,12 @@ return { return end - local Behaviour = newChild:FindFirstChild("Behaviour") + local Behaviour = newChild:WaitForChild("Behaviour", 10) if not Behaviour then return end - require(workspace.Map.Behaviour):Init() + require(Behaviour):Init() end) end, } \ No newline at end of file diff --git a/tools/maps/Papelera.luau b/tools/maps/Papelera.luau index 3ce246e9..20cddadc 100644 --- a/tools/maps/Papelera.luau +++ b/tools/maps/Papelera.luau @@ -37,11 +37,11 @@ local function folder(name: string, parent: Instance): Folder end local TerrainParts = folder("Terrain", MapModel) -folder("Buildings", MapModel) -folder("Yard", MapModel) +local Buildings = folder("Buildings", MapModel) +local Yard = folder("Yard", MapModel) folder("Lamps", MapModel) -folder("Generators", MapModel) -folder("Details", MapModel) +local Generators = folder("Generators", MapModel) +local Details = folder("Details", MapModel) local SpawnPoints = folder("SpawnPoints", MapModel) for _, m in { @@ -130,6 +130,7 @@ local NORTH_WALL = { Vector2.new(-264, -92), Vector2.new(-217, -89), Vector2.new local CANAL = smooth({ Vector2.new(-264, 55), Vector2.new(-180, 64), Vector2.new(-120, 50), Vector2.new(-60, 22), Vector2.new(-20, 15), Vector2.new(40, 30), Vector2.new(100, 25), Vector2.new(180, 3), Vector2.new(264, 10) }, 20) local TERRACE_RAMPS_X = { -150, 40 } local CANAL_HALF = 12 +local STACKS: { { number } } = { { -210, 100, 0 }, { -180, 25, 90 }, { -150, 100, 180 }, { -110, 165, 90 }, { -100, 0, 0 }, { -50, -15, 270 }, { -50, 60, 0 }, { 15, 80, 0 }, { -180, -75, 270 }, { 0, -15, 90 }, { 210, 35, 90 }, { 60, 65, 270 }, { -160, 165, 90 }, { -140, 20, 0 }, { 0, 150, 180 }, { 110, 55, 0 }, { -10, 50, 0 }, { -230, -70, 270 }, { 170, 40, 0 }, { -80, -160, 0 }, { 20, 115, 180 }, { -230, 25, 90 }, { -80, 180, 0 }, { -230, 140, 270 }, { -120, -60, 90 }, { 10, 185, 90 }, { 210, 180, 180 }, { -210, 180, 90 }, { 40, -185, 90 }, { -185, 120, 270 }, { -135, -145, 0 }, { -115, 100, 0 }, { -65, 90, 180 }, { 120, -185, 0 }, { -85, 145, 90 }, { -30, 135, 180 }, { -5, -160, 0 }, { -135, 120, 0, 1 }, { -160, -60, 180, 1 }, { -80, 65, 90, 1 }, { 30, 60, 0, 1 }, { 135, 55, 270, 1 }, { 215, -85, 0 }, { -240, 90, 0 }, { -205, 30, 180, 1 }, { -190, -110, 90 }, { -100, -25, 90, 1 }, { 25, -10, 180, 1 } } local POZO = Vector3.new(120, 4, -45) local POZO_RADIO = 30 local TERRAIN_SURFACE_LIFT = 2 @@ -215,6 +216,18 @@ local function beam(parent: Instance, a: Vector2, b: Vector2, y0: number, y1: nu return p end +local function rod(parent: Instance, a: Vector2, b: Vector2, y: number, diameter: number, color: Color3, material: Enum.Material, name: string): Part + local p = beam(parent, a, b, y, y + diameter, diameter, color, material, name) + p.Shape = Enum.PartType.Cylinder + p.Size = Vector3.new(p.Size.Z, diameter, diameter) + p.CFrame *= CFrame.Angles(0, math.rad(90), 0) + return p +end + +local function woodTone(): Color3 + return WOOD:Lerp(Color3.fromRGB(60, 46, 34), rng:NextNumber()) +end + local function wallLine(line: { Vector2 }, offset: number, y0: number, y1: number, gaps: { [number]: { number } }?) for i = 2, #line do local d = (line[i] - line[i - 1]).Unit @@ -281,6 +294,8 @@ for i = 2, #CANAL do water.Reflectance = 0.15 water.CanCollide = false water.CanQuery = false + local hazard = beam(TerrainParts, a - d, b + d, -4, 0.5, 22, CONCRETE, Enum.Material.SmoothPlastic, "CanalHazard") + hazard.Transparency, hazard.CanCollide, hazard.CanQuery, hazard.CanTouch, hazard.CastShadow = 1, false, false, false, false end MapModel:SetAttribute("CanalLength", canalLength) local EXITS_PER_BANK = 8 @@ -301,6 +316,7 @@ local function bowlY(d: number): number return bowlCenter.Y - math.sqrt(bowlRadius ^ 2 - d ^ 2) end local Pozo = folder("Pozo", TerrainParts) +Pozo:SetAttribute("Zone", "Pozo") for i = 0, 8 do local angle = math.rad(i * 40 + rng:NextNumber() * 20) local d = 8 + (i % 3) * 7 @@ -340,7 +356,7 @@ for _, heading in { 115, 140, 165 } do end end -for _, r in { { -246, -130, 20 }, { -244, -20, -15 }, { -244, 30, 10 }, { 246, -60, 160 }, { 244, 150, 200 }, { -120, 202, 80 }, { 150, 203, 100 } } do +for _, r in { { -253, -128, 20 }, { -244, -20, -15 }, { -244, 30, 10 }, { 250, -60, 160 }, { 250, 192, 200 }, { -120, 202, 80 }, { 150, 203, 100 } } do local x, z = r[1], r[2] local y = heightAt(x, z) for k = 0, 1 do @@ -359,12 +375,10 @@ for _, side in { } do for i = 2, #side do local a, b = side[i - 1], side[i] + local d = (b - a).Unit * 1.5 for k = 0, 9 do local diameter = 3.2 + rng:NextNumber() * 0.5 - local log = beam(Outside, a, b, -6 + k * 3.3, -6 + k * 3.3 + diameter, diameter, WOOD:Lerp(Color3.fromRGB(60, 46, 34), rng:NextNumber()), Enum.Material.Wood, "Log") - log.Shape = Enum.PartType.Cylinder - log.Size = Vector3.new(log.Size.Z + 3, diameter, diameter) - log.CFrame *= CFrame.Angles(0, math.rad(90), 0) + rod(Outside, a - d, b + d, -6 + k * 3.3, diameter, woodTone(), Enum.Material.Wood, "Log") end end end @@ -378,7 +392,7 @@ end local terrainOnly = RaycastParams.new() terrainOnly.FilterType = Enum.RaycastFilterType.Include terrainOnly.FilterDescendantsInstances = { Terrain } -local function pine(x: number, z: number, h: number, tiers: number, shadows: boolean) +local function pine(x: number, z: number, h: number, tiers: number, shadows: boolean): Part local hit = workspace:Raycast(ORIGIN + Vector3.new(x, 200, z), Vector3.new(0, -260, 0), terrainOnly) local base = Vector3.new(x, if hit then hit.Position.Y - ORIGIN.Y - 1 else 0, z) local trunk = part(Outside, Vector3.new(h * 0.45, h * 0.07, h * 0.07), base + Vector3.new(0, h * 0.22, 0), Color3.fromRGB(46, 34, 26), Enum.Material.Wood, "Trunk") @@ -401,6 +415,7 @@ local function pine(x: number, z: number, h: number, tiers: number, shadows: boo leaf.Parent = Outside end end + return trunk end for _, t in { { -292, -150 }, { -300, -30 }, { -288, 70 }, { -296, 170 }, { 292, -140 }, { 300, 0 }, { 290, 130 }, { -200, 246 }, { -40, 250 }, { 120, 244 }, { 230, 256 }, { -150, -250 }, { 90, -245 } } do pine(t[1], t[2], rng:NextNumber(150, 190), 3, true) @@ -414,6 +429,437 @@ for x = -416, 416, 64 do end end +local METAL = Color3.fromRGB(70, 72, 78) +local PAPER = Color3.fromRGB(196, 186, 164) +local CRANE = Color3.fromRGB(150, 120, 50) +local DOOR_H = 12 +type Gaps = { [number]: { { number } } } + +local function rot(c: Vector2, angle: number, p: Vector2): Vector2 + local a = math.rad(angle) + return c + Vector2.new(p.X * math.cos(a) - p.Y * math.sin(a), p.X * math.sin(a) + p.Y * math.cos(a)) +end + +local function rect(c: Vector2, angle: number, w: number, d: number): { Vector2 } + local out = {} + for _, k in { Vector2.new(-1, -1), Vector2.new(1, -1), Vector2.new(1, 1), Vector2.new(-1, 1) } do + table.insert(out, rot(c, angle, k * Vector2.new(w, d) / 2)) + end + return out +end + +local function zone(name: string, parent: Instance): Model + local m = Instance.new("Model") + m.Name = name + m:SetAttribute("Zone", name) + m.Parent = parent + return m +end + +local function walls(parent: Instance, poly: { Vector2 }, y: number, h: number, gaps: Gaps?) + for i, a in poly do + local b = poly[i % #poly + 1] + local d = (b - a).Unit + local from = -0.75 + for _, g in (gaps and gaps[i]) or {} do + local t, w, top = g[1], g[2], math.min(g[3] or DOOR_H, h) + beam(parent, a + d * from, a + d * (t - w / 2), y, y + h, 1.5, CONCRETE, Enum.Material.Concrete, "Wall") + if top < h then + beam(parent, a + d * (t - w / 2), a + d * (t + w / 2), y + top, y + h, 1.5, CONCRETE, Enum.Material.Concrete, "Lintel") + end + from = t + w / 2 + end + beam(parent, a + d * from, b + d * 0.75, y, y + h, 1.5, CONCRETE, Enum.Material.Concrete, "Wall") + end +end + +local function slab(parent: Instance, c: Vector2, angle: number, w: number, d: number, top: number, name: string): Part + local p = part(parent, Vector3.new(w, 1, d), Vector3.new(c.X, top - 0.5, c.Y), CONCRETE, Enum.Material.Concrete, name) + p.CFrame *= CFrame.Angles(0, -math.rad(angle), 0) + p:SetAttribute(if name == "Roof" then "Roof" else "Walk", true) + return p +end + +local function room(parent: Instance, c: Vector2, angle: number, w: number, d: number, y: number, h: number, gaps: Gaps): { Vector2 } + local poly = rect(c, angle, w, d) + walls(parent, poly, y, h, gaps) + slab(parent, c, angle, w + 1.5, d + 1.5, y + h + 1, "Roof") + return poly +end + +local function block(parent: Instance, size: Vector3, x: number, y: number, z: number, angle: number?, color: Color3?, material: Enum.Material?, name: string?): Part + local p = part(parent, size, Vector3.new(x, y + size.Y / 2, z), color or CONCRETE, material or Enum.Material.Concrete, name) + p.CFrame *= CFrame.Angles(0, -math.rad(angle or 0), 0) + return p +end + +local function tank(parent: Instance, diameter: number, height: number, x: number, y: number, z: number, name: string?, color: Color3?, material: Enum.Material?): Part + local p = part(parent, Vector3.new(height, diameter, diameter), Vector3.new(x, y + height / 2, z), color or METAL, material or Enum.Material.Metal, name or "Tank") + p.Shape = Enum.PartType.Cylinder + p.CFrame *= CFrame.Angles(0, 0, math.rad(90)) + return p +end + +local function pile(parent: Instance, c: Vector2, angle: number, length: number, y: number, layers: { number }) + for layer, count in layers do + for k = 1, count do + local v = (k - (count + 1) / 2) * 4 + rod(parent, rot(c, angle, Vector2.new(-length / 2, v)), rot(c, angle, Vector2.new(length / 2, v)), y + (layer - 1) * 3.4, 4, woodTone(), Enum.Material.Wood, "Log") + end + end +end + +local function crates(parent: Instance, x: number, y: number, z: number, levels: number) + for level = 0, levels - 1 do + block(parent, Vector3.new(4.5, 4.5, 4.5), x, y + level * 4.5, z, rng:NextNumber(-15, 15), WOOD, Enum.Material.WoodPlanks, "Crate") + end +end + +local function lumber(parent: Instance, x: number, y: number, z: number, angle: number) + block(parent, Vector3.new(14, 7, 6), x, y, z, angle, Color3.fromRGB(150, 120, 84), Enum.Material.WoodPlanks, "Lumber") +end + +local function bales(parent: Instance, x: number, y: number, z: number) + for i = 0, 7 do + block(parent, Vector3.new(5.8, 4.9, 5.8), x + (i % 2 - 0.5) * 6, y + (i // 4) * 5, z + (i // 2 % 2 - 0.5) * 6, rng:NextNumber(-4, 4), PAPER, Enum.Material.Fabric, "Bale") + end +end + +local function stack(parent: Instance, x: number, z: number, angle: number, logs: boolean, low: boolean) + local c, y = Vector2.new(x, z), heightAt(x, z) + local function at(u: number, v: number): Vector2 + return rot(c, angle, Vector2.new(u, v)) + end + local box = at(if logs then 4.5 else 3.5, if logs then 6 else 0) + crates(parent, box.X, y, box.Y, if low then 1 else 2) + if logs then + pile(parent, at(-2, -1), angle + 90, 18, y, if low then { 2, 1 } else { 2, 2, 1 }) + return + end + local a, b = at(0, -7), at(-4, 3) + lumber(parent, a.X, y, a.Y, angle) + if not low then + lumber(parent, a.X, y + 7, a.Y, angle + rng:NextNumber(-4, 4)) + end + lumber(parent, b.X, y, b.Y, angle + 90) +end + +local function truck(parent: Instance, c: Vector2, angle: number, y: number, logs: boolean) + local cab, bed = rot(c, angle, Vector2.new(15, 0)), rot(c, angle, Vector2.new(-2, 0)) + block(parent, Vector3.new(8, 9, 8), cab.X, y, cab.Y, angle, METAL, Enum.Material.Metal, "TruckCab") + block(parent, Vector3.new(24, 4, 8), bed.X, y, bed.Y, angle, METAL, Enum.Material.Metal, "TruckBed") + if logs then + pile(parent, bed, angle, 22, y + 4, { 2, 1 }) + else + block(parent, Vector3.new(22, 8, 8), bed.X, y + 4, bed.Y, angle, METAL, Enum.Material.CorrodedMetal, "TruckBox") + end +end + +local function panel(parent: Instance, poly: { Vector2 }, i: number, t: number, w: number, y: number, h: number, name: string): (Part, Vector3) + local a, b = poly[i], poly[i % #poly + 1] + local d = (b - a).Unit + return beam(parent, a + d * (t - w / 2), a + d * (t + w / 2), y, y + h, 0.4, METAL, Enum.Material.DiamondPlate, name), Vector3.new(d.Y, 0, -d.X) +end + +local function fireExit(parent: Instance, poly: { Vector2 }, i: number, t: number, y: number) + local m = Instance.new("Model") + m.Name = "FireExit" + local door, out = panel(m, poly, i, t, 6, y, 8, "Panel") + m.PrimaryPart = door + m:SetAttribute("OneWayDoor", true) + m:SetAttribute("OpenDirection", out) + m.Parent = parent +end + +local function generator(name: string, pos: Vector3, rotY: number) + local model = Instance.new("Model") + model.Name = name + model.Parent = Generators + local base = part(model, Vector3.new(5, 4, 3), Vector3.new(0, 2, 0), Color3.fromRGB(120, 104, 52), Enum.Material.DiamondPlate, "Base") + part(model, Vector3.new(3.2, 2.2, 0.3), Vector3.new(0, 2.4, -1.6), Color3.fromRGB(30, 32, 36), Enum.Material.Metal, "Panel") + part(model, Vector3.new(0.4, 2.5, 0.4), Vector3.new(1.8, 5.2, 0.8), METAL, Enum.Material.Metal, "Exhaust") + local status = part(model, Vector3.new(0.8, 0.8, 0.8), Vector3.new(-1.6, 4.4, -1), Color3.fromRGB(200, 40, 30), Enum.Material.Neon, "Status") + status.CastShadow = false + local light = Instance.new("PointLight") + light.Range = 12 + light.Brightness = 1.5 + light.Color = status.Color + light.Parent = status + model.PrimaryPart = base + model:PivotTo(CFrame.new(ORIGIN + pos + Vector3.new(0, 2, 0)) * CFrame.Angles(0, math.rad(rotY), 0)) +end + +local function sign(text: string, pos: Vector3, face: Enum.NormalId, color: Color3) + local s = detail(part(Details, Vector3.new(#text * 0.8 + 2, 2.4, 0.2), pos, Color3.fromRGB(28, 30, 34), Enum.Material.Metal, "Sign")) + s:RemoveTag("Detail") + local gui = Instance.new("SurfaceGui") + gui.Face = face + gui.SizingMode = Enum.SurfaceGuiSizingMode.PixelsPerStud + gui.PixelsPerStud = 40 + gui.LightInfluence = 1 + gui.Parent = s + local label = Instance.new("TextLabel") + label.Size = UDim2.fromScale(1, 1) + label.BackgroundTransparency = 1 + label.Text = text + label.TextScaled = true + label.Font = Enum.Font.GothamBlack + label.TextColor3 = color + label.Parent = gui +end + +local function crossing(x: number, isLog: boolean) + local c = Vector2.new(x, zAt(CANAL, x)) + local d = Vector2.new(2, zAt(CANAL, x + 1) - zAt(CANAL, x - 1)).Unit + local n = Vector2.new(-d.Y, d.X) + local width = if isLog then 4 else 8 + local function at(t: number, y: number): Vector3 + local p = c + n * t + return Vector3.new(p.X, y, p.Y) + end + local deck = beam(TerrainParts, c - n * 22, c + n * 18, 3.5, 4.5, width, METAL, Enum.Material.DiamondPlate, "Deck") + deck:SetAttribute("Walk", true) + for _, w in { ramp(at(-26, 4), at(-22, 4.5), width), ramp(at(30, 0), at(18, 4.5), width) } do + if isLog then + w.Color, w.Material, w.MaterialVariant = WOOD, Enum.Material.Wood, "" + end + end + if isLog then + deck.Transparency = 1 + detail(rod(TerrainParts, c - n * 24, c + n * 24, 0.5, 4, woodTone(), Enum.Material.Wood, "FallenLog")) + else + for _, side in { -1, 1 } do + beam(TerrainParts, c - n * 22 + d * side * 3.8, c + n * 18 + d * side * 3.8, 4.5, 7.5, 0.4, METAL, Enum.Material.Metal, "Rail") + end + end +end +crossing(-190, false) +crossing(140, false) +crossing(20, true) + +local Nave = zone("Nave", Buildings) +local navePoly = { Vector2.new(-245, -195), Vector2.new(-155, -195), Vector2.new(-155, -155), Vector2.new(-205, -155), Vector2.new(-205, -110), Vector2.new(-245, -110) } +walls(Nave, navePoly, 8, 22, { [2] = { { 20, 12 } }, [3] = { { 25, 6, 8 } }, [4] = { { 25, 12 } }, [5] = { { 20, 12 } }, [6] = { { 55, 6, 8 } } }) +slab(Nave, Vector2.new(-200, -175), 0, 91.5, 41.5, 31, "Roof") +slab(Nave, Vector2.new(-225, -131.75), 0, 41.5, 45, 31, "Roof") +fireExit(Nave, navePoly, 6, 55, 8) +for _, v in { Vector2.new(-225, -173), Vector2.new(-195, -178), Vector2.new(-225, -125) } do + tank(Nave, 14, 16, v.X, 8, v.Y, "Vat") +end +detail(beam(Nave, Vector2.new(-225, -173), Vector2.new(-195, -178), 19, 20.5, 1.5, METAL, Enum.Material.Metal, "Pipe")) +detail(beam(Nave, Vector2.new(-225, -173), Vector2.new(-225, -125), 21, 22.5, 1.5, METAL, Enum.Material.Metal, "Pipe")) +generator("NaveA", Vector3.new(-170, 8, -178), 90) +generator("NaveB", Vector3.new(-212, 8, -143), 0) + +local Oficinas = zone("Oficinas", Buildings) +local oficinasPoly = room(Oficinas, Vector2.new(195, -182), 0, 50, 36, 8, 18, { [2] = { { 8, 6, 7 } }, [3] = { { 25, 6, 7 } }, [4] = { { 26, 6, 7 } } }) +local killerDoor = panel(Oficinas, oficinasPoly, 4, 26, 6, 8, 7, "KillerDoor") +killerDoor.CollisionGroup = "KillerPassthrough" +killerDoor.Color = Color3.fromRGB(96, 36, 34) +killerDoor:SetAttribute("KillerDoor", true) +slab(Oficinas, Vector2.new(195, -192), 0, 48.5, 16, 17, "Floor") +for _, r in { { 170, 174 }, { 180, 210 }, { 216, 220 } } do + beam(Oficinas, Vector2.new(r[1], -184.2), Vector2.new(r[2], -184.2), 17, 20, 0.4, METAL, Enum.Material.Metal, "Rail") +end +for _, x in { 177, 213 } do + ramp(Vector3.new(x, 8, -166), Vector3.new(x, 17, -184), 6) +end +generator("Oficinas", Vector3.new(195, 17, -194), 180) +walls(Oficinas, rect(Vector2.new(195, -193), 0, 20, 14), 8, 8, { [2] = { { 9, 4, 7 } }, [3] = { { 5, 4, 7 }, { 15, 4, 7 } }, [4] = { { 5, 4, 7 } } }) +for _, z in { { -200, -193 }, { -189, -186 } } do + beam(Oficinas, Vector2.new(195, z[1]), Vector2.new(195, z[2]), 8, 16, 1, CONCRETE, Enum.Material.Concrete, "Partition") +end + +local Chimenea = zone("Pozo", Buildings) +block(Chimenea, Vector3.new(12, 150, 12), 150, 8, -122, 0, CONCRETE, Enum.Material.Brick, "Chimney") +room(Chimenea, Vector2.new(126, -139), 0, 28, 22, 8, 14, { [2] = { { 11, 6, 7 } }, [3] = { { 14, 12 } }, [4] = { { 11, 6, 7 } } }) +tank(Chimenea, 10, 10, 126, 8, -139, "Boiler") +room(Chimenea, Vector2.new(215, -57), 0, 30, 24, 4, 12, { [1] = { { 15, 6, 7 } }, [3] = { { 15, 12 } }, [4] = { { 12, 6, 7 } } }) +tank(Chimenea, 10, 14, 196, 4, -88) +tank(Chimenea, 10, 14, 248, 4, -24) +generator("Pozo", Vector3.new(182, 4, -28), -30) +room(Chimenea, Vector2.new(178, -78), 0, 12, 10, 4, 9, { [2] = { { 5, 6, 7 } }, [4] = { { 5, 6, 7 } } }) +block(Chimenea, Vector3.new(8, 8, 6), 160, 4, -22, 0, METAL, Enum.Material.Metal, "Transformer") +block(Chimenea, Vector3.new(6, 7, 6), 205, 4, -24, 0, METAL, Enum.Material.Metal, "Valves") + +local Rodillos = zone("Rodillos", Buildings) +local RC = Vector2.new(40, -55) +local function R(s: number, v: number, y: number): Vector3 + local p = rot(RC, 13, Vector2.new(s, v)) + return Vector3.new(p.X, y, p.Y) +end +local function R2(s: number, v: number): Vector2 + local p = R(s, v, 0) + return Vector2.new(p.X, p.Z) +end +room(Rodillos, R2(-2.5, 0), 13, 105, 26, 4, 20, { [1] = { { 45, 6, 7 }, { 95, 12 } }, [2] = { { 15.5, 21, 99 } }, [3] = { { 30, 12 } }, [4] = { { 13, 12 } } }) +for _, seg in { { -45, -25 }, { -17, 3 }, { 11, 26 }, { 34, 44 } } do + beam(Rodillos, R2(seg[1], 0), R2(seg[2], 0), 4, 11, 8, METAL, Enum.Material.Metal, "Machine") + for s = seg[1] + 2, seg[2] - 2, 5 do + detail(rod(Rodillos, R2(s, -4.5), R2(s, 4.5), 11, 2.4, CAP, Enum.Material.Metal, "Roller")) + end +end +for _, w in { beam(Rodillos, R2(-20, 10.5), R2(62, 10.5), 13, 14, 4, METAL, Enum.Material.DiamondPlate, "Catwalk"), beam(Rodillos, R2(7, -12.5), R2(7, 8.5), 13, 14, 8, METAL, Enum.Material.DiamondPlate, "Catwalk") } do + w:SetAttribute("Walk", true) +end +for _, r in { { -20, 3 }, { 11, 50 } } do + beam(Rodillos, R2(r[1], 8.7), R2(r[2], 8.7), 14, 17, 0.4, METAL, Enum.Material.Metal, "Rail") +end +ramp(R(-40, 10.5, 4), R(-20, 10.5, 14), 4) +ramp(R(31, -10.5, 4), R(11, -10.5, 14), 4) +generator("Rodillos", R(4.6, -1.5, 4), 257) + +local Patio = zone("Patio", Yard) +for _, x in { -200, -180, -160, -140 } do + for _, z in { -45, -15 } do + block(Patio, Vector3.new(1.5, 16, 1.5), x, 4, z, 0, WOOD, Enum.Material.Wood, "Post") + end +end +slab(Patio, Vector2.new(-170, -30), 0, 64, 34, 21, "Roof") +beam(Patio, Vector2.new(-200, -45), Vector2.new(-168, -45), 4, 20, 1, WOOD, Enum.Material.WoodPlanks, "Wall") +block(Patio, Vector3.new(32, 3.5, 5), -170, 4, -34, 0, METAL, Enum.Material.Metal, "SawTable") +detail(rod(Patio, Vector2.new(-165, -34.3), Vector2.new(-165, -33.7), 5, 9, CAP, Enum.Material.Metal, "Saw")) +pile(Patio, Vector2.new(-170, -23), 0, 26, 4, { 3, 2 }) +pile(Patio, Vector2.new(-225, 160), 0, 30, 0, { 2, 2, 1 }) +pile(Patio, Vector2.new(-95, 100), 90, 30, 0, { 2, 2, 1 }) +pile(Patio, Vector2.new(-45, 160), 20, 30, 0, { 2, 2, 1 }) +for _, x in { -105, -85 } do + for _, z in { 78, 128 } do + block(Patio, Vector3.new(2, 26, 2), x, 0, z, 0, CRANE, Enum.Material.Metal, "CraneLeg") + end + beam(Patio, Vector2.new(x, 77), Vector2.new(x, 129), 26, 28, 2, CRANE, Enum.Material.Metal, "CraneRail") +end +beam(Patio, Vector2.new(-106, 103), Vector2.new(-84, 103), 26.5, 29, 3, CRANE, Enum.Material.Metal, "CraneBridge") +detail(block(Patio, Vector3.new(0.3, 9, 0.3), -95, 17.5, 103, 0, METAL, Enum.Material.Metal, "Cable")) +detail(block(Patio, Vector3.new(3, 2, 3), -95, 15.5, 103, 0, METAL, Enum.Material.Metal, "Hook")) +truck(Patio, Vector2.new(-172, 140), 180, 0, true) +truck(Patio, Vector2.new(-20, 95), 90, 0, true) +for i = 0, 7 do + local x = -138 + i * 16 + local z = -32 - (i * 16 + 2) * 30 / 114 + block(Patio, Vector3.new(1.2, 8, 1.2), x, 4, z, 0, METAL, Enum.Material.Metal, "ConveyorPost") +end +beam(Patio, Vector2.new(-140, -31.5), Vector2.new(-24, -62), 12, 13, 3, METAL, Enum.Material.DiamondPlate, "Conveyor") +detail(rod(Patio, Vector2.new(-228, -27), Vector2.new(-228, 7), 5, 10, METAL, Enum.Material.CorrodedMetal, "Debarker")).CanQuery = true +for _, z in { -20, 0 } do + block(Patio, Vector3.new(12, 3, 3), -228, 4, z, 0, CONCRETE, Enum.Material.Concrete, "Cradle") +end +truck(Patio, Vector2.new(55, -12), 0, 4, true) +for i, s in STACKS do + stack(Yard, s[1], s[2], s[3], i % 2 == 0, s[4] == 1) +end +for _, t in { { -234, 116 }, { -174, -88 }, { -144, 184 }, { 49, -76 }, { 148, -20 } } do + tank(Yard, 8, 12, t[1], heightAt(t[1], t[2]), t[2]) +end +block(Patio, Vector3.new(10, 8, 8), -50, 0, 115, 30, METAL, Enum.Material.Metal, "Chipper") +detail(tank(Patio, 7, 5, -50, 8, 115, "Hopper")) +tank(Patio, 8, 10, 100, 4, -6) +room(Patio, Vector2.new(-20, 185), 0, 14, 12, 0, 10, { [2] = { { 6, 6, 7 } }, [4] = { { 6, 6, 7 } } }) +for _, x in { -72, -48 } do + block(Patio, Vector3.new(4, 18, 4), x, 0, 206, 0, CONCRETE, Enum.Material.Concrete, "GatePost") +end +for x = -69, -51, 2 do + block(Patio, Vector3.new(0.4, 14, 0.4), x, 0, 206, 0, METAL, Enum.Material.Metal, "GateBar") +end +sign("LA PAPELERA DEL BOSQUE", Vector3.new(-60, 16, 205.5), Enum.NormalId.Front, Color3.fromRGB(214, 190, 140)) +generator("Patio", Vector3.new(-128, 0, 140), 0) + +local Bosquecillo = zone("Bosquecillo", Yard) +for _, t in { { 55, 90, 118 }, { 85, 130, 132 }, { 60, 170, 124 }, { 100, 185, 140 }, { 105, 85, 112 } } do + local trunk = pine(t[1], t[2], t[3], 3, true) + trunk.CanCollide, trunk.CanQuery, trunk.Parent = true, true, Bosquecillo +end +for _, t in { { 80, 105, 70 }, { 66, 118, 64 }, { 92, 158, 58 }, { 35, 190, 60 } } do + local trunk = pine(t[1], t[2], t[3], 2, false) + trunk.CanCollide, trunk.CanQuery, trunk.Parent = true, true, Bosquecillo +end +rod(Bosquecillo, Vector2.new(28, 140), Vector2.new(58, 150), 0, 6.5, woodTone(), Enum.Material.Wood, "FallenLog") +rod(Bosquecillo, Vector2.new(112, 122), Vector2.new(118, 152), 0, 6.5, woodTone(), Enum.Material.Wood, "FallenLog") +generator("Bosquecillo", Vector3.new(80, 0, 160), 40) + +local Almacen = zone("Almacen", Buildings) +local AC, AA = Vector2.new(185, 125), -12 +local function A(u: number, v: number): Vector2 + return rot(AC, AA, Vector2.new(u, v)) +end +local almacenPoly = room(Almacen, AC, AA, 110, 70, 0, 18, { [1] = { { 55, 12 } }, [2] = { { 45, 6, 8 } }, [3] = { { 30, 12 } }, [4] = { { 35, 12 } } }) +fireExit(Almacen, almacenPoly, 2, 45, 0) +for _, side in { -1, 1 } do + local c = A(side * 23, -40.75) + block(Almacen, Vector3.new(34, 4, 10), c.X, 0, c.Y, AA, CONCRETE, Enum.Material.Concrete, "Dock"):SetAttribute("Walk", true) + local low, high = A(side * 52, -40.75), A(side * 40, -40.75) + ramp(Vector3.new(low.X, 0, low.Y), Vector3.new(high.X, 4, high.Y), 10) + detail(beam(Almacen, A(side * 16, -35.9), A(side * 30, -35.9), 4, 14, 0.3, METAL, Enum.Material.CorrodedMetal, "Shutter")) +end +for _, cell in { { -40, -22 }, { -28, -22 }, { -40, -10 }, { -6, -4 }, { 6, -4 }, { 18, -4 }, { 6, 8 }, { 40, -22 }, { 40, -10 }, { 28, -22 }, { -40, 22 }, { -28, 22 }, { -16, 22 }, { -28, 10 } } do + for _, o in { Vector2.new(-3, -3), Vector2.new(3, -3), Vector2.new(-3, 3), Vector2.new(3, 3) } do + local p = A(cell[1] + o.X, cell[2] + o.Y) + for layer = 0, 1 do + tank(Almacen, 5.8, 5.9, p.X, layer * 6, p.Y, "Bobbin", PAPER, Enum.Material.SmoothPlastic) + end + end +end +local dockTruck = A(-18, -55) +truck(Almacen, dockTruck, AA, 0, false) +for _, c in { { 28, -52, 2 }, { 34, -49, 1 } } do + local p = A(c[1], c[2]) + crates(Almacen, p.X, 0, p.Y, c[3]) +end +local almacenGen = A(38, 20) +generator("Almacen", Vector3.new(almacenGen.X, 0, almacenGen.Y), -AA) + +tank(Yard, 12, 18, -120, 8, -170) +tank(Yard, 10, 16, -70, 8, -185) +tank(Yard, 12, 18, 95, 8, -180) +room(Yard, Vector2.new(-95, -122), 0, 16, 12, 8, 10, { [3] = { { 8, 6, 7 } } }) +room(Yard, Vector2.new(25, -160), 0, 20, 14, 8, 10, { [2] = { { 7, 6, 7 } }, [4] = { { 7, 6, 7 } } }) +pile(Yard, Vector2.new(-35, -140), 15, 24, 8, { 3, 2, 1 }) +for _, c in { { 60, -128, 2 }, { 65, -124, 1 }, { 59, -122, 1 }, { 245, -140, 2 }, { 249, -135, 1 } } do + crates(Yard, c[1], 8, c[2], c[3]) +end +for _, c in { { 70, -85, 2 }, { 76, -82, 1 }, { 145, -88, 2 } } do + crates(Yard, c[1], 4, c[2], c[3]) +end +bales(Yard, -178, 8, -128) +bales(Yard, -62, 8, -128) +tank(Yard, 8, 12, -148, 8, -120) +tank(Chimenea, 10, 12, 178, 4, -52) +tank(Yard, 8, 10, 5, 8, -190) +tank(Yard, 10, 14, -30, 8, -172) +tank(Yard, 8, 12, 112, 4, -82) +block(Yard, Vector3.new(6, 7, 6), 80, 8, -150, 0, METAL, Enum.Material.Metal, "Pump") +truck(Yard, Vector2.new(205, -132), 0, 8, false) +for x = -135, 120, 15 do + local zw, zn = zAt(NORTH_WALL, x), zAt(NORTH_WALL, x + 15) + if math.abs(x - 40) > 10 then + for _, o in { -8, -18 } do + block(Yard, Vector3.new(1.2, 10, 1.2), x, 8, zw + o, 0, METAL, Enum.Material.Metal, "RackPost") + end + end + beam(Yard, Vector2.new(x, zw - 7), Vector2.new(x, zw - 19), 17, 18, 1, METAL, Enum.Material.Metal, "RackBeam") + if x < 120 then + beam(Yard, Vector2.new(x, zw - 13), Vector2.new(x + 15, zn - 13), 18, 18.4, 11, METAL, Enum.Material.DiamondPlate, "RackDeck") + for _, o in { -9.5, -13, -16.5 } do + detail(rod(Yard, Vector2.new(x, zw + o), Vector2.new(x + 15, zn + o), 18.4, 1.6, CAP, Enum.Material.Metal, "Pipe")) + end + end +end +block(Yard, Vector3.new(20, 18, 12), -20, 8, -210, 0, CONCRETE, Enum.Material.Concrete, "LabPortal") +detail(block(Yard, Vector3.new(12, 12, 0.4), -20, 8, -203.9, 0, METAL, Enum.Material.DiamondPlate, "LabDoor")) + +local Props = folder("ReactiveProps", MapModel) +for _, spot in { { -178, -186, false }, { -182, -183, true }, { -134, 146, false }, { -120, 132, true }, { 88, 166, false }, { 190, -36, true }, { -50, -140, false }, { -46, -136, true } } do + local x, z, barrel = spot[1], spot[2], spot[3] + local size = if barrel then Vector3.new(2.6, 2, 2) else Vector3.new(2, 2, 2) + local p = part(Props, size, Vector3.new(x, heightAt(x, z) + 1, z), if barrel then METAL else WOOD, if barrel then Enum.Material.CorrodedMetal else Enum.Material.WoodPlanks, if barrel then "Barrel" else "Crate") + if barrel then + p.Shape = Enum.PartType.Cylinder + p.CFrame *= CFrame.Angles(0, 0, math.rad(90)) + end + p.Anchored = false + p:AddTag("Reactive") +end + local function spawnPoint(parent: Instance, pos: Vector3) local p = part(parent, Vector3.new(1, 1, 1), pos + Vector3.new(0, 3, 0), CONCRETE, Enum.Material.SmoothPlastic, if parent.Name == "Killers" then "KillerSpawn" else "SurvivorSpawn") p.CFrame = CFrame.lookAt(ORIGIN + pos + Vector3.new(0, 3, 0), ORIGIN + Vector3.new(0, 3, 0)) @@ -491,12 +937,75 @@ sky.Parent = LightingModule local Behaviour = Instance.new("ModuleScript") Behaviour.Name = "Behaviour" Behaviour.Source = [[--!strict +local Players = game:GetService("Players") local RunService = game:GetService("RunService") +local function tagged(name: string): { Instance } + local found = {} + for _, d in script.Parent:GetDescendants() do + if d.Name == name or d:GetAttribute(name) then + table.insert(found, d) + end + end + return found +end + +local function hurtInCanal() + local hazards = tagged("CanalHazard") + while script:IsDescendantOf(workspace) do + for _, player in Players:GetPlayers() do + local character = player.Character + local humanoid = character and character:FindFirstChildOfClass("Humanoid") + local root = character and character:FindFirstChild("HumanoidRootPart") :: BasePart? + local role = character and character:FindFirstChild("Role") :: StringValue? + if humanoid and root and humanoid.Health > 1 and not (role and role.Value == "Killer") then + for _, hazard in hazards do + local box = hazard :: BasePart + local p = box.CFrame:PointToObjectSpace(root.Position) + if math.abs(p.X) < box.Size.X / 2 and math.abs(p.Y) < box.Size.Y / 2 and math.abs(p.Z) < box.Size.Z / 2 then + humanoid.Health = math.max(humanoid.Health - 5, 1) + break + end + end + end + end + task.wait(1) + end +end + +local function oneWayDoors() + local doors = tagged("OneWayDoor") + local passing: { [Instance]: boolean } = {} + local connection: RBXScriptConnection + connection = RunService.Heartbeat:Connect(function() + if not script:IsDescendantOf(workspace) then + connection:Disconnect() + return + end + local character = Players.LocalPlayer.Character + local root = character and character:FindFirstChild("HumanoidRootPart") :: BasePart? + if not root then + return + end + for _, door in doors do + local panel = (door :: Model).PrimaryPart :: BasePart + local side = (root.Position - panel.Position):Dot(door:GetAttribute("OpenDirection") :: Vector3) + if side < -1 then + passing[door] = true + elseif side > 3 then + passing[door] = false + end + panel.CanCollide = not passing[door] + end + end) +end + return { Init = function() - if not RunService:IsClient() then - return + if RunService:IsServer() then + task.spawn(hurtInCanal) + else + oneWayDoors() end end, }]] diff --git a/tools/maps/check_papelera.luau b/tools/maps/check_papelera.luau index 09427b02..c5115b8a 100644 --- a/tools/maps/check_papelera.luau +++ b/tools/maps/check_papelera.luau @@ -38,11 +38,20 @@ local ok, err = pcall(function() local bowl = heightAt(160, -45) - heightAt(120, -45) check(math.abs(bowl - 3) <= 1, string.format("el cuenco del pozo tiene %.1f de hondo, se esperaba 3", bowl)) + local roofs = {} + for _, d in probe:GetDescendants() do + if d:GetAttribute("Roof") then + table.insert(roofs, d) + end + end + local groundParams = RaycastParams.new() + groundParams.FilterType = Enum.RaycastFilterType.Exclude + groundParams.FilterDescendantsInstances = { probe:FindFirstChild("Generators"), table.unpack(roofs) } local ramps = 0 for _, ramp in probe:GetDescendants() do if ramp:IsA("BasePart") and ramp:GetAttribute("Ramp") then ramps += 1 - local hit = workspace:Raycast(ramp.Position + Vector3.new(0, 50, 0), Vector3.new(0, -100, 0), params) + local hit = workspace:Raycast(ramp.Position + Vector3.new(0, 50, 0), Vector3.new(0, -100, 0), groundParams) check(hit and hit.Instance == ramp and hit.Normal.Y >= math.cos(math.rad(30)), ramp:GetFullName() .. " pasa de 30 grados o no se pisa") end end @@ -81,6 +90,93 @@ local ok, err = pcall(function() end end + local generators = probe:FindFirstChild("Generators"):GetChildren() + check(#generators == 8, "hacen falta 8 sitios de generador, hay " .. #generators) + for _, g in generators do + local base = (g :: Model):GetPivot().Position + local hit = workspace:Raycast(base + Vector3.new(0, 1, 0), Vector3.new(0, -12, 0), groundParams) + check(hit and hit.Normal.Y > 0.99, g.Name .. ": el generador no está en suelo plano") + check(Vector2.new(base.X - ORIGIN.X - 120, base.Z - ORIGIN.Z + 45).Magnitude > 30, g.Name .. ": el generador está dentro del cuenco del pozo") + end + + local shadows, hazards, killerDoors, oneWay, zones = 0, 0, 0, 0, 0 + for _, d in probe:GetDescendants() do + if d:IsA("Light") and d.Shadows then + shadows += 1 + end + hazards += if d.Name == "CanalHazard" then 1 else 0 + killerDoors += if d:GetAttribute("KillerDoor") then 1 else 0 + oneWay += if d:GetAttribute("OpenDirection") then 1 else 0 + zones += if d:GetAttribute("Zone") then 1 else 0 + end + check(shadows <= 6, shadows .. " luces con sombra, el máximo es 6") + check(hazards > 0, "falta el peligro del canal") + check(killerDoors >= 1, "falta la puerta del asesino") + check(oneWay >= 2, "faltan 2 salidas de incendios de un sentido") + check(zones >= 6, "faltan zonas con atributo Zone") + local props = probe:FindFirstChild("ReactiveProps") + check(props and #props:GetChildren() >= 4, "faltan cajas sueltas en ReactiveProps") + + local hazards = {} + for _, d in probe:GetDescendants() do + if d.Name == "CanalHazard" then + table.insert(hazards, d) + end + end + local function inCanal(x: number, z: number): boolean + for _, h in hazards do + local p = h.CFrame:PointToObjectSpace(ORIGIN + Vector3.new(x, h.Position.Y - ORIGIN.Y, z)) + if math.abs(p.X) < h.Size.X / 2 and math.abs(p.Z) < h.Size.Z / 2 then + return true + end + end + return false + end + local function walkable(x: number, z: number): number? + local hit = workspace:Raycast(ORIGIN + Vector3.new(x, 120, z), Vector3.new(0, -200, 0), groundParams) + if not hit or hit.Normal.Y < 0.8 or inCanal(x, z) then + return nil + end + local inst = hit.Instance + if inst ~= workspace.Terrain and not inst:GetAttribute("Walk") and not inst:GetAttribute("Ramp") then + return nil + end + return hit.Position.Y - ORIGIN.Y + end + local everything = RaycastParams.new() + everything.FilterType = Enum.RaycastFilterType.Exclude + everything.FilterDescendantsInstances = {} + local function topAt(x: number, z: number): number + local hit = workspace:Raycast(ORIGIN + Vector3.new(x, 200, z), Vector3.new(0, -300, 0), everything) + return if hit then hit.Position.Y - ORIGIN.Y else -99 + end + local open = {} + for x = -250, 250, 10 do + for z = -200, 200, 10 do + local y = walkable(x, z) + if y then + local covered = false + for k = 0, 15 do + local dir = Vector2.new(math.cos(k * math.pi / 8), math.sin(k * math.pi / 8)) + for dist = 4, 20, 4 do + if topAt(x + dir.X * dist, z + dir.Y * dist) >= y + 6 then + covered = true + break + end + end + if covered then + break + end + end + if not covered then + table.insert(open, string.format("(%d,%d)", x, z)) + end + end + end + task.wait() + end + check(#open == 0, #open .. " puntos sin nada de 6 de alto a menos de 20: " .. table.concat(open, " ", 1, math.min(#open, 60))) + check(workspace.Terrain:GetMaterialColor(Enum.Material.Sand) == Color3.fromRGB(150, 118, 78), "falta el color del serrín") end) probe:Destroy() diff --git a/tools/maps/door_papelera.luau b/tools/maps/door_papelera.luau new file mode 100644 index 00000000..dce92df6 --- /dev/null +++ b/tools/maps/door_papelera.luau @@ -0,0 +1,36 @@ +local O = Vector3.new(2500, 0, -2500) +local character = game.Players.LocalPlayer.Character +local hrp = character.HumanoidRootPart +local humanoid = character.Humanoid + +local function walk(from: Vector3, to: Vector3): Vector3 + hrp.CFrame = CFrame.new(O + from + Vector3.new(0, 3.5, 0)) + hrp.AssemblyLinearVelocity = Vector3.zero + task.wait(0.8) + humanoid:MoveTo(O + to) + local t = 0 + while (hrp.Position - O - to) * Vector3.new(1, 0, 1) ~= Vector3.zero and t < 5 do + t += task.wait(0.1) + if ((hrp.Position - O - to) * Vector3.new(1, 0, 1)).Magnitude < 1.5 then + break + end + end + return hrp.Position - O +end + +local function A(u: number, v: number): Vector3 + local a = math.rad(-12) + return Vector3.new(185 + u * math.cos(a) - v * math.sin(a), 0, 125 + u * math.sin(a) + v * math.cos(a)) +end + +local out = {} +local p = walk(Vector3.new(-238, 8, -165), Vector3.new(-254, 8, -165)) +table.insert(out, string.format("nave exit out x=%.1f (want <-247)", p.X)) +p = walk(Vector3.new(-254, 8, -165), Vector3.new(-238, 8, -165)) +table.insert(out, string.format("nave exit back x=%.1f (want <-246)", p.X)) +local inside, outside = A(48, 10), A(64, 10) +p = walk(inside, outside) +table.insert(out, string.format("almacen exit out dist=%.1f (want <2)", ((p - outside) * Vector3.new(1, 0, 1)).Magnitude)) +p = walk(outside, inside) +table.insert(out, string.format("almacen exit back dist=%.1f (want >7.5)", ((p - inside) * Vector3.new(1, 0, 1)).Magnitude)) +return table.concat(out, " | ") diff --git a/tools/maps/walk_papelera.luau b/tools/maps/walk_papelera.luau index 6c388153..61d0e1b8 100644 --- a/tools/maps/walk_papelera.luau +++ b/tools/maps/walk_papelera.luau @@ -38,8 +38,57 @@ p = walk(Vector3.new(-60, 4, -77), { Vector3.new(-60, 8, -100) }) table.insert(out, string.format("wall no ramp y=%.1f z=%.1f (want ~7, stops at the wall face, z>-82)", p.Y, p.Z)) p = walk(Vector3.new(-150, 4, -62), { Vector3.new(-150, 8, -115) }) table.insert(out, string.format("terrace ramp y=%.1f (want ~11)", p.Y)) -p = walk(Vector3.new(118, 1.5, -42), { Vector3.new(175, 4, -45) }) -table.insert(out, string.format("bowl out y=%.1f x=%.1f (want ~7, x~175)", p.Y, p.X)) +p = walk(Vector3.new(118, 1.5, -46), { Vector3.new(168, 4, -46) }) +table.insert(out, string.format("bowl out y=%.1f x=%.1f (want ~7, x~168)", p.Y, p.X)) p = walk(Vector3.new(0, -4, 17), { Vector3.new(0, 0, 45) }) table.insert(out, string.format("canal south wall y=%.1f (want ~-1)", p.Y)) +local function rot(cx: number, cz: number, angle: number, u: number, v: number, y: number): Vector3 + local a = math.rad(angle) + return Vector3.new(cx + u * math.cos(a) - v * math.sin(a), y, cz + u * math.sin(a) + v * math.cos(a)) +end +p = walk(Vector3.new(195, 8, -158), { Vector3.new(195, 8, -170), Vector3.new(177, 8, -168), Vector3.new(177, 17, -188), Vector3.new(195, 17, -190) }) +table.insert(out, string.format("oficinas mezzanine y=%.1f (want ~20)", p.Y)) +p = walk(rot(40, -55, 13, -48, 10.5, 4), { rot(40, -55, 13, -18, 10.5, 14), rot(40, -55, 13, 40, 10.5, 14), rot(40, -55, 13, 64, 10.5, 14), rot(40, -55, 13, 76, 10.5, 4) }) +table.insert(out, string.format("catwalk drop y=%.1f x=%.1f (want <8, x>95)", p.Y, p.X)) +p = walk(rot(40, -55, 13, 8.5, -9, 4), { rot(40, -55, 13, 8.5, 9, 4) }) +local gap = p - rot(40, -55, 13, 8.5, 9, p.Y) +table.insert(out, string.format("rodillos gap past generator off=%.1f (want <2)", Vector2.new(gap.X, gap.Z).Magnitude)) +p = walk(Vector3.new(190, 8, -182), { Vector3.new(190, 8, -192), Vector3.new(200, 8, -191), Vector3.new(210, 8, -191) }) +table.insert(out, string.format("oficinas despachos x=%.1f z=%.1f (want x>208)", p.X, p.Z)) +for _, deck in map:GetDescendants() do + if deck.Name == "Deck" then + local look = deck.CFrame.LookVector + p = walk(deck.Position - O - look * 26 + Vector3.new(0, 0.5, 0), { deck.Position - O + look * 34 }) + table.insert(out, string.format("crossing x=%.0f y=%.1f along=%.1f (want ~3, ~34)", deck.Position.X - O.X, p.Y, (p - (deck.Position - O)):Dot(look))) + end +end +p = walk(rot(185, 125, -12, 60, -40.75, 0), { rot(185, 125, -12, 25, -40.75, 4) }) +table.insert(out, string.format("dock y=%.1f (want ~7)", p.Y)) +p = walk(Vector3.new(162, 8, -190), { Vector3.new(180, 8, -190) }) +table.insert(out, string.format("killer door as survivor x=%.1f (want <169)", p.X)) +local groups = {} +for _, part in character:GetDescendants() do + if part:IsA("BasePart") then + groups[part] = part.CollisionGroup + part.CollisionGroup = "Killer" + end +end +p = walk(Vector3.new(162, 8, -190), { Vector3.new(180, 8, -190) }) +table.insert(out, string.format("killer door as killer x=%.1f (want >172)", p.X)) +for part, group in groups do + part.CollisionGroup = group +end +local hazard +for _, h in map:GetDescendants() do + if h.Name == "CanalHazard" and (not hazard or (h.Position - O).Magnitude < (hazard.Position - O).Magnitude) then + hazard = h + end +end +humanoid.Health = 12 +hrp.CFrame = CFrame.new(Vector3.new(hazard.Position.X, O.Y - 0.5, hazard.Position.Z)) +task.wait(6) +local first = humanoid.Health +task.wait(3) +table.insert(out, string.format("canal health %.0f then %.0f (want 1, 1)", first, humanoid.Health)) +humanoid.Health = humanoid.MaxHealth return table.concat(out, " | ") From ee69bee318c1c4949b85ed68e231b6153e95a7d4 Mon Sep 17 00:00:00 2001 From: Norl4 Date: Sat, 7 Nov 2026 21:19:17 +0100 Subject: [PATCH 31/32] Light the Papelera with one colour per zone and the chimney landmark --- tools/maps/Papelera.luau | 158 ++++++++++++++++++++++++++++++--- tools/maps/check_papelera.luau | 16 ++++ 2 files changed, 162 insertions(+), 12 deletions(-) diff --git a/tools/maps/Papelera.luau b/tools/maps/Papelera.luau index 20cddadc..c0958e43 100644 --- a/tools/maps/Papelera.luau +++ b/tools/maps/Papelera.luau @@ -39,8 +39,9 @@ end local TerrainParts = folder("Terrain", MapModel) local Buildings = folder("Buildings", MapModel) local Yard = folder("Yard", MapModel) -folder("Lamps", MapModel) +local Lamps = folder("Lamps", MapModel) local Generators = folder("Generators", MapModel) +Generators:SetAttribute("ActiveCount", 5) local Details = folder("Details", MapModel) local SpawnPoints = folder("SpawnPoints", MapModel) @@ -68,6 +69,21 @@ local VARIANTS = { [Enum.Material.CeramicTiles] = "AnomalisLabTile", } +local ZONE_COLORS = { + Nave = Color3.fromRGB(50, 255, 90), + Rodillos = Color3.fromRGB(200, 222, 255), + Oficinas = Color3.fromRGB(255, 46, 40), + Pozo = Color3.fromRGB(165, 60, 255), + Patio = Color3.fromRGB(255, 160, 60), + Bosquecillo = Color3.fromRGB(70, 140, 90), + Almacen = Color3.fromRGB(60, 130, 255), +} +local YARD_LIGHT = Color3.fromRGB(190, 205, 255) +local function accent(c: Color3): Color3 + local h, s, v = c:ToHSV() + return Color3.fromHSV(h, s * 0.72, v) +end + local function part(parent: Instance, size: Vector3, pos: Vector3, color: Color3, material: Enum.Material, name: string?): Part local p = Instance.new("Part") p.Name = name or "Part" @@ -101,12 +117,12 @@ end local Outside = folder("Outside", MapModel) local Terrain = workspace.Terrain for material, color in { - [Enum.Material.Asphalt] = Color3.fromRGB(56, 54, 52), - [Enum.Material.Pavement] = Color3.fromRGB(40, 42, 48), - [Enum.Material.Mud] = Color3.fromRGB(62, 50, 38), - [Enum.Material.Ground] = Color3.fromRGB(66, 58, 44), - [Enum.Material.LeafyGrass] = Color3.fromRGB(48, 62, 40), - [Enum.Material.Rock] = Color3.fromRGB(74, 74, 78), + [Enum.Material.Asphalt] = Color3.fromRGB(84, 82, 80), + [Enum.Material.Pavement] = Color3.fromRGB(66, 68, 76), + [Enum.Material.Mud] = Color3.fromRGB(98, 82, 64), + [Enum.Material.Ground] = Color3.fromRGB(100, 88, 68), + [Enum.Material.LeafyGrass] = Color3.fromRGB(64, 82, 54), + [Enum.Material.Rock] = Color3.fromRGB(90, 90, 96), [Enum.Material.Sand] = Color3.fromRGB(150, 118, 78), } do Terrain:SetMaterialColor(material, color) @@ -130,6 +146,8 @@ local NORTH_WALL = { Vector2.new(-264, -92), Vector2.new(-217, -89), Vector2.new local CANAL = smooth({ Vector2.new(-264, 55), Vector2.new(-180, 64), Vector2.new(-120, 50), Vector2.new(-60, 22), Vector2.new(-20, 15), Vector2.new(40, 30), Vector2.new(100, 25), Vector2.new(180, 3), Vector2.new(264, 10) }, 20) local TERRACE_RAMPS_X = { -150, 40 } local CANAL_HALF = 12 +local PATIO_LAMPS: { { number } } = { { -200, 60 }, { -110, 135 }, { -130, 70 }, { -60, 40 }, { -20, 120 }, { -180, 190 }, { -60, 195 }, { 40, 90 } } +local YARD_LAMPS: { { number } } = { { -110, -125 }, { -10, -135 }, { 95, -135 }, { -60, -40 }, { 75, -25 } } local STACKS: { { number } } = { { -210, 100, 0 }, { -180, 25, 90 }, { -150, 100, 180 }, { -110, 165, 90 }, { -100, 0, 0 }, { -50, -15, 270 }, { -50, 60, 0 }, { 15, 80, 0 }, { -180, -75, 270 }, { 0, -15, 90 }, { 210, 35, 90 }, { 60, 65, 270 }, { -160, 165, 90 }, { -140, 20, 0 }, { 0, 150, 180 }, { 110, 55, 0 }, { -10, 50, 0 }, { -230, -70, 270 }, { 170, 40, 0 }, { -80, -160, 0 }, { 20, 115, 180 }, { -230, 25, 90 }, { -80, 180, 0 }, { -230, 140, 270 }, { -120, -60, 90 }, { 10, 185, 90 }, { 210, 180, 180 }, { -210, 180, 90 }, { 40, -185, 90 }, { -185, 120, 270 }, { -135, -145, 0 }, { -115, 100, 0 }, { -65, 90, 180 }, { 120, -185, 0 }, { -85, 145, 90 }, { -30, 135, 180 }, { -5, -160, 0 }, { -135, 120, 0, 1 }, { -160, -60, 180, 1 }, { -80, 65, 90, 1 }, { 30, 60, 0, 1 }, { 135, 55, 270, 1 }, { 215, -85, 0 }, { -240, 90, 0 }, { -205, 30, 180, 1 }, { -190, -110, 90 }, { -100, -25, 90, 1 }, { 25, -10, 180, 1 } } local POZO = Vector3.new(120, 4, -45) local POZO_RADIO = 30 @@ -317,6 +335,7 @@ local function bowlY(d: number): number end local Pozo = folder("Pozo", TerrainParts) Pozo:SetAttribute("Zone", "Pozo") +Pozo:SetAttribute("LightColor", accent(ZONE_COLORS.Pozo)) for i = 0, 8 do local angle = math.rad(i * 40 + rng:NextNumber() * 20) local d = 8 + (i % 3) * 7 @@ -452,6 +471,7 @@ local function zone(name: string, parent: Instance): Model local m = Instance.new("Model") m.Name = name m:SetAttribute("Zone", name) + m:SetAttribute("LightColor", accent(ZONE_COLORS[name])) m.Parent = parent return m end @@ -493,6 +513,52 @@ local function block(parent: Instance, size: Vector3, x: number, y: number, z: n return p end +local function glow(parent: Instance, pos: Vector3, color: Color3, range: number, brightness: number, shadows: boolean?): Light + local anchor = part(parent, Vector3.new(0.2, 0.2, 0.2), pos, color, Enum.Material.SmoothPlastic, "Glow") + anchor.Transparency, anchor.CanCollide, anchor.CanQuery, anchor.CanTouch, anchor.CastShadow = 1, false, false, false, false + local light = Instance.new("PointLight") + light.Color, light.Range, light.Brightness, light.Shadows = accent(color), range * 0.72, brightness * 0.6, shadows == true + light.Parent = anchor + return light +end + +local function lamp(parent: Instance, pos: Vector3, color: Color3, ceiling: boolean, shadows: boolean?, angle: number?): Part + local ground = heightAt(pos.X, pos.Z) + if not ceiling then + block(parent, Vector3.new(0.6, pos.Y - ground, 0.6), pos.X, ground, pos.Z, 0, METAL, Enum.Material.Metal, "Pole") + end + local head = block(parent, if ceiling then Vector3.new(6, 0.35, 0.8) else Vector3.new(2.2, 0.5, 2.2), pos.X, pos.Y, pos.Z, angle, color, Enum.Material.Neon, "Bulb") + head.CastShadow = false + local tint = accent(color) + local spot = Instance.new("SpotLight") + spot.Face, spot.Angle, spot.Color, spot.Shadows = Enum.NormalId.Bottom, 100, tint, shadows == true + spot.Range, spot.Brightness = if ceiling then 26 else 42, if ceiling then 3.5 else 6 + spot.Parent = head + local halo = Instance.new("PointLight") + halo.Color, halo.Range, halo.Brightness = tint, 10, 0.6 + halo.Parent = head + local top, bottom = Instance.new("Attachment"), Instance.new("Attachment") + bottom.Position = Vector3.new(0, ground - pos.Y + 0.2, 0) + top.Parent, bottom.Parent = head, head + local cone = Instance.new("Beam") + cone.Attachment0, cone.Attachment1 = top, bottom + cone.FaceCamera, cone.LightEmission, cone.Segments = true, 1, 1 + cone.Width0, cone.Width1 = 1.8, if ceiling then 9 else 16 + cone.Color, cone.Transparency = ColorSequence.new(tint), NumberSequence.new(0.92, 1) + cone:AddTag("Detail") + cone.Parent = head + return head +end + +local function windows(parent: Instance, a: Vector2, b: Vector2, y: number, color: Color3, count: number) + local d = (b - a).Unit + local n = Vector2.new(d.Y, -d.X) + for i = 1, count do + local c = a + d * ((b - a).Magnitude * i / (count + 1)) + n * 0.85 + beam(parent, c - d * 3, c + d * 3, y, y + 2.2, 0.3, color, Enum.Material.Neon, "Window").CastShadow = false + end +end + local function tank(parent: Instance, diameter: number, height: number, x: number, y: number, z: number, name: string?, color: Color3?, material: Enum.Material?): Part local p = part(parent, Vector3.new(height, diameter, diameter), Vector3.new(x, y + height / 2, z), color or METAL, material or Enum.Material.Metal, name or "Tank") p.Shape = Enum.PartType.Cylinder @@ -692,7 +758,7 @@ local function R2(s: number, v: number): Vector2 local p = R(s, v, 0) return Vector2.new(p.X, p.Z) end -room(Rodillos, R2(-2.5, 0), 13, 105, 26, 4, 20, { [1] = { { 45, 6, 7 }, { 95, 12 } }, [2] = { { 15.5, 21, 99 } }, [3] = { { 30, 12 } }, [4] = { { 13, 12 } } }) +local rodillosPoly = room(Rodillos, R2(-2.5, 0), 13, 105, 26, 4, 20, { [1] = { { 45, 6, 7 }, { 95, 12 } }, [2] = { { 15.5, 21, 99 } }, [3] = { { 30, 12 } }, [4] = { { 13, 12 } } }) for _, seg in { { -45, -25 }, { -17, 3 }, { 11, 26 }, { 34, 44 } } do beam(Rodillos, R2(seg[1], 0), R2(seg[2], 0), 4, 11, 8, METAL, Enum.Material.Metal, "Machine") for s = seg[1] + 2, seg[2] - 2, 5 do @@ -860,6 +926,74 @@ for _, spot in { { -178, -186, false }, { -182, -183, true }, { -134, 146, false p:AddTag("Reactive") end +local C = ZONE_COLORS +for _, f in { + { Nave, Vector2.new(-200, -175), 0, 88.5, 38.5, 8, Color3.fromRGB(88, 104, 92), Enum.Material.Concrete }, + { Nave, Vector2.new(-225, -133.25), 0, 38.5, 45, 8, Color3.fromRGB(88, 104, 92), Enum.Material.Concrete }, + { Oficinas, Vector2.new(195, -182), 0, 48.5, 34.5, 8, Color3.fromRGB(82, 50, 44), Enum.Material.WoodPlanks }, + { Rodillos, R2(-2.5, 0), 13, 103.5, 24.5, 4, Color3.fromRGB(136, 144, 156), Enum.Material.Concrete }, + { Almacen, AC, AA, 108.5, 68.5, 0, Color3.fromRGB(100, 110, 134), Enum.Material.Concrete }, + { Chimenea, Vector2.new(126, -139), 0, 26.5, 20.5, 8, Color3.fromRGB(72, 60, 80), Enum.Material.Brick }, + { Chimenea, Vector2.new(215, -57), 0, 28.5, 22.5, 4, Color3.fromRGB(72, 60, 80), Enum.Material.Brick }, +} do + local floorPart = slab(f[1], f[2], f[3], f[4], f[5], f[6] + 0.2, "Floor") + floorPart.Color, floorPart.Material, floorPart.MaterialVariant = f[7], f[8], VARIANTS[f[8]] or "" +end +for _, v in { Vector2.new(-225, -173), Vector2.new(-195, -178), Vector2.new(-225, -125) } do + tank(Nave, 14.6, 1.2, v.X, 18, v.Y, "VatBand", C.Nave, Enum.Material.Neon).CastShadow = false + glow(Nave, Vector3.new(v.X, 26, v.Y), C.Nave, 34, 1.6) +end +for i, v in { Vector2.new(-172, -172), Vector2.new(-208, -165), Vector2.new(-212, -140) } do + lamp(Nave, Vector3.new(v.X, 26, v.Y), C.Nave, true, i == 1, 0) +end +for _, e in { { 2, 2 }, { 3, 3 }, { 5, 2 } } do + windows(Nave, navePoly[e[1]], navePoly[e[1] % #navePoly + 1], 22, C.Nave, e[2]) +end +windows(Oficinas, oficinasPoly[3], oficinasPoly[4], 20, C.Oficinas, 4) +windows(Oficinas, oficinasPoly[3], oficinasPoly[4], 11, C.Oficinas, 2) +windows(Oficinas, oficinasPoly[2], oficinasPoly[3], 20, C.Oficinas, 2) +glow(Oficinas, Vector3.new(195, 14, -174), C.Oficinas, 30, 1.4) +glow(Oficinas, Vector3.new(195, 24, -192), C.Oficinas, 24, 1.2) +lamp(Oficinas, Vector3.new(195, 16.2, -162.6), C.Oficinas, true) +glow(Oficinas, Vector3.new(195, 9.5, -152), C.Oficinas, 28, 2) +for i, s in { -40, -12, 16, 40 } do + lamp(Rodillos, R(s, 0, 22), C.Rodillos, true, i == 3, 13) +end +windows(Rodillos, rodillosPoly[3], rodillosPoly[4], 17, C.Rodillos, 6) +windows(Rodillos, rodillosPoly[1], rodillosPoly[2], 17, C.Rodillos, 4) +glow(Pozo, Vector3.new(POZO.X, 2.5, POZO.Z), C.Pozo, 55, 2.6, true) +glow(Chimenea, Vector3.new(126, 18, -139), C.Pozo, 22, 1.4) +glow(Chimenea, Vector3.new(215, 12, -57), C.Pozo, 20, 1.2) +local BEACON_RED = Color3.fromRGB(255, 40, 30) +block(Chimenea, Vector3.new(12.6, 3, 12.6), 150, 149, -122, 0, BEACON_RED, Enum.Material.Neon, "BeaconBand").CastShadow = false +local beacon = block(Chimenea, Vector3.new(2, 2, 2), 150, 158, -122, 0, BEACON_RED, Enum.Material.Neon, "Beacon") +beacon.CastShadow = false +local beaconLight = Instance.new("PointLight") +beaconLight.Color, beaconLight.Range, beaconLight.Brightness = BEACON_RED, 60, 3 +beaconLight.Parent = beacon +for i, l in PATIO_LAMPS do + lamp(Patio, Vector3.new(l[1], heightAt(l[1], l[2]) + 14, l[2]), C.Patio, false, i <= 2) +end +lamp(Patio, Vector3.new(-170, 19.5, -30), C.Patio, true) +for _, l in YARD_LAMPS do + lamp(Lamps, Vector3.new(l[1], heightAt(l[1], l[2]) + 14, l[2]), YARD_LIGHT, false) +end +glow(Bosquecillo, Vector3.new(70, 5, 140), C.Bosquecillo, 60, 3.3) +glow(Bosquecillo, Vector3.new(95, 5, 175), C.Bosquecillo, 60, 3.3) +for i, u in { Vector2.new(-30, 8), Vector2.new(5, -12), Vector2.new(35, 12) } do + local sky = A(u.X, u.Y) + block(Almacen, Vector3.new(12, 0.3, 7), sky.X, 19, sky.Y, AA, C.Almacen, Enum.Material.Neon, "Skylight").CastShadow = false + lamp(Almacen, Vector3.new(sky.X, 15, sky.Y), C.Almacen, true, i == 2, AA) +end +for _, side in { -1, 1 } do + local dock = A(side * 23, -30) + lamp(Almacen, Vector3.new(dock.X, 15, dock.Y), C.Almacen, true, false, AA) +end +for _, u in { Vector2.new(-12, 10), Vector2.new(20, -18) } do + local floorGlow = A(u.X, u.Y) + glow(Almacen, Vector3.new(floorGlow.X, 3, floorGlow.Y), C.Almacen, 40, 3.5) +end + local function spawnPoint(parent: Instance, pos: Vector3) local p = part(parent, Vector3.new(1, 1, 1), pos + Vector3.new(0, 3, 0), CONCRETE, Enum.Material.SmoothPlastic, if parent.Name == "Killers" then "KillerSpawn" else "SurvivorSpawn") p.CFrame = CFrame.lookAt(ORIGIN + pos + Vector3.new(0, 3, 0), ORIGIN + Vector3.new(0, 3, 0)) @@ -893,8 +1027,8 @@ LightingModule.Source = [[return { ClockTime = 20, GeographicLatitude = 55, Brightness = 1.4, - Ambient = Color3.fromRGB(56, 58, 70), - OutdoorAmbient = Color3.fromRGB(92, 98, 118), + Ambient = Color3.fromRGB(88, 90, 112), + OutdoorAmbient = Color3.fromRGB(104, 110, 134), EnvironmentDiffuseScale = 0.3, EnvironmentSpecularScale = 1, GlobalShadows = true, @@ -908,7 +1042,7 @@ LightingModule.Parent = Root local atmosphere = Instance.new("Atmosphere") atmosphere.Density = 0.3 atmosphere.Offset = 0.12 -atmosphere.Color = Color3.fromRGB(96, 102, 124) +atmosphere.Color = Color3.fromRGB(92, 94, 128) atmosphere.Decay = Color3.fromRGB(30, 30, 44) atmosphere.Glare = 0 atmosphere.Haze = 1.5 @@ -919,7 +1053,7 @@ grade.Name = "Grade" grade.Contrast = 0.14 grade.Brightness = grade.Contrast / 2 grade.Saturation = -0.18 -grade.TintColor = Color3.fromRGB(240, 240, 252) +grade.TintColor = Color3.fromRGB(236, 232, 255) grade.Parent = LightingModule local bloom = Instance.new("BloomEffect") diff --git a/tools/maps/check_papelera.luau b/tools/maps/check_papelera.luau index c5115b8a..985568df 100644 --- a/tools/maps/check_papelera.luau +++ b/tools/maps/check_papelera.luau @@ -92,6 +92,7 @@ local ok, err = pcall(function() local generators = probe:FindFirstChild("Generators"):GetChildren() check(#generators == 8, "hacen falta 8 sitios de generador, hay " .. #generators) + check(probe.Generators:GetAttribute("ActiveCount") == 5, "Generators necesita ActiveCount = 5") for _, g in generators do local base = (g :: Model):GetPivot().Position local hit = workspace:Raycast(base + Vector3.new(0, 1, 0), Vector3.new(0, -12, 0), groundParams) @@ -114,6 +115,21 @@ local ok, err = pcall(function() check(killerDoors >= 1, "falta la puerta del asesino") check(oneWay >= 2, "faltan 2 salidas de incendios de un sentido") check(zones >= 6, "faltan zonas con atributo Zone") + for _, z in probe:GetDescendants() do + local want = z:GetAttribute("LightColor") + if z:GetAttribute("Zone") then + local lit = false + for _, l in z:GetDescendants() do + if l:IsA("Light") and typeof(want) == "Color3" and math.abs(l.Color.R - want.R) + math.abs(l.Color.G - want.G) + math.abs(l.Color.B - want.B) < 0.03 then + lit = true + break + end + end + check(lit, z:GetFullName() .. " no tiene una luz de su color") + end + end + local beacon = probe:FindFirstChild("Beacon", true) + check(beacon and beacon.Position.Y - ORIGIN.Y > 140 and beacon:FindFirstChildOfClass("PointLight"), "falta la luz roja arriba de la chimenea") local props = probe:FindFirstChild("ReactiveProps") check(props and #props:GetChildren() >= 4, "faltan cajas sueltas en ReactiveProps") From 01a83b2c5f97f4a8bd3cec58fba8573456ce3611 Mon Sep 17 00:00:00 2001 From: Norl4 Date: Sun, 8 Nov 2026 00:42:16 +0100 Subject: [PATCH 32/32] Bring the Papelera to life: moving machines, smoke, the Floracion reaction and the office phone --- tools/maps/Papelera.luau | 237 +++++++++++++++++++++++++++++---- tools/maps/check_papelera.luau | 9 ++ tools/maps/life_papelera.luau | 64 +++++++++ 3 files changed, 287 insertions(+), 23 deletions(-) create mode 100644 tools/maps/life_papelera.luau diff --git a/tools/maps/Papelera.luau b/tools/maps/Papelera.luau index c0958e43..dbf2e312 100644 --- a/tools/maps/Papelera.luau +++ b/tools/maps/Papelera.luau @@ -128,6 +128,9 @@ for material, color in { Terrain:SetMaterialColor(material, color) end +local SPORE = accent(ZONE_COLORS.Pozo) +local spores = 0 + local function smooth(points: { Vector2 }, step: number): { Vector2 } local out = {} for i = 1, #points - 1 do @@ -359,6 +362,10 @@ for i = 0, 4 do plank.CFrame *= CFrame.Angles(math.rad(rng:NextNumber(-5, 5)), math.rad(rng:NextNumber(-8, 8)), 0) end end +for _, side in { -1, 1 } do + local torn = detail(part(Pozo, Vector3.new(5, 0.5, 1.8), Vector3.new(POZO.X + side * 2.2, bowlY(0) + 1.6, POZO.Z), WOOD, Enum.Material.WoodPlanks, "TornPlank")) + torn.CFrame *= CFrame.Angles(0, math.rad(side * 6), math.rad(side * 55)) +end for _ = 1, 8 do local angle, d = rng:NextNumber(0, math.pi * 2), rng:NextNumber(6, 28) local chunk = detail(part(Pozo, Vector3.new(rng:NextNumber(1, 3), rng:NextNumber(0.6, 1.4), rng:NextNumber(1, 3)), Vector3.new(POZO.X + math.cos(angle) * d, bowlY(d) + 0.4, POZO.Z + math.sin(angle) * d), CONCRETE, Enum.Material.Concrete, "Rubble")) @@ -371,6 +378,17 @@ for _, heading in { 115, 140, 165 } do local turn = math.rad(heading + rng:NextNumber(-25, 25)) local q = p + Vector2.new(math.cos(turn), math.sin(turn)) * rng:NextNumber(6, 9) detail(beam(Pozo, p, q, 4.02, 4.1, width, CRACK, Enum.Material.Slate, "Crack")) + for _ = 1, 2 do + local m = p:Lerp(q, rng:NextNumber(0.1, 0.9)) + Vector2.new(rng:NextNumber(-1, 1), rng:NextNumber(-1, 1)) * width + local cap = detail(part(Pozo, Vector3.one * rng:NextNumber(0.5, 1), Vector3.new(m.X, 4.15, m.Y), SPORE, Enum.Material.Neon, "Mushroom")) + cap.Shape = Enum.PartType.Ball + spores += 1 + if spores % 8 == 1 then + local light = Instance.new("PointLight") + light.Color, light.Range, light.Brightness = SPORE, 8, 1.2 + light.Parent = cap + end + end p, width = q, width * 0.8 end end @@ -513,12 +531,16 @@ local function block(parent: Instance, size: Vector3, x: number, y: number, z: n return p end +local function anchor(parent: Instance, pos: Vector3, name: string): Part + local p = part(parent, Vector3.new(0.2, 0.2, 0.2), pos, CAP, Enum.Material.SmoothPlastic, name) + p.Transparency, p.CanCollide, p.CanQuery, p.CanTouch, p.CastShadow = 1, false, false, false, false + return p +end + local function glow(parent: Instance, pos: Vector3, color: Color3, range: number, brightness: number, shadows: boolean?): Light - local anchor = part(parent, Vector3.new(0.2, 0.2, 0.2), pos, color, Enum.Material.SmoothPlastic, "Glow") - anchor.Transparency, anchor.CanCollide, anchor.CanQuery, anchor.CanTouch, anchor.CastShadow = 1, false, false, false, false local light = Instance.new("PointLight") light.Color, light.Range, light.Brightness, light.Shadows = accent(color), range * 0.72, brightness * 0.6, shadows == true - light.Parent = anchor + light.Parent = anchor(parent, pos, "Glow") return light end @@ -559,6 +581,51 @@ local function windows(parent: Instance, a: Vector2, b: Vector2, y: number, colo end end +local SFX = game:GetService("SoundService"):FindFirstChild("SFX", true) +local function sound(parent: Instance, name: string, id: string, volume: number, radius: number, looped: boolean): Sound + local s = Instance.new("Sound") + s.Name, s.SoundId, s.Volume, s.Looped = name, "rbxassetid://" .. id, volume, looped + s.RollOffMinDistance, s.RollOffMaxDistance = radius * 0.3, radius + s.SoundGroup = SFX :: SoundGroup? + s.Parent = parent + return s +end + +local function puff(parent: Instance, pos: Vector3, name: string, color: Color3, size: number, life: number) + local e = Instance.new("ParticleEmitter") + e.Name, e.Texture, e.Color = name, "rbxasset://textures/particles/smoke_main.dds", ColorSequence.new(color) + e.Size = NumberSequence.new(size, size * 2.5) + e.Transparency = NumberSequence.new({ NumberSequenceKeypoint.new(0, 1), NumberSequenceKeypoint.new(0.2, 0.5), NumberSequenceKeypoint.new(1, 1) }) + e.LightEmission, e.LightInfluence = 0.25, 0.6 + e.Lifetime, e.Rate, e.Speed = NumberRange.new(life * 0.7, life), 3, NumberRange.new(1.5, 3) + e.SpreadAngle, e.Acceleration = Vector2.new(12, 12), Vector3.new(0.8, 0.6, 0) + e.Rotation, e.RotSpeed = NumberRange.new(0, 360), NumberRange.new(-15, 15) + e:AddTag("Detail") + e.Parent = anchor(parent, pos, name .. "Vent") +end + +local CREAM, BRAND_RED, INK, HAZARD = Color3.fromRGB(232, 220, 190), Color3.fromRGB(196, 38, 30), Color3.fromRGB(30, 26, 24), Color3.fromRGB(232, 186, 36) +local function sign(parent: Instance, c: Vector2, facing: Vector2, y: number, size: Vector2, text: string, paper: Color3, ink: Color3, glowing: number?): Part + local board = block(parent, Vector3.new(size.X, size.Y, 0.3), c.X, y, c.Y, math.deg(math.atan2(facing.X, -facing.Y)), paper, Enum.Material.SmoothPlastic, "Sign") + board.CastShadow = false + local gui = Instance.new("SurfaceGui") + gui.Face, gui.LightInfluence, gui.SizingMode, gui.PixelsPerStud = Enum.NormalId.Front, glowing or 1, Enum.SurfaceGuiSizingMode.PixelsPerStud, 30 + local label = Instance.new("TextLabel") + label.AnchorPoint, label.Position, label.Size = Vector2.new(0.5, 0.5), UDim2.fromScale(0.5, 0.5), UDim2.fromScale(0.92, 0.82) + label.BackgroundTransparency, label.Text, label.TextColor3, label.TextScaled = 1, text, ink, true + label.FontFace = Font.new("rbxasset://fonts/families/Oswald.json", Enum.FontWeight.Bold) + label.Parent = gui + gui.Parent = board + return board +end + +local function wallSign(parent: Instance, poly: { Vector2 }, edge: number, t: number, y: number, size: Vector2, text: string, paper: Color3, ink: Color3, glowing: number?): Part + local a, b = poly[edge], poly[edge % #poly + 1] + local d = (b - a).Unit + local n = Vector2.new(d.Y, -d.X) + return sign(parent, a + d * t + n * 0.9, n, y, size, text, paper, ink, glowing) +end + local function tank(parent: Instance, diameter: number, height: number, x: number, y: number, z: number, name: string?, color: Color3?, material: Enum.Material?): Part local p = part(parent, Vector3.new(height, diameter, diameter), Vector3.new(x, y + height / 2, z), color or METAL, material or Enum.Material.Metal, name or "Tank") p.Shape = Enum.PartType.Cylinder @@ -655,25 +722,6 @@ local function generator(name: string, pos: Vector3, rotY: number) model:PivotTo(CFrame.new(ORIGIN + pos + Vector3.new(0, 2, 0)) * CFrame.Angles(0, math.rad(rotY), 0)) end -local function sign(text: string, pos: Vector3, face: Enum.NormalId, color: Color3) - local s = detail(part(Details, Vector3.new(#text * 0.8 + 2, 2.4, 0.2), pos, Color3.fromRGB(28, 30, 34), Enum.Material.Metal, "Sign")) - s:RemoveTag("Detail") - local gui = Instance.new("SurfaceGui") - gui.Face = face - gui.SizingMode = Enum.SurfaceGuiSizingMode.PixelsPerStud - gui.PixelsPerStud = 40 - gui.LightInfluence = 1 - gui.Parent = s - local label = Instance.new("TextLabel") - label.Size = UDim2.fromScale(1, 1) - label.BackgroundTransparency = 1 - label.Text = text - label.TextScaled = true - label.Font = Enum.Font.GothamBlack - label.TextColor3 = color - label.Parent = gui -end - local function crossing(x: number, isLog: boolean) local c = Vector2.new(x, zAt(CANAL, x)) local d = Vector2.new(2, zAt(CANAL, x + 1) - zAt(CANAL, x - 1)).Unit @@ -827,7 +875,7 @@ end for x = -69, -51, 2 do block(Patio, Vector3.new(0.4, 14, 0.4), x, 0, 206, 0, METAL, Enum.Material.Metal, "GateBar") end -sign("LA PAPELERA DEL BOSQUE", Vector3.new(-60, 16, 205.5), Enum.NormalId.Front, Color3.fromRGB(214, 190, 140)) +sign(Details, Vector2.new(-60, 205.5), Vector2.new(0, -1), 14.8, Vector2.new(19.6, 2.4), "LA PAPELERA DEL BOSQUE", Color3.fromRGB(28, 30, 34), Color3.fromRGB(214, 190, 140)) generator("Patio", Vector3.new(-128, 0, 140), 0) local Bosquecillo = zone("Bosquecillo", Yard) @@ -942,6 +990,7 @@ end for _, v in { Vector2.new(-225, -173), Vector2.new(-195, -178), Vector2.new(-225, -125) } do tank(Nave, 14.6, 1.2, v.X, 18, v.Y, "VatBand", C.Nave, Enum.Material.Neon).CastShadow = false glow(Nave, Vector3.new(v.X, 26, v.Y), C.Nave, 34, 1.6) + puff(Nave, Vector3.new(v.X, 24.5, v.Y), "Steam", Color3.fromRGB(200, 220, 205), 2.5, 3) end for i, v in { Vector2.new(-172, -172), Vector2.new(-208, -165), Vector2.new(-212, -140) } do lamp(Nave, Vector3.new(v.X, 26, v.Y), C.Nave, true, i == 1, 0) @@ -971,6 +1020,7 @@ beacon.CastShadow = false local beaconLight = Instance.new("PointLight") beaconLight.Color, beaconLight.Range, beaconLight.Brightness = BEACON_RED, 60, 3 beaconLight.Parent = beacon +puff(Chimenea, Vector3.new(150, 161, -122), "Smoke", Color3.fromRGB(70, 66, 74), 7, 10) for i, l in PATIO_LAMPS do lamp(Patio, Vector3.new(l[1], heightAt(l[1], l[2]) + 14, l[2]), C.Patio, false, i <= 2) end @@ -994,6 +1044,52 @@ for _, u in { Vector2.new(-12, 10), Vector2.new(20, -18) } do glow(Almacen, Vector3.new(floorGlow.X, 3, floorGlow.Y), C.Almacen, 40, 3.5) end +sound(anchor(Rodillos, R(0, 0, 10), "Hum"), "Hum", "9112839126", 0.5, 90, true).Playing = true +sound(anchor(Nave, Vector3.new(-210, 14, -165), "Hum"), "Hum", "9113370956", 0.35, 60, true).Playing = true +sound(anchor(Bosquecillo, Vector3.new(80, 6, 160), "Hum"), "Hum", "9112764040", 0.4, 90, true).Playing = true +local pit = anchor(Pozo, Vector3.new(POZO.X, 3, POZO.Z), "PitVoice") +sound(pit, "Hum", "9112795571", 0.6, 80, true).Playing = true +sound(pit, "Siren", "9113073796", 1.2, 700, false) +sound(pit, "Thud", "9120916241", 1, 120, false) + +block(Oficinas, Vector3.new(3.4, 2.6, 1.8), 189, 8, -197.6, 0, WOOD, Enum.Material.WoodPlanks, "Desk") +local phone = part(Oficinas, Vector3.new(1, 0.5, 0.7), Vector3.new(189, 10.85, -197.6), Color3.fromRGB(24, 22, 24), Enum.Material.SmoothPlastic, "Phone") +phone.CanCollide = false +sound(phone, "Ring", "9117304268", 0.9, 60, true) +sign(Oficinas, Vector2.new(189, -199.1), Vector2.new(0, 1), 11.4, Vector2.new(2.4, 2.8), "MEMO INTERNO\n\nEl turno de noche NO baja al pozo.\nSi suena la sirena,\nno cojáis el teléfono.\n\n- Dirección", CREAM, INK) +wallSign(Oficinas, oficinasPoly, 3, 31, 9, Vector2.new(1.6, 1.8), "03:07", Color3.fromRGB(40, 40, 44), Color3.fromRGB(255, 60, 40), 0).Name = "TimeClock" +local rack = wallSign(Oficinas, oficinasPoly, 3, 33, 8.9, Vector2.new(1.4, 2), "TURNO\nNOCHE", METAL, CREAM) +for k = 1, 6 do + detail(part(Oficinas, Vector3.new(0.18, 1, 0.04), Vector3.zero, PAPER, Enum.Material.SmoothPlastic, "Card")).CFrame = rack.CFrame * CFrame.new(-0.6 + k * 0.17, 0.8, -0.2) +end +wallSign(Oficinas, oficinasPoly, 3, 33, 14, Vector2.new(7, 2.2), "DÍAS SIN ACCIDENTES: 0", CREAM, BRAND_RED) +wallSign(Nave, navePoly, 2, 31, 12.5, Vector2.new(8, 2.2), "NAVE 1 · PULPA", CREAM, INK) +wallSign(Rodillos, rodillosPoly, 3, 41, 8.5, Vector2.new(8, 2.2), "SALA DE RODILLOS", CREAM, INK) +wallSign(Almacen, almacenPoly, 1, 66, 5, Vector2.new(8, 2.2), "ALMACÉN 2 · BOBINAS", CREAM, INK) +for _, x in { -214, -186 } do + block(Nave, Vector3.new(0.6, 9, 0.6), x, 31, -157, 0, METAL, Enum.Material.Metal, "BillboardLeg") +end +sign(Nave, Vector2.new(-200, -156.5), Vector2.new(0, 1), 33.5, Vector2.new(34, 6), "PAPELERA DEL BOSQUE", CREAM, BRAND_RED, 0.3) +sign(Nave, Vector2.new(-200, -156.5), Vector2.new(0, 1), 31.4, Vector2.new(22, 1.8), "El papel que abraza tus ideas", CREAM, INK, 0.3) +detail(block(Pozo, Vector3.new(0.4, 4, 0.4), 88, heightAt(88, -40), -40, 0, METAL, Enum.Material.Metal, "Post")) +detail(sign(Pozo, Vector2.new(88, -40), Vector2.new(-1, 0), heightAt(88, -40) + 3.4, Vector2.new(5, 2.6), "PROHIBIDO EL PASO\nÁREA LAB-07", HAZARD, INK)) + +local Story = folder("Story", MapModel) +for _, l in { { 75, 155, 90, 166 }, { 48, 178, 60, 192 }, { -236, 50, -228, 64 } } do + detail(rod(Story, Vector2.new(l[1], l[2]), Vector2.new(l[3], l[4]), heightAt(l[1], l[2]), 2.4, woodTone(), Enum.Material.Wood, "FallenLog")) +end +local RUST = Color3.fromRGB(120, 62, 40) +for _, b in { { -150, -45, 30 }, { -25, 140, 140 }, { 40, -120, 250 } } do + local tray = detail(block(Story, Vector3.new(2.6, 1.1, 1.8), b[1], heightAt(b[1], b[2]), b[2], b[3], RUST, Enum.Material.CorrodedMetal, "Barrow")) + tray.CFrame *= CFrame.Angles(math.rad(105), 0, 0) + local wheel = detail(part(Story, Vector3.new(0.3, 1.4, 1.4), Vector3.zero, Color3.fromRGB(30, 30, 32), Enum.Material.Rubber, "Wheel")) + wheel.Shape = Enum.PartType.Cylinder + wheel.CFrame = tray.CFrame * CFrame.new(1.7, 0, 0) * CFrame.Angles(0, math.rad(90), 0) + for _, side in { -0.6, 0.6 } do + detail(part(Story, Vector3.new(2.4, 0.15, 0.15), Vector3.zero, WOOD, Enum.Material.Wood, "Handle")).CFrame = tray.CFrame * CFrame.new(-2.2, 0, side) + end +end + local function spawnPoint(parent: Instance, pos: Vector3) local p = part(parent, Vector3.new(1, 1, 1), pos + Vector3.new(0, 3, 0), CONCRETE, Enum.Material.SmoothPlastic, if parent.Name == "Killers" then "KillerSpawn" else "SurvivorSpawn") p.CFrame = CFrame.lookAt(ORIGIN + pos + Vector3.new(0, 3, 0), ORIGIN + Vector3.new(0, 3, 0)) @@ -1071,8 +1167,18 @@ sky.Parent = LightingModule local Behaviour = Instance.new("ModuleScript") Behaviour.Name = "Behaviour" Behaviour.Source = [[--!strict +local Debris = game:GetService("Debris") local Players = game:GetService("Players") local RunService = game:GetService("RunService") +local SoundService = game:GetService("SoundService") +local TweenService = game:GetService("TweenService") + +local VIOLET = Color3.fromRGB(165, 60, 255) +local SPIN: { [string]: number } = { Roller = 2.5, Saw = 9 } + +local function find(name: string): any + return script.Parent:FindFirstChild(name, true) +end local function tagged(name: string): { Instance } local found = {} @@ -1134,12 +1240,97 @@ local function oneWayDoors() end) end +local function floracion() + local connection: RBXScriptConnection + connection = workspace:GetAttributeChangedSignal("GeneratorsRepaired"):Connect(function() + if not script:IsDescendantOf(workspace) then + connection:Disconnect() + elseif (workspace:GetAttribute("GeneratorsRepaired") or 0) >= 3 then + script.Parent:SetAttribute("Floracion", true) + end + end) +end + +local function animate() + local spinning: { BasePart } = {} + local swinging: { [BasePart]: CFrame } = {} + for _, d in script.Parent:GetDescendants() do + if d:IsA("BasePart") and SPIN[d.Name] then + table.insert(spinning, d) + elseif d:IsA("BasePart") and (d.Name == "Hook" or d.Name == "Cable") then + swinging[d] = d.CFrame + end + end + local cable: BasePart = find("Cable") + local pivot = CFrame.new(cable.Position + Vector3.yAxis * cable.Size.Y / 2) + local connection: RBXScriptConnection + connection = RunService.RenderStepped:Connect(function(dt: number) + if not script:IsDescendantOf(workspace) then + connection:Disconnect() + return + end + for _, p in spinning do + p.CFrame *= CFrame.Angles(SPIN[p.Name] * dt, 0, 0) + end + local t = os.clock() + local sway = pivot * CFrame.Angles(math.sin(t * 0.9) * 0.05, 0, math.sin(t * 0.6) * 0.04) * pivot:Inverse() + for p, base in swinging do + p.CFrame = sway * base + end + end) +end + +local function bloom() + local map = script.Parent + local function start() + local siren: Sound, thud: Sound, beacon: BasePart, phone: BasePart = find("Siren"), find("Thud"), find("Beacon"), find("Phone") + siren:Play() + thud:Play() + for _, target in { beacon, find("BeaconBand"), beacon:FindFirstChildOfClass("PointLight") } do + TweenService:Create(target, TweenInfo.new(4), { Color = VIOLET }):Play() + end + for _, d in map:GetDescendants() do + if d:IsA("BasePart") and (d.Name == "Plank" or d.Name == "TornPlank") then + local base = d.CFrame + d.CFrame = base * CFrame.new(0, 0.8, 0) * CFrame.Angles(math.rad(math.random(-12, 12)), 0, math.rad(math.random(-12, 12))) + TweenService:Create(d, TweenInfo.new(0.5, Enum.EasingStyle.Bounce), { CFrame = base }):Play() + end + end + local ring = phone:FindFirstChild("Ring") :: Sound + ring:Play() + local prompt = Instance.new("ProximityPrompt") + prompt.ActionText, prompt.ObjectText, prompt.MaxActivationDistance, prompt.RequiresLineOfSight = "Coger", "Teléfono", 8, false + prompt.KeyboardKeyCode, prompt.GamepadKeyCode = Enum.KeyCode.C, Enum.KeyCode.ButtonX + prompt.Parent = phone + prompt.Triggered:Connect(function() + ring:Stop() + prompt:Destroy() + local call = siren:Clone() + call.Name, call.PlaybackSpeed, call.Parent = "Call", 0.85, SoundService + call:Play() + Debris:AddItem(call, 40) + end) + end + if map:GetAttribute("Floracion") then + start() + return + end + local connection: RBXScriptConnection + connection = map:GetAttributeChangedSignal("Floracion"):Connect(function() + connection:Disconnect() + start() + end) +end + return { Init = function() if RunService:IsServer() then task.spawn(hurtInCanal) + floracion() else oneWayDoors() + animate() + bloom() end end, }]] diff --git a/tools/maps/check_papelera.luau b/tools/maps/check_papelera.luau index 985568df..377a7d98 100644 --- a/tools/maps/check_papelera.luau +++ b/tools/maps/check_papelera.luau @@ -130,6 +130,15 @@ local ok, err = pcall(function() end local beacon = probe:FindFirstChild("Beacon", true) check(beacon and beacon.Position.Y - ORIGIN.Y > 140 and beacon:FindFirstChildOfClass("PointLight"), "falta la luz roja arriba de la chimenea") + local count: { [string]: number } = {} + for _, d in probe:GetDescendants() do + count[d.Name] = (count[d.Name] or 0) + 1 + end + check((count.Steam or 0) >= 3 and count.Smoke == 1, "faltan vapor en las cubas o humo en la chimenea") + check((count.Mushroom or 0) >= 20, "faltan hongos en las grietas del pozo") + check(count.Siren == 1 and count.Thud == 1 and count.Ring == 1 and count.Phone == 1, "faltan la sirena, el golpe o el teléfono") + check((count.Sign or 0) >= 8 and count.TimeClock == 1, "faltan carteles o el fichador") + check((count.FallenLog or 0) >= 3 and (count.Barrow or 0) >= 3 and count.TornPlank == 2, "faltan troncos caídos, carretillas o tablones arrancados") local props = probe:FindFirstChild("ReactiveProps") check(props and #props:GetChildren() >= 4, "faltan cajas sueltas en ReactiveProps") diff --git a/tools/maps/life_papelera.luau b/tools/maps/life_papelera.luau new file mode 100644 index 00000000..db341130 --- /dev/null +++ b/tools/maps/life_papelera.luau @@ -0,0 +1,64 @@ +--!strict +local RunService = game:GetService("RunService") + +local map = workspace:FindFirstChild("Map") +assert(map and map:FindFirstChild("Behaviour"), "no hay mapa en juego") + +if RunService:IsServer() then + workspace:SetAttribute("GeneratorsRepaired", 3) + task.wait(0.5) + assert(map:GetAttribute("Floracion") == true, "Floracion no se activa con 3 generadores") + return "server OK: Floracion" +end + +local results = {} +local function check(ok: any, message: string) + assert(ok, message) + table.insert(results, message) +end + +local function find(name: string): BasePart + return map:FindFirstChild(name, true) :: BasePart +end + +local roller, hook = find("Roller"), find("Hook") +local rollerBefore, hookBefore = roller.CFrame, hook.CFrame +task.wait(0.6) +check(roller.CFrame ~= rollerBefore, "los rodillos giran") +check((hook.Position - hookBefore.Position).Magnitude > 0.001, "el gancho se balancea") + +local steam, smoke = 0, 0 +for _, d in map:GetDescendants() do + if d:IsA("ParticleEmitter") then + steam += if d.Name == "Steam" then 1 else 0 + smoke += if d.Name == "Smoke" then 1 else 0 + end +end +check(steam >= 3 and smoke >= 1, "vapor en las cubas y humo en la chimenea") + +if map:GetAttribute("Floracion") then + task.wait(1.5) + local beacon = find("Beacon") + check(beacon.Color.B > beacon.Color.R, "la chimenea pasa a violeta") + local phone = find("Phone") + local prompt = phone:FindFirstChildOfClass("ProximityPrompt") + local ring = phone:FindFirstChild("Ring") :: Sound + check(prompt and ring and ring.Playing, "el teléfono suena y se puede coger") + local siren = map:FindFirstChild("Siren", true) :: Sound + check(siren and siren.Playing and siren.TimeLength > 0, "la sirena suena desde el pozo") + check(prompt.KeyboardKeyCode == Enum.KeyCode.C and prompt.GamepadKeyCode == Enum.KeyCode.ButtonX, "el teléfono se coge con C o con X del mando") + local hrp = (game:GetService("Players").LocalPlayer.Character :: Model):FindFirstChild("HumanoidRootPart") :: BasePart + while (hrp.Position - phone.Position).Magnitude > 5 do + local step = phone.Position + Vector3.new(0, 0, 3.6) - hrp.Position + hrp.CFrame = CFrame.new(hrp.Position + step.Unit * math.min(step.Magnitude, 300)) + task.wait(0.2) + end + task.wait(0.5) + prompt:InputHoldBegin() + prompt:InputHoldEnd() + task.wait(1) + local call = game:GetService("SoundService"):FindFirstChild("Call") :: Sound + check(call and call.Playing and call.TimePosition > 0.3 and not ring.Playing, "al coger el teléfono suena la llamada") +end + +return "client OK: " .. table.concat(results, ", ")