Initial flame_lua_runtime package

This commit is contained in:
gem
2026-06-07 22:53:58 +08:00
commit 733b2fb798
262 changed files with 28439 additions and 0 deletions

View File

@@ -0,0 +1,17 @@
---@type RuntimeCommands
local commands = runtime.import("runtime_commands")
local animation = {}
function animation.move_piece(piece_id, path)
return commands.move_path(piece_id, path, {
duration = 0.55,
onComplete = "piece_move_done"
})
end
function animation.toast(text)
return commands.toast(text)
end
return animation

View File

@@ -0,0 +1,91 @@
local board = {
route_length = 40,
finish_progress = 40,
start = {
red = 1,
yellow = 11,
blue = 21,
green = 31
},
homes = {
red = {
{ x = 92, y = 132 },
{ x = 142, y = 132 }
},
yellow = {
{ x = 518, y = 132 },
{ x = 568, y = 132 }
},
blue = {
{ x = 518, y = 548 },
{ x = 568, y = 548 }
},
green = {
{ x = 92, y = 548 },
{ x = 142, y = 548 }
}
},
finish = {
red = {
{ x = 290, y = 330 },
{ x = 250, y = 330 }
},
yellow = {
{ x = 350, y = 330 },
{ x = 390, y = 330 }
},
blue = {
{ x = 350, y = 390 },
{ x = 390, y = 390 }
},
green = {
{ x = 290, y = 390 },
{ x = 250, y = 390 }
}
},
route = {}
}
local function add(x, y)
table.insert(board.route, { x = x, y = y })
end
for i = 0, 9 do add(200 + i * 32, 104) end
for i = 0, 9 do add(520, 136 + i * 32) end
for i = 0, 9 do add(488 - i * 32, 456) end
for i = 0, 9 do add(168, 424 - i * 32) end
function board.home_position(piece)
local index = piece.id:sub(-1) == "2" and 2 or 1
return board.homes[piece.owner][index]
end
function board.finish_position(piece)
local index = piece.id:sub(-1) == "2" and 2 or 1
return board.finish[piece.owner][index]
end
function board.route_index(owner, progress)
local start = board.start[owner]
local index = start + progress - 1
while index > board.route_length do
index = index - board.route_length
end
return index
end
function board.route_position(owner, progress)
return board.route[board.route_index(owner, progress)]
end
function board.position(piece)
if piece.status == "home" then
return board.home_position(piece)
end
if piece.status == "finished" then
return board.finish_position(piece)
end
return board.route_position(piece.owner, piece.progress)
end
return board

View File

@@ -0,0 +1,154 @@
local state = runtime.import("state")
local rules = runtime.import("rules")
local ui = runtime.import("ui")
local animation = runtime.import("animation")
---@type RuntimeWidgets
local widgets = runtime.import("runtime_widgets")
local theme = runtime.import("theme")
widgets.configure({
primary = theme.colors.dice_button,
secondary = theme.colors.board_inner,
success = theme.colors.green,
overlay = "#99000000",
surface = theme.colors.top_bar,
surfaceAlt = theme.colors.board_inner,
card = theme.colors.top_bar,
text = theme.colors.text,
muted = theme.colors.muted_text,
progress = theme.colors.green,
transparent = "#00000000"
})
function smoke_test(ctx)
return ctx ~= nil
and ctx.runtimeApiVersion ~= nil
and state.current_player ~= nil
and rules.plan_move ~= nil
and ui.create_board_nodes ~= nil
and animation.move_piece ~= nil
and widgets.dialog ~= nil
end
function init(ctx)
return {
render = { creates = ui.create_board_nodes() },
ui = { creates = ui.create_ui_nodes() },
commands = {}
}
end
local function handle_roll_dice()
if state.phase == "game_over" then
return { commands = { animation.toast("游戏已结束") } }
end
if state.phase ~= "waiting_roll" then
return { commands = { animation.toast("请先完成当前移动") } }
end
state.dice = rules.next_dice()
local movable = rules.movable_pieces()
if #movable == 0 then
local message = state.current_player .. " 无可移动飞机"
if state.dice ~= 6 then
state.current_player = rules.next_player()
end
state.phase = "waiting_roll"
return {
ui = { updates = ui.dice_and_status_updates() },
render = { updates = ui.highlight_updates(rules.all_piece_ids(), false) },
commands = { animation.toast(message) }
}
end
state.phase = "waiting_piece"
return {
ui = { updates = ui.dice_and_status_updates() },
render = { updates = ui.highlight_updates(movable, true) }
}
end
local function handle_piece_tap(piece_id)
if state.phase ~= "waiting_piece" then
return {}
end
local piece = state.pieces[piece_id]
if piece == nil or piece.owner ~= state.current_player or not rules.can_move(piece, state.dice) then
return { commands = { animation.toast("该飞机不能移动") } }
end
local plan = rules.plan_move(piece, state.dice)
state.phase = "animating"
state.selected_piece = piece_id
state.pending_move = plan
return {
ui = { updates = ui.dice_and_status_updates() },
render = { updates = ui.highlight_updates(rules.all_piece_ids(), false) },
commands = { animation.move_piece(piece_id, plan.path) }
}
end
local function handle_move_done()
local plan = state.pending_move
if plan == nil then
state.phase = "waiting_roll"
return { ui = { updates = ui.dice_and_status_updates() } }
end
rules.commit_move(plan)
local moved_piece = state.pieces[plan.piece_id]
local updates = ui.piece_position_updates(plan.captures)
table.insert(updates, ui.piece_position_update(plan.piece_id))
local messages = {}
if #plan.captures > 0 then
table.insert(messages, "撞回 " .. tostring(#plan.captures) .. " 架飞机")
end
if rules.player_finished(moved_piece.owner) then
state.winner = moved_piece.owner
state.phase = "game_over"
table.insert(messages, moved_piece.owner .. " 获胜")
else
if state.dice ~= 6 then
state.current_player = rules.next_player()
else
table.insert(messages, "掷出 6额外回合")
end
state.phase = "waiting_roll"
end
state.selected_piece = nil
state.pending_move = nil
local commands = {}
if #messages > 0 then
commands = { animation.toast(table.concat(messages, "")) }
end
return {
ui = { updates = ui.dice_and_status_updates() },
render = { updates = updates },
commands = commands
}
end
function on_event(event)
if event.handler == "roll_dice" then
return handle_roll_dice()
end
if event.handler == "piece_tap" then
return handle_piece_tap(event.target)
end
if event.handler == "piece_move_done" then
return handle_move_done()
end
return {}
end

View File

@@ -0,0 +1,153 @@
local state = runtime.import("state")
local board = runtime.import("board")
local rules = {}
local function piece_index(piece_id)
return piece_id:sub(-1) == "2" and 2 or 1
end
function rules.next_player()
for index, player in ipairs(state.players) do
if player == state.current_player then
local next_index = index + 1
if next_index > #state.players then
next_index = 1
end
return state.players[next_index]
end
end
return state.players[1]
end
function rules.next_dice()
state.dice_index = state.dice_index + 1
if state.dice_index > #state.dice_values then
state.dice_index = 1
end
return state.dice_values[state.dice_index]
end
function rules.can_move(piece, dice)
if piece == nil or piece.status == "finished" then
return false
end
if piece.status == "home" then
return dice == 6
end
return true
end
function rules.movable_pieces()
local result = {}
for id, piece in pairs(state.pieces) do
if piece.owner == state.current_player and rules.can_move(piece, state.dice) then
table.insert(result, id)
end
end
return result
end
function rules.all_piece_ids()
local result = {}
for id, _ in pairs(state.pieces) do
table.insert(result, id)
end
return result
end
function rules.piece_position(piece)
return board.position(piece)
end
function rules.plan_move(piece, dice)
local path = {}
local final_status = "route"
local final_progress = piece.progress
if piece.status == "home" then
final_progress = 1
table.insert(path, board.route_position(piece.owner, final_progress))
else
for step = 1, dice do
local progress = piece.progress + step
if progress >= board.finish_progress then
final_status = "finished"
final_progress = board.finish_progress
table.insert(path, board.finish_position(piece))
break
end
table.insert(path, board.route_position(piece.owner, progress))
final_progress = progress
end
end
local captures = {}
if final_status == "route" then
local landing_index = board.route_index(piece.owner, final_progress)
for id, other in pairs(state.pieces) do
if id ~= piece.id and other.owner ~= piece.owner and other.status == "route" then
if board.route_index(other.owner, other.progress) == landing_index then
table.insert(captures, id)
end
end
end
end
return {
piece_id = piece.id,
final_status = final_status,
final_progress = final_progress,
path = path,
captures = captures
}
end
function rules.commit_move(plan)
local piece = state.pieces[plan.piece_id]
piece.status = plan.final_status
piece.progress = plan.final_progress
for _, captured_id in ipairs(plan.captures) do
local captured = state.pieces[captured_id]
captured.status = "home"
captured.progress = 0
end
end
function rules.player_finished(owner)
for _, piece in pairs(state.pieces) do
if piece.owner == owner and piece.status ~= "finished" then
return false
end
end
return true
end
function rules.status_text()
if state.winner ~= nil then
return "胜利者: " .. state.winner
end
if state.phase == "waiting_roll" then
return "" .. state.current_player .. " 掷骰子"
end
if state.phase == "waiting_piece" then
return "请选择 " .. state.current_player .. " 的飞机"
end
if state.phase == "animating" then
return "飞机移动中..."
end
return "准备中"
end
function rules.piece_label(piece)
if piece.status == "home" then
return "待飞"
end
if piece.status == "finished" then
return "到达"
end
return tostring(piece.progress)
end
return rules

View File

@@ -0,0 +1,589 @@
---@meta
--- COMMON RUNTIME TYPES SECTION.
--- Source of truth: tool/lua_runtime_defs_common.lua
--- After editing this common section, run:
--- dart run tool/generate_lua_runtime_defs.dart
---@alias RuntimeNodeType
---| 'panel'
---| 'button'
---| 'text'
---| 'circle'
---| 'rect'
---| 'line'
---| 'progress'
---| 'listView'
---| 'sprite'
---| 'image'
---| 'spine'
---| 'particle'
---@alias RuntimeAnchor
---| 'center'
---| 'topLeft'
---| 'topRight'
---| 'bottomLeft'
---| 'bottomRight'
---@alias RuntimeTextAlign
---| 'left'
---| 'center'
---| 'right'
---@alias RuntimeParticlePreset
---| 'burst'
---| 'trail'
---| 'snow'
---| 'confetti'
---@alias RuntimeCommandType
---| 'move_path'
---| 'move_to'
---| 'fade_to'
---| 'scale_to'
---| 'rotate_to'
---| 'remove_node'
---| 'sequence'
---| 'parallel'
---| 'delay'
---| 'toast'
---| 'play_sound'
---| 'play_bgm'
---| 'pause_bgm'
---| 'resume_bgm'
---| 'stop_bgm'
---| 'preload_resources'
---| 'evict_resources'
---| 'cancel_commands'
---| 'play_spine_animation'
---| 'copy_text'
---@alias RuntimeEventType
---| 'tap'
---| 'animation_done'
---| 'resize'
---| 'scroll'
---@alias RuntimeScaleMode
---| 'fit'
---| 'fill'
---| 'stretch'
---| 'none'
---@alias RuntimeLayoutAlign
---| 'start'
---| 'center'
---| 'end'
---@alias RuntimeButtonVariant
---| 'primary'
---| 'secondary'
---| 'ghost'
---@class (exact) RuntimeNode
---@field id string
---@field type RuntimeNodeType
---@field parent? string
---@field asset? string Normal image/sprite/spine asset key. For button nodes this is the normal-state image.
---@field pressedAsset? string Button pressed-state image asset key.
---@field disabledAsset? string Button disabled-state image asset key.
---@field animation? string
---@field skin? string
---@field loop? boolean
---@field text? string
---@field x? number
---@field y? number
---@field width? number
---@field height? number
---@field paddingLeft? number
---@field paddingTop? number
---@field paddingRight? number
---@field paddingBottom? number
---@field anchor? RuntimeAnchor
---@field layer? integer
---@field visible? boolean
---@field alpha? number
---@field scale? number
---@field rotation? number
---@field color? string
---@field fontSize? number
---@field textAlign? RuntimeTextAlign
---@field radius? number
---@field strokeWidth? number
---@field value? number
---@field scrollX? number
---@field scrollY? number
---@field contentWidth? number
---@field contentHeight? number
---@field virtualized? boolean
---@field cacheExtent? number
---@field inertia? boolean
---@field scrollbarThumbColor? string
---@field scrollbarTrackColor? string
---@field scrollbarThickness? number
---@field scrollbarVisible? boolean
---@field interactive? boolean
---@field onTap? string
---@field onScroll? string
---@field preset? RuntimeParticlePreset
---@field count? integer
---@field duration? number
---@field speedMin? number
---@field speedMax? number
---@field gravityX? number
---@field gravityY? number
---@field spread? number
---@field colorTo? string
---@field radiusTo? number
---@field autoRemove? boolean
---@field fadeOut? boolean
---@class (exact) RuntimeNodeProps
---@field type? RuntimeNodeType
---@field parent? string
---@field asset? string Normal image/sprite/spine asset key. For button nodes this is the normal-state image.
---@field pressedAsset? string Button pressed-state image asset key.
---@field disabledAsset? string Button disabled-state image asset key.
---@field animation? string
---@field skin? string
---@field loop? boolean
---@field text? string
---@field x? number
---@field y? number
---@field width? number
---@field height? number
---@field paddingLeft? number
---@field paddingTop? number
---@field paddingRight? number
---@field paddingBottom? number
---@field anchor? RuntimeAnchor
---@field layer? integer
---@field visible? boolean
---@field alpha? number
---@field scale? number
---@field rotation? number
---@field color? string
---@field fontSize? number
---@field textAlign? RuntimeTextAlign
---@field radius? number
---@field strokeWidth? number
---@field value? number
---@field scrollX? number
---@field scrollY? number
---@field contentWidth? number
---@field contentHeight? number
---@field virtualized? boolean
---@field cacheExtent? number
---@field inertia? boolean
---@field scrollbarThumbColor? string
---@field scrollbarTrackColor? string
---@field scrollbarThickness? number
---@field scrollbarVisible? boolean
---@field interactive? boolean
---@field onTap? string
---@field onScroll? string
---@field preset? RuntimeParticlePreset
---@field count? integer
---@field duration? number
---@field speedMin? number
---@field speedMax? number
---@field gravityX? number
---@field gravityY? number
---@field spread? number
---@field colorTo? string
---@field radiusTo? number
---@field autoRemove? boolean
---@field fadeOut? boolean
---Helper-only fields accepted by runtime_ui/runtime_widgets. They are normalized
---before the node/update crosses the Dart Runtime protocol boundary.
---@class RuntimeNodeInit: RuntimeNodeProps
---@field w? number Alias for width.
---@field h? number Alias for height.
---@field size? number Alias for both width and height.
---@field handler? string Alias for onTap.
---@field onClick? string Alias for onTap.
---@class (exact) RuntimeNodeUpdate
---@field id string
---@field props RuntimeNodeProps
---@class (exact) RuntimeNodeRemove
---@field id string
---@class (exact) RuntimeDiffSection
---@field creates? RuntimeNode[]
---@field updates? RuntimeNodeUpdate[]
---@field removes? (string|RuntimeNodeRemove)[]
---@class (exact) RuntimeDiff
---@field render? RuntimeDiffSection
---@field ui? RuntimeDiffSection
---@field commands? RuntimeCommand[]
---@class (exact) RuntimeEvent
---@field type RuntimeEventType|string
---@field target? string
---@field handler? string
---@field x? number
---@field y? number
---@field data? table
---@class (exact) RuntimeCommand
---@field type RuntimeCommandType
---@field target? string
---@field scope? string
---@field id? string
---@field group? string
---@field commandGroup? string
---@field onComplete? string
---@field duration? number
---@field commands? RuntimeCommand[]
---@field path? RuntimePoint[]
---@field x? number
---@field y? number
---@field alpha? number
---@field scale? number
---@field angle? number
---@field text? string
---@field message? string
---@field asset? string
---@field name? string
---@field volume? number
---@field channel? string
---@field loop? boolean
---@field failOnError? boolean
---@field animation? string
---@field track? integer
---@field queue? boolean
---@field delay? number
---@class (exact) RuntimeCommandOpts
---@field id? string
---@field group? string
---@field commandGroup? string
---@field scope? string
---@field onComplete? string
---@field duration? number
---@class (exact) RuntimeAudioCommandOpts: RuntimeCommandOpts
---@field volume? number
---@field name? string
---@class (exact) RuntimeBgmCommandOpts: RuntimeAudioCommandOpts
---@field channel? string
---@field loop? boolean
---@class (exact) RuntimeSpineCommandOpts: RuntimeCommandOpts
---@field track? integer
---@field loop? boolean
---@field queue? boolean
---@field delay? number
---@class (exact) RuntimeResourceCommandOpts: RuntimeCommandOpts
---@field failOnError? boolean
---@class (exact) RuntimePoint
---@field x number
---@field y number
---@class (exact) RuntimeLocaleContext
---@field requested string
---@field resolved string
---@field default string
---@field supported string[]
---@field languageCode string
---@field scriptCode? string
---@field countryCode? string
---@class (exact) RuntimeScreenContext
---@field width number
---@field height number
---@class (exact) RuntimeDesignContext
---@field width number
---@field height number
---@class (exact) RuntimeViewportContext
---@field x number
---@field y number
---@field width number
---@field height number
---@field scaleX number
---@field scaleY number
---@field scaleMode RuntimeScaleMode|string
---@class (exact) RuntimeContext
---@field screen RuntimeScreenContext
---@field design RuntimeDesignContext
---@field viewport RuntimeViewportContext
---@field seed integer
---@field runtimeApiVersion integer
---@field gameId string
---@field gameVersion string
---@field locale? RuntimeLocaleContext
---@class RuntimeUi
---@field style fun(base?: RuntimeNodeProps, opts?: RuntimeNodeProps): RuntimeNodeProps
---@field with_parent fun(parent: string, opts?: RuntimeNodeProps): RuntimeNodeProps
---@field node fun(node_type: RuntimeNodeType, id: string, opts?: RuntimeNodeInit): RuntimeNode
---@field panel fun(id: string, x: number|RuntimeNodeInit, y?: number, width?: number, height?: number, opts?: RuntimeNodeInit): RuntimeNode
---@field rect fun(id: string, x: number|RuntimeNodeInit, y?: number, width?: number, height?: number, opts?: RuntimeNodeInit): RuntimeNode
---@field circle fun(id: string, x: number|RuntimeNodeInit, y?: number, size?: number, opts?: RuntimeNodeInit): RuntimeNode
---@field line fun(id: string, x: number|RuntimeNodeInit, y?: number, width?: number, height?: number, opts?: RuntimeNodeInit): RuntimeNode
---@field progress fun(id: string, x: number|RuntimeNodeInit, y?: number, width?: number, height?: number, value?: number, opts?: RuntimeNodeInit): RuntimeNode
---@field particle fun(id: string, x: number|RuntimeNodeInit, y?: number, width?: number, height?: number, opts?: RuntimeNodeInit): RuntimeNode
---@field text fun(id: string, text: string|RuntimeNodeInit, x?: number, y?: number, width?: number, height?: number, opts?: RuntimeNodeInit): RuntimeNode
---@field button fun(id: string, text: string|RuntimeNodeInit, x?: number, y?: number, width?: number, height?: number, handler?: string, opts?: RuntimeNodeInit): RuntimeNode
---@field list_view fun(id: string, x: number|RuntimeNodeInit, y?: number, width?: number, height?: number, opts?: RuntimeNodeInit): RuntimeNode
---@field image fun(id: string, asset: string|RuntimeNodeInit, x?: number, y?: number, width?: number, height?: number, opts?: RuntimeNodeInit): RuntimeNode
---@field sprite fun(id: string, asset: string|RuntimeNodeInit, x?: number, y?: number, width?: number, height?: number, opts?: RuntimeNodeInit): RuntimeNode
---@field spine fun(id: string, asset: string|RuntimeNodeInit, x?: number, y?: number, width?: number, height?: number, animation?: string, opts?: RuntimeNodeInit): RuntimeNode
---@field update fun(id: string, props: RuntimeNodeInit): RuntimeNodeUpdate
---@field text_update fun(id: string, text: string): RuntimeNodeUpdate
---@field visible_update fun(id: string, visible: boolean): RuntimeNodeUpdate
---@field alpha_update fun(id: string, alpha: number): RuntimeNodeUpdate
---@field scale_update fun(id: string, scale: number): RuntimeNodeUpdate
---@field position_update fun(id: string, x: number, y: number): RuntimeNodeUpdate
---@field size_update fun(id: string, width: number, height: number): RuntimeNodeUpdate
---@field transform_update fun(id: string, x: number, y: number, scale: number, rotation: number): RuntimeNodeUpdate
---@field batch_update fun(ids: string[], props: RuntimeNodeInit): RuntimeNodeUpdate[]
---@field append fun(nodes: RuntimeNode[], node: RuntimeNode): RuntimeNode[]
---@field append_all fun(nodes: RuntimeNode[], extra_nodes: RuntimeNode[]): RuntimeNode[]
---@class (exact) RuntimeDialogButton
---@field id? string
---@field text string
---@field handler string
---@field color? string
---@class (exact) RuntimeDialogOpts
---@field screenWidth? number
---@field screenHeight? number
---@field overlay? boolean
---@field overlayColor? string
---@field blockInput? boolean
---@field layer? integer
---@field color? string
---@field radius? number
---@field panelStyle? RuntimeNodeProps
---@field titleColor? string
---@field titleSize? number
---@field titleStyle? RuntimeNodeProps
---@field messageColor? string
---@field messageSize? number
---@field messageStyle? RuntimeNodeProps
---@field buttons? RuntimeDialogButton[]
---@field buttonGap? number
---@field buttonStyle? RuntimeNodeProps
---@class RuntimeLabeledProgressOpts: RuntimeNodeInit
---@field labelHeight? number
---@field labelStyle? RuntimeNodeProps
---@class RuntimePillOpts: RuntimeNodeInit
---@field panelStyle? RuntimeNodeProps
---@field textStyle? RuntimeNodeProps
---@class RuntimeTextButtonOpts: RuntimeNodeInit
---@field variant? RuntimeButtonVariant
---@class RuntimeListItemOpts: RuntimeTextButtonOpts
---@field selected? boolean
---@field activeColor? string
---@field inactiveColor? string
---@class RuntimeTabItem
---@field id? string
---@field key? string
---@field text string
---@field handler? string
---@field selected? boolean
---@class RuntimeTabsOpts: RuntimeNodeInit
---@field tabs? RuntimeTabItem[]
---@field selected? string
---@field gap? number
---@field itemWidth? number
---@field itemHeight? number
---@field activeColor? string
---@field inactiveColor? string
---@field buttonStyle? RuntimeNodeProps
---@class RuntimeActionItem
---@field id? string
---@field text string
---@field handler? string
---@field visible? boolean
---@field color? string
---@field style? RuntimeNodeProps
---@class RuntimeActionRowOpts: RuntimeNodeInit
---@field actions? RuntimeActionItem[]
---@field gap? number
---@field itemWidth? number
---@field itemHeight? number
---@field buttonStyle? RuntimeNodeProps
---@class RuntimePanelHeaderOpts: RuntimeNodeInit
---@field eyebrow? string
---@field title string
---@field summary? string
---@field gap? number
---@field eyebrowId? string
---@field titleId? string
---@field summaryId? string
---@field eyebrowHeight? number
---@field titleHeight? number
---@field summaryHeight? number
---@field eyebrowStyle? RuntimeNodeProps
---@field titleStyle? RuntimeNodeProps
---@field summaryStyle? RuntimeNodeProps
---@class RuntimeWidgetTheme
---@field primary? string
---@field secondary? string
---@field success? string
---@field overlay? string
---@field surface? string
---@field surfaceAlt? string
---@field card? string
---@field text? string
---@field muted? string
---@field progress? string
---@field transparent? string
---@class RuntimeWidgets
---@field configure fun(tokens?: RuntimeWidgetTheme): RuntimeWidgets
---@field label fun(id: string, text: string|RuntimeNodeInit, x?: number, y?: number, width?: number, height?: number, opts?: RuntimeNodeInit): RuntimeNode
---@field section_title fun(id: string, text: string|RuntimeNodeInit, x?: number, y?: number, width?: number, height?: number, opts?: RuntimeNodeInit): RuntimeNode
---@field pill fun(id: string, text: string|RuntimePillOpts, x?: number, y?: number, width?: number, height?: number, opts?: RuntimePillOpts): RuntimeNode[]
---@field text_button fun(id: string, text: string|RuntimeTextButtonOpts, x?: number, y?: number, width?: number, height?: number, handler?: string, opts?: RuntimeTextButtonOpts): RuntimeNode
---@field list_item fun(id: string, text: string|RuntimeListItemOpts, x?: number, y?: number, width?: number, height?: number, handler?: string, opts?: RuntimeListItemOpts): RuntimeNode
---@field tabs fun(id: string, tabs: RuntimeTabItem[]|RuntimeTabsOpts, opts?: RuntimeTabsOpts): RuntimeNode[]
---@field action_row fun(id: string, actions: RuntimeActionItem[]|RuntimeActionRowOpts, opts?: RuntimeActionRowOpts): RuntimeNode[]
---@field panel_header fun(id: string, opts: RuntimePanelHeaderOpts): RuntimeNode[]
---@field overlay fun(id: string, width: number|RuntimeNodeInit, height?: number, opts?: RuntimeNodeInit): RuntimeNode
---@field card fun(id: string, x: number|RuntimeNodeInit, y?: number, width?: number, height?: number, opts?: RuntimeNodeInit): RuntimeNode
---@field progress_bar fun(id: string, x: number|RuntimeNodeInit, y?: number, width?: number, height?: number, value?: number, opts?: RuntimeNodeInit): RuntimeNode
---@field labeled_progress fun(id: string, label: string, x: number, y: number, width: number, height: number, value: number, opts?: RuntimeLabeledProgressOpts): RuntimeNode[]
---@field button_row fun(parent: string, id: string, buttons: RuntimeDialogButton[], x: number, y: number, width: number, height: number, gap?: number, opts?: RuntimeNodeProps): RuntimeNode[]
---@field dialog fun(id: string, title: string, message: string, x: number, y: number, width: number, height: number, opts?: RuntimeDialogOpts): RuntimeNode[]
---@class (exact) RuntimeLayoutItem
---@field node RuntimeNode
---@field marginLeft? number
---@field marginRight? number
---@field marginTop? number
---@field marginBottom? number
---@class RuntimeLayoutItemOpts
---@field margin? number
---@field mx? number
---@field my? number
---@field ml? number
---@field mr? number
---@field mt? number
---@field mb? number
---@field marginLeft? number
---@field marginRight? number
---@field marginTop? number
---@field marginBottom? number
---@class RuntimeLinearLayoutOpts
---@field x? number
---@field y? number
---@field width? number
---@field height? number
---@field gap? number
---@field align? RuntimeLayoutAlign
---@field padding? number
---@field paddingX? number
---@field paddingY? number
---@field px? number
---@field py? number
---@field paddingLeft? number
---@field paddingTop? number
---@class RuntimeBoxLayoutOpts: RuntimeLinearLayoutOpts
---@field rows? integer
---@field columns? integer
---@field cols? integer
---@field cellWidth? number
---@field cellHeight? number
---@field cellW? number
---@field cellH? number
---@field gapX? number
---@field gapY? number
---@field valign? RuntimeLayoutAlign
---@class RuntimeLayout
---@field item fun(node: RuntimeNode, opts?: RuntimeLayoutItemOpts): RuntimeLayoutItem
---@field local_position fun(origin: RuntimePoint, position: RuntimePoint): RuntimePoint
---@field row fun(parent?: string, items: (RuntimeNode|RuntimeLayoutItem)[], opts?: RuntimeLinearLayoutOpts): RuntimeNode[]
---@field column fun(parent?: string, items: (RuntimeNode|RuntimeLayoutItem)[], opts?: RuntimeLinearLayoutOpts): RuntimeNode[]
---@field stack fun(parent?: string, items: (RuntimeNode|RuntimeLayoutItem)[], opts?: RuntimeLinearLayoutOpts): RuntimeNode[]
---@field box fun(parent?: string, items: (RuntimeNode|RuntimeLayoutItem)[], opts?: RuntimeBoxLayoutOpts): RuntimeNode[]
---@class RuntimeCommands
---@field toast fun(text: string, opts?: RuntimeCommandOpts): RuntimeCommand
---@field copy_text fun(text: string, opts?: RuntimeCommandOpts): RuntimeCommand
---@field delay fun(duration: number, opts?: RuntimeCommandOpts): RuntimeCommand
---@field sequence fun(items: RuntimeCommand[], opts?: RuntimeCommandOpts): RuntimeCommand
---@field parallel fun(items: RuntimeCommand[], opts?: RuntimeCommandOpts): RuntimeCommand
---@field move_path fun(target: string, path: RuntimePoint[], opts?: RuntimeCommandOpts): RuntimeCommand
---@field move_to fun(target: string, x: number, y: number, opts?: RuntimeCommandOpts): RuntimeCommand
---@field fade_to fun(target: string, alpha: number, opts?: RuntimeCommandOpts): RuntimeCommand
---@field scale_to fun(target: string, scale: number, opts?: RuntimeCommandOpts): RuntimeCommand
---@field rotate_to fun(target: string, angle: number, opts?: RuntimeCommandOpts): RuntimeCommand
---@field remove_node fun(target: string, opts?: RuntimeCommandOpts): RuntimeCommand
---@field play_spine_animation fun(target: string, animation: string, opts?: RuntimeSpineCommandOpts): RuntimeCommand
---@field play_sound fun(asset: string, opts?: RuntimeAudioCommandOpts): RuntimeCommand
---@field play_bgm fun(asset: string, opts?: RuntimeBgmCommandOpts): RuntimeCommand
---@field pause_bgm fun(channel?: string, opts?: RuntimeCommandOpts): RuntimeCommand
---@field resume_bgm fun(channel?: string, opts?: RuntimeCommandOpts): RuntimeCommand
---@field stop_bgm fun(channel?: string, opts?: RuntimeCommandOpts): RuntimeCommand
---@field preload_group fun(group: string, opts?: RuntimeResourceCommandOpts): RuntimeCommand
---@field evict_group fun(group: string, opts?: RuntimeCommandOpts): RuntimeCommand
---@field cancel_id fun(id: string): RuntimeCommand
---@field cancel_group fun(group: string): RuntimeCommand
---@field cancel_scope fun(scope: string): RuntimeCommand
---@class RuntimeImportApi
---@field import fun(moduleName: string): table
---@type RuntimeImportApi
runtime = runtime
---@alias PlayerColor 'red'|'yellow'|'blue'|'green'
---@alias GamePhase 'waiting_roll'|'waiting_piece'|'animating'|'game_over'
---@alias PieceStatus 'home'|'route'|'finished'
---@class (exact) PieceState
---@field id string
---@field owner PlayerColor
---@field progress integer
---@field status PieceStatus
---@class (exact) LudoState
---@field current_player PlayerColor
---@field phase GamePhase
---@field dice_index integer
---@field dice_values integer[]
---@field dice? integer
---@field selected_piece? string
---@field pending_move? table
---@field winner? PlayerColor
---@field players PlayerColor[]
---@field pieces table<string, PieceState>
---@class (exact) BoardPoint
---@field x number
---@field y number
---@class (exact) BoardData
---@field red_home BoardPoint[]
---@field blue_home BoardPoint[]
---@field path BoardPoint[]
---@field start table<PlayerColor, integer>

View File

@@ -0,0 +1,22 @@
---@type table
return {
players = { "red", "yellow", "blue", "green" },
current_player = "red",
phase = "waiting_roll",
dice_index = 0,
dice_values = { 6, 3, 4, 6, 2, 5, 1, 6, 4, 2, 6, 5 },
dice = nil,
selected_piece = nil,
pending_move = nil,
winner = nil,
pieces = {
red_1 = { id = "red_1", owner = "red", status = "home", progress = 0 },
red_2 = { id = "red_2", owner = "red", status = "home", progress = 0 },
yellow_1 = { id = "yellow_1", owner = "yellow", status = "home", progress = 0 },
yellow_2 = { id = "yellow_2", owner = "yellow", status = "home", progress = 0 },
blue_1 = { id = "blue_1", owner = "blue", status = "home", progress = 0 },
blue_2 = { id = "blue_2", owner = "blue", status = "home", progress = 0 },
green_1 = { id = "green_1", owner = "green", status = "home", progress = 0 },
green_2 = { id = "green_2", owner = "green", status = "home", progress = 0 }
}
}

View File

@@ -0,0 +1,81 @@
local theme = runtime.import("theme")
local colors = theme.colors
local styles = {}
styles.layers = {
board = 1,
cell = 2,
home = 3,
plane = 10,
hud = 100,
hud_content = 101
}
styles.board = {
color = colors.board,
layer = styles.layers.board
}
styles.center = {
color = colors.board_inner,
layer = styles.layers.board
}
styles.route_cell = {
anchor = "center",
color = colors.route_cell,
layer = styles.layers.cell
}
styles.home_panel = function(owner)
return {
color = colors[owner .. "_home"],
layer = styles.layers.home
}
end
styles.hud = {
color = colors.top_bar,
layer = styles.layers.hud
}
styles.hud_text = {
fontSize = 20,
color = colors.text,
layer = styles.layers.hud_content
}
styles.small_text = {
fontSize = 14,
color = colors.muted_text,
layer = styles.layers.hud_content
}
styles.dice_button = {
color = colors.dice_button,
fontSize = 20,
layer = styles.layers.hud_content
}
styles.plane = function(owner)
return {
anchor = "center",
color = colors[owner],
layer = styles.layers.plane,
interactive = true,
onTap = "piece_tap",
alpha = 0.95
}
end
styles.plane_highlight = {
scale = 1.25,
alpha = 1
}
styles.plane_normal = {
scale = 1,
alpha = 0.95
}
return styles

View File

@@ -0,0 +1,22 @@
return {
title = "Lua 飞行棋",
colors = {
background = "#0f172a",
board = "#1e293b",
board_inner = "#334155",
route_cell = "#f8fafc",
route_text = "#0f172a",
top_bar = "#020617",
dice_button = "#2563eb",
text = "#ffffff",
muted_text = "#cbd5e1",
red = "#ef4444",
yellow = "#eab308",
blue = "#3b82f6",
green = "#22c55e",
red_home = "#7f1d1d",
yellow_home = "#713f12",
blue_home = "#1e3a8a",
green_home = "#14532d"
}
}

View File

@@ -0,0 +1,130 @@
---@type RuntimeUi
local runtime_ui = runtime.import("runtime_ui")
---@type RuntimeLayout
local layout = runtime.import("layout")
local theme = runtime.import("theme")
local styles = runtime.import("styles")
local state = runtime.import("state")
local board = runtime.import("board")
local rules = runtime.import("rules")
local ui = {}
local function color_name(owner)
if owner == "red" then return "" end
if owner == "yellow" then return "" end
if owner == "blue" then return "" end
if owner == "green" then return "绿" end
return owner
end
local function piece_size(piece)
if piece.status == "finished" then
return 30
end
return 34
end
function ui.highlight_updates(ids, enabled)
return runtime_ui.batch_update(ids, enabled and styles.plane_highlight or styles.plane_normal)
end
function ui.piece_position_update(piece_id)
local piece = state.pieces[piece_id]
local pos = rules.piece_position(piece)
return runtime_ui.update(piece_id, {
x = pos.x,
y = pos.y,
width = piece_size(piece),
height = piece_size(piece),
scale = 1,
alpha = 0.95
})
end
function ui.piece_position_updates(ids)
local updates = {}
for _, id in ipairs(ids) do
table.insert(updates, ui.piece_position_update(id))
end
return updates
end
function ui.all_piece_position_updates()
return ui.piece_position_updates(rules.all_piece_ids())
end
function ui.create_board_nodes()
local nodes = {
runtime_ui.panel("board_panel", 28, 82, 664, 610, styles.board),
runtime_ui.panel("center_airport", 252, 292, 168, 136, styles.center),
runtime_ui.text("board_title", theme.title, 268, 318, 140, 32, styles.hud_text),
runtime_ui.text("board_tip", "掷 6 起飞 · 撞子回家 · 全部到达获胜", 206, 360, 320, 28, styles.small_text),
runtime_ui.panel("home_red", 62, 98, 128, 92, styles.home_panel("red")),
runtime_ui.panel("home_yellow", 498, 98, 128, 92, styles.home_panel("yellow")),
runtime_ui.panel("home_blue", 498, 516, 128, 92, styles.home_panel("blue")),
runtime_ui.panel("home_green", 62, 516, 128, 92, styles.home_panel("green")),
runtime_ui.text("label_red", "红方", 96, 102, 60, 24, styles.small_text),
runtime_ui.text("label_yellow", "黄方", 532, 102, 60, 24, styles.small_text),
runtime_ui.text("label_blue", "蓝方", 532, 520, 60, 24, styles.small_text),
runtime_ui.text("label_green", "绿方", 96, 520, 60, 24, styles.small_text)
}
for index, cell in ipairs(board.route) do
runtime_ui.append(nodes, runtime_ui.circle("route_" .. tostring(index), cell.x, cell.y, 24, styles.route_cell))
end
for _, owner in ipairs(state.players) do
local start = board.route[board.start[owner]]
runtime_ui.append(nodes, runtime_ui.text("start_" .. owner, color_name(owner), start.x - 14, start.y - 14, 36, 24, {
fontSize = 14,
color = theme.colors[owner],
layer = styles.layers.cell + 1
}))
end
for id, piece in pairs(state.pieces) do
local pos = rules.piece_position(piece)
runtime_ui.append(nodes, runtime_ui.circle(id, pos.x, pos.y, piece_size(piece), styles.plane(piece.owner)))
end
return nodes
end
function ui.create_ui_nodes()
local hud_items = layout.row("top_bar", {
runtime_ui.text("turn_text", "当前: 红方", 0, 0, 120, 32, styles.hud_text),
runtime_ui.text("dice_text", "骰子: -", 0, 0, 100, 32, styles.hud_text),
runtime_ui.text("status_text", rules.status_text(), 0, 0, 260, 32, styles.hud_text),
layout.item(
runtime_ui.button("dice_button", "掷骰子", 0, 0, 120, 48, "roll_dice", styles.dice_button),
{ marginLeft = 18 }
)
}, {
x = 24,
height = 76,
gap = 16,
align = "center"
})
local nodes = { runtime_ui.panel("top_bar", 0, 0, 720, 76, styles.hud) }
return runtime_ui.append_all(nodes, hud_items)
end
function ui.turn_name(owner)
return "当前: " .. color_name(owner) .. ""
end
function ui.dice_and_status_updates()
return {
runtime_ui.text_update("dice_text", "骰子: " .. tostring(state.dice or "-")),
runtime_ui.text_update("turn_text", ui.turn_name(state.current_player)),
runtime_ui.text_update("status_text", rules.status_text())
}
end
function ui.status_update()
return { runtime_ui.text_update("status_text", rules.status_text()) }
end
return ui