|
3 | 3 | --@module = true
|
4 | 4 |
|
5 | 5 | local argparse = require('argparse')
|
| 6 | +local utils = require('utils') |
6 | 7 |
|
7 | 8 | local GLOBAL_KEY = 'starvingdead'
|
8 | 9 |
|
9 |
| -starvingDeadInstance = starvingDeadInstance or nil |
| 10 | +local function get_default_state() |
| 11 | + return { |
| 12 | + enabled=false, |
| 13 | + decay_rate=1, |
| 14 | + death_threshold=6, |
| 15 | + last_cycle_tick=0, |
| 16 | + } |
| 17 | +end |
| 18 | + |
| 19 | +state = state or get_default_state() |
10 | 20 |
|
11 | 21 | function isEnabled()
|
12 |
| - return starvingDeadInstance ~= nil |
| 22 | + return state.enabled |
13 | 23 | end
|
14 | 24 |
|
15 | 25 | local function persist_state()
|
16 |
| - dfhack.persistent.saveSiteData(GLOBAL_KEY, { |
17 |
| - enabled = isEnabled(), |
18 |
| - decay_rate = starvingDeadInstance and starvingDeadInstance.decay_rate or 1, |
19 |
| - death_threshold = starvingDeadInstance and starvingDeadInstance.death_threshold or 6 |
20 |
| - }) |
| 26 | + dfhack.persistent.saveSiteData(GLOBAL_KEY, state) |
21 | 27 | end
|
22 | 28 |
|
23 |
| -dfhack.onStateChange[GLOBAL_KEY] = function(sc) |
24 |
| - if sc == SC_MAP_UNLOADED then |
25 |
| - enabled = false |
26 |
| - return |
27 |
| - end |
28 |
| - |
29 |
| - if sc ~= SC_MAP_LOADED or df.global.gamemode ~= df.game_mode.DWARF then |
30 |
| - return |
31 |
| - end |
32 |
| - |
33 |
| - local persisted_data = dfhack.persistent.getSiteData(GLOBAL_KEY, {}) |
34 |
| - |
35 |
| - if persisted_data.enabled then |
36 |
| - starvingDeadInstance = StarvingDead{ |
37 |
| - decay_rate = persisted_data.decay_rate, |
38 |
| - death_threshold = persisted_data.death_threshold |
39 |
| - } |
40 |
| - end |
41 |
| -end |
42 |
| - |
43 |
| -StarvingDead = defclass(StarvingDead) |
44 |
| -StarvingDead.ATTRS{ |
45 |
| - decay_rate = 1, |
46 |
| - death_threshold = 6, |
47 |
| -} |
48 |
| - |
49 |
| -function StarvingDead:init() |
50 |
| - self.timeout_id = nil |
51 |
| - -- Percentage goal each attribute should reach before death. |
52 |
| - local attribute_goal = 10 |
53 |
| - self.attribute_decay = (attribute_goal ^ (1 / ((self.death_threshold * 28 / self.decay_rate)))) / 100 |
54 |
| - |
55 |
| - self:checkDecay() |
56 |
| - print(([[StarvingDead started, checking every %s days and killing off at %s months]]):format(self.decay_rate, self.death_threshold)) |
57 |
| -end |
58 |
| - |
59 |
| -function StarvingDead:checkDecay() |
60 |
| - for _, unit in pairs(df.global.world.units.active) do |
61 |
| - if (unit.enemy.undead and not unit.flags1.inactive) then |
62 |
| - -- time_on_site is measured in ticks, a month is 33600 ticks. |
63 |
| - -- @see https://dwarffortresswiki.org/index.php/Time |
64 |
| - for _, attribute in pairs(unit.body.physical_attrs) do |
65 |
| - attribute.value = math.floor(attribute.value - (attribute.value * self.attribute_decay)) |
66 |
| - end |
67 |
| - |
68 |
| - if unit.curse.interaction.time_on_site > (self.death_threshold * 33600) then |
69 |
| - unit.animal.vanish_countdown = 1 |
70 |
| - end |
| 29 | +-- threshold each attribute should reach before death. |
| 30 | +local ATTRIBUTE_THRESHOLD_PERCENT = 10 |
| 31 | + |
| 32 | +local TICKS_PER_DAY = 1200 |
| 33 | +local TICKS_PER_MONTH = 28 * TICKS_PER_DAY |
| 34 | +local TICKS_PER_YEAR = 12 * TICKS_PER_MONTH |
| 35 | + |
| 36 | +local function do_decay() |
| 37 | + local decay_exponent = state.decay_rate / (state.death_threshold * 28) |
| 38 | + local attribute_decay = (ATTRIBUTE_THRESHOLD_PERCENT ^ decay_exponent) / 100 |
| 39 | + |
| 40 | + for _, unit in pairs(df.global.world.units.active) do |
| 41 | + if (unit.enemy.undead and not unit.flags1.inactive) then |
| 42 | + for _,attribute in pairs(unit.body.physical_attrs) do |
| 43 | + attribute.value = math.floor(attribute.value - (attribute.value * attribute_decay)) |
| 44 | + end |
| 45 | + |
| 46 | + if unit.curse.interaction.time_on_site > (state.death_threshold * TICKS_PER_MONTH) then |
| 47 | + unit.animal.vanish_countdown = 1 |
| 48 | + end |
| 49 | + end |
71 | 50 | end
|
72 |
| - end |
| 51 | +end |
73 | 52 |
|
74 |
| - self.timeout_id = dfhack.timeout(self.decay_rate, 'days', self:callback('checkDecay')) |
| 53 | +local function get_normalized_tick() |
| 54 | + return dfhack.world.ReadCurrentTick() + TICKS_PER_YEAR * dfhack.world.ReadCurrentYear() |
75 | 55 | end
|
76 | 56 |
|
77 |
| -if dfhack_flags.module then |
78 |
| - return |
| 57 | +timeout_id = timeout_id or nil |
| 58 | + |
| 59 | +local function event_loop() |
| 60 | + if not state.enabled then return end |
| 61 | + |
| 62 | + local current_tick = get_normalized_tick() |
| 63 | + local ticks_per_cycle = TICKS_PER_DAY * state.decay_rate |
| 64 | + local timeout_ticks = ticks_per_cycle |
| 65 | + |
| 66 | + if current_tick - state.last_cycle_tick < ticks_per_cycle then |
| 67 | + timeout_ticks = state.last_cycle_tick - current_tick + ticks_per_cycle |
| 68 | + else |
| 69 | + do_decay() |
| 70 | + state.last_cycle_tick = current_tick |
| 71 | + persist_state() |
| 72 | + end |
| 73 | + timeout_id = dfhack.timeout(timeout_ticks, 'ticks', event_loop) |
79 | 74 | end
|
80 | 75 |
|
81 |
| -local options, args = { |
82 |
| - decay_rate = nil, |
83 |
| - death_threshold = nil |
84 |
| -}, {...} |
| 76 | +local function do_enable() |
| 77 | + if state.enabled then return end |
85 | 78 |
|
86 |
| -local positionals = argparse.processArgsGetopt(args, { |
87 |
| - {'h', 'help', handler = function() options.help = true end}, |
88 |
| - {'r', 'decay-rate', hasArg = true, handler=function(arg) options.decay_rate = argparse.positiveInt(arg, 'decay-rate') end }, |
89 |
| - {'t', 'death-threshold', hasArg = true, handler=function(arg) options.death_threshold = argparse.positiveInt(arg, 'death-threshold') end }, |
90 |
| -}) |
| 79 | + state.enabled = true |
| 80 | + state.last_cycle_tick = get_normalized_tick() |
| 81 | + event_loop() |
| 82 | +end |
91 | 83 |
|
92 |
| -if dfhack_flags.enable then |
93 |
| - if dfhack_flags.enable_state then |
94 |
| - if starvingDeadInstance then |
95 |
| - return |
| 84 | +local function do_disable() |
| 85 | + if not state.enabled then return end |
| 86 | + |
| 87 | + state.enabled = false |
| 88 | + if timeout_id then |
| 89 | + dfhack.timeout_active(timeout_id, nil) |
| 90 | + timeout_id = nil |
96 | 91 | end
|
| 92 | +end |
97 | 93 |
|
98 |
| - starvingDeadInstance = StarvingDead{} |
99 |
| - persist_state() |
100 |
| - else |
101 |
| - if not starvingDeadInstance then |
102 |
| - return |
| 94 | +dfhack.onStateChange[GLOBAL_KEY] = function(sc) |
| 95 | + if sc == SC_MAP_UNLOADED then |
| 96 | + do_disable() |
| 97 | + return |
103 | 98 | end
|
104 | 99 |
|
105 |
| - dfhack.timeout_active(starvingDeadInstance.timeout_id, nil) |
106 |
| - starvingDeadInstance = nil |
107 |
| - end |
108 |
| -else |
109 |
| - if not dfhack.isMapLoaded() then |
110 |
| - qerror('This script requires a fortress map to be loaded') |
111 |
| - end |
| 100 | + if sc ~= SC_MAP_LOADED or not dfhack.world.isFortressMode() then |
| 101 | + return |
| 102 | + end |
112 | 103 |
|
113 |
| - if positionals[1] == "help" or options.help then |
114 |
| - print(dfhack.script_help()) |
| 104 | + state = get_default_state() |
| 105 | + utils.assign(state, dfhack.persistent.getSiteData(GLOBAL_KEY, state)) |
| 106 | + |
| 107 | + event_loop() |
| 108 | +end |
| 109 | + |
| 110 | +if dfhack_flags.module then |
115 | 111 | return
|
116 |
| - end |
| 112 | +end |
117 | 113 |
|
118 |
| - if positionals[1] == nil then |
119 |
| - if starvingDeadInstance then |
120 |
| - starvingDeadInstance.decay_rate = options.decay_rate or starvingDeadInstance.decay_rate |
121 |
| - starvingDeadInstance.death_threshold = options.death_threshold or starvingDeadInstance.death_threshold |
| 114 | +if not dfhack.isMapLoaded() or not dfhack.world.isFortressMode() then |
| 115 | + qerror('This script requires a fortress map to be loaded') |
| 116 | +end |
122 | 117 |
|
123 |
| - print(([[StarvingDead is running, checking every %s days and killing off at %s months]]):format( |
124 |
| - starvingDeadInstance.decay_rate, starvingDeadInstance.death_threshold |
125 |
| - )) |
| 118 | +if dfhack_flags.enable then |
| 119 | + if dfhack_flags.enable_state then |
| 120 | + do_enable() |
126 | 121 | else
|
127 |
| - print("StarvingDead is not running!") |
| 122 | + do_disable() |
128 | 123 | end
|
129 |
| - end |
| 124 | +end |
| 125 | + |
| 126 | +local opts = {} |
| 127 | +local positionals = argparse.processArgsGetopt({...}, { |
| 128 | + {'h', 'help', handler=function() opts.help = true end}, |
| 129 | + {'r', 'decay-rate', hasArg=true, |
| 130 | + handler=function(arg) opts.decay_rate = argparse.positiveInt(arg, 'decay-rate') end }, |
| 131 | + {'t', 'death-threshold', hasArg=true, |
| 132 | + handler=function(arg) opts.death_threshold = argparse.positiveInt(arg, 'death-threshold') end }, |
| 133 | +}) |
| 134 | + |
| 135 | + |
| 136 | +if positionals[1] == "help" or opts.help then |
| 137 | + print(dfhack.script_help()) |
| 138 | + return |
| 139 | +end |
| 140 | + |
| 141 | +if opts.decay_rate then |
| 142 | + state.decay_rate = opts.decay_rate |
| 143 | +end |
| 144 | +if opts.death_threshold then |
| 145 | + state.death_threshold = opts.death_threshold |
| 146 | +end |
| 147 | +persist_state() |
| 148 | + |
| 149 | +if state.enabled then |
| 150 | + print(([[StarvingDead is running, decaying undead every %s day%s and killing off at %s month%s]]):format( |
| 151 | + state.decay_rate, state.decay_rate == 1 and '' or 's', state.death_threshold, state.death_threshold == 1 and '' or 's')) |
| 152 | +else |
| 153 | + print(([[StarvingDead is not running, but would decay undead every %s day%s and kill off at %s month%s]]):format( |
| 154 | + state.decay_rate, state.decay_rate == 1 and '' or 's', state.death_threshold, state.death_threshold == 1 and '' or 's')) |
130 | 155 | end
|
0 commit comments