Skip to content

Commit

Permalink
Merge pull request #2484 from tobylane/newgoals2
Browse files Browse the repository at this point in the history
Add groups to win/lose criteria
  • Loading branch information
TheCycoONE committed May 12, 2024
2 parents 29e9c60 + ddd1cf0 commit efc21ac
Show file tree
Hide file tree
Showing 5 changed files with 292 additions and 215 deletions.
4 changes: 2 additions & 2 deletions .luacheckrc
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ globals = { -- Globals
-- Game classes
"AIHospital", "AnimationManager", "AnimationEffect", "App", "Audio",
"CallsDispatcher", "Cheats", "ChildClass", "Command", "Door", "DrawFlags",
"DummyRootNode", "Entity", "EntityMap", "Epidemic", "FileSystem",
"FileTreeNode", "FilteredFileTreeNode", "GameUI", "Graphics",
"DummyRootNode", "EndConditions", "Entity", "EntityMap", "Epidemic",
"FileSystem", "FileTreeNode", "FilteredFileTreeNode", "GameUI", "Graphics",
"GrimReaper", "Hospital", "Humanoid", "HumanoidRawWalk",
"Inspector", "LoadGame", "LoadGameFile", "Litter", "Machine",
"Map", "MoviePlayer", "NoRealClass", "Object", "ParentClass",
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 = 187 -- Add tired values to level config
local SAVEGAME_VERSION = 188 -- Add groups to win and lose criteria.

class "App"

Expand Down
97 changes: 38 additions & 59 deletions CorsixTH/Lua/dialogs/fullscreen/progress_report.lua
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ function UIProgressReport:UIProgressReport(ui)
self:UIFullscreen(ui)

local world = self.ui.app.world
local hospital = ui.hospital
local gfx = ui.app.gfx

if not pcall(function()
Expand All @@ -49,53 +48,34 @@ function UIProgressReport:UIProgressReport(ui)

-- Selected hospital number
self.selected = 1
local hospital = world.hospitals[self.selected]

-- Collect which criteria to show here, draw columns in draw
-- Add the icons for the criteria
local x = 263
local world_goals = world.goals
for _, tab in ipairs(world_goals) do
local crit_name = world.level_criteria[tab.criterion].name
local res_value = world_goals[crit_name].win_value
world_goals[crit_name].visible = true
-- Special case for money, subtract loans
local cur_value = hospital[crit_name]
if crit_name == "balance" then
cur_value = cur_value - hospital.loan
local crit_data = world.endconditions:generateReportTable(hospital)
for _, crit_table in ipairs(crit_data) do
crit_table.visible = true
local crit_name = crit_table.name
local res_value = crit_table.win_value
local cur_value = world.endconditions:getAttribute(hospital, crit_name)
if crit_table.lose_value then
crit_table.red = true
res_value = crit_table.lose_value
end
if world_goals[crit_name].lose_value then
world_goals[crit_name].red = false

if cur_value < world_goals[crit_name].boundary then
world_goals[crit_name].red = true
res_value = world_goals[crit_name].lose_value
-- TODO: Make the ugly workaround for the special case "percentage_killed" better
if crit_name:find("killed") then
res_value = nil
world_goals[crit_name].visible = false
end
elseif not world_goals[crit_name].win_value then
world_goals[crit_name].visible = false
end
end
-- Only five criteria can be there at once.
if crit_name:find("killed") and world.winning_goal_count > 5 then
res_value = nil
world_goals[crit_name].visible = false
end
if res_value then
-- FIXME: res_value and cure_value are depersisted as floating points, using
-- string.format("%.0f", x) is not suitable due to %d (num) param in _S string
local tooltip
if world.level_criteria[tab.criterion].formats == 2 then
tooltip = _S.tooltip.status[crit_name]:format(math.floor(res_value), math.floor(cur_value))
else
tooltip = _S.tooltip.status[crit_name]:format(math.floor(res_value))
end
self:addPanel(world.level_criteria[tab.criterion].icon, x, 240)
self:makeTooltip(tooltip, x, 180, x + 30, 180 + 90)
x = x + 30
-- FIXME: res_value and cure_value are depersisted as floating points, using
-- string.format("%.0f", x) is not suitable due to %d (num) param in _S string
local tooltip
if crit_table.formats == 2 then
tooltip = _S.tooltip.status[crit_name]:format(math.floor(res_value), math.floor(cur_value))
else
tooltip = _S.tooltip.status[crit_name]:format(math.floor(res_value))
end
self:addPanel(crit_table.icon, x, 240)
self:makeTooltip(tooltip, x, 180, x + 30, 180 + 90)
x = x + 30
end
self.crit_data = crit_data

self:addPanel(0, 606, 447):makeButton(0, 0, 26, 26, 8, self.close):setTooltip(_S.tooltip.status.close)

Expand Down Expand Up @@ -182,9 +162,8 @@ function UIProgressReport:draw(canvas, x, y)
UIFullscreen.draw(self, canvas, x, y)

x, y = self.x + x, self.y + y
local hospital = self.ui.hospital
local world = hospital.world
local world_goals = world.goals
local world = self.ui.app.world
local hospital = world.hospitals[self.selected]

-- Names of the players playing
local ly = 73
Expand All @@ -194,23 +173,19 @@ function UIProgressReport:draw(canvas, x, y)
ly = ly + 25
end

-- Draw the vertical bars for the winning conditions
-- Draw the vertical bars for the selected conditions
local lx = 270
for _, tab in ipairs(world_goals) do
local crit_name = world.level_criteria[tab.criterion].name
if world_goals[crit_name].visible then
local sprite_offset = world_goals[crit_name].red and 2 or 0
local cur_value = hospital[crit_name]
-- Balance is special
if crit_name == "balance" then
cur_value = cur_value - hospital.loan
end
for _, crit_table in ipairs(self.crit_data) do
if crit_table.visible then
local sprite_offset = crit_table.red and 2 or 0
local crit_name = crit_table.name
local cur_value = world.endconditions:getAttribute(hospital, crit_name)
local height
if world_goals[crit_name].red then
local lose = world_goals[crit_name].lose_value
height = 1 + 49 * (1 - ((cur_value - lose)/(world_goals[crit_name].boundary - lose)))
if crit_table.red then
local lose = crit_table.lose_value
height = 1 + 49 * (1 - ((cur_value - lose)/(crit_table.boundary - lose)))
else
height = 1 + 49 * (cur_value/world_goals[crit_name].win_value)
height = 1 + 49 * (cur_value/crit_table.win_value)
end
if height > 50 then height = 50 end
local result_y = 0
Expand Down Expand Up @@ -244,5 +219,9 @@ function UIProgressReport:afterLoad(old, new)
self.panel_sprites = gfx:loadSpriteTable("QData", "Rep02V", true, palette)
end

if old < 188 then
self:close()
end

UIFullscreen.afterLoad(self, old, new)
end
235 changes: 235 additions & 0 deletions CorsixTH/Lua/endconditions.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
--[[ 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. --]]

-- The "Winning and Losing Conditions" section of level files uses Criteria
-- numbers from the order of this table.
-- Icon and formats are used in the progress report dialog.
local local_criteria_variable = {
{name = "reputation", icon = 10, formats = 2},
{name = "balance", icon = 11, formats = 2},
{name = "percentage_cured", icon = 12, formats = 2},
{name = "num_cured" , icon = 13, formats = 2},
{name = "percentage_killed",icon = 14, formats = 2},
{name = "value", icon = 15, formats = 2}, -- Hospital value
}

-- A table of functions for fetching criteria values that cannot be measured
-- directly from a hospital attribute of the same name.
local get_custom_criteria = {
balance = function(hospital) return hospital.balance - hospital.loan end,
}

class "EndConditions"
---@type EndConditions
local EndConditions = _G["EndConditions"]

--! Collect the end conditions for this level, if they exist
--!param level_config (table) The map's level_config, containing criteria.
--!param level_number (integer) The number of the map, if playing a campaign.
--!param freebuild (boolean) The free build status of the world
function EndConditions:EndConditions(level_config, level_number, freebuild)
self.win_goals, self.lose_goals, self.highest_group = {}, {}, 0
if freebuild then return end

local start, town = {}
if level_number and level_config.towns[level_number] then
town = level_config.towns[level_number]
else
town = level_config.town
end
start.balance = town.StartCash
start.reputation = town.StartRep

self:_loadGoals(level_config.win_criteria, self.win_goals, start, true)
self:_loadGoals(level_config.lose_criteria, self.lose_goals, start)
end

--! Load conditions (goals) to win and lose from the level config,
-- and store them in self.win_goals and self.lose_goals in their groups.
-- Put the highest number of group of conditions in self.highest_group.
-- These groups are often incomplete, possibly empty
-- ie more groups of conditions that lead to loss than win.
--!param criteria_tbl (table) The map's win or lose criteria.
--!param goals (table) The class table to fill
--!param start (table) The starting values of the hospital attributes
--!param win (boolean) If the win goals are being filled this time
function EndConditions:_loadGoals(criteria_tbl, goals, start, win)
for _, values in pairs(criteria_tbl) do
if values.Criteria ~= 0 then
local crit_name = local_criteria_variable[values.Criteria].name
if not goals[values.Group] then goals[values.Group] = {} end
goals[values.Group][crit_name] = {
name = crit_name,
boundary = values.Bound,
criterion = values.Criteria,
max_min = values.MaxMin,
icon = local_criteria_variable[values.Criteria].icon,
formats = local_criteria_variable[values.Criteria].formats,
start = start[crit_name] or 0,
}
if win then
goals[values.Group][crit_name].win_value = values.Value
else
goals[values.Group][crit_name].lose_value = values.Value
end
if values.Group > self.highest_group then self.highest_group = values.Group end
end
end
end

--! Checks if the player has won or lost by meeting all of any one group.
--!param hospital (Hospital) The hospital of the tests.
--!return state (string) "win" or "nothing", or
--!return reason (string) If the player lost, the latest criteria met
--!return limit (number) If the player lost, the number limit which the player passed
function EndConditions:checkEndGame(hospital)
-- If there are no goals at all, do nothing.
if (not self.win_goals or #self.win_goals == 0) and
(not self.lose_goals or #self.lose_goals == 0) then
return "nothing"
end
for _, tbl in pairs(self.win_goals) do
local score = self:_checkWinGroup(hospital, tbl)
if score == 1 and hospital.loan == 0 then return "win" end
end
for _, tbl in pairs(self.lose_goals) do
local reason, limit = self:_checkLoseGroup(hospital, tbl)
if reason then return reason, limit end
end

-- No win or lose group was met, or player has a loan preventing a win
return "nothing"
end

--! Generate table for the Progress Report dialog and progress advice.
--!param hospital (Hospital) The hospital of the tests.
--!return report_table (table) Maximum five fields of
-- lose criteria with the smallest gap between current value and boundary,
-- then fill up to five with win criteria in the best group.
function EndConditions:generateReportTable(hospital)
local count, lose_table, report_table, tmp_table = 0, {}, {}, {}
local win_group = self.win_goals[self:_findBestWinGroup(hospital)] or {}

-- Collect lose criteria over the boundary
for group, tbl in pairs(self.lose_goals) do
lose_table[group] = self:_checkLoseGroup(hospital, tbl, true)
end
-- Get the most relevant of each criterion in all groups
for _, group_table in pairs(lose_table) do
for crit_name, crit_table in pairs(group_table) do
if not tmp_table[crit_name] or tmp_table[crit_name].gap > crit_table.gap then
tmp_table[crit_name] = crit_table
end
end
end
-- Move into a numbered table
for _, crit_table in pairs(tmp_table) do
table.insert(report_table, crit_table)
count = count + 1
if count == 5 then break end
end

-- Fill up the report table with win criteria not already present as lose criteria
for i = 1, #local_criteria_variable do
local name = local_criteria_variable[i].name
if win_group[name] and not tmp_table[name] then
count = count + 1
report_table[count] = win_group[name]
if count == 5 then break end
end
end

-- Some criteria icons shouldn't be next to each other
table.sort(report_table, function(a,b) return a.criterion < b.criterion end)
return report_table
end

--!param hospital (Hospital) The hospital of the tests.
--!param lose_table (table) A group of lose conditions from level_config.
--!param report (boolean) Whether a report table will be returned.
--!return Losing criteria name and the limit breached,
-- or if report is true, the report table.
function EndConditions:_checkLoseGroup(hospital, lose_table, report)
local report_table, met_count, total_count, reason, limit = {}, 0, 0
for crit_name, crit_table in pairs(lose_table) do
local target = report and crit_table.boundary or crit_table.lose_value
local max_min = crit_table.max_min == 1 and 1 or -1
local measure = self:getAttribute(hospital, crit_name)
if (measure - target) * max_min > 0 then
if report then -- Collect the criteria that should be reported on
report_table[crit_name] = crit_table
report_table[crit_name].gap = math.abs(measure - target)
else
reason, limit = crit_name, crit_table.lose_value
met_count = met_count + 1
end
end
total_count = total_count + 1
end
if report then return report_table end
-- Have all criteria of the group been met?
if met_count == total_count then
-- The latest, probably only, criterion met for the lose message
return reason, limit
end
end

--!param hospital (Hospital) The hospital of the tests.
--!param win_table (table) A group of win conditions from level_config
--!return (number 0-1) The score of this group. 0 is no goals met, 1 is all met
function EndConditions:_checkWinGroup(hospital, win_table)
local met_count, total_count = 0, 0
for crit_name, crit_table in pairs(win_table) do
local max_min = crit_table.max_min == 1 and 1 or -1
if (self:getAttribute(hospital, crit_name) - crit_table.win_value) * max_min >= 0 then
met_count = met_count + 1
end
total_count = total_count + 1
end
if met_count > 0 then
return met_count / total_count
else return 0
end
end

-- Find the group of win conditions best met by the hospital.
--!param hospital (Hospital) The hospital of the tests.
--!return best (number) The number of the best group.
function EndConditions:_findBestWinGroup(hospital)
local score, best = 0, 1
for group = 1, self.highest_group do
if self.win_goals[group] then
local test = self:_checkWinGroup(hospital, self.win_goals[group])
if test and test > score then best = group end
end
end
-- Return the group number of the group that is most met by the hospital
return best
end

-- Fetch the attribute value, through the get_custom_criteria table of
-- functions if there is one for this attribute.
function EndConditions:getAttribute(hospital, attribute)
if get_custom_criteria[attribute] then
return get_custom_criteria[attribute](hospital)
else
return hospital[attribute]
end
end
Loading

0 comments on commit efc21ac

Please sign in to comment.