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
65 changes: 65 additions & 0 deletions docs/sunset-scale-correction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Sunset scale correction

The Sunset map scale is now `1.06`, up from `0.9502102049421427`.
The SVG artwork and its 416 by 473 viewBox are unchanged.

A nominal 30 m Crosscut radius previously represented 26.87 m against the
extracted minimap geometry. It now represents 29.97 m. Six fixed map corners
establish the transform, and ten separate corners check it. The held-out error
is 0.433 SVG units RMS and 0.818 units maximum. The affine axis ratio is 1.000118,
which does not support stretching the artwork to correct the range.

The calculation uses Sunset's extracted `Juliett_UIData` multiplier of
0.000078 per centimeter, 447.5358 SVG units per minimap UV unit, the SVG height
of 473, and Icarus's virtual height of 831:

```text
SVG units per meter = 0.000078 * 100 * 447.5358 = 3.490779
Map scale = 3.490779 * 831 / (5.78 * 473) = 1.061047
Rounded runtime value = 1.06
```

The [landmark fixture](../test/fixtures/map_calibration/sunset.json) pins the
coordinates and hashes of the source files. Reproduce the measurement with:

```powershell
python -m pip install numpy svgpathtools matplotlib
python scripts/audit_sunset_scale.py --check
```

The check rejects a scale error above 0.5% or excessive held-out landmark error.
Use `--fmodel-content <ShooterGame/Content>` to verify the extracted source hashes.

## Saved placements

Data version 98 preserves the anchor of each existing Sunset ability, lineup
ability, and scale-dependent utility. The migration runs after canonical
coordinate conversion, so it applies the same calculation to attack and defense
pages. Agent positions and fixed-size anchors do not move.

```text
newPosition = oldPosition + (oldAnchor - newAnchor) * 1000 / 831
```

Historical migrations 39, 45, and 97 retain the old Sunset scale. Each migration
is selected using the original input version, because intermediate helpers can
stamp the current version before later stages run. Current-version data does
not receive the correction twice. No Hive fields or adapters changed.

Regression coverage includes every ability and utility type, lineup metadata,
deleted flags, both page sides, pre-page imports, versions 16/38/39/44/45/96/97,
other maps, source immutability, and ZIP JSON export/import.

## Accuracy limits

The corrected range is about 0.84% larger than the visible circle in the
reporter's screenshot. That residual persists when fitting Riot's minimap UVs
directly to the screenshot without the SVG. It is accepted as a visual
discrepancy, not fitted away by changing the physical scale. The minimap outline
is not a 3D collision mesh, and a current live activation-boundary test has not
been performed.

The local Crosscut extraction contains older 24 m tuning. The 30 m specification
comes from [Riot's patch 13.00 notes](https://playvalorant.com/en-us/news/game-updates/valorant-patch-notes-13-00/).
Custom-shape diameter semantics and vision-boundary projection are separate
issues and are not changed here.
2 changes: 1 addition & 1 deletion lib/const/maps.dart
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ class Maps {
MapValue.breeze: 1.01, //modified
MapValue.lotus: 1.24, //modified
MapValue.icebox: 1.03, //modiefied
MapValue.sunset: 0.9502102049421427,
MapValue.sunset: 1.06,
MapValue.split: 1.1920129279062075, //modified
MapValue.haven: 1.06, //modified
MapValue.fracture: 1.21, //modified
Expand Down
2 changes: 1 addition & 1 deletion lib/const/settings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ class Settings {
static final Uri dicordLink = Uri.parse("https://discord.gg/PN2uKwCqYB");

static const Duration autoSaveOffset = Duration(seconds: 15);
static const int versionNumber = 97;
static const int versionNumber = 98;
static const String versionName = "4.6.1";
static final Uri desktopUpdaterArchiveUrl =
buildDesktopUpdaterArchiveUrl(kResolvedUpdateChannel);
Expand Down
3 changes: 2 additions & 1 deletion lib/migrations/ability_scale_migration.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,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/migrations/map_scale_history.dart';
import 'package:icarus/providers/strategy_page.dart';

class AbilityScaleMigration {
Expand Down Expand Up @@ -71,7 +72,7 @@ class AbilityScaleMigration {
if (data == null) return ability;

final oldMapScale = _oldMapScale[map] ?? 1.0;
final newMapScale = Maps.mapScale[map] ?? 1.0;
final newMapScale = mapScaleBeforeVersion98(map);

final oldAnchor = _oldAnchor(
data: data,
Expand Down
3 changes: 2 additions & 1 deletion lib/migrations/canonical_coordinates_migration.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import 'package:icarus/const/placed_classes.dart';
import 'package:icarus/const/placed_media_geometry.dart';
import 'package:icarus/const/settings.dart';
import 'package:icarus/const/utilities.dart';
import 'package:icarus/migrations/map_scale_history.dart';
import 'package:icarus/providers/strategy_page.dart';

abstract final class CanonicalCoordinatesMigration {
Expand All @@ -19,7 +20,7 @@ abstract final class CanonicalCoordinatesMigration {
required List<StrategyPage> pages,
required MapValue map,
}) {
final mapScale = Maps.mapScale[map] ?? 1;
final mapScale = mapScaleBeforeVersion98(map);
return [
for (final page in pages)
page.isAttack ? page : _migrateDefensePage(page, mapScale),
Expand Down
3 changes: 2 additions & 1 deletion lib/migrations/custom_circle_wrapper_migration.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:icarus/const/maps.dart';
import 'package:icarus/const/placed_classes.dart';
import 'package:icarus/const/utilities.dart';
import 'package:icarus/migrations/map_scale_history.dart';
import 'package:icarus/providers/strategy_page.dart';

class CustomCircleWrapperMigration {
Expand Down Expand Up @@ -50,7 +51,7 @@ class CustomCircleWrapperMigration {
return utility;
}

final mapScale = Maps.mapScale[map] ?? 1.0;
final mapScale = mapScaleBeforeVersion98(map);
final actualDiameterVirtual = CustomCircleUtility.diameterInVirtual(
diameterMeters: diameterMeters,
mapScale: mapScale,
Expand Down
6 changes: 6 additions & 0 deletions lib/migrations/map_scale_history.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import 'package:icarus/const/maps.dart';

// Migrations through version 97 must reconstruct the scale used at the time.
// Reading Sunset's live scale would apply the version 98 correction twice.
double mapScaleBeforeVersion98(MapValue map) =>
map == MapValue.sunset ? 0.9502102049421427 : Maps.mapScale[map] ?? 1.0;
77 changes: 77 additions & 0 deletions lib/migrations/sunset_scale_migration.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import 'dart:ui';

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/strategy_page.dart';

abstract final class SunsetScaleMigration {
static const int version = 98;
static const double _oldScale = 0.9502102049421427;
static const double _newScale = 1.06;
static const double _virtualToWorld = 1000 / 831;

// Positions are canonical after version 97, including defense pages.
static List<StrategyPage> migratePages({
required List<StrategyPage> pages,
required MapValue map,
}) {
if (map != MapValue.sunset) return pages;
return [
for (final page in pages)
page.copyWith(
abilityData: [
for (final ability in page.abilityData) _ability(ability),
],
utilityData: [
for (final utility in page.utilityData) _utility(utility),
],
lineUpGroups: [
for (final group in page.lineUpGroups)
group.copyWith(
items: [
for (final item in group.items)
item.copyWith(ability: _ability(item.ability)),
],
),
],
),
];
}

static PlacedAbility _ability(PlacedAbility ability) {
final data = ability.data.abilityData;
if (data == null) return ability;
final delta = (data.getAnchorPoint(
mapScale: _oldScale,
abilitySize: Settings.abilitySize,
) -
data.getAnchorPoint(
mapScale: _newScale,
abilitySize: Settings.abilitySize,
)) *
_virtualToWorld;
if (delta == Offset.zero) return ability;
return ability.copyWith(position: ability.position + delta)
..isDeleted = ability.isDeleted;
}

static PlacedUtility _utility(PlacedUtility utility) {
final data = UtilityData.utilityWidgets[utility.type]!;
Offset anchor(double scale) => data.getAnchorPoint(
id: utility.id,
length: utility.length,
rotation: utility.rotation,
mapScale: scale,
agentSize: Settings.agentSize,
abilitySize: Settings.abilitySize,
diameterMeters: utility.customDiameter,
widthMeters: utility.customWidth,
rectLengthMeters: utility.customLength,
);
final delta = (anchor(_oldScale) - anchor(_newScale)) * _virtualToWorld;
if (delta == Offset.zero) return utility;
return utility.copyWith(position: utility.position + delta);
}
}
57 changes: 48 additions & 9 deletions lib/providers/strategy_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import 'package:icarus/migrations/canonical_coordinates_migration.dart';
import 'package:icarus/migrations/custom_circle_wrapper_migration.dart';
import 'package:icarus/migrations/lineup_group_migration.dart';
import 'package:icarus/migrations/page_name_provenance_migration.dart';
import 'package:icarus/migrations/sunset_scale_migration.dart';
import 'package:icarus/providers/ability_provider.dart';
import 'package:icarus/providers/action_provider.dart';
import 'package:icarus/providers/agent_provider.dart';
Expand Down Expand Up @@ -597,20 +598,35 @@ class StrategyProvider extends Notifier<StrategyState> {
);
}

static StrategyData migrateToCurrentVersion(StrategyData strat,
{bool forceAbilityScale = false}) {
static StrategyData migrateToCurrentVersion(
StrategyData strat, {
bool forceAbilityScale = false,
}) {
final originalVersion = strat.versionNumber;
final needsCanonicalCoordinatesMigration =
strat.versionNumber < CanonicalCoordinatesMigration.version;
final needsAbilityVisionMigration =
strat.versionNumber < AbilityVisionConeMigration.version;
final needsPageNameProvenanceMigration =
strat.versionNumber < PageNameProvenanceMigration.version;
final worldMigrated = migrateToWorld16x9(strat);
final abilityScaleMigrated =
migrateAbilityScale(worldMigrated, force: forceAbilityScale);
final squareAoeMigrated = migrateSquareAoeCenter(abilityScaleMigrated);
final customCircleMigrated = migrateCustomCircleWrapper(squareAoeMigrated);
final lineUpGroupMigrated = migrateLineUpGroups(customCircleMigrated);
final abilityScaleMigrated = migrateAbilityScale(
worldMigrated,
force:
forceAbilityScale || originalVersion < AbilityScaleMigration.version,
);
final squareAoeMigrated = migrateSquareAoeCenter(
abilityScaleMigrated,
force: originalVersion < SquareAoeCenterMigration.version,
);
final customCircleMigrated = migrateCustomCircleWrapper(
squareAoeMigrated,
force: originalVersion < CustomCircleWrapperMigration.version,
);
final lineUpGroupMigrated = migrateLineUpGroups(
customCircleMigrated,
force: originalVersion < LineUpGroupMigration.version,
);
final abilityVisionMigrated = migrateAbilityVisionCones(
lineUpGroupMigrated,
force: needsAbilityVisionMigration,
Expand All @@ -619,10 +635,31 @@ class StrategyProvider extends Notifier<StrategyState> {
abilityVisionMigrated,
force: needsPageNameProvenanceMigration,
);
return migrateCanonicalCoordinates(
final canonicalMigrated = migrateCanonicalCoordinates(
pageNameMigrated,
force: needsCanonicalCoordinatesMigration,
);
return migrateSunsetScale(
canonicalMigrated,
force: originalVersion < SunsetScaleMigration.version,
);
}

static StrategyData migrateSunsetScale(
StrategyData strat, {
bool force = false,
}) {
if (!force && strat.versionNumber >= SunsetScaleMigration.version) {
return strat;
}
return strat.copyWith(
pages: SunsetScaleMigration.migratePages(
pages: strat.pages,
map: strat.mapData,
),
versionNumber: Settings.versionNumber,
lastEdited: DateTime.now(),
);
}

static StrategyData migrateCanonicalCoordinates(StrategyData strat,
Expand Down Expand Up @@ -776,10 +813,12 @@ class StrategyProvider extends Notifier<StrategyState> {
abilityVisionMigrated,
force: originalVersion < PageNameProvenanceMigration.version,
);
return migrateCanonicalCoordinates(
final canonicalMigrated = migrateCanonicalCoordinates(
pageNameMigrated,
force: originalVersion < CanonicalCoordinatesMigration.version,
);
return migrateSunsetScale(canonicalMigrated,
force: originalVersion < SunsetScaleMigration.version);
}

static StrategyData migrateToWorld16x9(StrategyData strat,
Expand Down
Loading
Loading