FreeKill/lua/core/player.lua

718 lines
20 KiB
Lua
Raw Normal View History

-- SPDX-License-Identifier: GPL-3.0-or-later
--- 玩家分为客户端要处理的玩家,以及服务端处理的玩家两种。
---
--- 客户端能知道的玩家的信息十分有限,而服务端知道一名玩家的所有细节。
---
--- Player类就是这两种玩家的基类包含它们共用的部分。
---
2022-03-30 08:33:56 +00:00
---@class Player : Object
---@field public id integer @ 玩家的id每名玩家的id是唯一的。机器人的id是负数。
---@field public hp integer @ 体力值
---@field public maxHp integer @ 体力上限
---@field public shield integer @ 护甲数
---@field public kingdom string @ 势力
---@field public role string @ 身份
---@field public general string @ 武将
---@field public gender integer @ 性别
---@field public seat integer @ 座位号
---@field public next Player @ 下家
---@field public phase Phase @ 当前阶段
---@field public faceup boolean @ 是否正面朝上
---@field public chained boolean @ 是否被横直
---@field public dying boolean @ 是否处于濒死
---@field public dead boolean @ 是否死亡
---@field public player_skills Skill[] @ 当前拥有的所有技能
---@field public derivative_skills table<Skill, Skill[]> @ 当前拥有的派生技能
---@field public flag string[] @ 当前拥有的flag不过好像没用过
---@field public tag table<string, any> @ 当前拥有的所有tag好像也没用过
---@field public mark table<string, integer> @ 当前拥有的所有标记,用烂了
---@field public player_cards table<integer, integer[]> @ 当前拥有的所有牌键是区域值是id列表
---@field public virtual_equips Card[] @ 当前的虚拟装备牌,其实也包含着虚拟延时锦囊这种
---@field public special_cards table<string, integer[]> @ 类似“屯田”这种的私人牌堆
---@field public cardUsedHistory table<string, integer[]> @ 用牌次数历史记录
---@field public skillUsedHistory table<string, integer[]> @ 发动技能次数的历史记录
---@field public fixedDistance table<Player, integer> @ 与其他玩家的固定距离列表
local Player = class("Player")
---@alias Phase integer
Player.RoundStart = 1
Player.Start = 2
Player.Judge = 3
Player.Draw = 4
Player.Play = 5
Player.Discard = 6
Player.Finish = 7
Player.NotActive = 8
Player.PhaseNone = 9
---@alias PlayerCardArea integer
Player.Hand = 1
Player.Equip = 2
Player.Judge = 3
Player.Special = 4
Player.HistoryPhase = 1
Player.HistoryTurn = 2
Player.HistoryRound = 3
Player.HistoryGame = 4
--- 构造函数。总之这不是随便调用的函数
function Player:initialize()
self.id = 114514
self.hp = 0
self.maxHp = 0
self.kingdom = "qun"
self.role = ""
self.general = ""
self.gender = General.Male
self.seat = 0
self.next = nil
self.phase = Player.NotActive
self.faceup = true
self.chained = false
self.dying = false
self.dead = false
self.state = ""
self.drank = 0
self.player_skills = {}
self.derivative_skills = {}
self.flag = {}
self.tag = {}
self.mark = {}
self.player_cards = {
[Player.Hand] = {},
[Player.Equip] = {},
[Player.Judge] = {},
}
self.virtual_equips = {}
self.special_cards = {}
self.cardUsedHistory = {}
self.skillUsedHistory = {}
self.fixedDistance = {}
end
function Player:__tostring()
return string.format("%s #%d", self.id < 0 and "Bot" or "Player", math.abs(self.id))
end
--- 设置角色、体力、技能。
---@param general General @ 角色类型
---@param setHp boolean @ 是否设置体力
---@param addSkills boolean @ 是否增加技能
function Player:setGeneral(general, setHp, addSkills)
self.general = general
if setHp then
self.maxHp = general.maxHp
self.hp = general.hp
end
if addSkills then
table.insertTable(self.player_skills, general.skills)
end
end
--- 查询角色是否存在flag。
---@param flag string @ 一种标记
function Player:hasFlag(flag)
return table.contains(self.flag, flag)
end
--- 为角色赋予flag。
---@param flag string @ 一种标记
function Player:setFlag(flag)
2023-02-26 08:51:29 +00:00
if flag == "." then
self:clearFlags()
return
end
if flag:sub(1, 1) == "-" then
flag = flag:sub(2, #flag)
table.removeOne(self.flag, flag)
return
end
if not self:hasFlag(flag) then
table.insert(self.flag, flag)
end
end
--- 清除角色flag。
function Player:clearFlags()
self.flag = {}
end
--- 为角色赋予Mark。
---@param mark string @ 标记
---@param count integer @ 为标记赋予的数量
-- mark name and UI:
-- 'xxx': invisible mark
-- '@mark': mark with extra data (maybe string or number)
-- '@@mark': mark without data
function Player:addMark(mark, count)
count = count or 1
local num = self.mark[mark]
num = num or 0
self:setMark(mark, math.max(num + count, 0))
end
--- 为角色移除Mark。
---@param mark string @ 标记
---@param count integer @ 为标记删除的数量
function Player:removeMark(mark, count)
count = count or 1
local num = self.mark[mark]
num = num or 0
self:setMark(mark, math.max(num - count, 0))
end
--- 为角色设置Mark至指定数量。
---@param mark string @ 标记
---@param count integer @ 为标记删除的数量
function Player:setMark(mark, count)
if self.mark[mark] ~= count then
self.mark[mark] = count
end
end
--- 获取角色对应Mark的数量。
---@param mark string @ 标记
---@param count integer @ 为标记删除的数量
function Player:getMark(mark)
return (self.mark[mark] or 0)
end
--- 获取角色有哪些Mark。
function Player:getMarkNames()
local ret = {}
for k, _ in pairs(self.mark) do
table.insert(ret, k)
end
return ret
end
--- 将指定数量的牌加入玩家的对应区域。
---@param playerArea PlayerCardArea @ 玩家牌所在的区域
---@param cardIds integer[] @ 牌的ID返回唯一牌
---@param specialName string @ 私人牌堆名
function Player:addCards(playerArea, cardIds, specialName)
assert(table.contains({ Player.Hand, Player.Equip, Player.Judge, Player.Special }, playerArea))
assert(playerArea ~= Player.Special or type(specialName) == "string")
if playerArea == Player.Special then
self.special_cards[specialName] = self.special_cards[specialName] or {}
table.insertTable(self.special_cards[specialName], cardIds)
else
table.insertTable(self.player_cards[playerArea], cardIds)
end
end
--- 将指定数量的牌移除出玩家的对应区域。
---@param playerArea PlayerCardArea @ 玩家牌所在的区域
---@param cardIds integer[] @ 牌的ID返回唯一牌
---@param specialName string @ 私人牌堆名
function Player:removeCards(playerArea, cardIds, specialName)
assert(table.contains({ Player.Hand, Player.Equip, Player.Judge, Player.Special }, playerArea))
assert(playerArea ~= Player.Special or type(specialName) == "string")
local fromAreaIds = playerArea == Player.Special and self.special_cards[specialName] or self.player_cards[playerArea]
if fromAreaIds then
for _, id in ipairs(cardIds) do
if #fromAreaIds == 0 then
break
end
table.removeOne(fromAreaIds, id)
end
end
end
-- virtual delayed trick can use these functions too
--- 为玩家提供虚拟装备。
---@param card Card @ 卡牌
function Player:addVirtualEquip(card)
assert(card and card:isInstanceOf(Card) and card:isVirtual())
table.insertIfNeed(self.virtual_equips, card)
end
--- 为玩家移除虚拟装备。
---@param cid integer @ 卡牌ID用来定位装备
function Player:removeVirtualEquip(cid)
for _, c in ipairs(self.virtual_equips) do
for _, id in ipairs(c.subcards) do
if id == cid then
table.removeOne(self.virtual_equips, c)
return c
end
end
end
end
--- 确认玩家是否存在虚拟装备。
---@param cid integer @ 卡牌ID用来定位装备
function Player:getVirualEquip(cid)
for _, c in ipairs(self.virtual_equips) do
for _, id in ipairs(c.subcards) do
if id == cid then
return c
end
end
end
end
--- 确认玩家判定区是否存在延迟锦囊牌。
function Player:hasDelayedTrick(card_name)
for _, id in ipairs(self:getCardIds(Player.Judge)) do
local c = self:getVirualEquip(id)
if not c then c = Fk:getCardById(id) end
if c.name == card_name then
return true
end
end
end
--- 获取玩家特定区域所有牌的ID。
---@param playerAreas PlayerCardArea @ 玩家牌所在的区域
---@param specialName string @私人牌堆名
---@return integer[] @ 返回对应区域的所有牌对应的ID
function Player:getCardIds(playerAreas, specialName)
local rightAreas = { Player.Hand, Player.Equip, Player.Judge }
playerAreas = playerAreas or rightAreas
assert(type(playerAreas) == "number" or type(playerAreas) == "table")
local areas = type(playerAreas) == "table" and playerAreas or { playerAreas }
local rightAreas = { Player.Hand, Player.Equip, Player.Judge, Player.Special }
local cardIds = {}
for _, area in ipairs(areas) do
assert(table.contains(rightAreas, area))
assert(area ~= Player.Special or type(specialName) == "string")
local currentCardIds = area == Player.Special and self.special_cards[specialName] or self.player_cards[area]
table.insertTable(cardIds, currentCardIds)
end
return cardIds
end
--- 通过名字检索获取玩家是否存在对应私人牌堆。
---@param name string @ 私人牌堆名
function Player:getPile(name)
return self.special_cards[name] or {}
end
--- 通过ID检索获取玩家是否存在对应私人牌堆。
---@param id integer @ 私人牌堆ID
---@return string|null
function Player:getPileNameOfId(id)
for k, v in pairs(self.special_cards) do
if table.contains(v, id) then return k end
end
end
-- for fkp only
function Player:getHandcardNum()
return #self:getCardIds(Player.Hand)
end
--- 检索玩家装备区是否存在对应类型的装备。
---@param cardSubtype CardSubtype @ 卡牌子类
---@return integer|null @ 返回卡牌ID或nil
function Player:getEquipment(cardSubtype)
for _, cardId in ipairs(self.player_cards[Player.Equip]) do
if Fk:getCardById(cardId).sub_type == cardSubtype then
return cardId
end
end
return nil
end
--- 获取玩家手牌上限。
function Player:getMaxCards()
local baseValue = math.max(self.hp, 0)
local status_skills = Fk:currentRoom().status_skills[MaxCardsSkill] or {}
local max_fixed = nil
for _, skill in ipairs(status_skills) do
local f = skill:getFixed(self)
if f ~= nil then
max_fixed = max_fixed and math.max(max_fixed, f) or f
end
end
if max_fixed then baseValue = math.max(max_fixed, 0) end
2023-02-26 08:51:29 +00:00
for _, skill in ipairs(status_skills) do
local c = skill:getCorrect(self)
baseValue = baseValue + (c or 0)
end
return math.max(baseValue, 0)
end
--- 获取玩家攻击距离。
function Player:getAttackRange()
local weapon = Fk:getCardById(self:getEquipment(Card.SubtypeWeapon))
local baseAttackRange = math.max(weapon and weapon.attack_range or 1, 0)
2023-04-10 13:34:23 +00:00
local status_skills = Fk:currentRoom().status_skills[AttackRangeSkill] or {}
for _, skill in ipairs(status_skills) do
local correct = skill:getCorrect(self, other)
baseAttackRange = baseAttackRange + correct
end
return math.max(baseAttackRange, 0)
end
--- 修改玩家与其他角色的固定距离。
---@param other Player @ 其他玩家
---@param num integer @ 距离数
function Player:setFixedDistance(other, num)
self.fixedDistance[other] = num
end
--- 移除玩家与其他角色的固定距离。
---@param other Player @ 其他玩家
function Player:removeFixedDistance(other)
self.fixedDistance[other] = nil
end
--- 获取玩家与其他角色的实际距离。
---
--- 通过 二者位次+距离技能之和 与 两者间固定距离 进行对比,更大的为实际距离。
---@param other Player @ 其他玩家
function Player:distanceTo(other)
assert(other:isInstanceOf(Player))
local right = 0
local temp = self
while temp ~= other do
if not temp.dead then
right = right + 1
end
temp = temp.next
end
local left = #Fk:currentRoom().alive_players - right
local ret = math.min(left, right)
local status_skills = Fk:currentRoom().status_skills[DistanceSkill] or {}
for _, skill in ipairs(status_skills) do
local correct = skill:getCorrect(self, other)
if correct == nil then correct = 0 end
ret = ret + correct
end
2023-02-26 08:51:29 +00:00
if self.fixedDistance[other] then
ret = self.fixedDistance[other]
end
return math.max(ret, 1)
end
--- 获取其他玩家是否在玩家的攻击距离内。
---@param other Player @ 其他玩家
function Player:inMyAttackRange(other)
if self == other then
return false
end
local baseAttackRange = self:getAttackRange()
return self:distanceTo(other) <= baseAttackRange
end
--- 增加玩家使用特定牌的历史次数。
---@param cardName string @ 牌名
---@param num integer @ 次数
function Player:addCardUseHistory(cardName, num)
num = num or 1
assert(type(num) == "number" and num ~= 0)
self.cardUsedHistory[cardName] = self.cardUsedHistory[cardName] or {0, 0, 0, 0}
local t = self.cardUsedHistory[cardName]
for i, _ in ipairs(t) do
t[i] = t[i] + num
end
end
--- 设定玩家使用特定牌的历史次数。
---@param cardName string @ 牌名
---@param num integer @ 次数
---@param scope integer @ 查询历史范围
function Player:setCardUseHistory(cardName, num, scope)
if cardName == "" and num == nil and scope == nil then
self.cardUsedHistory = {}
return
end
num = num or 0
if cardName == "" then
for _, v in pairs(self.cardUsedHistory) do
v[scope] = num
end
return
end
if self.cardUsedHistory[cardName] then
self.cardUsedHistory[cardName][scope] = num
end
end
--- 增加玩家使用特定技能的历史次数。
---@param skill_name string @ 技能名
---@param num integer @ 次数
function Player:addSkillUseHistory(skill_name, num)
num = num or 1
assert(type(num) == "number" and num ~= 0)
self.skillUsedHistory[skill_name] = self.skillUsedHistory[skill_name] or {0, 0, 0, 0}
local t = self.skillUsedHistory[skill_name]
for i, _ in ipairs(t) do
t[i] = t[i] + num
end
end
--- 设定玩家使用特定技能的历史次数。
---@param skill_name string @ 技能名
---@param num integer @ 次数
---@param scope integer @ 查询历史范围
function Player:setSkillUseHistory(skill_name, num, scope)
if skill_name == "" and num == nil and scope == nil then
self.skillUsedHistory = {}
return
end
num = num or 0
if skill_name == "" then
for _, v in pairs(self.skillUsedHistory) do
v[scope] = num
end
return
end
if self.skillUsedHistory[skill_name] then
self.skillUsedHistory[skill_name][scope] = num
end
end
--- 获取玩家使用特定牌的历史次数。
---@param cardName string @ 牌名
---@param scope integer @ 查询历史范围
function Player:usedCardTimes(cardName, scope)
if not self.cardUsedHistory[cardName] then
return 0
end
scope = scope or Player.HistoryTurn
return self.cardUsedHistory[cardName][scope]
end
--- 获取玩家使用特定技能的历史次数。
---@param skill_name string @ 技能名
---@param scope integer @ 查询历史范围
function Player:usedSkillTimes(cardName, scope)
if not self.skillUsedHistory[cardName] then
return 0
end
scope = scope or Player.HistoryTurn
return self.skillUsedHistory[cardName][scope]
end
--- 获取玩家是否无手牌。
function Player:isKongcheng()
return #self:getCardIds(Player.Hand) == 0
end
--- 获取玩家是否无手牌及装备牌。
function Player:isNude()
return #self:getCardIds{Player.Hand, Player.Equip} == 0
end
--- 获取玩家所有区域是否无牌。
function Player:isAllNude()
return #self:getCardIds() == 0
end
--- 获取玩家是否受伤。
function Player:isWounded()
return self.hp < self.maxHp
end
--- 获取玩家已失去体力。
function Player:getLostHp()
return math.min(self.maxHp - self.hp, self.maxHp)
end
---@param skill string | Skill
---@return Skill
local function getActualSkill(skill)
if type(skill) == "string" then
skill = Fk.skills[skill]
end
assert(skill:isInstanceOf(Skill))
return skill
end
--- 检索玩家是否有对应技能。
---@param skill string | Skill @ 技能名
---@param ignoreNullified boolean @ 忽略技能是否被无效
---@param ignoreAlive boolean @ 忽略角色在场与否
function Player:hasSkill(skill, ignoreNullified, ignoreAlive)
if not ignoreAlive and self.dead then
return false
end
skill = getActualSkill(skill)
if not (ignoreNullified or skill:isEffectable(self)) then
return false
end
if table.contains(self.player_skills, skill) then
return true
end
for _, v in pairs(self.derivative_skills) do
if table.contains(v, skill) then
return true
end
end
return false
end
--- 为玩家增加对应技能。
---@param skill string | Skill @ 技能名
---@param source_skill string | Skill | nil @ 本有技能(和衍生技能相对)
---@return Skill[] @ got skills that Player didn't have at start
function Player:addSkill(skill, source_skill)
skill = getActualSkill(skill)
local toget = {table.unpack(skill.related_skills)}
table.insert(toget, skill)
local room = Fk:currentRoom()
local ret = {}
for _, s in ipairs(toget) do
if not self:hasSkill(s, true, true) then
table.insert(ret, s)
if s:isInstanceOf(TriggerSkill) and RoomInstance then
room.logic:addTriggerSkill(s)
end
if s:isInstanceOf(StatusSkill) then
2023-03-18 07:34:42 +00:00
room.status_skills[s.class] = room.status_skills[s.class] or {}
table.insertIfNeed(room.status_skills[s.class], s)
end
end
end
if source_skill then
source_skill = getActualSkill(source_skill)
if not self.derivative_skills[source_skill] then
self.derivative_skills[source_skill] = {}
end
table.insertIfNeed(self.derivative_skills[source_skill], skill)
else
table.insertIfNeed(self.player_skills, skill)
end
-- add related skills
if not self.derivative_skills[skill] then
self.derivative_skills[skill] = {}
end
for _, s in ipairs(skill.related_skills) do
table.insertIfNeed(self.derivative_skills[skill], s)
end
return ret
end
--- 为玩家删除对应技能。
---@param skill string | Skill @ 技能名
---@param source_skill string | Skill | nil @ 本有技能(和衍生技能相对)
---@return Skill[] @ lost skills that the Player doesn't have anymore
function Player:loseSkill(skill, source_skill)
skill = getActualSkill(skill)
if source_skill then
source_skill = getActualSkill(source_skill)
if not self.derivative_skills[source_skill] then
self.derivative_skills[source_skill] = {}
end
table.removeOne(self.derivative_skills[source_skill], skill)
else
table.removeOne(self.player_skills, skill)
end
-- clear derivative skills of this skill as well
local tolose = self.derivative_skills[skill]
table.insert(tolose, skill)
self.derivative_skills[skill] = nil
local ret = {} ---@type Skill[]
for _, s in ipairs(tolose) do
if not self:hasSkill(s, true, true) then
table.insert(ret, s)
end
end
return ret
end
--- 获取对应玩家所有技能。
-- return all skills that xxx:hasSkill() == true
function Player:getAllSkills()
local ret = {table.unpack(self.player_skills)}
for _, t in pairs(self.derivative_skills) do
for _, s in ipairs(t) do
table.insertIfNeed(ret, s)
end
end
return ret
end
---@param to Player
---@param card Card
function Player:isProhibited(to, card)
local r = Fk:currentRoom()
local status_skills = r.status_skills[ProhibitSkill] or {}
for _, skill in ipairs(status_skills) do
if skill:isProhibited(self, to, card) then
return true
end
end
return false
end
---@param card Card
function Player:prohibitUse(card)
local status_skills = Fk:currentRoom().status_skills[ProhibitSkill] or {}
for _, skill in ipairs(status_skills) do
if skill:prohibitUse(self, card) then
return true
end
end
return false
end
---@param card Card
function Player:prohibitResponse(card)
local status_skills = Fk:currentRoom().status_skills[ProhibitSkill] or {}
for _, skill in ipairs(status_skills) do
if skill:prohibitResponse(self, card) then
return true
end
end
return false
end
---@param card Card
function Player:prohibitDiscard(card)
local status_skills = Fk:currentRoom().status_skills[ProhibitSkill] or {}
for _, skill in ipairs(status_skills) do
if skill:prohibitDiscard(self, card) then
return true
end
end
return false
end
return Player