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
14 changes: 11 additions & 3 deletions lib/replay/replay_frame.dart
Original file line number Diff line number Diff line change
Expand Up @@ -316,13 +316,18 @@ class ReplayFrameBuilder {
/// How much of a thrown utility's path trails behind it.
static const trailMs = 350;

/// The placed id of [subject]'s agent on every frame.
static String agentIdOf(String subject) => 'replay-player-$subject';

/// The moment at [timeMs]. With [cuts], each living player's cone is
/// asked for where they are now and drawn from its latest cut; see
/// [ReplayFrame.cones].
/// [ReplayFrame.cones]. While [watching] a player, only theirs is: the
/// rest are drawn without a cone, and none is cut for them.
ReplayFrame frameAt(
int timeMs, {
required ReplayTeam perspective,
ReplayConeCuts? cuts,
String? watching,
}) {
final round = document.roundAt(timeMs);
final isAttack = (round?.attackingTeam ?? ReplayTeam.red) == perspective;
Expand All @@ -345,7 +350,7 @@ class ReplayFrameBuilder {
if (!_onCanvas(standing)) continue;
final position = standing - agentAnchor;
final isAlly = team == perspective;
final id = 'replay-player-${player.subject}';
final id = agentIdOf(player.subject);
final visionElevation = _visionElevation(pose);
final rotation = projection.rotationForYaw(pose.yaw);
// A flash shortens the cone; one shorter than the editor allows is
Expand All @@ -354,7 +359,10 @@ class ReplayFrameBuilder {
state.alive ? document.blinds[player.subject]?.flashAt(timeMs) : null;
final blindness = flash?.blindness ?? 0.0;
final sightLength = coneLength * (1 - blindness);
final sees = state.alive && sightLength >= ViewConeUtility.minLength;
final watched = watching == null || watching == player.subject;
final sees = watched &&
state.alive &&
sightLength >= ViewConeUtility.minLength;
if (flash != null) {
flashed.add(ReplayFlashed(
agentId: id,
Expand Down
78 changes: 77 additions & 1 deletion lib/replay/replay_playback.dart
Original file line number Diff line number Diff line change
Expand Up @@ -49,19 +49,95 @@ class ReplayPlayback extends ChangeNotifier {
final cached = _frame;
if (cached != null &&
cached.timeMs == _timeMs &&
_framePerspective == _perspective) {
_framePerspective == _perspective &&
_frameWatching == _watching) {
return cached;
}
_framePerspective = _perspective;
_frameWatching = _watching;
return _frame = frames.frameAt(
_timeMs,
perspective: _perspective,
cuts: _coneCuts,
watching: _watching,
);
}

ReplayFrame? _frame;
ReplayTeam? _framePerspective;
String? _frameWatching;

/// The moment on screen built afresh, sharing no objects with [frame]:
/// what a capture saves, cones and all, exactly as drawn.
ReplayFrame freshFrame() => frames.frameAt(
_timeMs,
perspective: _perspective,
watching: _watching,
);

/// The player being watched, by subject: only their view cone is drawn.
/// Null while everyone's is.
String? get watching => _watching;
String? _watching;

/// Whether the camera keeps the watched player centred. Moving the map
/// by hand stops it; picking the player again starts it again.
bool get following => _following;
var _following = false;

/// Picks [subject] to watch, and follows them. Picking the player already
/// watched follows them again if the camera had stopped, and otherwise
/// stops watching.
void watch(String subject) {
if (_watching == subject && _following) {
stopWatching();
return;
}
_watching = subject;
_following = true;
notifyListeners();
}

void stopWatching() {
if (_watching == null) return;
_watching = null;
_following = false;
notifyListeners();
}

/// The user moved the map: the watched player stays watched, but the
/// camera stops following them.
void stopFollowing() {
if (!_following) return;
_following = false;
notifyListeners();
}

/// [team]'s players in the order the roster lists them.
List<ReplayPlayer> playersOf(ReplayTeam team) => [
for (final player in document.players)
if (player.team == team) player,
];

/// The player watched with number key [slot] (0 for the 1 key, 9 for the
/// 0 key): the allies top to bottom on 1–5, the enemies on 6–0.
ReplayPlayer? playerInSlot(int slot) {
final team = slot < 5 ? _perspective : _perspective.other;
final players = playersOf(team);
final index = slot % 5;
return index < players.length ? players[index] : null;
}

/// The number key that watches [player], as it is labelled; null when
/// they have none.
String? watchKeyOf(ReplayPlayer player) {
final team = player.team;
if (team == null) return null;
final index = playersOf(team).indexOf(player);
if (index < 0 || index >= 5) return null;
final slot = index + (team == _perspective ? 0 : 5);
return '${(slot + 1) % 10}';
}

/// Where view cones are cut, once the worker is up.
ReplayConeCuts? get coneCuts => _coneCuts;
Expand Down
68 changes: 51 additions & 17 deletions lib/replay_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -256,23 +256,8 @@ class _ReplayViewState extends ConsumerState<ReplayView>

KeyEventResult _onKey(FocusNode node, KeyEvent event) {
final playback = _playback;
if (playback == null || event is KeyUpEvent) return KeyEventResult.ignored;
switch (event.logicalKey) {
case LogicalKeyboardKey.space:
if (event is KeyRepeatEvent) return KeyEventResult.handled;
playback.togglePlaying();
case LogicalKeyboardKey.arrowLeft:
playback.seek(playback.timeMs - 5000);
case LogicalKeyboardKey.arrowRight:
playback.seek(playback.timeMs + 5000);
case LogicalKeyboardKey.arrowUp:
playback.previousRound();
case LogicalKeyboardKey.arrowDown:
playback.nextRound();
default:
return KeyEventResult.ignored;
}
return KeyEventResult.handled;
if (playback == null) return KeyEventResult.ignored;
return replayKey(playback, event);
}

@override
Expand Down Expand Up @@ -306,6 +291,55 @@ class _ReplayViewState extends ConsumerState<ReplayView>
}
}

/// What a key does to [playback]: Space plays and pauses, the arrows move
/// through time and rounds, 1–0 watch a player (the allies top to bottom on
/// 1–5, the enemies on 6–0, as the roster lists them) and Esc stops
/// watching.
KeyEventResult replayKey(ReplayPlayback playback, KeyEvent event) {
if (event is KeyUpEvent) return KeyEventResult.ignored;
final slot = _watchSlots.indexOf(event.logicalKey);
if (slot >= 0) {
// Held down, the key would flick between watching and not.
if (event is KeyRepeatEvent) return KeyEventResult.handled;
final player = playback.playerInSlot(slot);
if (player == null) return KeyEventResult.ignored;
playback.watch(player.subject);
return KeyEventResult.handled;
}
switch (event.logicalKey) {
case LogicalKeyboardKey.space:
if (event is KeyRepeatEvent) return KeyEventResult.handled;
playback.togglePlaying();
case LogicalKeyboardKey.arrowLeft:
playback.seek(playback.timeMs - 5000);
case LogicalKeyboardKey.arrowRight:
playback.seek(playback.timeMs + 5000);
case LogicalKeyboardKey.arrowUp:
playback.previousRound();
case LogicalKeyboardKey.arrowDown:
playback.nextRound();
case LogicalKeyboardKey.escape when playback.watching != null:
playback.stopWatching();
default:
return KeyEventResult.ignored;
}
return KeyEventResult.handled;
}

/// The number row, 1 to 0, as the game's replay numbers its players.
const _watchSlots = [
LogicalKeyboardKey.digit1,
LogicalKeyboardKey.digit2,
LogicalKeyboardKey.digit3,
LogicalKeyboardKey.digit4,
LogicalKeyboardKey.digit5,
LogicalKeyboardKey.digit6,
LogicalKeyboardKey.digit7,
LogicalKeyboardKey.digit8,
LogicalKeyboardKey.digit9,
LogicalKeyboardKey.digit0,
];

class _ReplayStrip extends StatelessWidget {
const _ReplayStrip({required this.map, required this.probe});

Expand Down
Loading
Loading