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,121 @@
import 'package:flame_lua_runtime/runtime/models/game_diff.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('GameDiff', () {
test('parses render, ui and commands', () {
final diff = GameDiff.fromMap({
'render': {
'creates': [
{'id': 'board', 'type': 'image', 'asset': 'board'},
],
'updates': [
{
'id': 'piece_red_1',
'props': {'x': 100, 'y': 120},
},
],
'removes': ['old_piece'],
},
'ui': {
'creates': [
{'id': 'dice_button', 'type': 'button', 'text': 'Roll'},
],
},
'commands': [
{'type': 'move_path', 'target': 'piece_red_1', 'duration': 0.5},
],
});
expect(diff.render.creates.single.id, 'board');
expect(diff.render.updates.single.id, 'piece_red_1');
expect(diff.render.updates.single.props['x'], 100);
expect(diff.render.removes.single.id, 'old_piece');
expect(diff.ui.creates.single.id, 'dice_button');
expect(diff.commands.single.type, 'move_path');
expect(diff.commands.single.target, 'piece_red_1');
expect(diff.commands.single.payload['duration'], 0.5);
});
test('treats missing sections as empty diffs', () {
final diff = GameDiff.fromMap({});
expect(diff.render.creates, isEmpty);
expect(diff.render.updates, isEmpty);
expect(diff.render.removes, isEmpty);
expect(diff.ui.creates, isEmpty);
expect(diff.commands, isEmpty);
});
test('accepts Lua numeric-key tables as lists', () {
final diff = GameDiff.fromMap({
'render': {
'creates': {
2: {'id': 'b', 'type': 'text'},
1: {'id': 'a', 'type': 'text'},
},
'updates': {},
'removes': {
1: {'id': 'old_a'},
2: 'old_b',
},
},
'commands': {
1: {'type': 'toast', 'message': 'hi'},
},
});
expect(diff.render.creates.map((node) => node.id), ['a', 'b']);
expect(diff.render.removes.map((node) => node.id), ['old_a', 'old_b']);
expect(diff.commands.single.type, 'toast');
expect(diff.commands.single.payload['message'], 'hi');
});
test('rejects malformed diff fields', () {
expect(
() => GameDiff.fromMap({
'render': {'creates': 'bad'},
}),
throwsFormatException,
);
expect(
() => GameDiff.fromMap({
'render': {
'updates': [
{'id': 'node', 'props': 'bad'},
],
},
}),
throwsFormatException,
);
expect(
() => GameDiff.fromMap({
'commands': [
{'type': ''},
],
}),
throwsFormatException,
);
expect(() => GameDiff.fromMap({'unknown': {}}), throwsFormatException);
expect(
() => GameDiff.fromMap({
'render': {'createz': []},
}),
throwsFormatException,
);
expect(
() => GameDiff.fromMap({
'render': {
'updates': [
{
'id': 'node',
'props': {'interative': true},
},
],
},
}),
throwsFormatException,
);
});
});
}

View File

@@ -0,0 +1,77 @@
import 'package:flame_lua_runtime/runtime/models/runtime_command.dart';
import 'package:flame_lua_runtime/runtime/models/runtime_event.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('RuntimeEvent', () {
test('serializes only present fields', () {
final event = RuntimeEvent(
type: 'tap',
target: 'dice_button',
handler: 'roll_dice',
x: 10,
y: 20,
data: {'pointer': 1},
);
expect(event.toMap(), {
'type': 'tap',
'target': 'dice_button',
'handler': 'roll_dice',
'x': 10,
'y': 20,
'data': {'pointer': 1},
});
});
test('omits null and empty optional fields', () {
expect(const RuntimeEvent(type: 'animation_done').toMap(), {
'type': 'animation_done',
});
});
});
group('RuntimeCommand', () {
test('parses command payload without type and target', () {
final command = RuntimeCommand.fromMap({
'type': 'move_path',
'target': 'piece_red_1',
'duration': 0.5,
'onComplete': 'done',
});
expect(command.type, 'move_path');
expect(command.target, 'piece_red_1');
expect(command.payload, {'duration': 0.5, 'onComplete': 'done'});
});
test('rejects invalid command shape', () {
expect(() => RuntimeCommand.fromMap({'type': ''}), throwsFormatException);
expect(
() => RuntimeCommand.fromMap({'type': 'toast', 'target': 1}),
throwsFormatException,
);
expect(
() => RuntimeCommand.fromMap({'type': 'unknown'}),
throwsFormatException,
);
expect(
() => RuntimeCommand.fromMap({
'type': 'move_to',
'target': 'piece',
'x': 1,
'y': 2,
'duraton': 0.5,
}),
throwsFormatException,
);
expect(
() => RuntimeCommand.fromMap({
'type': 'preload_resources',
'groups': 'pieces',
}),
throwsFormatException,
);
});
});
}

View File

@@ -0,0 +1,324 @@
import 'package:flame_lua_runtime/runtime/models/runtime_node.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('RuntimeNode', () {
test('parses required and optional fields', () {
final node = RuntimeNode.fromMap({
'id': 'dice_button',
'type': 'button',
'parent': 'top_bar',
'asset': 'dice_normal',
'pressedAsset': 'dice_pressed',
'disabledAsset': 'dice_disabled',
'animation': 'idle',
'skin': 'red',
'loop': false,
'text': 'Roll',
'x': 10,
'y': 20.5,
'width': 120,
'height': 48,
'paddingLeft': 4,
'paddingTop': 5,
'paddingRight': 6,
'paddingBottom': 7,
'anchor': 'center',
'layer': 3,
'visible': false,
'alpha': 0.7,
'scale': 1.2,
'rotation': 0.4,
'color': '#112233',
'fontSize': 18,
'textAlign': 'left',
'radius': 10,
'strokeWidth': 3,
'value': 0.6,
'scrollX': 15,
'scrollY': 20,
'contentWidth': 220,
'contentHeight': 180,
'virtualized': true,
'cacheExtent': 12,
'inertia': false,
'scrollbarThumbColor': '#abcdef',
'scrollbarTrackColor': '#123456',
'scrollbarThickness': 6,
'scrollbarVisible': false,
'interactive': true,
'onTap': 'roll_dice',
'onScroll': 'list_scrolled',
'preset': 'burst',
'count': 32,
'duration': 0.6,
'speedMin': 60,
'speedMax': 180,
'gravityX': 0,
'gravityY': 120,
'spread': 360,
'colorTo': '#00ffcc33',
'radiusTo': 0,
'autoRemove': false,
'fadeOut': false,
});
expect(node.id, 'dice_button');
expect(node.type, 'button');
expect(node.parent, 'top_bar');
expect(node.asset, 'dice_normal');
expect(node.pressedAsset, 'dice_pressed');
expect(node.disabledAsset, 'dice_disabled');
expect(node.animation, 'idle');
expect(node.skin, 'red');
expect(node.loop, isFalse);
expect(node.text, 'Roll');
expect(node.x, 10);
expect(node.y, 20.5);
expect(node.width, 120);
expect(node.height, 48);
expect(node.paddingLeft, 4);
expect(node.paddingTop, 5);
expect(node.paddingRight, 6);
expect(node.paddingBottom, 7);
expect(node.anchor, 'center');
expect(node.layer, 3);
expect(node.visible, isFalse);
expect(node.alpha, 0.7);
expect(node.scale, 1.2);
expect(node.rotation, 0.4);
expect(node.color, const Color(0xff112233));
expect(node.fontSize, 18);
expect(node.textAlign, 'left');
expect(node.radius, 10);
expect(node.strokeWidth, 3);
expect(node.value, 0.6);
expect(node.scrollX, 15);
expect(node.scrollY, 20);
expect(node.contentWidth, 220);
expect(node.contentHeight, 180);
expect(node.virtualized, isTrue);
expect(node.cacheExtent, 12);
expect(node.inertia, isFalse);
expect(node.scrollbarThumbColor, const Color(0xffabcdef));
expect(node.scrollbarTrackColor, const Color(0xff123456));
expect(node.scrollbarThickness, 6);
expect(node.scrollbarVisible, isFalse);
expect(node.interactive, isTrue);
expect(node.onTap, 'roll_dice');
expect(node.onScroll, 'list_scrolled');
expect(node.preset, 'burst');
expect(node.count, 32);
expect(node.duration, 0.6);
expect(node.speedMin, 60);
expect(node.speedMax, 180);
expect(node.gravityX, 0);
expect(node.gravityY, 120);
expect(node.spread, 360);
expect(node.colorTo, const Color(0x00ffcc33));
expect(node.radiusTo, 0);
expect(node.autoRemove, isFalse);
expect(node.fadeOut, isFalse);
});
test('applies default values', () {
final node = RuntimeNode.fromMap({'id': 'label', 'type': 'text'});
expect(node.x, 0);
expect(node.y, 0);
expect(node.anchor, 'topLeft');
expect(node.layer, 0);
expect(node.visible, isTrue);
expect(node.alpha, 1);
expect(node.scale, 1);
expect(node.rotation, 0);
expect(node.loop, isTrue);
expect(node.textAlign, 'center');
expect(node.scrollbarVisible, isTrue);
expect(node.paddingLeft, 0);
expect(node.paddingTop, 0);
expect(node.paddingRight, 0);
expect(node.paddingBottom, 0);
expect(node.autoRemove, isTrue);
expect(node.fadeOut, isTrue);
expect(node.interactive, isFalse);
});
test('copies only provided props', () {
final node = RuntimeNode.fromMap({
'id': 'piece',
'type': 'circle',
'x': 1,
'y': 2,
'color': '#ff0000',
});
final updated = node.copyWithProps({
'x': 20,
'parent': 'board',
'visible': false,
'color': '#8000ff00',
'radius': 8,
'strokeWidth': 2,
'value': 0.75,
'width': 70,
'height': 60,
'paddingLeft': 8,
'paddingTop': 9,
'paddingRight': 10,
'paddingBottom': 11,
'contentWidth': 120,
'contentHeight': 100,
'pressedAsset': 'button_pressed',
'disabledAsset': 'button_disabled',
'scrollX': 90,
'scrollY': 80,
'textAlign': 'right',
'preset': 'trail',
'count': 12,
});
expect(updated.id, 'piece');
expect(updated.type, 'circle');
expect(updated.parent, 'board');
expect(updated.x, 20);
expect(updated.y, 2);
expect(updated.visible, isFalse);
expect(updated.color, const Color(0x8000ff00));
expect(updated.radius, 8);
expect(updated.strokeWidth, 2);
expect(updated.value, 0.75);
expect(updated.width, 70);
expect(updated.height, 60);
expect(updated.paddingLeft, 8);
expect(updated.paddingTop, 9);
expect(updated.paddingRight, 10);
expect(updated.paddingBottom, 11);
expect(updated.contentWidth, 120);
expect(updated.contentHeight, 100);
expect(updated.pressedAsset, 'button_pressed');
expect(updated.disabledAsset, 'button_disabled');
expect(updated.scrollX, 68);
expect(updated.scrollY, 60);
expect(updated.textAlign, 'right');
expect(updated.preset, 'trail');
expect(updated.count, 12);
});
test('rejects invalid values', () {
expect(
() => RuntimeNode.fromMap({'id': '', 'type': 'text'}),
throwsFormatException,
);
expect(
() => RuntimeNode.fromMap({'id': 'a', 'type': 'text', 'x': 'bad'}),
throwsFormatException,
);
expect(
() => RuntimeNode.fromMap({'id': 'a', 'type': 'text', 'color': 'red'}),
throwsFormatException,
);
expect(
() => RuntimeNode.fromMap({'id': 'a', 'type': 'unknown'}),
throwsFormatException,
);
expect(
() => RuntimeNode.fromMap({
'id': 'a',
'type': 'text',
'anchor': 'middle',
}),
throwsFormatException,
);
expect(
() => RuntimeNode.fromMap({'id': 'a', 'type': 'progress', 'value': 2}),
throwsFormatException,
);
expect(
() => RuntimeNode.fromMap({
'id': 'a',
'type': 'text',
'textAlign': 'justify',
}),
throwsFormatException,
);
expect(
() =>
RuntimeNode.fromMap({'id': 'a', 'type': 'listView', 'scrollY': -1}),
throwsFormatException,
);
expect(
() => RuntimeNode.fromMap({
'id': 'a',
'type': 'particle',
'preset': 'unknown',
}),
throwsFormatException,
);
expect(
() => RuntimeNode.fromMap({'id': 'a', 'type': 'particle', 'count': 0}),
throwsFormatException,
);
expect(
() => RuntimeNode.fromMap({
'id': 'a',
'type': 'listView',
'paddingTop': -1,
}),
throwsFormatException,
);
expect(
() => RuntimeNode.fromMap({
'id': 'a',
'type': 'listView',
'cacheExtent': -1,
}),
throwsFormatException,
);
expect(
() => RuntimeNode.fromMap({
'id': 'a',
'type': 'rect',
'interactive': 'yes',
}),
throwsFormatException,
);
expect(
() => RuntimeNode.fromMap({
'id': 'a',
'type': 'button',
'pressedAsset': 1,
}),
throwsFormatException,
);
expect(
() => RuntimeNode.fromMap({'id': 'a', 'type': 'rect', 'parent': 1}),
throwsFormatException,
);
expect(
() => RuntimeNode.fromMap({'id': 'a', 'type': 'spine', 'loop': 'yes'}),
throwsFormatException,
);
expect(
() => RuntimeNode.fromMap({'id': 'a', 'type': 'rect', 'parent': 'a'}),
throwsFormatException,
);
expect(
() => RuntimeNode.fromMap({
'id': 'a',
'type': 'rect',
'interative': true,
}),
throwsFormatException,
);
expect(
() => RuntimeNode.fromMap({
'id': 'a',
'type': 'rect',
}).copyWithProps({'screenWitdh': 720}),
throwsFormatException,
);
});
});
}