Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 6 additions & 7 deletions lib/page_transition/agent_path.dart
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,8 @@ class AgentTransitionPathPlanner {
required CoordinateSystem coordinateSystem,
}) {
final native = navigation?.geometry ?? geometry?.navigationGeometry;
Offset centerFor(Offset position, double size) =>
position +
coordinateSystem.virtualOffsetToWorld(Offset(size / 2, size / 2));
Offset centerFor(Offset position) =>
position + coordinateSystem.virtualOffsetToWorld(storedAgentAnchor);
if (requireNavigation && native == null) {
// Missing map data cannot establish a safe route. Page navigation still
// finishes, but agents wait for the destination page instead of crossing
Expand All @@ -93,8 +92,8 @@ class AgentTransitionPathPlanner {
if (entry.kind == TransitionKind.move &&
entry.visualWidget is PlacedAgentNode)
entry.id: AgentTransitionPath.unreachable(
centerFor(entry.startPos, startAgentSize),
centerFor(entry.endPos, endAgentSize),
centerFor(entry.startPos),
centerFor(entry.endPos),
),
};
}
Expand Down Expand Up @@ -129,8 +128,8 @@ class AgentTransitionPathPlanner {
if (entry.kind == TransitionKind.move &&
entry.visualWidget is PlacedAgentNode)
entry.id: () {
final startCenter = centerFor(entry.startPos, startAgentSize);
final endCenter = centerFor(entry.endPos, endAgentSize);
final startCenter = centerFor(entry.startPos);
final endCenter = centerFor(entry.endPos);
final startElevation = elevationFor(entry.from!, startCenter);
final endElevation = elevationFor(entry.to!, endCenter);
if (native != null) {
Expand Down
5 changes: 3 additions & 2 deletions lib/widgets/draggable_widgets/agents/agent_widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import 'package:icarus/const/line_provider.dart';
import 'package:icarus/const/maps.dart';
import 'package:icarus/const/placed_classes.dart';
import 'package:icarus/const/settings.dart';
import 'package:icarus/const/transition_data.dart';
import 'package:icarus/const/utilities.dart';
import 'package:icarus/providers/ability_bar_provider.dart';
import 'package:icarus/providers/action_provider.dart';
Expand Down Expand Up @@ -245,7 +246,7 @@ class AgentWidget extends ConsumerWidget {
coordinateSystem.positionForSide(
canonicalPosition: viewConeAgent!.position +
coordinateSystem.virtualOffsetToWorld(
Offset(agentSize / 2, agentSize / 2),
storedAgentAnchor,
),
reflectionOffset: Offset.zero,
isAttack: mapState!.isAttack,
Expand Down Expand Up @@ -327,7 +328,7 @@ class AgentWidget extends ConsumerWidget {
model: svgHeightModel,
canonicalOrigin: viewConeAgent.position +
coordinateSystem.virtualOffsetToWorld(
Offset(agentSize / 2, agentSize / 2),
storedAgentAnchor,
),
rotation: coordinateSystem.rotationForSide(
viewConeAgent.rotation,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ class ViewConeAgentComposite extends ConsumerWidget {
(clipToGeometry
? agent.position +
coordinateSystem.virtualOffsetToWorld(
Offset(agentSize / 2, agentSize / 2),
storedAgentAnchor,
)
: null),
visionElevation: agent.visionElevation,
Expand Down Expand Up @@ -303,8 +303,7 @@ class _PlacedViewConeAgentWidgetState
dragAnchorStrategy: (draggable, context, position) {
_dragOrigin.start(
origin: current.position +
coordinateSystem.virtualOffsetToWorld(
Offset(agentSize / 2, agentSize / 2)),
coordinateSystem.virtualOffsetToWorld(storedAgentAnchor),
coordinates: coordinateSystem,
zoom: ref.read(screenZoomProvider),
isAttack: isAttack);
Expand Down
2 changes: 1 addition & 1 deletion lib/widgets/page_transition_overlay.dart
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ class _EntryRenderer {
? null
: pathPosition -
coordinateSystem.virtualOffsetToWorld(
Offset(agentSize / 2, agentSize / 2),
storedAgentAnchor,
);
final coordinatePosition = pathTopLeft ??
(Offset.lerp(entry.startPos, entry.endPos, t) ?? entry.endPos);
Expand Down
20 changes: 13 additions & 7 deletions test/navigation_geometry_provider_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -136,13 +136,16 @@ void main() {
0
]);
const start = Offset(5, 5), end = Offset(25, 5);
final coordinates = CoordinateSystem(playAreaSize: const Size(1920, 1080));
final anchor = coordinates.virtualOffsetToWorld(storedAgentAnchor);
final entries = [
PageTransitionEntry.move(
from: PlacedAgent(id: 'sova', type: AgentType.sova, position: start),
to: PlacedAgent(id: 'sova', type: AgentType.sova, position: end),
from: PlacedAgent(
id: 'sova', type: AgentType.sova, position: start - anchor),
to: PlacedAgent(
id: 'sova', type: AgentType.sova, position: end - anchor),
)
];
final coordinates = CoordinateSystem(playAreaSize: const Size(1920, 1080));
for (final offset in [
Offset.zero,
const Offset(100, 50),
Expand Down Expand Up @@ -217,11 +220,14 @@ void main() {
VisionGeometryMap.projectUv(MapValue.split, const Offset(.3, .4));
final end =
VisionGeometryMap.projectUv(MapValue.split, const Offset(.7, .6));
final anchor = coordinates.virtualOffsetToWorld(storedAgentAnchor);
final routes = AgentTransitionPathPlanner.plan(
entries: [
PageTransitionEntry.move(
from: PlacedAgent(id: 'sova', type: AgentType.sova, position: start),
to: PlacedAgent(id: 'sova', type: AgentType.sova, position: end),
from: PlacedAgent(
id: 'sova', type: AgentType.sova, position: start - anchor),
to: PlacedAgent(
id: 'sova', type: AgentType.sova, position: end - anchor),
)
],
geometry: null,
Expand Down Expand Up @@ -281,9 +287,9 @@ void main() {
final path = routes['sova']!;
expect(path.isReachable, isFalse);
expect(path.positionAt(.999),
start + coordinates.virtualOffsetToWorld(const Offset(10, 10)));
start + coordinates.virtualOffsetToWorld(storedAgentAnchor));
expect(path.positionAt(1),
end + coordinates.virtualOffsetToWorld(const Offset(20, 20)));
end + coordinates.virtualOffsetToWorld(storedAgentAnchor));
}
});
}
Expand Down
59 changes: 58 additions & 1 deletion test/page_transition_overlay_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ import 'package:icarus/const/placed_classes.dart';
import 'package:icarus/const/settings.dart';
import 'package:icarus/const/transition_data.dart';
import 'package:icarus/const/utilities.dart';
import 'package:icarus/page_transition/agent_path.dart';
import 'package:icarus/providers/map_provider.dart';
import 'package:icarus/providers/transition_provider.dart';
import 'package:icarus/widgets/draggable_widgets/agents/agent_widget.dart';
import 'package:icarus/widgets/draggable_widgets/utilities/role_icon_utility_widget.dart';
import 'package:icarus/widgets/draggable_widgets/utilities/view_cone_widget.dart';
import 'package:icarus/widgets/mouse_watch.dart';
Expand Down Expand Up @@ -286,7 +288,7 @@ void main() {
preview.worldOrigin,
animatedPosition +
CoordinateSystem.instance.virtualOffsetToWorld(
const Offset(20, 20),
storedAgentAnchor,
),
);

Expand All @@ -299,6 +301,61 @@ void main() {
await tester.pump();
});

for (final attack in [true, false]) {
testWidgets('animated cone and marker follow the path center, side=$attack',
(tester) async {
final container = attack ? _createContainer() : _createDefenseContainer();
addTearDown(container.dispose);
final coordinates = CoordinateSystem.instance;
const startCenter = Offset(500, 250), endCenter = Offset(700, 350);
final anchor = coordinates.virtualOffsetToWorld(storedAgentAnchor);
final from = PlacedViewConeAgent(
id: 'path-cone',
type: AgentType.jett,
presetType: UtilityType.viewCone90,
position: startCenter - anchor,
rotation: .3,
length: 90,
);
final to = from.copyWith(position: endCenter - anchor);
final path = AgentTransitionPath([startCenter, endCenter]);
for (final size in [15.0, 25.0, 35.0, 45.0]) {
for (final progress in [0.0, .5, 1.0]) {
await tester.pumpWidget(UncontrolledProviderScope(
container: container,
child: ShadApp(
home: Scaffold(
body: TransitionEntriesLayer(
entries: [PageTransitionEntry.move(from: from, to: to)],
agentPaths: {from.id: path},
t: progress,
direction: PageTransitionDirection.forward,
agentSize: size,
abilitySize: 40,
))),
));
final center = path.positionAt(progress);
final cone =
tester.widget<ViewConeWidget>(find.byType(ViewConeWidget));
expect((cone.worldOrigin! - center).distance, lessThan(1e-7),
reason:
'size=$size progress=$progress must raycast from the path');
final expectedScreen = coordinates.coordinateToScreen(
coordinates.positionForSide(
canonicalPosition: center,
reflectionOffset: Offset.zero,
isAttack: attack));
expect(
(tester.getCenter(find.byType(AgentWidget)) - expectedScreen)
.distance,
lessThan(1e-7),
reason: 'The marker must follow that same center.');
}
}
await tester.pumpWidget(const SizedBox.shrink());
});
}

testWidgets(
'defense appear and disappear offsets keep view-cone geometry aligned',
(tester) async {
Expand Down
174 changes: 174 additions & 0 deletions test/view_cone_agent_anchor_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import 'dart:convert';
import 'dart:io';
import 'dart:math' as math;
import 'dart:typed_data';
import 'dart:ui' as ui;

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:icarus/const/agents.dart';
import 'package:icarus/const/app_provider_container.dart';
import 'package:icarus/const/coordinate_system.dart';
import 'package:icarus/const/maps.dart';
import 'package:icarus/const/placed_classes.dart';
import 'package:icarus/const/settings.dart';
import 'package:icarus/const/utilities.dart';
import 'package:icarus/providers/agent_provider.dart';
import 'package:icarus/providers/map_provider.dart';
import 'package:icarus/providers/strategy_settings_provider.dart';
import 'package:icarus/providers/svg_height_runtime_provider.dart';
import 'package:icarus/view_cone/svg_height_visibility.dart';
import 'package:icarus/widgets/draggable_widgets/agents/agent_widget.dart';
import 'package:icarus/widgets/draggable_widgets/agents/placed_view_cone_agent_widget.dart';
import 'package:icarus/widgets/draggable_widgets/utilities/svg_height_view_cone.dart';
import 'package:icarus/widgets/draggable_widgets/utilities/view_cone_widget.dart';
import 'package:shadcn_ui/shadcn_ui.dart';

class _HavenMap extends MapProvider {
@override
MapState build() => MapState(currentMap: MapValue.haven, isAttack: false);
}

void main() {
TestWidgetsFlutterBinding.ensureInitialized();
late SvgHeightRuntime runtime;
setUpAll(() {
appProviderContainer = ProviderContainer();
SvgHeightVisibility load(String side) {
final model = SvgHeightVisibility.fromJson(jsonDecode(utf8.decode(
gzip.decode(File('assets/maps/haven_svg_height_$side.json.gz')
.readAsBytesSync()))));
final library = Platform.environment['ICARUS_SVG_NATIVE_LIBRARY'];
if (library != null) {
expect(model.enableNativeAcceleration(libraryPath: library), isTrue);
}
return model;
}

runtime = SvgHeightRuntime.forMap(
MapValue.haven, load('attack'), load('defense'));
});
tearDownAll(() {
runtime.dispose();
appProviderContainer.dispose();
});

for (final attack in [false, true]) {
testWidgets(
'Haven cone stays on its walls when markers resize, side=$attack',
(tester) async {
tester.view.physicalSize = const Size(1920, 1080);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
final coordinates =
CoordinateSystem(playAreaSize: const Size(1920, 1080));
final container = ProviderContainer(overrides: [
mapProvider.overrideWith(_HavenMap.new),
svgHeightRuntimeProvider.overrideWith((ref, map) async => runtime),
]);
addTearDown(container.dispose);
container.read(mapProvider.notifier).fromHive(MapValue.haven, attack);

// The reported pose, expressed at the historical marker size. Resizing
// changes presentation only, so every size must paint the same sightline.
const canonicalCenter = Offset(885.124, 437.01);
final agent = PlacedViewConeAgent(
id: 'haven-resized-cone',
type: AgentType.neon,
presetType: UtilityType.viewCone90,
position: canonicalCenter -
coordinates.virtualOffsetToWorld(
const Offset(Settings.agentSize / 2, Settings.agentSize / 2)),
rotation: -1.505 - math.pi / 2,
length: 162.584 *
SvgHeightMapTransform.forMap(MapValue.haven).scale /
coordinates.virtualLengthToWorld(1),
);
container.read(agentProvider.notifier).fromHive([agent]);
await tester.pumpWidget(UncontrolledProviderScope(
container: container,
child: ShadApp(
home: Scaffold(
body: Stack(children: [
PlacedViewConeAgentWidget(agent: agent, onDragEnd: (_, __) {}),
]))),
));
await tester.pump();
await tester.pump();

Offset display(Offset canonical) =>
coordinates.coordinateToScreen(coordinates.positionForSide(
canonicalPosition: canonical,
reflectionOffset: Offset.zero,
isAttack: attack));

void expectAligned() {
final origin = tester
.widget<ViewConeWidget>(find.byType(ViewConeWidget))
.worldOrigin!;
final center = tester.getCenter(find.byType(AgentWidget));
expect((display(origin) - center).distance, lessThan(1e-7),
reason: 'Raycasting must start at the rendered marker center.');
}

Uint8List? baseline;
for (final size in [35.0, 15.0, 25.0, 45.0, 35.0]) {
container.read(strategySettingsProvider.notifier).updateAgentSize(size);
await tester.pump();
expectAligned();
final finder = find.byWidgetPredicate((widget) =>
widget is CustomPaint &&
widget.painter is SvgHeightViewConePainter);
final box = tester.renderObject<RenderBox>(finder);
final painter = tester.widget<CustomPaint>(finder).painter!;
final pixels = (await tester.runAsync(() async {
final recorder = ui.PictureRecorder();
final canvas = Canvas(recorder);
final center = display(canonicalCenter);
canvas.translate(160 - center.dx, 160 - center.dy);
canvas.transform(box.getTransformTo(null).storage);
painter.paint(canvas, box.size);
final picture = recorder.endRecording();
final image = await picture.toImage(320, 320);
final bytes =
(await image.toByteData(format: ui.ImageByteFormat.rawRgba))!
.buffer
.asUint8List();
image.dispose();
picture.dispose();
return bytes;
}))!;
if (baseline == null) {
baseline = pixels;
expect([
for (var i = 3; i < pixels.length; i += 4) pixels[i]
], contains(greaterThan(0)), reason: 'The cone must actually paint.');
} else {
expect(pixels, orderedEquals(baseline),
reason:
'Marker size $size must not move wall contacts or shadows.');
}
}

for (final size in [15.0, 45.0]) {
container.read(strategySettingsProvider.notifier).updateAgentSize(size);
await tester.pump();
final gesture = await tester
.startGesture(tester.getCenter(find.byType(AgentWidget)));
await gesture.moveBy(const Offset(12, 6));
await tester.pump();
expectAligned();
await gesture.moveBy(const Offset(5, -3));
await tester.pump();
expectAligned();
expect(container.read(agentProvider).single.position, agent.position,
reason: 'Previewing a drag must not save a position.');
await gesture.up();
await tester.pump();
}
await tester.pumpWidget(const SizedBox.shrink());
});
}
}
Loading