feat: implement fighter separation logic and optimize special effect casting

This commit is contained in:
Horoli 2026-05-29 17:44:24 +09:00
parent 23376e8cbb
commit 3b1a883787
3 changed files with 223 additions and 106 deletions

View File

@ -76,7 +76,7 @@ export const FIGHTER = {
TYPE: ["melee", "magic"], TYPE: ["melee", "magic"],
STACK_SIZE: 100, STACK_SIZE: 100,
VISUAL_SCALE_MULTIPLIER: 5, VISUAL_SCALE_MULTIPLIER: 5,
ATTACK_EFFECT_SCALE_MULTIPLIER: 5, ATTACK_EFFECT_SCALE_MULTIPLIER: 2,
HP_BONUS_RATIO: 2, HP_BONUS_RATIO: 2,
ATTACK_RANGE_MULTIPLIER: 1.5, ATTACK_RANGE_MULTIPLIER: 1.5,
ATTACK_DAMAGE_BONUS_MULTIPLIER: 1.1, ATTACK_DAMAGE_BONUS_MULTIPLIER: 1.1,
@ -146,6 +146,12 @@ export const COMBAT = {
FINAL_SLOW_MOTION_HOLD_DURATION: 14000, FINAL_SLOW_MOTION_HOLD_DURATION: 14000,
FINAL_SLOW_MOTION_EXIT_DURATION: 14000, FINAL_SLOW_MOTION_EXIT_DURATION: 14000,
FINAL_SLOW_MOTION_SCALE: 0.28, FINAL_SLOW_MOTION_SCALE: 0.28,
// 전투원 간 공간 분리 (spatial grid 기반 군중 밀착 방지)
FIGHTER_SEPARATION_ENABLED: true,
// 전투원 중심 간 최소 이격 거리(px). HITBOX_WIDTH=22, SCALE=3이므로 약 1.5배
FIGHTER_SEPARATION_DISTANCE: 42 * 3,
// 밀착 시 밀어내는 힘. moveSpeed(148~163)보다 낮아야 자연스러움
FIGHTER_SEPARATION_FORCE: 148 / 2,
}; };
// 5. PROJECTILE 도메인 // 5. PROJECTILE 도메인

View File

@ -38,16 +38,20 @@ export function updateFighter(scene, fighter, time, onWinner) {
|| !fighter.active || !fighter.active
|| fighter.isDead || fighter.isDead
|| fighter.isFrostStunned || fighter.isFrostStunned
|| fighter.isLocked
) { ) {
fighter.body?.setVelocity(0, 0); fighter.body?.setVelocity(0, 0);
return; return;
} }
if (fighter.isLocked) {
fighter.body?.setVelocity(0, 0);
return;
}
const enemy = resolveTargetEnemy(scene, fighter, time); const enemy = resolveTargetEnemy(scene, fighter, time);
if (!enemy) { if (!enemy) {
fighter.body?.setVelocity(0, 0); applySeparationVelocity(scene, fighter);
return; return;
} }
@ -63,11 +67,12 @@ export function updateFighter(scene, fighter, time, onWinner) {
enemy, enemy,
combatStatsFor(fighter).moveSpeed * fighterMovementSpeedMultiplier(fighter), combatStatsFor(fighter).moveSpeed * fighterMovementSpeedMultiplier(fighter),
); );
applySeparationVelocity(scene, fighter, { blend: true });
playIfNeeded(fighter, "walk"); playIfNeeded(fighter, "walk");
return; return;
} }
fighter.body.setVelocity(0, 0); applySeparationVelocity(scene, fighter);
if (time >= fighter.nextAttackAt) { if (time >= fighter.nextAttackAt) {
beginAttack(scene, fighter, enemy, time, onWinner); beginAttack(scene, fighter, enemy, time, onWinner);
@ -77,6 +82,68 @@ export function updateFighter(scene, fighter, time, onWinner) {
playIfNeeded(fighter, "idle"); playIfNeeded(fighter, "idle");
} }
function applySeparationVelocity(scene, fighter, { blend = false } = {}) {
if (!COMBAT.FIGHTER_SEPARATION_ENABLED) {
if (!blend) {
fighter.body?.setVelocity(0, 0);
}
return;
}
const targetIndex = scene.combatTargetIndex;
if (!targetIndex) {
fighter.body?.setVelocity(0, 0);
return;
}
const cellX = clampCell(fighter.x, targetIndex.cellSize, targetIndex.maxCellX);
const cellY = clampCell(fighter.y, targetIndex.cellSize, targetIndex.maxCellY);
const sepDist = COMBAT.FIGHTER_SEPARATION_DISTANCE;
const sepDistSq = sepDist * sepDist;
const maxForce = COMBAT.FIGHTER_SEPARATION_FORCE;
let forceX = 0;
let forceY = 0;
for (let dx = -1; dx <= 1; dx += 1) {
for (let dy = -1; dy <= 1; dy += 1) {
const cell = targetIndex.cells.get(targetCellKey(cellX + dx, cellY + dy));
if (!cell) {
continue;
}
cell.forEach((candidate) => {
if (candidate === fighter || !candidate.active || candidate.isDead) {
return;
}
const deltaX = fighter.x - candidate.x;
const deltaY = fighter.y - candidate.y;
const distSq = deltaX * deltaX + deltaY * deltaY;
if (distSq >= sepDistSq || distSq < 0.001) {
return;
}
const dist = Math.sqrt(distSq);
const strength = (1 - dist / sepDist) * maxForce;
forceX += (deltaX / dist) * strength;
forceY += (deltaY / dist) * strength;
});
}
}
if (blend) {
fighter.body.velocity.x += forceX;
fighter.body.velocity.y += forceY;
return;
}
fighter.body.setVelocity(forceX, forceY);
}
export function clearCombatObjects(scene) { export function clearCombatObjects(scene) {
scene.combatObjects?.forEach((object) => { scene.combatObjects?.forEach((object) => {
object.cleanup?.(); object.cleanup?.();

View File

@ -1,8 +1,5 @@
import Phaser from "phaser"; import Phaser from "phaser";
import { import { ARENA, SPECIAL_EFFECT } from "../../constants.js";
ARENA,
SPECIAL_EFFECT,
} from "../../constants.js";
import { import {
applySpecialEffectInstantKill, applySpecialEffectInstantKill,
disposeCombatObject, disposeCombatObject,
@ -12,10 +9,7 @@ import {
ensureFighterTeamAnimation, ensureFighterTeamAnimation,
fighterSheetKey, fighterSheetKey,
} from "../fighter/fighterAssets.js"; } from "../fighter/fighterAssets.js";
import { import { FIGHTER_TYPES, getFighterType } from "../fighter/fighterStats.js";
FIGHTER_TYPES,
getFighterType,
} from "../fighter/fighterStats.js";
const SPECIAL_ANIMATION_SUFFIX = "anim"; const SPECIAL_ANIMATION_SUFFIX = "anim";
@ -103,7 +97,8 @@ function maybeRetrySpecialEffect(scene, matchId) {
return; return;
} }
const elapsed = scene.time.now - (scene.specialEffectStartedAt ?? scene.time.now); const elapsed =
scene.time.now - (scene.specialEffectStartedAt ?? scene.time.now);
if (elapsed >= SPECIAL_EFFECT.TRIGGER_DELAY_MAX_MS) { if (elapsed >= SPECIAL_EFFECT.TRIGGER_DELAY_MAX_MS) {
return; return;
@ -156,14 +151,19 @@ function beginSpecialCast(scene, caster, target, matchId) {
caster.setFlipX(direction.x < 0); caster.setFlipX(direction.x < 0);
playCasterAttack(scene, state, caster); playCasterAttack(scene, state, caster);
addStateTimer(scene, state, SPECIAL_EFFECT.CASTER.ATTACK_LAUNCH_DELAY_MS, () => { addStateTimer(
if (!isSpecialCastValid(scene, caster, matchId)) { scene,
cleanupSpecialCastState(scene, state, { restoreCaster: true }); state,
return; SPECIAL_EFFECT.CASTER.ATTACK_LAUNCH_DELAY_MS,
} () => {
if (!isSpecialCastValid(scene, caster, matchId)) {
cleanupSpecialCastState(scene, state, { restoreCaster: true });
return;
}
spawnSpecialProjectile(scene, state, caster, direction); spawnSpecialProjectile(scene, state, caster, direction);
}); },
);
}); });
} }
@ -198,7 +198,10 @@ function playCasterAttack(scene, state, caster) {
return; return;
} }
caster.off(Phaser.Animations.Events.ANIMATION_COMPLETE, clearSpecialCastLock); caster.off(
Phaser.Animations.Events.ANIMATION_COMPLETE,
clearSpecialCastLock,
);
state.attackCompleteHandler = null; state.attackCompleteHandler = null;
if (!caster.active || caster.isDead) { if (!caster.active || caster.isDead) {
@ -208,7 +211,8 @@ function playCasterAttack(scene, state, caster) {
caster.isSpecialCasting = false; caster.isSpecialCasting = false;
caster.isLocked = false; caster.isLocked = false;
caster.anims.timeScale = 1; caster.anims.timeScale = 1;
caster.nextAttackAt = scene.time.now + SPECIAL_EFFECT.CASTER.POST_CAST_COOLDOWN_MS; caster.nextAttackAt =
scene.time.now + SPECIAL_EFFECT.CASTER.POST_CAST_COOLDOWN_MS;
}; };
state.attackCompleteHandler = clearSpecialCastLock; state.attackCompleteHandler = clearSpecialCastLock;
@ -226,12 +230,19 @@ function spawnSpecialProjectile(scene, state, caster, direction) {
); );
const travelDistance = ARENA.TILE_SIZE * projectileConfig.travelTiles; const travelDistance = ARENA.TILE_SIZE * projectileConfig.travelTiles;
const end = resolveProjectileEndPoint(start, direction, travelDistance); const end = resolveProjectileEndPoint(start, direction, travelDistance);
const actualTravelDistance = Phaser.Math.Distance.Between(start.x, start.y, end.x, end.y); const actualTravelDistance = Phaser.Math.Distance.Between(
start.x,
start.y,
end.x,
end.y,
);
const duration = Math.max( const duration = Math.max(
1, 1,
Math.min( Math.min(
projectileConfig.maxLifetimeMs, projectileConfig.maxLifetimeMs,
Math.round((actualTravelDistance / Math.max(1, projectileConfig.speed)) * 1000), Math.round(
(actualTravelDistance / Math.max(1, projectileConfig.speed)) * 1000,
),
), ),
); );
const projectile = scene.physics.add const projectile = scene.physics.add
@ -344,7 +355,10 @@ function resolveSpecialProjectileVisual(caster) {
function grantSpecialCasterInvulnerability(caster) { function grantSpecialCasterInvulnerability(caster) {
const now = resolveRealtimeNow(); const now = resolveRealtimeNow();
const duration = Math.max(0, Number(SPECIAL_EFFECT.CASTER.INVULNERABLE_MS) || 0); const duration = Math.max(
0,
Number(SPECIAL_EFFECT.CASTER.INVULNERABLE_MS) || 0,
);
const invulnerableUntil = Math.max( const invulnerableUntil = Math.max(
Number(caster.specialInvulnerableUntil) || 0, Number(caster.specialInvulnerableUntil) || 0,
now + duration, now + duration,
@ -360,15 +374,18 @@ function grantSpecialCasterInvulnerability(caster) {
caster.isSpecialInvulnerable = true; caster.isSpecialInvulnerable = true;
caster.specialInvulnerableUntil = invulnerableUntil; caster.specialInvulnerableUntil = invulnerableUntil;
caster.specialInvulnerabilityTimer = globalThis.setTimeout(() => { caster.specialInvulnerabilityTimer = globalThis.setTimeout(
if (caster.specialInvulnerableUntil !== invulnerableUntil) { () => {
return; if (caster.specialInvulnerableUntil !== invulnerableUntil) {
} return;
}
caster.isSpecialInvulnerable = false; caster.isSpecialInvulnerable = false;
caster.specialInvulnerableUntil = 0; caster.specialInvulnerableUntil = 0;
caster.specialInvulnerabilityTimer = null; caster.specialInvulnerabilityTimer = null;
}, Math.max(0, invulnerableUntil - now)); },
Math.max(0, invulnerableUntil - now),
);
} }
function resolveRealtimeNow() { function resolveRealtimeNow() {
@ -388,10 +405,10 @@ function resolveProjectileHits(scene, projectile, state, attacker) {
[...scene.fighters].forEach((fighter) => { [...scene.fighters].forEach((fighter) => {
if ( if (
!isLivingFighter(fighter) !isLivingFighter(fighter) ||
|| fighter === attacker fighter === attacker ||
|| state.hitFighters.has(fighter) state.hitFighters.has(fighter) ||
|| !projectileSegmentHitsFighter(segmentStart, segmentEnd, fighter) !projectileSegmentHitsFighter(segmentStart, segmentEnd, fighter)
) { ) {
return; return;
} }
@ -420,12 +437,13 @@ function projectileReachedEnd(projectile, start, end, travelDistance) {
const pathY = end.y - start.y; const pathY = end.y - start.y;
const traveledAlongPath = traveledX * pathX + traveledY * pathY; const traveledAlongPath = traveledX * pathX + traveledY * pathY;
return traveledAlongPath >= (travelDistance * travelDistance); return traveledAlongPath >= travelDistance * travelDistance;
} }
function projectileSegmentHitsFighter(segmentStart, segmentEnd, fighter) { function projectileSegmentHitsFighter(segmentStart, segmentEnd, fighter) {
const center = fighter.body?.center ?? fighter; const center = fighter.body?.center ?? fighter;
const hitRadius = SPECIAL_EFFECT.PROJECTILE.hitRadius + fighterHitRadius(fighter); const hitRadius =
SPECIAL_EFFECT.PROJECTILE.hitRadius + fighterHitRadius(fighter);
const distanceSq = pointToSegmentDistanceSq(center, segmentStart, segmentEnd); const distanceSq = pointToSegmentDistanceSq(center, segmentStart, segmentEnd);
return distanceSq <= hitRadius * hitRadius; return distanceSq <= hitRadius * hitRadius;
@ -443,8 +461,9 @@ function pointToSegmentDistanceSq(point, segmentStart, segmentEnd) {
} }
const t = Phaser.Math.Clamp( const t = Phaser.Math.Clamp(
((point.x - segmentStart.x) * deltaX + (point.y - segmentStart.y) * deltaY) ((point.x - segmentStart.x) * deltaX +
/ lengthSq, (point.y - segmentStart.y) * deltaY) /
lengthSq,
0, 0,
1, 1,
); );
@ -457,14 +476,13 @@ function pointToSegmentDistanceSq(point, segmentStart, segmentEnd) {
} }
function fighterHitRadius(fighter) { function fighterHitRadius(fighter) {
const bodyRadius = Math.max( const bodyRadius =
fighter.body?.width ?? 0, Math.max(fighter.body?.width ?? 0, fighter.body?.height ?? 0) / 2;
fighter.body?.height ?? 0, const visualRadius =
) / 2; Math.max(
const visualRadius = Math.max( Math.abs(fighter.displayWidth ?? 0),
Math.abs(fighter.displayWidth ?? 0), Math.abs(fighter.displayHeight ?? 0),
Math.abs(fighter.displayHeight ?? 0), ) * 0.14;
) * 0.14;
return Math.max(8, bodyRadius, visualRadius); return Math.max(8, bodyRadius, visualRadius);
} }
@ -516,10 +534,10 @@ function cleanupSpecialCastState(
} }
if ( if (
restoreCaster restoreCaster &&
&& state.caster?.active state.caster?.active &&
&& !state.caster.isDead !state.caster.isDead &&
&& state.caster.isSpecialCasting state.caster.isSpecialCasting
) { ) {
state.caster.isSpecialCasting = false; state.caster.isSpecialCasting = false;
state.caster.isLocked = false; state.caster.isLocked = false;
@ -626,16 +644,14 @@ function pauseSpecialPreparationCombat(scene, state) {
object.anims.pause(); object.anims.pause();
} }
scene.tweens scene.tweens?.getTweensOf(object)?.forEach((tween) => {
?.getTweensOf(object) if (tween.paused) {
?.forEach((tween) => { return;
if (tween.paused) { }
return;
}
pausedTweens.add(tween); pausedTweens.add(tween);
tween.pause(); tween.pause();
}); });
combatObjectStates.set(object, objectState); combatObjectStates.set(object, objectState);
}); });
@ -762,9 +778,16 @@ function createBlurredBattlefieldLayer(scene, config) {
.setOrigin(0, 0) .setOrigin(0, 0)
.setDepth(config.BLUR_DEPTH) .setDepth(config.BLUR_DEPTH)
.setAlpha(0); .setAlpha(0);
const entries = scene.children.getChildren().filter((gameObject) => const entries = scene.children
shouldDrawInSpecialFocusSnapshot(scene, gameObject, renderTexture, config), .getChildren()
); .filter((gameObject) =>
shouldDrawInSpecialFocusSnapshot(
scene,
gameObject,
renderTexture,
config,
),
);
try { try {
renderTexture.draw(entries); renderTexture.draw(entries);
@ -785,14 +808,19 @@ function createBlurredBattlefieldLayer(scene, config) {
return renderTexture; return renderTexture;
} }
function shouldDrawInSpecialFocusSnapshot(scene, gameObject, renderTexture, config) { function shouldDrawInSpecialFocusSnapshot(
scene,
gameObject,
renderTexture,
config,
) {
return Boolean( return Boolean(
gameObject gameObject &&
&& gameObject !== renderTexture gameObject !== renderTexture &&
&& gameObject.active gameObject.active &&
&& gameObject.visible gameObject.visible &&
&& gameObject !== scene.minimapGraphics gameObject !== scene.minimapGraphics &&
&& gameObject.depth < config.BLUR_DEPTH gameObject.depth < config.BLUR_DEPTH,
); );
} }
@ -856,10 +884,11 @@ function selectSpecialCaster(scene) {
.filter((summary) => summary.count === highestCount) .filter((summary) => summary.count === highestCount)
.map((summary) => summary.teamId), .map((summary) => summary.teamId),
); );
const candidates = livingFighters.filter((fighter) => const candidates = livingFighters.filter(
!leadingTeamIds.has(fighter.team?.id) (fighter) =>
&& !fighter.isElite !leadingTeamIds.has(fighter.team?.id) &&
&& getFighterType(fighter.skin) !== FIGHTER_TYPES.MAGIC, // && !fighter.isElite
getFighterType(fighter.skin) !== FIGHTER_TYPES.MAGIC,
); );
if (candidates.length === 0) { if (candidates.length === 0) {
@ -906,9 +935,8 @@ function findDensestSpecialTarget(fighters) {
SPECIAL_EFFECT.PROJECTILE.targetAreaTiles, SPECIAL_EFFECT.PROJECTILE.targetAreaTiles,
ARENA.GRID_SIZE, ARENA.GRID_SIZE,
); );
const tileCounts = Array.from( const tileCounts = Array.from({ length: ARENA.GRID_SIZE }, () =>
{ length: ARENA.GRID_SIZE }, Array(ARENA.GRID_SIZE).fill(0),
() => Array(ARENA.GRID_SIZE).fill(0),
); );
fighters.forEach((fighter) => { fighters.forEach((fighter) => {
@ -974,7 +1002,10 @@ function livingTeamSummaries(livingFighters) {
function resolveLaunchDirection(caster, target) { function resolveLaunchDirection(caster, target) {
if (target) { if (target) {
const direction = new Phaser.Math.Vector2(target.x - caster.x, target.y - caster.y); const direction = new Phaser.Math.Vector2(
target.x - caster.x,
target.y - caster.y,
);
if (direction.lengthSq() > 0) { if (direction.lengthSq() > 0) {
return direction.normalize(); return direction.normalize();
@ -992,7 +1023,10 @@ function pointInDirection(origin, direction, distance) {
} }
function resolveProjectileEndPoint(start, direction, travelDistance) { function resolveProjectileEndPoint(start, direction, travelDistance) {
const padding = Math.max(0, Number(SPECIAL_EFFECT.PROJECTILE.arenaEdgePadding) || 0); const padding = Math.max(
0,
Number(SPECIAL_EFFECT.PROJECTILE.arenaEdgePadding) || 0,
);
const maximumDistance = Math.max(0, Number(travelDistance) || 0); const maximumDistance = Math.max(0, Number(travelDistance) || 0);
const edgeDistance = distanceToArenaEdge(start, direction, padding); const edgeDistance = distanceToArenaEdge(start, direction, padding);
const distance = Math.max(0, Math.min(maximumDistance, edgeDistance)); const distance = Math.max(0, Math.min(maximumDistance, edgeDistance));
@ -1041,18 +1075,17 @@ function clampPointInsideArena(point, padding) {
} }
function createSummedAreaTable(tileCounts) { function createSummedAreaTable(tileCounts) {
const sums = Array.from( const sums = Array.from({ length: ARENA.GRID_SIZE + 1 }, () =>
{ length: ARENA.GRID_SIZE + 1 }, Array(ARENA.GRID_SIZE + 1).fill(0),
() => Array(ARENA.GRID_SIZE + 1).fill(0),
); );
for (let row = 0; row < ARENA.GRID_SIZE; row += 1) { for (let row = 0; row < ARENA.GRID_SIZE; row += 1) {
for (let column = 0; column < ARENA.GRID_SIZE; column += 1) { for (let column = 0; column < ARENA.GRID_SIZE; column += 1) {
sums[row + 1][column + 1] = sums[row + 1][column + 1] =
tileCounts[row][column] tileCounts[row][column] +
+ sums[row][column + 1] sums[row][column + 1] +
+ sums[row + 1][column] sums[row + 1][column] -
- sums[row][column]; sums[row][column];
} }
} }
@ -1064,10 +1097,10 @@ function sumArea(sums, column, row, areaTiles) {
const right = column + areaTiles; const right = column + areaTiles;
return ( return (
sums[bottom][right] sums[bottom][right] -
- sums[row][right] sums[row][right] -
- sums[bottom][column] sums[bottom][column] +
+ sums[row][column] sums[row][column]
); );
} }
@ -1078,7 +1111,11 @@ function resolveTileCount(value, maximum) {
function resolveFighterSheetKey(scene, fighter, action) { function resolveFighterSheetKey(scene, fighter, action) {
ensureFighterTeamAnimation(scene, fighter.skin, action, fighter.team?.color); ensureFighterTeamAnimation(scene, fighter.skin, action, fighter.team?.color);
const teamSheetKey = fighterSheetKey(fighter.skin, action, fighter.team?.color); const teamSheetKey = fighterSheetKey(
fighter.skin,
action,
fighter.team?.color,
);
if (scene.textures.exists(teamSheetKey)) { if (scene.textures.exists(teamSheetKey)) {
return teamSheetKey; return teamSheetKey;
@ -1089,12 +1126,20 @@ function resolveFighterSheetKey(scene, fighter, action) {
function playFighterAnimation(scene, fighter, action) { function playFighterAnimation(scene, fighter, action) {
fighter.play( fighter.play(
ensureFighterTeamAnimation(scene, fighter.skin, action, fighter.team?.color), ensureFighterTeamAnimation(
scene,
fighter.skin,
action,
fighter.team?.color,
),
true, true,
); );
} }
function createSpecialAnimation(scene, { frameRate, frameSequence, frames, key, repeat }) { function createSpecialAnimation(
scene,
{ frameRate, frameSequence, frames, key, repeat },
) {
const animationKey = specialAnimationKey(key); const animationKey = specialAnimationKey(key);
if (scene.anims.exists(animationKey)) { if (scene.anims.exists(animationKey)) {
@ -1103,7 +1148,11 @@ function createSpecialAnimation(scene, { frameRate, frameSequence, frames, key,
scene.anims.create({ scene.anims.create({
key: animationKey, key: animationKey,
frames: resolveSpecialAnimationFrames(scene, { frameSequence, frames, key }), frames: resolveSpecialAnimationFrames(scene, {
frameSequence,
frames,
key,
}),
frameRate, frameRate,
repeat, repeat,
}); });
@ -1139,18 +1188,11 @@ function resolveInitialDelayMs() {
} }
function isSpecialCastValid(scene, caster, matchId) { function isSpecialCastValid(scene, caster, matchId) {
return ( return isLiveMatch(scene, matchId) && caster?.active && !caster.isDead;
isLiveMatch(scene, matchId)
&& caster?.active
&& !caster.isDead
);
} }
function isLivingEnemy(fighter, candidate) { function isLivingEnemy(fighter, candidate) {
return ( return isLivingFighter(candidate) && candidate.team?.id !== fighter.team?.id;
isLivingFighter(candidate)
&& candidate.team?.id !== fighter.team?.id
);
} }
function isLivingFighter(fighter) { function isLivingFighter(fighter) {
@ -1166,5 +1208,7 @@ function randomEntry(entries) {
} }
function isLiveMatch(scene, matchId = scene.matchId) { function isLiveMatch(scene, matchId = scene.matchId) {
return !scene.matchOver && !scene.presentationMode && scene.matchId === matchId; return (
!scene.matchOver && !scene.presentationMode && scene.matchId === matchId
);
} }