Forked from
tome / Tales of MajEyal
6645 commits behind the upstream repository.
-
Alex Ksandra authoredAlex Ksandra authored
Zone.lua 35.96 KiB
-- TE4 - T-Engine 4
-- Copyright (C) 2009 - 2014 Nicolas Casalini
--
-- This program is free software: you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.
--
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-- GNU General Public License for more details.
--
-- You should have received a copy of the GNU General Public License
-- along with this program. If not, see <http://www.gnu.org/licenses/>.
--
-- Nicolas Casalini "DarkGod"
-- darkgod@te4.org
require "engine.class"
local Savefile = require "engine.Savefile"
local Dialog = require "engine.ui.Dialog"
local Map = require "engine.Map"
local Astar = require "engine.Astar"
local forceprint = print
local print = function() end
--- Defines a zone: a set of levels, with depth, npcs, objects, level generator, ...
module(..., package.seeall, class.make)
_no_save_fields = {temp_memory_levels=true, _tmp_data=true}
-- Keep a list of currently existing maps
-- this is a weak table so it doesn't prevents GC
__zone_store = {}
setmetatable(__zone_store, {__mode="k"})
--- List of rules to run through when applying an ego to an entity.
_M.ego_rules = {}
--- Adds an ego rule.
-- Static method
function _M:addEgoRule(kind, rule)
self.ego_rules[kind] = self.ego_rules[kind] or {}
table.insert(self.ego_rules[kind], rule)
end
--- Setup classes to use for level generation
-- Static method
-- @param t table that contains the name of the classes to use
-- @usage Required fields:
-- npc_class (default engine.Actor)
-- grid_class (default engine.Grid)
-- object_class (default engine.Object)
function _M:setup(t)
self.map_class = require(t.map_class or "engine.Map")
self.level_class = require(t.level_class or "engine.Level")
self.npc_class = require(t.npc_class or "engine.Actor")
self.grid_class = require(t.grid_class or "engine.Grid")
self.trap_class = require(t.trap_class or "engine.Trap")
self.object_class = require(t.object_class or "engine.Object")
self.on_setup = t.on_setup
self.ood_factor = t.ood_factor or 3
end
--- Set a cache of the last visited zones
-- Static method
-- Zones will be kept in memory (up to max last zones) and the cache will be used to reload, removing the need for disk load, thus speeding up loading
function _M:enableLastPersistZones(max)
_M.persist_last_zones = { config = {max=max} }
end
function _M:addLastPersistZone(zone)
while #_M.persist_last_zones > _M.persist_last_zones.config.max do
local id = table.remove(_M.persist_last_zones)
forceprint("[ZONE] forgetting old zone", id)
local zone = _M.persist_last_zones[id]
_M.persist_last_zones[id] = nil
end
_M.persist_last_zones[zone.short_name] = zone
table.insert(_M.persist_last_zones, 1, zone.short_name)
end
function _M:removeLastPersistZone(id)
for i = 1, #_M.persist_last_zones do
if _M.persist_last_zones[i] == id then
table.remove(_M.persist_last_zones, i)
forceprint("[ZONE] using old zone", id)
_M.persist_last_zones[id] = nil
return
end
end
end
--- Loads a zone definition
-- @param short_name the short name of the zone to load, if should correspond to a directory in your module data/zones/short_name/ with a zone.lua, npcs.lua, grids.lua and objects.lua files inside
function _M:init(short_name, dynamic)
__zone_store[self] = true
if _M.persist_last_zones and _M.persist_last_zones[short_name] then
local zone = _M.persist_last_zones[short_name]
forceprint("[ZONE] loading from last persistent", short_name, zone)
_M:removeLastPersistZone(short_name)
self:replaceWith(zone)
_M:addLastPersistZone(self)
return
end
self.short_name = short_name
self.specific_base_level = self.specific_base_level or {}
if not self:load(dynamic) then
self.level_range = self.level_range or {1,1}
if type(self.level_range) == "number" then self.level_range = {self.level_range, self.level_range} end
self.level_scheme = self.level_scheme or "fixed"
assert(self.max_level, "no zone max level")
self.levels = self.levels or {}
if not dynamic then self:loadBaseLists() end
if self.on_setup then self:on_setup() end
self:updateBaseLevel()
forceprint("Initiated zone", self.name, "with base_level", self.base_level)
else
if self.update_base_level_on_enter then self:updateBaseLevel() end
forceprint("Loaded zone", self.name, "with base_level", self.base_level)
end
if _M.persist_last_zones then
forceprint("[ZONE] persisting to persist_last_zones", self.short_name)
_M:addLastPersistZone(self)
end
self._tmp_data = {}
end
--- Computes the current base level based on the zone infos
function _M:updateBaseLevel()
-- Determine a zone base level
self.base_level = self.level_range[1]
if self.level_scheme == "player" then
local plev = game:getPlayer().level
self.base_level = util.bound(plev, self.level_range[1], self.level_range[2])
end
end
--- Returns the base folder containing the zone
function _M:getBaseName()
local name = self.short_name
local base = "/data"
local _, _, addon, rname = name:find("^([^+]+)%+(.+)$")
if addon and rname then
base = "/data-"..addon
name = rname
end
return base.."/zones/"..name.."/"
end
--- Loads basic entities lists
local _load_zone = nil
function _M:loadBaseLists()
_load_zone = self
self.npc_list = self.npc_class:loadList(self:getBaseName().."npcs.lua")
self.grid_list = self.grid_class:loadList(self:getBaseName().."grids.lua")
self.object_list = self.object_class:loadList(self:getBaseName().."objects.lua")
self.trap_list = self.trap_class:loadList(self:getBaseName().."traps.lua")
_load_zone = nil
end
--- Gets the currently loading zone
function _M:getCurrentLoadingZone()
return _load_zone
end
--- Leaves a zone
-- Saves the zone to a .teaz file if requested with persistent="zone" flag
function _M:leave()
if type(self.persistent) == "string" and self.persistent == "zone" then savefile_pipe:push(game.save_name, "zone", self) end
for id, level in pairs(self.memory_levels or {}) do
level.map:close()
forceprint("[ZONE] Closed level map", id)
end
for id, level in pairs(self.temp_memory_levels or {}) do
level.map:close()
forceprint("[ZONE] Closed level map", id)
end
forceprint("[ZONE] Left zone", self.name)
game.level = nil
end
function _M:level_adjust_level(level, type)
return self.base_level + (self.specific_base_level[type] or 0) + (level.level - 1) + (add_level or 0)
end
function _M:adjustComputeRaritiesLevel(level, type, lev)
return lev
end
--- Parses the npc/objects list and compute rarities for random generation
-- ONLY entities with a rarity properties will be considered.<br/>
-- This means that to get a never-random entity you simply do not put a rarity property on it.
function _M:computeRarities(type, list, level, filter, add_level, rarity_field)
rarity_field = rarity_field or "rarity"
local r = { total=0 }
print("******************", level.level, type)
local lev = self:level_adjust_level(level, self, type) + (add_level or 0)
lev = self:adjustComputeRaritiesLevel(level, type, lev)
for i, e in ipairs(list) do
if e[rarity_field] and e.level_range and (not filter or filter(e)) then
-- print("computing rarity of", e.name)
local max = 10000
if lev < e.level_range[1] then max = 10000 / (self.ood_factor * (e.level_range[1] - lev))
elseif e.level_range[2] and lev > e.level_range[2] then max = 10000 / (lev - e.level_range[2])
end
local genprob = math.floor(max / e[rarity_field])
print(("Entity(%30s) got %3d (=%3d / %3d) chance to generate. Level range(%2d-%2s), current %2d"):format(e.name, math.floor(genprob), math.floor(max), e[rarity_field], e.level_range[1], e.level_range[2] or "--", lev))
-- Generate and store egos list if needed
if e.egos and e.egos_chance then
if _G.type(e.egos_chance) == "number" then e.egos_chance = {e.egos_chance} end
for ie, edata in pairs(e.egos_chance) do
local etype = ie
if _G.type(ie) == "number" then etype = "" end
if not level:getEntitiesList(type.."/"..e.egos..":"..etype) then
self:generateEgoEntities(level, type, etype, e.egos, e.__CLASSNAME)
end
end
end
-- Generate and store addons list if needed
if e.addons then
if not level:getEntitiesList(type.."/"..e.addons..":addon") then
self:generateEgoEntities(level, type, "addon", e.addons, e.__CLASSNAME)
end
end
if genprob > 0 then
-- genprob = math.ceil(genprob / 10 * math.sqrt(genprob))
r.total = r.total + genprob
r[#r+1] = { e=e, genprob=r.total, level_diff = lev - level.level }
end
end
end
table.sort(r, function(a, b) return a.genprob < b.genprob end)
local prev = 0
local tperc = 0
for i, ee in ipairs(r) do
local perc = 100 * (ee.genprob - prev) / r.total
tperc = tperc + perc
print(("entity chance %2d : chance(%4d : %4.5f%%): %s"):format(i, ee.genprob, perc, ee.e.name))
prev = ee.genprob
end
print("*DONE", r.total, tperc.."%")
return r
end
--- Checks an entity against a filter
function _M:checkFilter(e, filter, type)
if e.unique and game.uniques[e.__CLASSNAME.."/"..e.unique] then print("refused unique", e.name, e.__CLASSNAME.."/"..e.unique) return false end
if not filter then return true end
if filter.ignore and self:checkFilter(e, filter.ignore, type) then return false end
print("Checking filter", filter.type, filter.subtype, "::", e.type,e.subtype,e.name)
if filter.type and filter.type ~= e.type then return false end
if filter.subtype and filter.subtype ~= e.subtype then return false end
if filter.name and filter.name ~= e.name then return false end
if filter.define_as and filter.define_as ~= e.define_as then return false end
if filter.unique and not e.unique then return false end
if filter.properties then
for i = 1, #filter.properties do if not e[filter.properties[i]] then return false end end
end
if filter.not_properties then
for i = 1, #filter.not_properties do if e[filter.not_properties[i]] then return false end end
end
if e.checkFilter and not e:checkFilter(filter) then return false end
if filter.special and not filter.special(e) then return false end
if self.check_filter and not self:check_filter(e, filter, type) then return false end
if filter.max_ood and resolvers.current_level and e.level_range and resolvers.current_level + filter.max_ood < e.level_range[1] then print("Refused max_ood", e.name, e.level_range[1]) return false end
if e.unique then print("accepted unique", e.name, e.__CLASSNAME.."/"..e.unique) end
return true
end
--- Return a string describing the filter
function _M:filterToString(filter)
local ps = ""
for what, check in pairs(filter) do
ps = ps .. what.."="..check..","
end
return ps
end
--- Picks an entity from a computed probability list
function _M:pickEntity(list)
if #list == 0 then return nil end
local r = rng.range(1, list.total)
for i = 1, #list do
-- print("test", r, ":=:", list[i].genprob)
if r <= list[i].genprob then
-- print(" * select", list[i].e.name)
return list[i].e
end
end
return nil
end
--- Compute posible egos for this list
function _M:generateEgoEntities(level, type, etype, e_egos, e___CLASSNAME)
print("Generating specific ego list", type.."/"..e_egos..":"..etype)
local egos = self:getEgosList(level, type, e_egos, e___CLASSNAME)
if egos then
local egos_prob = self:computeRarities(type, egos, level, etype ~= "" and function(e) return e[etype] end or nil)
level:setEntitiesList(type.."/"..e_egos..":"..etype, egos_prob)
level:setEntitiesList(type.."/base/"..e_egos..":"..etype, egos)
return egos_prob
end
end
--- Gets the possible egos
function _M:getEgosList(level, type, group, class)
-- Already loaded ? use it
local list = level:getEntitiesList(type.."/"..group)
if list then return list end
-- otherwise loads it and store it
list = require(class):loadList(group, true)
level:setEntitiesList(type.."/"..group, list)
return list
end
function _M:getEntities(level, type)
local list = level:getEntitiesList(type)
if not list then
if type == "actor" then level:setEntitiesList("actor", self:computeRarities("actor", self.npc_list, level, nil))
elseif type == "object" then level:setEntitiesList("object", self:computeRarities("object", self.object_list, level, nil))
elseif type == "trap" then level:setEntitiesList("trap", self:computeRarities("trap", self.trap_list, level, nil))
end
list = level:getEntitiesList(type)
end
return list
end
--- Picks and resolve an entity
-- @param level a Level object to generate for
-- @param type one of "object" "terrain" "actor" "trap"
-- @param filter a filter table
-- @param force_level if not nil forces the current level for resolvers to this one
-- @param prob_filter if true a new probability list based on this filter will be generated, ensuring to find objects better but at a slightly slower cost (maybe)
-- @return the fully resolved entity, ready to be used on a level. Or nil if a filter was given an nothing found
function _M:makeEntity(level, type, filter, force_level, prob_filter)
resolvers.current_level = self.base_level + level.level - 1
if force_level then resolvers.current_level = force_level end
if prob_filter == nil then prob_filter = util.getval(self.default_prob_filter, self, type) end
if filter == nil then filter = util.getval(self.default_filter, self, level, type) end
if filter and self.alter_filter then filter = util.getval(self.alter_filter, self, level, type, filter) end
local e
-- No probability list, use the default one and apply filter
if not prob_filter then
local list = self:getEntities(level, type)
local tries = filter and filter.nb_tries or 500
-- CRUDE ! Brute force ! Make me smarter !
while tries > 0 do
e = self:pickEntity(list)
if e and self:checkFilter(e, filter, type) then break end
tries = tries - 1
end
if tries == 0 then return nil end
-- Generate a specific probability list, slower to generate but no need to "try and be lucky"
elseif filter then
local base_list = nil
if filter.base_list then
if _G.type(filter.base_list) == "table" then base_list = filter.base_list
else
local _, _, class, file = filter.base_list:find("(.*):(.*)")
if class and file then
base_list = require(class):loadList(file)
end
end
elseif type == "actor" then base_list = self.npc_list
elseif type == "object" then base_list = self.object_list
elseif type == "trap" then base_list = self.trap_list
else base_list = self:getEntities(level, type) if not base_list then return nil end end
local list = self:computeRarities(type, base_list, level, function(e) return self:checkFilter(e, filter, type) end, filter.add_levels, filter.special_rarity)
e = self:pickEntity(list)
print("[MAKE ENTITY] prob list generation", e and e.name, "from list size", #list)
if not e then return nil end
-- No filter
else
local list = self:getEntities(level, type)
local tries = filter and filter.nb_tries or 50 -- A little crude here too but we only check 50 times, this is simply to prevent duplicate uniques
while tries > 0 do
e = self:pickEntity(list)
if e and self:checkFilter(e, nil, type) then break end
tries = tries - 1
end
if tries == 0 then return nil end
end
if filter then e.force_ego = filter.force_ego end
if filter and self.post_filter then e = util.getval(self.post_filter, self, level, type, e, filter) or e end
e = self:finishEntity(level, type, e, (filter and filter.ego_filter) or (filter and filter.ego_chance))
e.__forced_level = filter and filter.add_levels
return e
end
--- Find a given entity and resolve it
-- @return the fully resolved entity, ready to be used on a level. Or nil if a filter was given an nothing found
function _M:makeEntityByName(level, type, name, force_unique)
resolvers.current_level = self.base_level + level.level - 1
local e
if _G.type(type) == "table" then e = type[name] type = type.__real_type or type
elseif type == "actor" then e = self.npc_list[name]
elseif type == "object" then e = self.object_list[name]
elseif type == "grid" or type == "terrain" then e = self.grid_list[name]
elseif type == "trap" then e = self.trap_list[name]
end
if not e then return nil end
local forced = false
if e.unique and game.uniques[e.__CLASSNAME.."/"..e.unique] then
if not force_unique then
forceprint("Refused unique by name", e.name, e.__CLASSNAME.."/"..e.unique)
return nil
else
forced = true
end
end
e = self:finishEntity(level, type, e)
return e, forced
end
local pick_ego = function(self, level, e, eegos, egos_list, type, picked_etype, etype, ego_filter)
picked_etype[etype] = true
if _G.type(etype) == "number" then etype = "" end
local egos = e.egos and level:getEntitiesList(type.."/"..e.egos..":"..etype)
if not egos then egos = self:generateEgoEntities(level, type, etype, eegos, e.__CLASSNAME) end
if self.ego_filter then ego_filter = self.ego_filter(self, level, type, etype, e, ego_filter, egos_list, picked_etype) end
-- Filter the egos if needed
if ego_filter then
local list = {}
for z = 1, #egos do list[#list+1] = egos[z].e end
egos = self:computeRarities(type, list, level, function(e) return self:checkFilter(e, ego_filter) end, ego_filter.add_levels, ego_filter.special_rarity)
end
egos_list[#egos_list+1] = self:pickEntity(egos)
if egos_list[#egos_list] then print("Picked ego", type.."/"..eegos..":"..etype, ":=>", egos_list[#egos_list].name) else print("Picked ego", type.."/"..eegos..":"..etype, ":=>", #egos_list) end
end
-- Applies a single ego to a (pre-resolved) entity
-- May be in need to resolve afterwards
function _M:applyEgo(e, ego, type, no_name_change)
if not e.__original then e.__original = e:clone() end
print("ego", ego.__CLASSNAME, ego.name, getmetatable(ego))
local orig_ego = ego
ego = ego:clone()
local newname = e.name
if not no_name_change then
if ego.prefix then newname = ego.name .. e.name
else newname = e.name .. ego.name end
end
print("applying ego", ego.name, "to ", e.name, "::", newname, "///", e.unided_name, ego.unided_name)
ego.unided_name = nil
ego.__CLASSNAME = nil
ego.__ATOMIC = nil
-- The ego requested instant resolving before merge ?
if ego.instant_resolve then ego:resolve(nil, nil, e) end
if ego.instant_resolve == "last" then ego:resolve(nil, true, e) end
ego.instant_resolve = nil
-- Void the uid, we dont want to erase the base entity's one
ego.uid = nil
ego.rarity = nil
ego.level_range = nil
-- Merge according to Object's ego rules.
table.ruleMergeAppendAdd(e, ego, self.ego_rules[type] or {})
e.name = newname
e.egoed = true
e.ego_list = e.ego_list or {}
e.ego_list[#e.ego_list + 1] = {orig_ego, type, no_name_change}
end
local function reapplyEgos(e)
if not e.__original then return e end
local brandNew = e.__original -- it will be cloned upon first ego application
if e.ego_list and #e.ego_list > 0 then
for _, ego_args in ipairs(e.ego_list) do
self:applyEgo(brandNew, unpack(ego_args))
end
end
e:replaceWith(brandNew)
end
-- Remove an ego
function _M:removeEgo(e, ego)
local idx = nil
for i, v in ipairs(e.ego_list or {}) do
if v[1] == ego then
idx = i
end
end
if not idx then return end
table.remove(e.ego_list, idx)
reapplyEgos(e)
return ego
end
function _M:getEgoByName(e, ego_name)
for i, v in ipairs(e.ego_list or {}) do
if v[1].name == ego_name then return v[1] end
end
end
function _M:removeEgoByName(e, ego_name)
for i, v in ipairs(e.ego_list or {}) do
if v[1].name == ego_name then return self:removeEgo(e, v[1]) end
end
end
--- Finishes generating an entity
function _M:finishEntity(level, type, e, ego_filter)
e = e:clone()
e:resolve()
-- Add "addon" properties, always
if not e.unique and e.addons then
local egos_list = {}
pick_ego(self, level, e, e.addons, egos_list, type, {}, "addon", nil)
if #egos_list > 0 then
for ie, ego in ipairs(egos_list) do
self:applyEgo(e, ego, type)
end
-- Re-resolve with the (possibly) new resolvers
e:resolve()
end
e.addons = nil
end
-- Add "ego" properties, sometimes
if not e.unique and e.egos and (e.force_ego or e.egos_chance) then
local egos_list = {}
local ego_chance = 0
if _G.type(ego_filter) == "number" then ego_chance = ego_filter; ego_filter = nil
elseif _G.type(ego_filter) == "table" then ego_chance = ego_filter.ego_chance or 0
else ego_filter = nil
end
if not e.force_ego then
if _G.type(e.egos_chance) == "number" then e.egos_chance = {e.egos_chance} end
if not ego_filter or not ego_filter.tries then
--------------------------------------
-- Natural ego
--------------------------------------
-- Pick an ego, then an other and so until we get no more
local chance_decay = 1
local picked_etype = {}
local etype = e.ego_first_type and e.ego_first_type or rng.tableIndex(e.egos_chance, picked_etype)
local echance = etype and e.egos_chance[etype]
while etype and rng.percent(util.bound(echance / chance_decay + (ego_chance or 0), 0, 100)) do
pick_ego(self, level, e, e.egos, egos_list, type, picked_etype, etype, ego_filter)
etype = rng.tableIndex(e.egos_chance, picked_etype)
echance = e.egos_chance[etype]
if e.egos_chance_decay then chance_decay = chance_decay * e.egos_chance_decay end
end
else
--------------------------------------
-- Semi Natural ego
--------------------------------------
-- Pick an ego, then an other and so until we get no more
local picked_etype = {}
for i = 1, #ego_filter.tries do
local try = ego_filter.tries[i]
local etype = (i == 1 and e.ego_first_type and e.ego_first_type) or rng.tableIndex(e.egos_chance, picked_etype)
-- forceprint("EGO TRY", i, ":", etype, echance, try)
if not etype then break end
local echance = etype and try[etype]
pick_ego(self, level, e, e.egos, egos_list, type, picked_etype, etype, try)
end
end
else
--------------------------------------
-- Forced ego
--------------------------------------
local name = e.force_ego
if _G.type(name) == "table" then name = rng.table(name) end
print("Forcing ego", name)
local egos = level:getEntitiesList(type.."/base/"..e.egos..":")
egos_list = {egos[name]}
e.force_ego = nil
end
if #egos_list > 0 then
for ie, ego in ipairs(egos_list) do
self:applyEgo(e, ego, type)
end
-- Re-resolve with the (possibly) new resolvers
e:resolve()
end
if not ego_filter or not ego_filter.keep_egos then
e.egos = nil e.egos_chance = nil e.force_ego = nil
end
end
-- Generate a stack ?
if e.generate_stack then
local s = {}
while e.generate_stack > 0 do
s[#s+1] = e:clone()
e.generate_stack = e.generate_stack - 1
end
for i = 1, #s do e:stack(s[i], true) end
end
e:resolve(nil, true)
e:check("finish", e, self, level)
return e
end
--- Do the various stuff needed to setup an entity on the level
-- Grids do not really need that, this is mostly done for traps, objects and actors<br/>
-- This will do all the correct initializations and setup required
-- @param level the level on which to add the entity
-- @param e the entity to add
-- @param typ the type of entity, one of "actor", "object", "trap" or "terrain"
-- @param x the coordinates where to add it. This CAN be null in which case it wont be added to the map
-- @param y the coordinates where to add it. This CAN be null in which case it wont be added to the map
function _M:addEntity(level, e, typ, x, y, no_added)
if typ == "actor" then
-- We are additing it, this means there is no old position
e.x = nil
e.y = nil
if x and y then e:move(x, y, true) end
level:addEntity(e, nil, true)
if not no_added then e:added() end
-- Levelup ?
if self.actor_adjust_level and e.forceLevelup then
local newlevel = self:actor_adjust_level(level, e)
e:forceLevelup(newlevel + (e.__forced_level or 0))
end
elseif typ == "projectile" then
-- We are additing it, this means there is no old position
e.x = nil
e.y = nil
if x and y then e:move(x, y, true) end
if e.src then level:addEntity(e, e.src, true)
else level:addEntity(e, nil, true) end
if not no_added then e:added() end
elseif typ == "object" then
if x and y then level.map:addObject(x, y, e) end
if not no_added then e:added() end
elseif typ == "trap" then
if x and y then level.map(x, y, Map.TRAP, e) end
if not no_added then e:added() end
elseif typ == "terrain" or typ == "grid" then
if x and y then level.map(x, y, Map.TERRAIN, e) end
end
e:check("addedToLevel", level, x, y)
e:check("on_added", level, x, y)
end
--- If we are loaded we need a new uid
function _M:loaded()
__zone_store[self] = true
self._tmp_data = {}
if type(self.reload_lists) ~= "boolean" or self.reload_lists then
self:loadBaseLists()
end
if self.on_loaded then self:on_loaded() end
end
function _M:load(dynamic)
local ret = true
-- Try to load from a savefile
local data = savefile_pipe:doLoad(game.save_name, "zone", nil, self.short_name)
if not data and not dynamic then
local f, err = loadfile(self:getBaseName().."zone.lua")
if err then error(err) end
setfenv(f, setmetatable({self=self, short_name=self.short_name}, {__index=_G}))
data = f()
ret = false
if type(data.reload_lists) ~= "boolean" or data.reload_lists then
self._no_save_fields = table.clone(self._no_save_fields, true)
self._no_save_fields.npc_list = true
self._no_save_fields.grid_list = true
self._no_save_fields.object_list = true
self._no_save_fields.trap_list = true
end
for k, e in pairs(data) do self[k] = e end
self:onLoadZoneFile(self:getBaseName())
if self.on_loaded then self:on_loaded() end
elseif not data and dynamic then
data = dynamic
ret = false
for k, e in pairs(data) do self[k] = e end
if self.on_loaded then self:on_loaded() end
else
for k, e in pairs(data) do self[k] = e end
end
return ret
end
--- Called when the zone file is loaded
-- Does nothing, overload it
function _M:onLoadZoneFile(basedir)
end
local recurs = function(t)
local nt = {}
for k, e in pairs(nt) do if k ~= "__CLASSNAME" and k ~= "__ATOMIC" then nt[k] = e end end
return nt
end
function _M:getLevelData(lev)
local res = table.clone(self, true, self._no_save_fields)
if self.levels[lev] then
table.merge(res, self.levels[lev], true, self._no_save_fields)
end
if res.alter_level_data then res.alter_level_data(self, lev, res) end
-- Make sure it is not considered a class
res.__CLASSNAME = nil
res.__ATOMIC = nil
return res
end
--- Leave the level, forgetting uniques and such
function _M:leaveLevel(no_close, lev, old_lev)
-- Before doing anything else, close the current level
if not no_close and game.level and game.level.map then
game:leaveLevel(game.level, lev, old_lev)
if type(game.level.data.persistent) == "string" and game.level.data.persistent == "zone_temporary" then
print("[LEVEL] persisting to zone memory (temporary)", game.level.id)
self.temp_memory_levels = self.temp_memory_levels or {}
self.temp_memory_levels[game.level.level] = game.level
elseif type(game.level.data.persistent) == "string" and game.level.data.persistent == "zone" and not self.save_per_level then
print("[LEVEL] persisting to zone memory", game.level.id)
self.memory_levels = self.memory_levels or {}
self.memory_levels[game.level.level] = game.level
elseif type(game.level.data.persistent) == "string" and game.level.data.persistent == "memory" then
print("[LEVEL] persisting to memory", game.level.id)
game.memory_levels = game.memory_levels or {}
game.memory_levels[game.level.id] = game.level
elseif game.level.data.persistent then
print("[LEVEL] persisting to disk file", game.level.id)
savefile_pipe:push(game.save_name, "level", game.level)
game.level.map:close()
else
game.level:removed()
game.level.map:close()
end
end
end
--- Asks the zone to generate a level of level "lev"
-- @param lev the level (from 1 to zone.max_level)
-- @return a Level object
function _M:getLevel(game, lev, old_lev, no_close)
self:leaveLevel(no_close, lev, old_lev)
local level_data = self:getLevelData(lev)
local levelid = self.short_name.."-"..lev
local level
local new_level = false
-- Load persistent level?
if type(level_data.persistent) == "string" and level_data.persistent == "zone_temporary" then
forceprint("Loading zone temporary level", self.short_name, lev)
local popup = Dialog:simpleWaiter("Loading level", "Please wait while loading the level...", nil, 10000)
core.display.forceRedraw()
self.temp_memory_levels = self.temp_memory_levels or {}
level = self.temp_memory_levels[lev]
if level then
-- Setup the level in the game
game:setLevel(level)
-- Recreate the map because it could have been saved with a different tileset or whatever
-- This is not needed in case of a direct to file persistance becuase the map IS recreated each time anyway
level.map:recreate()
end
popup:done()
elseif type(level_data.persistent) == "string" and level_data.persistent == "zone" and not self.save_per_level then
forceprint("Loading zone persistance level", self.short_name, lev)
local popup = Dialog:simpleWaiter("Loading level", "Please wait while loading the level...", nil, 10000)
core.display.forceRedraw()
self.memory_levels = self.memory_levels or {}
level = self.memory_levels[lev]
if level then
-- Setup the level in the game
game:setLevel(level)
-- Recreate the map because it could have been saved with a different tileset or whatever
-- This is not needed in case of a direct to file persistance becuase the map IS recreated each time anyway
level.map:recreate()
end
popup:done()
elseif type(level_data.persistent) == "string" and level_data.persistent == "memory" then
forceprint("Loading memory persistance level", self.short_name, lev)
local popup = Dialog:simpleWaiter("Loading level", "Please wait while loading the level...", nil, 10000)
core.display.forceRedraw()
game.memory_levels = game.memory_levels or {}
level = game.memory_levels[levelid]
if level then
-- Setup the level in the game
game:setLevel(level)
-- Recreate the map because it could have been saved with a different tileset or whatever
-- This is not needed in case of a direct to file persistance becuase the map IS recreated each time anyway
level.map:recreate()
end
popup:done()
elseif level_data.persistent then
forceprint("Loading level persistance level", self.short_name, lev)
local popup = Dialog:simpleWaiter("Loading level", "Please wait while loading the level...", nil, 10000)
core.display.forceRedraw()
-- Try to load from a savefile
level = savefile_pipe:doLoad(game.save_name, "level", nil, self.short_name, lev)
if level then
-- Setup the level in the game
game:setLevel(level)
end
popup:done()
end
-- In any cases, make one if none was found
if not level then
forceprint("Creating level", self.short_name, lev)
local popup = Dialog:simpleWaiter("Generating level", "Please wait while generating the level...", nil, 10000)
core.display.forceRedraw()
level = self:newLevel(level_data, lev, old_lev, game)
new_level = true
popup:done()
end
-- Clean up things
collectgarbage("collect")
-- Re-open the level if needed (the method does the check itself)
level.map:reopen()
return level, new_level
end
function _M:getGenerator(what, level, spots)
assert(level.data.generator[what], "requested zone generator of type "..tostring(what).." but it is not defined")
assert(level.data.generator[what].class, "requested zone generator of type "..tostring(what).." but it has no class field")
print("[GENERATOR] requiring", what, level.data.generator and level.data.generator[what] and level.data.generator[what].class)
if not level.data.generator[what].zoneclass then
return require(level.data.generator[what].class).new(
self,
level.map,
level,
spots
)
else
local base = require(level.data.generator[what].class)
local c = class.inherit(base){}
local name = self:getBaseName().."generator"..what:capitalize()..".lua"
print("[ZONE] Custom zone generator for "..what.." loading from "..name)
local add = loadfile(name)
setfenv(add, setmetatable({
_M = c,
baseGenerator = base,
Zone = _M,
Map = Map,
}, {__index=_G}))
add()
return c.new(
self,
level.map,
level,
spots
)
end
end
function _M:newLevel(level_data, lev, old_lev, game)
local map = self.map_class.new(level_data.width, level_data.height)
map.updateMap = function() end
if level_data.all_lited then map:liteAll(0, 0, map.w, map.h) end
if level_data.all_remembered then map:rememberAll(0, 0, map.w, map.h) end
-- Setup the entities list
local level = self.level_class.new(lev, map)
level:setEntitiesList("actor", self:computeRarities("actor", self.npc_list, level, nil))
level:setEntitiesList("object", self:computeRarities("object", self.object_list, level, nil))
level:setEntitiesList("trap", self:computeRarities("trap", self.trap_list, level, nil))
-- Save level data
level.data = level_data or {}
level.id = self.short_name.."-"..lev
-- Setup the level in the game
game:setLevel(level)
-- Generate the map
local generator = self:getGenerator("map", level, level_data.generator.map)
local ux, uy, dx, dy, spots = generator:generate(lev, old_lev)
spots = spots or {}
for i = 1, #spots do print("[NEW LEVEL] spot", spots[i].x, spots[i].y, spots[i].type, spots[i].subtype) end
level.default_up = {x=ux, y=uy}
level.default_down = {x=dx, y=dy}
level.spots = spots
-- Call a map finisher
if level_data.post_process_map then
level_data.post_process_map(level, self)
if level.force_recreate then
level:removed()
return self:newLevel(level_data, lev, old_lev, game)
end
end
-- Add the entities we are told to
for i = 0, map.w - 1 do for j = 0, map.h - 1 do
if map.room_map[i] and map.room_map[i][j] and map.room_map[i][j].add_entities then
for z = 1, #map.room_map[i][j].add_entities do
local ae = map.room_map[i][j].add_entities[z]
self:addEntity(level, ae[2], ae[1], i, j, true)
end
end
end end
-- Now update it all in one go (faster than letter the generators do it since they usualy overlay multiple terrains)
map.updateMap = nil
map:redisplay()
-- Generate actors
if level_data.generator.actor and level_data.generator.actor.class then
local generator = self:getGenerator("actor", level, spots)
generator:generate()
end
-- Generate objects
if level_data.generator.object and level_data.generator.object.class then
local generator = self:getGenerator("object", level, spots)
generator:generate()
end
-- Generate traps
if level_data.generator.trap and level_data.generator.trap.class then
local generator = self:getGenerator("trap", level, spots)
generator:generate()
end
-- Adjust shown & obscure colors
if level_data.color_shown then map:setShown(unpack(level_data.color_shown)) end
if level_data.color_obscure then map:setObscure(unpack(level_data.color_obscure)) end
-- Call a finisher
if level_data.post_process then
level_data.post_process(level, self)
if level.force_recreate then
level:removed()
return self:newLevel(level_data, lev, old_lev, game)
end
end
-- Delete the room_map, now useless
map.room_map = nil
-- Check for connectivity from entrance to exit
local a = Astar.new(map, game:getPlayer())
if not level_data.no_level_connectivity then
print("[LEVEL GENERATION] checking entrance to exit A*", ux, uy, "to", dx, dy)
if ux and uy and dx and dy and (ux ~= dx or uy ~= dy) and not a:calc(ux, uy, dx, dy) then
forceprint("Level unconnected, no way from entrance to exit", ux, uy, "to", dx, dy)
level:removed()
return self:newLevel(level_data, lev, old_lev, game)
end
end
for i = 1, #spots do
local spot = spots[i]
if spot.check_connectivity then
local cx, cy
if type(spot.check_connectivity) == "string" and spot.check_connectivity == "entrance" then cx, cy = ux, uy
elseif type(spot.check_connectivity) == "string" and spot.check_connectivity == "exit" then cx, cy = dx, dy
else cx, cy = spot.check_connectivity.x, spot.check_connectivity.y
end
print("[LEVEL GENERATION] checking A*", spot.x, spot.y, "to", cx, cy)
if spot.x and spot.y and cx and cy and (spot.x ~= cx or spot.y ~= cy) and not a:calc(spot.x, spot.y, cx, cy) then
forceprint("Level unconnected, no way from", spot.x, spot.y, "to", cx, cy)
level:removed()
return self:newLevel(level_data, lev, old_lev, game)
end
end
end
return level
end