Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • amagad/t-engine4
  • HirumaKai/t-engine4
  • Hogulus/t-engine4
  • Inkie/t-engine4
  • Liberty/t-engine4
  • Lokean/t-engine4
  • Mawootad/t-engine4
  • Michelle/t-engine4
  • MrFrog/t-engine4
  • Nagyhal/t-engine4
  • Recaiden/t-engine4
  • RootOfAllThings/t-engine4
  • Sebsebeleb/t-engine4
  • Sheila/t-engine4
  • Shibari/t-engine4
  • Stof/t-engine4
  • Umbral/t-engine4
  • tome/t-engine4
  • 0player/t-engine4
  • BreezyIdiot/t-engine4
  • Bunny/t-engine4
  • Effigy/t-engine4
  • Hachem_Muche/t-engine4
  • razakai/t-engine4
  • Zireael/t-engine4
  • cinornu/t-engine4
  • edge2054/t-engine4
  • gordaxx727/t-engine4
  • grayswandir/t-engine4
  • helminthauge/t-engine4
  • housepet/t-engine4
  • minqmay/t-engine4
  • nsrr/t-engine4
  • orange/t-engine4
  • otowakotori/t-engine4
  • purequestion/t-engine4
  • rexorcorum/t-engine4
  • rgeens/t-engine4
  • sageacrin/t-engine4
  • stuntofthelitter/t-engine4
  • tiger_eye/t-engine4
  • xelivous/t-engine4
  • yutio888/t-engine4
43 results
Show changes
Commits on Source (146)
Showing
with 422 additions and 114 deletions
......@@ -69,6 +69,32 @@ end
function _M:setTarget(target)
end
--- cloneActor default alt_node fields (controls fields copied by cloneCustom)
-- modules should update this as needed
_M.clone_nodes = {player=false, x=false, y=false,
fov_computed=false, fov={v={actors={}, actors_dist={}}}, distance_map={v={}},
_mo=false, _last_mo=false, add_mos=false, add_displays=false,
shader=false, shader_args=false,
}
--- cloneActor default fields (merged by _M.cloneActor with cloneCustom)
-- modules may define this as a table to automatically merge into cloned actors
_M.clone_copy = nil
--- Special version of cloneFull that clones an Actor, automatically managing duplication of some fields
-- uses class.CloneCustom
-- @param[optional, type=?table] post_copy a table merged into the cloned actor
-- updated with self.clone_copy if it is defined
-- @param[default=self.clone_nodes, type=?table] alt_nodes a table containing parameters for cloneCustom
-- to be merged with self.clone_nodes
-- @return the cloned actor
function _M:cloneActor(post_copy, alt_nodes)
alt_nodes = table.merge(alt_nodes or {}, self.clone_nodes, true)
if post_copy or self.clone_copy then post_copy = post_copy or {} table.update(post_copy, self.clone_copy or {}, true) end
local a = self:cloneCustom(alt_nodes, post_copy)
a:removeAllMOs()
return a, post_copy
end
--- Setup minimap color for this entity
-- You may overload this method to customize your minimap
-- @param mo
......
......@@ -360,14 +360,14 @@ function _M:apply()
self.actor.descriptor = {}
local stats, inc_stats = {}, {}
for i, d in ipairs(self.descriptors) do
print("[BIRTHER] Applying descriptor "..(d.name or "none"))
print("[BIRTHER] Applying descriptor "..(d.type or "untyped").."."..(d.name or "none"))
self.actor.descriptor[d.type or "none"] = (d.name or "none")
if d.copy then
local copy = table.clone(d.copy, true)
-- Append array part
while #copy > 0 do
local f = table.remove(copy)
local f = table.remove(copy, 1)
table.insert(self.actor, f)
end
-- Copy normal data
......@@ -377,7 +377,7 @@ function _M:apply()
local copy = table.clone(d.copy_add, true)
-- Append array part
while #copy > 0 do
local f = table.remove(copy)
local f = table.remove(copy, 1)
table.insert(self.actor, f)
end
-- Copy normal data
......@@ -410,6 +410,7 @@ function _M:apply()
end
if d.talents then
for tid, lev in pairs(d.talents) do
print("[BIRTHER} learning talent", tid, lev)
for i = 1, lev do
self.actor:learnTalent(tid, true)
end
......
......@@ -43,6 +43,8 @@ history = {
[[< Ctrl+A or Home :=: Move the cursor to the beginning of the line >]],
[[< Ctrl+E or End :=: Move the cursor to the end of the line >]],
[[< Ctrl+K or Ctrl+End :=: Move the cursor to the end of the line >]],
[[< Ctrl+Backspace :=: Delete to beginning of line >]],
[[< Ctrl+Del :=: Delete to end of line >]],
[[< Up/down arrows :=: Move between previous/later executed lines >]],
[[< Ctrl+Space :=: Print help for the function to the left of the cursor >]],
[[< Ctrl+Shift+Space :=: Print the entire definition for the function >]],
......@@ -216,13 +218,19 @@ function _M:init()
end,
_BACKSPACE = function()
if _M.line_pos > 0 then
_M.line = _M.line:sub(1, _M.line_pos - 1) .. _M.line:sub(_M.line_pos + 1)
_M.line_pos = _M.line_pos - 1
local st = core.key.modState("ctrl") and 0 or _M.line_pos - 1
for i = _M.line_pos - 1, st, -1 do
_M.line = _M.line:sub(1, _M.line_pos - 1) .. _M.line:sub(_M.line_pos + 1)
_M.line_pos = _M.line_pos - 1
end
end
self.changed = true
end,
_DELETE = function()
_M.line = _M.line:sub(1, _M.line_pos) .. _M.line:sub(_M.line_pos + 2)
local st = core.key.modState("ctrl") and #_M.line or _M.line_pos
for i = _M.line_pos, st do
_M.line = _M.line:sub(1, _M.line_pos) .. _M.line:sub(_M.line_pos + 2)
end
self.changed = true
end,
[{"_END", "ctrl"}] = function()
......@@ -452,7 +460,7 @@ end
-- @param[type=boolean] verbose give extra junk
function _M:functionHelp(func, verbose)
if type(func) ~= "function" then return nil, "Can only give help on functions." end
local info = debug.getinfo(func)
local info = debug.getinfo(func, "S")
-- Check the path exists
local fpath = string.gsub(info.source,"@","")
if not fs.exists(fpath) then return nil, ([[%s does not exist.]]):format(fpath) end
......
......@@ -712,14 +712,28 @@ function _M:toScreen(tiles, x, y, w, h, a, allow_cb, allow_shader)
core.map.mapObjectsToScreen(x, y, w, h, a, allow_cb, allow_shader, unpack(list))
end
--- Resolves an entity
-- This is called when generating the final clones of an entity for use in a level.
-- This can be used to make random enchants on objects, random properties on actors, ...
-- by default this only looks for properties with a table value containing a __resolver field
-- @param[type=?table] t table defaults to self
-- @param[type=?boolean] last resolve last
-- @param[type=?boolean] on_entity
-- @param[type=?table] key_chain stores keys, defaults to {}
--- Resolves an entity, finishing resolvers previously defined for it
-- Called when generating the final clones of (instantiating) a prototype entity for use in the game
-- This may be used to make random enchants on objects, add random properties on actors, etc.
-- Recursively searches for all property fields assigned a resolver table, assigning final values to those properties
-- (resolver tables are tables containing a __resolver field by default)
-- @see resolvers.lua
-- @param[type=?table] t table to search recursively for resolver tables (defaults to self)
-- @param[type=?boolean] last handle resolvers with .__resolve_last set (which are skipped if this is not set)
-- @param[type=?table] on_entity alternate entity for which to evaluate resolvers
-- @param[type=?table] key_chain (defaults to {}) contains keys recursively traversed while resolving
-- During the recursive search:
-- subtables with .__ATOMIC or .__CLASSNAME are skipped
-- the key_chain table is appended with each recursive key
-- resolver tables (containing .__resolver) are evaluated
-- resolvers are 2-part functions:
-- resolver.foo(...) (called when creating the entity prototype) assigns a resolver table to a property field within the entity
-- The resolver table contains .__resolver == "foo" and all other data needed to create the final value for the property field when the final entity instance is prepared (performed here)
--- If the resolver table contains .__resolve_instant, it will be resolved immediately (before the next level of recursion)
-- resolver.calc.foo(t, e) (called here) calculates the final value for ("resolves") the property field where:
-- t = table generated by resolver.foo and stored in the property field
-- e = the entity on which to perform the changes
-- the property field is set to the return value of this function
function _M:resolve(t, last, on_entity, key_chain)
t = t or self
key_chain = key_chain or {}
......@@ -737,8 +751,8 @@ function _M:resolve(t, last, on_entity, key_chain)
-- Then we handle it, this is because resolvers can modify the list with their returns, or handlers, so we must make sure to not modify the list we are iterating over
local r
for k, e in pairs(list) do
if type(e) == "table" and e.__resolver then
end
-- if type(e) == "table" and e.__resolver then
-- end
if type(e) == "table" and e.__resolver and (not e.__resolve_last or last) then
if not resolvers.calc[e.__resolver] then error("missing resolver "..e.__resolver.." on entity "..tostring(t).." key "..table.concat(key_chain, ".")) end
r = resolvers.calc[e.__resolver](e, on_entity or self, self, t, k, key_chain)
......
......@@ -1016,6 +1016,7 @@ function _M:instanciate(mod, name, new_game, no_reboot, extra_module_info)
profile:addStatFields(unpack(mod.profile_stats_fields or {}))
profile:setConfigsBatch(true)
profile:loadModuleProfile(mod.short_name, mod)
profile:incrLoadProfile(mod)
profile:currentCharacter(mod.full_version_string, "game did not tell us")
UIBase:clearCache()
......@@ -1068,28 +1069,30 @@ function _M:instanciate(mod, name, new_game, no_reboot, extra_module_info)
-- Add user chat if needed
if mod.allow_userchat and _G.game.key then
profile.chat:setupOnGame()
if not config.settings.chat or not config.settings.chat.channels or not config.settings.chat.channels[mod.short_name] then
if type(mod.allow_userchat) == "table" then
for _, chan in ipairs(mod.allow_userchat) do
profile.chat:join(chan)
profile:onAuth(function()
if not config.settings.chat or not config.settings.chat.channels or not config.settings.chat.channels[mod.short_name] then
if type(mod.allow_userchat) == "table" then
for _, chan in ipairs(mod.allow_userchat) do
profile.chat:join(chan)
end
if mod.allow_userchat[1] then profile.chat:selectChannel(mod.allow_userchat[1]) end
else
profile.chat:join(mod.short_name)
profile.chat:join(mod.short_name.."-spoiler")
profile.chat:join("global")
profile.chat:selectChannel(mod.short_name)
end
if mod.allow_userchat[1] then profile.chat:selectChannel(mod.allow_userchat[1]) end
print("Joining default channels")
else
profile.chat:join(mod.short_name)
profile.chat:join(mod.short_name.."-spoiler")
profile.chat:join("global")
profile.chat:selectChannel(mod.short_name)
end
print("Joining default channels")
else
local def = false
for c, _ in pairs(config.settings.chat.channels[mod.short_name]) do
profile.chat:join(c)
if c == mod.short_name then def = true end
local def = false
for c, _ in pairs(config.settings.chat.channels[mod.short_name]) do
profile.chat:join(c)
if c == mod.short_name then def = true end
end
if def then profile.chat:selectChannel(mod.short_name) else profile.chat:selectChannel( (next(config.settings.chat.channels[mod.short_name])) ) end
print("Joining selected channels")
end
if def then profile.chat:selectChannel(mod.short_name) else profile.chat:selectChannel( (next(config.settings.chat.channels[mod.short_name])) ) end
print("Joining selected channels")
end
end)
end
-- Disable the profile if ungood
......
......@@ -79,6 +79,7 @@ function _M:init()
self.generic = {}
self.modules = {}
self.evt_cbs = {}
self.data_log = {log={}}
self.stats_fields = {}
local checkstats = function(self, field) return self.stats_fields[field] end
self.config_settings =
......@@ -236,6 +237,7 @@ function _M:loadModuleProfile(short_name, mod_def)
-- Delay when we are currently saving
if savefile_pipe and savefile_pipe.saving then savefile_pipe:pushGeneric("loadModuleProfile", function() self:loadModuleProfile(short_name) end) return end
local def = mod_def.profile_defs or {}
local function load(online)
local pop = self:mountProfile(online, short_name)
local d = "/current-profile/modules/"..short_name.."/"
......@@ -251,6 +253,12 @@ function _M:loadModuleProfile(short_name, mod_def)
else
self.modules[short_name][field] = self.modules[short_name][field] or {}
self:loadData(f, self.modules[short_name][field])
if def[field].incr_only then
if not self.modules[short_name][field].incr_only then
print("[PROFILE] Old non incremental data for "..field..": discarding")
self.modules[short_name][field] = {}
end
end
end
end
end
......@@ -319,6 +327,7 @@ function _M:saveModuleProfile(name, data, nosync, nowrite)
if module == "boot" then return end
core.game.resetLocale()
if not game or not game.__mod_info.profile_defs then return end
if game.__mod_info.profile_defs[name].incr_only then print("[PROFILE] data in incr only mode but called with saveModuleProfile", name) return end
-- Delay when we are currently saving
if savefile_pipe and savefile_pipe.saving then savefile_pipe:pushGeneric("saveModuleProfile", function() self:saveModuleProfile(name, data, nosync) end) return end
......@@ -364,6 +373,49 @@ function _M:saveModuleProfile(name, data, nosync, nowrite)
if not nosync and not game.__mod_info.profile_defs[name].no_sync then self:setConfigs(module, name, data) end
end
--- Loads the incremental log data
function _M:incrLoadProfile(mod_def)
if not mod_def or not mod_def.short_name then return end
local pop = self:mountProfile(true)
local file = "/current-profile/modules/"..mod_def.short_name.."/incr.log"
if fs.exists(file) then
local f, err = loadfile(file)
if not f and err then
print("Error loading incr log", file, err)
else
self:loadData(f, self.data_log)
end
end
self:umountProfile(true, pop)
end
--- Saves a incr profile data
function _M:incrDataProfile(name, data)
if module == "boot" then return end
core.game.resetLocale()
if not game or not game.__mod_info.profile_defs then return end
if not game.__mod_info.profile_defs[name].incr_only then print("[PROFILE] data in non-incr only mode but called with incrDataProfile", name) return end
-- Delay when we are currently saving
if savefile_pipe and savefile_pipe.saving then savefile_pipe:pushGeneric("incrDataProfile", function() self:incrDataProfile(name, data) end) return end
local module = self.mod_name
-- Check for readability
local dataenv = self.data_log.log
dataenv[#dataenv+1] = {module=game.__mod_info.short_name, kind=name, data=data}
local pop = self:mountProfile(true, module)
local f = fs.open("/modules/"..module.."/incr.log", "w")
if f then
f:write(serialize(self.data_log))
f:close()
end
self:umountProfile(true, pop)
self:syncIncrData()
end
function _M:checkFirstRun()
local result = self.generic.firstrun
if not result then
......@@ -483,6 +535,12 @@ function _M:waitFirstAuth(timeout)
end
end
function _M:onAuth(fct)
if self.auth then fct() return end
self.on_auth_cb = self.on_auth_cb or {}
self.on_auth_cb[#self.on_auth_cb+1] = fct
end
function _M:eventAuth(e)
self.waiting_auth = false
self.connected = true
......@@ -491,6 +549,8 @@ function _M:eventAuth(e)
self.auth = e.ok:unserialize()
print("[PROFILE] Main thread got authed", self.auth.name)
self:getConfigs("generic", function(e) self:syncOnline(e.module) end)
for _, fct in ipairs(self.on_auth_cb or {}) do fct() end
self.on_auth_cb = nil
else
self.auth_last_error = e.reason or "unknown"
end
......@@ -503,6 +563,16 @@ function _M:eventGetNews(e)
end
end
function _M:eventIncrLogConsume(e)
local module = game.__mod_info.short_name
if not module then return end
print("[PROFILE] Server accepted our incr log, deleting")
local pop = self:mountProfile(true, module)
fs.delete("/modules/"..module.."/incr.log")
self:umountProfile(true, pop)
self.data_log.log = {}
end
function _M:eventGetConfigs(e)
local data = zlib.decompress(e.data):unserialize()
local module = e.module
......@@ -542,11 +612,13 @@ end
function _M:eventConnected(e)
if game and type(game) == "table" and game.log then game.log("#YELLOW#Connection to online server established.") end
print("[PlayerProfile] eventConnected")
self.connected = true
end
function _M:eventDisconnected(e)
if game and type(game) == "table" and game.log and self.connected then game.log("#YELLOW#Connection to online server lost, trying to reconnect.") end
print("[PlayerProfile] eventDisconnected")
self.connected = false
end
......@@ -607,14 +679,14 @@ function _M:getConfigs(module, cb, mod_def)
if not self.auth then return end
self.evt_cbs.GetConfigs = cb
if module == "generic" then
for k, _ in pairs(generic_profile_defs) do
if not _.no_sync then
for k, def in pairs(generic_profile_defs) do
if not def.no_sync then
core.profile.pushOrder(table.serialize{o="GetConfigs", module=module, kind=k})
end
end
else
for k, _ in pairs((mod_def or game.__mod_info).profile_defs or {}) do
if not _.no_sync then
for k, def in pairs((mod_def or game.__mod_info).profile_defs or {}) do
if not def.no_sync and not def.incr_only then
core.profile.pushOrder(table.serialize{o="GetConfigs", module=module, kind=k})
end
end
......@@ -625,6 +697,15 @@ function _M:setConfigsBatch(v)
core.profile.pushOrder(table.serialize{o="SetConfigsBatch", v=v and true or false})
end
function _M:syncIncrData()
self:waitFirstAuth()
if not self.auth then return end
local module = game and game.__mod_info.short_name
if not module then return end
core.profile.pushOrder(table.serialize{o="SendIncrLog", data=zlib.compress(table.serialize(self.data_log.log))})
end
function _M:setConfigs(module, name, data)
self:waitFirstAuth()
if not self.auth then return end
......@@ -660,7 +741,7 @@ function _M:syncOnline(module, mod_def)
end
else
for k, def in pairs((mod_def or game.__mod_info).profile_defs or {}) do
if not def.no_sync and def.export and sync[k] then
if not def.no_sync and not def.incr_only and def.export and sync[k] then
local f = def.export
local ret = {}
setfenv(f, setmetatable({add=function(d) ret[#ret+1] = d end}, {__index=_G}))
......
......@@ -698,17 +698,22 @@ function _M:loadEntity(name)
local loadedEntity = self:loadReal("main")
-- Delay loaded must run
for i, o in ipairs(self.delayLoad) do
-- print("loader executed for class", o, o.__CLASSNAME)
o:loaded()
end
local ok = false
pcall(function()
for i, o in ipairs(self.delayLoad) do
-- print("loader executed for class", o, o.__CLASSNAME)
o:loaded()
end
ok = true
end)
-- We check for the server return, while we loaded the save it probably responded so we do not wait at all
if not checker() then self:badMD5Load() end
if ok and not checker() then self:badMD5Load() end
popup:done()
fs.umount(path)
if not ok then return false end
return loadedEntity
end
......
......@@ -263,11 +263,30 @@ function _M:computeRarities(type, list, level, filter, add_level, rarity_field)
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
-- @param e: the entity to check
-- @param filter: the filter to use, a table specifying the checks to perform on the entity
-- @param typ: the type of entity, one of "actor", "projectie", "object", "trap", "terrain", "grid", "trigger"
-- @return true if the entity passes all of the filter checks
-- filter fields interpreted:
-- allow_uniques: don't reject existing uniques
-- unique: e[unique] must be defined
-- ignore: a filter specifying what entities to be specifically rejected
-- type: e[type] must match exactly
-- subtype: e[subtype] must match exactly
-- name: e[name] must match exactly
-- define_as: e.define_as must match exactly
-- properties: table of keys, e[key] MUST BE defined for all
-- not_properties: table of keys, e[key] MUST NOT BE defined for all
-- special: function(e, filter) that must return true
-- max_ood: perform an Out of Depth check (requires resolvers.current_level + max_ood > e.level_range[1])
-- Rejects existing uniques
-- If it is defined, e.checkFilter(filter) must return true
-- If it is defined, self:check_filter(e, filter, typ) must return true
function _M:checkFilter(e, filter, typ)
if e.unique and not (filter and filter.allow_uniques) 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
if filter.ignore and self:checkFilter(e, filter.ignore, typ) 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
......@@ -282,8 +301,8 @@ function _M:checkFilter(e, filter, type)
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.special and not filter.special(e, filter) then return false end
if self.check_filter and not self:check_filter(e, filter, typ) 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
......@@ -291,18 +310,14 @@ function _M:checkFilter(e, filter, type)
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
--- Return a string summary of a filter
function _M:filterToString(filter, ...)
return string.fromTable(filter, ...)
end
--- Picks an entity from a computed probability list
function _M:pickEntity(list)
if #list == 0 then return nil end
if not list or #list == 0 then return nil end
local r = rng.range(1, list.total)
for i = 1, #list do
-- print("test", r, ":=:", list[i].genprob)
......@@ -351,10 +366,13 @@ function _M:getEntities(level, type)
return list
end
--- Picks and resolve an entity
--- Picks and resolves an entity
-- @param level a Level object to generate for
-- @param type one of "object" "terrain" "actor" "trap"
-- @param filter a filter table
-- @param type one of "object" "terrain" "actor" "trap" or a table of entities with __real_type defined
-- @param filter a filter table with optional fields:
-- base_list: an entities list (table) or a specifier to load the entities list from a file, format: <classname>:<file path> (takes priority over type)
-- special_rarity: alternate field for entity rarity field (default 'rarity')
-- nb_tries: maximum number of attempts to randomly pick the entity from the list
-- @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[1] nil if a filter was given an nothing found
......@@ -367,10 +385,15 @@ function _M:makeEntity(level, type, filter, force_level, prob_filter)
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 list
if _G.type(type) == "table" then -- use the provided list
list = type
type = type.__real_type or ""
end
local e
-- No probability list, use the default one and apply filter
if not prob_filter then
local list = self:getEntities(level, type)
list = list or self:getEntities(level, type)
local tries = filter and filter.nb_tries or 500
-- CRUDE ! Brute force ! Make me smarter !
while tries > 0 do
......@@ -381,7 +404,7 @@ function _M:makeEntity(level, type, filter, force_level, prob_filter)
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
local base_list = list
if filter.base_list then
if _G.type(filter.base_list) == "table" then base_list = filter.base_list
else
......@@ -390,17 +413,21 @@ function _M:makeEntity(level, type, filter, force_level, prob_filter)
base_list = require(class):loadList(file)
end
end
type = base_list and base_list.__real_type or type
elseif base_list then -- type = base_list.__real_type
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
elseif not base_list then
base_list = self:getEntities(level, type)
end
if not base_list then return nil 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)
else -- No filter
list = list or 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)
......@@ -420,9 +447,20 @@ function _M:makeEntity(level, type, filter, force_level, prob_filter)
return e
end
--- Find a given entity and resolve it
-- @return[1] nil if a filter was given an nothing found
--- Find a specific entity and resolve it
-- @param level a Level object to generate for
-- @param type defines where to find the entity definition:
-- a table is searched directly as a list of entities
-- "object" -- look in zone.object_list
-- "terrain", "grid", "trigger" -- look in zone.grid_list
-- "actor" -- look in zone.npc_list
-- "trap" -- look in zone.trap_list
-- other strings -- specifier to load the entities list from a file, format: <classname>:<file path>
-- @param name the name of the entity to find (must match entity.define_as exactly)
-- @param force_unique if not set, duplicate uniques will not be generated
-- @return[1] nil if the entity could not be found or was a pre-existing unique
-- @return[2] the fully resolved entity, ready to be used on a level
-- @return[2] boolean true if the entity was a pre-existing unique
function _M:makeEntityByName(level, type, name, force_unique)
resolvers.current_level = self.base_level + level.level - 1
......@@ -432,7 +470,16 @@ function _M:makeEntityByName(level, type, name, force_unique)
elseif type == "object" then e = self.object_list[name]
elseif type == "grid" or type == "terrain" or type == "trigger" then e = self.grid_list[name]
elseif type == "trap" then e = self.trap_list[name]
else
local base_list
local _, _, class, file = type:find("(.*):(.*)")
if class and file then
base_list = require(class):loadList(file)
type = base_list.__real_type or type
end
e = base_list and base_list[name]
end
if not e then return nil end
local forced = false
......@@ -679,29 +726,31 @@ function _M:finishEntity(level, type, e, ego_filter)
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/>
-- Grids do not really need this, it 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
-- @param no_added have we added it
-- @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", "projectile", "object", "trap", "terrain", "grid", "trigger"
-- @param x: x coordinate. This CAN be null in which case it wont be added to the map
-- @param y: y coordinate. This CAN be null in which case it wont be added to the map
-- @param no_added: set true to prevent calling e:added()
-- checks e.addedToLevel then e.on_added after adding the entity
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
-- We are adding 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
if self.actor_adjust_level and e.forceLevelup and not e._actor_adjust_level_applied then
local newlevel = self:actor_adjust_level(level, e)
e:forceLevelup(newlevel + (e.__forced_level or 0))
e._actor_adjust_level_applied = true
end
elseif typ == "projectile" then
-- We are additing it, this means there is no old position
-- We are adding 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
......@@ -1107,6 +1156,9 @@ function _M:newLevel(level_data, lev, old_lev, game)
-- Delete the room_map if it's no longer needed
if not self._retain_level_room_map then map.room_map = nil end
-- Call a "post" finisher
if level_data.post_process_end then level_data.post_process_end(level, self) end
return level
end
......
......@@ -265,13 +265,13 @@ end
--- Clones the object, and all subobjects without cloning a subobject twice
-- @param[type=table] self Object to be cloned.
-- @param[type=table] post_copy Optional, a table to be merged with the new object after cloning.
-- @param[type=table] post_copy Optional, a table to be merged (recursively) with the new object after cloning.
-- @return a reference to the clone
function _M:cloneFull(post_copy)
local clonetable = {}
local n = clonerecursfull(clonetable, self, nil, nil)
if post_copy then
for k, e in pairs(post_copy) do n[k] = e end
table.merge(n, post_copy, true)
end
return n
-- return core.serial.cloneFull(self)
......@@ -340,13 +340,13 @@ end
-- @ or nil to use the default name/ref as keys on the clone
-- @ v = the value to assign for instances of this node,
-- @ or nil to use the default assignment value
-- @param[type=table] post_copy Optional, a table to be merged with the new object after cloning.
-- @param[type=table] post_copy Optional, a table to be merged (recursively) with the new object after cloning.
-- @return a reference to the clone
function _M:cloneCustom(alt_nodes, post_copy)
local clonetable = {}
local n = cloneCustomRecurs(clonetable, self, nil, nil, alt_nodes)
if post_copy then
for k, e in pairs(post_copy) do n[k] = e end
table.merge(n, post_copy, true)
end
return n
end
......
......@@ -38,6 +38,7 @@ function _M:init(adds)
{name = "Other achievements", kind = "achievement_other"},
}
for i, l in ipairs(adds or {}) do list[#list+1] = l end
self:triggerHook{"ChatFilters:list", list=list}
local c_desc = Textzone.new{width=self.iw - 10, height=1, auto_height=true, text="Select which types of chat events to see or not."}
local uis = { {left=0, top=0, ui=c_desc} }
......
......@@ -191,7 +191,6 @@ function _M:generate(lev, old_lev)
break
end
end
local r = self:roomAlloc(rroom, #rooms+1, lev, old_lev)
if r then nb_room = nb_room -1 end
tries = tries - 1
......
......@@ -66,20 +66,22 @@ function _M:init(t)
self:initBody()
end
--- generate inventories according to the body definition table
--- Generate inventories according to the body definition table
-- This creates new inventories or updates existing ones
-- @param self.body = {SLOT_ID = max, ...}
-- @param max = number of slots if number or table of properties (max = , stack_limit = , ..) merged into definition
function _M:initBody()
if self.body then
local def
local long_name, def
for inven, max in pairs(self.body) do
def = self.inven_def[self["INVEN_"..inven]]
assert(def, "inventory slot undefined")
self.inven[self["INVEN_"..inven]] = {worn=def.is_worn, id=self["INVEN_"..inven], name=inven, stack_limit = def.stack_limit}
long_name = "INVEN_"..inven
def = self.inven_def[self[long_name]]
assert(def, "inventory slot undefined: "..inven)
self.inven[self[long_name]] = table.merge(self.inven[self[long_name]] or {}, {worn=def.is_worn, id=self[long_name], name=inven, short_name=def.short_name, stack_limit = def.stack_limit})
if type(max) == "table" then
table.merge(self.inven[self["INVEN_"..inven]], max, true)
table.merge(self.inven[self[long_name]], max, true)
else
self.inven[self["INVEN_"..inven]].max = max
self.inven[self[long_name]].max = max
end
end
self.body = nil
......@@ -97,7 +99,7 @@ function _M:getInven(id)
end
end
--- Returns the content of an inventory as a table
--- Returns the inventory definition
function _M:getInvenDef(id)
if type(id) == "number" then
return self.inven_def[id]
......
......@@ -53,6 +53,7 @@ function _M:defineResource(name, short_name, talent, regen_prop, desc, min, max,
talent = talent,
regen_prop = regen_prop,
invert_values = false, -- resource value decreases as it is consumed by default
switch_direction = false, -- resource value prefers to go to the min instead of max
description = desc,
minname = minname,
maxname = maxname,
......@@ -126,7 +127,7 @@ function _M:init(t)
for i, r in ipairs(_M.resources_def) do
self[r.minname] = t[r.minname] or r.min
self[r.maxname] = t[r.maxname] or r.max
self[r.short_name] = t[r.short_name] or self[r.maxname]
self[r.short_name] = t[r.short_name] or (r.switch_direction and self[r.minname] or self[r.maxname])
if r.regen_prop then
self[r.regen_prop] = t[r.regen_prop] or 0
end
......
......@@ -815,6 +815,9 @@ function _M:updateTalentTypeMastery(tt)
end
end
end
if self.talents_types_def[tt] and self.talents_types_def[tt].on_mastery_change then
self.talents_types_def[tt].on_mastery_change(self, self:getTalentTypeMastery(tt), tt)
end
end
--- Return talent definition from id
......
......@@ -135,7 +135,7 @@ function _M:setEffect(eff_id, dur, p, silent)
local ret, fly = ed.on_gain(self, p)
if not silent and not had then
if ret then
game.logSeen(self, ret:gsub("#Target#", self.name:capitalize()):gsub("#target#", self.name):gsub("#himher#", self.female and "her" or "him"))
game.logSeen(self, ret:gsub("#Target#", self.name:capitalize()):gsub("#target#", self.name):gsub("#himher#", self.female and "her" or "him"):gsub("#hisher#", self.female and "her" or "his"))
end
if fly and game.flyers and self.x and self.y and game.level.map.seens(self.x, self.y) then
if fly == true then fly = "+"..ed.desc end
......
......@@ -19,6 +19,14 @@
--- @script engine.resolvers
--- resolvers are 2-part functions used to turn prototype Entities into finished versions for use in the game:
-- resolver.foo(...) (called when creating the Entity prototype) assigns a resolver table to a property field within the entity
-- The resolver table contains .__resolver == "foo" and all other data needed to create the final value for the property field when the final entity instance is prepared (performed by Entity:resolve)
--- If the resolver table contains .__resolve_instant, it will be resolved before other resolvers.
-- resolver.calc.foo(t, e) (called by Entity:resolve) calculates the final value for ("resolves") the property field where:
-- t = table generated by resolver.foo and stored in the property field
-- e = the entity on which to perform the changes
-- Entity:resolve assigns the return value of this function to the property field
resolvers = {}
resolvers.calc = {}
......
......@@ -243,8 +243,10 @@ end
-- @param w, h = width and height of the dialog (in pixels, optional: dialog sized to its elements by default)
-- @param no_leave set true to force a selection
-- @param escape = the default choice (number) to select if escape is pressed
function _M:multiButtonPopup(title, text, button_list, w, h, choice_fct, no_leave, escape)
-- @param default = the default choice (number) to select (highlight) when the dialog opens, default 1
function _M:multiButtonPopup(title, text, button_list, w, h, choice_fct, no_leave, escape, default)
escape = escape or 1
default = default or 1
-- compute display limits
local max_w, max_h = w or game.w*.75, h or game.h*.75
......@@ -256,7 +258,10 @@ function _M:multiButtonPopup(title, text, button_list, w, h, choice_fct, no_leav
local d = new(title, w or 1, h or 1)
--print(("[multiButtonPopup] initialized: (w:%s,h:%s), (maxw:%s,maxh:%s) "):format(w, h, max_w, max_h))
if not no_leave then d.key:addBind("EXIT", function() game:unregisterDialog(d) game:unregisterDialog(d) choice_fct(button_list[escape]) end) end
if not no_leave then d.key:addBind("EXIT", function() game:unregisterDialog(d)
if choice_fct then choice_fct(button_list[escape]) end
end)
end
local num_buttons = math.min(#button_list, 50)
local buttons, buttons_width, button_height = {}, 0, 0
......@@ -298,7 +303,7 @@ function _M:multiButtonPopup(title, text, button_list, w, h, choice_fct, no_leav
local width = w or math.min(max_w, math.max(text_width + 20, max_buttons_width + 20))
local height = h or math.min(max_h, text_height + 10 + nrow*button_height)
local uis = {
{left = (width - text_width)/2, top = 3, ui=require("engine.ui.Textzone").new{width=text_width, height=text_height, text=text}}
{left = (width - text_width)/2, top = 3, ui=require("engine.ui.Textzone").new{width=text_width, height=text_height, text=text, can_focus=false}}
}
-- actually place the buttons in the dialog
top = math.max(text_height, text_height + (height - text_height - nrow*button_height - 5)/2)
......@@ -312,7 +317,10 @@ function _M:multiButtonPopup(title, text, button_list, w, h, choice_fct, no_leav
end
end
d:loadUI(uis)
if uis[escape + 1] then d:setFocus(uis[escape + 1]) end
-- set default focus if possible
if uis[default + 1] then d:setFocus(uis[default + 1])
elseif uis[escape + 1] then d:setFocus(uis[escape + 1])
end
d:setupUI(not w, not h)
game:registerDialog(d)
return d
......
......@@ -98,17 +98,23 @@ function _M:generate()
self.key:addCommands{
_LEFT = function() self.cursor = util.bound(self.cursor - 1, 1, #self.tmp+1) self.scroll = util.scroll(self.cursor, self.scroll, self.max_display) self:updateText() end,
_RIGHT = function() self.cursor = util.bound(self.cursor + 1, 1, #self.tmp+1) self.scroll = util.scroll(self.cursor, self.scroll, self.max_display) self:updateText() end,
_DELETE = function()
if self.cursor <= #self.tmp then
table.remove(self.tmp, self.cursor)
_BACKSPACE = function()
if self.cursor > 1 then
local st = core.key.modState("ctrl") and 1 or self.cursor - 1
for i = self.cursor - 1, st, -1 do
table.remove(self.tmp, i)
self.cursor = self.cursor - 1
self.scroll = util.scroll(self.cursor, self.scroll, self.max_display)
end
self:updateText()
end
end,
_BACKSPACE = function()
if self.cursor > 1 then
table.remove(self.tmp, self.cursor - 1)
self.cursor = self.cursor - 1
self.scroll = util.scroll(self.cursor, self.scroll, self.max_display)
_DELETE = function()
if self.cursor <= #self.tmp then
local num = core.key.modState("ctrl") and #self.tmp - self.cursor + 1 or 1
for i = 1, num do
table.remove(self.tmp, self.cursor)
end
self:updateText()
end
end,
......@@ -145,6 +151,10 @@ function _M:generate()
self:updateText()
end
end,
[{"_c", "ctrl"}] = function(c)
self:updateText()
core.key.setClipboard(self.text)
end,
}
end
......
......@@ -158,7 +158,7 @@ end
--- Returns a clone of a table
-- @param tbl The original table to be cloned
-- @param deep Boolean to determine if recursive cloning occurs
-- @param deep Boolean allow recursive cloning (unless .__ATOMIC or .__CLASSNAME is defined)
-- @param k_skip A table containing key values set to true if you want to skip them.
-- @return The cloned table.
function table.clone(tbl, deep, k_skip)
......@@ -181,27 +181,27 @@ end
table.NIL_MERGE = {}
--- Merges two tables in-place.
-- The table.NIL_MERGE is a special value that will nil out the corresponding dst key.
-- @param dst The destination table, which will have all merged values.
-- @param src The source table, supplying values to be merged.
-- @param deep Boolean that determines if tables will be recursively merged.
-- @param k_skip A table containing key values set to true if you want to skip them.
-- @param k_skip_deep Like k_skip, except this table is passed on to the deep recursions.
-- @param addnumbers Boolean that determines if two numbers will be added rather than replaced.
-- assign the special value table.NIL_MERGE in src to set the corresponding dst field to nil.
-- subtables containing .__ATOMIC or .__CLASSNAME will be copied by reference.
function table.merge(dst, src, deep, k_skip, k_skip_deep, addnumbers)
k_skip = k_skip or {}
k_skip_deep = k_skip_deep or {}
for k, e in pairs(src) do
if not k_skip[k] and not k_skip_deep[k] then
-- Recursively merge tables
if deep and dst[k] and type(e) == "table" and type(dst[k]) == "table" and not e.__ATOMIC and not e.__CLASSNAME then
if e == table.NIL_MERGE then -- remove corresponding field
dst[k] = nil
elseif deep and dst[k] and type(e) == "table" and type(dst[k]) == "table" and not e.__ATOMIC and not e.__CLASSNAME then
table.merge(dst[k], e, deep, nil, k_skip_deep, addnumbers)
-- Clone tables if into the destination
elseif deep and not dst[k] and type(e) == "table" and not e.__ATOMIC and not e.__CLASSNAME then
dst[k] = table.clone(e, deep, nil, k_skip_deep)
-- Nil out any NIL_MERGE entries
elseif e == table.NIL_MERGE then
dst[k] = nil
-- Add number entries if "add" is set
elseif addnumbers and not dst.__no_merge_add and dst[k] and type(dst[k]) == "number" and type(e) == "number" then
dst[k] = dst[k] + e
......@@ -814,6 +814,92 @@ function string.splitLines(str, max_width, font)
return lines
end
--- create a textual abbreviation for a function
-- @param fct the function
-- @param fmt output format for filepath, line number, first line of code
-- (default: "\"<function( defined: %s, line %s): %s>\"" )
-- @return string using the format provided
function string.fromFunction(fct, fmt)
local info = debug.getinfo(fct, "S")
local fpath = string.gsub(info.source,"@","")
local firstline = ""
fmt = fmt or "\"<function( defined: %s, line %s): %s>\""
if not fs.exists(fpath) then
fpath = "no file path"
firstline = info.short_src
else
local f = fs.open(fpath, "r")
local line_num = 0
while true do -- could continue with body here
firstline = f:readLine()
if firstline then
line_num = line_num + 1
if line_num == info.linedefined then break end
end
end
end
return (fmt):format(fpath, info.linedefined, tostring(firstline))
end
--- Create a textual representation of a value
-- similar to tostring, but includes special handling of tables and functions
-- surrounds non-numbers/booleans/nils/functions with ""
-- @param v: the value
-- @param recurse: the recursion level for string.fromTable, set < 0 for basic tostring
-- @param offset, prefix, suffix: inputs to string.fromTable for converting tables
function string.fromValue(v, recurse, offset, prefix, suffix)
recurse, offset, prefix, suffix = recurse or 0, offset or ", ", prefix or "{", suffix or "}"
local vt, vs = type(v)
if vt == "table" then
if recurse < 0 then vs = tostring(v)
elseif v.__ATOMIC or v.__CLASSNAME then -- create entity/atomic label
local abv = {}
if v.__CLASSNAME then abv[#abv+1] = "__CLASSNAME="..tostring(v.__CLASSNAME) end
if v.__ATOMIC then abv[#abv+1] = "ATOMIC" end
vs = ("%s\"%s%s%s\"%s"):format(prefix, v, v.__CLASSNAME and ", __CLASSNAME="..tostring(v.__CLASSNAME) or "", v.__ATOMIC and ", ATOMIC" or "", suffix)
elseif recurse > 0 then -- get recursive string
vs = string.fromTable(v, recurse - 1, offset, prefix, suffix)
else vs = prefix.."\""..tostring(v).."\""..suffix
end
elseif vt == "function" then
vs = recurse >= 0 and string.fromFunction(v) or tostring(v)
elseif not (vt == "number" or vt == "boolean" or vt == "nil") then
vs = "\""..tostring(v).."\""
end
return vs or tostring(v)
end
--- Create a textual representation of a table
-- This is like reverse-interpreting back to lua code, compatible for strings, numbers, and tables
-- @param src: source table
-- @param recurse: recursion level for subtables (default 0)
-- @param offset: string to insert between table fields (default: ", ")
-- @param prefix: prefix for the table and subtables (default: "{")
-- @param suffix: suffix for the table and subtables (default: "}")
-- @param key_recurse the recursion level for keys that are tables (default 0)
-- @return[1] single line text representation of src
-- non-string table.keys are surrounded by "[", "]"
-- @return[2] indexed table containing strings for each key/value pair in src (@recursion level 0)
-- recursed subtables are converted and embedded
-- subtables containing .__ATOMIC or .__CLASSNAME are never converted, but are noted
-- functions are converted to embedded strings using string.fromFunction
function string.fromTable(src, recurse, offset, prefix, suffix, key_recurse)
if type(src) ~= "table" then print("string.fromTable has no table:", src) return tostring(src) end
local tt = {}
recurse, offset, prefix, suffix = recurse or 0, offset or ", ", prefix or "{", suffix or "}"
for k, v in pairs(src) do
local kt, vt = type(k), type(v)
local ks, vs
if kt ~= "string" then
ks = "["..string.fromValue(k, key_recurse, offset, prefix, suffix).."]"
end
vs = string.fromValue(v, recurse, offset, prefix, suffix)
tt[#tt+1] = ("%s=%s"):format(ks or tostring(k), vs or tostring(v))
end
-- could sort here if desired
return prefix..table.concat(tt, offset)..suffix, tt
end
-- Split a string by the given character(s)
function string.split(str, char, keep_separator)
char = lpeg.P(char)
......
......@@ -41,7 +41,7 @@ function _M:init()
l[#l+1] = {name="New Game", fct=function() game:registerDialog(require("mod.dialogs.NewGame").new()) end}
l[#l+1] = {name="Load Game", fct=function() game:registerDialog(require("mod.dialogs.LoadGame").new()) end}
-- l[#l+1] = {name="Online Profile", fct=function() game:registerDialog(require("mod.dialogs.Profile").new()) end}
l[#l+1] = {name="View High Scores", fct=function() game:registerDialog(require("mod.dialogs.ViewHighScores").new()) end}
-- l[#l+1] = {name="View High Scores", fct=function() game:registerDialog(require("mod.dialogs.ViewHighScores").new()) end}
l[#l+1] = {name="Addons", fct=function() game:registerDialog(require("mod.dialogs.Addons").new()) end}
-- if config.settings.install_remote then l[#l+1] = {name="Install Module", fct=function() end} end
-- l[#l+1] = {name="Update", fct=function() game:registerDialog(require("mod.dialogs.UpdateAll").new()) end}
......