import 'package:flame/components.dart'; import 'package:flame_lua_runtime/runtime/models/runtime_node.dart'; import 'package:flame_lua_runtime/runtime/protocol/runtime_protocol.dart'; import 'package:flame_lua_runtime/runtime/rendering/runtime_component.dart'; import 'package:flame_lua_runtime/runtime/resources/game_resource_manager.dart'; import 'package:flutter/material.dart' show Color; import 'package:flutter/rendering.dart' show Rect; import 'package:flutter_test/flutter_test.dart'; void main() { group('RuntimeComponent', () { test('applies base transform and priority from node', () { final component = RuntimeComponent( node: const RuntimeNode( id: 'rect', type: RuntimeNodeType.rect, x: 10, y: 20, width: 120, height: 48, scale: 1.5, rotation: 0.25, anchor: RuntimeAnchorValue.center, layer: 7, ), resources: GameResourceManager(), onNodeTap: (_, __) {}, ); expect(component.position, Vector2(10, 20)); expect(component.size, Vector2(120, 48)); expect(component.scale, Vector2.all(1.5)); expect(component.angle, 0.25); expect(component.anchor, Anchor.center); expect(component.priority, 7); expect(component.isVisible, isTrue); }); test('updates node and transform', () { final component = RuntimeComponent( node: const RuntimeNode(id: 'node', type: RuntimeNodeType.rect), resources: GameResourceManager(), onNodeTap: (_, __) {}, ); component.updateNode( const RuntimeNode( id: 'node', type: RuntimeNodeType.progress, x: 30, y: 40, width: 200, height: 16, value: 0.5, layer: 3, ), ); expect(component.node.type, RuntimeNodeType.progress); expect(component.node.value, 0.5); expect(component.position, Vector2(30, 40)); expect(component.size, Vector2(200, 16)); expect(component.priority, 3); expect(component.isVisible, isTrue); }); test('visibility hides component subtree and disables hit testing', () { final component = RuntimeComponent( node: const RuntimeNode( id: 'button', type: RuntimeNodeType.button, text: 'Hidden', width: 100, height: 40, visible: false, interactive: true, ), resources: GameResourceManager(), onNodeTap: (_, __) {}, ); expect(component.isVisible, isFalse); expect(component.containsLocalPoint(Vector2(10, 10)), isFalse); component.updateNode( const RuntimeNode( id: 'button', type: RuntimeNodeType.button, text: 'Shown', width: 100, height: 40, visible: true, interactive: true, ), ); expect(component.isVisible, isTrue); expect(component.containsLocalPoint(Vector2(10, 10)), isTrue); }); test('supports runtime alpha override for fade commands', () { final component = RuntimeComponent( node: const RuntimeNode( id: 'panel', type: RuntimeNodeType.rect, alpha: 0.8, ), resources: GameResourceManager(), onNodeTap: (_, __) {}, ); expect(component.renderAlpha, 0.8); component.setRuntimeAlpha(0.25); expect(component.renderAlpha, 0.25); component.setRuntimeAlpha(2); expect(component.renderAlpha, 1); }); test('inherits parent alpha for prefab-like subtrees', () async { final parent = RuntimeComponent( node: const RuntimeNode( id: 'parent', type: RuntimeNodeType.panel, alpha: 0.8, ), resources: GameResourceManager(), onNodeTap: (_, __) {}, ); final child = RuntimeComponent( node: const RuntimeNode( id: 'child', type: RuntimeNodeType.text, text: 'Child', alpha: 0.5, color: Color(0xffffffff), ), resources: GameResourceManager(), onNodeTap: (_, __) {}, ); parent.add(child); parent.updateTree(0); child.setInheritedAlpha(parent.renderAlpha); expect(child.renderAlpha, closeTo(0.4, 0.001)); final text = child.children.whereType().single; expect( ((text.textRenderer as TextPaint).style.color!).a, closeTo(0.4, 0.003), ); parent.setRuntimeAlpha(0.25); expect(child.renderAlpha, closeTo(0.125, 0.001)); final updatedText = child.children.whereType().single; expect(identical(updatedText, text), isTrue); expect( ((updatedText.textRenderer as TextPaint).style.color!).a, closeTo(0.125, 0.003), ); }); test('propagates parent node alpha updates to child subtree', () { final parent = RuntimeComponent( node: const RuntimeNode( id: 'parent', type: RuntimeNodeType.panel, alpha: 0.8, ), resources: GameResourceManager(), onNodeTap: (_, __) {}, ); final child = RuntimeComponent( node: const RuntimeNode( id: 'child', type: RuntimeNodeType.text, text: 'Child', alpha: 0.5, color: Color(0xffffffff), ), resources: GameResourceManager(), onNodeTap: (_, __) {}, ); parent.add(child); parent.updateTree(0); child.setInheritedAlpha(parent.renderAlpha); parent.updateNode( const RuntimeNode( id: 'parent', type: RuntimeNodeType.panel, alpha: 0.6, ), ); final text = child.children.whereType().single; expect(child.renderAlpha, closeTo(0.3, 0.001)); expect( ((text.textRenderer as TextPaint).style.color!).a, closeTo(0.3, 0.003), ); }); test('multiplies color alpha with node and runtime alpha', () { expect( composeRuntimeColorAlpha(const Color(0xffffffff), 1).a, closeTo(1, 0.001), ); expect( composeRuntimeColorAlpha(const Color(0x80ffffff), 1).a, closeTo(0.5, 0.003), ); expect( composeRuntimeColorAlpha(const Color(0x80ffffff), 0.5).a, closeTo(0.25, 0.003), ); expect( composeRuntimeColorAlpha(const Color(0x00ffffff), 1).a, closeTo(0, 0.001), ); expect( composeRuntimeColorAlpha(const Color(0x80ffffff), 0.25).a, closeTo(0.125, 0.003), ); }); test('computes atlas source region and clamps to image bounds', () { expect( runtimeImageSourceRect( imageWidth: 100, imageHeight: 80, sourceX: 10, sourceY: 12, sourceWidth: 30, sourceHeight: 20, ), const Rect.fromLTWH(10, 12, 30, 20), ); expect( runtimeImageSourceRect( imageWidth: 100, imageHeight: 80, sourceX: 90, sourceY: 70, sourceWidth: 30, sourceHeight: 20, ), const Rect.fromLTWH(90, 70, 10, 10), ); }); test('computes nine-slice source and destination rects', () { final parts = runtimeNineSliceRects( source: const Rect.fromLTWH(10, 20, 30, 40), destination: const Rect.fromLTWH(0, 0, 90, 120), sliceLeft: 5, sliceTop: 6, sliceRight: 7, sliceBottom: 8, ); expect(parts, hasLength(9)); expect(parts.first.source, const Rect.fromLTRB(10, 20, 15, 26)); expect(parts.first.destination, const Rect.fromLTRB(0, 0, 5, 6)); expect(parts[4].source, const Rect.fromLTRB(15, 26, 33, 52)); expect(parts[4].destination, const Rect.fromLTRB(5, 6, 83, 112)); expect(parts.last.source, const Rect.fromLTRB(33, 52, 40, 60)); expect(parts.last.destination, const Rect.fromLTRB(83, 112, 90, 120)); }); test('overlaps nine-slice destination seams without changing bounds', () { final parts = runtimeNineSliceRects( source: const Rect.fromLTWH(0, 0, 30, 30), destination: const Rect.fromLTWH(0, 0, 90, 90), sliceLeft: 10, sliceTop: 10, sliceRight: 10, sliceBottom: 10, destinationOverlap: 0.5, ); expect(parts.first.destination, const Rect.fromLTRB(0, 0, 10.5, 10.5)); expect(parts[4].destination, const Rect.fromLTRB(9.5, 9.5, 80.5, 80.5)); expect(parts.last.destination, const Rect.fromLTRB(79.5, 79.5, 90, 90)); expect(parts[4].source, const Rect.fromLTRB(10, 10, 20, 20)); }); test('insets nine-slice source rects to avoid atlas edge sampling', () { final parts = runtimeNineSliceRects( source: const Rect.fromLTWH(10, 20, 30, 30), destination: const Rect.fromLTWH(0, 0, 90, 90), sliceLeft: 10, sliceTop: 10, sliceRight: 10, sliceBottom: 10, sourceInset: 0.5, ); expect(parts.first.source, const Rect.fromLTRB(10.5, 20.5, 19.5, 29.5)); expect(parts[4].source, const Rect.fromLTRB(20.5, 30.5, 29.5, 39.5)); expect(parts.last.source, const Rect.fromLTRB(30.5, 40.5, 39.5, 49.5)); }); test('keeps tiny nine-slice source rects when inset would collapse them', () { final parts = runtimeNineSliceRects( source: const Rect.fromLTWH(0, 0, 3, 3), destination: const Rect.fromLTWH(0, 0, 30, 30), sliceLeft: 1, sliceTop: 1, sliceRight: 1, sliceBottom: 1, sourceInset: 0.5, ); expect(parts[4].source, const Rect.fromLTRB(1, 1, 2, 2)); }); test('updates text alpha style without rebuilding text component', () { final component = RuntimeComponent( node: const RuntimeNode( id: 'text', type: RuntimeNodeType.text, text: 'Fade me', alpha: 0, color: Color(0xffffffff), ), resources: GameResourceManager(), onNodeTap: (_, __) {}, ); final text = component.children.whereType().single; expect(((text.textRenderer as TextPaint).style.color!).a, 0); component.setRuntimeAlpha(1); final updatedText = component.children.whereType().single; expect(identical(updatedText, text), isTrue); expect(((updatedText.textRenderer as TextPaint).style.color!).a, 1); }); test( 'updates button text alpha style without rebuilding text component', () { final component = RuntimeComponent( node: const RuntimeNode( id: 'button', type: RuntimeNodeType.button, text: 'Fade me', alpha: 0, ), resources: GameResourceManager(), onNodeTap: (_, __) {}, ); final text = component.children.whereType().single; expect(((text.textRenderer as TextPaint).style.color!).a, 0); component.setRuntimeAlpha(1); final updatedText = component.children .whereType() .single; expect(identical(updatedText, text), isTrue); expect(((updatedText.textRenderer as TextPaint).style.color!).a, 1); }, ); test('applies text shadow style', () { final component = RuntimeComponent( node: const RuntimeNode( id: 'text', type: RuntimeNodeType.text, text: 'Shadowed', alpha: 0.5, textShadowColor: Color(0x80000000), textShadowOffsetX: 2, textShadowOffsetY: 3, textShadowBlur: 4, ), resources: GameResourceManager(), onNodeTap: (_, __) {}, ); final text = component.children.whereType().single; final style = (text.textRenderer as TextPaint).style; final shadow = style.shadows!.single; expect(shadow.color.a, closeTo(0.25, 0.003)); expect(shadow.offset.dx, 2); expect(shadow.offset.dy, 3); expect(shadow.blurRadius, 4); }); test('multi-line non-button text is top aligned', () { final component = RuntimeComponent( node: const RuntimeNode( id: 'text', type: RuntimeNodeType.text, text: 'line1\nline2', width: 120, height: 80, textAlign: RuntimeTextAlignValue.left, ), resources: GameResourceManager(), onNodeTap: (_, __) {}, ); final text = component.children.whereType().single; expect(text.anchor, Anchor.topLeft); expect(text.position, Vector2.zero()); }); test('only interactive nodes contain local points', () { final passive = RuntimeComponent( node: const RuntimeNode( id: 'passive', type: RuntimeNodeType.rect, width: 100, height: 40, ), resources: GameResourceManager(), onNodeTap: (_, __) {}, ); final interactive = RuntimeComponent( node: const RuntimeNode( id: 'button', type: RuntimeNodeType.button, width: 100, height: 40, interactive: true, ), resources: GameResourceManager(), onNodeTap: (_, __) {}, ); expect(passive.containsLocalPoint(Vector2(10, 10)), isFalse); expect(interactive.containsLocalPoint(Vector2(10, 10)), isTrue); expect(interactive.containsLocalPoint(Vector2(101, 10)), isFalse); expect(interactive.containsLocalPoint(Vector2(10, 41)), isFalse); }); }); }