Merge pull request 'turn-order' (#17) from turn-order into main

Reviewed-on: #17
This commit is contained in:
PeaAshMeter 2025-11-09 17:56:09 +03:00
commit c16870102b
10 changed files with 178 additions and 19 deletions

View File

@ -19,6 +19,7 @@ end
function behavior:endCast() function behavior:endCast()
self.state = "idle" self.state = "idle"
self.cast = nil self.cast = nil
Tree.level.turnOrder:reorder()
Tree.level.selector:unlock() Tree.level.selector:unlock()
end end

View File

@ -1,16 +1,22 @@
--- @class StatsBehavior : Behavior --- @class StatsBehavior : Behavior
--- @field hp integer --- @field hp integer
--- @field mana integer --- @field mana integer
--- @field initiative integer
--- @field isInTurnOrder boolean
local behavior = {} local behavior = {}
behavior.__index = behavior behavior.__index = behavior
behavior.id = "stats" behavior.id = "stats"
--- @param hp? integer --- @param hp? integer
--- @param mana? integer --- @param mana? integer
function behavior.new(hp, mana) --- @param initiative? integer
--- @param isInTurnOrder? boolean
function behavior.new(hp, mana, initiative, isInTurnOrder)
return setmetatable({ return setmetatable({
hp = hp or 20, hp = hp or 20,
mana = mana or 10 mana = mana or 10,
initiative = initiative or 10,
isInTurnOrder = isInTurnOrder or true
}, behavior) }, behavior)
end end

View File

@ -16,8 +16,8 @@ character.__index = character
--- @param spriteDir table --- @param spriteDir table
--- @param position? Vec3 --- @param position? Vec3
--- @param size? Vec3 --- @param size? Vec3
--- @param level? integer --- @param initiative? integer
local function spawn(name, spriteDir, position, size, level) local function spawn(name, spriteDir, position, size, initiative)
local char = {} local char = {}
char = setmetatable(char, character) char = setmetatable(char, character)
@ -28,7 +28,7 @@ local function spawn(name, spriteDir, position, size, level)
char:addBehavior { char:addBehavior {
Tree.behaviors.residentsleeper.new(), Tree.behaviors.residentsleeper.new(),
Tree.behaviors.stats.new(), Tree.behaviors.stats.new(nil, nil, initiative),
Tree.behaviors.map.new(position, size), Tree.behaviors.map.new(position, size),
Tree.behaviors.sprite.new(spriteDir), Tree.behaviors.sprite.new(spriteDir),
Tree.behaviors.spellcaster.new() Tree.behaviors.spellcaster.new()

View File

@ -18,7 +18,9 @@ controls.keymap = {
cameraMoveDown = control("key", "s"), cameraMoveDown = control("key", "s"),
cameraMoveScroll = control("mouse", "3"), cameraMoveScroll = control("mouse", "3"),
fullMana = control("key", "m"), fullMana = control("key", "m"),
select = control("mouse", "1") select = control("mouse", "1"),
endTurnTest = control("key", "e"),
toggleTurns = control("key", "r"),
} }
local currentKeys = {} local currentKeys = {}

View File

@ -7,6 +7,7 @@ local utils = require "lib.utils.utils"
--- @field selector Selector --- @field selector Selector
--- @field camera Camera --- @field camera Camera
--- @field tileGrid TileGrid --- @field tileGrid TileGrid
--- @field turnOrder TurnOrder
local level = {} local level = {}
level.__index = level level.__index = level
@ -24,6 +25,7 @@ local function new(type, template)
tileGrid = (require "lib.level.grid.tile_grid").new(type, template, size), tileGrid = (require "lib.level.grid.tile_grid").new(type, template, size),
selector = (require "lib.level.selector").new(), selector = (require "lib.level.selector").new(),
camera = (require "lib.level.camera").new(), camera = (require "lib.level.camera").new(),
turnOrder = (require "lib.level.turn_order").new(),
}, level) }, level)
end end

View File

@ -27,14 +27,18 @@ function selector:update(dt)
local selectedId = Tree.level.characterGrid:get(Vec3 { mousePosition.x, mousePosition.y }) local selectedId = Tree.level.characterGrid:get(Vec3 { mousePosition.x, mousePosition.y })
if not self.id then if not self.id then
if selectedId ~= Tree.level.turnOrder.current and Tree.level.turnOrder.isTurnsEnabled then return end
return self:select(selectedId) return self:select(selectedId)
else else
local char = Tree.level.characters[self.id] local char = Tree.level.characters[self.id]
char:try(Tree.behaviors.spellcaster, function(b) char:try(Tree.behaviors.spellcaster, function(b)
if not b.cast then if not b.cast then
self:select(selectedId) -- тут какая-то страшная дичь, я даже не уверен что оно работает
return -- зато я точно уверен, что это надо было писать не так
if not selectedId then self:select(selectedId) end
if selectedId ~= Tree.level.turnOrder.current and Tree.level.turnOrder.isTurnsEnabled then return end
return self:select(selectedId)
end end
if b.cast:cast(char, mousePosition) then if b.cast:cast(char, mousePosition) then
self:lock() self:lock()

112
lib/level/turn_order.lua Normal file
View File

@ -0,0 +1,112 @@
local PriorityQueue = require "lib.utils.priority_queue"
local initiativeComparator = function(id_a, id_b)
local res = Tree.level.characters[id_a]:try(Tree.behaviors.stats, function(astats)
local res = Tree.level.characters[id_b]:try(Tree.behaviors.stats, function(bstats)
return astats.initiative > bstats.initiative
end)
return res
end)
return res or false
end
--- @class TurnOrder
--- @field actedQueue PriorityQueue Очередь тех, кто сделал ход в текущем раунде
--- @field pendingQueue PriorityQueue Очередь тех, кто ждет своего хода в текущем раунде
--- @field current? Id Считаем того, кто сейчас ходит, отдельно, т.к. он ВСЕГДА первый в списке
--- @field isTurnsEnabled boolean
local turnOrder = {}
turnOrder.__index = turnOrder
local function new()
return setmetatable({
actedQueue = PriorityQueue.new(initiativeComparator),
pendingQueue = PriorityQueue.new(initiativeComparator),
isTurnsEnabled = true,
}, turnOrder)
end
--- Перемещаем активного персонажа в очередь сходивших
---
--- Если в очереди на ход больше никого нет, заканчиваем раунд
function turnOrder:next()
Tree.level.selector.id = nil
self.actedQueue:insert(self.current)
local next = self.pendingQueue:peek()
if not next then return self:endRound() end
self.current = self.pendingQueue:pop()
end
--- Меняем местами очередь сходивших и не сходивших (пустую)
function turnOrder:endRound()
assert(self.pendingQueue:size() == 0, "[TurnOrder]: tried to end the round before everyone had a turn")
print("[TurnOrder]: end of the round")
self.actedQueue, self.pendingQueue = self.pendingQueue, self.actedQueue
self.current = self.pendingQueue:pop()
end
--- Пересчитать очередность хода
function turnOrder:reorder()
local _acted, _pending = PriorityQueue.new(initiativeComparator), PriorityQueue.new(initiativeComparator)
--- сортировка отдельно кучи не ходивших и ходивших
while self.pendingQueue:peek() do
_pending:insert(self.pendingQueue:pop())
end
while self.actedQueue:peek() do
_acted:insert(self.actedQueue:pop())
end
self.actedQueue, self.pendingQueue = _acted, _pending
local t = {}
for id in self:getOrder(10) do
table.insert(t, id)
end
print("[TurnOrder]: next 10 turns")
print(table.concat(t, ", "))
end
--- Итератор по бесконечной цикличной очереди хода
--- @param count integer?
function turnOrder:getOrder(count)
local order = { self.current }
local _acted, _pending = self.actedQueue:copy(), self.pendingQueue:copy()
local i, j = 0, 0
local nextTurn = false -- если вышли за пределы текущего хода, то сортируем список, чтобы поставить активного персонажа на свое место
return function()
-------------------- Очередь этого хода: активный + не сходившие
i = i + 1
if count and count < 1 then return nil end
if count and i > count then return nil end
if i == 1 then return self.current end
if _pending:peek() then
local next = _pending:pop()
table.insert(order, next)
return next
end
-------------------- Очередь следующих ходов: цикл по всем персонажам в порядке инициативы
if not nextTurn then
while _acted:peek() do
table.insert(order, _acted:pop())
end
table.sort(order, initiativeComparator)
nextTurn = true
end
j = j + 1
if j % #order == 0 then return order[#order] end
return order[j % #order]
end
end
--- @param id Id
function turnOrder:add(id)
self.actedQueue:insert(id) -- новые персонажи по умолчанию попадают в очередь следующего хода
end
return { new = new }

View File

@ -81,8 +81,9 @@ regenerateMana.tag = "dev_mana"
function regenerateMana:cast(caster, target) function regenerateMana:cast(caster, target)
caster:try(Tree.behaviors.stats, function(stats) caster:try(Tree.behaviors.stats, function(stats)
stats.mana = 10 stats.mana = 10
stats.initiative = stats.initiative + 10
end) end)
print(caster.id, "has regenerated mana") print(caster.id, "has regenerated mana and gained initiative")
local sprite = caster:has(Tree.behaviors.sprite) local sprite = caster:has(Tree.behaviors.sprite)
if not sprite then return true end if not sprite then return true end
AnimationNode { AnimationNode {

View File

@ -1,11 +1,11 @@
---@class PriorityQueue ---@class PriorityQueue
---@field private data any[] # внутренний массив-куча (индексация с 1) ---@field private data any[] внутренний массив-куча (индексация с 1)
---@field private cmp fun(a:any, b:any):boolean # компаратор: true, если a выше по приоритету, чем b ---@field private cmp fun(a:any, b:any):boolean компаратор: true, если a выше по приоритету, чем b
local PriorityQueue = {} local PriorityQueue = {}
PriorityQueue.__index = PriorityQueue PriorityQueue.__index = PriorityQueue
---Создать очередь с приоритетом. ---Создать очередь с приоритетом.
---@param cmp fun(a:any, b:any):boolean|nil # если nil, используется a < b (мин-куча) ---@param cmp fun(a:any, b:any):boolean|nil если nil, используется a < b (мин-куча)
---@return PriorityQueue ---@return PriorityQueue
function PriorityQueue.new(cmp) function PriorityQueue.new(cmp)
local self = setmetatable({}, PriorityQueue) local self = setmetatable({}, PriorityQueue)
@ -16,8 +16,8 @@ end
-- ===== Внутренние утилиты ===== -- ===== Внутренние утилиты =====
---@param i integer @индекс узла ---@param i integer индекс узла
---@param j integer @индекс узла ---@param j integer индекс узла
function PriorityQueue:_swap(i, j) function PriorityQueue:_swap(i, j)
self.data[i], self.data[j] = self.data[j], self.data[i] self.data[i], self.data[j] = self.data[j], self.data[i]
end end
@ -103,4 +103,16 @@ function PriorityQueue:is_empty()
return #self.data == 0 return #self.data == 0
end end
--- Shallow-копирование очереди
function PriorityQueue:copy()
local _data = {}
for i, v in ipairs(self.data) do
_data[i] = v
end
return setmetatable({
data = _data,
cmp = self.cmp
}, PriorityQueue)
end
return PriorityQueue return PriorityQueue

View File

@ -9,8 +9,15 @@ function love.conf(t)
end end
function love.load() function love.load()
character.spawn("Foodor", Tree.assets.files.sprites.character) character.spawn("Foodor", Tree.assets.files.sprites.character, nil, nil, 1)
character.spawn("Baris", Tree.assets.files.sprites.character, Vec3 { 3, 3 }) character.spawn("Baris", Tree.assets.files.sprites.character, Vec3 { 3, 3 }, nil, 2)
character.spawn("Foodor Jr", Tree.assets.files.sprites.character, Vec3 { 0, 3 }, nil, 3)
character.spawn("Baris Jr", Tree.assets.files.sprites.character, Vec3 { 0, 6 }, nil, 4)
for id, _ in pairs(Tree.level.characters) do
Tree.level.turnOrder:add(id)
end
Tree.level.turnOrder:endRound()
print("Now playing:", Tree.level.turnOrder.current)
love.window.setMode(1080, 720, { resizable = true, msaa = 4, vsync = true }) love.window.setMode(1080, 720, { resizable = true, msaa = 4, vsync = true })
end end
@ -21,6 +28,18 @@ function love.update(dt)
testLayout:update(dt) -- логика UI-слоя должна отработать раньше всех, потому что нужно перехватить жесты и не пустить их дальше testLayout:update(dt) -- логика UI-слоя должна отработать раньше всех, потому что нужно перехватить жесты и не пустить их дальше
Tree.panning:update(dt) Tree.panning:update(dt)
Tree.level:update(dt) Tree.level:update(dt)
-- для тестов очереди ходов
-- удалить как только появится ui для людей
if Tree.controls:isJustPressed("endTurnTest") then
Tree.level.turnOrder:next()
print("Now playing:", Tree.level.turnOrder.current)
end
if Tree.controls:isJustPressed("toggleTurns") then
print('toggle turns')
Tree.level.turnOrder:toggleTurns()
end
Tree.controls:cache() Tree.controls:cache()
local t2 = love.timer.getTime() local t2 = love.timer.getTime()