---@class Player : Object ---@field id integer ---@field hp integer ---@field maxHp integer ---@field kingdom string ---@field role string ---@field general string ---@field handcard_num integer ---@field seat integer ---@field next Player ---@field phase Phase ---@field faceup boolean ---@field chained boolean ---@field dying boolean ---@field dead boolean ---@field state string ---@field player_skills Skill[] ---@field derivative_skills table ---@field flag string[] ---@field tag table ---@field mark table ---@field player_cards table ---@field special_cards table ---@field cardUsedHistory table ---@field fixedDistance table 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 function Player:initialize() self.id = 114514 self.hp = 0 self.maxHp = 0 self.kingdom = "qun" self.role = "" self.general = "" self.seat = 0 self.next = nil self.phase = Player.PhaseNone self.faceup = true self.chained = false self.dying = false self.dead = false self.state = "" self.player_skills = {} self.derivative_skills = {} self.flag = {} self.tag = {} self.mark = {} self.player_cards = { [Player.Hand] = {}, [Player.Equip] = {}, [Player.Judge] = {}, } self.special_cards = {} self.cardUsedHistory = {} self.fixedDistance = {} 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 ---@param flag string function Player:hasFlag(flag) return table.contains(self.flag, flag) end ---@param flag string function Player:setFlag(flag) 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 function Player:clearFlags() self.flag = {} end 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 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 function Player:setMark(mark, count) if self.mark[mark] ~= count then self.mark[mark] = count end end function Player:getMark(mark) return (self.mark[mark] or 0) end 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[] ---@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[] ---@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 ---@param playerAreas PlayerCardArea ---@param specialName string ---@return integer[] 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 cardSubtype CardSubtype ---@return integer|null 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) return baseValue end function Player:getAttackRange() local weapon = Fk:getCardById(self:getEquipment(Card.SubtypeWeapon)) local baseAttackRange = math.max(weapon and weapon.attack_range or 1, 0) 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) ret = ret + correct end if self.fixedDistance[other] then ret = self.fixedDistance[other] end return math.max(ret, 1) end ---@param other Player function Player:inMyAttackRange(other) return self ~= other and self:distanceTo(other) <= self:getAttackRange() end function Player:addCardUseHistory(cardName, num) assert(type(num) == "number" and num ~= 0) self.cardUsedHistory[cardName] = self.cardUsedHistory[cardName] or 0 self.cardUsedHistory[cardName] = self.cardUsedHistory[cardName] + num end function Player:resetCardUseHistory(cardName) if not cardName then self.cardUsedHistory = {} end if self.cardUsedHistory[cardName] then self.cardUsedHistory[cardName] = 0 end end function Player:usedTimes(cardName) return self.cardUsedHistory[cardName] or 0 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 ---@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 function Player:hasEquipSkill(skill) skill = getActualSkill(skill) local equips = self.player_cards[Player.Equip] for _, id in ipairs(equips) do local card = Fk:getCardById(id) if card.skill == skill then return true end end return false end ---@param skill string | Skill function Player:hasSkill(skill) skill = getActualSkill(skill) if self:hasEquipSkill(skill) then return true 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) then table.insert(ret, s) if skill:isInstanceOf(TriggerSkill) and RoomInstance then room.logic:addTriggerSkill(skill) end if table.contains(StatusSkills, skill.class) then room.status_skills[skill.class] = room.status_skills[skill.class] or {} table.insertIfNeed(room.status_skills[skill.class], skill) 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) then table.insert(ret, s) end end return ret end return Player