Advertisement
Guest User

anon's level_weathers.script

a guest
May 23rd, 2013
443
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 16.77 KB | None | 0 0
  1. -- [[ Dynamic Weather v0.9.5 script ]] --
  2.  
  3. -- enable/disable printing debug messages to console
  4. local debug = false
  5.  
  6.  
  7. -- stubs for OL2.2
  8. function dynamic_weather_time(level_name)
  9.     return false
  10. end
  11.  
  12. function dynamic_weather_level(level_name)
  13.     return false
  14. end
  15. -- end of OL2.2 stubs
  16.  
  17.  
  18. -- check if level name corresponds to one of the levels
  19. -- that should use special indoor level as configured
  20. -- in game_maps_single.ltx
  21. local function is_indoor(level_name)
  22.     return (1)
  23. end
  24.  
  25.  
  26. -- time in minutes since midnight
  27. local function dayminute()
  28.     return level.get_time_hours() * 60 + level.get_time_minutes()
  29. end
  30.  
  31.  
  32. -- returns true if t1 <= t <= t2 assuming 24 hour clock
  33. local function is_time_between(t, t1, t2)
  34.     return (t1 < t2 and (t >= t1 and t <= t2))
  35.         or (t1 > t2 and (t >= t1 or  t <= t2))
  36. end
  37.  
  38.  
  39. -- absolute time difference between two times in minutes
  40. local function delta_time(t1, t2)
  41.     return (t2 + 1440 - t1) % 1440
  42. end
  43.  
  44.  
  45. class "WeatherManager"
  46. function WeatherManager:__init()
  47.     self.ini = ini_file("weathers\\weather_dynamic.ltx")
  48.     self.time = 0
  49.     self.lvl_weather = ""    -- weather for the level as set in game_maps_single.ltx
  50.     self.dyn_weather = ""    -- currently selected dynamic weather cycle name
  51.     self.cur_weather = ""    -- weather that should be used right now in current level
  52.                              -- NOTE: this is equal to lvl_weather for indoor levels
  53.                              -- and to dyn_weather for outdoor levels
  54.  
  55.     self.cycle_time = 0      -- the time that the current cycle has run
  56.     self.cycles = {}         -- weather cycle info table by cycle id
  57.     self.cycle_idx = {}      -- cycle name to cycle id lookup table
  58.     self.trans_lists = {}    -- master list of transition lists by time of day
  59.     self.trans_times = {}    -- corresponding table of transition times
  60.  
  61.     self.last_time_idx = 0   -- index into trans_times for last update time
  62.     self.next_time_idx = 0   -- ditto for next update time
  63.     self.last_time = 0       -- last update time of day in minutes since midnight
  64.     self.next_time = 0       -- ditto for next
  65.  
  66.     local rand, res, key, val, n
  67.  
  68.     n = self.ini:line_count("weather_cycles")
  69.  
  70.     -- randomly select a cycle ID for a default cycle
  71.     rand = math.random(n) - 1
  72.  
  73.     -- load cycle information from the .ltx
  74.     for i = 0, n - 1 do
  75.         res, key, val = self.ini:r_line("weather_cycles", i, "", "")
  76.  
  77.         -- setup a default weather cycle for initialization etc.
  78.         if self.default_cycle_id == nil or rand == i then
  79.             self.default_cycle_id = key
  80.         end
  81.  
  82.         -- temporary table of tokens from comma separated string
  83.         local tmptab = {}
  84.  
  85.         -- split the comma separated string and store the tokens
  86.         for w in string.gmatch(val, "[%w_-]+") do
  87.             table.insert(tmptab, w)
  88.         end
  89.  
  90.         -- construct the cycle info from the string tokens
  91.         self.cycles[key]             = {}
  92.         self.cycles[key].name        = tmptab[1]
  93.         self.cycles[key].to          = tonumber(tmptab[2])
  94.         self.cycles[key].from        = tonumber(tmptab[3])
  95.         self.cycles[key].to_weight   = tonumber(tmptab[4])
  96.         self.cycles[key].from_weight = tonumber(tmptab[5])
  97.  
  98.         -- least-frequently-used counter, incremented each time this
  99.         -- cycle is NOT transitioned to
  100.         self.cycles[key].skipped     = 0
  101.  
  102.         -- provide a cycle name to cycle id lookup
  103.         self.cycle_idx[self.cycles[key].name] = key
  104.     end
  105.  
  106.     n = self.ini:line_count("weather_transition_times")
  107.  
  108.     -- read a by-minute-of-day list of transition list sections
  109.     for i = 0, n - 1 do
  110.         res, key, val = self.ini:r_line("weather_transition_times", i, "", "")
  111.         table.insert(self.trans_times, tonumber(key))
  112.         self.trans_lists[tonumber(key)] = val
  113.     end
  114.  
  115.     -- sort the table since reading from .ltx does not guarantee any order
  116.     -- table needs to be sorted for the sliding time window indexes to work
  117.     table.sort(self.trans_times)
  118. end
  119.  
  120.  
  121. -- debug message print function - can be disabled by setting debug to false
  122. function WeatherManager:dbgmsg(fmt, ...)
  123.     if debug == true then
  124.         local msgtext = string.format("%02d:%02d idx:%d->%d,time:%02d:%02d->%02d:%02d ",
  125.                                       level.get_time_hours(), level.get_time_minutes(),
  126.                                       self.last_time_idx, self.next_time_idx,
  127.                                       math.floor(self.last_time / 60), self.last_time % 60,
  128.                                       math.floor(self.next_time / 60), self.next_time % 60)
  129.  
  130.         msgtext = msgtext .. string.format(fmt, ...)
  131.         msgtext = string.gsub(msgtext, "%s", "_")
  132.         get_console():execute(">>"..msgtext)
  133.     end
  134. end
  135.  
  136.  
  137. -- get cycle name for the weather that should be in use right now
  138. function WeatherManager:current_weather()
  139.     if is_indoor(level.name()) then
  140.         return self.lvl_weather
  141.     end
  142.     return self.dyn_weather
  143. end
  144.  
  145.  
  146. -- update the sliding time window indexes
  147. function WeatherManager:update_times(last_idx)
  148.     self.last_time_idx = last_idx
  149.     self.next_time_idx = last_idx % #self.trans_times + 1
  150.     self.last_time = self.trans_times[self.last_time_idx]
  151.     self.next_time = self.trans_times[self.next_time_idx]
  152. end
  153.  
  154.  
  155. -- adjust time indexes so that current time is within the window
  156. -- that is defined by these indexes
  157. function WeatherManager:fix_times()
  158.     self.next_time_idx = 1
  159.     repeat
  160.        self:update_times(self.next_time_idx)
  161.     until is_time_between(self.time, self.last_time, self.next_time)
  162. end
  163.  
  164.  
  165. -- setup a valid weather cycle name and the correct time indexes
  166. -- used if savedgame is from an incompatible version and when a new game is started
  167. function WeatherManager:convert_savedgame()
  168.     self.cycle_time = 0
  169.     if self.cycle_idx[self.dyn_weather] == nil then
  170.         self.dyn_weather = self.cycles[self.default_cycle_id].name
  171.     end
  172.     self:fix_times()
  173. end
  174.  
  175.  
  176. -- update weather selection
  177. -- used on game load / level change with force = 0 to resume last weather
  178. -- and on timed update with force = 1 to do dynamic weather updates
  179. function WeatherManager:select_weather(force)
  180.     local tmp_dyn_weather = self.dyn_weather
  181.     if force then
  182.         -- load list of transitions that complete / initiate at this time of day
  183.         local list_sect = self.trans_lists[self.next_time]
  184.  
  185.         -- update time that this weather has been active by time since last time update
  186.         self.cycle_time = self.cycle_time + delta_time(self.last_time, self.next_time)
  187.  
  188.         -- if there is a transition from currect weather at this time of day
  189.         if self.ini:section_exist(list_sect) and self.ini:line_exist(list_sect, self.dyn_weather) then
  190.             -- load the string with possible transitions from current weather
  191.             local transdef = self.ini:r_string(list_sect, self.dyn_weather)
  192.  
  193.             -- get id for current weather cycle - if not found then this must be a transition
  194.             local cycle_id = self.cycle_idx[self.dyn_weather]
  195.  
  196.             if cycle_id ~= nil then
  197.                 -- probability of transition based on the time this cycle has been active
  198.                 local trans_prob = 20 + self.cycles[cycle_id].from_weight * self.cycle_time / 1800
  199.  
  200.                 if math.random(1000) <= 10 * trans_prob then
  201.                     local min_skips        -- skip count for the least skipped cycle
  202.                     local max_skips        -- ditto for most skipped
  203.                     local transtab    = {} -- array of available transitions
  204.                     local weighttab   = {} -- weight table for available transitions
  205.                     local weightsum   = 0  -- sum of weights for these transitions
  206.  
  207.                     -- find minimum and maximum skip counts
  208.  
  209.                     min_skips = self.cycles[self.default_cycle_id].skipped
  210.                     max_skips = min_skips
  211.  
  212.                     for cycle_id, cycle in ipairs(self.cycles) do
  213.                         if cycle.skipped < min_skips then
  214.                             min_skips = cycle.skipped
  215.                         end
  216.                         if cycle.skipped > max_skips then
  217.                             max_skips = cycle.skipped
  218.                         end
  219.                     end
  220.  
  221.                     -- assume that transdef is a comma separated list of transition_name:cycle_id
  222.                     for trans, cycle_id in string.gmatch(transdef, "([%w_-]+):(%d+)") do
  223.                         local skipped_weight
  224.  
  225.                         -- least skipped cycle gets 0.5 weight, most skipped used gets 1.5
  226.                         if min_skips < max_skips then
  227.                             skipped_weight = 0.5
  228.                                            + (self.cycles[cycle_id].skipped - min_skips)
  229.                                                    / (max_skips - min_skips)
  230.                         else
  231.                             skipped_weight = 1
  232.                         end
  233.  
  234.                         weightsum = weightsum + self.cycles[cycle_id].to_weight * skipped_weight
  235.                         table.insert(weighttab, weightsum)
  236.                         table.insert(transtab, trans)
  237.  
  238.                         -- use the opportunity to mark all these cycles as skipped
  239.                         --  note: the cycle that will be picked for transition will be "unmarked"
  240.                         self.cycles[cycle_id].skipped = self.cycles[cycle_id].skipped + 1
  241.                     end
  242.  
  243.                     local rand = math.random(weightsum)
  244.  
  245.                     -- use weight table for random transition selection
  246.                     for i = 1, #weighttab do
  247.                         if rand <= weighttab[i] then
  248.                             self.dyn_weather = transtab[i]
  249.                             self.cycle_time = 0
  250.                             break
  251.                         end
  252.                     end
  253.                 end
  254.             else -- current weather is a transition phase so complete the transition
  255.                 local cycle_id = self.cycle_idx[transdef]
  256.                 self.dyn_weather = transdef
  257.                 self.cycle_time = 0
  258.  
  259.                 -- undo skip increment since this cycle has just been selected
  260.                 self.cycles[cycle_id].skipped = self.cycles[cycle_id].skipped - 1
  261.             end
  262.         end
  263.  
  264.         self:update_times(self.next_time_idx)
  265.  
  266.         if not is_time_between(self.time, self.last_time, self.next_time) then
  267.             self:dbgmsg("need to fix times")
  268.             self:fix_times()
  269.  
  270.             -- HACK: transition was initiated at wrong time so undo it
  271.             if self.cycle_idx[self.dyn_weather] == nil then
  272.                 self.dyn_wather = tmp_dyn_weather
  273.             end
  274.         end
  275.  
  276.         -- OL2.2 mod updates (if applicable)
  277.         if olp_mod and amk.load_variable("blowout", 0) == 0 then
  278.             if olp_mod and olp_mod.anoms
  279.                and amk.load_variable("blowout_happened", 0) == 0
  280.             then
  281.                 -- self:dbgmsg("remove/create anoms")
  282.                 amk.mod_call("remove_anoms")  
  283.                 amk.mod_call("create_anoms")
  284.             end
  285.             amk.del_variable("blowout_happened")
  286.         end
  287.  
  288.     end
  289.  
  290.     -- for cycle testing set the weather here like this:
  291.     -- self.dyn_weather = "thunder"
  292.  
  293.     self.cur_weather = self:current_weather()
  294.    
  295.     -- tell the script in DEXXX's blowout mod to use this weather instead of default
  296.     if ogsm_funcs then
  297.         ogsm_funcs.wthr = self.cur_weather
  298.     end
  299.  
  300.     -- if blowout is not in progress then apply the current weather
  301.     if (not olp_mod or amk.load_variable("blowout", 0) == 0)
  302.        and (not blowout or blowout.GetBlowout().is_blowout_on == false)
  303.        and (not ogsm_funcs or ogsm_funcs.load_variable("blt", 0) == 0)
  304.        and (not Psy or (db.Flag2 == 0 and db.Flag3 == 0 and db.Flag4 == 0 and db.Flag5 == 0))
  305.     then
  306.         level.set_weather(self.cur_weather)
  307.     elseif olp_mod and amk_mod.blowout_type() < 1 then
  308.         level.set_weather("pre_blow")
  309.         level.set_weather_fx("amk_surge_day")
  310.     end
  311.  
  312.     self:dbgmsg("weather:%s,cycle_time:%d", level.get_weather(), self.cycle_time)
  313.      
  314. end
  315.  
  316.  
  317. -- called on game begin, game load and level change
  318. function WeatherManager:reset()
  319.     local ini = ini_file("game.ltx")
  320.     local postprocess = utils.cfg_get_string(ini, level.name(), "postprocess", db.actor, false, "")
  321.  
  322.     self.lvl_weather = utils.cfg_get_string(ini, level.name(), "weathers", db.actor, false, "", "default")
  323.  
  324.     if postprocess ~= nil then
  325.         level.add_pp_effector(postprocess, 999, true)  
  326.     else
  327.         level.remove_pp_effector(999)
  328.     end
  329.     if self.dyn_weather == "" then
  330.         self.time = dayminute()
  331.         self:convert_savedgame()
  332.     end
  333.  
  334.     self:select_weather(false)
  335. end
  336.  
  337.  
  338. -- save states by encoding the necessary state variables
  339. -- to fit inside existing vanilla savegame structure
  340. function WeatherManager:save(F)
  341.     local composite = self.cycle_time * 1440 + self.last_time_idx
  342.  
  343.     -- encode a string with comma separated cycle:count pairs that keep
  344.     -- track of the skip count for each cycle
  345.     -- the first element in this list is the name of current cycle
  346.  
  347.     local info_str = self.dyn_weather
  348.  
  349.     for id, cycle in pairs(self.cycles) do
  350.         info_str = info_str .. "," .. cycle.name .. ":" .. cycle.skipped
  351.     end
  352.  
  353.     -- set_save_marker(F, "save", false, "WeatherManager")
  354.     F:w_stringZ(info_str)
  355.     F:w_u32(composite)
  356.     -- set_save_marker(F, "save", true, "WeatherManager")
  357. end
  358.  
  359.  
  360. -- load and decode state variables - if they don't make sense setup from scratch
  361. function WeatherManager:load(F)
  362.     -- set_save_marker(F, "load", false, "WeatherManager")
  363.     local info_str = F:r_stringZ()
  364.     local composite = F:r_u32()
  365.     -- set_save_marker(F, "load", true, "WeatherManager")
  366.  
  367.     local lastidx = composite % 1440
  368.     self.cycle_time = math.floor(composite / 1440)
  369.     self.time = dayminute()
  370.  
  371.     -- recover current cycle name as first element of the saved info string
  372.     self.dyn_weather = string.sub(info_str, string.find(info_str, "^[%w-_]*"))
  373.  
  374.     -- load the skip counters from the remaining portion of the string
  375.     for name, skipped in string.gmatch(info_str, "([%w-_]+):(%d+)") do
  376.         local cycle_id = self.cycle_idx[name]
  377.  
  378.         if cycle_id ~= nil then
  379.             self.cycles[cycle_id].skipped = skipped
  380.         else
  381.             self:dbgmsg("Warning: unknown cycle '%s' with skip count %d", name, skipped)
  382.             lastidx = 0    -- set to invalid value so the savedgame is "converted"
  383.         end
  384.     end
  385.  
  386.     if lastidx > 0
  387.        and lastidx <= #self.trans_times
  388.        and self.cycle_time <= 14400
  389.        and self.ini:line_exist("weather_dynamic", self.dyn_weather)
  390.     then
  391.         self.cur_weather = self:current_weather()
  392.         self:update_times(lastidx)
  393.  
  394.         -- allow up to 5 minute tolerance
  395.         if is_time_between(self.time, self.last_time, self.next_time)
  396.            or delta_time(self.time, self.next_time) <= 5
  397.         then
  398.             -- set default weather in DEXXX's blowout mod script
  399.             if ogsm_funcs then
  400.                 ogsm_funcs.wthr = self.cur_weather
  401.             end
  402.             return
  403.         end        
  404.     end
  405.  
  406.     self:dbgmsg("savedgame is incompatible")
  407.  
  408.     self:convert_savedgame()
  409. end
  410.  
  411.  
  412. -- update function called from bind_stalker.script
  413. function WeatherManager:update()
  414.     local time = dayminute()
  415.     local update_flag = 0
  416.  
  417.     -- weather is not what it should be so unless a blowout is underway
  418.     -- the correct weather should be resumed
  419.  
  420.     if level.get_weather() ~= self.cur_weather
  421.        and ((olp_mod and amk.load_variable("blowout_happened", 0) ~= 0)
  422.             or (blowout and blowout.GetBlowout().is_blowout_on == false))
  423.     then
  424.         self:dbgmsg("restoring weather after blowout")
  425.         update_flag = 1
  426.     end
  427.  
  428.     -- check if time has increased by at least one minute
  429.     -- care must be taken around midnight when time wraps around
  430.     if time <= self.time and self.time < time + 1380 and update_flag == 0 then
  431.         -- don't do anyhitng this minute
  432.         return
  433.     else
  434.         self.time = time
  435.     end
  436.  
  437.     -- time of next available transition is reached
  438.     if self.time == self.next_time
  439.        or not is_time_between(self.time, self.last_time, self.next_time)
  440.     then
  441.         self:select_weather(true)
  442.  
  443.     -- something makes it necessary to update now (e.g. post-blowout state)
  444.     elseif update_flag ~= 0 then
  445.         self:select_weather(false)
  446.     end
  447. end
  448.  
  449.  
  450. -- singleton object
  451. local weather_manager = nil
  452.  
  453.  
  454. function get_weather_manager()
  455.     if weather_manager == nil then
  456.         weather_manager = WeatherManager()
  457.     end
  458.     return weather_manager
  459. end
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement