diff --git a/lua/core/card.lua b/lua/core/card.lua index 51653d57..68a27b0f 100644 --- a/lua/core/card.lua +++ b/lua/core/card.lua @@ -6,6 +6,8 @@ ---@field color Color ---@field id integer ---@field type CardType +---@field sub_type CardSubtype +---@field area CardArea local Card = class("Card") ---@alias Suit integer @@ -29,6 +31,28 @@ Card.TypeBasic = 2 Card.TypeTrick = 3 Card.TypeEquip = 4 +---@alias CardSubtype integer + +Card.SubtypeNone = 1 +Card.SubtypeDelayedTrick = 2 +Card.SubtypeWeapon = 3 +Card.SubtypeArmor = 4 +Card.SubtypeDefensiveRide = 5 +Card.SubtypeOffensiveRide = 6 +Card.SubtypeTreasure = 7 + +---@alias CardArea integer + +Card.Unknown = 0 +Card.PlayerHand = 1 +Card.PlayerEquip = 2 +Card.PlayerJudge = 3 +Card.PlayerSpecial = 4 +Card.Processing = 5 +Card.DrawPile = 6 +Card.DiscardPile = 7 +Card.Void = 8 + function Card:initialize(name, suit, number, color) self.name = name self.suit = suit or Card.NoSuit @@ -47,6 +71,7 @@ function Card:initialize(name, suit, number, color) self.package = nil self.id = 0 self.type = 0 + self.sub_type = Card.SubTypeNone end return Card diff --git a/lua/core/card_type/basic.lua b/lua/core/card_type/basic.lua index 87dd0952..06f0ca48 100644 --- a/lua/core/card_type/basic.lua +++ b/lua/core/card_type/basic.lua @@ -6,4 +6,12 @@ function BasicCard:initialize(name, suit, number) self.type = Card.TypeBasic end +---@param suit Suit +---@param number integer +---@return BasicCard +function BasicCard:clone(suit, number) + local newCard = BasicCard:new(self.name, suit, number) + return newCard +end + return BasicCard diff --git a/lua/core/card_type/equip.lua b/lua/core/card_type/equip.lua index d971ddd4..9ac303b2 100644 --- a/lua/core/card_type/equip.lua +++ b/lua/core/card_type/equip.lua @@ -6,4 +6,85 @@ function EquipCard:initialize(name, suit, number) self.type = Card.TypeEquip end -return EquipCard +---@class Weapon : EquipCard +local Weapon = EquipCard:subclass("Weapon") + +function Weapon:initialize(name, suit, number, attackRange) + EquipCard.initialize(self, name, suit, number) + self.sub_type = Card.SubtypeWeapon + self.attack_range = attackRange or 1 +end + +---@param suit Suit +---@param number integer +---@return Weapon +function Weapon:clone(suit, number) + local newCard = Weapon:new(self.name, suit, number, self.attack_range) + return newCard +end + +---@class Armor : EquipCard +local Armor = EquipCard:subclass("armor") + +function Armor:initialize(name, suit, number) + EquipCard.initialize(self, name, suit, number) + self.sub_type = Card.SubtypeArmor +end + +---@param suit Suit +---@param number integer +---@return Armor +function Armor:clone(suit, number) + local newCard = Armor:new(self.name, suit, number) + return newCard +end + +---@class DefensiveRide : EquipCard +local DefensiveRide = EquipCard:subclass("DefensiveRide") + +function DefensiveRide:initialize(name, suit, number) + EquipCard.initialize(self, name, suit, number) + self.sub_type = Card.SubtypeDefensiveRide +end + +---@param suit Suit +---@param number integer +---@return DefensiveRide +function DefensiveRide:clone(suit, number) + local newCard = DefensiveRide:new(self.name, suit, number) + return newCard +end + +---@class OffensiveRide : EquipCard +local OffensiveRide = EquipCard:subclass("OffensiveRide") + +function OffensiveRide:initialize(name, suit, number) + EquipCard.initialize(self, name, suit, number) + self.sub_type = Card.SubtypeOffensiveRide +end + +---@param suit Suit +---@param number integer +---@return OffensiveRide +function OffensiveRide:clone(suit, number) + local newCard = OffensiveRide:new(self.name, suit, number) + return newCard +end + +---@class Treasure : EquipCard +local Treasure = EquipCard:subclass("Treasure") + +function Treasure:initialize(name, suit, number) + EquipCard.initialize(self, name, suit, number) + self.sub_type = Card.SubtypeTreasure +end + +---@param suit Suit +---@param number integer +---@return Treasure +function Treasure:clone(suit, number) + local newCard = Treasure:new(self.name, suit, number) + return newCard +end + +return { EquipCard, Weapon, Armor, DefensiveRide, OffensiveRide, Treasure } diff --git a/lua/core/card_type/trick.lua b/lua/core/card_type/trick.lua index ce91a5fd..13106541 100644 --- a/lua/core/card_type/trick.lua +++ b/lua/core/card_type/trick.lua @@ -6,4 +6,28 @@ function TrickCard:initialize(name, suit, number) self.type = Card.TypeTrick end -return TrickCard +---@param suit Suit +---@param number integer +---@return TrickCard +function TrickCard:clone(suit, number) + local newCard = TrickCard:new(self.name, suit, number) + return newCard +end + +---@class DelayedTrickCard : TrickCard +local DelayedTrickCard = TrickCard:subclass("DelayedTrickCard") + +function DelayedTrickCard:initialize(name, suit, number) + TrickCard.initialize(self, name, suit, number) + self.sub_type = Card.SubtypeDelayedTrick +end + +---@param suit Suit +---@param number integer +---@return DelayedTrickCard +function DelayedTrickCard:clone(suit, number) + local newCard = DelayedTrickCard:new(self.name, suit, number) + return newCard +end + +return { TrickCard, DelayedTrickCard } diff --git a/lua/core/engine.lua b/lua/core/engine.lua index e7cbe4d5..d81457c4 100644 --- a/lua/core/engine.lua +++ b/lua/core/engine.lua @@ -164,4 +164,23 @@ function Engine:getAllGenerals(except) return result end +---@param except integer[] +---@return integer[] +function Engine:getAllCardIds(except) + local result = {} + for _, card in ipairs(self.cards) do + if not (except and table.contains(except, card.id)) then + table.insert(result, card.id) + end + end + + return result +end + +---@param id integer +---@return Card +function Engine:getCardById(id) + return self.cards[id] +end + return Engine diff --git a/lua/core/player.lua b/lua/core/player.lua index 422623b6..6e35c55d 100644 --- a/lua/core/player.lua +++ b/lua/core/player.lua @@ -17,6 +17,8 @@ ---@field flag string[] ---@field tag table ---@field mark table +---@field player_cards table +---@field special_cards table local Player = class("Player") ---@alias Phase integer @@ -31,6 +33,13 @@ 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 @@ -38,7 +47,6 @@ function Player:initialize() self.kingdom = "qun" self.role = "" self.general = "" - self.handcard_num = 0 self.seat = 0 self.phase = Player.PhaseNone self.faceup = true @@ -51,6 +59,12 @@ function Player:initialize() self.flag = {} self.tag = {} self.mark = {} + self.player_cards = { + [Player.Hand] = {}, + [Player.Equip] = {}, + [Player.Judge] = {}, + } + self.special_cards = {} end ---@param general General @@ -125,4 +139,86 @@ function Player:getMarkNames() 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 + +function Player:getMaxCards() + local baseValue = math.max(self.hp, 0) + + return baseValue +end + +---@param subtype CardSubtype +---@return integer|null +function Player:getEquipBySubtype(subtype) + local equipId = nil + for _, id in ipairs(self.player_cards[Player.Equip]) do + if Fk.getCardById(id).sub_type == subtype then + equipId = id + break + end + end + + return equipId +end + +function Player:getAttackRange() + local weapon = Fk.getCardById(self:getEquipBySubtype(Card.SubtypeWeapon)) + local baseAttackRange = math.max(weapon and weapon.attack_range or 1, 0) + + return math.max(baseAttackRange, 0) +end + return Player diff --git a/lua/fk_ex.lua b/lua/fk_ex.lua index 8de23dd7..bce0141d 100644 --- a/lua/fk_ex.lua +++ b/lua/fk_ex.lua @@ -2,10 +2,13 @@ SkillCard = require "core.card_type.skill" BasicCard = require "core.card_type.basic" -TrickCard = require "core.card_type.trick" -EquipCard = require "core.card_type.equip" +local Trick = require "core.card_type.trick" +TrickCard, DelayedTrickCard = table.unpack(Trick) +local Equip = require "core.card_type.equip" +_, Weapon, Armor, DefensiveRide, OffensiveRide, Treasure = table.unpack(Equip) dofile "lua/server/event.lua" +dofile "lua/server/system_enum.lua" TriggerSkill = require "core.skill_type.trigger" ---@class CardSpec: Card @@ -27,10 +30,10 @@ TriggerSkill = require "core.skill_type.trigger" ---@return BasicCard function fk.CreateBasicCard(spec) assert(type(spec.name) == "string" or type(spec.class_name) == "string") - if not spec.name then spec.name = spec.class_name - elseif not spec.class_name then spec.class_name = spec.name end - if spec.suit then assert(type(spec.suit) == "number") end - if spec.number then assert(type(spec.number) == "number") end + if not spec.name then spec.name = spec.class_name + elseif not spec.class_name then spec.class_name = spec.name end + if spec.suit then assert(type(spec.suit) == "number") end + if spec.number then assert(type(spec.number) == "number") end local card = BasicCard:new(spec.name, spec.suit, spec.number) return card @@ -40,25 +43,91 @@ end ---@return TrickCard function fk.CreateTrickCard(spec) assert(type(spec.name) == "string" or type(spec.class_name) == "string") - if not spec.name then spec.name = spec.class_name - elseif not spec.class_name then spec.class_name = spec.name end - if spec.suit then assert(type(spec.suit) == "number") end - if spec.number then assert(type(spec.number) == "number") end + if not spec.name then spec.name = spec.class_name + elseif not spec.class_name then spec.class_name = spec.name end + if spec.suit then assert(type(spec.suit) == "number") end + if spec.number then assert(type(spec.number) == "number") end local card = TrickCard:new(spec.name, spec.suit, spec.number) return card end ---@param spec CardSpec ----@return EquipCard -function fk.CreateEquipCard(spec) - assert(type(spec.name) == "string" or type(spec.class_name) == "string") - if not spec.name then spec.name = spec.class_name - elseif not spec.class_name then spec.class_name = spec.name end - if spec.suit then assert(type(spec.suit) == "number") end - if spec.number then assert(type(spec.number) == "number") end +---@return DelayedTrickCard +function fk.CreateDelayedTrickCard(spec) + assert(type(spec.name) == "string" or type(spec.class_name) == "string") + if not spec.name then spec.name = spec.class_name + elseif not spec.class_name then spec.class_name = spec.name end + if spec.suit then assert(type(spec.suit) == "number") end + if spec.number then assert(type(spec.number) == "number") end - local card = EquipCard:new(spec.name, spec.suit, spec.number) + local card = DelayedTrickCard:new(spec.name, spec.suit, spec.number) + return card +end + +---@param spec CardSpec +---@return Weapon +function fk.CreateWeapon(spec) + assert(type(spec.name) == "string" or type(spec.class_name) == "string") + if not spec.name then spec.name = spec.class_name + elseif not spec.class_name then spec.class_name = spec.name end + if spec.suit then assert(type(spec.suit) == "number") end + if spec.number then assert(type(spec.number) == "number") end + if spec.attack_range then assert(type(spec.attack_range) == "number" and spec.attack_range >= 0) end + + local card = Weapon:new(spec.name, spec.suit, spec.number, spec.attack_range) + return card +end + +---@param spec CardSpec +---@return Armor +function fk.CreateArmor(spec) + assert(type(spec.name) == "string" or type(spec.class_name) == "string") + if not spec.name then spec.name = spec.class_name + elseif not spec.class_name then spec.class_name = spec.name end + if spec.suit then assert(type(spec.suit) == "number") end + if spec.number then assert(type(spec.number) == "number") end + + local card = Armor:new(spec.name, spec.suit, spec.number) + return card +end + +---@param spec CardSpec +---@return DefensiveRide +function fk.CreateDefensiveRide(spec) + assert(type(spec.name) == "string" or type(spec.class_name) == "string") + if not spec.name then spec.name = spec.class_name + elseif not spec.class_name then spec.class_name = spec.name end + if spec.suit then assert(type(spec.suit) == "number") end + if spec.number then assert(type(spec.number) == "number") end + + local card = DefensiveRide:new(spec.name, spec.suit, spec.number) + return card +end + +---@param spec CardSpec +---@return OffensiveRide +function fk.CreateOffensiveRide(spec) + assert(type(spec.name) == "string" or type(spec.class_name) == "string") + if not spec.name then spec.name = spec.class_name + elseif not spec.class_name then spec.class_name = spec.name end + if spec.suit then assert(type(spec.suit) == "number") end + if spec.number then assert(type(spec.number) == "number") end + + local card = OffensiveRide:new(spec.name, spec.suit, spec.number) + return card +end + +---@param spec CardSpec +---@return Treasure +function fk.CreateTreasure(spec) + assert(type(spec.name) == "string" or type(spec.class_name) == "string") + if not spec.name then spec.name = spec.class_name + elseif not spec.class_name then spec.class_name = spec.name end + if spec.suit then assert(type(spec.suit) == "number") end + if spec.number then assert(type(spec.number) == "number") end + + local card = Treasure:new(spec.name, spec.suit, spec.number) return card end diff --git a/lua/server/event.lua b/lua/server/event.lua index 43ed0bc4..3ed1abb5 100644 --- a/lua/server/event.lua +++ b/lua/server/event.lua @@ -9,30 +9,45 @@ fk.EventPhaseEnd = 6 fk.EventPhaseChanging = 7 fk.EventPhaseSkipping = 8 -fk.DrawNCards = 9 -fk.AfterDrawNCards = 10 -fk.DrawInitialCards = 11 -fk.AfterDrawInitialCards = 12 +fk.BeforeCardsMove = 9 +fk.AfterCardsMove = 10 -fk.PreHpRecover = 13 -fk.HpRecover = 14 -fk.PreHpLost = 15 -fk.HpLost = 16 -fk.HpChanged = 17 -fk.MaxHpChanged = 18 +fk.DrawNCards = 11 +fk.AfterDrawNCards = 12 +fk.DrawInitialCards = 13 +fk.AfterDrawInitialCards = 14 -fk.EventLoseSkill = 19 -fk.EventAcquireSkill = 20 +fk.PreHpRecover = 15 +fk.HpRecover = 16 +fk.PreHpLost = 17 +fk.HpLost = 18 +fk.BeforeHpChanged = 19 +fk.HpChanged = 20 +fk.MaxHpChanged = 21 -fk.StartJudge = 21 -fk.AskForRetrial = 22 -fk.FinishRetrial = 23 -fk.FinishJudge = 24 +fk.EventLoseSkill = 22 +fk.EventAcquireSkill = 23 -fk.PindianVerifying = 25 -fk.Pindian = 26 +fk.StartJudge = 24 +fk.AskForRetrial = 25 +fk.FinishRetrial = 26 +fk.FinishJudge = 27 -fk.TurnedOver = 27 -fk.ChainStateChanged = 28 +fk.PindianVerifying = 28 +fk.Pindian = 29 -fk.NumOfEvents = 29 +fk.TurnedOver = 30 +fk.ChainStateChanged = 31 + +fk.PreDamage = 32 +fk.DamageCaused = 33 +fk.DamageInflicted = 34 +fk.Damage = 35 +fk.Damaged = 36 +fk.DamageFinished = 37 + +fk.EnterDying = 38 +fk.Dying = 39 +fk.AfterDying = 40 + +fk.NumOfEvents = 41 diff --git a/lua/server/gamelogic.lua b/lua/server/gamelogic.lua index 2986e0b9..a5c86d1d 100644 --- a/lua/server/gamelogic.lua +++ b/lua/server/gamelogic.lua @@ -64,7 +64,7 @@ function GameLogic:chooseGenerals() local lord = room:getLord() local lord_general = nil if lord ~= nil then - room.current = lord + room.current = lord local generals = Fk:getGeneralsRandomly(3) for i = 1, #generals do generals[i] = generals[i].name @@ -129,6 +129,12 @@ function GameLogic:prepareForStart() -- TODO: prepare drawPile -- TODO: init cards in drawPile + local allCardIds = Fk:getAllCardIds() + table.shuffle(allCardIds) + room.draw_pile = allCardIds + for _, id in ipairs(room.draw_pile) do + self.room:setCardArea(id, Card.DrawPile) + end self:addTriggerSkill(GameRule) for _, trig in ipairs(Fk.global_trigger) do @@ -139,6 +145,11 @@ end function GameLogic:action() self:trigger(fk.GameStart) local room = self.room + + for _, p in ipairs(room.players) do + self:trigger(fk.DrawInitialCards, p, { num = 4 }) + end + while true do self:trigger(fk.TurnStart, room.current) if room.game_finished then break end diff --git a/lua/server/room.lua b/lua/server/room.lua index 3c1e76f4..2431c2da 100644 --- a/lua/server/room.lua +++ b/lua/server/room.lua @@ -6,6 +6,11 @@ ---@field game_finished boolean ---@field timeout integer ---@field tag table +---@field draw_pile integer[] +---@field discard_pile integer[] +---@field processing_area integer[] +---@field void integer[] +---@field card_place table local Room = class("Room") -- load classes used by the game @@ -36,6 +41,11 @@ function Room:initialize(_room) self.game_finished = false self.timeout = _room:getTimeout() self.tag = {} + self.draw_pile = {} + self.discard_pile = {} + self.processing_area = {} + self.void = {} + self.card_place = {} end -- When this function returns, the Room(C++) thread stopped. @@ -161,7 +171,244 @@ function Room:adjustSeats() self:doBroadcastNotify("ArrangeSeats", json.encode(player_circle)) end ----@return ServerPlayer | nil +function Room:shuffleDrawPile() + if #self.draw_pile + #self.discard_pile == 0 then + return + end + + table.insertTable(self.draw_pile, self.discard_pile) + for _, id in ipairs(self.discard_pile) do + self:setCardArea(id, Card.DrawPile) + end + self.discard_pile = {} + table.shuffle(self.draw_pile) +end + +---@param num integer +---@param from string +---@return integer[] +function Room:getNCards(num, from) + from = from or "top" + assert(from == "top" or from == "bottom") + + local cardIds = {} + while num > 0 do + if #self.draw_pile < 1 then + self:shuffleDrawPile() + end + + local index = from == "top" and 1 or #self.draw_pile + table.insert(cardIds, self.draw_pile[index]) + table.remove(self.draw_pile, index) + + num = num - 1 + end + + return cardIds +end + +---@param cardId integer +---@param cardArea CardArea +function Room:setCardArea(cardId, cardArea) + self.card_place[cardId] = cardArea +end + +---@param cardId integer +---@return CardArea +function Room:getCardArea(cardId) + return self.card_place[cardId] or Card.Unknown +end + +---@vararg CardsMoveInfo +---@return boolean +function Room:moveCards(...) + ---@type CardsMoveStruct[] + local cardsMoveStructs = {} + local infoCheck = function(info) + assert(table.contains({ Card.PlayerHand, Card.PlayerEquip, Card.PlayerJudge, Card.PlayerSpecial, Card.Processing, Card.DrawPile, Card.DiscardPile, Card.Void }, info.toArea)) + assert(info.toArea ~= Card.PlayerSpecial or type(info.specialName) == "string") + assert(type(info.moveReason) == "number") + end + + for _, cardsMoveInfo in ipairs({...}) do + if #cardsMoveInfo.ids > 0 then + infoCheck(cardsMoveInfo) + + ---@type MoveInfo[] + local infos = {} + for _, id in ipairs(cardsMoveInfo.ids) do + table.insert(infos, { cardId = id, fromArea = self:getCardArea(id) }) + end + + ---@type CardsMoveStruct + local cardsMoveStruct = { + moveInfo = infos, + from = cardsMoveInfo.from, + to = cardsMoveInfo.to, + toArea = cardsMoveInfo.toArea, + moveReason = cardsMoveInfo.moveReason, + proposer = cardsMoveInfo.proposer, + skillName = cardsMoveInfo.skillName, + moveVisible = cardsMoveInfo.moveVisible, + specialName = cardsMoveInfo.specialName, + specialVisible = cardsMoveInfo.specialVisible, + } + + table.insert(cardsMoveStructs, cardsMoveStruct) + end + end + + if #cardsMoveStructs < 1 then + return false + end + + if self.logic:trigger(fk.BeforeCardsMove, nil, cardsMoveStructs) then + return false + end + + for _, data in ipairs(cardsMoveStructs) do + if #data.moveInfo > 0 then + infoCheck(data) + + ---@param info MoveInfo + for _, info in ipairs(data.moveInfo) do + local realFromArea = self:getCardArea(info.cardId) + local playerAreas = { Player.Hand, Player.Equip, Player.Judge, Player.Special } + + if table.contains(playerAreas, realFromArea) and data.from then + self:getPlayerById(data.from):removeCards(realFromArea, { info.cardId }, data.specialName) + elseif realFromArea ~= Card.Unknown then + local fromAreaIds = {} + if realFromArea == Card.Processing then + fromAreaIds = self.processing_area + elseif realFromArea == Card.DrawPile then + fromAreaIds = self.draw_pile + elseif realFromArea == Card.DiscardPile then + fromAreaIds = self.discard_pile + elseif realFromArea == Card.Void then + fromAreaIds = self.void + end + + table.removeOne(fromAreaIds, info.cardId) + end + + if table.contains(playerAreas, data.toArea) and data.to then + self:getPlayerById(data.to):addCards(data.toArea, { info.cardId }, data.specialName) + else + local toAreaIds = {} + if data.toArea == Card.Processing then + toAreaIds = self.processing_area + elseif data.toArea == Card.DrawPile then + toAreaIds = self.draw_pile + elseif data.toArea == Card.DiscardPile then + toAreaIds = self.discard_pile + elseif data.toArea == Card.Void then + toAreaIds = self.void + end + + table.insert(toAreaIds, toAreaIds == Card.DrawPile and 1 or #toAreaIds + 1, info.cardId) + end + self:setCardArea(info.cardId, data.toArea) + end + end + end + + self.logic:trigger(fk.AfterCardsMove, nil, cardsMoveStructs) + return true +end + +---@param player ServerPlayer +---@param num integer +---@param skillName string +---@param fromPlace "top"|"bottom" +---@return integer[] +function Room:drawCards(player, num, skillName, fromPlace) + local topCards = self:getNCards(num, fromPlace) + self:moveCards({ + ids = topCards, + to = player:getId(), + toArea = Card.PlayerHand, + moveReason = fk.ReasonDraw, + proposer = player:getId(), + skillName = skillName, + }) + + return { table.unpack(topCards) } +end + +---@param player ServerPlayer +---@param minNum integer +---@param maxNum integer +---@param includeEquip boolean +---@param skillName string +function Room:askForDiscard(player, minNum, maxNum, includeEquip, skillName) + if minNum < 1 then + return nil + end + + local hands = player:getCardIds(Player.Hand) + local toDiscard = {} + for i = 1, minNum do + local randomId = hands[math.random(1, #hands)] + table.insert(toDiscard, randomId) + table.removeOne(hands, randomId) + end + + self:moveCards({ + ids = toDiscard, + from = player:getId(), + toArea = Card.DiscardPile, + moveReason = fk.ReasonDiscard, + proposer = player:getId(), + skillName = skillName + }) +end + +---@param id integer +---@return ServerPlayer +function Room:getPlayerById(id) + assert(type(id) == "number") + + for _, p in ipairs(self.players) do + if p:getId() == id then + return p + end + end + + error("cannot find player by " .. id) +end + +---@param sortBySeat boolean +---@return ServerPlayer[] +function Room:getAlivePlayers(sortBySeat) + sortBySeat = sortBySeat or true + + local alivePlayers = {} + for _, player in ipairs(self.players) do + if player:isAlive() then + table.insert(alivePlayers, player) + end + end + + return alivePlayers +end + +---@param player ServerPlayer +---@param sortBySeat boolean +---@return ServerPlayer[] +function Room:getOtherPlayers(player, sortBySeat) + local alivePlayers = self:getAlivePlayers(sortBySeat) + for _, p in ipairs(alivePlayers) do + if p:getId() == player:getId() then + table.removeOne(alivePlayers, player) + break + end + end + + return alivePlayers +end + +---@return ServerPlayer | null function Room:getLord() local lord = self.players[1] if lord.role == "lord" then return lord end @@ -210,16 +457,6 @@ function Room:gameOver() self.room:gameOver() end ----@param id integer -function Room:findPlayerById(id) - for _, p in ipairs(self.players) do - if p:getId() == id then - return p - end - end - return nil -end - ---@param player ServerPlayer ---@param choices string[] ---@param skill_name string @@ -246,6 +483,203 @@ function Room:askForSkillInvoke(player, skill_name, data) return invoked end +---@param player ServerPlayer +---@param num integer +---@param reason "loseHp"|"damage"|"recover"|null +---@param skillName string +---@param damageStruct DamageStruct|null +---@return boolean +function Room:changeHp(player, num, reason, skillName, damageStruct) + if num == 0 then + return false + end + assert(reason == nil or table.contains({ "loseHp", "damage", "recover" }, reason)) + + ---@type HpChangedData + local data = { + num = num, + reason = reason, + skillName = skillName, + } + + if self.logic:trigger(fk.BeforeHpChanged, player, data) then + return false + end + + assert(not (data.reason == "recover" and data.num < 0)) + player.hp = math.min(player.hp + data.num, player.maxHp) + + self.logic:trigger(fk.HpChanged, player, data) + + if player.hp < 1 then + ---@type DyingStruct + local dyingStruct = { + who = player:getId(), + damage = damageStruct, + } + self:enterDying(dyingStruct) + elseif player.dying then + player.dying = false + end + + return true +end + +---@param player ServerPlayer +---@param num integer +---@param skillName string +---@return boolean +function Room:loseHp(player, num, skillName) + if num == nil then + num = 1 + elseif num < 1 then + return false + end + + ---@type HpLostData + local data = { + num = num, + skillName = skillName, + } + if self.logic:trigger(fk.PreHpLost, player, data) or data.num < 1 then + return false + end + + if not self:changeHp(player, -num, "loseHp", skillName) then + return false + end + + self.logic:trigger(fk.HpLost, player, data) + return true +end + +---@param player ServerPlayer +---@param num integer +---@return boolean +function Room:changeMaxHp(player, num) + if num == 0 then + return false + end + + player.maxHp = math.max(player.maxHp + num, 0) + local diff = player.hp - player.maxHp + if diff > 0 then + if not self:changeHp(player, -diff) then + player.hp = player.hp - diff + end + end + + if player.maxHp == 0 then + self:killPlayer({ who = player:getId() }) + end + + self.logic:trigger(fk.MaxHpChanged, player, { num = num }) + return true +end + +---@param damageStruct DamageStruct +---@return boolean +function Room:damage(damageStruct) + if damageStruct.damage < 1 then + return false + end + + assert(type(damageStruct.to) == "number") + + local stages = { + [fk.PreDamage] = damageStruct.from, + [fk.DamageCaused] = damageStruct.from, + [fk.DamageInflicted] = damageStruct.to, + } + + for event, playerId in ipairs(stages) do + local player = playerId and self:getPlayerById(playerId) or nil + if self.logic:trigger(event, player, damageStruct) or damageStruct.damage < 1 then + return false + end + + assert(type(damageStruct.to) == "number") + end + + assert(self:getPlayerById(damageStruct.to)) + local victim = self:getPlayerById(damageStruct.to) + if not victim:isAlive() then + return false + end + + if not self:changeHp(victim, -damageStruct.damage, "damage", damageStruct.skillName, damageStruct) then + return false + end + + stages = { + [fk.Damage] = damageStruct.from, + [fk.Damaged] = damageStruct.to, + [fk.DamageFinished] = damageStruct.from, + } + + for event, playerId in ipairs(stages) do + local player = playerId and self:getPlayerById(playerId) or nil + self.logic:trigger(event, player, damageStruct) + end + + return true +end + +---@param recoverStruct RecoverStruct +---@return boolean +function Room:recover(recoverStruct) + if recoverStruct.num < 1 then + return false + end + + local who = self:getPlayerById(recoverStruct.who) + if self.logic:trigger(fk.PreHpRecover, who, recoverStruct) or recoverStruct.num < 1 then + return false + end + + if not self:changeHp(who, recoverStruct.num, "recover", recoverStruct.skillName) then + return false + end + + self.logic:trigger(fk.HpRecover, who, recoverStruct) + return true +end + +---@param dyingStruct DyingStruct +function Room:enterDying(dyingStruct) + local dyingPlayer = self:getPlayerById(dyingStruct.who) + dyingPlayer.dying = true + self.logic:trigger(fk.EnterDying, dyingPlayer, dyingStruct) + + if dyingPlayer.hp < 1 then + local alivePlayers = self:getAlivePlayers() + for _, player in ipairs(alivePlayers) do + self.logic:trigger(fk.Dying, player, dyingStruct) + + if player.hp > 0 then + break + end + end + + if dyingPlayer.hp < 1 then + ---@type DeathStruct + local deathData = { + who = dyingPlayer:getId(), + damage = dyingStruct.damage, + } + self:killPlayer(deathData) + end + end + + self.logic:trigger(fk.AfterDying, dyingPlayer, dyingStruct) +end + +---@param deathStruct DeathStruct +function Room:killPlayer(deathStruct) + print(self:getPlayerById(deathStruct.who).general .. " is dead") + self:gameOver() +end + fk.room_callback["QuitRoom"] = function(jsonData) -- jsonData: [ int uid ] local data = json.decode(jsonData) @@ -289,7 +723,7 @@ fk.room_callback["PlayerStateChanged"] = function(jsonData) local data = json.decode(jsonData) local id = data[1] local stateString = data[2] - RoomInstance:findPlayerById(id).state = stateString + RoomInstance:getPlayerById(id).state = stateString end fk.room_callback["RoomDeleted"] = function(jsonData) diff --git a/lua/server/system_enum.lua b/lua/server/system_enum.lua new file mode 100644 index 00000000..39628169 --- /dev/null +++ b/lua/server/system_enum.lua @@ -0,0 +1,29 @@ +---@alias CardsMoveInfo {ids: integer[], from: integer|null, to: integer|null, toArea: CardArea, moveReason: CardMoveReason, proposer: integer, skillName: string|null, moveVisible: boolean|null, specialName: string|null, specialVisible: boolean|null } +---@alias MoveInfo {cardId: integer, fromArea: CardArea} +---@alias CardsMoveStruct {moveInfo: {id: integer, fromArea: CardArea}[], from: integer|null, to: integer|null, toArea: CardArea, moveReason: CardMoveReason, proposer: integer|null, skillName: string|null, moveVisible: boolean|null, specialName: string|null, specialVisible: boolean|null, fromSpecialName: string|null } + +---@alias HpChangedData { num: integer, reason: string, skillName: string } +---@alias HpLostData { num: integer, skillName: string } +---@alias DamageStruct { from: integer|null, to: integer, damage: integer, damageType: DamageType, skillName: string } +---@alias RecoverStruct { who: integer, num: integer, recoverBy: integer|null, skillName: string|null } + +---@alias DyingStruct { who: integer, damage: DamageStruct } +---@alias DeathStruct { who: integer, damage: DamageStruct } + + +---@alias MoveReason integer + +fk.ReasonJustMove = 1 +fk.ReasonDraw = 2 +fk.ReasonDiscard = 3 +fk.ReasonGive = 4 +fk.ReasonPut = 5 +fk.ReasonPutIntoDiscardPile = 6 +fk.ReasonPrey = 7 +fk.ReasonExchange = 8 + +---@alias DamageType integer + +fk.NormalDamage = 1 +fk.ThunderDamage = 2 +fk.FireDamage = 3 diff --git a/packages/standard/game_rule.lua b/packages/standard/game_rule.lua index 508fe04c..efc99486 100644 --- a/packages/standard/game_rule.lua +++ b/packages/standard/game_rule.lua @@ -1,7 +1,7 @@ GameRule = fk.CreateTriggerSkill{ name = "game_rule", events = { - fk.GameStart, fk.TurnStart, + fk.GameStart, fk.DrawInitialCards, fk.TurnStart, fk.EventPhaseProceeding, fk.EventPhaseEnd, fk.EventPhaseChanging, }, priority = 0, @@ -26,6 +26,19 @@ GameRule = fk.CreateTriggerSkill{ local room = player.room switch(event, { + [fk.DrawInitialCards] = function() + if data.num > 0 then + -- TODO: need a new function to call the UI + local cardIds = room:getNCards(data.num) + player:addCards(Player.Hand, cardIds) + + for _, id in ipairs(cardIds) do + room:setCardArea(id, Card.PlayerHand) + end + + room.logic:trigger(fk.AfterDrawInitialCards, player, data) + end + end, [fk.TurnStart] = function() player = room.current if room.tag["FirstRound"] == true then @@ -59,6 +72,7 @@ GameRule = fk.CreateTriggerSkill{ end, [Player.Draw] = function() print("Proceeding Draw.") + room:drawCards(player, 2, self.name) end, [Player.Play] = function() print("Proceeding Play.") @@ -66,6 +80,10 @@ GameRule = fk.CreateTriggerSkill{ end, [Player.Discard] = function() print("Proceeding Discard.") + local discardNum = #player:getCardIds(Player.Hand) - player:getMaxCards() + if discardNum > 0 then + room:askForDiscard(player, discardNum, discardNum, false, self.name) + end end, [Player.Finish] = function() print("Proceeding Finish.") diff --git a/packages/standard_cards/init.lua b/packages/standard_cards/init.lua index b3faa8e7..82174c91 100644 --- a/packages/standard_cards/init.lua +++ b/packages/standard_cards/init.lua @@ -3,6 +3,504 @@ extension.metadata = require "packages.standard_cards.metadata" local slash = fk.CreateBasicCard{ name = "slash", + number = 7, + suit = Card.Spade, +} +Fk:loadTranslationTable{ + ["slash"] = "杀", } +extension:addCards({ + slash, + slash:clone(Card.Spade, 8), + slash:clone(Card.Spade, 8), + slash:clone(Card.Spade, 9), + slash:clone(Card.Spade, 9), + slash:clone(Card.Spade, 10), + slash:clone(Card.Spade, 10), + + slash:clone(Card.Club, 2), + slash:clone(Card.Club, 3), + slash:clone(Card.Club, 4), + slash:clone(Card.Club, 5), + slash:clone(Card.Club, 6), + slash:clone(Card.Club, 7), + slash:clone(Card.Club, 8), + slash:clone(Card.Club, 8), + slash:clone(Card.Club, 9), + slash:clone(Card.Club, 9), + slash:clone(Card.Club, 10), + slash:clone(Card.Club, 10), + slash:clone(Card.Club, 11), + slash:clone(Card.Club, 11), + + slash:clone(Card.Heart, 10), + slash:clone(Card.Heart, 10), + slash:clone(Card.Heart, 11), + + slash:clone(Card.Diamond, 6), + slash:clone(Card.Diamond, 7), + slash:clone(Card.Diamond, 8), + slash:clone(Card.Diamond, 9), + slash:clone(Card.Diamond, 10), + slash:clone(Card.Diamond, 13), +}) + +local jink = fk.CreateBasicCard{ + name = "jink", + suit = Card.Heart, + number = 2, +} +Fk:loadTranslationTable{ + ["jink"] = "闪", +} + +extension:addCards({ + jink, + jink:clone(Card.Heart, 2), + jink:clone(Card.Heart, 13), + + jink:clone(Card.Diamond, 2), + jink:clone(Card.Diamond, 2), + jink:clone(Card.Diamond, 3), + jink:clone(Card.Diamond, 4), + jink:clone(Card.Diamond, 5), + jink:clone(Card.Diamond, 6), + jink:clone(Card.Diamond, 7), + jink:clone(Card.Diamond, 8), + jink:clone(Card.Diamond, 9), + jink:clone(Card.Diamond, 10), + jink:clone(Card.Diamond, 11), + jink:clone(Card.Diamond, 11), +}) + +local peach = fk.CreateBasicCard{ + name = "peach", + suit = Card.Heart, + number = 3, +} +Fk:loadTranslationTable{ + ["peach"] = "桃", +} + +extension:addCards({ + peach, + peach:clone(Card.Heart, 4), + peach:clone(Card.Heart, 6), + peach:clone(Card.Heart, 7), + peach:clone(Card.Heart, 8), + peach:clone(Card.Heart, 9), + peach:clone(Card.Heart, 12), + peach:clone(Card.Heart, 12), +}) + +local dismantlement = fk.CreateTrickCard{ + name = "dismantlement", + suit = Card.Spade, + number = 3, +} +Fk:loadTranslationTable{ + ["dismantlement"] = "过河拆桥", +} + +extension:addCards({ + dismantlement, + dismantlement:clone(Card.Spade, 4), + dismantlement:clone(Card.Spade, 12), + + dismantlement:clone(Card.Club, 3), + dismantlement:clone(Card.Club, 4), + + dismantlement:clone(Card.Heart, 12), +}) + +local snatch = fk.CreateTrickCard{ + name = "snatch", + suit = Card.Spade, + number = 3, +} +Fk:loadTranslationTable{ + ["snatch"] = "顺手牵羊", +} + +extension:addCards({ + snatch, + snatch:clone(Card.Spade, 4), + snatch:clone(Card.Spade, 11), + + snatch:clone(Card.Diamond, 3), + snatch:clone(Card.Diamond, 4), +}) + +local duel = fk.CreateTrickCard{ + name = "duel", + suit = Card.Spade, + number = 1, +} +Fk:loadTranslationTable{ + ["duel"] = "决斗", +} + +extension:addCards({ + duel, + + duel:clone(Card.Club, 1), + + duel:clone(Card.Diamond, 1), +}) + +local collateral = fk.CreateTrickCard{ + name = "collateral", + suit = Card.Club, + number = 12, +} +Fk:loadTranslationTable{ + ["collateral"] = "借刀杀人", +} + +extension:addCards({ + collateral, + collateral:clone(Card.Club, 13), +}) + +local exNihilo = fk.CreateTrickCard{ + name = "ex_nihilo", + suit = Card.Heart, + number = 7, +} +Fk:loadTranslationTable{ + ["ex_nihilo"] = "无中生有", +} + +extension:addCards({ + exNihilo, + exNihilo:clone(Card.Heart, 8), + exNihilo:clone(Card.Heart, 9), + exNihilo:clone(Card.Heart, 11), +}) + +local nullification = fk.CreateTrickCard{ + name = "nullification", + suit = Card.Spade, + number = 11, +} +Fk:loadTranslationTable{ + ["nullification"] = "无懈可击", +} + +extension:addCards({ + nullification, + + nullification:clone(Card.Club, 12), + nullification:clone(Card.Club, 13), + + nullification:clone(Card.Diamond, 12), +}) + +local savageAssault = fk.CreateTrickCard{ + name = "savage_assault", + suit = Card.Spade, + number = 7, +} +Fk:loadTranslationTable{ + ["savage_assault"] = "南蛮入侵", +} + +extension:addCards({ + savageAssault, + savageAssault:clone(Card.Spade, 13), + savageAssault:clone(Card.Club, 7), +}) + +local archeryAttack = fk.CreateTrickCard{ + name = "archery_attack", + suit = Card.Heart, + number = 1, +} +Fk:loadTranslationTable{ + ["archery_attack"] = "万箭齐发", +} + +extension:addCards({ + archeryAttack, +}) + +local godSalvation = fk.CreateTrickCard{ + name = "god_salvation", + suit = Card.Heart, + number = 1, +} +Fk:loadTranslationTable{ + ["god_salvation"] = "桃园结义", +} + +extension:addCards({ + godSalvation, +}) + +local amazingGrace = fk.CreateTrickCard{ + name = "amazing_grace", + suit = Card.Heart, + number = 3, +} +Fk:loadTranslationTable{ + ["amazing_grace"] = "五谷丰登", +} + +extension:addCards({ + amazingGrace, + amazingGrace:clone(Card.Heart, 4), +}) + +local lightning = fk.CreateDelayedTrickCard{ + name = "lightning", + suit = Card.Spade, + number = 1, +} +Fk:loadTranslationTable{ + ["lightning"] = "闪电", +} + +extension:addCards({ + lightning, + lightning:clone(Card.Heart, 12), +}) + +local indulgence = fk.CreateDelayedTrickCard{ + name = "indulgence", + suit = Card.Spade, + number = 6, +} +Fk:loadTranslationTable{ + ["indulgence"] = "乐不思蜀", +} + +extension:addCards({ + indulgence, + indulgence:clone(Card.Club, 6), +}) + +local crossbow = fk.CreateWeapon{ + name = "crossbow", + suit = Card.Club, + number = 1, +} +Fk:loadTranslationTable{ + ["crossbow"] = "诸葛连弩", +} + +extension:addCards({ + crossbow, + crossbow:clone(Card.Diamond, 1), +}) + +local qingGang = fk.CreateWeapon{ + name = "qinggang_sword", + suit = Card.Spade, + number = 6, +} +Fk:loadTranslationTable{ + ["qinggang_sword"] = "青釭剑", +} + +extension:addCards({ + qingGang, +}) + +local iceSword = fk.CreateWeapon{ + name = "ice_sword", + suit = Card.Spade, + number = 2, +} +Fk:loadTranslationTable{ + ["ice_sword"] = "寒冰剑", +} + +extension:addCards({ + iceSword, +}) + +local doubleSwords = fk.CreateWeapon{ + name = "double_swords", + suit = Card.Spade, + number = 2, +} +Fk:loadTranslationTable{ + ["double_swords"] = "雌雄双股剑", +} + +extension:addCards({ + doubleSwords, +}) + +local blade = fk.CreateWeapon{ + name = "blade", + suit = Card.Spade, + number = 5, +} +Fk:loadTranslationTable{ + ["blade"] = "青龙偃月刀", +} + +extension:addCards({ + blade, +}) + +local spear = fk.CreateWeapon{ + name = "spear", + suit = Card.Spade, + number = 12, +} +Fk:loadTranslationTable{ + ["spear"] = "丈八蛇矛", +} + +extension:addCards({ + spear, +}) + +local axe = fk.CreateWeapon{ + name = "axe", + suit = Card.Diamond, + number = 5, +} +Fk:loadTranslationTable{ + ["axe"] = "贯石斧", +} + +extension:addCards({ + axe, +}) + +local halberd = fk.CreateWeapon{ + name = "halberd", + suit = Card.Diamond, + number = 12, +} +Fk:loadTranslationTable{ + ["halberd"] = "方天画戟", +} + +extension:addCards({ + halberd, +}) + +local kylinBow = fk.CreateWeapon{ + name = "kylin_bow", + suit = Card.Heart, + number = 5, +} +Fk:loadTranslationTable{ + ["kylin_bow"] = "麒麟弓", +} + +extension:addCards({ + kylinBow, +}) + +local eightDiagram = fk.CreateArmor{ + name = "eight_diagram", + suit = Card.Spade, + number = 2, +} +Fk:loadTranslationTable{ + ["eight_diagram"] = "八卦阵", +} + +extension:addCards({ + eightDiagram, + eightDiagram:clone(Card.Club, 2), +}) + +local niohShield = fk.CreateArmor{ + name = "nioh_shield", + suit = Card.Club, + number = 2, +} +Fk:loadTranslationTable{ + ["nioh_shield"] = "仁王盾", +} + +extension:addCards({ + niohShield, +}) + +local diLu = fk.CreateDefensiveRide{ + name = "dilu", + suit = Card.Club, + number = 5, +} +Fk:loadTranslationTable{ + ["dilu"] = "的卢", +} + +extension:addCards({ + diLu, +}) + +local jueYing = fk.CreateDefensiveRide{ + name = "jueying", + suit = Card.Spade, + number = 5, +} +Fk:loadTranslationTable{ + ["jueying"] = "绝影", +} + +extension:addCards({ + jueYing, +}) + +local zhuaHuangFeiDian = fk.CreateDefensiveRide{ + name = "zhuahuangfeidian", + suit = Card.Heart, + number = 13, +} +Fk:loadTranslationTable{ + ["zhuahuangfeidian"] = "爪黄飞电", +} + +extension:addCards({ + zhuaHuangFeiDian, +}) + +local chiTu = fk.CreateOffensiveRide{ + name = "chitu", + suit = Card.Heart, + number = 5, +} +Fk:loadTranslationTable{ + ["chitu"] = "赤兔", +} + +extension:addCards({ + chiTu, +}) + +local daYuan = fk.CreateOffensiveRide{ + name = "dayuan", + suit = Card.Spade, + number = 13, +} +Fk:loadTranslationTable{ + ["dayuan"] = "大宛", +} + +extension:addCards({ + daYuan, +}) + +local ziXing = fk.CreateOffensiveRide{ + name = "zixing", + suit = Card.Heart, + number = 5, +} +Fk:loadTranslationTable{ + ["zixing"] = "紫骍", +} + +extension:addCards({ + ziXing, +}) + return extension