diff --git a/assets/agents/Hush/1.webp b/assets/agents/Hush/1.webp new file mode 100644 index 000000000..d03449047 Binary files /dev/null and b/assets/agents/Hush/1.webp differ diff --git a/assets/agents/Hush/2.webp b/assets/agents/Hush/2.webp new file mode 100644 index 000000000..a50f2c0c0 Binary files /dev/null and b/assets/agents/Hush/2.webp differ diff --git a/assets/agents/Hush/3.webp b/assets/agents/Hush/3.webp new file mode 100644 index 000000000..e839493b1 Binary files /dev/null and b/assets/agents/Hush/3.webp differ diff --git a/assets/agents/Hush/4.webp b/assets/agents/Hush/4.webp new file mode 100644 index 000000000..469c17309 Binary files /dev/null and b/assets/agents/Hush/4.webp differ diff --git a/assets/agents/Hush/icon.webp b/assets/agents/Hush/icon.webp new file mode 100644 index 000000000..608ef5a98 Binary files /dev/null and b/assets/agents/Hush/icon.webp differ diff --git a/lib/const/agents.dart b/lib/const/agents.dart index 115230f09..1602c3c2a 100644 --- a/lib/const/agents.dart +++ b/lib/const/agents.dart @@ -32,6 +32,7 @@ enum AgentType { waylay, veto, miks, + hush, } enum AgentState { @@ -1192,6 +1193,38 @@ class AgentData implements DraggableData { agent.abilities[3] = miksHarmonize; agent.abilities.add(miksUlt); + return agent; + })(), + AgentType.hush: (() { + final agent = AgentData( + type: AgentType.hush, + role: AgentRole.duelist, + name: "Hush", + abilityNames: const [ + "Flow", + "Flashbulb", + "Displacement", + "Suspension", + ], + ); + + // Radii from the Valorant 13.09 files: Displacement's pull area + // (RangeRadius 575) and the players Suspension lifts + // (PlayerHitRadius 700; its 900 dome only lifts dropped guns). + agent.abilities[2].abilityData = CircleAbility( + iconPath: agent.abilities[2].iconPath, + size: 5.75, + rangeOutlineColor: const Color(0xFFF47AC7), + hasCenterDot: true, + ); + + agent.abilities.last.abilityData = CircleAbility( + iconPath: agent.abilities.last.iconPath, + size: 7, + rangeOutlineColor: const Color(0xFFF47AC7), + hasCenterDot: true, + ); + return agent; })(), }; diff --git a/lib/const/json_converters.dart b/lib/const/json_converters.dart index e07dceb9b..618eabeac 100644 --- a/lib/const/json_converters.dart +++ b/lib/const/json_converters.dart @@ -53,6 +53,7 @@ const _agentTypeEnumMap = { AgentType.waylay: 'waylay', AgentType.veto: 'veto', AgentType.miks: 'miks', + AgentType.hush: 'hush', }; class AbilityInfoConverter diff --git a/lib/hive/hive_adapters.g.dart b/lib/hive/hive_adapters.g.dart index ecb23eaf1..c4cb3b17c 100644 --- a/lib/hive/hive_adapters.g.dart +++ b/lib/hive/hive_adapters.g.dart @@ -405,6 +405,8 @@ class AgentTypeAdapter extends TypeAdapter { return AgentType.veto; case 28: return AgentType.miks; + case 29: + return AgentType.hush; default: return AgentType.jett; } @@ -471,6 +473,8 @@ class AgentTypeAdapter extends TypeAdapter { writer.writeByte(27); case AgentType.miks: writer.writeByte(28); + case AgentType.hush: + writer.writeByte(29); } } diff --git a/lib/hive/hive_adapters.g.yaml b/lib/hive/hive_adapters.g.yaml index 71b21b088..d1a5cdbf9 100644 --- a/lib/hive/hive_adapters.g.yaml +++ b/lib/hive/hive_adapters.g.yaml @@ -129,7 +129,7 @@ types: index: 12 AgentType: typeId: 7 - nextIndex: 29 + nextIndex: 30 fields: jett: index: 0 @@ -189,6 +189,8 @@ types: index: 27 miks: index: 28 + hush: + index: 29 Offset: typeId: 8 nextIndex: 2 diff --git a/lib/replay/replay_ability_catalog.dart b/lib/replay/replay_ability_catalog.dart index 97b4d0943..50e1b30b2 100644 --- a/lib/replay/replay_ability_catalog.dart +++ b/lib/replay/replay_ability_catalog.dart @@ -85,6 +85,8 @@ const _followUps = { 'GameObject_Nox_Wall', 'GameObject_Nox_BarbedWire', 'Patch_Nox_BarbedWire', + 'GameObject_Paprika_E_RedirectPullArea', + 'GameObject_Paprika_X_LevitateZone', }; /// The asset name of a class path, the part Riot names the blueprint by: @@ -542,6 +544,18 @@ final Map _catalog = { 'GameObject_Mage_E_WorldSmoke': _smoke(AgentType.harbor, 2), 'GameObject_Mage_X_TidalWave': _rectangle(AgentType.harbor, 3), + // Hush (Paprika), 13.09 asset index. Flow's actor is its anchor; the + // Displacement and Suspension areas open where their projectiles go off. + 'GameObject_Paprika_4_PullBlast': _marker(AgentType.hush, 0), + 'Projectile_Paprika_Q_RedirectFlash': _projectile(AgentType.hush, 1), + 'Projectile_Paprika_Q_RedirectFlash_Underhand': + _projectile(AgentType.hush, 1), + 'Projectile_Paprika_E_RedirectPull': _flight(AgentType.hush, 2), + 'Projectile_Paprika_E_RedirectPull_Underhand': _flight(AgentType.hush, 2), + 'GameObject_Paprika_E_RedirectPullArea': _area(AgentType.hush, 2), + 'Projectile_Paprika_X_FullBendPop': _flight(AgentType.hush, 3), + 'GameObject_Paprika_X_LevitateZone': _area(AgentType.hush, 3), + // Iso (Sequoia). 'GameObject_Sequoia_4_MovingCover': _rectangle(AgentType.iso, 0), 'GameObject_Sequoia_4_MovingCover_Slower': _rectangle(AgentType.iso, 0), diff --git a/lib/replay/replay_agents.dart b/lib/replay/replay_agents.dart index 5ce8ad016..8d5a3e4ed 100644 --- a/lib/replay/replay_agents.dart +++ b/lib/replay/replay_agents.dart @@ -14,6 +14,7 @@ const Map replayAgentsById = { 'dade69b4-4f5a-8528-247b-219e5a1facd6': AgentType.fade, 'e370fa57-4757-3604-3648-499e1f642d3f': AgentType.gekko, '95b78ed7-4637-86d9-7e41-71ba8c293152': AgentType.harbor, + 'ddd5a15c-4fbe-42fa-3789-11a55ca5ee04': AgentType.hush, '0e38b510-41a8-5780-5e8f-568b2a4f2d6c': AgentType.iso, 'add6443a-41bd-e414-f6ad-e58d267f4e95': AgentType.jett, '601dbbe7-43ce-be57-2a40-4abd24953621': AgentType.kayo, diff --git a/pubspec.yaml b/pubspec.yaml index b074b9eb2..40c2d7d56 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -155,5 +155,6 @@ flutter: - assets/agents/Waylay/ - assets/agents/Veto/ - assets/agents/Miks/ + - assets/agents/Hush/ uses-material-design: true diff --git a/test/agent_ability_names_test.dart b/test/agent_ability_names_test.dart index 633b8718d..29cc50169 100644 --- a/test/agent_ability_names_test.dart +++ b/test/agent_ability_names_test.dart @@ -102,6 +102,7 @@ void main() { 'Harmonize', 'Bassquake', ], + AgentType.hush: ['Flow', 'Flashbulb', 'Displacement', 'Suspension'], }; expect(AgentData.agents.keys.toSet(), expectedNames.keys.toSet()); diff --git a/test/canvas_compatibility_fixture_test.dart b/test/canvas_compatibility_fixture_test.dart index b97132df8..5f5ebef1a 100644 --- a/test/canvas_compatibility_fixture_test.dart +++ b/test/canvas_compatibility_fixture_test.dart @@ -12,6 +12,9 @@ import 'package:path/path.dart' as path; const _fixtureName = 'canvas-compatibility-v97.ica'; +/// Agents Icarus added after the fixture was frozen. +const _agentsSinceFixture = {AgentType.hush}; + void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -58,13 +61,16 @@ void main() { } }); - test('contains every current agent and ability', () { + test('contains every agent and ability Icarus had at v97', () { final agents = _entries(pages, 'agentData'); final actualAgentTypes = agents.map((agent) => agent['type'] as String).toSet(); expect( actualAgentTypes, - AgentType.values.map((type) => type.name).toSet(), + AgentType.values + .where((type) => !_agentsSinceFixture.contains(type)) + .map((type) => type.name) + .toSet(), ); expect( agents.map((agent) => agent['kind'] as String? ?? 'plain').toSet(), @@ -75,8 +81,9 @@ void main() { final expectedAbilities = { for (final type in AgentType.values) - for (final info in AgentData.agents[type]!.abilities) - '${type.name}:${info.index}', + if (!_agentsSinceFixture.contains(type)) + for (final info in AgentData.agents[type]!.abilities) + '${type.name}:${info.index}', }; final actualAbilities = _entries(pages, 'abilityData') .map((ability) => Map.from(ability['data'] as Map)) diff --git a/test/desktop_4_6_3_adapters_test_support.dart b/test/desktop_4_6_3_adapters_test_support.dart index 27bd1eb03..4ef4b2718 100644 --- a/test/desktop_4_6_3_adapters_test_support.dart +++ b/test/desktop_4_6_3_adapters_test_support.dart @@ -1,7 +1,9 @@ // Desktop 4.6.3's adapters for the typeIds this build writes by hand, copied // verbatim from the desktop-stable-v4.6.3+103 tag (only the class names -// changed), plus its WeaponTypeAdapter, which this build extended with the -// Warden. Every other adapter is unchanged since that release. Rollback +// changed), plus its WeaponTypeAdapter and AgentTypeAdapter, which this +// build extended with the Warden and Hush. Each of those two gains only the +// write case Dart's exhaustive switch demands for the new value, which +// throws. Every other adapter is unchanged since that release. Rollback // tests decode with these to prove 4.6.3 opens, and keeps, what this build // saves. Never edit them to match this build. @@ -619,6 +621,155 @@ class Desktop463WeaponTypeAdapter extends TypeAdapter { typeId == other.typeId; } +class Desktop463AgentTypeAdapter extends TypeAdapter { + @override + final typeId = 7; + + @override + AgentType read(BinaryReader reader) { + switch (reader.readByte()) { + case 0: + return AgentType.jett; + case 1: + return AgentType.raze; + case 2: + return AgentType.pheonix; + case 3: + return AgentType.astra; + case 4: + return AgentType.clove; + case 5: + return AgentType.breach; + case 6: + return AgentType.iso; + case 7: + return AgentType.viper; + case 8: + return AgentType.deadlock; + case 9: + return AgentType.yoru; + case 10: + return AgentType.sova; + case 11: + return AgentType.skye; + case 12: + return AgentType.kayo; + case 13: + return AgentType.killjoy; + case 14: + return AgentType.brimstone; + case 15: + return AgentType.cypher; + case 16: + return AgentType.chamber; + case 17: + return AgentType.fade; + case 18: + return AgentType.gekko; + case 19: + return AgentType.harbor; + case 20: + return AgentType.neon; + case 21: + return AgentType.omen; + case 22: + return AgentType.reyna; + case 23: + return AgentType.sage; + case 24: + return AgentType.vyse; + case 25: + return AgentType.tejo; + case 26: + return AgentType.waylay; + case 27: + return AgentType.veto; + case 28: + return AgentType.miks; + default: + return AgentType.jett; + } + } + + @override + void write(BinaryWriter writer, AgentType obj) { + switch (obj) { + case AgentType.jett: + writer.writeByte(0); + case AgentType.raze: + writer.writeByte(1); + case AgentType.pheonix: + writer.writeByte(2); + case AgentType.astra: + writer.writeByte(3); + case AgentType.clove: + writer.writeByte(4); + case AgentType.breach: + writer.writeByte(5); + case AgentType.iso: + writer.writeByte(6); + case AgentType.viper: + writer.writeByte(7); + case AgentType.deadlock: + writer.writeByte(8); + case AgentType.yoru: + writer.writeByte(9); + case AgentType.sova: + writer.writeByte(10); + case AgentType.skye: + writer.writeByte(11); + case AgentType.kayo: + writer.writeByte(12); + case AgentType.killjoy: + writer.writeByte(13); + case AgentType.brimstone: + writer.writeByte(14); + case AgentType.cypher: + writer.writeByte(15); + case AgentType.chamber: + writer.writeByte(16); + case AgentType.fade: + writer.writeByte(17); + case AgentType.gekko: + writer.writeByte(18); + case AgentType.harbor: + writer.writeByte(19); + case AgentType.neon: + writer.writeByte(20); + case AgentType.omen: + writer.writeByte(21); + case AgentType.reyna: + writer.writeByte(22); + case AgentType.sage: + writer.writeByte(23); + case AgentType.vyse: + writer.writeByte(24); + case AgentType.tejo: + writer.writeByte(25); + case AgentType.waylay: + writer.writeByte(26); + case AgentType.veto: + writer.writeByte(27); + case AgentType.miks: + writer.writeByte(28); + // Not in 4.6.3, which can never hold one; only Dart's exhaustive + // switch needs the case. + case AgentType.hush: + throw StateError('Desktop 4.6.3 has no Hush'); + } + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is Desktop463AgentTypeAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + int _readDrawingHiveColorValue( Map fields, { required int colorFieldIndex, diff --git a/test/hive_desktop_rollback_test.dart b/test/hive_desktop_rollback_test.dart index d954d0244..ec1d665a2 100644 --- a/test/hive_desktop_rollback_test.dart +++ b/test/hive_desktop_rollback_test.dart @@ -30,8 +30,9 @@ HiveImpl _currentRegistry() { } /// What desktop 4.6.3 registers: this build's adapters, which are unchanged -/// since that release, except the eight this build writes by hand and the -/// weapon list, which gained the Warden. +/// since that release, except the eight this build writes by hand, the +/// weapon list, which gained the Warden, and the agent list, which gained +/// Hush. HiveImpl _desktop463Registry() { final registry = _currentRegistry(); // hive_ce warns on every override, which is the point here. @@ -47,7 +48,8 @@ HiveImpl _desktop463Registry() { ..registerAdapter(Desktop463LineAdapter(), override: true) ..registerAdapter(Desktop463RectangleDrawingAdapter(), override: true) ..registerAdapter(Desktop463EllipseDrawingAdapter(), override: true) - ..registerAdapter(Desktop463WeaponTypeAdapter(), override: true); + ..registerAdapter(Desktop463WeaponTypeAdapter(), override: true) + ..registerAdapter(Desktop463AgentTypeAdapter(), override: true); } finally { Logger.level = previousLevel; } @@ -77,6 +79,39 @@ PlacedAbility _ability( ); } +/// A page holding each of [types] and every one of its abilities, with ids by +/// position so pages of different agents compare. +StrategyPage _rosterPage(List types) { + return StrategyPage( + id: 'page-roster', + name: 'Roster', + drawingData: const [], + agentData: [ + for (var a = 0; a < types.length; a++) + PlacedAgent( + id: 'agent-$a', + type: types[a], + position: const Offset(1, 1), + ), + ], + abilityData: [ + for (var a = 0; a < types.length; a++) + for (final (i, info) in AgentData.agents[types[a]]!.abilities.indexed) + PlacedAbility( + id: 'ability-$a-$i', + data: info, + position: const Offset(5, 5), + ), + ], + textData: const [], + imageData: const [], + utilityData: const [], + sortIndex: 0, + isAttack: true, + settings: StrategySettings(), + ); +} + StrategyPage _richPage() { const hidden = AbilityVisualState(showRangeFill: false); return StrategyPage( @@ -215,39 +250,23 @@ void main() { _expectSamePage(_across(desktop463, current, rolledBack), page); }); - test('every agent and ability survives a rollback', () { - final page = StrategyPage( - id: 'page-roster', - name: 'Roster', - drawingData: const [], - agentData: [ - for (final type in AgentType.values) - PlacedAgent( - id: 'agent-${type.name}', - type: type, - position: const Offset(1, 1), - ), - ], - abilityData: [ - for (final agent in AgentData.agents.values) - for (var i = 0; i < agent.abilities.length; i++) - PlacedAbility( - id: 'ability-${agent.type.name}-$i', - data: agent.abilities[i], - position: const Offset(5, 5), - ), - ], - textData: const [], - imageData: const [], - utilityData: const [], - sortIndex: 0, - isAttack: true, - settings: StrategySettings(), - ); + test('every agent 4.6.3 knows, and its abilities, survives a rollback', () { + final page = _rosterPage([ + for (final type in AgentType.values) + if (type != AgentType.hush) type, + ]); _expectSamePage(_across(current, desktop463, page), page); }); + test('desktop 4.6.3 opens a Hush this build saved as Jett', () { + // 4.6.3 reads an agent index it doesn't know as Jett, and an ability by + // its agent and index, so Hush's four abilities open as Jett's. + final opened = _across(current, desktop463, _rosterPage([AgentType.hush])); + + _expectSamePage(opened, _rosterPage([AgentType.jett])); + }); + test('folders keep their exact icon and color through a rollback', () { for (final iconId in [ FolderIconRegistry.defaultId,