Skip to content
Snippets Groups Projects
Forked from tome / Tales of MajEyal
7958 commits behind the upstream repository.
Party.lua 13.24 KiB
-- ToME - Tales of Maj'Eyal
-- Copyright (C) 2009, 2010, 2011, 2012, 2013 Nicolas Casalini
--
-- This program is free software: you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.
--
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-- GNU General Public License for more details.
--
-- You should have received a copy of the GNU General Public License
-- along with this program.  If not, see <http://www.gnu.org/licenses/>.
--
-- Nicolas Casalini "DarkGod"
-- darkgod@te4.org

require "engine.class"
require "engine.Entity"
local Map = require "engine.Map"
local Dialog = require "engine.ui.Dialog"
local GetQuantity = require "engine.dialogs.GetQuantity"
local PartyOrder = require "mod.dialogs.PartyOrder"
local PartyIngredients = require "mod.class.interface.PartyIngredients"
local PartyLore = require "mod.class.interface.PartyLore"
local PartyRewardSelector = require "mod.dialogs.PartyRewardSelector"

module(..., package.seeall, class.inherit(
	engine.Entity, PartyIngredients, PartyLore
))

function _M:init(t, no_default)
	t.name = t.name or "party"
	engine.Entity.init(self, t, no_default)
	PartyIngredients.init(self, t)
	PartyLore.init(self, t)

	self.members = {}
	self.m_list = {}
	self.energy = {value = 0, mod=100000} -- "Act" every tick
	self.on_death_show_achieved = {}
end

function _M:addMember(actor, def)
	if self.members[actor] then
		print("[PARTY] error trying to add existing actor: ", actor.uid, actor.name)
		return false
	end
	if type(def.control) == "nil" then def.control = "no" end
	def.title = def.title or "Party member"
	self.members[actor] = def
	self.m_list[#self.m_list+1] = actor
	def.index = #self.m_list

	if #self.m_list >= 6 then
		game:getPlayer(true):attr("huge_party", 1)
	end

	actor.ai_state = actor.ai_state or {}
	actor.ai_state.tactic_leash_anchor = actor.ai_state.tactic_leash_anchor or game.player
	actor.ai_state.tactic_leash = actor.ai_state.tactic_leash or 10

	actor.addEntityOrder = function(self, level)
		print("[PARTY] New member, add after", self.name, game.party.m_list[1].name)
		return game.party.m_list[1] -- Make the sure party is always consecutive in the level entities list
	end

	-- Turn NPCs into party members
	if not actor.no_party_class then
		local uid = actor.uid
		actor.replacedWith = false
		actor:replaceWith(require("mod.class.PartyMember").new(actor))
		actor.uid = uid
		__uids[uid] = actor
		actor.replacedWith = nil
	end

	-- Notify the UI
	if game.player then game.player.changed = true end
end

function _M:removeMember(actor, silent)
	if not self.members[actor] then
		if not silent then
			print("[PARTY] error trying to remove non-existing actor: ", actor.uid, actor.name)
		end
		return false
	end
	table.remove(self.m_list, self.members[actor].index)
	self.members[actor] = nil

	actor.addEntityOrder = nil

	-- Update indexes
	for i = 1, #self.m_list do
		self.members[self.m_list[i]].index = i
	end

	-- Notify the UI
	game.player.changed = true
end

function _M:leftLevel()
	local todel = {}
	local newplayer = false
	for i, actor in ipairs(self.m_list) do
		local def = self.members[actor]
		if def.temporary_level then
			todel[#todel+1] = actor
			if actor == game.player then newplayer = true end
		end
		if def.leave_level then -- Special function on leaving the level.
			def.leave_level(actor, def)
		end
	end
	for i = 1, #todel do
		self:removeMember(todel[i])
		todel[i]:disappear()
	end
	if not game.player or not self.members[game.player].keep_between_levels then
		self:findSuitablePlayer()
	end
end

function _M:hasMember(actor)
	return self.members[actor]
end

function _M:findMember(filter)
	for i, actor in ipairs(self.m_list) do
		local ok = true
		local def = self.members[actor]

		if filter.main and not def.main then ok = false end
		if filter.type and def.type ~= filter.type then ok = false end

		if ok then return actor end
	end
end

function _M:countInventoryAble()
	local nb = 0
	for i, actor in ipairs(self.m_list) do
		if not actor.no_inventory_access and actor:getInven(actor.INVEN_INVEN) then nb = nb + 1 end
	end
	return nb
end

function _M:setDeathTurn(actor, turn)
	local def = self.members[actor]
	if not def then return end
	def.last_death_turn = turn
end

function _M:findLastDeath()
	local max_turn = -9999
	local last = nil

	for i, actor in ipairs(self.m_list) do
		local def = self.members[actor]

		if def.last_death_turn and def.last_death_turn > max_turn then max_turn = def.last_death_turn; last = actor end
	end
	return last or self:findMember{main=true}
end

function _M:canControl(actor, vocal)
	if not actor then return false end
	if actor == game.player then
		print("[PARTY] error trying to set player, same")
		return false
	end

	if game.player and game.player.no_leave_control then
		print("[PARTY] error trying to set player but current player is modal")
		return false
	end
	if not self.members[actor] then
		print("[PARTY] error trying to set player, not a member of party: ", actor.uid, actor.name)
		return false
	end
	if self.members[actor].control ~= "full" then
		print("[PARTY] error trying to set player, not controlable: ", actor.uid, actor.name)
		return false
	end
	if actor.dead or (game.level and not game.level:hasEntity(actor)) then
		if vocal then game.logPlayer(game.player, "Can not switch control to this creature.") end
		print("[PARTY] error trying to set player, no entity or dead")
		return false
	end
	if actor.on_can_control and not actor:on_can_control(vocal) then
		print("[PARTY] error trying to set player, forbade")
		return false
	end
	return true
end

function _M:setPlayer(actor, bypass)
	if type(actor) == "number" then actor = self.m_list[actor] end

	if not bypass then
		local ok, err = self:canControl(actor, true)
		if not ok then return nil, err end
	end

	if actor == game.player then return true end

	-- Stop!!
	if game.player and game.player.runStop then game.player:runStop("Switching control") end
	if game.player and game.player.restStop then game.player:restStop("Switching control") end

	local def = self.members[actor]
	local oldp = self.player
	self.player = actor

	-- Convert the class to always be a player
	if actor.__CLASSNAME ~= "mod.class.Player" and not actor.no_party_class then
		actor.__PREVIOUS_CLASSNAME = actor.__CLASSNAME
		local uid = actor.uid
		actor.replacedWith = false
		actor:replaceWith(mod.class.Player.new(actor))
		actor.replacedWith = nil
		actor.uid = uid
		__uids[uid] = actor
		actor.changed = true
	end

	-- Setup as the curent player
	actor.player = true
	game.paused = actor:enoughEnergy()
	game.player = actor
	game.uiset.hotkeys_display.actor = actor
	Map:setViewerActor(actor)
	if game.target then game.target.source_actor = actor end
	if game.level and actor.x and actor.y then game.level.map:moveViewSurround(actor.x, actor.y, 8, 8) end
	actor._move_others = actor.move_others
	actor.move_others = true

	-- Change back the old actor to a normal actor
	if oldp and oldp ~= actor then
		if self.members[oldp] and self.members[oldp].on_uncontrol then self.members[oldp].on_uncontrol(oldp) end

		if oldp.__PREVIOUS_CLASSNAME then
			local uid = oldp.uid
			oldp.replacedWith = false
			oldp:replaceWith(require(oldp.__PREVIOUS_CLASSNAME).new(oldp))
			oldp.replacedWith = nil
			oldp.uid = uid
			__uids[uid] = oldp
		end

		actor.move_others = actor._move_others
		oldp.changed = true
		oldp.player = nil
		if game.level and oldp.x and oldp.y then oldp:move(oldp.x, oldp.y, true) end
	end

	if def.on_control then def.on_control(actor) end

	if game.level and actor.x and actor.y then actor:move(actor.x, actor.y, true) end

	if not actor.hotkeys_sorted then actor:sortHotkeys() end

	game.logPlayer(actor, "#MOCCASIN#Character control switched to %s.", actor.name)

	return true
end

function _M:findSuitablePlayer(type)
	for i, actor in ipairs(self.m_list) do
		local def = self.members[actor]
		if def.control == "full" and (not type or def.type == type) and not actor.dead and game.level:hasEntity(actor) then
			if self:setPlayer(actor, true) then
				return true
			end
		end
	end
	return false
end

function _M:canOrder(actor, order, vocal)
	if not actor then return false end
	if actor == game.player then return false end

	if not self.members[actor] then
		print("[PARTY] error trying to order, not a member of party: ", actor.uid, actor.name)
		return false
	end
	if (self.members[actor].control ~= "full" and self.members[actor].control ~= "order") or not self.members[actor].orders then
		print("[PARTY] error trying to order, not controlable: ", actor.uid, actor.name)
		return false
	end
	if actor.dead or (game.level and not game.level:hasEntity(actor)) then
		if vocal then game.logPlayer(game.player, "Can not give orders to this creature.") end
		return false
	end
	if actor.on_can_order and not actor:on_can_order(vocal) then
		print("[PARTY] error trying to order, can order forbade")
		return false
	end
	if order and not self.members[actor].orders[order] then
		print("[PARTY] error trying to order, unknown order: ", actor.uid, actor.name)
		return false
	end
	return true
end

function _M:giveOrders(actor)
	if type(actor) == "number" then actor = self.m_list[actor] end

	local ok, err = self:canOrder(actor, nil, true)
	if not ok then return nil, err end

	local def = self.members[actor]

	game:registerDialog(PartyOrder.new(actor, def))

	return true
end

function _M:giveOrder(actor, order)
	if type(actor) == "number" then actor = self.m_list[actor] end

	local ok, err = self:canOrder(actor, order, true)
	if not ok then return nil, err end

	local def = self.members[actor]

	if order == "leash" then
		game:registerDialog(GetQuantity.new("Set action radius: "..actor.name, "Set the maximum distance this creature can go from the party master", actor.ai_state.tactic_leash, actor.ai_state.tactic_leash_max or 100, function(qty)
			actor.ai_state.tactic_leash = util.bound(qty, 1, actor.ai_state.tactic_leash_max or 100)
			game.logPlayer(game.player, "%s maximum action radius set to %d.", actor.name:capitalize(), actor.ai_state.tactic_leash)
		end), 1)
	elseif order == "anchor" then
		local co = coroutine.create(function()
			local x, y, act = game.player:getTarget({type="hit", range=10, nowarning=true})
			local anchor
			if x and y then
				if act then
					anchor = act
				else
					anchor = {x=x, y=y, name="that location"}
				end
				actor.ai_state.tactic_leash_anchor = anchor
				game.logPlayer(game.player, "%s will stay near %s.", actor.name:capitalize(), anchor.name)
			end
		end)
		local ok, err = coroutine.resume(co)
		if not ok and err then print(debug.traceback(co)) error(err) end
	elseif order == "target" then
		local co = coroutine.create(function()
			local x, y, act = game.player:getTarget({type="hit", range=10})
			if act then
				actor:setTarget(act)
				game.player:logCombat(act, "%s targets #Target#.", actor.name:capitalize())
			end
		end)
		local ok, err = coroutine.resume(co)
		if not ok and err then print(debug.traceback(co)) error(err) end
	elseif order == "behavior" then
		game:registerDialog(require("mod.dialogs.orders."..order:capitalize()).new(actor, def))
	elseif order == "talents" then
		game:registerDialog(require("mod.dialogs.orders."..order:capitalize()).new(actor, def))

	-------------------------------------------
	-- Escort specifics
	-------------------------------------------
	elseif order == "escort_rest" then
		-- Rest for a few turns
		if actor.ai_state.tactic_escort_rest then actor:doEmote("No, we must hurry!", 40) return true end
		actor.ai_state.tactic_escort_rest = rng.range(6, 10)
		actor:doEmote("Ok, but not for long.", 40)
	elseif order == "escort_portal" then
		local dist = core.fov.distance(actor.escort_target.x, actor.escort_target.y, actor.x, actor.y)
		if dist < 8 then dist = "very close"
		elseif dist < 16 then dist = "close"
		else dist = "still far away"
		end

		local dir = game.level.map:compassDirection(actor.escort_target.x - actor.x, actor.escort_target.y - actor.y)
		actor:doEmote(("The portal is %s, to the %s."):format(dist, dir or "???"), 45)
	end

	return true
end

function _M:select(actor)
	if not actor then return false end
	if type(actor) == "number" then actor = self.m_list[actor] end
	if actor == game.player then print("[PARTY] control fail, same", actor, game.player) return false end

	if self:canControl(actor) then return self:setPlayer(actor)
	elseif self:canOrder(actor) then return self:giveOrders(actor)
	end
	return false
end

function _M:canReward(actor)
	if not actor then return false end

	if not self.members[actor] then
		return false
	end
	if self.members[actor].control ~= "full" then
		return false
	end
	if actor.dead or (game.level and not game.level:hasEntity(actor)) then
		return false
	end
	if actor.summon_time then
		return false
	end
	return true
end

function _M:reward(title, action)
	local d = PartyRewardSelector.new(title, action)
	if #d.list == 1 then
		action(d.list[1].actor)
		return
	end
	game:registerDialog(d)
end

function _M:findInAllPartyInventoriesBy(prop, value)
	local o, item, inven_id
	for i, mem in ipairs(game.party.m_list) do
		o, item, inven_id = mem:findInAllInventoriesBy(prop, value)
		if o then return mem, o, item, inven_id  end
	end
end
_M.findInAllInventoriesBy = _M.findInAllPartyInventoriesBy