Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor earthquakes to its own class #2573

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .luacheckrc
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ globals = { -- Globals
-- Game classes
"AIHospital", "AnimationManager", "AnimationEffect", "App", "Audio",
"CallsDispatcher", "Cheats", "ChildClass", "Command", "Door", "DrawFlags",
"DummyRootNode", "EndConditions", "Entity", "EntityMap", "Epidemic",
"FileSystem", "FileTreeNode", "FilteredFileTreeNode", "GameUI", "Graphics",
"GrimReaper", "Hospital", "Humanoid", "HumanoidRawWalk",
"DummyRootNode", "Earthquake", "EndConditions", "Entity", "EntityMap",
"Epidemic", "FileSystem", "FileTreeNode", "FilteredFileTreeNode", "GameUI",
"Graphics", "GrimReaper", "Hospital", "Humanoid", "HumanoidRawWalk",
"Inspector", "LoadGame", "LoadGameFile", "Litter", "Machine",
"Map", "MoviePlayer", "NoRealClass", "Object", "ParentClass",
"Patient", "Plant", "PlayerHospital", "Queue", "ResearchDepartment", "Room",
Expand Down
2 changes: 1 addition & 1 deletion CorsixTH/Lua/app.lua
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ local runDebugger = corsixth.require("run_debugger")
-- and add compatibility code in afterLoad functions
-- Recommended: Also replace/Update the summary comment

local SAVEGAME_VERSION = 188 -- Add groups to win and lose criteria.
local SAVEGAME_VERSION = 189 -- Refactor earthquakes

class "App"

Expand Down
8 changes: 3 additions & 5 deletions CorsixTH/Lua/cheats.lua
Original file line number Diff line number Diff line change
Expand Up @@ -147,20 +147,18 @@ function Cheats:cheatToggleEpidemic()
end

function Cheats:cheatEarthquake()
if not self.hospital.earthquakes_disabled then
self.hospital.world:createEarthquake()
end
return self.hospital.world.earthquake:createEarthquake()
end

--! Toggles the possibility of earthquakes
function Cheats:cheatToggleEarthquake()
local world, msg = self.hospital.world
if world.earthquakes_disabled then
if world.earthquake.disabled then
msg = _S.misc.earthquakes_on
else
msg = _S.misc.earthquakes_off
end
world.earthquakes_disabled = not world.earthquakes_disabled
world.earthquake.disabled = not world.earthquake.disabled
return true, msg
end

Expand Down
242 changes: 242 additions & 0 deletions CorsixTH/Lua/earthquake.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
--[[ Copyright (c) 2024 Toby "tobylane"

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. --]]

--! Manages all earthquakes in the world
class "Earthquake"
tobylane marked this conversation as resolved.
Show resolved Hide resolved

---@type Earthquake
local Earthquake = _G["Earthquake"]

--[[
All fields relating to the current or next earthquake:
active (boolean) If there is a current earthquake, after start_day of start_month
has passed, during the warning_length, warning_period or damage timers
next_planned (boolean) If the next earthquake has been planned/generated
start_month (integer) The month the earthquake warning is triggered (1-12)
start_day (integer) The day of the month the earthquake warning is triggered (1-31)
size (integer) The amount of damage the earthquake causes (1-9)
remaining_damage (integer) The amount of damage this earthquake has yet to inflict (1-9)
damage_timer (integer) The number of hours until the earthquake next inflicts damage if active (0-16)
warning_timer (integer) The number of hours left until the real damaging earthquake begins (0-600)
current_map_earthquake (integer) The number of earthquakes that have happened on this map so far
disabled (boolean) If earthquakes have been disabled by the cheat menu
--]]

local damage_time = 16 -- Hours between each damage caused by an earthquake
local warning_period = 600 -- Hours between warning and main quakes
local warning_length = 25 -- Length of early warning quake

local month_length = {
31, -- Jan
28, -- Feb (29 in leap years, but TH doesn't have leap years)
31, -- Mar
30, -- Apr
31, -- May
30, -- Jun
31, -- Jul
31, -- Aug
30, -- Sep
31, -- Oct
30, -- Nov
31, -- Dec
}

--! Track earthquakes in the world
--!param world (table) Owned by this world
--!param convert (boolean) If true, world is upgraded from before this Earthquake class
function Earthquake:Earthquake(world, convert)
self.world = world
if not convert then
self.current_map_earthquake = 0
self:nextEarthquake()
end
end

--! Perform actions to simulate an active earthquake.
function Earthquake:tick()
if not self.active or self.disabled then return end
local hosp = self.world:getLocalPlayerHospital()
-- Check if this is the day that the earthquake is supposed to stop
if self.remaining_damage == 0 then
self.active = false
hosp:tickEarthquake("end")
-- If the earthquake measured more than 7 on the richter scale, tell the user about it
if self.size > 7 then
hosp:giveAdvice({_A.earthquake.ended:format(math.floor(self.size))})
end

-- Set up the next earthquake date
self:nextEarthquake()
return
end

-- Start of warning quake
if self.warning_timer == warning_period then
self.next_planned = false
hosp:tickEarthquake("warning_start")
end

-- End of warning quake
if self.warning_timer >= warning_period - warning_length and
self.warning_timer - self.world.hours_per_tick < warning_period - warning_length then
hosp:tickEarthquake("end")
end

if self.warning_timer > 0 then
self.warning_timer = self.warning_timer - self.world.hours_per_tick
-- Nothing more to do during inactive warning period
if self.warning_timer < warning_period - warning_length then
return
end

-- Start of real earthquake
if self.warning_timer <= 0 then
hosp:tickEarthquake("main_start")
end
end

-- All earthquakes start and end small (small earthquakes never become
-- larger), so when there has been less than 2 damage applied or only
-- 2 damage remaining to be applied, move the screen with less
-- intensity than otherwise.
if self.remaining_damage <= 2 or
self.size - self.remaining_damage <= 2 then
hosp:tickEarthquake("small_damage")
else
hosp:tickEarthquake("large_damage")
end

hosp:tickEarthquake("sound")

-- Do not continue to damage phase while in a warning quake
if self.warning_timer > 0 then
return
end

-- Check if damage phase should go ahead
self.damage_timer = self.damage_timer - self.world.hours_per_tick
if self.damage_timer > 0 then return end

for _, room in pairs(self.world.rooms) do
for object, _ in pairs(room.objects) do
if object.strength then
object:machineUsed(room)
end
end
end

self.remaining_damage = self.remaining_damage - 1
self.damage_timer = self.damage_timer + damage_time

-- Patient falling: work in progress, see PR 1848
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

*tips hat*

end

--! Check if it's time for an earthquake
function Earthquake:onEndDay()
if self.world.game_date:monthOfGame() == self.start_month and
self.world.game_date:dayOfMonth() == self.start_day and
not self.disabled then
-- Mark the planned earthquake as ready to start in the next tick.
self.active = true
end
end

--! Called when it is time to have another earthquake
function Earthquake:nextEarthquake()
self.active = false

local level_config = self.world.map.level_config
-- Check carefully that no value that we are going to use is going to be nil
if level_config.quake_control and level_config.quake_control[self.current_map_earthquake] and
level_config.quake_control[self.current_map_earthquake].Severity ~= 0 then
-- This map has rules to follow when making earthquakes, let's follow them
local control = level_config.quake_control[self.current_map_earthquake]
self.start_month = math.random(control.StartMonth, control.EndMonth)

-- Month length of the start of the earthquake. From start to finish
-- earthquakes do not persist for >= a month so we can wrap all days
-- after the start around the month length unambiguously.
local eqml = month_length[(self.start_month % 12) + 1]
self.start_day = math.random(1, eqml)

self.size = control.Severity
self.remaining_damage = self.size
self.damage_timer = damage_time
self.warning_timer = warning_period
self.current_map_earthquake = self.current_map_earthquake + 1
self.next_planned = true
end
end

--! Create a small or medium quake now for the "Create Earthquake" cheat
function Earthquake:createEarthquake()
-- Make sure an earthquake isn't already happening, or are disabled
if self.active or self.disabled then return false end

self.start_day = self.world.game_date:dayOfMonth()
self.start_month = self.world.game_date:monthOfGame()
if not self.next_planned then
-- Forcefully make an earthquake if there are none left in level file
self.size = math.random(1, 6) -- Above 6 seems disastrous
self.remaining_damage = self.size
self.damage_timer = damage_time
self.warning_timer = warning_period
end
end

--! Check if there is a current earthquake
--!return (boolean) True if there is a current earthquake
function Earthquake:isActive()
return self.active
end

--! Afterload sections before 189 were taken from World
--!param old (integer) The old version of the save game.
--!param new (integer) The current version of the save game format.
function Earthquake:afterLoad(old, new) -- luacheck: ignore 212 keep params from parent function
local old_quake = self.world.next_earthquake
if old < 115 then
if old_quake.active then
local rd = 0
for _, room in pairs(self.world.rooms) do
for object, _ in pairs(room.objects) do
if object.quake_points then
rd = math.max(rd, object.quake_points)
object.quake_points = nil
end
end
end
old_quake.remaining_damage = rd
old_quake.damage_timer = damage_time
old_quake.warning_timer = 0
end
end
Comment on lines +215 to +230
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is from 0.61... I don't think we would be at any risk of removing this afterload too.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now it's done, I'll leave it for a mass clean up.


if old < 189 then
self.disabled = self.world.earthquakes_disabled -- Used in cheats
if old_quake then
for k, v in pairs(old_quake) do
self[k] = v
end
else
self:nextEarthquake()
end
end
end
4 changes: 4 additions & 0 deletions CorsixTH/Lua/hospital.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2496,3 +2496,7 @@ end
--! Play a sound file
function Hospital:playSound(sound)
end

--! The UI parts of earthquake ticks
function Hospital:tickEarthquake(stage)
end
45 changes: 43 additions & 2 deletions CorsixTH/Lua/hospitals/player_hospital.lua
Original file line number Diff line number Diff line change
Expand Up @@ -469,10 +469,10 @@ end
--!param room The room of the machine
function PlayerHospital:announceRepair(room)
local sound = room.room_info.handyman_call_sound
local earthquake = self.world.next_earthquake
local earthquake = self.world.earthquake
self.world.ui:playAnnouncement("machwarn.wav", AnnouncementPriority.Critical)
-- If an earthquake is happening don't play the call sound to prevent spamming
if earthquake.active and earthquake.warning_timer == 0 then return end
if earthquake:isActive() and earthquake.warning_timer == 0 then return end
if self:countStaffOfCategory("Handyman", 1) == 0 then return end
if sound then self.world.ui:playAnnouncement(sound, AnnouncementPriority.Critical) end
end
Expand Down Expand Up @@ -774,6 +774,47 @@ function PlayerHospital:playSound(sound)
end
end

--! The UI parts of earthquake ticks
--!param stage (string) Stage of the active earthquake. This must be one of the following,
-- "warning_start", "main_start", "end", "pause", "small_damage", "large damage", "sound"
function PlayerHospital:tickEarthquake(stage)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The trouble here is an earthquake is a world event. The announcements aren't though.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are the UI parts of the world event, though there may be more from competitor hospitals later.

local ui = self.world.ui
local announcements = {
"quake001.wav", "quake002.wav", "quake003.wav", "quake004.wav",
}

-- Start of earthquakes
if stage == "warning_start" then
ui:beginShakeScreen(0.2)
ui:playRandomAnnouncement(announcements, AnnouncementPriority.Critical)
elseif stage == "main_start" then
ui:playRandomAnnouncement(announcements, AnnouncementPriority.Critical)

-- At the end of the warning or main earthquake, or on game pause, stop all screen movement
elseif stage == "end" or stage == "pause" then
ui:endShakeScreen()

-- All earthquakes start and end small (small earthquakes never become
-- larger), so when there has been less than 2 damage applied or only
-- 2 damage remaining to be applied, move the screen with less
-- intensity than otherwise.
elseif stage == "small_damage" then
ui:beginShakeScreen(0.5)
elseif stage == "large_damage" then
ui:beginShakeScreen(1)

elseif stage == "sound" then
-- Play the earthquake sound. It has different names depending on what the language contains.
if self.world.app.audio:soundExists("quake2.wav") then
ui:playSound("quake2.wav")
else
ui:playSound("quake.wav")
end
else
assert(false, "Unknown stage: " .. (stage or "nil"))
end
end

function PlayerHospital:afterLoad(old, new)
if old < 146 then
self.adviser_data = {
Expand Down
Loading