diff --git a/changelog.txt b/changelog.txt index 9b676acc1..ff710b92a 100644 --- a/changelog.txt +++ b/changelog.txt @@ -31,6 +31,8 @@ Template for new versions: ## New Features ## Fixes +- `starvingdead`: properly restore to correct enabled state when loading a new game that is different from the first game loaded in this session +- `starvingdead`: ensure undead decay does not happen faster than the declared decay rate when saving and loading the game ## Misc Improvements diff --git a/docs/starvingdead.rst b/docs/starvingdead.rst index 12f7efa14..84faaf950 100644 --- a/docs/starvingdead.rst +++ b/docs/starvingdead.rst @@ -10,11 +10,11 @@ gradually decay, losing strength, speed, and toughness. After six months, they collapse upon themselves, never to be reanimated. Strength lost is proportional to the time until death, all units will have -roughly 10% of each of their attributes' values when close to being removed. +roughly 10% of each of their attributes' values when they succumb to decay. In any game, this can be a welcome gameplay feature, but it is especially -useful in preventing undead cascades in the caverns in reanimating biomes, -where constant combat can lead to hundreds of undead roaming the caverns and +useful in preventing undead cascades in the caverns in reanimating biomes. +Constant combat can lead to hundreds of undead roaming the caverns and destroying your FPS. Usage diff --git a/emigration.lua b/emigration.lua index 1bba9810e..fd18f2129 100644 --- a/emigration.lua +++ b/emigration.lua @@ -9,7 +9,10 @@ local unit_link_utils = reqscript('internal/emigration/unit-link-utils') local GLOBAL_KEY = 'emigration' -- used for state change hooks and persistence local function get_default_state() - return {enabled=false, last_cycle_tick=0} + return { + enabled=false, + last_cycle_tick=0 + } end state = state or get_default_state() @@ -127,15 +130,15 @@ function checkmigrationnow() end local function event_loop() - if state.enabled then - local current_tick = dfhack.world.ReadCurrentTick() + TICKS_PER_YEAR * dfhack.world.ReadCurrentYear() - if current_tick - state.last_cycle_tick < TICKS_PER_MONTH then - local timeout_ticks = state.last_cycle_tick - current_tick + TICKS_PER_MONTH - dfhack.timeout(timeout_ticks, 'ticks', event_loop) - else - checkmigrationnow() - dfhack.timeout(1, 'months', event_loop) - end + if not state.enabled then return end + + local current_tick = dfhack.world.ReadCurrentTick() + TICKS_PER_YEAR * dfhack.world.ReadCurrentYear() + if current_tick - state.last_cycle_tick < TICKS_PER_MONTH then + local timeout_ticks = state.last_cycle_tick - current_tick + TICKS_PER_MONTH + dfhack.timeout(timeout_ticks, 'ticks', event_loop) + else + checkmigrationnow() + dfhack.timeout(1, 'months', event_loop) end end diff --git a/starvingdead.lua b/starvingdead.lua index 42742d5a2..5518676ad 100644 --- a/starvingdead.lua +++ b/starvingdead.lua @@ -3,128 +3,153 @@ --@module = true local argparse = require('argparse') +local utils = require('utils') local GLOBAL_KEY = 'starvingdead' -starvingDeadInstance = starvingDeadInstance or nil +local function get_default_state() + return { + enabled=false, + decay_rate=1, + death_threshold=6, + last_cycle_tick=0, + } +end + +state = state or get_default_state() function isEnabled() - return starvingDeadInstance ~= nil + return state.enabled end local function persist_state() - dfhack.persistent.saveSiteData(GLOBAL_KEY, { - enabled = isEnabled(), - decay_rate = starvingDeadInstance and starvingDeadInstance.decay_rate or 1, - death_threshold = starvingDeadInstance and starvingDeadInstance.death_threshold or 6 - }) + dfhack.persistent.saveSiteData(GLOBAL_KEY, state) end -dfhack.onStateChange[GLOBAL_KEY] = function(sc) - if sc == SC_MAP_UNLOADED then - enabled = false - return - end - - if sc ~= SC_MAP_LOADED or df.global.gamemode ~= df.game_mode.DWARF then - return - end - - local persisted_data = dfhack.persistent.getSiteData(GLOBAL_KEY, {}) - - if persisted_data.enabled then - starvingDeadInstance = StarvingDead{ - decay_rate = persisted_data.decay_rate, - death_threshold = persisted_data.death_threshold - } - end -end - -StarvingDead = defclass(StarvingDead) -StarvingDead.ATTRS{ - decay_rate = 1, - death_threshold = 6, -} - -function StarvingDead:init() - self.timeout_id = nil - -- Percentage goal each attribute should reach before death. - local attribute_goal = 10 - self.attribute_decay = (attribute_goal ^ (1 / ((self.death_threshold * 28 / self.decay_rate)))) / 100 - - self:checkDecay() - print(([[StarvingDead started, checking every %s days and killing off at %s months]]):format(self.decay_rate, self.death_threshold)) -end - -function StarvingDead:checkDecay() - for _, unit in pairs(df.global.world.units.active) do - if (unit.enemy.undead and not unit.flags1.inactive) then - -- time_on_site is measured in ticks, a month is 33600 ticks. - -- @see https://dwarffortresswiki.org/index.php/Time - for _, attribute in pairs(unit.body.physical_attrs) do - attribute.value = math.floor(attribute.value - (attribute.value * self.attribute_decay)) - end - - if unit.curse.interaction.time_on_site > (self.death_threshold * 33600) then - unit.animal.vanish_countdown = 1 - end +-- threshold each attribute should reach before death. +local ATTRIBUTE_THRESHOLD_PERCENT = 10 + +local TICKS_PER_DAY = 1200 +local TICKS_PER_MONTH = 28 * TICKS_PER_DAY +local TICKS_PER_YEAR = 12 * TICKS_PER_MONTH + +local function do_decay() + local decay_exponent = state.decay_rate / (state.death_threshold * 28) + local attribute_decay = (ATTRIBUTE_THRESHOLD_PERCENT ^ decay_exponent) / 100 + + for _, unit in pairs(df.global.world.units.active) do + if (unit.enemy.undead and not unit.flags1.inactive) then + for _,attribute in pairs(unit.body.physical_attrs) do + attribute.value = math.floor(attribute.value - (attribute.value * attribute_decay)) + end + + if unit.curse.interaction.time_on_site > (state.death_threshold * TICKS_PER_MONTH) then + unit.animal.vanish_countdown = 1 + end + end end - end +end - self.timeout_id = dfhack.timeout(self.decay_rate, 'days', self:callback('checkDecay')) +local function get_normalized_tick() + return dfhack.world.ReadCurrentTick() + TICKS_PER_YEAR * dfhack.world.ReadCurrentYear() end -if dfhack_flags.module then - return +timeout_id = timeout_id or nil + +local function event_loop() + if not state.enabled then return end + + local current_tick = get_normalized_tick() + local ticks_per_cycle = TICKS_PER_DAY * state.decay_rate + local timeout_ticks = ticks_per_cycle + + if current_tick - state.last_cycle_tick < ticks_per_cycle then + timeout_ticks = state.last_cycle_tick - current_tick + ticks_per_cycle + else + do_decay() + state.last_cycle_tick = current_tick + persist_state() + end + timeout_id = dfhack.timeout(timeout_ticks, 'ticks', event_loop) end -local options, args = { - decay_rate = nil, - death_threshold = nil -}, {...} +local function do_enable() + if state.enabled then return end -local positionals = argparse.processArgsGetopt(args, { - {'h', 'help', handler = function() options.help = true end}, - {'r', 'decay-rate', hasArg = true, handler=function(arg) options.decay_rate = argparse.positiveInt(arg, 'decay-rate') end }, - {'t', 'death-threshold', hasArg = true, handler=function(arg) options.death_threshold = argparse.positiveInt(arg, 'death-threshold') end }, -}) + state.enabled = true + state.last_cycle_tick = get_normalized_tick() + event_loop() +end -if dfhack_flags.enable then - if dfhack_flags.enable_state then - if starvingDeadInstance then - return +local function do_disable() + if not state.enabled then return end + + state.enabled = false + if timeout_id then + dfhack.timeout_active(timeout_id, nil) + timeout_id = nil end +end - starvingDeadInstance = StarvingDead{} - persist_state() - else - if not starvingDeadInstance then - return +dfhack.onStateChange[GLOBAL_KEY] = function(sc) + if sc == SC_MAP_UNLOADED then + do_disable() + return end - dfhack.timeout_active(starvingDeadInstance.timeout_id, nil) - starvingDeadInstance = nil - end -else - if not dfhack.isMapLoaded() then - qerror('This script requires a fortress map to be loaded') - end + if sc ~= SC_MAP_LOADED or not dfhack.world.isFortressMode() then + return + end - if positionals[1] == "help" or options.help then - print(dfhack.script_help()) + state = get_default_state() + utils.assign(state, dfhack.persistent.getSiteData(GLOBAL_KEY, state)) + + event_loop() +end + +if dfhack_flags.module then return - end +end - if positionals[1] == nil then - if starvingDeadInstance then - starvingDeadInstance.decay_rate = options.decay_rate or starvingDeadInstance.decay_rate - starvingDeadInstance.death_threshold = options.death_threshold or starvingDeadInstance.death_threshold +if not dfhack.isMapLoaded() or not dfhack.world.isFortressMode() then + qerror('This script requires a fortress map to be loaded') +end - print(([[StarvingDead is running, checking every %s days and killing off at %s months]]):format( - starvingDeadInstance.decay_rate, starvingDeadInstance.death_threshold - )) +if dfhack_flags.enable then + if dfhack_flags.enable_state then + do_enable() else - print("StarvingDead is not running!") + do_disable() end - end +end + +local opts = {} +local positionals = argparse.processArgsGetopt({...}, { + {'h', 'help', handler=function() opts.help = true end}, + {'r', 'decay-rate', hasArg=true, + handler=function(arg) opts.decay_rate = argparse.positiveInt(arg, 'decay-rate') end }, + {'t', 'death-threshold', hasArg=true, + handler=function(arg) opts.death_threshold = argparse.positiveInt(arg, 'death-threshold') end }, +}) + + +if positionals[1] == "help" or opts.help then + print(dfhack.script_help()) + return +end + +if opts.decay_rate then + state.decay_rate = opts.decay_rate +end +if opts.death_threshold then + state.death_threshold = opts.death_threshold +end +persist_state() + +if state.enabled then + print(([[StarvingDead is running, decaying undead every %s day%s and killing off at %s month%s]]):format( + state.decay_rate, state.decay_rate == 1 and '' or 's', state.death_threshold, state.death_threshold == 1 and '' or 's')) +else + print(([[StarvingDead is not running, but would decay undead every %s day%s and kill off at %s month%s]]):format( + state.decay_rate, state.decay_rate == 1 and '' or 's', state.death_threshold, state.death_threshold == 1 and '' or 's')) end