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 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/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..e305a0b3 100644 --- a/src/ReplicatedStorage/Assets/Achievements.luau +++ b/src/ReplicatedStorage/Assets/Achievements.luau @@ -14,8 +14,9 @@ return { LayoutOrder = 1, Achievements = { Welcome = { - Title = "Welcome To [INSERT GAME NAME]", - Description = "Play [INSERT GAME NAME] for the first time", + Title = "Bienvenido a Anomalis", + Icon = "rbxassetid://108807232649489", + 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..8d6ab97f 100644 --- a/src/ReplicatedStorage/Assets/Emotes/TPose.luau +++ b/src/ReplicatedStorage/Assets/Emotes/TPose.luau @@ -4,7 +4,8 @@ local Emote = require(ReplicatedStorage.Classes.Emote) return Emote.New({ Config = { Name = "T-Pose", - Quote = "be not afraid.", + Render = "rbxassetid://134166673644911", + Quote = "no temas.", }, AnimationIds = "rbxassetid://87300475457340", SoundIds = "rbxassetid://1839549655", 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/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 8892ba69..1ebf4fc3 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)) @@ -156,12 +154,12 @@ end local NullexVoyd: Types.Killer = Character.CreateKiller({ Config = { Name = "Nullex Voyd", - Quote = "Order was the first lie.", - Render = "rbxassetid://139883756028491", + Quote = "El orden fue la primera mentira.", + Render = "rbxassetid://100323356058904", Price = 1308, Origin = { - TooltipText = "Originates from Project Terminus!", + TooltipText = "¡Viene del Proyecto Terminus!", Icon = "rbxasset://textures/ui/GuiImagePlaceholder.png", }, @@ -178,6 +176,7 @@ local NullexVoyd: Types.Killer = Character.CreateKiller({ DataAnchor = Ability.New({ Name = "Data Anchor", + RenderImage = "rbxassetid://82042307155133", FirstCooldown = 3, SecondCooldown = 20, @@ -199,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, @@ -208,6 +208,7 @@ local NullexVoyd: Types.Killer = Character.CreateKiller({ Overcharge = Ability.New({ Name = "Overcharge", + RenderImage = "rbxassetid://114480081841217", Cooldown = 45, InputName = "FourthAbility", SpeedDuration = 3.2, @@ -226,7 +227,7 @@ local NullexVoydNameLabel = ""..NullexVoyd.Co NullexVoyd.Config.Description = { { Type = "Separator", - Text = "GENERAL INFO", + Text = "INFORMACIÓN GENERAL", }, { Type = "Header", @@ -238,31 +239,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", @@ -270,8 +271,8 @@ NullexVoyd.Config.Description = { }, { Type = "Text", - Text = NullexVoydNameLabel.." performs a basic slash, dealing "..tostring(NullexVoyd.GameplayConfig.Abilities.Slash.Damage).." damage.", - Image = "rbxassetid://9759886280", + Text = NullexVoydNameLabel.." lanza un tajo básico que hace "..tostring(NullexVoyd.GameplayConfig.Abilities.Slash.Damage).." de daño.", + Image = "rbxassetid://82563286387450", }, { Type = "Header", @@ -279,7 +280,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", @@ -288,8 +289,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.", }, }, { @@ -298,8 +299,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 3c90f815..73626457 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, } }) @@ -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, @@ -175,10 +175,10 @@ local Commander: Types.Survivor = Character.CreateSurvivor({ Config = { Name = "Commander", Quote = "*pyro noises*", - Render = "rbxassetid://108440668883002", + Render = "rbxassetid://80481019529425", Origin = { - TooltipText = "Originates from Project Terminus!", + TooltipText = "¡Viene del Proyecto Terminus!", Icon = "rbxasset://textures/ui/GuiImagePlaceholder.png", }, }, @@ -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,7 +224,8 @@ 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.", + 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 = "", Cooldown = 45, @@ -249,7 +253,7 @@ local Commander: Types.Survivor = Character.CreateSurvivor({ Commander.Config.Description = { { Type = "Separator", - Text = "GENERAL INFO", + Text = "INFORMACIÓN GENERAL", }, { Type = "Header", @@ -261,30 +265,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 +296,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 +313,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 +325,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 +335,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/AbilityGUI.luau b/src/ReplicatedStorage/Classes/Ability/AbilityGUI.luau index cdfb8456..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, @@ -45,7 +46,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} = {} @@ -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/Classes/Ability/Slash.luau b/src/ReplicatedStorage/Classes/Ability/Slash.luau index c337e460..7f2834ca 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, }) @@ -45,7 +45,7 @@ function SlashModule.New(Props: Types.Ability?): Types.Ability Cooldown = 2, Duration = 0.4, Damage = 20, - RenderImage = "rbxassetid://11218451110", + RenderImage = "rbxassetid://82563286387450", 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/ReplicatedStorage/Classes/Hitbox.luau b/src/ReplicatedStorage/Classes/Hitbox.luau index 308170e4..51214554 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) @@ -25,13 +26,17 @@ local Hitbox = { ExecuteOnKill = false, IsProjectile = false, FriendlyFire = false, - Reason = "Slash Attack", + Reason = "un tajo", Shape = Enum.PartType.Block, MinBlockingWallHeight = 3, IgnoreWalls = false, 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. @@ -150,6 +155,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 +173,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) @@ -319,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, @@ -351,15 +359,17 @@ 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 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) + 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 @@ -431,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) @@ -523,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/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/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"`. 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/Modules/InkFX.luau b/src/ReplicatedStorage/Modules/InkFX.luau new file mode 100644 index 00000000..f3eecb83 --- /dev/null +++ b/src/ReplicatedStorage/Modules/InkFX.luau @@ -0,0 +1,205 @@ +--!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://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 + +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.BackgroundTransparency = 1 + Ring.ImageTransparency = 0.1 + Index * 0.25 + 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.fromOffset(Size.Y * Grow, Size.Y * Grow), Rotation = Ring.Rotation + 40, ImageTransparency = 1}):Play() + task.delay(Duration + Delay, Ring.Destroy, Ring) + end +end + +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.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 + local Veil = Instance.new("ImageLabel") + Veil.Image = InkVeilImage + Veil.BackgroundTransparency = 1 + Veil.ScaleType = Enum.ScaleType.Stretch + Veil.Size = UDim2.fromScale(2, 1) + 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 = 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?) + 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/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 new file mode 100644 index 00000000..355993dd --- /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 = `Generador {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 = "Reparar" + prompt.ObjectText = `Generador 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/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/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/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 8ab2e65f..c81f6810 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 @@ -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/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/ServerScriptService/System/CommonFunctions.luau b/src/ServerScriptService/System/CommonFunctions.luau index d2d82d7a..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 = { @@ -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/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 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/Miscellaneous/GeneratorClient.luau b/src/StarterPlayerScripts/Miscellaneous/GeneratorClient.luau new file mode 100644 index 00000000..eb3e4126 --- /dev/null +++ b/src/StarterPlayerScripts/Miscellaneous/GeneratorClient.luau @@ -0,0 +1,177 @@ +--!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 InkFX = require(ReplicatedStorage.Modules.InkFX) + +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 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 + 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 = "REPARAR" + 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 }) + InkFX.Ripple(needle, 2.4, 0.5) + else + InkFX.Pop(bar, 0.6, 0.35) + 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 "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) + 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/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/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/src/StarterPlayerScripts/Miscellaneous/RoundTips.luau b/src/StarterPlayerScripts/Miscellaneous/RoundTips.luau new file mode 100644 index 00000000..d1d88356 --- /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 = "toca REPARAR", sprint = "toca el botón de correr" } + elseif input.Name:find("Gamepad") then + return { repair = "pulsa X", sprint = "mantén L2" } + end + return { repair = "pulsa C", sprint = "mantén Shift" } +end + +local function tipsFor(role: string): { string } + local k = keys() + if role == "Killer" then + return { + "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 { + `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 + +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({ "Has caído. Mira a los demás: la siguiente ronda empieza pronto." }) + end + end) + end, +} 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/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/src/StarterPlayerScripts/Miscellaneous/TerrorPulse.luau b/src/StarterPlayerScripts/Miscellaneous/TerrorPulse.luau new file mode 100644 index 00000000..26fb275d --- /dev/null +++ b/src/StarterPlayerScripts/Miscellaneous/TerrorPulse.luau @@ -0,0 +1,103 @@ +--!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("Frame") + edge.Size = UDim2.fromScale(1, 1) + edge.BackgroundTransparency = 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 }, + { 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") + table.insert(bands, band) + 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.Visible = false + return + end + local closeness = 1 - math.clamp(nearestKillerDistance(root) / TERROR_RADIUS, 0, 1) + if closeness <= 0 then + 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.Visible = true + for _, band in bands do + band.BackgroundTransparency = 1 - closeness * (0.25 + 0.2 * beat) + end + end)) + janitor:Add(function() + edge.Visible = false + end, true) + end) + end, +} 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/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 5dc7f8e6..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) @@ -19,14 +20,14 @@ 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 = { Title = "?????", Description = "????????????????????", - Icon = "rbxassetid://15117261700", + 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 @@ -135,7 +136,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 +270,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..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 @@ -44,51 +45,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!", }, }, } @@ -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/CharacterStatsManager.luau b/src/StarterPlayerScripts/UI/CharacterStatsManager.luau index 35989289..f3894502 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") @@ -27,7 +28,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 @@ -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,28 @@ 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 + local LastPop = 0 Utils.Instance.ObserveProperty(Humanoid, "Health", function(value: number) + 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 Ready and value > LastHealth + 0.5 then + LastPop = os.clock() + InkFX.Pop(HealthText, 1.15, 0.4) + end + LastHealth = value HealthText.Text = tostring(math.round(Humanoid.Health)).."/"..tostring(Humanoid.MaxHealth) end) diff --git a/src/StarterPlayerScripts/UI/Credits.luau b/src/StarterPlayerScripts/UI/Credits.luau index 02ab87a6..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 @@ -24,16 +25,16 @@ local Credits: {{ Name: string, Credits: {Credit} }} = { Name = "Dysymmetrical", Credits = { { - Render = "rbxassetid://112215528635901", + Render = "rbxassetid://132085691188764", 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.", }, }, }, @@ -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/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/src/StarterPlayerScripts/UI/Inventory/init.luau b/src/StarterPlayerScripts/UI/Inventory/init.luau index 699154c5..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) @@ -148,7 +149,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() @@ -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 @@ -715,9 +716,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..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 @@ -56,7 +57,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}) @@ -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/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..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 @@ -19,51 +20,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!", }, } @@ -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/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..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) @@ -208,7 +209,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 @@ -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 @@ -760,12 +761,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 +890,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 +1058,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/SideBar.luau b/src/StarterPlayerScripts/UI/SideBar.luau index 33f69074..20851c07 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,6 +26,30 @@ type Button = { } local InvertedInfo = TweenInfo.new(0.25, Enum.EasingStyle.Sine, Enum.EasingDirection.InOut) +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 = 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 + end) +end local UI local BarButtonPrefabs = { @@ -116,83 +141,86 @@ function SideBar:Init() local PlayerUIModules = LocalPlayer.PlayerScripts.UI + local Breathing = {} + Breathe(Breathing) + 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, @@ -213,8 +241,10 @@ function SideBar:Init() Menu.CloseOnOtherOpen = button.CloseOnOtherOpen Menu.CloseOthersOnOpen = button.CloseOthersOnOpen BarButton.Button.MouseButton1Click:Connect(function() + InkFX.Ripple(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 +255,11 @@ function SideBar:Init() BarButton.Parent = ButtonContainer local OriginalYLabelBound = BarButton.Title.Holder.Label.AbsoluteSize.Y - BarButton.MouseEnter:Connect(function() + local function Hover() + if not InkFX.Calm() then + InkFX.Ripple(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 +270,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/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 2b808499..818e1e23 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(_) @@ -17,17 +18,24 @@ 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)) 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) + 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) + end) EnoughPlayers.Changed:Connect(function() Update(State.Value) end) diff --git a/tools/maps/Instalacion.luau b/tools/maps/Instalacion.luau new file mode 100644 index 00000000..497115f4 --- /dev/null +++ b/tools/maps/Instalacion.luau @@ -0,0 +1,1062 @@ +--!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 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", + [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) +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") + 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").Reflectance = if lab then 0.12 else 0 +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 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 = tint + 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 + 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 +end + +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 +end + +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 } }, +}) +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(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 = 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 +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 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) +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(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 + 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(-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)) +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") + 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(-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(-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) +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("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) + 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(-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 + +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")) + 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 WHITE, RED, AMBER = Color3.fromRGB(220, 224, 230), Color3.fromRGB(210, 40, 36), Color3.fromRGB(230, 170, 40) +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) +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 +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 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).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 + 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 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")) + 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") + 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(-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 + 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 = 2, + Ambient = Color3.fromRGB(22, 22, 34), + OutdoorAmbient = Color3.fromRGB(105, 115, 155), + EnvironmentDiffuseScale = 0.4, + EnvironmentSpecularScale = 1, + GlobalShadows = true, + 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.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 +atmosphere.Parent = LightingModule + +local grade = Instance.new("ColorCorrectionEffect") +grade.Name = "Grade" +grade.Brightness = 0 +grade.Contrast = 0.08 +grade.Saturation = 0.12 +grade.TintColor = Color3.fromRGB(206, 212, 255) +grade.Parent = LightingModule + +local bloom = Instance.new("BloomEffect") +bloom.Intensity = 1.2 +bloom.Size = 24 +bloom.Threshold = 0.9 +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 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 + 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.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 + +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") diff --git a/tools/maps/Papelera.luau b/tools/maps/Papelera.luau new file mode 100644 index 00000000..dbf2e312 --- /dev/null +++ b/tools/maps/Papelera.luau @@ -0,0 +1,1341 @@ +--!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) +local Buildings = folder("Buildings", MapModel) +local Yard = folder("Yard", 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) + +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 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" + 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) +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) + +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(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) +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 + 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 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 +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 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 + 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 + 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 +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) +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 + 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 _, 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")) + 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")) + 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 + +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 + 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] + local d = (b - a).Unit * 1.5 + for k = 0, 9 do + local diameter = 3.2 + rng:NextNumber() * 0.5 + rod(Outside, a - d, b + d, -6 + k * 3.3, diameter, woodTone(), Enum.Material.Wood, "Log") + 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): 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") + 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 + 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) +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 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:SetAttribute("LightColor", accent(ZONE_COLORS[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 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 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(parent, pos, "Glow") + 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 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 + 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 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 +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 + 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(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) +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 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) + 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) +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 +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 +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 + +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)) + 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(88, 90, 112), + OutdoorAmbient = Color3.fromRGB(104, 110, 134), + 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(92, 94, 128) +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(236, 232, 255) +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 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 = {} + 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 + +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, +}]] +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..377a7d98 --- /dev/null +++ b/tools/maps/check_papelera.luau @@ -0,0 +1,212 @@ +--!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 + +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 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), 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 + 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 + + 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) + 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") + 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 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") + + 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() +if not ok then + error(err, 0) +end + +return "OK " .. checks .. " checks" 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/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, ", ") diff --git a/tools/maps/walk_papelera.luau b/tools/maps/walk_papelera.luau new file mode 100644 index 00000000..61d0e1b8 --- /dev/null +++ b/tools/maps/walk_papelera.luau @@ -0,0 +1,94 @@ +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, -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, " | ") 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() diff --git a/tools/test.py b/tools/test.py new file mode 100644 index 00000000..d87ecc87 --- /dev/null +++ b/tools/test.py @@ -0,0 +1,277 @@ +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,") +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"), 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"{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 +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.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('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() + 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 + 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 +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 +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)) +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()) + 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) + + +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) 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" diff --git a/tools/tinta_ui.py b/tools/tinta_ui.py new file mode 100644 index 00000000..d4e2be62 --- /dev/null +++ b/tools/tinta_ui.py @@ -0,0 +1,64 @@ +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") + +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") 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"