diff --git a/game/engines/default/engine/Game.lua b/game/engines/default/engine/Game.lua index 18b0bc25780e67fc90d111c67d59bfd89a626a4a..b74966e81735b0be8dfd51a162950d8b0a058810 100644 --- a/game/engines/default/engine/Game.lua +++ b/game/engines/default/engine/Game.lua @@ -54,6 +54,7 @@ function _M:loaded() self.mouse:setCurrent() self.__threads = self.__threads or {} + self.__coroutines = self.__coroutines or {} end --- Defines the default fields to be saved by the savefile code @@ -113,6 +114,25 @@ end --- This is the "main game loop", do something here function _M:tick() + local stop = {} + local id, co = next(self.__coroutines) + while id do + local ok, err = coroutine.resume(co) + if not ok then + print(debug.traceback(co)) + print("[COROUTINE] error", err) + end + if coroutine.status(co) == "dead" then + stop[#stop+1] = id + end + id, co = next(self.__coroutines, id) + end + if #stop > 0 then + for i = 1, #stop do + self.__coroutines[stop[i]] = nil + print("[COROUTINE] dead", stop[i]) + end + end end --- Called when a zone leaves a level @@ -213,6 +233,35 @@ end function _M:saveGame() end +--- Add a coroutine to the pool +-- Coroutines registered will be run each game tick +function _M:registerCoroutine(id, co) + print("[COROUTINE] registering", id, co) + self.__coroutines[id] = co +end + +--- Get the coroutine corresponding to the id +function _M:getCoroutine(id) + return self.__coroutines[id] +end + +--- Ask a registered coroutine to cancel +-- The coroutine must accept a "cancel" action +function _M:cancelCoroutine(id) + local co = self.__coroutines[id] + if not co then return end + local ok, err = coroutine.resume(co, "cancel") + if not ok then + print(debug.traceback(co)) + print("[COROUTINE] error", err) + end + if coroutine.status(co) == "dead" then + self.__coroutines[id] = nil + else + error("Told coroutine "..id.." to cancel, but it is not dead!") + end +end + --- Save a thread into the thread pool -- Threads will be auto join'ed when the module exits or when it can<br/> -- ALL THREADS registered *MUST* return true when they exit diff --git a/game/engines/default/engine/GameEnergyBased.lua b/game/engines/default/engine/GameEnergyBased.lua index ce6d2622e4073f74c27d97cc3ef5cc46b139e52c..00ff9ec67d2d67dd029f71ac5fef8b5c41c635f5 100644 --- a/game/engines/default/engine/GameEnergyBased.lua +++ b/game/engines/default/engine/GameEnergyBased.lua @@ -63,7 +63,7 @@ function _M:tick() for i = 1, #arr do e = arr[i] if e and e.act and e.energy then --- print("<ENERGY", e.name, e.uid, "::", e.energy.value, game.paused, "::", e.player) +-- print("<ENERGY", e.name, e.uid, "::", e.energy.value, self.paused, "::", e.player) if e.energy.value < self.energy_to_act then e.energy.value = (e.energy.value or 0) + self.energy_per_tick * (e.energy.mod or 1) end @@ -71,7 +71,7 @@ function _M:tick() e.energy.used = false e:act(self) end --- print(">ENERGY", e.name, e.uid, "::", e.energy.value, game.paused, "::", e.player) +-- print(">ENERGY", e.name, e.uid, "::", e.energy.value, self.paused, "::", e.player) end end end diff --git a/game/engines/default/engine/GameTurnBased.lua b/game/engines/default/engine/GameTurnBased.lua index 27c30e187e5404e49a974d709eb191aa99bd3afd..8fecc3b36e8c4b029d9216d254254f541a502c23 100644 --- a/game/engines/default/engine/GameTurnBased.lua +++ b/game/engines/default/engine/GameTurnBased.lua @@ -37,7 +37,7 @@ end function _M:tick() if self.paused then -- Auto unpause if the player has no energy to act - if game:getPlayer() and not game:getPlayer():enoughEnergy() then game.paused = false end + if self:getPlayer() and not self:getPlayer():enoughEnergy() then self.paused = false end -- If we are paused do not get energy, but still process frames if needed engine.Game.tick(self) diff --git a/game/engines/default/engine/Savefile.lua b/game/engines/default/engine/Savefile.lua index bcb5e956cb336dca3360644a7c0102857a22c59d..1ec4b3ecc9ce24aeabf13806d9f1cab8b1e71493 100644 --- a/game/engines/default/engine/Savefile.lua +++ b/game/engines/default/engine/Savefile.lua @@ -92,25 +92,36 @@ function _M:saveObject(obj, zip) local tbl = table.remove(self.process) self.tables[tbl] = self:getFileName(tbl) zip:add(self:getFileName(tbl), tbl:save()) + -- If run from a coroutine, we pause every object + if coroutine.running() then + local coret = coroutine.yield() + if coret and type(coret) == "string" and coret == "cancel" then + print("[SAVE] abording") + break + end + end end return self.tables[obj] end --- Save the given world -function _M:saveWorld(world) +function _M:saveWorld(world, no_dialog) collectgarbage("collect") fs.mkdir(self.save_dir) - local popup = Dialog:simplePopup("Saving world", "Please wait while saving the world...") - popup.__showup = nil + local popup + if not no_dialog then + popup = Dialog:simplePopup("Saving world", "Please wait while saving the world...") + popup.__showup = nil + end core.display.forceRedraw() local zip = fs.zipOpen(self.save_dir.."world.teaw") self:saveObject(world, zip) zip:close() - game:unregisterDialog(popup) + if not no_dialog then game:unregisterDialog(popup) end end --- Save the given birth descriptors, used for quick start @@ -142,13 +153,16 @@ function _M:loadQuickBirth() end --- Save the given game -function _M:saveGame(game) +function _M:saveGame(game, no_dialog) collectgarbage("collect") fs.mkdir(self.save_dir) - local popup = Dialog:simplePopup("Saving game", "Please wait while saving the game...") - popup.__showup = nil + local popup + if not no_dialog then + popup = Dialog:simplePopup("Saving game", "Please wait while saving the game...") + popup.__showup = nil + end core.display.forceRedraw() local zip = fs.zipOpen(self.save_dir.."game.teag") @@ -162,37 +176,43 @@ function _M:saveGame(game) f:write(("description = %q\n"):format(desc.description)) f:close() - game:unregisterDialog(popup) + if not no_dialog then game:unregisterDialog(popup) end end --- Save a zone -function _M:saveZone(zone) +function _M:saveZone(zone, no_dialog) fs.mkdir(self.save_dir) - local popup = Dialog:simplePopup("Saving zone", "Please wait while saving the zone...") - popup.__showup = nil + local popup + if not no_dialog then + popup = Dialog:simplePopup("Saving zone", "Please wait while saving the zone...") + popup.__showup = nil + end core.display.forceRedraw() local zip = fs.zipOpen(self.save_dir..("zone-%s.teaz"):format(zone.short_name)) self:saveObject(zone, zip) zip:close() - game:unregisterDialog(popup) + if not no_dialog then game:unregisterDialog(popup) end end --- Save a level -function _M:saveLevel(level) +function _M:saveLevel(level, no_dialog) fs.mkdir(self.save_dir) - local popup = Dialog:simplePopup("Saving level", "Please wait while saving the level...") - popup.__showup = nil + local popup + if not no_dialog then + popup = Dialog:simplePopup("Saving level", "Please wait while saving the level...") + popup.__showup = nil + end core.display.forceRedraw() local zip = fs.zipOpen(self.save_dir..("level-%s-%d.teal"):format(level.data.short_name, level.level)) self:saveObject(level, zip) zip:close() - game:unregisterDialog(popup) + if not no_dialog then game:unregisterDialog(popup) end end local function resolveSelf(o, base, allow_object) diff --git a/game/engines/default/engine/class.lua b/game/engines/default/engine/class.lua index 50a696abe74c6bb83fbd9c3982294581c4aeaca5..91a73719a0260fe02adac161f8e325e4d5c295a4 100644 --- a/game/engines/default/engine/class.lua +++ b/game/engines/default/engine/class.lua @@ -108,6 +108,34 @@ function _M:clone(t) return n end + +local function clonerecursfull(clonetable, d) + local n = {} + clonetable[d] = n + + for k, e in pairs(d) do + local nk, ne = k, e + + if clonetable[k] then nk = clonetable[k] + elseif type(k) == "table" then nk = clonerecursfull(clonetable, k) + end + + if clonetable[e] then ne = clonetable[e] + elseif type(e) == "table" then ne = clonerecursfull(clonetable, e) + end + n[nk] = ne + end + setmetatable(n, getmetatable(d)) + return n +end + +--- Clones the object, all subobjects without cloning twice a subobject +function _M:cloneFull() + local clonetable = {} + local n = clonerecursfull(clonetable, self) + return n +end + --- Replaces the object with an other, by copying (not deeply) function _M:replaceWith(t) -- Delete fields diff --git a/game/engines/default/engine/interface/ActorProject.lua b/game/engines/default/engine/interface/ActorProject.lua index 49997ad03146bf78c671ba288c1a9fca1480f673..f9543ffe316a337403180e08ab51ac50170a402a 100644 --- a/game/engines/default/engine/interface/ActorProject.lua +++ b/game/engines/default/engine/interface/ActorProject.lua @@ -74,14 +74,14 @@ function _M:project(t, x, y, damtype, dam, particles) -- Ok if we are at the end reset lx and ly for the next code if not lx and not ly then lx, ly = x, y end - if typ.ball then + if typ.ball and typ.ball > 0 then core.fov.calc_circle(lx, ly, typ.ball, function(_, px, py) -- Deal damage: ball addGrid(px, py) if not typ.no_restrict and game.level.map:checkEntity(px, py, Map.TERRAIN, "block_move") then return true end end, function()end, nil) addGrid(lx, ly) - elseif typ.cone then + elseif typ.cone and typ.cone > 0 then core.fov.calc_beam(lx, ly, typ.cone, initial_dir, typ.cone_angle, function(_, px, py) -- Deal damage: cone addGrid(px, py) @@ -247,14 +247,14 @@ function _M:projectDoStop(typ, tg, damtype, dam, particles, lx, ly, tmp) grids[x][y] = true end - if typ.ball then + if typ.ball and typ.ball > 0 then core.fov.calc_circle(lx, ly, typ.ball, function(_, px, py) -- Deal damage: ball addGrid(px, py) if not typ.no_restrict and game.level.map:checkEntity(px, py, Map.TERRAIN, "block_move") then return true end end, function()end, nil) addGrid(lx, ly) - elseif typ.cone then + elseif typ.cone and typ.cone > 0 then local initial_dir = lx and util.getDir(lx, ly, x, y) or 5 core.fov.calc_beam(lx, ly, typ.cone, initial_dir, typ.cone_angle, function(_, px, py) -- Deal damage: cone diff --git a/game/modules/tome/class/Game.lua b/game/modules/tome/class/Game.lua index c41ae4112d2337c319d88be44bd66af1d7e232ed..8cf98ad33f8cb0a3f2acea2e8ec6c99b9c63e29e 100644 --- a/game/modules/tome/class/Game.lua +++ b/game/modules/tome/class/Game.lua @@ -376,7 +376,7 @@ function _M:tick() -- (since display is on a set FPS while tick() ticks as much as possible -- engine.GameEnergyBased.tick(self) end - if game.paused then return true end + if self.paused and not self.saving then return true end end --- Called every game turns diff --git a/game/modules/tome/class/PlayerDisplay.lua b/game/modules/tome/class/PlayerDisplay.lua index ab1283c070cceaa6273ce280ff1a685bfd1875a6..fad6545c5ebfe85d843e1e0b69272b1b91c32620 100644 --- a/game/modules/tome/class/PlayerDisplay.lua +++ b/game/modules/tome/class/PlayerDisplay.lua @@ -109,6 +109,8 @@ function _M:display() self.surface:drawColorStringBlended(self.font, ("#904010#Vim: #ffffff#%d/%d"):format(player:getVim(), player.max_vim), 0, h, 255, 255, 255) h = h + self.font_h end + if game.saving then h = h + self.font_h self.surface:drawColorStringBlended(self.font, "#YELLOW#Saving...", 0, h, 255, 255, 255) h = h + self.font_h end + h = h + self.font_h for tid, act in pairs(player.sustain_talents) do if act then self.surface:drawColorStringBlended(self.font, ("#LIGHT_GREEN#%s"):format(player:getTalentFromId(tid).name), 0, h, 255, 255, 255) h = h + self.font_h end diff --git a/game/modules/tome/class/World.lua b/game/modules/tome/class/World.lua index ea2cc0b54e06247c32dad1ac9eb4628afc84cafe..05cf3d30e42c81ab5d558d0bd50338d8a24dc62a 100644 --- a/game/modules/tome/class/World.lua +++ b/game/modules/tome/class/World.lua @@ -33,9 +33,9 @@ function _M:run() end --- Requests the world to save -function _M:saveWorld() +function _M:saveWorld(no_dialog) local save = Savefile.new("") - save:saveWorld(self) + save:saveWorld(self, no_dialog) save:close() game.log("Saved world.") end diff --git a/game/modules/tome/data/talents/corruptions/plague.lua b/game/modules/tome/data/talents/corruptions/plague.lua new file mode 100644 index 0000000000000000000000000000000000000000..4584d65dc782d83385acf798af9623592d0f35a3 --- /dev/null +++ b/game/modules/tome/data/talents/corruptions/plague.lua @@ -0,0 +1,219 @@ +-- ToME - Tales of Middle-Earth +-- Copyright (C) 2009, 2010 Nicolas Casalini +-- +-- This program is free software: you can redistribute it and/or modify +-- it under the terms of the GNU General Public License as published by +-- the Free Software Foundation, either version 3 of the License, or +-- (at your option) any later version. +-- +-- This program is distributed in the hope that it will be useful, +-- but WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +-- GNU General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License +-- along with this program. If not, see <http://www.gnu.org/licenses/>. +-- +-- Nicolas Casalini "DarkGod" +-- darkgod@te4.org + +newTalent{ + name = "Virulent Disease", + type = {"corruption/plague", 1}, + require = corrs_req1, + points = 5, + vim = 8, + cooldown = 3, + range = function(self, t) return 5 + math.floor(self:getTalentLevel(t) * 1.3) end, + action = function(self, t) + local tg = {type="bolt", range=self:getTalentRange(t)} + local x, y = self:getTarget(tg) + if not x or not y then return nil end + + local diseases = {{self.EFF_WEAKNESS_DISEASE, "str"}, {self.EFF_ROTTING_DISEASE,"con"}, {self.EFF_DECREPITUDE_DISEASE,"dex"}} + local disease = rng.table(diseases) + + -- Try to rot ! + self:project(tg, x, y, function(px, py) + local target = game.level.map(px, py, engine.Map.ACTOR) + if not target then return end + if target:checkHit(self:combatSpellpower(), target:combatSpellResist(), 0, 95, 12 - self:getTalentLevel(t)) and target:canBe("disease") then + target:setEffect(disease[1], 6, {src=self, dam=self:combatTalentSpellDamage(t, 5, 45), [disease[2]]=self:combatTalentSpellDamage(t, 5, 25)}) + else + game.logSeen(target, "%s resists the disease!", target.name:capitalize()) + end + game.level.map:particleEmitter(px, py, 1, "slime") + end) + game:playSoundNear(self, "talents/slime") + + return true + end, + info = function(self, t) + return ([[Fires a bolt of pure filth, diseasing your target with a random disease doing %0.2f blight damage per turns for 6 turns and reducing one of its physical stats (strength, constitution, dexterity) by %d. + The effect will increase with your Magic stat.]]): + format(self:combatTalentSpellDamage(t, 5, 45), self:combatTalentSpellDamage(t, 5, 25)) + end, +} + +newTalent{ + name = "Cyst Burst", + type = {"corruption/plague", 2}, + require = corrs_req2, + points = 5, + vim = 18, + cooldown = 9, + range = 15, + action = function(self, t) + local tg = {type="bolt", range=self:getTalentRange(t)} + local x, y = self:getTarget(tg) + if not x or not y then return nil end + + local dam = self:combatTalentSpellDamage(t, 15, 85) + local diseases = {} + + -- Try to rot ! + local source = nil + self:project(tg, x, y, function(px, py) + local target = game.level.map(px, py, engine.Map.ACTOR) + if not target then return end + + for eff_id, p in pairs(target.tmp) do + local e = target.tempeffect_def[eff_id] + if e.type == "disease" then + diseases[#diseases+1] = {id=eff_id, params=p} + end + end + + if #diseases > 0 then + DamageType:get(DamageType.BLIGHT).projector(self, px, py, DamageType.BLIGHT, dam * #diseases) + game.level.map:particleEmitter(px, py, 1, "slime") + end + source = target + end) + + if #diseases > 0 then + self:project({type="ball", radius=1, range=self:getTalentRange(t)}, x, y, function(px, py) + local target = game.level.map(px, py, engine.Map.ACTOR) + if not target or target == source or target == self then return end + + local disease = rng.table(diseases) + target:setEffect(disease.id, 6, {src=self, dam=disease.params.dam, str=disease.params.str, dex=disease.params.dex, con=disease.params.con}) + game.level.map:particleEmitter(px, py, 1, "slime") + end) + end + game:playSoundNear(self, "talents/slime") + + return true + end, + info = function(self, t) + return ([[Make your target's diseases burst, doing %0.2f blight damage for each diseases it is infected with. + This will also spread the diseases to any nearby foes in a radius of 1. + The damage will increase with your Magic stat.]]): + format(self:combatTalentSpellDamage(t, 15, 85)) + end, +} + +newTalent{ + name = "Catalepsy", + type = {"corruption/plague", 3}, + require = corrs_req3, + points = 5, + vim = 35, + cooldown = 15, + range = 10, + action = function(self, t) + local tg = {type="ball", range=self:getTalentRange(t), radius=2} + local x, y = self:getTarget(tg) + if not x or not y then return nil end + + local dur = math.floor(2 + self:getTalentLevel(t) / 2) + + local source = nil + self:project(tg, x, y, function(px, py) + local target = game.level.map(px, py, engine.Map.ACTOR) + if not target then return end + + -- List all diseas + local diseases = {} + for eff_id, p in pairs(target.tmp) do + local e = target.tempeffect_def[eff_id] + if e.type == "disease" then + diseases[#diseases+1] = {id=eff_id, params=p} + end + end + -- Make them EXPLODE !!! + for i, d in ipairs(diseases) do + target:removeEffect(d.id) + DamageType:get(DamageType.BLIGHT).projector(self, px, py, DamageType.BLIGHT, d.params.dam * d.params.dur) + end + + if #diseases > 0 and target:checkHit(self:combatSpellpower(), target:combatSpellResist(), 0, 95, 8) and target:canBe("stun") then + target:setEffect(target.EFF_STUNNED, dur, {}) + else + game.logSeen(target, "%s resists the stun!", target.name:capitalize()) + end + game.level.map:particleEmitter(px, py, 1, "slime") + end) + game:playSoundNear(self, "talents/slime") + + return true + end, + info = function(self, t) + return ([[All your foes infected with a disease enter a catalepsy, stunning them for %d turns and dealing all the diseases remaining damage instantly.]]): + format(math.floor(2 + self:getTalentLevel(t) / 2)) + end, +} + +newTalent{ + name = "Epidemic", + type = {"corruption/plague", 4}, + require = corrs_req4, + points = 5, + vim = 20, + cooldown = 13, + range = 10, + do_spread = function(self, t, carrier) + -- List all diseas + local diseases = {} + for eff_id, p in pairs(carrier.tmp) do + local e = carrier.tempeffect_def[eff_id] + if e.type == "disease" then + diseases[#diseases+1] = {id=eff_id, params=p} + end + end + + self:project({type="ball", radius=2}, carrier.x, carrier.y, function(px, py) + local target = game.level.map(px, py, engine.Map.ACTOR) + if not target or target == carrier or target == self then return end + + local disease = rng.table(diseases) + target:setEffect(disease.id, 6, {src=self, dam=disease.params.dam, str=disease.params.str, dex=disease.params.dex, con=disease.params.con}) + game.level.map:particleEmitter(px, py, 1, "slime") + end) + end, + action = function(self, t) + local tg = {type="bolt", range=self:getTalentRange(t)} + local x, y = self:getTarget(tg) + if not x or not y then return nil end + + -- Try to rot ! + self:project(tg, x, y, function(px, py) + local target = game.level.map(px, py, engine.Map.ACTOR) + if not target then return end + if target:checkHit(self:combatSpellpower(), target:combatSpellResist(), 0, 95, 12 - self:getTalentLevel(t)) and target:canBe("disease") then + target:setEffect(self.EFF_EPIDEMIC, 6, {src=self, dam=self:combatTalentSpellDamage(t, 15, 50)}) + else + game.logSeen(target, "%s resists the disease!", target.name:capitalize()) + end + game.level.map:particleEmitter(px, py, 1, "slime") + end) + game:playSoundNear(self, "talents/slime") + + return true + end, + info = function(self, t) + return ([[Infects the target with a very contagious disease doing %0.2f damage per turn for 6 turns. + If any blight damage from non-diseases hit the target the epidemic will activate and spread diseases to nearby targets.]]): + format(self:combatTalentSpellDamage(t, 15, 50)) + end, +}