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,37 @@
---@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.7,
onComplete = "piece_move_done"
})
end
function animation.toast(text)
return commands.toast(text)
end
function animation.play_sound(name)
return commands.play_sound(name)
end
function animation.play_bgm(name, channel)
return commands.play_bgm(name, { channel = channel or "bgm" })
end
function animation.pause_bgm(channel)
return commands.pause_bgm(channel)
end
function animation.resume_bgm(channel)
return commands.resume_bgm(channel)
end
function animation.stop_bgm(channel)
return commands.stop_bgm(channel)
end
return animation

View File

@@ -0,0 +1,20 @@
---@type BoardData
return {
red_home = {
{x = 80, y = 520},
{x = 140, y = 520}
},
blue_home = {
{x = 520, y = 120},
{x = 580, y = 120}
},
path = {
{x = 120, y = 420}, {x = 180, y = 420}, {x = 240, y = 420},
{x = 300, y = 420}, {x = 360, y = 420}, {x = 420, y = 420},
{x = 480, y = 420}, {x = 480, y = 360}, {x = 480, y = 300},
{x = 480, y = 240}, {x = 420, y = 240}, {x = 360, y = 240},
{x = 300, y = 240}, {x = 240, y = 240}, {x = 180, y = 240},
{x = 120, y = 240}, {x = 120, y = 300}, {x = 120, y = 360}
},
start = { red = 1, blue = 10 }
}

View File

@@ -0,0 +1,103 @@
---@class RuntimeI18n
local i18n = {}
local default_locale = "zh-Hans"
local current_locale = default_locale
local messages = {
["zh-Hans"] = {
title = "Lua 飞行棋",
roll_button = "掷骰子",
dice_empty = "骰子: -",
dice_value = "骰子: {value}",
current_player = "当前玩家: {player}",
["player.red"] = "红方",
["player.blue"] = "蓝方",
["toast.move_first"] = "请先移动棋子",
["toast.no_movable_piece"] = "无可移动棋子",
["toast.invalid_piece"] = "该棋子不能移动"
},
en = {
title = "Lua Ludo",
roll_button = "Roll",
dice_empty = "Dice: -",
dice_value = "Dice: {value}",
current_player = "Current player: {player}",
["player.red"] = "Red",
["player.blue"] = "Blue",
["toast.move_first"] = "Move a piece first",
["toast.no_movable_piece"] = "No movable pieces",
["toast.invalid_piece"] = "This piece cannot move"
}
}
---@param locale? string
---@return string
local function normalize_locale(locale)
if type(locale) ~= "string" or locale == "" then
return default_locale
end
locale = string.gsub(locale, "_", "-")
if messages[locale] ~= nil then
return locale
end
local language = string.match(locale, "^([A-Za-z]+)")
if language ~= nil and messages[language] ~= nil then
return language
end
return default_locale
end
---@param value string
---@return string
local function escape_pattern(value)
return string.gsub(value, "([%(%)%.%%%+%-%*%?%[%]%^%$])", "%%%1")
end
---@param template string
---@param vars? table<string, string|number>
---@return string
local function interpolate(template, vars)
if vars == nil then
return template
end
local result = template
for key, value in pairs(vars) do
result = string.gsub(result, "{" .. escape_pattern(key) .. "}", tostring(value))
end
return result
end
---@param ctx? RuntimeContext
function i18n.configure(ctx)
local locale = nil
if ctx ~= nil and ctx.locale ~= nil then
locale = ctx.locale.resolved or ctx.locale.requested or ctx.locale.default
end
current_locale = normalize_locale(locale)
end
---@return string
function i18n.locale()
return current_locale
end
---@param key string
---@param vars? table<string, string|number>
---@return string
function i18n.t(key, vars)
local bundle = messages[current_locale] or messages[default_locale]
local fallback = messages[default_locale]
local value = bundle[key] or fallback[key] or key
return interpolate(value, vars)
end
---@param color PlayerColor
---@return string
function i18n.player(color)
return i18n.t("player." .. color)
end
return i18n

View File

@@ -0,0 +1,117 @@
local state = runtime.import("state")
local rules = runtime.import("rules")
local ui = runtime.import("ui")
local animation = runtime.import("animation")
local i18n = runtime.import("i18n")
---@type RuntimeWidgets
local widgets = runtime.import("runtime_widgets")
local theme = runtime.import("theme")
widgets.configure({
primary = theme.colors.dice_button,
secondary = theme.colors.board,
success = theme.colors.blue,
overlay = "#99000000",
surface = theme.colors.top_bar,
surfaceAlt = theme.colors.board,
card = theme.colors.top_bar,
text = theme.colors.text,
muted = "#ffcbd5e1",
progress = theme.colors.blue,
transparent = "#00000000"
})
function smoke_test(ctx)
i18n.configure(ctx)
return ctx ~= nil
and ctx.runtimeApiVersion ~= nil
and state.current_player ~= nil
and rules.next_player ~= nil
and ui.create_board_nodes ~= nil
and animation.move_piece ~= nil
and widgets.dialog ~= nil
end
function init(ctx)
i18n.configure(ctx)
return {
render = { creates = ui.create_board_nodes() },
ui = { creates = ui.create_ui_nodes() },
commands = {}
}
end
local function handle_roll_dice()
if state.phase ~= "waiting_roll" then
return { commands = { animation.toast(i18n.t("toast.move_first")) } }
end
state.dice = rules.next_dice()
local movable = rules.movable_pieces()
if #movable == 0 then
state.current_player = rules.next_player()
state.phase = "waiting_roll"
return {
ui = { updates = ui.dice_and_turn_updates(state.dice, state.current_player) },
render = { updates = ui.highlight_updates(rules.all_piece_ids(), false) },
commands = { animation.toast(i18n.t("toast.no_movable_piece")) }
}
end
state.phase = "waiting_piece"
return {
ui = { updates = ui.dice_update(state.dice) },
render = { updates = ui.highlight_updates(movable, true) },
commands = { animation.play_sound("dice") }
}
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(i18n.t("toast.invalid_piece")) } }
end
local path = rules.calculate_path(piece, state.dice)
rules.apply_move(piece, state.dice)
state.phase = "animating"
state.selected_piece = piece_id
return {
render = { updates = ui.highlight_updates(rules.all_piece_ids(), false) },
commands = { animation.move_piece(piece_id, path) }
}
end
local function handle_move_done()
if state.dice ~= 6 then
state.current_player = rules.next_player()
end
state.phase = "waiting_roll"
state.selected_piece = nil
return {
ui = { updates = ui.turn_update(state.current_player) }
}
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,88 @@
local state = runtime.import("state")
local board = runtime.import("board")
local rules = {}
function rules.next_player()
if state.current_player == "red" then
return "blue"
end
return "red"
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.piece_home_position(piece)
if piece.owner == "red" then
if piece.id == "red_1" then return board.red_home[1] end
return board.red_home[2]
end
if piece.id == "blue_1" then return board.blue_home[1] end
return board.blue_home[2]
end
function rules.can_move(piece, dice)
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.calculate_path(piece, dice)
local path = {}
if piece.status == "home" then
local start_index = board.start[piece.owner]
local pos = board.path[start_index]
table.insert(path, {x = pos.x, y = pos.y})
return path
end
for i = 1, dice do
local index = piece.path_index + i
while index > #board.path do
index = index - #board.path
end
local pos = board.path[index]
table.insert(path, {x = pos.x, y = pos.y})
end
return path
end
function rules.apply_move(piece, dice)
if piece.status == "home" then
piece.status = "path"
piece.path_index = board.start[piece.owner]
return
end
piece.path_index = piece.path_index + dice
while piece.path_index > #board.path do
piece.path_index = piece.path_index - #board.path
end
end
return rules

View File

@@ -0,0 +1,587 @@
---@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'|'blue'
---@alias GamePhase 'waiting_roll'|'waiting_piece'|'animating'
---@alias PieceStatus 'home'|'path'|'finished'
---@class (exact) PieceState
---@field id string
---@field owner PlayerColor
---@field path_index 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 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,15 @@
---@type LudoState
return {
current_player = "red",
phase = "waiting_roll",
dice_index = 0,
dice_values = {6, 3, 5, 2, 6, 4},
selected_piece = nil,
players = {"red", "blue"},
pieces = {
red_1 = { id = "red_1", owner = "red", path_index = 0, status = "home" },
red_2 = { id = "red_2", owner = "red", path_index = 0, status = "home" },
blue_1 = { id = "blue_1", owner = "blue", path_index = 0, status = "home" },
blue_2 = { id = "blue_2", owner = "blue", path_index = 0, status = "home" }
}
}

View File

@@ -0,0 +1,80 @@
local theme = runtime.import("theme")
local colors = theme.colors
local styles = {}
styles.layers = {
background = 0,
board = 1,
board_content = 2,
piece = 10,
hud = 100,
hud_content = 101
}
styles.board_panel = {
color = colors.board,
layer = styles.layers.background
}
styles.board_title = {
fontSize = 24,
color = colors.text,
layer = styles.layers.board_content
}
styles.red_home = {
color = colors.red_home,
layer = styles.layers.board
}
styles.blue_home = {
color = colors.blue_home,
layer = styles.layers.board
}
styles.path_cell = {
anchor = "center",
color = colors.path_cell,
layer = styles.layers.board_content
}
styles.top_bar = {
color = colors.top_bar,
layer = styles.layers.hud
}
styles.hud_text = {
fontSize = 22,
color = colors.text,
layer = styles.layers.hud_content
}
styles.dice_button = {
color = colors.dice_button,
fontSize = 20,
layer = styles.layers.hud_content
}
styles.piece_highlight = {
scale = 1.25,
alpha = 1
}
styles.piece_normal = {
scale = 1,
alpha = 0.95
}
function styles.piece(owner)
return {
anchor = "center",
asset = "piece_" .. owner,
color = colors[owner],
layer = styles.layers.piece,
interactive = true,
onTap = "piece_tap"
}
end
return styles

View File

@@ -0,0 +1,18 @@
local i18n = runtime.import("i18n")
return {
title = function()
return i18n.t("title")
end,
colors = {
red = "#ef4444",
blue = "#3b82f6",
top_bar = "#020617",
dice_button = "#2563eb",
board = "#1e293b",
red_home = "#7f1d1d",
blue_home = "#1e3a8a",
path_cell = "#f8fafc",
text = "#ffffff"
}
}

View File

@@ -0,0 +1,74 @@
---@type RuntimeUi
local runtime_ui = runtime.import("runtime_ui")
---@type RuntimeLayout
local layout = runtime.import("layout")
local theme = runtime.import("theme")
local i18n = runtime.import("i18n")
local styles = runtime.import("styles")
local state = runtime.import("state")
local board = runtime.import("board")
local rules = runtime.import("rules")
local ui = {}
local board_origin = { x = 40, y = 90 }
function ui.highlight_updates(ids, enabled)
return runtime_ui.batch_update(ids, enabled and styles.piece_highlight or styles.piece_normal)
end
function ui.create_board_nodes()
local nodes = {
runtime_ui.sprite("board_panel", "board", 40, 90, 640, 520, styles.board_panel),
runtime_ui.text("board_title", theme.title(), 240, 15, 180, 40, runtime_ui.with_parent("board_panel", styles.board_title)),
runtime_ui.panel("red_home", 20, 390, 130, 90, runtime_ui.with_parent("board_panel", styles.red_home)),
runtime_ui.panel("blue_home", 460, -10, 130, 90, runtime_ui.with_parent("board_panel", styles.blue_home))
}
for index, cell in ipairs(board.path) do
local pos = layout.local_position(board_origin, cell)
runtime_ui.append(nodes, runtime_ui.circle("cell_" .. tostring(index), pos.x, pos.y, 28, runtime_ui.with_parent("board_panel", styles.path_cell)))
end
for id, piece in pairs(state.pieces) do
local pos = rules.piece_home_position(piece)
runtime_ui.append(nodes, runtime_ui.circle(id, pos.x, pos.y, 44, styles.piece(piece.owner)))
end
return nodes
end
function ui.create_ui_nodes()
local hud_items = layout.row("top_bar", {
runtime_ui.text("turn_text", i18n.t("current_player", { player = i18n.player(state.current_player) }), 0, 0, 230, 32, styles.hud_text),
runtime_ui.text("dice_text", i18n.t("dice_empty"), 0, 0, 120, 32, styles.hud_text),
layout.item(
runtime_ui.button("dice_button", i18n.t("roll_button"), 0, 0, 130, 48, "roll_dice", styles.dice_button),
{ marginLeft = 134 }
)
}, {
x = 24,
height = 76,
gap = 16,
align = "center"
})
local nodes = { runtime_ui.panel("top_bar", 0, 0, 720, 76, styles.top_bar) }
return runtime_ui.append_all(nodes, hud_items)
end
function ui.dice_and_turn_updates(dice, current_player)
return {
runtime_ui.text_update("dice_text", i18n.t("dice_value", { value = dice })),
runtime_ui.text_update("turn_text", i18n.t("current_player", { player = i18n.player(current_player) }))
}
end
function ui.dice_update(dice)
return { runtime_ui.text_update("dice_text", i18n.t("dice_value", { value = dice })) }
end
function ui.turn_update(current_player)
return { runtime_ui.text_update("turn_text", i18n.t("current_player", { player = i18n.player(current_player) })) }
end
return ui