Enhancement (#263)

- smart_ai搭了个壳子
- askForPoxi
- 增加判断额外回合之法以及fix
- 修trigger
- 增加使用和打出的禁止技提示
- 修复卡牌标记,attach主动技显示为蓝色按钮
This commit is contained in:
notify 2023-09-19 14:27:54 +08:00 committed by GitHub
parent 048071a6d5
commit 203736e38e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 452 additions and 61 deletions

View File

@ -139,7 +139,7 @@ Item {
Text {
width: parent.width
horizontalAlignment: Text.AlignHCenter
text: Backend.translate("Room List")
text: Backend.translate("Room List").arg(roomModel.count)
}
ListView {
id: roomList

View File

@ -59,7 +59,7 @@ Item {
id: bgm
source: config.bgmFile
// loops: MediaPlayer.Infinite
loops: MediaPlayer.Infinite
onPlaybackStateChanged: {
if (playbackState == MediaPlayer.StoppedState && roomScene.isStarted)
play();

View File

@ -1070,6 +1070,31 @@ callbacks["AskForCardsChosen"] = (jsonData) => {
});
}
callbacks["AskForPoxi"] = (jsonData) => {
const { type, data } = JSON.parse(jsonData);
roomScene.state = "replying";
roomScene.popupBox.sourceComponent = Qt.createComponent("../RoomElement/PoxiBox.qml");
const box = roomScene.popupBox.item;
box.poxi_type = type;
box.card_data = data;
for (let d of data) {
const arr = [];
const ids = d[1];
ids.forEach(id => {
const card_data = JSON.parse(Backend.callLuaFunction("GetCardData", [id]));
arr.push(card_data);
});
box.addCustomCards(d[0], arr);
}
roomScene.popupBox.moveToCenter();
box.cardsSelected.connect((ids) => {
replyToServer(JSON.stringify(ids));
});
}
callbacks["AskForMoveCardInBoard"] = (jsonData) => {
const data = JSON.parse(jsonData);
const { cards, cardsPosition, generalNames, playerIds } = data;

View File

@ -62,7 +62,12 @@ Item {
}
if (mark_name.startsWith('@$')) {
params.cardNames = mark_extra.split(',');
let data = mark_extra.split(',');
if (!Object.is(parseInt(data[0]), NaN)) {
params.ids = data.map(s => parseInt(s));
} else {
params.cardNames = data;
}
} else {
let data = JSON.parse(Backend.callLuaFunction("GetPile", [root.parent.playerid, mark_name]));
data = data.filter((e) => e !== -1);

View File

@ -31,11 +31,13 @@ Item {
property string color: "" // only use when suit is empty
property string footnote: "" // footnote, e.g. "A use card to B"
property bool footnoteVisible: false
property string prohibitReason: ""
property bool known: true // if false it only show a card back
property bool enabled: true // if false the card will be grey
property alias card: cardItem
property alias glow: glowItem
property var mark: ({})
property alias chosenInBox: chosen.visible
function getColor() {
if (suit != "")
@ -88,6 +90,7 @@ Item {
visible: false
}
Image {
id: cardItem
source: known ? SkinBank.getCardPicture(cid || name)
@ -217,6 +220,15 @@ Item {
}
}
Image {
id: chosen
visible: false
source: SkinBank.CARD_DIR + "chosen"
anchors.horizontalCenter: parent.horizontalCenter
y: 90
scale: 1.25
}
Rectangle {
visible: !root.selectable
anchors.fill: parent
@ -224,6 +236,24 @@ Item {
opacity: 0.7
}
Text {
id: prohibitText
visible: !root.selectable
anchors.centerIn: parent
font.family: fontLibian.name
font.pixelSize: 18
opacity: 0.9
horizontalAlignment: Text.AlignHCenter
lineHeight: 18
lineHeightMode: Text.FixedHeight
color: "snow"
width: 20
wrapMode: Text.WrapAnywhere
style: Text.Outline
styleColor: "red"
text: prohibitReason
}
TapHandler {
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.NoButton
gesturePolicy: TapHandler.WithinBounds

View File

@ -166,17 +166,35 @@ RowLayout {
const ids = [];
let cards = handcardAreaItem.cards;
for (let i = 0; i < cards.length; i++) {
cards[i].prohibitReason = "";
if (cardValid(cards[i].cid, cname)) {
ids.push(cards[i].cid);
} else {
const prohibitReason = Backend.callLuaFunction(
"GetCardProhibitReason",
[cards[i].cid, roomScene.respond_play ? "response" : "use", cname]
);
if (prohibitReason) {
cards[i].prohibitReason = prohibitReason;
}
}
}
cards = self.equipArea.getAllCards();
cards.forEach(c => {
c.prohibitReason = "";
if (cardValid(c.cid, cname)) {
ids.push(c.cid);
if (!expanded_piles["_equip"]) {
expandPile("_equip");
}
} else {
const prohibitReason = Backend.callLuaFunction(
"GetCardProhibitReason",
[c.cid, roomScene.respond_play ? "response" : "use", cname]
);
if (prohibitReason) {
c.prohibitReason = prohibitReason;
}
}
});
@ -202,6 +220,7 @@ RowLayout {
const ids = [], cards = handcardAreaItem.cards;
for (let i = 0; i < cards.length; i++) {
cards[i].prohibitReason = "";
if (JSON.parse(Backend.callLuaFunction("CanUseCard", [cards[i].cid, Self.id]))) {
ids.push(cards[i].cid);
} else {
@ -214,6 +233,14 @@ RowLayout {
break;
}
}
// still cannot use? show message on card
if (!ids.includes(cards[i].cid)) {
const prohibitReason = Backend.callLuaFunction("GetCardProhibitReason", [cards[i].cid, "play"]);
if (prohibitReason) {
cards[i].prohibitReason = prohibitReason;
}
}
}
}
handcardAreaItem.enableCards(ids)
@ -372,6 +399,11 @@ RowLayout {
item.enabled = item.pressed;
}
const cards = handcardAreaItem.cards;
for (let i = 0; i < cards.length; i++) {
cards[i].prohibitReason = "";
}
updatePending();
}

View File

@ -48,6 +48,7 @@ Item {
card.selectable = false;
card.showDetail = false;
card.selectedChanged.disconnect(adjustCards);
card.prohibitReason = "";
}
return result;
}

View File

@ -78,10 +78,10 @@ GraphicsBox {
}
onSelectedChanged: {
if (selected) {
virt_name = "$Selected";
chosenInBox = true;
root.selected_ids.push(cid);
} else {
virt_name = "";
chosenInBox = false;
root.selected_ids.splice(root.selected_ids.indexOf(cid), 1);
}
root.selected_ids = root.selected_ids;
@ -122,38 +122,6 @@ GraphicsBox {
return ret;
}
function addHandcards(cards) {
let handcards = findAreaModel('$Hand').areaCards;
if (cards instanceof Array) {
for (let i = 0; i < cards.length; i++)
handcards.append(cards[i]);
} else {
handcards.append(cards);
}
}
function addEquips(cards)
{
let equips = findAreaModel('$Equip').areaCards;
if (cards instanceof Array) {
for (let i = 0; i < cards.length; i++)
equips.append(cards[i]);
} else {
equips.append(cards);
}
}
function addDelayedTricks(cards)
{
let delayedTricks = findAreaModel('$Judge').areaCards;
if (cards instanceof Array) {
for (let i = 0; i < cards.length; i++)
delayedTricks.append(cards[i]);
} else {
delayedTricks.append(cards);
}
}
function addCustomCards(name, cards) {
let area = findAreaModel(name).areaCards;
if (cards instanceof Array) {

138
Fk/RoomElement/PoxiBox.qml Normal file
View File

@ -0,0 +1,138 @@
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick
import QtQuick.Layouts
import Fk.Pages
GraphicsBox {
id: root
title.text: Backend.callLuaFunction("PoxiPrompt", [poxi_type, card_data])
// TODO: Adjust the UI design in case there are more than 7 cards
width: 70 + 700
height: 64 + Math.min(cardView.contentHeight, 400) + 20
signal cardSelected(int cid)
signal cardsSelected(var ids)
property var selected_ids: []
property string poxi_type
property var card_data
ListModel {
id: cardModel
}
ListView {
id: cardView
anchors.fill: parent
anchors.topMargin: 40
anchors.leftMargin: 20
anchors.rightMargin: 20
anchors.bottomMargin: 20
spacing: 20
model: cardModel
clip: true
delegate: RowLayout {
spacing: 15
visible: areaCards.count > 0
Rectangle {
border.color: "#A6967A"
radius: 5
color: "transparent"
width: 18
height: 130
Layout.alignment: Qt.AlignTop
Text {
color: "#E4D5A0"
text: Backend.translate(areaName)
anchors.fill: parent
wrapMode: Text.WrapAnywhere
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
font.pixelSize: 15
}
}
GridLayout {
columns: 7
Repeater {
model: areaCards
CardItem {
cid: model.cid
name: model.name || ""
suit: model.suit || ""
number: model.number || 0
autoBack: false
known: model.cid !== -1
selectable: {
return root.selected_ids.includes(model.cid) || JSON.parse(Backend.callLuaFunction(
"PoxiFilter",
[root.poxi_type, model.cid, root.selected_ids, root.card_data]
));
}
onSelectedChanged: {
if (selected) {
chosenInBox = true;
root.selected_ids.push(cid);
} else {
chosenInBox = false;
root.selected_ids.splice(root.selected_ids.indexOf(cid), 1);
}
root.selected_ids = root.selected_ids;
}
}
}
}
}
}
MetroButton {
anchors.bottom: parent.bottom
text: Backend.translate("OK")
enabled: {
return JSON.parse(Backend.callLuaFunction(
"PoxiFeasible",
[root.poxi_type, root.selected_ids, root.card_data]
));
}
onClicked: root.cardsSelected(root.selected_ids)
}
onCardSelected: finished();
function findAreaModel(name) {
let ret;
for (let i = 0; i < cardModel.count; i++) {
let item = cardModel.get(i);
if (item.areaName == name) {
ret = item;
break;
}
}
if (!ret) {
ret = {
areaName: name,
areaCards: [],
}
cardModel.append(ret);
ret = findAreaModel(name);
}
return ret;
}
function addCustomCards(name, cards) {
let area = findAreaModel(name).areaCards;
if (cards instanceof Array) {
for (let i = 0; i < cards.length; i++)
area.append(cards[i]);
} else {
area.append(cards);
}
}
}

View File

@ -23,9 +23,17 @@ Item {
x: -13 - 120 * 0.166
y: -6 - 55 * 0.166
scale: 0.66
source: type === "notactive" ? ""
: AppPath + "/image/button/skill/" + type + "/"
+ (enabled ? (pressed ? "pressed" : "normal") : "disabled")
source: {
if (type === "notactive") {
return "";
}
let ret = AppPath + "/image/button/skill/" + type + "/";
let suffix = enabled ? (pressed ? "pressed" : "normal") : "disabled";
if (enabled && type === "active" && orig.endsWith("&")) {
suffix += "-attach";
}
return ret + suffix;
}
}
Image {

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

BIN
image/card/chosen.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -684,4 +684,52 @@ function SaveRecord()
c.client:saveRecord(json.encode(c.record), c.record[2])
end
function GetCardProhibitReason(cid, method, pattern)
local card = Fk:getCardById(cid)
if not card then return "" end
if method == "play" and not card.skill:canUse(Self, card) then return "" end
if method ~= "play" and not card:matchPattern(pattern) then return "" end
if method == "play" then method = "use" end
local status_skills = Fk:currentRoom().status_skills[ProhibitSkill] or Util.DummyTable
local s
for _, skill in ipairs(status_skills) do
local fn = method == "use" and skill.prohibitUse or skill.prohibitResponse
if fn(skill, Self, card) then
s = skill
break
end
end
if not s then return "" end
-- try to return a translated string
local skillName = s.name
local ret = Fk:translate(skillName)
if ret ~= skillName then
-- TODO: translate
return ret .. "" .. (method == "use" and "使用" or "打出")
elseif skillName:endsWith("_prohibit") and skillName:startsWith("#") then
return Fk:translate(skillName:sub(2, -10)) .. "" .. (method == "use" and "使用" or "打出")
end
end
function PoxiPrompt(poxi_type, data)
local poxi = Fk.poxi_methods[poxi_type]
if not poxi or not poxi.prompt then return "" end
if type(poxi.prompt) == "string" then return Fk:translate(poxi.prompt) end
return poxi.prompt(data)
end
function PoxiFilter(poxi_type, to_select, selected, data)
local poxi = Fk.poxi_methods[poxi_type]
if not poxi then return "false" end
return json.encode(poxi.card_filter(to_select, selected, data))
end
function PoxiFeasible(poxi_type, selected, data)
local poxi = Fk.poxi_methods[poxi_type]
if not poxi then return "false" end
return json.encode(poxi.feasible(selected, data))
end
dofile "lua/client/i18n/init.lua"

View File

@ -2,7 +2,7 @@
Fk:loadTranslationTable({
-- Lobby
-- ["Room List"] = "房间列表",
["Room List"] = "Room List (currently have %1 rooms)",
-- ["Enter"] = "进入",
-- ["Observe"] = "旁观",

View File

@ -2,7 +2,7 @@
Fk:loadTranslationTable{
-- Lobby
["Room List"] = "房间列表",
["Room List"] = "房间列表 (共%1个房间)",
["Enter"] = "进入",
["Observe"] = "旁观",

View File

@ -25,6 +25,7 @@
---@field public filtered_cards table<integer, Card> @ 被锁视技影响的卡牌
---@field public printed_cards table<integer, Card> @ 被某些房间现场打印的卡牌id都是负数且从-2开始
---@field private _custom_events any[] @ 自定义事件列表
---@field public poxi_methods table<string, PoxiSpec> @ “魄袭”框操作方法表
local Engine = class("Engine")
--- Engine的构造函数。
@ -55,6 +56,7 @@ function Engine:initialize()
self.game_mode_disabled = {}
self.kingdoms = {}
self._custom_events = {}
self.poxi_methods = {}
self:loadPackages()
self:loadDisabled()
@ -334,6 +336,16 @@ function Engine:addGameEvent(name, pfunc, mfunc, cfunc, efunc)
table.insert(self._custom_events, { name = name, p = pfunc, m = mfunc, c = cfunc, e = efunc })
end
---@param spec PoxiSpec
function Engine:addPoxiMethod(spec)
assert(type(spec.name) == "string")
assert(type(spec.card_filter) == "function")
assert(type(spec.feasible) == "function")
self.poxi_methods[spec.name] = spec
spec.default_choice = spec.default_choice or function() return {} end
spec.post_select = spec.post_select or function(s) return s end
end
--- 从已经开启的拓展包中,随机选出若干名武将。
---
--- 对于同名武将不会重复选取。

View File

@ -588,3 +588,13 @@ function fk.CreateGameMode(spec)
end
return ret
end
-- other
---@class PoxiSpec
---@field name string
---@field card_filter fun(to_select: int, selected: int[], data: any): bool
---@field feasible fun(selected: int[], data: any): bool
---@field post_select nil | fun(selected: int[], data: any): int[]
---@field default_choice nil | fun(data: any): int[]
---@field prompt nil | string | fun(data: any): string

View File

@ -7,6 +7,7 @@
---@alias null nil
---@alias bool boolean | nil
---@alias int integer
---@class fk
---FreeKill's lua API

View File

@ -3,3 +3,25 @@
AI = require "server.ai.ai"
TrustAI = require "server.ai.trust_ai"
RandomAI = require "server.ai.random_ai"
SmartAI = require "server.ai.smart_ai"
-- load ai module from packages
local directories = FileIO.ls("packages")
require "packages.standard.ai"
require "packages.standard_cards.ai"
require "packages.maneuvering.ai"
table.removeOne(directories, "standard")
table.removeOne(directories, "standard_cards")
table.removeOne(directories, "maneuvering")
local _disable_packs = json.decode(fk.GetDisabledPacks())
for _, dir in ipairs(directories) do
if (not string.find(dir, ".disabled")) and not table.contains(_disable_packs, dir)
and FileIO.isDir("packages/" .. dir)
and FileIO.exists("packages/" .. dir .. "/ai/init.lua") then
require(string.format("packages.%s.ai", dir))
end
end

View File

@ -0,0 +1,10 @@
-- SPDX-License-Identifier: GPL-3.0-or-later
---@class SmartAI: AI
local SmartAI = AI:subclass("RandomAI")
function SmartAI:initialize(player)
AI.initialize(self, player)
end
return SmartAI

View File

@ -73,6 +73,13 @@ function GameEvent:addExitFunc(f)
table.insert(self.extra_exit_funcs, f)
end
function GameEvent:prependExitFunc(f)
if self.extra_exit_funcs == Util.DummyTable then
self.extra_exit_funcs = {}
end
table.insert(self.extra_exit_funcs, 1, f)
end
function GameEvent:findParent(eventType, includeSelf)
if includeSelf and self.event == eventType then return self end
local e = self.parent

View File

@ -376,14 +376,14 @@ function GameLogic:trigger(event, target, data, refresh_only)
end
repeat do
local triggerables = table.filter(skills, function(skill)
local invoked_skills = {}
local filter_func = function(skill)
return skill.priority_table[event] == prio and
not table.contains(invoked_skills, skill) and
skill:triggerable(event, target, player, data)
end)
end
local skill_names = table.map(triggerables, function(skill)
return skill.name
end)
local skill_names = table.map(table.filter(skills, filter_func), Util.NameMapper)
while #skill_names > 0 do
local skill_name = prio <= 0 and table.random(skill_names) or
@ -392,23 +392,14 @@ function GameLogic:trigger(event, target, data, refresh_only)
local skill = skill_name == "game_rule" and GameRule
or Fk.skills[skill_name]
local len = #skills
table.insert(invoked_skills, skill)
broken = skill:trigger(event, target, player, data)
table.insertTable(
skill_names,
table.map(table.filter(table.slice(skills, len - #skills), function(s)
return
s.priority_table[event] == prio and
s:triggerable(event, target, player, data)
end), function(s) return s.name end)
)
skill_names = table.map(table.filter(skills, filter_func), Util.NameMapper)
broken = broken or (event == fk.AskForPeaches
and room:getPlayerById(data.who).hp > 0)
if broken then break end
table.removeOne(skill_names, skill_name)
end
if broken then break end
@ -428,6 +419,18 @@ function GameLogic:getCurrentEvent()
return self.game_event_stack.t[self.game_event_stack.p]
end
--- 如果当前事件刚好是技能生效事件,就返回那个技能名,否则返回空串。
function GameLogic:getCurrentSkillName()
local skillEvent = self:getCurrentEvent()
local ret = ""
if skillEvent.event == GameEvent.SkillEffect then
local _, _, _skill = table.unpack(skillEvent.data)
local skill = _skill.main_skill and _skill.main_skill or _skill
ret = skill.name
end
return ret
end
-- 在指定历史范围中找至多n个符合条件的事件
---@param eventType integer @ 要查找的事件类型
---@param n integer @ 最多找多少个

View File

@ -1457,6 +1457,33 @@ function Room:askForCardsChosen(chooser, target, min, max, flag, reason)
return new_ret
end
--- 谋askForCardsChosen需使用Fk:addPoxiMethod定义好方法
---
--- 选卡规则和返回值啥的全部自己想办法解决data填入所有卡的列表类似ui.card_data
---
--- 注意一定要返回一个表,毕竟本质上是选卡函数
---@param player ServerPlayer
---@param poxi_type string
---@param data any
---@return integer[]
function Room:askForPoxi(player, poxi_type, data)
local poxi = Fk.poxi_methods[poxi_type]
if not poxi then return {} end
local command = "AskForPoxi"
self:notifyMoveFocus(player, poxi_type)
local result = self:doRequest(player, command, json.encode {
type = poxi_type,
data = data,
})
if result == "" then
return poxi.default_choice(data)
else
return poxi.post_select(json.decode(result), data)
end
end
--- 询问一名玩家从众多选项中选择一个。
---@param player ServerPlayer @ 要询问的玩家
---@param choices string[] @ 可选选项列表

View File

@ -469,7 +469,7 @@ function ServerPlayer:gainAnExtraPhase(phase, delay)
local logic = room.logic
local turn = logic:getCurrentEvent():findParent(GameEvent.Phase, true)
if turn then
turn:addExitFunc(function() self:gainAnExtraPhase(phase, false) end)
turn:prependExitFunc(function() self:gainAnExtraPhase(phase, false) end)
return
end
end
@ -484,7 +484,6 @@ function ServerPlayer:gainAnExtraPhase(phase, delay)
arg = phase_name_table[phase],
}
GameEvent(GameEvent.Phase, self, self.phase):exec()
self.phase = current
@ -580,6 +579,7 @@ function ServerPlayer:skip(phase)
end
end
--- 当进行到出牌阶段空闲点时,结束出牌阶段。
function ServerPlayer:endPlayPhase()
self._play_phase_end = true
-- TODO: send log
@ -592,7 +592,7 @@ function ServerPlayer:gainAnExtraTurn(delay)
local logic = room.logic
local turn = logic:getCurrentEvent():findParent(GameEvent.Turn, true)
if turn then
turn:addExitFunc(function() self:gainAnExtraTurn(false) end)
turn:prependExitFunc(function() self:gainAnExtraTurn(false) end)
return
end
end
@ -604,10 +604,32 @@ function ServerPlayer:gainAnExtraTurn(delay)
local current = room.current
room.current = self
self.tag["_extra_turn_count"] = self.tag["_extra_turn_count"] or {}
local ex_tag = self.tag["_extra_turn_count"]
local skillName = room.logic:getCurrentSkillName()
table.insert(ex_tag, skillName)
GameEvent(GameEvent.Turn, self):exec()
table.remove(ex_tag)
room.current = current
end
function ServerPlayer:insideExtraTurn()
return self.tag["_extra_turn_count"] and #self.tag["_extra_turn_count"] > 0
end
---@return string
function ServerPlayer:getCurrentExtraTurnReason()
local ex_tag = self.tag["_extra_turn_count"]
if (not ex_tag) or #ex_tag == 0 then
return "game_rule"
end
return ex_tag[#ex_tag]
end
function ServerPlayer:drawCards(num, skillName, fromPlace)
return self.room:drawCards(self, num, skillName, fromPlace)
end

View File

View File

View File

View File

@ -90,6 +90,12 @@ local control = fk.CreateActiveSkill{
-- room:swapSeat(from, to)
for _, pid in ipairs(effect.tos) do
local to = room:getPlayerById(pid)
-- p(room:askForPoxi(from, "test", {
-- { "你自己", from:getCardIds "h" },
-- { "对方", to:getCardIds "h" },
-- }))
-- room:setPlayerMark(from, "@$a", {1,2,3})
-- room:setPlayerMark(from, "@$b", {'slash','duel','axe'})
if to:getMark("mouxushengcontrolled") == 0 then
room:addPlayerMark(to, "mouxushengcontrolled")
from:control(to)
@ -124,6 +130,22 @@ local control = fk.CreateActiveSkill{
-- room:useVirtualCard("slash", nil, from, room:getOtherPlayers(from), self.name, true)
end,
}
--[[
Fk:addPoxiMethod{
name = "test",
card_filter = function(to_select, selected, data)
local s = Fk:getCardById(to_select).suit
for _, id in ipairs(selected) do
if Fk:getCardById(id).suit == s then return false end
end
return true
end,
feasible = function(selected, data)
return #selected == 0 or #selected == 4
end,
prompt = "魄袭:选你们俩手牌总共四个花色,或者不选直接按确定按钮"
}
--]]
local test_vs = fk.CreateViewAsSkill{
name = "test_vs",
pattern = "nullification",