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
27 changes: 27 additions & 0 deletions assets/weapons/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Weapon silhouettes

Bundled in-game kill-feed icons retrieved from [Valorant-API](https://valorant-api.com/v1/weapons) on 2026-09-13. Valorant and the artwork belong to Riot Games. Valorant-API is an unofficial asset source.

The PNGs are unmodified and retain transparency. `WeaponIcon` fits their visible bounds at render time so wide transparent margins do not shrink pistols. No runtime downloads are needed.

| File | Source |
| --- | --- |
| odin.png | [Odin](https://media.valorant-api.com/weapons/63e6c2b6-4a8e-869c-3d4c-e38355226584/killstreamicon.png) |
| ares.png | [Ares](https://media.valorant-api.com/weapons/55d8a0f4-4274-ca67-fe2c-06ab45efdf58/killstreamicon.png) |
| vandal.png | [Vandal](https://media.valorant-api.com/weapons/9c82e19d-4575-0200-1a81-3eacf00cf872/killstreamicon.png) |
| bulldog.png | [Bulldog](https://media.valorant-api.com/weapons/ae3de142-4d85-2547-dd26-4e90bed35cf7/killstreamicon.png) |
| phantom.png | [Phantom](https://media.valorant-api.com/weapons/ee8e8d15-496b-07ac-e5f6-8fae5d4c7b1a/killstreamicon.png) |
| judge.png | [Judge](https://media.valorant-api.com/weapons/ec845bf4-4f79-ddda-a3da-0db3774b2794/killstreamicon.png) |
| bucky.png | [Bucky](https://media.valorant-api.com/weapons/910be174-449b-c412-ab22-d0873436b21b/killstreamicon.png) |
| frenzy.png | [Frenzy](https://media.valorant-api.com/weapons/44d4e95c-4157-0037-81b2-17841bf2e8e3/killstreamicon.png) |
| classic.png | [Classic](https://media.valorant-api.com/weapons/29a0cfab-485b-f5d5-779a-b59f85e204a8/killstreamicon.png) |
| bandit.png | [Bandit](https://media.valorant-api.com/weapons/410b2e0b-4ceb-1321-1727-20858f7f3477/killstreamicon.png) |
| ghost.png | [Ghost](https://media.valorant-api.com/weapons/1baa85b4-4c70-1284-64bb-6481dfc3bb4e/killstreamicon.png) |
| sheriff.png | [Sheriff](https://media.valorant-api.com/weapons/e336c6b8-418d-9340-d77f-7a9e4cfe0702/killstreamicon.png) |
| shorty.png | [Shorty](https://media.valorant-api.com/weapons/42da8ccc-40d5-affc-beec-15aa47b42eda/killstreamicon.png) |
| operator.png | [Operator](https://media.valorant-api.com/weapons/a03b24d3-4319-996d-0f8c-94bbfba1dfc7/killstreamicon.png) |
| guardian.png | [Guardian](https://media.valorant-api.com/weapons/4ade7faa-4cf1-8376-95ef-39884480959b/killstreamicon.png) |
| outlaw.png | [Outlaw](https://media.valorant-api.com/weapons/5f0aaf7a-4289-3998-d5ff-eb9a5cf7ef5c/killstreamicon.png) |
| marshal.png | [Marshal](https://media.valorant-api.com/weapons/c4883e50-4494-202c-3ec3-6b8a9284f00b/killstreamicon.png) |
| spectre.png | [Spectre](https://media.valorant-api.com/weapons/462080d1-4035-2937-7c09-27aa2a5c27a7/killstreamicon.png) |
| stinger.png | [Stinger](https://media.valorant-api.com/weapons/f7e1b454-4ad4-1063-ec0a-159e56b58941/killstreamicon.png) |
Binary file added assets/weapons/ares.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/weapons/bandit.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/weapons/bucky.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/weapons/bulldog.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/weapons/classic.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/weapons/frenzy.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/weapons/ghost.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/weapons/guardian.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/weapons/judge.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/weapons/marshal.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/weapons/odin.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/weapons/operator.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/weapons/outlaw.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/weapons/phantom.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/weapons/sheriff.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/weapons/shorty.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/weapons/spectre.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/weapons/stinger.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/weapons/vandal.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
28 changes: 28 additions & 0 deletions docs/verification/agent-firearms/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Agent firearm verification

Windows captures from commit `9a1c96ddc7416f5ce4f1cc29ad45113a67455126`, taken on 2026-09-13. The native test app uses in-memory providers and never opens a user's library.

Run from PowerShell:

```powershell
$env:ICARUS_WEAPON_TEST_ARTIFACT_DIR = "$PWD/firearm-captures"
flutter test integration_test/agent_weapon_test.dart -d windows
```

All 11 scenarios passed. [The native output](windows-test-output.txt) includes `gun overhang paints outside the portrait and has no hit target`. That scenario checks the badge bounds, moves and right-clicks outside the portrait, then requires actual white pixels outside the portrait in the rendered image. The pixel assertion is unconditional.

The screenshots below were also inspected visually. Every gun is visible at the portrait's bottom-right, and all portraits keep the same dimensions. The white silhouette has a dark outline for contrast against the portrait.

![All 19 firearm badges beside an agent without a firearm](all-firearms.png)

![Right-click category menu with sidearms](sidearms-menu.png)

The Windows CI run also passed all 493 unit/widget tests, including the overhang pixel assertion, with one existing skipped test: [CI run](https://github.com/SunkenInTime/icarus/actions/runs/34738623168).

## Isolated pixel check

Greptile's first review exposed a test timing issue. Running only the overhang test started with a cold image cache and captured before the firearm asset decoded. The full suite had already loaded it during the menu scenarios. The test now awaits `precacheImage` before inspecting pixels. This isolated command reproduces the original failure before that fix and passes after it:

```powershell
flutter test test/agent_weapon_widgets_test.dart --plain-name 'gun overhang paints outside the portrait and has no hit target'
```
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
26 changes: 26 additions & 0 deletions docs/verification/agent-firearms/windows-test-output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
00:00 +0: loading C:/Users/shawn/.codex/worktrees/icarus-release/agent-firearms/integration_test/agent_weapon_test.dart
Building Windows application...
CMake Warning (dev) at flutter/ephemeral/.plugin_symlinks/flutter_inappwebview_windows/windows/CMakeLists.txt:31 (add_custom_command):
The following keywords are not supported when using
add_custom_command(TARGET): DEPENDS.

Policy CMP0175 is not set: add_custom_command() rejects invalid arguments.
Run "cmake --help-policy CMP0175" for policy details. Use the cmake_policy
command to set the policy and suppress this warning.
This warning is for project developers. Use -Wno-dev to suppress it.

Building Windows application... 47.3s
√ Built build\windows\x64\runner\Debug\icarus.exe
00:00 +0: plain agent selects, changes and removes a firearm by menu
00:05 +1: plain firearm preserves drag anchor at different sizes and zooms
00:07 +2: cone agent selects, changes and removes a firearm by menu
00:12 +3: cone firearm preserves drag anchor at different sizes and zooms
00:14 +4: circle agent selects, changes and removes a firearm by menu
00:18 +5: circle firearm preserves drag anchor at different sizes and zooms
00:21 +6: gun overhang paints outside the portrait and has no hit target
00:22 +7: lineup origin offers the same firearm menu
00:23 +8: lineup draft firearm edits do not change the source agent
00:25 +9: shared page and video renderer fades weapon changes at one anchor
00:27 +10: every bundled firearm renders beside an unchanged portrait
00:28 +11: (tearDownAll)
00:28 +11: All tests passed!
10 changes: 10 additions & 0 deletions integration_test/agent_weapon_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import 'package:integration_test/integration_test.dart';

import '../test/agent_weapon_widgets_test.dart' as scenarios;

/// Runs the same interactions on the native desktop engine. The harness uses
/// in-memory providers and never opens the user's Hive library or other windows.
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
scenarios.main();
}
42 changes: 42 additions & 0 deletions lib/const/line_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'dart:ui';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hive_ce_flutter/adapters.dart';
import 'package:icarus/const/agents.dart';
import 'package:icarus/const/weapons.dart';
import 'package:icarus/const/placed_classes.dart';
import 'package:icarus/const/settings.dart';
import 'package:icarus/providers/action_provider.dart';
Expand Down Expand Up @@ -730,6 +731,15 @@ class LineUpProvider extends Notifier<LineUpState> {
state = state.copyWith(placement: state.placement!.copyWith(draftAgent: draft));
}

void setDraftAgentWeapon(WeaponType weapon) {
final placement = state.placement;
final draft = placement?.draftAgent;
if (placement == null || draft == null || draft.weapon == weapon) return;
state = state.copyWith(
placement: placement.copyWith(draftAgent: draft.copyWith(weapon: weapon)),
);
}

void updateDraftAbilityPosition(Offset position) {
final draft = state.placement?.draftAbility;
if (draft == null) return;
Expand Down Expand Up @@ -818,6 +828,30 @@ class LineUpProvider extends Notifier<LineUpState> {
return link;
}

void setOriginWeapon(String originId, WeaponType weapon) {
final index = state.origins.indexWhere((origin) => origin.id == originId);
if (index < 0 || state.origins[index].agent.weapon == weapon) return;
final previous = state.origins[index].agent.weapon;
_applyOriginWeapon(originId, weapon);
ref.read(actionProvider.notifier).addAction(WeaponSelectionAction(
id: originId,
group: ActionGroup.lineUp,
before: previous,
after: weapon,
));
}

void _applyOriginWeapon(String originId, WeaponType weapon) {
final index = state.origins.indexWhere((origin) => origin.id == originId);
if (index < 0) return;
final origins = [...state.origins];
final origin = origins[index];
origins[index] = origin.copyWith(
agent: origin.agent.copyWith(weapon: weapon),
);
state = state.copyWith(origins: origins);
}

// --- Edits (no action recorded; callers wrap in performTransaction) -------

void updateLink(LineUpLink link) {
Expand Down Expand Up @@ -956,6 +990,10 @@ class LineUpProvider extends Notifier<LineUpState> {
}

void undoAction(UserAction action) {
if (action is WeaponSelectionAction) {
_applyOriginWeapon(action.id, action.before);
return;
}
switch (action.type) {
case ActionType.addition:
_removeForAction(action.id);
Expand All @@ -971,6 +1009,10 @@ class LineUpProvider extends Notifier<LineUpState> {
}

void redoAction(UserAction action) {
if (action is WeaponSelectionAction) {
_applyOriginWeapon(action.id, action.after);
return;
}
switch (action.type) {
case ActionType.addition:
_restoreForAction(action.id);
Expand Down
14 changes: 14 additions & 0 deletions lib/const/placed_classes.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
import 'package:hive_ce_flutter/hive_flutter.dart';
import 'package:icarus/const/abilities.dart';
import 'package:icarus/const/agents.dart';
import 'package:icarus/const/weapons.dart';
import 'package:icarus/const/json_converters.dart';
import 'package:icarus/const/utilities.dart';
import 'package:json_annotation/json_annotation.dart';
Expand Down Expand Up @@ -312,12 +313,16 @@ sealed class PlacedAgentNode extends PlacedWidget {
@JsonKey(defaultValue: AgentState.none)
AgentState state;

@JsonKey(defaultValue: WeaponType.none)
WeaponType weapon;

PlacedAgentNode({
required this.type,
required super.position,
required super.id,
this.isAlly = true,
this.state = AgentState.none,
this.weapon = WeaponType.none,
});

String get kind;
Expand Down Expand Up @@ -362,6 +367,7 @@ class PlacedAgent extends PlacedAgentNode {
super.isAlly = true,
this.lineUpID,
super.state = AgentState.none,
super.weapon = WeaponType.none,
});

@override
Expand All @@ -383,6 +389,7 @@ class PlacedAgent extends PlacedAgentNode {
bool? isAlly,
String? lineUpID,
AgentState? state,
WeaponType? weapon,
}) {
final copied = PlacedAgent(
type: type ?? this.type,
Expand All @@ -391,6 +398,7 @@ class PlacedAgent extends PlacedAgentNode {
isAlly: isAlly ?? this.isAlly,
lineUpID: lineUpID ?? this.lineUpID,
state: state ?? this.state,
weapon: weapon ?? this.weapon,
);
copied.isDeleted = isDeleted;
return copied;
Expand Down Expand Up @@ -451,6 +459,7 @@ class PlacedViewConeAgent extends PlacedAgentNode {
this.visionElevation,
super.isAlly = true,
super.state = AgentState.none,
super.weapon = WeaponType.none,
}) : assert(
UtilityData.isViewConePresetType(presetType),
'presetType must be a view cone preset.',
Expand Down Expand Up @@ -546,6 +555,7 @@ class PlacedViewConeAgent extends PlacedAgentNode {
String? id,
bool? isAlly,
AgentState? state,
WeaponType? weapon,
UtilityType? presetType,
double? rotation,
double? length,
Expand All @@ -557,6 +567,7 @@ class PlacedViewConeAgent extends PlacedAgentNode {
id: id ?? this.id,
isAlly: isAlly ?? this.isAlly,
state: state ?? this.state,
weapon: weapon ?? this.weapon,
presetType: presetType ?? this.presetType,
rotation: rotation ?? this.rotation,
length: length ?? this.length,
Expand Down Expand Up @@ -584,6 +595,7 @@ class PlacedCircleAgent extends PlacedAgentNode {
this.opacityPercent = 100,
super.isAlly = true,
super.state = AgentState.none,
super.weapon = WeaponType.none,
});

@override
Expand Down Expand Up @@ -674,6 +686,7 @@ class PlacedCircleAgent extends PlacedAgentNode {
String? id,
bool? isAlly,
AgentState? state,
WeaponType? weapon,
double? diameterMeters,
int? colorValue,
int? opacityPercent,
Expand All @@ -684,6 +697,7 @@ class PlacedCircleAgent extends PlacedAgentNode {
id: id ?? this.id,
isAlly: isAlly ?? this.isAlly,
state: state ?? this.state,
weapon: weapon ?? this.weapon,
diameterMeters: diameterMeters ?? this.diameterMeters,
colorValue: colorValue ?? this.colorValue,
opacityPercent: opacityPercent ?? this.opacityPercent,
Expand Down
32 changes: 32 additions & 0 deletions lib/const/placed_classes.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion lib/const/settings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ class Settings {
static const double agentSize = 35;
static const double agentSizeMin = 15;
static const double agentSizeMax = 45;
static const double agentWeaponWidthRatio = 0.70;
static const double agentWeaponHeightRatio = 0.40;
static const double agentWeaponRightOverhangRatio = 0.18;
static const double agentWeaponBottomOverhangRatio = 0.12;
static const double agentWeaponOutlineWidthRatio = 0.035;

static const double abilitySize = 25;
static const double abilitySizeMin = 15;
Expand Down Expand Up @@ -100,7 +105,7 @@ class Settings {
static final Uri dicordLink = Uri.parse("https://discord.gg/PN2uKwCqYB");

static const Duration autoSaveOffset = Duration(seconds: 15);
static const int versionNumber = 99;
static const int versionNumber = 100;
static const String versionName = "4.6.1";
static final Uri desktopUpdaterArchiveUrl =
buildDesktopUpdaterArchiveUrl(kResolvedUpdateChannel);
Expand Down
7 changes: 6 additions & 1 deletion lib/const/transition_data.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'package:icarus/const/agents.dart';
import 'package:icarus/const/abilities.dart';
import 'package:icarus/const/coordinate_system.dart';
import 'package:icarus/const/placed_classes.dart';
import 'package:icarus/const/weapons.dart';
import 'package:icarus/const/settings.dart';
import 'package:icarus/const/utilities.dart';

Expand Down Expand Up @@ -481,7 +482,10 @@ class PageTransitionEntry {
return null;
}

/// Every property a [PageTransitionEntry.move] can tween, as one comparable
static WeaponType? weaponOf(PlacedWidget w) =>
w is PlacedAgentNode ? w.weapon : null;

/// Every property a [PageTransitionEntry.move] can animate, as one comparable
/// list. Kept next to the `*Of` helpers so adding a tweened property means
/// updating both in the same place.
static List<Object?> _tweenedPropsOf(PlacedWidget w) => [
Expand All @@ -491,6 +495,7 @@ class PageTransitionEntry {
scaleOf(w),
textSizeOf(w),
agentStateOf(w),
weaponOf(w),
customDiameterOf(w),
customWidthOf(w),
customLengthOf(w),
Expand Down
Loading
Loading