Use skill (#25)

* distance & snatch

* distance & snatch - clean

* fix: client.alive_players

* room:askForCardChosen(todo: snatch)

* add skill to players(uncomplete)

* ui of skill btn

* expand pile(not completed)

* Use card2 (#23)

* snatch (todo: owner_map)

* owner_map

* remove too many snatch

* update

* call active skill's functions

* use zhiheng

* Qt6 (#24)

* use qt6

* android compiling

* remove version number of qml import

* correct anti-sql injection; update deprecated code

* add fkparse as submodule

* link fkparse, and write simple functions

* adjust ui

* adjust layout for photos; fix qml warning

* android problem fix (partially)

* move ico

* update copy_assets

* update logic for ok/cancel btn
This commit is contained in:
notify 2022-09-14 13:01:10 +08:00 committed by GitHub
parent 7da5fcfa2c
commit 162b3af505
68 changed files with 1441 additions and 191 deletions

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "fkparse"]
path = fkparse
url = git@github.com:Notify-ctrl/fkparse

View File

@ -2,7 +2,10 @@ cmake_minimum_required(VERSION 3.16)
project(FreeKill VERSION 0.0.1)
find_package(Qt5 REQUIRED COMPONENTS
include_directories(fkparse/src)
add_subdirectory(fkparse)
find_package(Qt6 REQUIRED COMPONENTS
Gui
Qml
Network
@ -15,7 +18,7 @@ find_package(SQLite3)
set(CMAKE_AUTOMOC ON)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED True)
set(REQUIRED_QT_VERSION "5.15.2")
set(REQUIRED_QT_VERSION "6.3")
include_directories(include/lua)
include_directories(include/sqlite3)
@ -26,4 +29,15 @@ include_directories(src/network)
include_directories(src/server)
include_directories(src/ui)
file(GLOB SWIG_FILES "${PROJECT_SOURCE_DIR}/src/swig/*.i")
add_custom_command(
OUTPUT ${PROJECT_SOURCE_DIR}/src/swig/freekill-wrap.cxx
DEPENDS ${SWIG_FILES}
COMMENT "Generating freekill-wrap.cxx"
COMMAND swig -c++ -lua -Wall -o
${PROJECT_SOURCE_DIR}/src/swig/freekill-wrap.cxx
${PROJECT_SOURCE_DIR}/src/swig/freekill.i
)
qt_add_executable(FreeKill)
add_subdirectory(src)

2
android/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
assets/
res/

View File

@ -0,0 +1,51 @@
<?xml version="1.0"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.notify.FreeKill"
android:installLocation="auto"
android:versionCode="1"
android:versionName="1.0">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<supports-screens
android:anyDensity="true"
android:largeScreens="true"
android:normalScreens="true"
android:smallScreens="true" />
<application
android:name="org.qtproject.qt.android.bindings.QtApplication"
android:hardwareAccelerated="true"
android:label="FreeKill"
android:icon="@mipmap/icon"
android:requestLegacyExternalStorage="true"
android:allowNativeHeapPointerTagging="false"
android:allowBackup="true"
android:fullBackupOnly="false">
<activity
android:name="org.qtproject.qt.android.bindings.QtActivity"
android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation|mcc|mnc|density"
android:label="FreeKill"
android:launchMode="singleTop"
android:screenOrientation="sensorLandscape"
android:theme="@android:style/Theme.NoTitleBar.Fullscreen"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data
android:name="android.app.lib_name"
android:value="FreeKill" />
<meta-data
android:name="android.app.arguments"
android:value="" />
<meta-data
android:name="android.app.extract_android_style"
android:value="minimal" />
</activity>
</application>
</manifest>

45
android/copy_assets.sh Executable file
View File

@ -0,0 +1,45 @@
#!/bin/sh
if [ ! -e res/mipmap ]; then
mkdir -p res/mipmap
fi
cp ../image/icon.png res/mipmap
if [ ! -e assets/res ]; then
mkdir -p assets/res
fi
cp -r ../fonts assets/res
cp -r ../image assets/res
cp -r ../lua assets/res
cp -r ../packages assets/res
cp -r ../qml assets/res
cp -r ../server assets/res
rm assets/res/server/users.db
cp ../LICENSE assets/res
# Due to Qt Android's bug, we need make sure every directory has a subfile (not subdir)
function fixDir() {
cd $1
hasSubfile=false
for f in $(ls); do
if [ -f $f ]; then
hasSubfile=true
break
fi
done
if ! $hasSubfile; then
echo "辣鸡Qt" > bug.txt
fi
for f in $(ls); do
if [ -d $f ]; then
fixDir $f
fi
done
cd ..
}
fixDir assets/res

1
fkparse Submodule

@ -0,0 +1 @@
Subproject commit bbf45faf7dd67fca4bedf535c14a8037990a7399

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

BIN
image/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
lib/android/liblua54.so Normal file

Binary file not shown.

BIN
lib/android/libsqlite3.so Normal file

Binary file not shown.

View File

@ -1,6 +1,9 @@
---@class Client
---@field client fk.Client
---@field players ClientPlayer[]
---@field alive_players ClientPlayer[]
---@field current ClientPlayer
---@field discard_pile integer[]
Client = class('Client')
-- load client classes
@ -23,17 +26,42 @@ function Client:initialize()
end
self.players = {} -- ClientPlayer[]
self.alive_players = {}
self.discard_pile = {}
end
---@param id integer
---@return ClientPlayer
function Client:findPlayer(id)
function Client:getPlayerById(id)
for _, p in ipairs(self.players) do
if p.player:getId() == id then return p end
if p.id == id then return p end
end
return nil
end
function Client:moveCards(moves)
for _, move in ipairs(moves) do
if move.from and move.fromArea then
local from = self:getPlayerById(move.from)
if from.id ~= Self.id and move.fromArea == Card.PlayerHand then
for i = 1, #move.ids do
table.remove(from.player_cards[Player.Hand])
end
else
from:removeCards(move.fromArea, move.ids)
end
elseif move.fromArea == Card.DiscardPile then
table.removeOne(self.discard_pile, move.ids[1])
end
if move.to and move.toArea then
self:getPlayerById(move.to):addCards(move.toArea, move.ids)
elseif move.toArea == Card.DiscardPile then
table.insert(self.discard_pile, move.ids[1])
end
end
end
fk.client_callback["Setup"] = function(jsonData)
-- jsonData: [ int id, string screenName, string avatar ]
local data = json.decode(jsonData)
@ -47,6 +75,8 @@ end
fk.client_callback["EnterRoom"] = function(jsonData)
ClientInstance.players = {Self}
ClientInstance.alive_players = {Self}
ClientInstance.discard_pile = {}
ClientInstance:notifyUI("EnterRoom", jsonData)
end
@ -56,7 +86,9 @@ fk.client_callback["AddPlayer"] = function(jsonData)
local data = json.decode(jsonData)
local id, name, avatar = data[1], data[2], data[3]
local player = fk.ClientInstance:addPlayer(id, name, avatar)
table.insert(ClientInstance.players, ClientPlayer:new(player))
local p = ClientPlayer:new(player)
table.insert(ClientInstance.players, p)
table.insert(ClientInstance.alive_players, p)
ClientInstance:notifyUI("AddPlayer", jsonData)
end
@ -67,6 +99,7 @@ fk.client_callback["RemovePlayer"] = function(jsonData)
for _, p in ipairs(ClientInstance.players) do
if p.player:getId() == id then
table.removeOne(ClientInstance.players, p)
table.removeOne(ClientInstance.alive_players, p)
break
end
end
@ -80,7 +113,9 @@ fk.client_callback["ArrangeSeats"] = function(jsonData)
local players = {}
for i = 1, n do
table.insert(players, ClientInstance:findPlayer(data[i]))
local p = ClientInstance:getPlayerById(data[i])
p.seat = i
table.insert(players, p)
end
ClientInstance.players = players
@ -91,10 +126,31 @@ fk.client_callback["PropertyUpdate"] = function(jsonData)
-- jsonData: [ int id, string property_name, value ]
local data = json.decode(jsonData)
local id, name, value = data[1], data[2], data[3]
ClientInstance:findPlayer(id)[name] = value
ClientInstance:getPlayerById(id)[name] = value
ClientInstance:notifyUI("PropertyUpdate", jsonData)
end
fk.client_callback["AskForCardChosen"] = function(jsonData)
-- jsonData: [ int target_id, string flag, int reason ]
local data = json.decode(jsonData)
local id, flag, reason = data[1], data[2], data[3]
local target = ClientInstance:getPlayerById(id)
local hand = target.player_cards[Player.Hand]
local equip = target.player_cards[Player.Equip]
local judge = target.player_cards[Player.Judge]
if not string.find(flag, "h") then
hand = {}
end
if not string.find(flag, "e") then
equip = {}
end
if not string.find(flag, "j") then
judge = {}
end
local ui_data = {hand, equip, judge, reason}
ClientInstance:notifyUI("AskForCardChosen", json.encode(ui_data))
end
--- separated moves to many moves(one card per move)
---@param moves CardsMoveStruct[]
local function separateMoves(moves)
@ -113,13 +169,15 @@ local function separateMoves(moves)
return ret
end
--- merge separated moves (one fromArea per move)
--- merge separated moves that information is the same
local function mergeMoves(moves)
local ret = {}
local temp = {}
for _, move in ipairs(moves) do
if temp[move.fromArea] == nil then
temp[move.fromArea] = {
local info = string.format("%q,%q,%q,%q",
move.from, move.to, move.fromArea, move.toArea)
if temp[info] == nil then
temp[info] = {
ids = {},
from = move.from,
to = move.to,
@ -127,7 +185,7 @@ local function mergeMoves(moves)
toArea = move.toArea
}
end
table.insert(temp[move.fromArea].ids, move.ids[1])
table.insert(temp[info].ids, move.ids[1])
end
for _, v in pairs(temp) do
table.insert(ret, v)
@ -139,10 +197,35 @@ fk.client_callback["MoveCards"] = function(jsonData)
-- jsonData: CardsMoveStruct[]
local raw_moves = json.decode(jsonData)
local separated = separateMoves(raw_moves)
ClientInstance:moveCards(separated)
local merged = mergeMoves(separated)
ClientInstance:notifyUI("MoveCards", json.encode(merged))
end
fk.client_callback["LoseSkill"] = function(jsonData)
-- jsonData: [ int player_id, string skill_name ]
local data = json.decode(jsonData)
local id, skill_name = data[1], data[2]
local target = ClientInstance:getPlayerById(id)
local skill = Fk.skills[skill_name]
target:loseSkill(skill)
if skill.visible then
ClientInstance:notifyUI("LoseSkill", jsonData)
end
end
fk.client_callback["AddSkill"] = function(jsonData)
-- jsonData: [ int player_id, string skill_name ]
local data = json.decode(jsonData)
local id, skill_name = data[1], data[2]
local target = ClientInstance:getPlayerById(id)
local skill = Fk.skills[skill_name]
target:addSkill(skill)
if skill.visible then
ClientInstance:notifyUI("AddSkill", jsonData)
end
end
-- Create ClientInstance (used by Lua)
ClientInstance = Client:new()
dofile "lua/client/client_util.lua"

View File

@ -87,7 +87,7 @@ function CanUseCard(card, player)
error()
end
local ret = c.skill:canUse(ClientInstance:findPlayer(player))
local ret = c.skill:canUse(ClientInstance:getPlayerById(player))
return json.encode(ret)
end
@ -102,7 +102,8 @@ function CanUseCardToTarget(card, to_select, selected)
c = Fk:getCardById(card)
selected_cards = {card}
else
error()
local t = json.decode(card)
return ActiveTargetFilter(t.skill, to_select, selected, t.subcards)
end
local ret = c.skill:targetFilter(to_select, selected, selected_cards)
@ -137,13 +138,70 @@ function CardFeasible(card, selected_targets)
c = Fk:getCardById(card)
selected_cards = {card}
else
error()
local t = json.decode(card)
return ActiveFeasible(t.skill, selected_targets, t.subcards)
end
local ret = c.skill:feasible(selected_cards, selected_targets)
local ret = c.skill:feasible(selected_targets, selected_cards)
return json.encode(ret)
end
-- Handle skills
function GetSkillData(skill_name)
local skill = Fk.skills[skill_name]
local freq = "notactive"
if skill:isInstanceOf(ActiveSkill) then
freq = "active"
end
return json.encode{
skill = Fk:translate(skill_name),
orig_skill = skill_name,
freq = freq
}
end
function ActiveCanUse(skill_name)
local skill = Fk.skills[skill_name]
local ret = false
if skill and skill:isInstanceOf(ActiveSkill) then
ret = skill:canUse(Self)
end
return json.encode(ret)
end
function ActiveCardFilter(skill_name, to_select, selected, selected_targets)
local skill = Fk.skills[skill_name]
local ret = false
if skill and skill:isInstanceOf(ActiveSkill) then
ret = skill:cardFilter(to_select, selected, selected_targets)
end
return json.encode(ret)
end
function ActiveTargetFilter(skill_name, to_select, selected, selected_cards)
local skill = Fk.skills[skill_name]
local ret = false
if skill and skill:isInstanceOf(ActiveSkill) then
ret = skill:targetFilter(to_select, selected, selected_cards)
end
return json.encode(ret)
end
function ActiveFeasible(skill_name, selected, selected_cards)
local skill = Fk.skills[skill_name]
local ret = false
if skill and skill:isInstanceOf(ActiveSkill) then
ret = skill:feasible(selected, selected_cards)
end
return json.encode(ret)
end
-- ViewAsSkill (Todo)
function CanViewAs(skill_name, card_ids)
return "true"
end
Fk:loadTranslationTable{
-- Lobby
["Room List"] = "房间列表",
@ -190,4 +248,11 @@ Fk:loadTranslationTable{
["AskForGeneral"] = "选择武将",
["AskForChoice"] = "选择",
["PlayCard"] = "出牌",
["AskForCardChosen"] = "选牌",
["#AskForChooseCard"] = "%1请选择其一张卡牌",
["$ChooseCard"] = "请选择一张卡牌",
["$Hand"] = "手牌区",
["$Equip"] = "装备区",
["$Judge"] = "判定区",
}

View File

@ -1,13 +1,30 @@
---@class ClientPlayer
---@class ClientPlayer: Player
---@field player fk.Player
---@field handcardNum integer
---@field known_cards integer[]
---@field global_known_cards integer[]
local ClientPlayer = Player:subclass("ClientPlayer")
function ClientPlayer:initialize(cp)
Player.initialize(self)
self.id = cp:getId()
self.player = cp
self.handcardNum = 0
self.known_cards = {}
self.known_cards = {} -- you know he/she have this card, but not shown
self.global_known_cards = {} -- card that visible to all players
end
---@param skill Skill
function ClientPlayer:hasSkill(skill)
return table.contains(self.player_skills, skill)
end
---@param skill Skill
function ClientPlayer:addSkill(skill)
table.insert(self.player_skills, skill)
end
---@param skill Skill
function ClientPlayer:loseSkill(skill)
table.removeOne(self.player_skills, skill)
end
return ClientPlayer

View File

@ -209,4 +209,11 @@ function Engine:getCardById(id)
return self.cards[id]
end
function Engine:currentRoom()
if ClientInstance then
return ClientInstance
end
return RoomInstance
end
return Engine

View File

@ -14,6 +14,7 @@
---@field dead boolean
---@field state string
---@field player_skills Skill[]
---@field derivative_skills table<Skill, Skill[]>
---@field flag string[]
---@field tag table<string, any>
---@field mark table<string, integer>
@ -57,6 +58,7 @@ function Player:initialize()
self.state = ""
self.player_skills = {}
self.derivative_skills = {}
self.flag = {}
self.tag = {}
self.mark = {}
@ -236,6 +238,20 @@ function Player:getAttackRange()
return math.max(baseAttackRange, 0)
end
---@param other Player
function Player:distanceTo(other)
local right = math.abs(self.seat - other.seat)
local left = #Fk:currentRoom().alive_players - right
local ret = math.min(left, right)
-- TODO: corrent distance here using skills
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)
@ -249,4 +265,126 @@ function Player:resetCardUseHistory(cardName)
end
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
---@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.clone(skill.related_skills)
table.insert(toget, skill)
local ret = {}
for _, s in ipairs(toget) do
if not self:hasSkill(s) then
table.insert(ret, s)
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

View File

@ -2,6 +2,7 @@
---@field name string
---@field frequency Frequency
---@field visible boolean
---@field related_skills Skill[]
local Skill = class("Skill")
---@alias Frequency integer
@ -17,6 +18,12 @@ function Skill:initialize(name, frequency)
self.name = name
self.frequency = frequency
self.visible = true
self.related_skills = {}
end
---@param skill Skill
function Skill:addRelatedSkill(skill)
table.insert(self.related_skills, skill)
end
return Skill

View File

@ -38,9 +38,9 @@ end
--- Determine if selected cards and targets are valid for this skill
--- If returns true, the OK button should be enabled
--- only used in skill of players
---@param selected integer[] @ ids of selected cards
---@param selected_targets integer[] @ ids of selected players
function ActiveSkill:feasible(selected, selected_targets)
---@param selected integer[] @ ids of selected players
---@param selected_cards integer[] @ ids of selected cards
function ActiveSkill:feasible(selected, selected_cards)
return true
end
@ -51,7 +51,7 @@ end
function ActiveSkill:onUse(room, cardUseEvent) end
---@param room Room
---@param cardEffectEvent CardEffectEvent
---@param cardEffectEvent CardEffectEvent | SkillEffectEvent
function ActiveSkill:onEffect(room, cardEffectEvent) end
return ActiveSkill

View File

@ -66,6 +66,29 @@ function table.clone(self)
return ret
end
-- if table does not contain the element, we insert it
function table:insertIfNeed(element)
if not table.contains(self, element) then
table.insert(self, element)
end
end
---@param delimiter string
---@return string[]
function string:split(delimiter)
if #self == 0 then return {} end
local result = {}
local from = 1
local delim_from, delim_to = string.find(self, delimiter, from)
while delim_from do
table.insert(result, string.sub(self, from, delim_from - 1))
from = delim_to + 1
delim_from, delim_to = string.find(self, delimiter, from)
end
table.insert(result, string.sub(self, from))
return result
end
---@class Sql
Sql = {
---@param filename string

View File

@ -87,7 +87,7 @@ end
---@field can_use fun(self: ActiveSkill, player: Player): boolean
---@field card_filter fun(self: ActiveSkill, to_select: integer, selected: integer[], selected_targets: integer[]): boolean
---@field target_filter fun(self: ActiveSkill, to_select: integer, selected: integer[], selected_cards: integer[]): boolean
---@field feasible fun(self: ActiveSkill, selected: integer[], selected_targets: integer[]): boolean
---@field feasible fun(self: ActiveSkill, selected: integer[], selected_cards: integer[]): boolean
---@field on_use fun(self: ActiveSkill, room: Room, cardUseEvent: CardUseStruct): boolean
---@field on_effect fun(self: ActiveSkill, room: Room, cardEffectEvent: CardEffectEvent): boolean

View File

@ -133,7 +133,11 @@ function GameLogic:prepareForStart()
table.shuffle(allCardIds)
room.draw_pile = allCardIds
for _, id in ipairs(room.draw_pile) do
self.room:setCardArea(id, Card.DrawPile)
self.room:setCardArea(id, Card.DrawPile, nil)
end
for _, p in ipairs(room.alive_players) do
room:handleAddLoseSkills(p, "zhiheng")
end
self:addTriggerSkill(GameRule)

View File

@ -11,6 +11,7 @@
---@field processing_area integer[]
---@field void integer[]
---@field card_place table<integer, CardArea>
---@field owner_map table<integer, integer>
local Room = class("Room")
-- load classes used by the game
@ -32,6 +33,7 @@ function Room:initialize(_room)
end
self.room.startGame = function(_self)
Room.initialize(self, _room) -- clear old data
self:run()
end
@ -46,6 +48,7 @@ function Room:initialize(_room)
self.processing_area = {}
self.void = {}
self.card_place = {}
self.owner_map = {}
end
-- When this function returns, the Room(C++) thread stopped.
@ -81,7 +84,7 @@ end
---@param command string
---@param jsonData string
---@param players ServerPlayer[] @ default all players
---@param players ServerPlayer[] | nil @ default all players
function Room:doBroadcastNotify(command, jsonData, players)
players = players or self.players
local tolist = fk.SPlayerList()
@ -177,7 +180,7 @@ function Room:shuffleDrawPile()
table.insertTable(self.draw_pile, self.discard_pile)
for _, id in ipairs(self.discard_pile) do
self:setCardArea(id, Card.DrawPile)
self:setCardArea(id, Card.DrawPile, nil)
end
self.discard_pile = {}
table.shuffle(self.draw_pile)
@ -208,8 +211,10 @@ end
---@param cardId integer
---@param cardArea CardArea
function Room:setCardArea(cardId, cardArea)
---@param integer owner
function Room:setCardArea(cardId, cardArea, owner)
self.card_place[cardId] = cardArea
self.owner_map[cardId] = owner
end
---@param cardId integer
@ -345,7 +350,7 @@ function Room:moveCards(...)
table.insert(toAreaIds, toAreaIds == Card.DrawPile and 1 or #toAreaIds + 1, info.cardId)
end
self:setCardArea(info.cardId, data.toArea)
self:setCardArea(info.cardId, data.toArea, data.to)
end
end
end
@ -354,6 +359,22 @@ function Room:moveCards(...)
return true
end
---@param player integer
---@param cid integer
---@param unhide boolean
---@param reason CardMoveReason
function Room:obtainCard(player, cid, unhide, reason)
self:moveCards({
ids = {cid},
from = self.owner_map[cid],
to = player,
toArea = Card.PlayerHand,
moveReason = reason or fk.ReasonJustMove,
proposer = player,
moveVisible = unhide or false,
})
end
---@param player ServerPlayer
---@param num integer
---@param skillName string
@ -504,6 +525,31 @@ function Room:askForGeneral(player, generals)
return defaultChoice
end
---@param chooser ServerPlayer
---@param target ServerPlayer
---@param flag string @ "hej", h for handcard, e for equip, j for judge
---@param reason string
function Room:askForCardChosen(chooser, target, flag, reason)
local command = "AskForCardChosen"
self:notifyMoveFocus(chooser, command)
local data = {target.id, flag, reason}
local result = self:doRequest(chooser, command, json.encode(data))
if result == "" then
-- FIXME: generate a random card according to flag
result = -1
else
result = tonumber(result)
end
if result == -1 then
local handcards = target.player_cards[Player.Hand]
result = handcards[math.random(1, #handcards)]
end
return result
end
function Room:gameOver()
self.game_finished = true
-- dosomething
@ -954,6 +1000,60 @@ function Room:useCard(cardUseEvent)
end
end
---@param player ServerPlayer
---@param skill_names string[] | string
---@param source_skill string | Skill | nil
function Room:handleAddLoseSkills(player, skill_names, source_skill)
if type(skill_names) == "string" then
skill_names = skill_names:split("|")
end
if #skill_names == 0 then return end
local losts = {} ---@type boolean[]
local triggers = {} ---@type Skill[]
for _, skill in ipairs(skill_names) do
if string.sub(skill, 1, 1) == "-" then
local actual_skill = string.sub(skill, 2, #skill)
if player:hasSkill(actual_skill) then
local lost_skills = player:loseSkill(actual_skill, source_skill)
for _, s in ipairs(lost_skills) do
self:doBroadcastNotify("LoseSkill", json.encode{
player.id,
s.name
})
-- TODO: send a log here
table.insert(losts, true)
table.insert(triggers, s)
end
end
else
local sk = Fk.skills[skill]
if sk and not player:hasSkill(sk) then
local got_skills = player:addSkill(sk)
for _, s in ipairs(got_skills) do
-- TODO: limit skill mark
self:doBroadcastNotify("AddSkill", json.encode{
player.id,
s.name
})
-- TODO: send log
table.insert(losts, false)
table.insert(triggers, s)
end
end
end
end
if #triggers > 0 then
for i = 1, #triggers do
local event = losts[i] and fk.EventLoseSkill or fk.EventAcquireSkill
self.logic:trigger(event, player, triggers[i])
end
end
end
fk.room_callback["QuitRoom"] = function(jsonData)
-- jsonData: [ int uid ]
local data = json.decode(jsonData)

View File

@ -65,11 +65,6 @@ function ServerPlayer:waitForReply(timeout)
return result
end
---@param skill Skill
function ServerPlayer:hasSkill(skill)
return table.contains(self.player_skills, skill)
end
function ServerPlayer:isAlive()
return self.dead == false
end

View File

@ -13,8 +13,9 @@
---@alias CardUseStruct { from: integer, tos: TargetGroup, cardId: integer, toCardId: integer|null, responseToEvent: CardUseStruct|null, nullifiedTargets: interger[]|null, extraUse: boolean|null, disresponsiveList: integer[]|null, unoffsetableList: integer[]|null, addtionalDamage: integer|null, customFrom: integer|null, cardIdsResponded: integer[]|null }
---@alias AimStruct { from: integer, cardId: integer, tos: AimGroup, to: integer, targetGroup: TargetGroup|null, nullifiedTargets: integer[]|null, firstTarget: boolean, additionalDamage: integer|null, disresponsive: boolean|null, unoffsetableList: boolean|null }
---@alias CardEffectEvent { from: integer, tos: TargetGroup, cardId: integer, toCardId: integer|null, responseToEvent: CardUseStruct|null, nullifiedTargets: interger[]|null, extraUse: boolean|null, disresponsiveList: integer[]|null, unoffsetableList: integer[]|null, addtionalDamage: integer|null, customFrom: integer|null, cardIdsResponded: integer[]|null }
---@alias SkillEffectEvent { from: integer, tos: integer[], cards: integer[] }
---@alias MoveReason integer
---@alias CardMoveReason integer
fk.ReasonJustMove = 1
fk.ReasonDraw = 2

View File

@ -42,7 +42,7 @@ GameRule = fk.CreateTriggerSkill{
room:notifyMoveCards(room.players, {move_to_notify})
for _, id in ipairs(cardIds) do
room:setCardArea(id, Card.PlayerHand)
room:setCardArea(id, Card.PlayerHand, player.id)
end
room.logic:trigger(fk.AfterDrawInitialCards, player, data)
@ -91,14 +91,25 @@ GameRule = fk.CreateTriggerSkill{
local data = json.decode(result)
local card = data.card
local targets = data.targets
local use = {} ---@type CardUseStruct
use.from = player.id
use.tos = {}
for _, target in ipairs(targets) do
table.insert(use.tos, { target })
if type(card) == "string" then
local card_data = json.decode(card)
local skill = Fk.skills[card_data.skill]
local selected_cards = card_data.subcards
skill:onEffect(room, {
from = player.id,
cards = selected_cards,
tos = targets,
})
else
local use = {} ---@type CardUseStruct
use.from = player.id
use.tos = {}
for _, target in ipairs(targets) do
table.insert(use.tos, { target })
end
use.cardId = card
room:useCard(use)
end
use.cardId = card
room:useCard(use)
end
end,
[Player.Discard] = function()

View File

@ -94,10 +94,21 @@ Fk:loadTranslationTable{
["huangyueying"] = "黄月英",
}
local zhiheng = fk.CreateActiveSkill{
name = "zhiheng",
feasible = function(self, selected, selected_cards)
return #selected == 0 and #selected_cards > 0
end,
on_effect = function(self, room, effect)
room:drawCards(room:getPlayerById(effect.from), #effect.cards, "zhiheng")
end
}
local sunquan = General:new(extension, "sunquan", "wu", 4)
sunquan:addSkill(zhiheng)
extension:addGeneral(sunquan)
Fk:loadTranslationTable{
["sunquan"] = "孙权",
["zhiheng"] = "制衡",
}
local ganning = General:new(extension, "ganning", "wu", 4)

View File

@ -118,10 +118,36 @@ extension:addCards({
dismantlement:clone(Card.Heart, 12),
})
local snatchSkill = fk.CreateActiveSkill{
name = "snatch_skill",
target_filter = function(self, to_select, selected)
if #selected == 0 then
local player = Fk:currentRoom():getPlayerById(to_select)
return Self ~= player and Self:distanceTo(player) <= 1
and not player:isAllNude()
end
end,
feasible = function(self, selected)
return #selected == 1
end,
on_effect = function(self, room, effect)
local to = TargetGroup:getRealTargets(effect.tos)[1]
local from = effect.from
local cid = room:askForCardChosen(
room:getPlayerById(from),
room:getPlayerById(to),
"hej",
"snatch"
)
room:obtainCard(from, cid)
end
}
local snatch = fk.CreateTrickCard{
name = "snatch",
suit = Card.Spade,
number = 3,
skill = snatchSkill,
}
Fk:loadTranslationTable{
["snatch"] = "顺手牵羊",

View File

@ -1,4 +1,4 @@
import QtQuick 2.15
import QtQuick
QtObject {
// Client configuration

View File

@ -1,6 +1,6 @@
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.0
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import "RoomElement"
Item {

View File

@ -1,6 +1,6 @@
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.0
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import "RoomElement"
Item {

View File

@ -1,8 +1,9 @@
import QtQuick 2.15
import QtQuick.Controls 2.0
import QtQuick
import QtQuick.Controls
Item {
id: root
scale: 2
Frame {
id: join_server

View File

@ -1,7 +1,7 @@
import QtQuick 2.15
import QtQuick.Controls 2.0
import QtQuick.Window 2.0
import QtQuick.Layouts 1.15
import QtQuick
import QtQuick.Controls
import QtQuick.Window
import QtQuick.Layouts
import "Logic.js" as Logic
Item {

View File

@ -1,4 +1,4 @@
import QtQuick 2.15
import QtQuick
Item {
property bool enabled: true

View File

@ -1,6 +1,6 @@
import QtQuick 2.15
import QtQuick.Controls 2.0
import QtQuick.Layouts 1.15
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import "RoomElement"
import "RoomLogic.js" as Logic
@ -42,6 +42,15 @@ Item {
ClientInstance.notifyServer("AddRobot", "[]");
}
}
Button {
text: "test"
onClicked: dashboard.expandPile("_equip");
}
Button {
text: "test2"
x: 60
onClicked: dashboard.retractPile("_equip");
}
states: [
State { name: "notactive" }, // Normal status
@ -61,6 +70,7 @@ Item {
endPhaseButton.visible = false;
dashboard.disableAllCards();
dashboard.disableSkills();
if (dashboard.pending_skill !== "")
dashboard.stopPending();
selected_targets = [];
@ -77,6 +87,7 @@ Item {
ScriptAction {
script: {
dashboard.enableCards();
dashboard.enableSkills();
progress.visible = true;
okCancel.visible = true;
endPhaseButton.visible = true;
@ -88,6 +99,8 @@ Item {
from: "*"; to: "responding"
ScriptAction {
script: {
dashboard.enableCards();
dashboard.enableSkills();
progress.visible = true;
okCancel.visible = true;
}
@ -98,6 +111,8 @@ Item {
from: "*"; to: "replying"
ScriptAction {
script: {
dashboard.disableAllCards();
dashboard.disableSkills();
progress.visible = true;
}
}
@ -122,7 +137,7 @@ Item {
Item {
id: roomArea
width: roomScene.width
height: roomScene.height - dashboard.height
height: roomScene.height - dashboard.height + 20
Repeater {
id: photos
@ -164,7 +179,7 @@ Item {
width: parent.width * 0.6
height: 150
x: parent.width * 0.2
y: parent.height * 0.6
y: parent.height * 0.6 + 20
}
}
@ -193,7 +208,7 @@ Item {
Logic.updateSelectedTargets(self.playerid, selected);
}
onCardSelected: {
onCardSelected: function(card) {
Logic.enableTargets(card);
}
}
@ -201,7 +216,7 @@ Item {
Item {
id: controls
anchors.bottom: dashboard.top
anchors.bottomMargin: -40
anchors.bottomMargin: -60
width: roomScene.width
Text {
@ -268,6 +283,7 @@ Item {
Loader {
id: popupBox
z: 999
onSourceChanged: {
if (item === null)
return;
@ -290,6 +306,15 @@ Item {
}
}
function activateSkill(skill_name, pressed) {
if (pressed) {
dashboard.startPending(skill_name);
cancelButton.enabled = true;
} else {
Logic.doCancelButton();
}
}
Component.onCompleted: {
toast.show(Backend.translate("$EnterRoom"));

View File

@ -1,4 +1,4 @@
import QtQuick 2.15
import QtQuick
// CardArea stores CardItem.

View File

@ -1,5 +1,5 @@
import QtQuick 2.15
import QtGraphicalEffects 1.0
import QtQuick
import Qt5Compat.GraphicalEffects
import "../skin-bank.js" as SkinBank
/* Layout of card:
@ -122,7 +122,7 @@ Item {
glow.color: "black"
glow.spread: 1
glow.radius: 1
glow.samples: 12
//glow.samples: 12
}
Rectangle {
@ -138,7 +138,7 @@ Item {
drag.axis: Drag.XAndYAxis
hoverEnabled: true
onReleased: {
onReleased: function(mouse) {
root.isClicked = mouse.isClick;
parent.released();
if (autoBack)

View File

@ -1,4 +1,4 @@
import QtQuick 2.15
import QtQuick
import ".."
GraphicsBox {

View File

@ -1,4 +1,4 @@
import QtQuick 2.15
import QtQuick
import ".."
import "../skin-bank.js" as SkinBank

View File

@ -1,6 +1,6 @@
import QtQuick 2.15
import QtQuick.Layouts 1.1
import QtGraphicalEffects 1.0
import QtQuick
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
RowLayout {
id: root
@ -19,26 +19,40 @@ RowLayout {
property var pendings: [] // int[], store cid
property int selected_card: -1
property alias skillButtons: skillPanel.skill_buttons
property var expanded_piles: ({}) // name -> int[]
signal cardSelected(var card)
Item {
width: 40
}
Item { width: 5 }
HandcardArea {
id: handcardAreaItem
Layout.fillWidth: true
Layout.preferredHeight: 130
Layout.alignment: Qt.AlignVCenter
Layout.alignment: Qt.AlignBottom
Layout.bottomMargin: 24
onWidthChanged: updateCardPosition(true);
}
SkillArea {
Layout.fillHeight: true
Layout.fillWidth: true
Layout.maximumWidth: width
Layout.maximumHeight: height
Layout.alignment: Qt.AlignBottom
Layout.bottomMargin: 32
Layout.rightMargin: -16
id: skillPanel
}
Photo {
id: selfPhoto
Layout.rightMargin: -175 / 8 + (roomArea.width - 175 * 0.75 * 7) / 8
handcards: handcardAreaItem.length
}
Item { width: 5 }
Connections {
target: handcardAreaItem
function onCardSelected(cardId, selected) {
@ -54,6 +68,49 @@ RowLayout {
handcardAreaItem.unselectAll(expectId);
}
function expandPile(pile) {
let expanded_pile_names = Object.keys(expanded_piles);
if (expanded_pile_names.indexOf(pile) !== -1)
return;
let component = Qt.createComponent("CardItem.qml");
let parentPos = roomScene.mapFromItem(selfPhoto, 0, 0);
// FIXME: only expand equip area here. modify this if need true pile
expanded_piles[pile] = [];
if (pile === "_equip") {
let equips = selfPhoto.equipArea.getAllCards();
equips.forEach(data => {
data.x = parentPos.x;
data.y = parentPos.y;
let card = component.createObject(roomScene, data);
handcardAreaItem.add(card);
})
handcardAreaItem.updateCardPosition();
}
}
function retractPile(pile) {
let expanded_pile_names = Object.keys(expanded_piles);
if (expanded_pile_names.indexOf(pile) === -1)
return;
let parentPos = roomScene.mapFromItem(selfPhoto, 0, 0);
delete expanded_piles[pile];
if (pile === "_equip") {
let equips = selfPhoto.equipArea.getAllCards();
equips.forEach(data => {
let card = handcardAreaItem.remove([data.cid])[0];
card.origX = parentPos.x;
card.origY = parentPos.y;
card.destroyOnStop();
card.goBack(true);
})
handcardAreaItem.updateCardPosition();
}
}
function enableCards() {
// TODO: expand pile
let ids = [], cards = handcardAreaItem.cards;
@ -62,6 +119,9 @@ RowLayout {
ids.push(cards[i].cid);
}
handcardAreaItem.enableCards(ids)
if (pending_skill === "") {
cancelButton.enabled = false;
}
}
function selectCard(cardId, selected) {
@ -100,23 +160,21 @@ RowLayout {
if (pending_skill === "") return;
let enabled_cards = [];
let targets = roomScene.selected_targets;
handcardAreaItem.cards.forEach(function(card) {
if (card.selected || Router.vs_view_filter(pending_skill, pendings, card.cid))
handcardAreaItem.cards.forEach((card) => {
if (card.selected || JSON.parse(Backend.callLuaFunction(
"ActiveCardFilter",
[pending_skill, card.cid, pendings, targets]
)))
enabled_cards.push(card.cid);
});
handcardAreaItem.enableCards(enabled_cards);
let equip;
for (let i = 0; i < 5; i++) {
equip = equipAreaItem.equips.itemAt(i);
if (equip.selected || equip.cid !== -1 &&
Router.vs_view_filter(pending_skill, pendings, equip.cid))
enabled_cards.push(equip.cid);
}
equipAreaItem.enableCards(enabled_cards);
if (Router.vs_can_view_as(pending_skill, pendings)) {
if (JSON.parse(Backend.callLuaFunction(
"CanViewAs",
[pending_skill, pendings]
))) {
pending_card = {
skill: pending_skill,
subcards: pendings
@ -141,8 +199,8 @@ RowLayout {
}
function deactivateSkillButton() {
for (let i = 0; i < headSkills.length; i++) {
headSkillButtons.itemAt(i).pressed = false;
for (let i = 0; i < skillButtons.count; i++) {
skillButtons.itemAt(i).pressed = false;
}
}
@ -152,17 +210,31 @@ RowLayout {
// TODO: expand pile
let equip;
for (let i = 0; i < 5; i++) {
equip = equipAreaItem.equips.itemAt(i);
if (equip.name !== "") {
equip.selected = false;
equip.selectable = false;
}
}
// TODO: equipment
pendings = [];
handcardAreaItem.adjustCards();
handcardAreaItem.unselectAll();
cardSelected(-1);
}
function addSkill(skill_name) {
skillPanel.addSkill(skill_name);
}
function loseSkill(skill_name) {
skillPanel.loseSkill(skill_name);
}
function enableSkills() {
for (let i = 0; i < skillButtons.count; i++) {
let item = skillButtons.itemAt(i);
item.enabled = JSON.parse(Backend.callLuaFunction("ActiveCanUse", [item.orig]));
}
}
function disableSkills() {
for (let i = 0; i < skillButtons.count; i++)
skillButtons.itemAt(i).enabled = false;
}
}

View File

@ -1,4 +1,4 @@
import QtQuick 2.15
import QtQuick
import "../skin-bank.js" as SkinBank
/* Layout of general card:

View File

@ -1,5 +1,5 @@
import QtQuick 2.15
import QtGraphicalEffects 1.0
import QtQuick
import Qt5Compat.GraphicalEffects
Item {
property alias text: textItem.text

View File

@ -1,5 +1,5 @@
import QtQuick 2.15
import QtGraphicalEffects 1.0
import QtQuick
import Qt5Compat.GraphicalEffects
Item {
property alias title: titleItem
@ -22,7 +22,7 @@ Item {
anchors.fill: background
color: "#B0000000"
radius: 5
samples: 12
//samples: 12
spread: 0.2
horizontalOffset: 5
verticalOffset: 4

View File

@ -1,4 +1,4 @@
import QtQuick 2.15
import QtQuick
import "../../util.js" as Utility
Item {

View File

@ -1,4 +1,4 @@
import QtQuick 2.15
import QtQuick
Item {
property point start: Qt.point(0, 0)

View File

@ -1,4 +1,4 @@
import QtQuick 2.15
import QtQuick
Item {
property var cards: []

View File

@ -1,6 +1,6 @@
import QtQuick 2.15
import QtGraphicalEffects 1.15
import QtQuick.Controls 2.15
import QtQuick
import Qt5Compat.GraphicalEffects
import QtQuick.Controls
import "PhotoElement"
import "../skin-bank.js" as SkinBank
@ -8,7 +8,7 @@ Item {
id: root
width: 175
height: 233
scale: 0.8
scale: 0.75
property int playerid
property string general: ""
property string screenName: ""
@ -282,7 +282,7 @@ Item {
glow.color: "brown"
glow.spread: 0.2
glow.radius: 8
glow.samples: 12
//glow.samples: 12
}
SequentialAnimation {

View File

@ -1,5 +1,4 @@
import QtQuick 2.15
import QtQuick
import ".."
import "../../skin-bank.js" as SkinBank

View File

@ -1,4 +1,4 @@
import QtQuick 2.15
import QtQuick
import ".."
import "../../skin-bank.js" as SkinBank
@ -116,5 +116,9 @@ Column {
{
area.updateCardPosition(animated);
}
function getAllCards() {
return area.cards;
}
}

View File

@ -1,4 +1,4 @@
import QtQuick 2.15
import QtQuick
import ".."
import "../../../util.js" as Utility
import "../../skin-bank.js" as SkinBank
@ -40,20 +40,16 @@ Item {
glow.color: "black"
glow.spread: 0.75
glow.radius: 2
glow.samples: 4
//glow.samples: 4
x: parent.width - 24
y: 1
}
GlowText {
Text {
id: textItem
font.family: fontLibian.name
color: "white"
font.pixelSize: 18
glow.color: "black"
glow.spread: 0.9
glow.radius: 2
glow.samples: 6
anchors.left: iconItem.right
anchors.leftMargin: -8
verticalAlignment: Text.AlignVCenter

View File

@ -1,4 +1,4 @@
import QtQuick 2.15
import QtQuick
import ".."
Column {
@ -36,7 +36,7 @@ Column {
glow.color: "#3E3F47"
glow.spread: 0.8
glow.radius: 8
glow.samples: 12
//glow.samples: 12
}
GlowText {
@ -51,7 +51,7 @@ Column {
glow.color: hpItem.glow.color
glow.spread: hpItem.glow.spread
glow.radius: hpItem.glow.radius
glow.samples: hpItem.glow.samples
//glow.samples: hpItem.glow.samples
}
GlowText {
@ -65,7 +65,7 @@ Column {
glow.color: hpItem.glow.color
glow.spread: hpItem.glow.spread
glow.radius: hpItem.glow.radius
glow.samples: hpItem.glow.samples
//glow.samples: hpItem.glow.samples
}
}
}

View File

@ -1,4 +1,4 @@
import QtQuick 2.15
import QtQuick
import "../../skin-bank.js" as SkinBank
Image {

View File

@ -1,5 +1,4 @@
import QtQuick 2.15
import QtQuick
import "../../skin-bank.js" as SkinBank
Image {

View File

@ -1,5 +1,5 @@
import QtQuick 2.15
import Qt.labs.folderlistmodel 2.15
import QtQuick
import Qt.labs.folderlistmodel
import "../skin-bank.js" as SkinBank
Item {

View File

@ -0,0 +1,188 @@
import QtQuick
import QtQuick.Layouts
GraphicsBox {
signal cardSelected(int cid)
id: root
title.text: Backend.translate("$ChooseCard")
//@to-do: Adjust the UI design in case there are more than 7 cards
width: 70 + Math.min(7, Math.max(1, handcards.count, equips.count, delayedTricks.count)) * 100
height: 50 + (handcards.count > 0 ? 150 : 0) + (equips.count > 0 ? 150 : 0) + (delayedTricks.count > 0 ? 150 : 0)
ListModel {
id: handcards
}
ListModel {
id: equips
}
ListModel {
id: delayedTricks
}
ColumnLayout {
anchors.fill: parent
anchors.topMargin: 40
anchors.leftMargin: 20
anchors.rightMargin: 20
anchors.bottomMargin: 20
Row {
height: 130
spacing: 15
visible: handcards.count > 0
Rectangle {
border.color: "#A6967A"
radius: 5
color: "transparent"
width: 18
height: parent.height
Text {
color: "#E4D5A0"
text: Backend.translate("$Hand")
anchors.fill: parent
wrapMode: Text.WrapAnywhere
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
font.pixelSize: 15
}
}
Row {
spacing: 7
Repeater {
model: handcards
CardItem {
name: "card-back"
cid: -1
suit: ""
number: 0
autoBack: false
selectable: true
onClicked: root.cardSelected(cid);
}
}
}
}
Row {
height: 130
spacing: 15
visible: equips.count > 0
Rectangle {
border.color: "#A6967A"
radius: 5
color: "transparent"
width: 18
height: parent.height
Text {
color: "#E4D5A0"
text: Backend.translate("$Equip")
anchors.fill: parent
wrapMode: Text.WrapAnywhere
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
font.pixelSize: 15
}
}
Row {
spacing: 7
Repeater {
model: equips
CardItem {
cid: model.cid
name: model.name
suit: model.suit
number: model.number
autoBack: false
selectable: true
onClicked: root.cardSelected(cid);
}
}
}
}
Row {
height: 130
spacing: 15
visible: delayedTricks.count > 0
Rectangle {
border.color: "#A6967A"
radius: 5
color: "transparent"
width: 18
height: parent.height
Text {
color: "#E4D5A0"
text: Backend.translate("$Judge")
anchors.fill: parent
wrapMode: Text.WrapAnywhere
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
font.pixelSize: 15
}
}
Row {
spacing: 7
Repeater {
model: delayedTricks
CardItem {
cid: model.cid
name: model.name
suit: model.suit
number: model.number
autoBack: false
selectable: true
onClicked: root.cardSelected(cid);
}
}
}
}
}
onCardSelected: finished();
function addHandcards(cards)
{
if (cards instanceof Array) {
for (var i = 0; i < cards.length; i++)
handcards.append(cards[i]);
} else {
handcards.append(cards);
}
}
function addEquips(cards)
{
if (cards instanceof Array) {
for (var i = 0; i < cards.length; i++)
equips.append(cards[i]);
} else {
equips.append(cards);
}
}
function addDelayedTricks(cards)
{
if (cards instanceof Array) {
for (var i = 0; i < cards.length; i++)
delayedTricks.append(cards[i]);
} else {
delayedTricks.append(cards);
}
}
}

View File

@ -1,5 +1,95 @@
import QtQuick 2.15
import QtQuick
import QtQuick.Layouts
Flickable {
id: root
property alias skill_buttons: skill_buttons
clip: true
contentWidth: panel.width
contentHeight: panel.height
width: panel.width
height: Math.min(180, panel.height)
flickableDirection: Flickable.AutoFlickIfNeeded
ListModel {
id: active_skills
}
ListModel {
id: not_active_skills
}
Item {
id: panel
width: Math.max(grid1.width, grid2.width)
height: grid1.height + grid2.height
Grid {
id: grid1
columns: 2
columnSpacing: 2
rowSpacing: 2
Repeater {
id: skill_buttons
model: active_skills
onItemAdded: parent.forceLayout()
SkillButton {
skill: model.skill
type: "active"
enabled: false
orig: model.orig_skill
onPressedChanged: {
if (enabled)
roomScene.activateSkill(orig, pressed);
}
}
}
}
Grid {
id: grid2
anchors.top: grid1.bottom
anchors.topMargin: 2
columns: 3
columnSpacing: 2
rowSpacing: 2
Repeater {
model: not_active_skills
onItemAdded: parent.forceLayout()
SkillButton {
skill: model.skill
orig: model.orig_skill
type: "notactive"
}
}
}
}
function addSkill(skill_name) {
let data = JSON.parse(Backend.callLuaFunction(
"GetSkillData",
[skill_name]
));
if (data.freq = "active") {
active_skills.append(data);
} else {
not_active_skills.append(data);
}
}
function loseSkill(skill_name) {
for (let i = 0; i < active_skills.count; i++) {
let item = active_skills.at(i);
if (item.skill == skill_name) {
active_skills.remove(i);
}
}
for (let i = 0; i < not_active_skills.count; i++) {
let item = not_active_skills.at(i);
if (item.skill == skill_name) {
not_active_skills.remove(i);
}
}
}
}

View File

@ -0,0 +1,60 @@
import QtQuick
import Qt5Compat.GraphicalEffects
Item {
id: root
property alias skill: skill.text
property string type: "active"
property string orig: ""
property bool pressed: false
onEnabledChanged: {
if (!enabled)
pressed = false;
}
width: type === "active" ? 120 * 0.66 : 72 * 0.66
height: type === "active" ? 55 * 0.66 : 36 * 0.66
Image {
x: -13 - 120 * 0.166
y: -6 - 55 * 0.166
scale: 0.66
source: type !== "active" ? ""
: AppPath + "/image/button/skill/active/"
+ (enabled ? (pressed ? "pressed" : "normal") : "disabled")
}
Text {
anchors.centerIn: parent
id: skill
font.family: fontLi2.name
font.pixelSize: 36 * 0.66
visible: false
}
Glow {
id: glowItem
source: skill
anchors.fill: skill
radius: 6
//samples: 8
color: "grey"
}
LinearGradient {
anchors.fill: skill
source: skill
gradient: Gradient {
GradientStop { position: 0; color: "#FFE07C" }
GradientStop { position: 1; color: "#B79A5F" }
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
enabled: root.type === "active" && root.enabled
onClicked: parent.pressed = !parent.pressed;
}
}

View File

@ -1,4 +1,4 @@
import QtQuick 2.15
import QtQuick
Item {
property var discardedCards: []

View File

@ -19,21 +19,24 @@ function arrangePhotos() {
* +---------------+
*/
const photoWidth = 175;
const roomAreaPadding = 10;
let verticalPadding = Math.max(10, roomArea.width * 0.01);
let horizontalSpacing = Math.max(30, roomArea.height * 0.1);
let verticalSpacing = (roomArea.width - photoWidth * 7 - verticalPadding * 2) / 6;
const photoWidth = 175 * 0.75;
// Padding is negative, because photos are scaled.
const roomAreaPadding = -16;
const verticalPadding = -175 / 8;
const horizontalSpacing = 32;
let verticalSpacing = (roomArea.width - photoWidth * 7) / 8;
// Position 1-7
const regions = [
{ x: verticalPadding + (photoWidth + verticalSpacing) * 6, y: roomAreaPadding + horizontalSpacing * 2 },
{ x: verticalPadding + (photoWidth + verticalSpacing) * 5, y: roomAreaPadding + horizontalSpacing },
{ x: verticalPadding + (photoWidth + verticalSpacing) * 4, y: roomAreaPadding },
{ x: verticalPadding + (photoWidth + verticalSpacing) * 3, y: roomAreaPadding },
{ x: verticalPadding + (photoWidth + verticalSpacing) * 2, y: roomAreaPadding },
{ x: verticalPadding + photoWidth + verticalSpacing, y: roomAreaPadding + horizontalSpacing },
{ x: verticalPadding, y: roomAreaPadding + horizontalSpacing * 2 },
let startX = verticalPadding + verticalSpacing;
let padding = photoWidth + verticalSpacing;
let regions = [
{ x: startX + padding * 6, y: roomAreaPadding + horizontalSpacing * 3 },
{ x: startX + padding * 5, y: roomAreaPadding + horizontalSpacing },
{ x: startX + padding * 4, y: roomAreaPadding },
{ x: startX + padding * 3, y: roomAreaPadding },
{ x: startX + padding * 2, y: roomAreaPadding },
{ x: startX + padding, y: roomAreaPadding + horizontalSpacing },
{ x: startX, y: roomAreaPadding + horizontalSpacing * 3 },
];
const regularSeatIndex = [
@ -61,7 +64,7 @@ function arrangePhotos() {
}
function doOkButton() {
if (roomScene.state == "playing") {
if (roomScene.state == "playing" || roomScene.state == "responding") {
replyToServer(JSON.stringify(
{
card: dashboard.getSelectedCard(),
@ -74,6 +77,20 @@ function doOkButton() {
}
function doCancelButton() {
if (roomScene.state == "playing") {
dashboard.deactivateSkillButton();
dashboard.unSelectAll();
dashboard.stopPending();
dashboard.enableCards();
return;
} else if (roomScene.state == "responding") {
dashboard.deactivateSkillButton();
dashboard.unSelectAll();
dashboard.stopPending();
replyToServer("");
return;
}
replyToServer("");
}
@ -271,6 +288,7 @@ function enableTargets(card) { // card: int | { skill: string, subcards: int[] }
function updateSelectedTargets(playerid, selected) {
let i = 0;
let card = dashboard.getSelectedCard();
let candidate = (!isNaN(card) && card !== -1) || typeof(card) === "string";
let all_photos = [dashboard.self]
for (i = 0; i < playerNum - 1; i++) {
all_photos.push(photos.itemAt(i))
@ -282,19 +300,28 @@ function updateSelectedTargets(playerid, selected) {
selected_targets.splice(selected_targets.indexOf(playerid), 1);
}
all_photos.forEach(photo => {
if (photo.selected) return;
let id = photo.playerid;
let ret = JSON.parse(Backend.callLuaFunction(
"CanUseCardToTarget",
[card, id, selected_targets]
));
photo.selectable = ret;
})
if (candidate) {
all_photos.forEach(photo => {
if (photo.selected) return;
let id = photo.playerid;
let ret = JSON.parse(Backend.callLuaFunction(
"CanUseCardToTarget",
[card, id, selected_targets]
));
photo.selectable = ret;
})
okButton.enabled = JSON.parse(Backend.callLuaFunction(
"CardFeasible", [card, selected_targets]
));
okButton.enabled = JSON.parse(Backend.callLuaFunction(
"CardFeasible", [card, selected_targets]
));
} else {
all_photos.forEach(photo => {
photo.state = "normal";
photo.selected = false;
});
okButton.enabled = false;
}
}
callbacks["RemovePlayer"] = function(jsonData) {
@ -469,6 +496,47 @@ callbacks["AskForChoice"] = function(jsonData) {
});
}
callbacks["AskForCardChosen"] = function(jsonData) {
// jsonData: [ int[] handcards, int[] equips, int[] delayedtricks,
// string reason ]
let data = JSON.parse(jsonData);
let handcard_ids = data[0];
let equip_ids = data[1];
let delayedTrick_ids = data[2];
let reason = data[3];
let handcards = [];
let equips = [];
let delayedTricks = [];
handcard_ids.forEach(id => {
let card_data = JSON.parse(Backend.callLuaFunction("GetCardData", [id]));
handcards.push(card_data);
});
equip_ids.forEach(id => {
let card_data = JSON.parse(Backend.callLuaFunction("GetCardData", [id]));
equips.push(card_data);
});
delayedTrick_ids.forEach(id => {
let card_data = JSON.parse(Backend.callLuaFunction("GetCardData", [id]));
delayedTricks.push(card_data);
});
roomScene.promptText = Backend.translate("#AskForChooseCard")
.arg(Backend.translate(reason));
roomScene.state = "replying";
roomScene.popupBox.source = "RoomElement/PlayerCardBox.qml";
let box = roomScene.popupBox.item;
box.addHandcards(handcards);
box.addEquips(equips);
box.addDelayedTricks(delayedTricks);
roomScene.popupBox.moveToCenter();
box.cardSelected.connect(function(cid){
replyToServer(cid);
});
}
callbacks["MoveCards"] = function(jsonData) {
// jsonData: merged moves
let moves = JSON.parse(jsonData);
@ -481,5 +549,26 @@ callbacks["PlayCard"] = function(jsonData) {
if (playerId == Self.id) {
roomScene.promptText = Backend.translate("#PlayCard");
roomScene.state = "playing";
okButton.enabled = false;
}
}
callbacks["LoseSkill"] = function(jsonData) {
// jsonData: [ int player_id, string skill_name ]
let data = JSON.parse(jsonData);
let id = data[0];
let skill_name = data[1];
if (id === Self.id) {
dashboard.loseSkill(skill_name);
}
}
callbacks["AddSkill"] = function(jsonData) {
// jsonData: [ int player_id, string skill_name ]
let data = JSON.parse(jsonData);
let id = data[0];
let skill_name = data[1];
if (id === Self.id) {
dashboard.addSkill(skill_name);
}
}

View File

@ -1,4 +1,4 @@
import QtQuick 2.15
import QtQuick
Rectangle {
function show(text, duration) {

View File

@ -1,4 +1,4 @@
import QtQuick 2.15
import QtQuick
// copy from https://gist.github.com/jonmcclung/bae669101d17b103e94790341301c129
// and modified some code

View File

@ -1,16 +1,24 @@
import QtQuick 2.15
import QtQuick.Controls 2.0
import QtQuick.Window 2.15
import QtQuick
import QtQuick.Controls
import QtQuick.Window
import "Logic.js" as Logic
import "Pages"
Window {
id: mainWindow
visible: true
width: 720
height: 480
width: 960
height: 540
property var callbacks: Logic.callbacks
Item {
id: mainWindow
width: (parent.width / parent.height < 960 / 540)
? 960 : 540 * parent.width / parent.height
height: (parent.width / parent.height > 960 / 540)
? 540 : 960 * parent.height / parent.width
scale: parent.width / width
anchors.centerIn: parent
Image {
source: AppPath + "/image/background"
anchors.fill: parent
@ -130,6 +138,7 @@ Window {
}
}
}
}
onClosing: {
Backend.quitLobby();

View File

@ -31,21 +31,28 @@ set(freekill_HEADERS
if (WIN32)
set(LUA_LIB ${PROJECT_SOURCE_DIR}/lib/win/lua54.dll)
set(SQLITE3_LIB ${PROJECT_SOURCE_DIR}/lib/win/sqlite3.dll)
elseif (ANDROID)
set(LUA_LIB ${PROJECT_SOURCE_DIR}/lib/android/liblua54.so)
set(SQLITE3_LIB ${PROJECT_SOURCE_DIR}/lib/android/libsqlite3.so)
set_target_properties(FreeKill PROPERTIES
QT_ANDROID_PACKAGE_SOURCE_DIR ${PROJECT_SOURCE_DIR}/android
QT_ANDROID_EXTRA_LIBS "${LUA_LIB};${SQLITE3_LIB}"
)
else ()
set(LUA_LIB lua5.4)
set(SQLITE3_LIB sqlite3)
endif ()
source_group("Include" FILES ${freekill_HEADERS})
add_executable(FreeKill ${freekill_SRCS})
target_sources(FreeKill PRIVATE ${freekill_SRCS})
target_precompile_headers(FreeKill PRIVATE "pch.h")
target_link_libraries(FreeKill ${LUA_LIB} ${SQLITE3_LIB} Qt5::Qml Qt5::Gui Qt5::Network Qt5::Multimedia)
file(GLOB SWIG_FILES "${PROJECT_SOURCE_DIR}/src/swig/*.i")
add_custom_command(
OUTPUT ${PROJECT_SOURCE_DIR}/src/swig/freekill-wrap.cxx
DEPENDS ${SWIG_FILES}
COMMENT "Generating freekill-wrap.cxx"
COMMAND swig -c++ -lua -Wall -o
${PROJECT_SOURCE_DIR}/src/swig/freekill-wrap.cxx
${PROJECT_SOURCE_DIR}/src/swig/freekill.i
target_link_libraries(FreeKill PRIVATE
${LUA_LIB}
${SQLITE3_LIB}
fkparse
Qt6::Qml
Qt6::Gui
Qt6::Network
Qt6::Multimedia
)

View File

@ -1,12 +1,46 @@
#include "qmlbackend.h"
#include "server.h"
#ifdef Q_OS_ANDROID
static bool copyPath(const QString &srcFilePath, const QString &tgtFilePath)
{
QFileInfo srcFileInfo(srcFilePath);
if (srcFileInfo.isDir()) {
QDir targetDir(tgtFilePath);
if (!targetDir.exists()) {
targetDir.cdUp();
if (!targetDir.mkdir(QFileInfo(tgtFilePath).fileName()))
return false;
}
QDir sourceDir(srcFilePath);
QStringList fileNames = sourceDir.entryList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot | QDir::Hidden | QDir::System);
foreach (const QString &fileName, fileNames) {
const QString newSrcFilePath
= srcFilePath + QLatin1Char('/') + fileName;
const QString newTgtFilePath
= tgtFilePath + QLatin1Char('/') + fileName;
if (!copyPath(newSrcFilePath, newTgtFilePath))
return false;
}
} else {
QFile::remove(tgtFilePath);
if (!QFile::copy(srcFilePath, tgtFilePath))
return false;
}
return true;
}
#endif
int main(int argc, char *argv[])
{
QCoreApplication *app;
QCoreApplication::setApplicationName("FreeKill");
QCoreApplication::setApplicationVersion("Alpha 0.0.1");
#ifdef Q_OS_ANDROID
copyPath("assets:/res", QDir::currentPath());
#endif
QCommandLineParser parser;
parser.setApplicationDescription("FreeKill server");
parser.addHelpOption();

View File

@ -165,13 +165,13 @@ void Server::handleNameAndPassword(ClientSocket *client, const QString& name, co
{
// First check the name and password
// Matches a string that does not contain special characters
QRegExp nameExp("[^\\0000-\\0057\\0072-\\0100\\0133-\\0140\\0173-\\0177]+");
QRegularExpression nameExp("[\\000-\\057\\072-\\100\\133-\\140\\173-\\177]");
QByteArray passwordHash = QCryptographicHash::hash(password.toLatin1(), QCryptographicHash::Sha256).toHex();
bool passed = false;
QString error_msg;
QJsonObject result;
if (nameExp.exactMatch(name)) {
if (!nameExp.match(name).hasMatch()) {
// Then we check the database,
QString sql_find = QString("SELECT * FROM userinfo \
WHERE name='%1';").arg(name);

View File

@ -9,6 +9,8 @@ public:
static QString pwd();
static bool exists(const QString &file);
static bool isDir(const QString &file);
void parseFkp(const QString &file);
};
extern QmlBackend *Backend;

View File

@ -9,6 +9,13 @@ QmlBackend::QmlBackend(QObject* parent)
{
Backend = this;
engine = nullptr;
parser = fkp_new_parser();
}
QmlBackend::~QmlBackend()
{
Backend = nullptr;
fkp_close(parser);
}
QQmlApplicationEngine *QmlBackend::getEngine() const
@ -104,21 +111,21 @@ QString QmlBackend::translate(const QString &src) {
void QmlBackend::pushLuaValue(lua_State *L, QVariant v) {
QVariantList list;
switch(v.type()) {
case QVariant::Bool:
switch (v.typeId()) {
case QMetaType::Bool:
lua_pushboolean(L, v.toBool());
break;
case QVariant::Int:
case QVariant::UInt:
case QMetaType::Int:
case QMetaType::UInt:
lua_pushinteger(L, v.toInt());
break;
case QVariant::Double:
case QMetaType::Double:
lua_pushnumber(L, v.toDouble());
break;
case QVariant::String:
case QMetaType::QString:
lua_pushstring(L, v.toString().toUtf8().data());
break;
case QVariant::List:
case QMetaType::QVariantList:
lua_newtable(L);
list = v.toList();
for (int i = 1; i <= list.length(); i++) {
@ -128,7 +135,7 @@ void QmlBackend::pushLuaValue(lua_State *L, QVariant v) {
}
break;
default:
qDebug() << "cannot handle QVariant type" << v.type();
qDebug() << "cannot handle QVariant type" << v.typeId();
lua_pushnil(L);
break;
}
@ -154,3 +161,57 @@ QString QmlBackend::callLuaFunction(const QString &func_name,
lua_pop(L, 1);
return QString(result);
}
void QmlBackend::parseFkp(const QString &fileName) {
if (!QFile::exists(fileName)) {
// errorEdit->setText(tr("File does not exist!"));
return;
}
QString cwd = QDir::currentPath();
QStringList strlist = fileName.split('/');
QString shortFileName = strlist.last();
strlist.removeLast();
QString path = strlist.join('/');
QDir::setCurrent(path);
bool error = fkp_parse(
parser,
shortFileName.toUtf8().data(),
FKP_QSAN_LUA
);
/* setError(error);
if (error) {
QStringList tmplist = shortFileName.split('.');
tmplist.removeLast();
QString fName = tmplist.join('.') + "-error.txt";
if (!QFile::exists(fName)) {
errorEdit->setText(tr("Unknown compile error."));
} else {
QFile f(fName);
f.open(QIODevice::ReadOnly);
errorEdit->setText(f.readAll());
f.remove();
}
} else {
errorEdit->setText(tr("Successfully compiled chosen file."));
}
*/
QDir::setCurrent(cwd);
}
static void copyFkpHash2QHash(QHash<QString, QString> &dst, fkp_hash *from) {
dst.clear();
for (size_t i = 0; i < from->capacity; i++) {
if (from->entries[i].key != NULL) {
dst[from->entries[i].key] = QString((const char *)from->entries[i].value);
}
}
}
void QmlBackend::readHashFromParser() {
copyFkpHash2QHash(generals, parser->generals);
copyFkpHash2QHash(skills, parser->skills);
copyFkpHash2QHash(marks, parser->marks);
}

View File

@ -1,10 +1,13 @@
#ifndef _QMLBACKEND_H
#define _QMLBACKEND_H
#include "fkparse.h"
class QmlBackend : public QObject {
Q_OBJECT
public:
QmlBackend(QObject *parent = nullptr);
~QmlBackend();
QQmlApplicationEngine *getEngine() const;
void setEngine(QQmlApplicationEngine *engine);
@ -29,14 +32,21 @@ public:
Q_INVOKABLE QString translate(const QString &src);
Q_INVOKABLE QString callLuaFunction(const QString &func_name,
QVariantList params);
// support fkp
Q_INVOKABLE void parseFkp(const QString &filename);
signals:
void notifyUI(const QString &command, const QString &jsonData);
private:
QQmlApplicationEngine *engine;
fkp_parser *parser;
QHash<QString, QString> generals;
QHash<QString, QString> skills;
QHash<QString, QString> marks;
void pushLuaValue(lua_State *L, QVariant v);
void readHashFromParser();
};
extern QmlBackend *Backend;