Commit 23e8dd318f3d6040dc136c8dcdf70513afc023da

Authored by DarkGod
2 parents e326db0d ccbcbe9c

Merge branch 'master' into WaveFunctionCollapse

Showing 72 changed files with 2008 additions and 748 deletions

Too many changes to show.

To preserve performance only 72 of 72+ files are displayed.

... ... @@ -29,12 +29,19 @@ if __SELFEXE then
29 29 print("SelfExe gave us app directory of:", dir)
30 30 fs.mount(dir..fs.getPathSeparator().."game"..fs.getPathSeparator().."thirdparty", "/", true)
31 31 fs.mount(dir..fs.getPathSeparator().."game", "/", true)
  32 + fs.setPathAllowed(dir..fs.getPathSeparator().."game", false)
32 33 else
33 34 print("No SelfExe, using basic path")
34 35 fs.mount("game"..fs.getPathSeparator().."thirdparty", "/", true)
35 36 fs.mount("game", "/", true)
36 37 end
37 38
  39 +fs.setPathAllowed(fs.getRealPath("/engines/"), false)
  40 +fs.setPathAllowed(fs.getRealPath("/thirdparty/"), false)
  41 +fs.setPathAllowed(fs.getRealPath("/addons/"), true)
  42 +if fs.getRealPath("/dlcs/") then fs.setPathAllowed(fs.getRealPath("/dlcs/"), true) end
  43 +fs.setPathAllowed(fs.getRealPath("/modules/"), true)
  44 +
38 45 -- Look for a core
39 46 function get_core(coretype, id)
40 47 coretype = coretype or "te4core"
... ...
... ... @@ -71,6 +71,11 @@ newoption {
71 71 }
72 72
73 73 newoption {
  74 + trigger = "discord-nolib",
  75 + description = "Dont build discord lib"
  76 +}
  77 +
  78 +newoption {
74 79 trigger = "web-awesomium",
75 80 description = "Use awesomium embedded browser as the webcore"
76 81 }
... ...
... ... @@ -620,7 +620,7 @@ if _OPTIONS.steam then
620 620 dofile("../steamworks/build/steam-code.lua")
621 621 end
622 622
623   -if _OPTIONS.discord then
  623 +if _OPTIONS.discord and not _OPTIONS['discord-nolib'] then
624 624 project "te4-discord"
625 625 configuration "linux"
626 626 kind "SharedLib"
... ...
... ... @@ -73,7 +73,7 @@ end
73 73 -- modules should update this as needed
74 74 _M.clone_nodes = {player=false, x=false, y=false,
75 75 fov_computed=false, fov={v={actors={}, actors_dist={}}}, distance_map={v={}},
76   - _mo=false, _last_mo=false, add_mos=false, add_displays=false,
  76 + _mo=false, _last_mo=false, add_displays=false,
77 77 shader=false, shader_args=false,
78 78 }
79 79 --- cloneActor default fields (merged by _M.cloneActor with cloneCustom)
... ... @@ -92,6 +92,13 @@ function _M:cloneActor(post_copy, alt_nodes)
92 92 if post_copy or self.clone_copy then post_copy = post_copy or {} table.update(post_copy, self.clone_copy or {}, true) end
93 93 -- Clone all except sub-actors which need to simply reference the same ones
94 94 local a = self:cloneCustom(alt_nodes, function(d) return not d:isClassName("mod.class.Actor") end, post_copy)
  95 + -- Handle add_displays as a special case
  96 + if self.add_displays then
  97 + a.add_displays = {}
  98 + for i, d in ipairs(self.add_displays) do
  99 + table.insert(a.add_displays, d:cloneFull())
  100 + end
  101 + end
95 102 a:removeAllMOs()
96 103 return a, post_copy
97 104 end
... ...
... ... @@ -43,16 +43,17 @@ function _M:init(name, npc, player, data)
43 43
44 44 local f, err = loadfile(self:getChatFile(name))
45 45 if not f and err then error(err) end
46   - setfenv(f, setmetatable({
  46 + local env = setmetatable({
47 47 cur_chat = self,
48 48 setDialogWidth = function(w) self.force_dialog_width = w end,
49 49 newChat = function(c) self:addChat(c) end,
50 50 setTextFont = function(font, size) self.dialog_text_font = {font, size} end,
51 51 setAnswerFont = function(font, size) self.dialog_answer_font = {font, size} end,
52   - }, {__index=data}))
  52 + }, {__index=data})
  53 + setfenv(f, env)
53 54 self.default_id = f()
54 55
55   - self:triggerHook{"Chat:load", data=data}
  56 + self:triggerHook{"Chat:load", data=data, env=env}
56 57 end
57 58
58 59 --- Get chat file
... ...
... ... @@ -32,17 +32,18 @@ frame_oy2 = 15
32 32 --- @string text
33 33 -- @int[opt=60] dur
34 34 -- @param[opt=colors.Black] color
35   -function _M:init(text, dur, color)
  35 +function _M:init(text, dur, color, font)
36 36 self.text = text
37 37 self.dur = dur or 60
38 38 self.color = color or colors.BLACK
  39 + self.use_font = font
39 40
40   - Base.init(self, {font = {"/data/font/DroidSans-Bold.ttf", 16}})
  41 + Base.init(self, {font = self.use_font or {"/data/font/DroidSans-Bold.ttf", 16}})
41 42 end
42 43
43 44 --- on loaded
44 45 function _M:loaded()
45   - Base.init(self, {font = {"/data/font/DroidSans-Bold.ttf", 16}})
  46 + Base.init(self, {font = self.use_font or {"/data/font/DroidSans-Bold.ttf", 16}})
46 47 end
47 48
48 49 --- Serialization
... ...
... ... @@ -475,7 +475,7 @@ function _M:addonMD5(add, base)
475 475 print("[MODULE LOADER] addon ", add.short_name, " MD5", fmd5, "computed in ", core.game.getTime() - t, vbase)
476 476
477 477 if __module_extra_info.compute_md5_only then
478   - local f = io.open(__module_extra_info.compute_md5_only, "a")
  478 + local f = fs.open(__module_extra_info.compute_md5_only, "a")
479 479 f:write(("%s : addon[%s] md5\n"):format(fmd5, add.version_name))
480 480 f:close()
481 481 end
... ... @@ -982,7 +982,7 @@ function _M:instanciate(mod, name, new_game, no_reboot, extra_module_info)
982 982 print("[MODULE LOADER] module MD5", module_md5, "computed in ", core.game.getTime() - t)
983 983
984 984 if __module_extra_info.compute_md5_only then
985   - local f = io.open(__module_extra_info.compute_md5_only, "w")
  985 + local f = fs.open(__module_extra_info.compute_md5_only, "w")
986 986 f:write(("%s : module[%s] md5\n"):format(module_md5, mod.version_string))
987 987 f:close()
988 988 end
... ...
... ... @@ -205,6 +205,9 @@ function _M:getSubtypeOrder()
205 205 return self.subtype or ""
206 206 end
207 207
  208 +--- Describe requirements flags naming
  209 +_M.requirement_flags_names = {}
  210 +
208 211 --- Describe requirements
209 212 function _M:getRequirementDesc(who)
210 213 local req = rawget(self, "require")
... ... @@ -212,6 +215,19 @@ function _M:getRequirementDesc(who)
212 215
213 216 local str = tstring{"Requires:", true}
214 217
  218 + if req.flag then
  219 + for _, flag in ipairs(req.flag) do
  220 + if type(flag) == "table" then
  221 + local name = self.requirement_flags_names[flag[1]] or flag[1]
  222 + local c = (who:attr(flag[1]) and who:attr(flag[1]) >= flag[2]) and {"color", 0x00,0xff,0x00} or {"color", 0xff,0x00,0x00}
  223 + str:add(c, "- ", ("%s (level %d)"):format(name, flag[2]), {"color", "LAST"}, true)
  224 + else
  225 + local name = self.requirement_flags_names[flag] or flag
  226 + local c = who:attr(flag) and {"color", 0x00,0xff,0x00} or {"color", 0xff,0x00,0x00}
  227 + str:add(c, "- ", ("%s"):format(name), {"color", "LAST"}, true)
  228 + end
  229 + end
  230 + end
215 231 if req.stat then
216 232 for s, v in pairs(req.stat) do
217 233 local c = (who:getStat(s) >= v) and {"color", 0x00,0xff,0x00} or {"color", 0xff,0x00,0x00}
... ...
... ... @@ -23,6 +23,7 @@ local url = require "socket.url"
23 23 local ltn12 = require "ltn12"
24 24 local Dialog = require "engine.ui.Dialog"
25 25 local UserChat = require "engine.UserChat"
  26 +local sha1 = require("sha1").sha1
26 27 require "Json2"
27 28
28 29 --- Handles the player profile, possibly online
... ... @@ -98,6 +99,12 @@ function _M:start()
98 99 self:loadGenericProfile()
99 100
100 101 if self.generic.online and self.generic.online.login and self.generic.online.pass then
  102 + -- Convert to encrypted pass
  103 + if not self.generic.online.v2 then
  104 + self.generic.online.pass = sha1(self.generic.online.pass)
  105 + self:saveGenericProfile("online", {login=self.generic.online.login, pass=self.generic.online.pass, v2=true})
  106 + end
  107 +
101 108 self.login = self.generic.online.login
102 109 self.pass = self.generic.online.pass
103 110 self:tryAuth()
... ... @@ -130,7 +137,7 @@ function _M:mountProfile(online, module)
130 137 fs.mkdir(string.format("/profiles/%s/generic/", online and "online" or "offline"))
131 138 if module then fs.mkdir(string.format("/profiles/%s/modules/%s", online and "online" or "offline", module)) end
132 139
133   - local path = engine.homepath.."/profiles/"..(online and "online" or "offline")
  140 + local path = engine.homepath..fs.getPathSeparator().."profiles"..fs.getPathSeparator()..(online and "online" or "offline")
134 141 fs.mount(path, "/current-profile")
135 142 print("[PROFILE] mounted ", online and "online" or "offline", "on /current-profile")
136 143 fs.setWritePath(path)
... ... @@ -138,7 +145,7 @@ function _M:mountProfile(online, module)
138 145 return restore
139 146 end
140 147 function _M:umountProfile(online, pop)
141   - local path = engine.homepath.."/profiles/"..(online and "online" or "offline")
  148 + local path = engine.homepath..fs.getPathSeparator().."profiles"..fs.getPathSeparator()..(online and "online" or "offline")
142 149 fs.umount(path)
143 150 print("[PROFILE] unmounted ", online and "online" or "offline", "from /current-profile")
144 151
... ... @@ -147,9 +154,9 @@ end
147 154
148 155 -- Define the fields that are sync'ed online, and how they are sync'ed
149 156 local generic_profile_defs = {
150   - firstrun = {nosync=true, {firstrun="number"}, receive=function(data, save) save.firstrun = data.firstrun end },
151   - online = {nosync=true, {login="string:40", pass="string:40"}, receive=function(data, save) save.login = data.login save.pass = data.pass end },
152   - onlinesteam = {nosync=true, {autolog="boolean"}, receive=function(data, save) save.autolog = data.autolog end },
  157 + firstrun = {nosync=true, no_sync=true, {firstrun="number"}, receive=function(data, save) save.firstrun = data.firstrun end },
  158 + online = {nosync=true, no_sync=true, {login="string:40", pass="string:40", v2="number"}, receive=function(data, save) save.login = data.login save.pass = data.pass save.v2 = data.v2 end },
  159 + onlinesteam = {nosync=true, no_sync=true, {autolog="boolean"}, receive=function(data, save) save.autolog = data.autolog end },
153 160 modules_played = { {name="index:string:30"}, {time_played="number"}, receive=function(data, save) max_set(save, data.name, data, "time_played") end, export=function(env) for k, v in pairs(env) do add{name=k, time_played=v} end end },
154 161 modules_loaded = { {name="index:string:30"}, {nb="number"}, receive=function(data, save) max_set(save, data.name, data, "nb") end, export=function(env) for k, v in pairs(env) do add{name=k, nb=v} end end },
155 162 }
... ... @@ -425,14 +432,16 @@ function _M:checkFirstRun()
425 432 end
426 433
427 434 function _M:performlogin(login, pass)
  435 + pass = sha1(pass)
  436 +
428 437 self.login=login
429 438 self.pass=pass
430 439 print("[ONLINE PROFILE] attempting log in ", self.login)
431 440 self.auth_tried = nil
432 441 self:tryAuth()
433 442 self:waitFirstAuth()
434   - if (profile.auth) then
435   - self:saveGenericProfile("online", {login=login, pass=pass})
  443 + if profile.auth then
  444 + self:saveGenericProfile("online", {login=login, pass=pass, v2=true})
436 445 self:getConfigs("generic")
437 446 self:syncOnline("generic")
438 447 end
... ... @@ -477,6 +486,9 @@ function _M:popEvent(specific)
477 486 end
478 487
479 488 function _M:waitEvent(name, cb, wait_max)
  489 + -- Dont try as it would fail and we'd fait for nothing
  490 + if config.settings.disable_all_connectivity then return end
  491 +
480 492 -- Wait anwser, this blocks thegame but cant really be avoided :/
481 493 local stop = false
482 494 local first = true
... ... @@ -509,6 +521,9 @@ function _M:noMoreAuthWait()
509 521 end
510 522
511 523 function _M:waitFirstAuth(timeout)
  524 + -- Dont try as it would fail and we'd fait for nothing
  525 + if config.settings.disable_all_connectivity then return end
  526 +
512 527 if self.no_more_wait_auth then return end
513 528 if self.auth_tried and self.auth_tried >= 1 then return end
514 529 if not self.waiting_auth then return end
... ... @@ -564,7 +579,7 @@ function _M:eventGetNews(e)
564 579 end
565 580
566 581 function _M:eventIncrLogConsume(e)
567   - local module = game.__mod_info.short_name
  582 + local module = type(game) == "table" and game.__mod_info.short_name
568 583 if not module then return end
569 584 print("[PROFILE] Server accepted our incr log, deleting")
570 585 local pop = self:mountProfile(true, module)
... ... @@ -592,6 +607,13 @@ function _M:eventGetConfigs(e)
592 607 end
593 608
594 609 function _M:eventPushCode(e)
  610 + if not config.settings.allow_online_events then
  611 + if e.return_uuid then
  612 + core.profile.pushOrder(string.format("o='CodeReturn' uuid=%q data=%q", e.return_uuid, table.serialize{error='user disabled events, refusing to load code'}))
  613 + end
  614 + return
  615 + end
  616 +
595 617 local f, err = loadstring(e.code)
596 618 if not f then
597 619 if e.return_uuid then
... ... @@ -649,6 +671,9 @@ function _M:getNews(callback, steam)
649 671 end
650 672
651 673 function _M:tryAuth()
  674 + -- Dont try as it would fail and we'd fait for nothing
  675 + if config.settings.disable_all_connectivity then return end
  676 +
652 677 print("[ONLINE PROFILE] auth")
653 678 self.auth_last_error = nil
654 679 if self.steam_token then
... ... @@ -1019,7 +1044,8 @@ function _M:isDonator(s)
1019 1044 end
1020 1045
1021 1046 function _M:allowDLC(dlc)
1022   - if core.steam then if core.steam.checkDLC(dlc[2]) then return true end end
1023   - if self.auth and self.auth.dlcs and self.auth.dlcs[dlc[1]] then return true end
1024   - return false
  1047 + -- if core.steam then if core.steam.checkDLC(dlc[2]) then return true end end
  1048 + -- if self.auth and self.auth.dlcs and self.auth.dlcs[dlc[1]] then return true end
  1049 + -- return false
  1050 + return true
1025 1051 end
... ...
... ... @@ -253,8 +253,6 @@ Again, thank you, and enjoy Eyal!
253 253
254 254 #{italic}#Your malevolent local god of darkness, #GOLD#DarkGod#{normal}#]]):format(data.donated, data.donated * 10, data.items_vault_slots)
255 255 Dialog:simpleLongPopup("Thank you!", text, 600)
256   - elseif self.uc_ext then
257   - self.uc_ext:event(e)
258 256 end
259 257 elseif e.se == "SelfJoin" then
260 258 self:addMessage("join", e.channel, profile.auth.login, e.channel, "#{italic}#Joined channel#{normal}#", nil, true)
... ... @@ -329,6 +327,10 @@ Again, thank you, and enjoy Eyal!
329 327 self.channels_changed = true
330 328 end
331 329
  330 + if self.uc_ext then
  331 + self.uc_ext:event(e)
  332 + end
  333 +
332 334 for fct, _ in pairs(self.on_event) do
333 335 fct(e)
334 336 end
... ...
... ... @@ -769,7 +769,8 @@ function _M:addEntity(level, e, typ, x, y, no_added)
769 769 if x and y then level.map(x, y, Map.TRIGGER, e) end
770 770 end
771 771 e:check("addedToLevel", level, x, y)
772   - e:check("on_added", level, x, y)
  772 + e:check("on_added", level, x, y) -- Sustains are activated here
  773 + e:check("on_added_final", level, x, y)
773 774 end
774 775
775 776 --- If we are loaded we need a new uid
... ...
... ... @@ -131,7 +131,7 @@ function _M:onDownload(handlers)
131 131 local Dialog = require "engine.ui.Dialog"
132 132
133 133 handlers.on_download_request = function(downid, url, file, mime)
134   - if mime == "application/t-engine-addon" and self.allow_downloads.addons and url:find("^http://te4%.org/") then
  134 + if mime == "application/t-engine-addon" and self.allow_downloads.addons and url:find("^https://te4%.org/") then
135 135 local name = file
136 136 if self._next_download_name and os.time() - self._next_download_name.time <= 3 then name = self._next_download_name.name self._next_download_name = nil end
137 137 print("Accepting addon download to:", self.dest)
... ... @@ -140,7 +140,7 @@ function _M:onDownload(handlers)
140 140 game:registerDialog(self.download_dialog)
141 141 self.view:downloadAction(downid, self.dest)
142 142 return
143   - elseif mime == "application/t-engine-module" and self.allow_downloads.modules and url:find("^http://te4%.org/") then
  143 + elseif mime == "application/t-engine-module" and self.allow_downloads.modules and url:find("^https://te4%.org/") then
144 144 local name = file
145 145 if self._next_download_name and os.time() - self._next_download_name.time <= 3 then name = self._next_download_name.name self._next_download_name = nil end
146 146 print("Accepting module download to:", self.dest)
... ...
... ... @@ -49,6 +49,8 @@ end
49 49
50 50 --- place a door in a wall
51 51 function _M:doorOnWall(wall)
  52 + print("=WALLLDOOR")
  53 + table.print(wall)
52 54 if wall.vert then
53 55 local j = rng.table(table.keys(wall.ps))
54 56 self.map(wall.base, j, Map.TERRAIN, self:resolve("door"))
... ... @@ -72,11 +74,11 @@ function _M:addWall(vert, base, p1, p2)
72 74 ps[z] = nil
73 75 else
74 76 rm_map.walled = true
75   - if z == p1 or z == p2 or not game.level.map:checkEntity(x, y, Map.TERRAIN, "block_move") or 2 ~=
76   - (game.level.map:checkEntity(x - 1, y, Map.TERRAIN, "block_move") and 1 or 0) +
77   - (game.level.map:checkEntity(x + 1, y, Map.TERRAIN, "block_move") and 1 or 0) +
78   - (game.level.map:checkEntity(x, y - 1, Map.TERRAIN, "block_move") and 1 or 0) +
79   - (game.level.map:checkEntity(x, y + 1, Map.TERRAIN, "block_move") and 1 or 0) then
  77 + if z == p1 or z == p2 or not self.map:checkEntity(x, y, Map.TERRAIN, "block_move") or 2 ~=
  78 + (self.map:checkEntity(x - 1, y, Map.TERRAIN, "block_move") and 1 or 0) +
  79 + (self.map:checkEntity(x + 1, y, Map.TERRAIN, "block_move") and 1 or 0) +
  80 + (self.map:checkEntity(x, y - 1, Map.TERRAIN, "block_move") and 1 or 0) +
  81 + (self.map:checkEntity(x, y + 1, Map.TERRAIN, "block_move") and 1 or 0) then
80 82 ps[z] = nil
81 83 end
82 84 end
... ...
... ... @@ -24,7 +24,7 @@ require "engine.Generator"
24 24 --- @classmod engine.generator.map.Hexacle
25 25 module(..., package.seeall, class.inherit(engine.Generator))
26 26
27   -function _M:init(zone, map, grid_list, data)
  27 +function _M:init(zone, map, level, data)
28 28 engine.Generator.init(self, zone, map, level)
29 29 data.segment_wide_chance = data.segment_wide_chance or 70
30 30 data.nb_segments = data.nb_segments or 8
... ...
... ... @@ -24,7 +24,7 @@ require "engine.Generator"
24 24 --- @classmod engine.generator.map.Maze
25 25 module(..., package.seeall, class.inherit(engine.Generator))
26 26
27   -function _M:init(zone, map, grid_list, data)
  27 +function _M:init(zone, map, level, data)
28 28 engine.Generator.init(self, zone, map, level)
29 29 self.data = data
30 30 data.widen_w = data.widen_w or 1
... ...
... ... @@ -191,7 +191,7 @@ function _M:tmxLoad(file)
191 191 local g = self:getLoader(t)
192 192 local map = lom.parse(data)
193 193 local mapprops = {}
194   - if map:findOne("properties") then mapprops = map:findOne("properties"):findAllAttrs("property", "name", "value") end
  194 + if map:findOne("properties", nil, nil, true) then mapprops = map:findOne("properties", nil, nil, true):findAllAttrsValueOrBody("property", "name", "value") end
195 195 local w, h = tonumber(map.attr.width), tonumber(map.attr.height)
196 196 local tw, th = tonumber(map.attr.tilewidth), tonumber(map.attr.tileheight)
197 197 local chars = {}
... ...
... ... @@ -45,10 +45,14 @@ fs.mkdir(fs.getHomePath().."/4.0/")
45 45 fs.mkdir(fs.getHomePath().."/4.0/profiles/")
46 46 fs.mkdir(fs.getHomePath().."/4.0/settings/")
47 47
48   -fs.setPathAllowed(engine.homepath)
49   -fs.setPathAllowed(fs.getRealPath("/addons/"))
50   -if fs.getRealPath("/dlcs/") then fs.setPathAllowed(fs.getRealPath("/dlcs/")) end
51   -fs.setPathAllowed(fs.getRealPath("/modules/"))
  48 +fs.setPathAllowed(engine.homepath, true)
  49 +fs.setPathAllowed(fs.getRealPath("/addons/"), true)
  50 +if fs.getRealPath("/dlcs/") then fs.setPathAllowed(fs.getRealPath("/dlcs/"), true) end
  51 +fs.setPathAllowed(fs.getRealPath("/modules/"), true)
  52 +
  53 +-- Last resort, add currently mounted paths, as readonly, so taht reset mounts work
  54 +for _, path in ipairs(fs.getSearchPath()) do fs.setPathAllowed(path) end
  55 +
52 56 fs.doneSettingPathAllowed()
53 57 fs.setWritePath(engine.homepath)
54 58
... ... @@ -76,14 +80,34 @@ censor_boot = true
76 80 chat.filter = {}
77 81 chat.ignores = {}
78 82 addons = {}
  83 +allow_online_events = true
  84 +disable_all_connectivity = true
  85 +upload_charsheet = true
79 86 upgrades { v1_0_5=true }
80 87 ]]
  88 +local loaded_config_files = {}
81 89 for i, file in ipairs(fs.list("/settings/")) do
82 90 if file:find(".cfg$") then
83 91 config.load("/settings/"..file)
  92 + loaded_config_files[file] = true
84 93 end
85 94 end
86 95
  96 +-- Keep the same setting when upgrading
  97 +-- What a FRELLING MESS
  98 +if not loaded_config_files["disable_all_connectivity.cfg"] and not config.settings.firstrun_gdpr then
  99 + config.settings.disable_all_connectivity = false
  100 +end
  101 +
  102 +if config.settings.disable_all_connectivity then
  103 + core.game.disableConnectivity()
  104 + local function void(t) for _, k in ipairs(table.keys(t)) do t[k] = nil end end
  105 + -- if core.steam then void(core.steam) core.steam = nil end
  106 + if core.discord then void(core.discord) core.discord = nil end
  107 + if core.webview then void(core.webview) core.webview = nil end
  108 + if socketcore then void(socketcore) socketcore = nil end
  109 +end
  110 +
87 111 if config.settings.force_safeboot then
88 112 util.removeForceSafeBoot()
89 113 core.display.forceSafeMode()
... ...
... ... @@ -316,13 +316,15 @@ function _M:aiTalentTargets(t, aitarget, tg, all_targets, ax, ay)
316 316 friendlyfire = typ.friendlyfire and (type(typ.friendlyfire) == "number" and typ.friendlyfire or 100) or 0
317 317 if all_targets then typ.selffire, typ.friendlyfire = 100, 100 end
318 318 targets = {}
319   - self:project(typ, ax, ay, function(px, py)
320   - local tgt = game.level.map(px, py, typ.scan_on or Map.ACTOR)
321   - if tgt and not tgt.dead then
322   - if log_detail > 2 then print("[aiTalentTargets]", t.id, "may affect", px, py, "actor:", tgt.uid, tgt.name) end
323   - targets[#targets+1] = tgt
324   - end
325   - end)
  319 + if ax and ay then
  320 + self:project(typ, ax, ay, function(px, py)
  321 + local tgt = game.level.map(px, py, typ.scan_on or Map.ACTOR)
  322 + if tgt and not tgt.dead then
  323 + if log_detail > 2 then print("[aiTalentTargets]", t.id, "may affect", px, py, "actor:", tgt.uid, tgt.name) end
  324 + targets[#targets+1] = tgt
  325 + end
  326 + end)
  327 + end
326 328 end
327 329 end
328 330 return targets, selffire, friendlyfire, tg
... ...
... ... @@ -51,6 +51,7 @@ function _M:newTalentType(t)
51 51 assert(t.name, "no talent type name")
52 52 assert(t.type, "no talent type type")
53 53 t.description = t.description or ""
  54 + t.category = t.category or t.type:gsub("/.*", "")
54 55 t.points = t.points or 1
55 56 t.talents = {}
56 57 table.insert(self.talents_types_def, t)
... ... @@ -95,7 +96,8 @@ end
95 96 function _M:init(t)
96 97 self.talents = t.talents or {}
97 98 self.talents_types = t.talents_types or {}
98   - self.talents_types_mastery = self.talents_types_mastery or {}
  99 + self.talents_types_mastery = self.talents_types_mastery or {}
  100 + self.talents_mastery_bonus = self.talents_mastery_bonus or {}
99 101 self.talents_cd = self.talents_cd or {}
100 102 self.sustain_talents = self.sustain_talents or {}
101 103 self.talents_auto = self.talents_auto or {}
... ... @@ -831,7 +833,8 @@ function _M:getTalentLevel(id)
831 833 else
832 834 t = _M.talents_def[id]
833 835 end
834   - return t and (self:getTalentLevelRaw(id)) * ((self.talents_types_mastery[t.type[1]] or 0) + 1) or 0
  836 + return t and (self:getTalentLevelRaw(id)) * (self:getTalentMastery(t) or 0) or 0
  837 +
835 838 end
836 839
837 840 --- Talent type level, sum of all raw levels of talents inside
... ... @@ -997,12 +1000,13 @@ function _M:getTalentDisplayName(t)
997 1000 return t.display_name
998 1001 end
999 1002
1000   ---- Cooldown all talents by one
  1003 +--- Cooldown all talents
1001 1004 -- This should be called in your actors "act()" method
1002   -function _M:cooldownTalents()
  1005 +-- @param turns the number of turns to cooldown the talents
  1006 +function _M:cooldownTalents(turns)
1003 1007 for tid, c in pairs(self.talents_cd) do
1004 1008 self.changed = true
1005   - self.talents_cd[tid] = self.talents_cd[tid] - 1
  1009 + self.talents_cd[tid] = self.talents_cd[tid] - (turns or 1)
1006 1010 if self.talents_cd[tid] <= 0 then
1007 1011 self.talents_cd[tid] = nil
1008 1012 if self.onTalentCooledDown then self:onTalentCooledDown(tid) end
... ... @@ -1079,14 +1083,24 @@ function _M:triggerTalent(tid, name, ...)
1079 1083
1080 1084 local t = _M.talents_def[tid]
1081 1085 name = name or "trigger"
1082   - if t[name] then return t[name](self, t, ...) end
  1086 + if t[name] then
  1087 + self.__talent_running = t
  1088 + local ret = {t[name](self, t, ...)}
  1089 + self.__talent_running = nil
  1090 + return unpack(ret, 1, table.maxn(ret))
  1091 + end
1083 1092 end
1084 1093
1085 1094 --- Trigger a talent method
1086 1095 function _M:callTalent(tid, name, ...)
1087 1096 local t = _M.talents_def[tid]
1088 1097 name = name or "trigger"
1089   - if t[name] then return t[name](self, t, ...) end
  1098 + if t[name] then
  1099 + self.__talent_running = t
  1100 + local ret = {t[name](self, t, ...)}
  1101 + self.__talent_running = nil
  1102 + return unpack(ret, 1, table.maxn(ret))
  1103 + end
1090 1104 end
1091 1105
1092 1106 --- Trigger all talents matching
... ...
... ... @@ -36,6 +36,18 @@ function _M:init()
36 36
37 37 -- Allow scrolling when targetting
38 38 self.target.on_set_target = function(self, how)
  39 + if self.target and self.target_type and self.target.x and self.active and self.target_type.stop_before_target and how == "scan" then
  40 + local start_x = self.target_type.start_x or self.target_type.x or self.target_type.source_actor and self.target_type.source_actor.x or self.x
  41 + local start_y = self.target_type.start_y or self.target_type.y or self.target_type.source_actor and self.target_type.source_actor.y or self.y
  42 + local l = core.fov.line(self.target.x, self.target.y, start_x, start_y)
  43 + local lx, ly = l:step()
  44 + if lx and ly then
  45 + self.target.x = lx
  46 + self.target.y = ly
  47 + self.target.entity = game.level.map(self.target.x, self.target.y, engine.Map.ACTOR)
  48 + end
  49 + end
  50 +
39 51 if self.key ~= self.targetmode_key then return end
40 52 local dx, dy = game.level.map:moveViewSurround(self.target.x, self.target.y, 1, 1, true)
41 53 if how == "mouse" and (dx ~= 0 or dy ~= 0) then
... ...
... ... @@ -29,6 +29,7 @@ allow_late_uuid = false
29 29
30 30 --- Register the character on te4.org and return a UUID for it
31 31 function _M:getUUID()
  32 + if game.allowJSONDump and not game:allowJSONDump() then return end
32 33 if self.__te4_uuid then return self.__te4_uuid end
33 34 local uuid = profile:registerNewCharacter(game.__mod_info.short_name)
34 35 if uuid then
... ... @@ -38,6 +39,7 @@ end
38 39
39 40 --- Call this when a character is saved to upload data to te4.org
40 41 function _M:saveUUID(do_charball)
  42 + if game.allowJSONDump and not game:allowJSONDump() then return end
41 43 if game:isTainted() then return end
42 44 if not self.__te4_uuid then
43 45 -- Try to grab an UUID even after char reg
... ...
... ... @@ -222,6 +222,8 @@ A usual problem is shaders and thus should be your first target to disable.]], 7
222 222 if reboot_message then
223 223 Dialog:simpleLongPopup("Message", reboot_message, 700)
224 224 end
  225 +
  226 + self:checkBootLoginRegister()
225 227 end
226 228
227 229 function _M:grabAddons()
... ... @@ -540,39 +542,46 @@ function _M:onQuit()
540 542 end, "Quit", "Continue")
541 543 end
542 544
543   -profile_help_text = [[#LIGHT_GREEN#T-Engine4#LAST# allows you to sync your player profile with the website #LIGHT_BLUE#https://te4.org/#LAST#
  545 +profile_help_text = [[Welcome to #LIGHT_GREEN#Tales of Maj'Eyal#LAST#!
  546 +
  547 +Before you can start dying in many innovative ways we need to ask you about online play.
544 548
545   -This allows you to:
  549 +This is a #{bold}#single player game#{normal}# but it also features many online features to enhance your gameplay and connect you to the community:
546 550 * Play from several computers without having to copy unlocks and achievements.
547   -* Keep track of your modules progression, kill count, ...
548   -* Talk ingame to other fellow players
549   -* Cool statistics for each module to help sharpen your gameplay style
  551 +* Talk ingame to other fellow players, ask for advice, share your most memorable moments...
  552 +* Keep track of your kill count, deaths, most played classes...
  553 +* Cool statistics for to help sharpen your gameplay style
  554 +* Install official expansions and third-party addons directly from the game, hassle-free
  555 +* Access your purchaser / donator bonuses if you have bought the game or donated on https://te4.org/
550 556 * Help the game developers balance and refine the game
551 557
552   -You will also have a user page on https://te4.org/ where you can show off your achievements to your friends.
553   -This is all optional, you are not forced to use this feature at all, but the developers would thank you if you did as it will make balancing easier.
554   -Online profile requires an internet connection, if not available it will wait and sync when it finds one.]]
  558 +You will also have a user page on #LIGHT_BLUE#https://te4.org/#LAST# to show off to your friends.
  559 +This is all optional, you are not forced to use this feature at all, but the developer would thank you if you did as it will make balancing easier.]]
555 560
556 561 function _M:checkFirstTime()
557   - if not profile.generic.firstrun and not core.steam then
558   - profile:checkFirstRun()
559   - local text = "Thanks for downloading T-Engine/ToME.\n\n"..profile_help_text
560   - Dialog:yesnocancelLongPopup("Welcome to T-Engine", text, 600, function(ret, cancel)
561   - if cancel then return end
562   - if not ret then
563   - local dialogdef = {}
564   - dialogdef.fct = function(login) self:setPlayerLogin(login) end
565   - dialogdef.name = "login"
566   - dialogdef.justlogin = true
567   - game:registerDialog(require('mod.dialogs.ProfileLogin').new(dialogdef, game.profile_help_text))
568   - else
569   - local dialogdef = {}
570   - dialogdef.fct = function(login) self:setPlayerLogin(login) end
571   - dialogdef.name = "creation"
572   - dialogdef.justlogin = false
573   - game:registerDialog(require('mod.dialogs.ProfileLogin').new(dialogdef, game.profile_help_text))
  562 + if not profile.generic.firstrun then
  563 + local d = require("mod.dialogs.FirstRun").new(profile_help_text)
  564 + local mm = self.dialogs[#self.dialogs]
  565 + self:unregisterDialog(mm)
  566 + self.tooltip = nil
  567 + self:registerDialog(d)
  568 + end
  569 +end
  570 +
  571 +function _M:checkBootLoginRegister()
  572 + if __module_extra_info.boot_and_register then
  573 + if core.steam then
  574 + local mm = self.dialogs[#self.dialogs]
  575 + if mm and mm:isClassName("mod.dialogs.MainMenu") then
  576 + mm:loginSteam()
574 577 end
575   - end, "Register new account", "Log in existing account", "Maybe later")
  578 + else
  579 + local dialogdef = {}
  580 + dialogdef.fct = function(login) self:setPlayerLogin(login) end
  581 + dialogdef.name = "creation"
  582 + dialogdef.justlogin = false
  583 + game:registerDialog(require('mod.dialogs.ProfileLogin').new(dialogdef, game.profile_help_text))
  584 + end
576 585 end
577 586 end
578 587
... ...
... ... @@ -84,6 +84,7 @@ function _M:init()
84 84
85 85 self.c_list = List.new{width=self.iw, nb_items=#self.list, list=self.list, fct=function(item) end, font={FontPackage:getFont("default")}}
86 86
  87 + self.c_discord = ButtonImage.new{no_decoration=true, alpha_unfocus=0.5, file="discord.png", fct=function() util.browserOpenUrl("https://discord.gg/tales-of-majeyal", {is_external=true}) end}
87 88 self.c_facebook = ButtonImage.new{no_decoration=true, alpha_unfocus=0.5, file="facebook.png", fct=function() util.browserOpenUrl("https://www.facebook.com/tales.of.maj.eyal", {is_external=true}) end}
88 89 self.c_twitter = ButtonImage.new{no_decoration=true, alpha_unfocus=0.5, file="twitter.png", fct=function() util.browserOpenUrl("https://twitter.com/TalesOfMajEyal", {is_external=true}) end}
89 90 self.c_forums = ButtonImage.new{no_decoration=true, alpha_unfocus=0.5, file="forums.png", fct=function() util.browserOpenUrl("http://forums.te4.org/", {is_external=true}) end}
... ... @@ -92,6 +93,7 @@ function _M:init()
92 93 {left=0, top=0, ui=self.c_list},
93 94 {left=0, bottom=0, absolute=true, ui=self.c_background},
94 95 {right=self.c_facebook.w, bottom=0, absolute=true, ui=self.c_version},
  96 + {right=0, bottom=self.c_facebook.h+self.c_twitter.h+self.c_forums.h, absolute=true, ui=self.c_discord},
95 97 {right=0, bottom=self.c_facebook.h+self.c_twitter.h, absolute=true, ui=self.c_forums},
96 98 {right=0, bottom=self.c_twitter.h, absolute=true, ui=self.c_facebook},
97 99 {right=0, bottom=0, absolute=true, ui=self.c_twitter},
... ... @@ -121,10 +123,12 @@ end
121 123 function _M:updateUI()
122 124 local uis = table.clone(self.base_uis)
123 125
124   - if profile.auth then
125   - self:uiStats(uis)
126   - else
127   - self:uiLogin(uis)
  126 + if not config.settings.disable_all_connectivity then
  127 + if profile.auth then
  128 + self:uiStats(uis)
  129 + else
  130 + self:uiLogin(uis)
  131 + end
128 132 end
129 133
130 134 self:loadUI(uis)
... ...
... ... @@ -27,7 +27,7 @@ local Textzone = require "engine.ui.Textzone"
27 27 module(..., package.seeall, class.inherit(Dialog))
28 28
29 29 function _M:init(dialogdef, profile_help_text)
30   - Dialog.init(self, "Online profile "..dialogdef.name, 600, 400)
  30 + Dialog.init(self, "Online profile "..dialogdef.name, math.min(800, game.w * 0.9), 400)
31 31 self.profile_help_text = profile_help_text
32 32 self.dialogdef = dialogdef
33 33 self.alpha = 230
... ... @@ -65,9 +65,10 @@ function _M:init(dialogdef, profile_help_text)
65 65 self.c_pass = Textbox.new{title="Password: ", size_title=pwa, text="", chars=30, max_len=20, hide=true, filter=pass_filter, fct=function(text) self:okclick() end}
66 66 self.c_pass2 = Textbox.new{title=pwa, text="", size_title=pwa, chars=30, max_len=20, hide=true, filter=pass_filter, fct=function(text) self:okclick() end}
67 67 self.c_email = Textbox.new{title="Email: ", size_title=pwa, text="", chars=30, max_len=60, filter=pass_filter, fct=function(text) self:okclick() end}
68   - self.c_news = Checkbox.new{title="Accept to receive #{bold}#very infrequent#{normal}# (a few per year) mails", default=true, fct=function() self:okclick() end}
69   - self.c_news2 = Textzone.new{text="about important game events from us.", width=self.iw - 20, auto_height=true}
  68 + self.c_news = Checkbox.new{title="Accept to receive #{bold}#very infrequent#{normal}# (a few per year) mails about important game events from us.", default=false, fct=function() self:okclick() end}
  69 + self.c_age = Checkbox.new{title="You at least 16 years old, or have parental authorization to play the game.", default=false, fct=function() self:okclick() end}
70 70 local ok = require("engine.ui.Button").new{text="Create", fct=function() self:okclick() end}
  71 + local privacy = require("engine.ui.Button").new{text="Privacy Policy (opens in browser)", fct=function() self:privacypolicy() end}
71 72 local cancel = require("engine.ui.Button").new{text="Cancel", fct=function() self:cancelclick() end}
72 73
73 74 self:loadUI{
... ... @@ -77,9 +78,10 @@ function _M:init(dialogdef, profile_help_text)
77 78 {left=0, top=self.c_desc.h+self.c_login.h+self.c_pass.h+5, ui=self.c_pass2},
78 79 {left=0, top=self.c_desc.h+self.c_login.h+self.c_pass.h+self.c_pass2.h+10, ui=self.c_email},
79 80 {left=0, top=self.c_desc.h+self.c_login.h+self.c_pass.h+self.c_pass2.h+self.c_email.h+10, ui=self.c_news},
80   - {left=0, top=self.c_desc.h+self.c_login.h+self.c_pass.h+self.c_pass2.h+self.c_email.h+self.c_news2.h+10, ui=self.c_news2},
  81 + {left=0, top=self.c_desc.h+self.c_login.h+self.c_pass.h+self.c_pass2.h+self.c_email.h+self.c_news.h+10, ui=self.c_age},
81 82 {left=0, bottom=0, ui=ok},
82 83 {right=0, bottom=0, ui=cancel},
  84 + {hcenter=0, bottom=0, ui=privacy},
83 85 }
84 86 self:setFocus(self.c_login)
85 87 end
... ... @@ -108,6 +110,10 @@ function _M:okclick()
108 110 self:simplePopup("Email", "Your email seems invalid")
109 111 return
110 112 end
  113 + if not self.c_age.checked then
  114 + self:simplePopup("Age Check", "You need to be 16 years old or more or to have parental authorization to play this game.")
  115 + return
  116 + end
111 117
112 118 game:unregisterDialog(self)
113 119 game:createProfile({create=self.c_email and true or false, login=self.c_login.text, pass=self.c_pass.text, email=self.c_email and self.c_email.text, news=self.c_news and self.c_news.checked})
... ... @@ -116,3 +122,7 @@ end
116 122 function _M:cancelclick()
117 123 self.key:triggerVirtual("EXIT")
118 124 end
  125 +
  126 +function _M:privacypolicy()
  127 + util.browserOpenUrl("https://te4.org/privacy-policy-data", {is_external=true})
  128 +end
... ...
... ... @@ -27,7 +27,7 @@ local Textzone = require "engine.ui.Textzone"
27 27 module(..., package.seeall, class.inherit(Dialog))
28 28
29 29 function _M:init()
30   - Dialog.init(self, "Steam User Account", 500, 400)
  30 + Dialog.init(self, "Steam User Account", math.min(800, game.w * 0.9), 400)
31 31 self.alpha = 230
32 32
33 33 self.c_desc = Textzone.new{width=math.floor(self.iw - 10), auto_height=true, text=[[Welcome to #GOLD#Tales of Maj'Eyal#LAST#.
... ... @@ -43,18 +43,20 @@ Luckily this is very easy to do: you only require a profile name and optionally
43 43
44 44 self.c_login = Textbox.new{title="Username: ", text="", chars=30, max_len=20, fct=function(text) self:okclick() end}
45 45 self.c_email = Textbox.new{title="Email: ", size_title=self.c_login.title, text="", chars=30, max_len=60, fct=function(text) self:okclick() end}
46   - self.c_news = Checkbox.new{title="Accept to receive #{bold}#very infrequent#{normal}# (a few per year) mails", default=true, fct=function() self:okclick() end}
47   - self.c_news2 = Textzone.new{text="about important game events from us.", width=self.iw - 20, auto_height=true}
  46 + self.c_news = Checkbox.new{title="Accept to receive #{bold}#very infrequent#{normal}# (a few per year) mails about important game events from us.", default=false, fct=function() self:okclick() end}
  47 + self.c_age = Checkbox.new{title="You at least 16 years old, or have parental authorization to play the game.", default=false, fct=function() self:okclick() end}
48 48 local ok = require("engine.ui.Button").new{text="Register", fct=function() self:okclick() end}
49 49 local cancel = require("engine.ui.Button").new{text="Cancel", fct=function() self:cancelclick() end}
  50 + local privacy = require("engine.ui.Button").new{text="Privacy Policy (opens in browser)", fct=function() self:privacypolicy() end}
50 51 self:loadUI{
51 52 {left=0, top=0, ui=self.c_desc},
52 53 {left=0, top=self.c_desc.h, ui=self.c_login},
53 54 {left=0, top=self.c_desc.h+self.c_login.h+5, ui=self.c_email},
54 55 {left=0, top=self.c_desc.h+self.c_login.h+self.c_email.h+5, ui=self.c_news},
55   - {left=0, top=self.c_desc.h+self.c_login.h+self.c_email.h+self.c_news.h+5, ui=self.c_news2},
  56 + {left=0, top=self.c_desc.h+self.c_login.h+self.c_email.h+self.c_news.h+5, ui=self.c_age},
56 57 {left=0, bottom=0, ui=ok},
57 58 {right=0, bottom=0, ui=cancel},
  59 + {hcenter=0, bottom=0, ui=privacy},
58 60 }
59 61 self:setFocus(self.c_login)
60 62 self:setupUI(true, true)
... ... @@ -74,7 +76,11 @@ function _M:okclick()
74 76 self:simplePopup("Email", "Your email does not look right.")
75 77 return
76 78 end
77   -
  79 + if not self.c_age.checked then
  80 + self:simplePopup("Age Check", "You need to be 16 years old or more or to have parental authorization to play this game.")
  81 + return
  82 + end
  83 +
78 84 local d = self:simpleWaiter("Registering...", "Registering on https://te4.org/, please wait...") core.display.forceRedraw()
79 85 d:timeout(30, function() Dialog:simplePopup("Steam", "Steam client not found.") end)
80 86 core.steam.sessionTicket(function(ticket)
... ... @@ -99,3 +105,7 @@ end
99 105 function _M:cancelclick()
100 106 self.key:triggerVirtual("EXIT")
101 107 end
  108 +
  109 +function _M:privacypolicy()
  110 + util.browserOpenUrl("https://te4.org/privacy-policy-data", {is_external=true})
  111 +end
... ...
... ... @@ -129,6 +129,9 @@ _M.temporary_values_conf.force_melee_damtype = "last"
129 129 -- AI
130 130 _M.temporary_values_conf.ai_move = "last"
131 131
  132 +-- Misc
  133 +_M.temporary_values_conf.death_dialog = "last"
  134 +
132 135 _M.projectile_class = "mod.class.Projectile"
133 136
134 137 function _M:init(t, no_default)
... ... @@ -600,17 +603,53 @@ function _M:actBase()
600 603 end
601 604
602 605 -- Cooldown talents after effects, because some of them involve breaking sustains.
603   - if not self:attr("no_talents_cooldown") then self:cooldownTalents() end
  606 +
  607 + if not self:attr("no_talents_cooldown") then
  608 + if self:attr("half_talents_cooldown") then self:cooldownTalents(0.5) else self:cooldownTalents(1) end
  609 + end
604 610
605 611 self:checkStillInCombat()
606 612 end
607 613
  614 +function _M:hasProc(proc)
  615 + if not self.turn_procs then return end
  616 + if self.turn_procs[proc] then
  617 + return self.turn_procs[proc]
  618 + elseif self.turn_procs.multi then
  619 + return self.turn_procs.multi[proc]
  620 + end
  621 +end
  622 +
  623 +function _M:setProc(name, val, turns)
  624 + turns = turns or 1
  625 + val = val or true
  626 + local proc = {val = val, turns = turns}
  627 + if turns > 1 then
  628 + table.set(self, "turn_procs", "multi", name, proc)
  629 + else
  630 + self.turn_procs[name] = proc
  631 + end
  632 +end
  633 +
608 634 -- General entry point for Actors to act, called by NPC:act or Player:act
609 635 function _M:act()
610 636 if not engine.Actor.act(self) then return end
611 637
612 638 self.changed = true
  639 +
  640 + -- Store procs with more than 1 turn remaining and re-add them after we clear turn_procs
  641 + local temp = {}
  642 + if self.turn_procs.multi then
  643 + for proc, val in pairs(self.turn_procs.multi) do
  644 + if proc then
  645 + self.turn_procs.multi[proc].turns = self.turn_procs.multi[proc].turns - 1
  646 + if self.turn_procs.multi[proc].turns > 0 then temp[proc] = val end
  647 + end
  648 + end
  649 + end
  650 +
613 651 self.turn_procs = {}
  652 + if temp then self.turn_procs.multi = temp end
614 653
615 654 -- Break some sustains if certain resources are too low
616 655 -- Note: force_talent_ignore_ressources has no effect here
... ... @@ -1312,7 +1351,7 @@ function _M:move(x, y, force)
1312 1351
1313 1352 -- Confused ?
1314 1353 if not force and self:attr("confused") then
1315   - if rng.percent(self:attr("confused")) then
  1354 + if rng.percent(util.bound(self:attr("confused"), 0, 50)) then
1316 1355 x, y = self.x + rng.range(-1, 1), self.y + rng.range(-1, 1)
1317 1356 end
1318 1357 end
... ... @@ -1406,7 +1445,7 @@ function _M:move(x, y, force)
1406 1445 end
1407 1446
1408 1447 -- Break channels
1409   - if moved then
  1448 + if not force and moved and ox and oy and (ox ~= self.x or oy ~= self.y) then
1410 1449 self:breakPsionicChannel()
1411 1450 self:breakSpacetimeTuning()
1412 1451 end
... ... @@ -1593,7 +1632,7 @@ function _M:teleportRandom(x, y, dist, min_dist)
1593 1632
1594 1633 -- after moving
1595 1634 if self:attr("defense_on_teleport") or self:attr("resist_all_on_teleport") or self:attr("effect_reduction_on_teleport") then
1596   - self:setEffect(self.EFF_OUT_OF_PHASE, 5, {defense=self:attr("defense_on_teleport") or 0, resists=self:attr("resist_all_on_teleport") or 0, effect_reduction=self:attr("effect_reduction_on_teleport") or 0})
  1635 + self:setEffect(self.EFF_OUT_OF_PHASE, 5, {})
1597 1636 end
1598 1637 end
1599 1638
... ... @@ -1860,7 +1899,8 @@ function _M:tooltip(x, y, seen_by)
1860 1899 if self.type == "humanoid" or self.type == "giant" then ts:add({"font","italic"}, "(", self.female and "female" or "male", ")", {"font","normal"}, true) else ts:add(true) end
1861 1900 ts:add(self.type:capitalize(), " / ", self.subtype:capitalize(), true)
1862 1901 ts:add("Rank: ") ts:merge(rank_color:toTString()) ts:add(rank, {"color", "WHITE"}, true)
1863   - ts:add({"color", 0, 255, 255}, ("Level: %d"):format(self.level), {"color", "WHITE"}, true)
  1902 + if self.hide_level_tooltip then ts:add({"color", 0, 255, 255}, "Level: unknown", {"color", "WHITE"}, true)
  1903 + else ts:add({"color", 0, 255, 255}, ("Level: %d"):format(self.level), {"color", "WHITE"}, true) end
1864 1904 if self:attr("invulnerable") then ts:add({"color", "PURPLE"}, "INVULNERABLE!", true) end
1865 1905 ts:add({"color", 255, 0, 0}, ("HP: %d (%d%%)"):format(self.life, self.life * 100 / self.max_life), {"color", "WHITE"})
1866 1906
... ... @@ -2018,7 +2058,11 @@ function _M:tooltip(x, y, seen_by)
2018 2058 if e.type == "physical" then
2019 2059 effphysical:add(true, "- ", {"color", "LIGHT_RED"}, desceffect(e, p, dur), {"color", "WHITE"} )
2020 2060 elseif e.type == "magical" then
2021   - effmagical:add(true, "- ", {"color", "DARK_ORCHID"}, desceffect(e, p, dur), {"color", "WHITE"} )
  2061 + if e.subtype and e.subtype.disease then
  2062 + effmagical:add(true, "- ", {"color", "DARK_GREEN"}, desceffect(e, p, dur), {"color", "WHITE"} )
  2063 + else
  2064 + effmagical:add(true, "- ", {"color", "DARK_ORCHID"}, desceffect(e, p, dur), {"color", "WHITE"} )
  2065 + end
2022 2066 elseif e.type == "mental" then
2023 2067 effmental:add(true, "- ", {"color", "YELLOW"}, desceffect(e, p, dur), {"color", "WHITE"} )
2024 2068 elseif e.type == "other" then
... ... @@ -2386,14 +2430,6 @@ function _M:onTakeHit(value, src, death_note)
2386 2430 end
2387 2431 end
2388 2432
2389   - if value > 0 and self:isTalentActive(self.T_BONE_SHIELD) then
2390   - local t = self:getTalentFromId(self.T_BONE_SHIELD)
2391   - if t.absorb(self, t, self:isTalentActive(self.T_BONE_SHIELD)) then
2392   - game:delayedLogDamage(src, self, 0, ("#SLATE#(%d to bones)#LAST#"):format(value), false)
2393   - value = 0
2394   - end
2395   - end
2396   -
2397 2433 if value <=0 then return 0 end
2398 2434 if self.knowTalent and (self:knowTalent(self.T_SEETHE) or self:knowTalent(self.T_GRIM_RESOLVE)) then
2399 2435 if not self:hasEffect(self.EFF_CURSED_FORM) then
... ... @@ -2583,7 +2619,7 @@ function _M:onTakeHit(value, src, death_note)
2583 2619 end
2584 2620
2585 2621 -- Bloodlust!
2586   - if value > 0 and src and src.knowTalent and src:knowTalent(src.T_BLOODLUST) then
  2622 + if value > 0 and src and not (src == self) and src.knowTalent and src:knowTalent(src.T_BLOODLUST) then
2587 2623 src:setEffect(src.EFF_BLOODLUST, 1, {})
2588 2624 end
2589 2625
... ... @@ -2722,7 +2758,7 @@ function _M:onTakeHit(value, src, death_note)
2722 2758 local vt = self:getTalentFromId(self.T_LEECH)
2723 2759 self:incVim(vt.getVim(self, vt))
2724 2760 self:heal(vt.getHeal(self, vt), src)
2725   - if self.player then src:logCombat(src, "#AQUAMARINE#You leech a part of #Target#'s vim.") end
  2761 + --if self.player then src:logCombat(src, "#AQUAMARINE#You leech a part of #Target#'s vim.") end
2726 2762 end
2727 2763
2728 2764 -- Invisible on hit
... ... @@ -2860,7 +2896,7 @@ end
2860 2896
2861 2897 function _M:takeHit(value, src, death_note)
2862 2898 self:enterCombatStatus(src)
2863   - if src.enterCombatStatus then src:enterCombatStatus(self) end
  2899 + if src and src.enterCombatStatus then src:enterCombatStatus(self) end
2864 2900
2865 2901 for eid, p in pairs(self.tmp) do
2866 2902 local e = self.tempeffect_def[eid]
... ... @@ -2879,15 +2915,6 @@ function _M:takeHit(value, src, death_note)
2879 2915
2880 2916 if src and src.fireTalentCheck then src:fireTalentCheck("callbackOnDealDamage", val, self, dead, death_note) end
2881 2917
2882   - if dead and src and src.attr and src:attr("overkill") and src.project and not src.turn_procs.overkill then
2883   - src.turn_procs.overkill = true
2884   - local dam = (self.die_at - self.life) * src:attr("overkill") / 100
2885   - local incdam = self.inc_damage
2886   - self.inc_damage = {}
2887   - src:project({type="ball", radius=2, selffire=false, x=self.x, y=self.y}, self.x, self.y, DamageType.BLIGHT, dam, {type="acid"})
2888   - self.inc_damage = incdam
2889   - end
2890   -
2891 2918 return dead, val
2892 2919 end
2893 2920
... ... @@ -3292,25 +3319,391 @@ function _M:die(src, death_note)
3292 3319 return true
3293 3320 end
3294 3321
3295   -function _M:learnStats(statorder)
3296   - self.auto_stat_cnt = self.auto_stat_cnt or 1
  3322 +--- Learn stats (stat increment) in a specified order up to a maximum
  3323 +-- @param[table] statorder: an orded list of stat ids in which to learn, generally all learned in one character level
  3324 +-- @param repeats: maximum number of times to apply the statorder <1>
  3325 +function _M:learnStats(statorder, repeats)
  3326 + statorder.idx = statorder.idx or 1
  3327 + repeats = (repeats or 1)*#statorder
3297 3328 local nb = 0
3298 3329 local max = 60
3299 3330
3300   - -- Allow to go over a natural 60, up to 80 at level 50
  3331 + -- Allow stats to go over a natural 60, up to 80 at level 50
3301 3332 if not self.no_auto_high_stats then max = 60 + (self.level * 20 / 50) end
3302 3333
3303   - while self.unused_stats > 0 do
3304   - if self:getStat(statorder[self.auto_stat_cnt]) < max then
3305   - self:incIncStat(statorder[self.auto_stat_cnt], 1)
  3334 + while self.unused_stats > 0 and nb < repeats do
  3335 + if self:getStat(statorder[statorder.idx]) < max then
  3336 + self:incIncStat(statorder[statorder.idx], 1)
3306 3337 self.unused_stats = self.unused_stats - 1
3307 3338 end
3308   - self.auto_stat_cnt = util.boundWrap(self.auto_stat_cnt + 1, 1, #statorder)
  3339 + statorder.idx = util.boundWrap(statorder.idx + 1, 1, #statorder)
3309 3340 nb = nb + 1
3310   - if nb >= #statorder then break end
3311 3341 end
3312 3342 end
3313 3343
  3344 +--- Actor learns/advances in a character class, randomly gaining stats and learning talents based on levels in the class
  3345 +-- When first called, the actor gains starting stat bonuses and talents and learns talent categories according to the birth class descriptor
  3346 +-- Note: this does not handle any special class requirements, such as creating a golem, generating equipment, etc.
  3347 +-- @see game.state:applyRandomClass
  3348 +-- @param c_data[table] contains info on the class to learn/levelup in:
  3349 +-- @field class: name (birth descriptor subclass name) of the character class to learn ("Bulwark", "Solipsist", ...)
  3350 +-- @field start_level: starting actor level to begin leveling in the class <1>
  3351 +-- @field level_rate: rate levels in character class are gained as % of actor level <100>
  3352 +-- @field check_talents_level: set true to enforce character level limits on talent levels (i.e. level 5 at level 50) <nil>
  3353 +-- @field level_by_class: set true to use class level rather than actor level when checking talent/stat limits <nil>
  3354 +-- @foe;d ignore_special: set true to skip checking talent special requirements
  3355 +-- @field auto_sustain: set true to automatically turn on sustained talents learned <nil>
  3356 +-- @field tt_focus: talent type focus, higher values cause talent selections to be focused within fewer talent types <3>
  3357 +-- @field use_actor_points: set true to apply stat/talent points from base actor levels in addtion to class levels <nil>
  3358 +-- The following fields are automatically generated or updated from the class birth descriptor:
  3359 +-- @field ttypes[table]: talent trees to learn talents from (updated with birth descriptor)
  3360 +-- {talent_type_name = {[1]=known, [2]=mastery_add}, ...}
  3361 +-- @field auto_stats: ordered list of stat ids to use when applying unused_stats points
  3362 +-- generated from class descriptor by default, set false to disable, see Actor:learnStats
  3363 +-- Additional talent inputs for each talent definition t:
  3364 +-- t.random_boss_rarity: if defined, the percent chance the talent may be learned each time randomly selected
  3365 +function _M:levelupClass(c_data)
  3366 + c_data.last_level = c_data.last_level or 0
  3367 + c_data.start_level = c_data.start_level or 1
  3368 +
  3369 + local new_level = math.ceil((self.level - c_data.start_level + 1)*(c_data.level_rate or 100)/100)
  3370 +
  3371 + if new_level <= c_data.last_level then return end
  3372 + print("[Actor:levelupClass]", self.name, "auto level up", c_data.class, c_data.last_level, "-->", new_level, c_data)
  3373 +
  3374 + local ttypes
  3375 + -- temporarily remove any previous stat/talent points if they won't be used
  3376 + local base_points = {self.unused_stats, self.unused_talents, self.unused_generics}
  3377 + if not c_data.use_actor_points then
  3378 + self.unused_stats, self.unused_talents, self.unused_generics = 0, 0, 0
  3379 + end
  3380 +
  3381 + -- Initialize if needed, updating auto_classes table
  3382 + if c_data.class and not c_data.initialized then -- build talent category list from the class
  3383 + local Birther = require "engine.Birther"
  3384 + local c_def = Birther.birth_descriptor_def.subclass[c_data.class]
  3385 + if not c_def then
  3386 + print("[Actor:levelupClass] ### undefined class:", c_data.class)
  3387 + return
  3388 + end
  3389 +
  3390 + local mclasses = Birther.birth_descriptor_def.class
  3391 + local mclass = nil
  3392 + for name, data in pairs(mclasses) do
  3393 + if data.descriptor_choices and data.descriptor_choices.subclass and data.descriptor_choices.subclass[c_def.name] then mclass = data break end
  3394 + end
  3395 + if not mclass then
  3396 + print("[Actor:levelupClass] ###class", c_data.class, "has no parent class type###")
  3397 + return
  3398 + end
  3399 +
  3400 + print(("[Actor:levelupClass] %s %s ## Initialzing auto_class %s (%s) %s%% level_rate from level %s ##"):format(self.uid, self.name, c_data.class, mclass.name, c_data.level_rate, c_data.start_level))
  3401 +
  3402 + -- update class descriptor list and build inherent power sources
  3403 + self.descriptor = self.descriptor or {}
  3404 + self.descriptor.classes = self.descriptor.classes or {}
  3405 + table.append(self.descriptor.classes, {c_def.name})
  3406 +
  3407 + -- build inherent power sources and forbidden power sources
  3408 + -- self.forbid_power_source --> self.not_power_source used for classes
  3409 + self.power_source = table.merge(self.power_source or {}, c_def.power_source or {})
  3410 + self.not_power_source = table.merge(self.not_power_source or {}, c_def.not_power_source or {})
  3411 + -- update power source parameters with the new class
  3412 + self.not_power_source, self.power_source = game.state:updatePowers(game.state:attrPowers(self, self.not_power_source), self.power_source)
  3413 + print(" *** power types: not_power_source =", table.concat(table.keys(self.not_power_source),","), "power_source =", table.concat(table.keys(self.power_source),","))
  3414 +
  3415 + -- apply class stat bonuses and set up class auto_stats (in auto_classes)
  3416 + local auto_stats
  3417 + if c_def.stats and c_data.auto_stats ~= false then
  3418 + auto_stats = {}
  3419 + for stat, v in pairs(c_def.stats or {}) do
  3420 + local stat_id = self.stats_def[stat].id
  3421 + self.stats[stat_id] = (self.stats[stat_id] or 10) + v
  3422 + for i = 1, v do auto_stats[#auto_stats+1] = stat_id end
  3423 + end
  3424 + c_data.auto_stats = auto_stats
  3425 + end
  3426 + c_data.auto_stats = c_data.auto_stats or auto_stats
  3427 +
  3428 + ttypes = {}
  3429 + for tt, d in pairs(mclass.talents_types or {}) do
  3430 + self:learnTalentType(tt, d[1]) self:setTalentTypeMastery(tt, (self:getTalentTypeMastery(tt) or 1) + d[2])
  3431 + ttypes[tt] = table.clone(d)
  3432 + end
  3433 + for tt, d in pairs(mclass.unlockable_talents_types or {}) do
  3434 + self:learnTalentType(tt, d[1]) self:setTalentTypeMastery(tt, (self:getTalentTypeMastery(tt) or 1) + d[2])
  3435 + ttypes[tt] = table.clone(d)
  3436 + end
  3437 + for tt, d in pairs(c_def.talents_types or {}) do
  3438 + self:learnTalentType(tt, d[1]) self:setTalentTypeMastery(tt, (self:getTalentTypeMastery(tt) or 1) + d[2])
  3439 + ttypes[tt] = table.clone(d)
  3440 + end
  3441 + for tt, d in pairs(c_def.unlockable_talents_types or {}) do
  3442 + self:learnTalentType(tt, d[1]) self:setTalentTypeMastery(tt, (self:getTalentTypeMastery(tt) or 1) + d[2])
  3443 + ttypes[tt] = table.clone(d)
  3444 + end
  3445 +
  3446 + -- set up input talent categories specified (generally for non-class talents)
  3447 + if c_data.ttypes then
  3448 + for tt, d in pairs(c_data.ttypes) do
  3449 + if not self:knowTalentType(tt) then
  3450 + if type(d) ~= "table" then
  3451 + d={true, type(d) == "number" and d or rng.range(1, 3)*0.1}
  3452 + else d=table.clone(d)
  3453 + end
  3454 + self:learnTalentType(tt, d[1])
  3455 + self:setTalentTypeMastery(tt, (self:getTalentTypeMastery(tt) or 1) + d[2])
  3456 + ttypes[tt] = table.mergeAdd(ttypes[tt] or {}, d)
  3457 + end
  3458 + end
  3459 + end
  3460 + if not next(ttypes) then -- if not specified, use all known talent types
  3461 + for tt, known in pairs(self.talents_types) do
  3462 + ttypes[tt] = {known, (self:getTalentTypeMastery(tt) or 1) - 1}
  3463 + end
  3464 + end
  3465 +
  3466 + -- Note: could limit number of talent trees selected here to limit # talents learned
  3467 +
  3468 +--print("\t *** auto_levelup initialized talent category choices:", mclass.name , c_def.name , "\n\t")
  3469 + -- set up (semi-random) rarity levels for talent categories
  3470 + -- This tends to focus learned talents within certain trees (usually those with improved mastery)
  3471 + local tt_count = 0
  3472 + local unknown_tt={}
  3473 + local tt_focus = c_data.tt_focus or 3
  3474 + for tt, d in pairs(ttypes) do
  3475 + d.tt = tt
  3476 + tt_count = tt_count + 1
  3477 + d.tt_count = tt_count
  3478 + d.rarity = d.rarity or (1.3 + 0.5*tt_count)/math.max(0.1, self:getTalentTypeMastery(tt)*rng.float(0.1, tt_focus))^2
  3479 +--print(("\t *** %-40s rarity %5.3f"):format(tt, d.rarity))
  3480 + if not d[1] then table.insert(unknown_tt, d) end
  3481 + end
  3482 + c_data.unknown_tt = unknown_tt