354 lines
23 KiB
JavaScript
354 lines
23 KiB
JavaScript
export const COURT = { width: 15, length: 14, boundaryX: 7.5, minZ: 0, maxZ: 14, rim: { x: 0, z: 13.25 } };
|
|
export const GRID_SIZE = 0.5;
|
|
export const PLAYER_SPEED = 4.5;
|
|
export const DEFENSE_REACTION_DELAY = 0.20;
|
|
export const PASS_DURATION = 0.35;
|
|
export const SCREEN_HOLD = 0.60;
|
|
export const SHOOT_DURATION = 0.80;
|
|
export const RIM_HEIGHT = 3.05;
|
|
export const BALL_HOLD_HEIGHT = 1.15;
|
|
export const DEFENSE_TYPES = ['man-to-man', '2-3', '3-2'];
|
|
|
|
const OFFENSE_START = [
|
|
{ x: 0, z: 2.1 }, { x: -5.8, z: 1.2 }, { x: 5.8, z: 1.2 },
|
|
{ x: -2.1, z: 7.2 }, { x: 2.1, z: 7.2 },
|
|
];
|
|
const DEFENSE_START = {
|
|
'man-to-man': [{ x: 0, z: 5.2 }, { x: -4.2, z: 3.9 }, { x: 4.2, z: 3.9 }, { x: -1.8, z: 9 }, { x: 1.8, z: 9 }],
|
|
'2-3': [{ x: -3.2, z: 5 }, { x: 3.2, z: 5 }, { x: -4.8, z: 9 }, { x: 0, z: 10.1 }, { x: 4.8, z: 9 }],
|
|
'3-2': [{ x: -4.7, z: 6.3 }, { x: 0, z: 6.5 }, { x: 4.7, z: 6.3 }, { x: -2.7, z: 10.3 }, { x: 2.7, z: 10.3 }],
|
|
};
|
|
|
|
export const copyLocation = (location) => ({ x: Number(location?.x) || 0, z: Number(location?.z) || 0 });
|
|
export const clampLocation = (location) => ({
|
|
x: Math.max(-COURT.boundaryX, Math.min(COURT.boundaryX, Number(location?.x) || 0)),
|
|
z: Math.max(COURT.minZ, Math.min(COURT.maxZ, Number(location?.z) || 0)),
|
|
});
|
|
export const snapLocation = (location, grid = GRID_SIZE) => clampLocation({ x: Math.round((Number(location?.x) || 0) / grid) * grid, z: Math.round((Number(location?.z) || 0) / grid) * grid });
|
|
export const distance = (a, b) => Math.hypot((b?.x || 0) - (a?.x || 0), (b?.z || 0) - (a?.z || 0));
|
|
export const locationEqual = (a, b) => Boolean(a && b && a.x === b.x && a.z === b.z);
|
|
|
|
export function makePlayer(id, team, number, location) {
|
|
return { id, team, number, location: clampLocation(location) };
|
|
}
|
|
|
|
export function createInitialPlay(name = '새 전술', defenseType = 'man-to-man') {
|
|
const defense = DEFENSE_TYPES.includes(defenseType) ? defenseType : 'man-to-man';
|
|
const players = [
|
|
...OFFENSE_START.map((p, i) => makePlayer(`offense-${i + 1}`, 'offense', i + 1, p)),
|
|
...DEFENSE_START[defense].map((p, i) => makePlayer(`defense-${i + 1}`, 'defense', i + 1, p)),
|
|
];
|
|
return {
|
|
id: `play-${Date.now().toString(36)}`,
|
|
name: String(name || '새 전술').trim() || '새 전술',
|
|
defenseType: defense,
|
|
players,
|
|
sequences: [createSequence('Sequence 1', players)],
|
|
};
|
|
}
|
|
|
|
export function createSequence(name, players, previous = null) {
|
|
const tracks = players.map((player) => {
|
|
const previousTrack = previous?.tracks?.find((track) => track.playerId === player.id);
|
|
return { playerId: player.id, startLocation: copyLocation(previousTrack?.actions?.at(-1)?.location || previousTrack?.startLocation || player.location), actions: [] };
|
|
});
|
|
return { id: `sequence-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 7)}`, name, ballOwnerId: previous ? finalBallOwner(previous) : players.find((player) => player.team === 'offense')?.id || null, ballOwnerInherited: Boolean(previous), tracks };
|
|
}
|
|
|
|
export function finalTrackLocation(track) { return copyLocation(track?.actions?.at(-1)?.location || track?.startLocation); }
|
|
export function finalBallOwner(sequence) { return sequence?.tracks?.find((track) => track.actions.some((action) => action.type === 'pass'))?.actions.findLast((action) => action.type === 'pass')?.targetPlayerId || sequence?.ballOwnerId || null; }
|
|
export function hasShoot(play) { return Boolean(play?.sequences?.some((sequence) => sequence.tracks?.some((track) => track.actions?.some((action) => action.type === 'shoot')))); }
|
|
function shootActions(play) { return (play?.sequences || []).flatMap((sequence, sequenceIndex) => (sequence.tracks || []).flatMap((track) => (track.actions || []).filter((action) => action.type === 'shoot').map((action) => ({ action, sequence, sequenceIndex, track })))); }
|
|
export function shootInvariantsValid(play) {
|
|
const shots = shootActions(play); if (!shots.length) return true; if (shots.length !== 1) return false;
|
|
const { action, sequence, sequenceIndex, track } = shots[0]; const ownerId = finalBallOwner(sequence); const ownerTrack = sequence.tracks.find((candidate) => candidate.playerId === ownerId);
|
|
const previousLocation = track.actions.length > 1 ? track.actions.at(-2).location : track.startLocation;
|
|
return sequenceIndex === play.sequences.length - 1 && track.playerId === ownerId && track.actions.at(-1) === action && play.players.find((player) => player.id === track.playerId)?.team === 'offense' && action.targetPlayerId === null && action.facing === null && action.lookAt?.type === 'rim' && ownerTrack === track && locationEqual(action.location, previousLocation);
|
|
}
|
|
export function isDefenseTrack(track, players = []) { return players.find((player) => player.id === track?.playerId)?.team === 'defense' || track?.playerId?.startsWith('defense-'); }
|
|
|
|
export function hasScreen(sequence, playerId) { return Boolean(sequence?.tracks?.find((track) => track.playerId === playerId)?.actions.some((action) => action.type === 'screen')); }
|
|
function relinkFollowingOwners(play, firstSequenceIndex) {
|
|
const first = play.sequences[firstSequenceIndex]; let owner = finalBallOwner(first); if (hasScreen(first, owner)) return false;
|
|
const inherited = [];
|
|
for (let index = firstSequenceIndex + 1; index < play.sequences.length; index += 1) {
|
|
const sequence = play.sequences[index];
|
|
if (sequence.ballOwnerInherited !== false) {
|
|
if (hasScreen(sequence, owner)) return false;
|
|
inherited.push({ sequence, owner });
|
|
owner = finalBallOwner({ ...sequence, ballOwnerId: owner });
|
|
} else owner = finalBallOwner(sequence);
|
|
if (hasScreen(sequence, owner)) return false;
|
|
}
|
|
for (const assignment of inherited) { assignment.sequence.ballOwnerId = assignment.owner; assignment.sequence.ballOwnerInherited = true; }
|
|
return true;
|
|
}
|
|
|
|
function relinkFollowingStarts(play, playerId, firstSequenceIndex) {
|
|
for (let index = Math.max(1, firstSequenceIndex); index < play.sequences.length; index += 1) {
|
|
const previousTrack = play.sequences[index - 1].tracks.find((candidate) => candidate.playerId === playerId);
|
|
const track = play.sequences[index].tracks.find((candidate) => candidate.playerId === playerId);
|
|
if (track) track.startLocation = copyLocation(previousTrack?.actions.at(-1)?.location || previousTrack?.startLocation || play.players.find((player) => player.id === playerId)?.location);
|
|
}
|
|
}
|
|
|
|
export function trackActionSnapshot(play, sequenceId, playerId) {
|
|
const sequence = play?.sequences?.find((candidate) => candidate.id === sequenceId); const track = sequence?.tracks?.find((candidate) => candidate.playerId === playerId);
|
|
return track ? { sequenceId, playerId, actions: structuredClone(track.actions) } : null;
|
|
}
|
|
|
|
function sequenceActionsValid(play, sequence) {
|
|
if (!sequence) return false;
|
|
let passCount = 0;
|
|
for (const track of sequence.tracks || []) {
|
|
const player = play.players.find((candidate) => candidate.id === track.playerId);
|
|
for (const action of track.actions || []) {
|
|
if (action.type === 'pass') {
|
|
passCount += 1; const target = play.players.find((candidate) => candidate.id === action.targetPlayerId); const targetTrack = sequence.tracks.find((candidate) => candidate.playerId === action.targetPlayerId);
|
|
if (player?.team !== 'offense' || sequence.ballOwnerId !== track.playerId || !target || target.team !== 'offense' || target.id === track.playerId || targetTrack?.actions.some((candidate) => candidate.type === 'screen')) return false;
|
|
}
|
|
if (action.type === 'screen') {
|
|
const target = play.players.find((candidate) => candidate.id === action.targetPlayerId); if (player?.team !== 'offense' || !target || target.team !== 'defense' || !sequence.tracks.some((candidate) => candidate.playerId === target.id)) return false;
|
|
}
|
|
if (action.type === 'shoot' && (player?.team !== 'offense' || action.targetPlayerId !== null || action.facing !== null || action.lookAt?.type !== 'rim')) return false;
|
|
}
|
|
}
|
|
return passCount <= 1 && !hasScreen(sequence, finalBallOwner(sequence)) && shootInvariantsValid(play);
|
|
}
|
|
|
|
export function restoreTrackActionSnapshot(play, snapshot) {
|
|
if (hasShoot(play)) {
|
|
const currentShoot = shootActions(play)[0];
|
|
if (!currentShoot || snapshot?.sequenceId !== currentShoot.sequence.id || snapshot?.playerId !== currentShoot.track.playerId || snapshot.actions?.some((action) => action.type === 'shoot')) return play;
|
|
}
|
|
const next = structuredClone(play); const sequenceIndex = next.sequences.findIndex((sequence) => sequence.id === snapshot?.sequenceId); const sequence = next.sequences[sequenceIndex]; const track = sequence?.tracks?.find((candidate) => candidate.playerId === snapshot?.playerId);
|
|
if (!sequence || !track || !snapshot) return play;
|
|
track.actions = structuredClone(snapshot.actions || []);
|
|
if (!sequenceActionsValid(next, sequence)) return play;
|
|
relinkFollowingStarts(next, snapshot.playerId, sequenceIndex + 1);
|
|
if (!relinkFollowingOwners(next, Math.max(0, sequenceIndex)) || !shootInvariantsValid(next)) return play;
|
|
return next;
|
|
}
|
|
|
|
export function setPlayerStartLocation(play, playerId, location) {
|
|
if (hasShoot(play)) return play;
|
|
const next = structuredClone(play);
|
|
const player = next.players.find((candidate) => candidate.id === playerId);
|
|
if (!player) return next;
|
|
player.location = snapLocation(location);
|
|
const firstTrack = next.sequences[0]?.tracks.find((candidate) => candidate.playerId === playerId);
|
|
if (firstTrack) firstTrack.startLocation = copyLocation(player.location);
|
|
relinkFollowingStarts(next, playerId, 1);
|
|
return next;
|
|
}
|
|
|
|
export function setSequenceBallOwner(play, sequenceIndex, playerId) {
|
|
const player = play.players.find((candidate) => candidate.id === playerId && candidate.team === 'offense');
|
|
const sequence = play.sequences[sequenceIndex];
|
|
const playerTrack = sequence?.tracks.find((track) => track.playerId === playerId);
|
|
if (!player || !sequence || hasShoot(play) || playerTrack?.actions.some((action) => action.type === 'screen') || (sequence.tracks.some((track) => track.actions.some((action) => action.type === 'pass')) && playerId !== sequence.ballOwnerId)) return play;
|
|
const next = structuredClone(play);
|
|
next.sequences[sequenceIndex].ballOwnerId = playerId;
|
|
next.sequences[sequenceIndex].ballOwnerInherited = false;
|
|
if (!relinkFollowingOwners(next, sequenceIndex)) return play;
|
|
return next;
|
|
}
|
|
|
|
export function addAction(play, sequenceIndex, playerId, location, options = {}) {
|
|
if (options.type === 'pass') return addPassAction(play, sequenceIndex, playerId, options.targetPlayerId);
|
|
if (options.type === 'screen') return addScreenAction(play, sequenceIndex, playerId, options.targetPlayerId);
|
|
if (options.type === 'shoot') return addShootAction(play, sequenceIndex, playerId);
|
|
if (hasShoot(play)) return play;
|
|
const sequence = play.sequences[sequenceIndex];
|
|
const player = play.players.find((candidate) => candidate.id === playerId);
|
|
const track = sequence?.tracks.find((candidate) => candidate.playerId === playerId);
|
|
if (!track) return play;
|
|
const next = structuredClone(play);
|
|
const nextSequence = next.sequences[sequenceIndex];
|
|
const nextTrack = nextSequence.tracks.find((candidate) => candidate.playerId === playerId);
|
|
const safeLocation = snapLocation(location);
|
|
const safeLookAt = normalizeLookAt(options.lookAt, playerId, next.players);
|
|
nextTrack.actions.push({
|
|
id: `action-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 7)}`,
|
|
location: safeLocation,
|
|
facing: options.facing ?? null,
|
|
lookAt: safeLookAt,
|
|
type: options.type || 'move',
|
|
targetPlayerId: options.targetPlayerId ?? null,
|
|
});
|
|
relinkFollowingStarts(next, playerId, sequenceIndex + 1);
|
|
return next;
|
|
}
|
|
|
|
export function addPassAction(play, sequenceIndex, passerId, targetPlayerId) {
|
|
if (hasShoot(play)) return play;
|
|
const next = structuredClone(play);
|
|
const sequence = next.sequences[sequenceIndex];
|
|
const passer = next.players.find((player) => player.id === passerId);
|
|
const target = next.players.find((player) => player.id === targetPlayerId);
|
|
const targetTrack = sequence?.tracks.find((track) => track.playerId === targetPlayerId);
|
|
if (!sequence || !passer || passer.team !== 'offense' || sequence.ballOwnerId !== passerId || !target || target.team !== 'offense' || target.id === passerId || targetTrack?.actions.some((action) => action.type === 'screen') || sequence.tracks.some((track) => track.actions.some((action) => action.type === 'pass'))) return play;
|
|
const track = sequence.tracks.find((candidate) => candidate.playerId === passerId);
|
|
track.actions.push({ id: `action-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 7)}`, location: finalTrackLocation(track), facing: null, lookAt: null, type: 'pass', targetPlayerId });
|
|
if (!relinkFollowingOwners(next, sequenceIndex)) return play;
|
|
return next;
|
|
}
|
|
|
|
export function addScreenAction(play, sequenceIndex, screenerId, defenderId) {
|
|
if (hasShoot(play)) return play;
|
|
const sequence = play.sequences[sequenceIndex]; const screener = play.players.find((player) => player.id === screenerId); const defender = play.players.find((player) => player.id === defenderId);
|
|
const screenerTrack = sequence?.tracks.find((track) => track.playerId === screenerId); const defenderTrack = sequence?.tracks.find((track) => track.playerId === defenderId); const ownerId = sequence ? finalBallOwner(sequence) : null;
|
|
if (!sequence || !screener || screener.team !== 'offense' || screenerId === ownerId || !defender || defender.team !== 'defense' || !screenerTrack || !defenderTrack) return play;
|
|
const screenerFinal = finalTrackLocation(screenerTrack); const defenderFinal = finalTrackLocation(defenderTrack); const length = distance(defenderFinal, screenerFinal);
|
|
if (length < 0.76) return play;
|
|
const location = length <= 0.85 ? screenerFinal : { x: defenderFinal.x + (screenerFinal.x - defenderFinal.x) * 0.85 / length, z: defenderFinal.z + (screenerFinal.z - defenderFinal.z) * 0.85 / length };
|
|
const next = structuredClone(play); const track = next.sequences[sequenceIndex].tracks.find((candidate) => candidate.playerId === screenerId); const nextDefenderFinal = finalTrackLocation(next.sequences[sequenceIndex].tracks.find((candidate) => candidate.playerId === defenderId));
|
|
track.actions.push({ id: `action-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 7)}`, location: { ...location }, facing: Math.atan2(nextDefenderFinal.x - location.x, nextDefenderFinal.z - location.z), lookAt: { type: 'player', targetId: defenderId }, type: 'screen', targetPlayerId: defenderId });
|
|
relinkFollowingStarts(next, screenerId, sequenceIndex + 1); return next;
|
|
}
|
|
|
|
export function addShootAction(play, sequenceIndex, shooterId) {
|
|
if (hasShoot(play) || sequenceIndex !== play.sequences.length - 1) return play;
|
|
const sequence = play.sequences[sequenceIndex]; const ownerId = finalBallOwner(sequence); if (shooterId !== ownerId) return play;
|
|
const track = sequence?.tracks.find((candidate) => candidate.playerId === shooterId); const shooter = play.players.find((player) => player.id === shooterId); if (!track || shooter?.team !== 'offense' || track.actions.at(-1)?.type === 'shoot') return play;
|
|
const next = structuredClone(play); const nextTrack = next.sequences[sequenceIndex].tracks.find((candidate) => candidate.playerId === shooterId); nextTrack.actions.push({ id: `action-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 7)}`, location: finalTrackLocation(nextTrack), facing: null, lookAt: { type: 'rim' }, type: 'shoot', targetPlayerId: null });
|
|
return shootInvariantsValid(next) ? next : play;
|
|
}
|
|
|
|
export function removeAction(play, sequenceIndex, playerId, actionIndex) {
|
|
const originalAction = play.sequences[sequenceIndex]?.tracks.find((candidate) => candidate.playerId === playerId)?.actions[actionIndex];
|
|
if (hasShoot(play) && originalAction?.type !== 'shoot') return play;
|
|
const next = structuredClone(play);
|
|
const track = next.sequences[sequenceIndex]?.tracks.find((candidate) => candidate.playerId === playerId);
|
|
if (track && actionIndex >= 0 && actionIndex < track.actions.length) {
|
|
track.actions.splice(actionIndex, 1);
|
|
relinkFollowingStarts(next, playerId, sequenceIndex + 1);
|
|
if (!relinkFollowingOwners(next, sequenceIndex) || !shootInvariantsValid(next)) return play;
|
|
}
|
|
return next;
|
|
}
|
|
|
|
export function addSequence(play) {
|
|
if (hasShoot(play)) return play;
|
|
const next = structuredClone(play);
|
|
const previous = next.sequences.at(-1);
|
|
next.sequences.push(createSequence(`Sequence ${next.sequences.length + 1}`, next.players, previous));
|
|
return next;
|
|
}
|
|
|
|
export function removeSequence(play, sequenceIndex) {
|
|
if (hasShoot(play)) return play;
|
|
const next = structuredClone(play);
|
|
if (next.sequences.length <= 1 || sequenceIndex < 0 || sequenceIndex >= next.sequences.length) return next;
|
|
next.sequences.splice(sequenceIndex, 1);
|
|
for (const player of next.players) relinkFollowingStarts(next, player.id, sequenceIndex);
|
|
if (next.sequences[sequenceIndex] && !relinkFollowingOwners(next, Math.max(0, sequenceIndex - 1))) return play;
|
|
return next;
|
|
}
|
|
|
|
export function trackPoints(track) { return [track.startLocation, ...track.actions.map((action) => action.location)]; }
|
|
|
|
export function actionExtraDuration(action) { return action?.type === 'pass' ? PASS_DURATION : action?.type === 'screen' ? SCREEN_HOLD : 0; }
|
|
|
|
export function trackDuration(track, speed = PLAYER_SPEED, players = []) {
|
|
if (!track || !speed || speed < 0) return 0;
|
|
return trackNaturalDuration(track, speed) + (isDefenseTrack(track, players) && track.actions.length ? DEFENSE_REACTION_DELAY : 0);
|
|
}
|
|
|
|
export function trackNaturalDuration(track, speed = PLAYER_SPEED) {
|
|
if (!track || !speed || speed < 0) return 0;
|
|
let current = track.startLocation; let total = 0;
|
|
for (const action of track.actions || []) { if (action.type === 'shoot') continue; total += distance(current, action.location) / speed + actionExtraDuration(action); current = action.location; }
|
|
return total;
|
|
}
|
|
|
|
export function offensePhaseDuration(sequence, speed = PLAYER_SPEED, players = []) {
|
|
return Math.max(0, ...(sequence?.tracks || []).filter((track) => !isDefenseTrack(track, players)).map((track) => trackNaturalDuration(track, speed)));
|
|
}
|
|
|
|
export function scheduledTrackDuration(sequence, track, speed = PLAYER_SPEED, players = []) {
|
|
const natural = trackNaturalDuration(track, speed);
|
|
if (!isDefenseTrack(track, players)) return natural;
|
|
if (!track?.actions?.length) return 0;
|
|
if (natural <= 0) return DEFENSE_REACTION_DELAY;
|
|
return DEFENSE_REACTION_DELAY + Math.max(natural, offensePhaseDuration(sequence, speed, players) - DEFENSE_REACTION_DELAY);
|
|
}
|
|
|
|
export function sequenceDuration(sequence, speed = PLAYER_SPEED, players = []) {
|
|
return sequenceNormalDuration(sequence, speed, players) + (sequence?.tracks?.some((track) => track.actions.some((action) => action.type === 'shoot')) ? SHOOT_DURATION : 0);
|
|
}
|
|
export function sequenceNormalDuration(sequence, speed = PLAYER_SPEED, players = []) { return Math.max(0, ...(sequence?.tracks || []).map((track) => scheduledTrackDuration(sequence, track, speed, players))); }
|
|
|
|
export function allSequenceDurations(play, speed = PLAYER_SPEED) {
|
|
return (play.sequences || []).map((sequence) => sequenceDuration(sequence, speed, play.players));
|
|
}
|
|
|
|
const directionFacing = (from, to) => Math.atan2((to?.x || 0) - (from?.x || 0), (to?.z || 0) - (from?.z || 0));
|
|
|
|
export function resolveFacing(track, actionIndex, speed = PLAYER_SPEED) {
|
|
if (!track) return 0;
|
|
const action = track.actions[actionIndex];
|
|
if (action?.facing !== null && action?.facing !== undefined && Number.isFinite(action.facing)) return action.facing;
|
|
const points = trackPoints(track);
|
|
const from = points[actionIndex];
|
|
const to = points[actionIndex + 1];
|
|
if (from && to && distance(from, to) > 1e-6) return directionFacing(from, to);
|
|
for (let i = actionIndex - 1; i >= 0; i -= 1) {
|
|
if (track.actions[i]?.facing !== null && Number.isFinite(track.actions[i].facing)) return track.actions[i].facing;
|
|
if (distance(points[i], points[i + 1]) > 1e-6) return directionFacing(points[i], points[i + 1]);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
export function normalizeLookAt(lookAt, playerId, players = []) {
|
|
if (!lookAt || typeof lookAt !== 'object') return null;
|
|
if (lookAt.type === 'rim' || lookAt.type === 'ball') return { type: lookAt.type };
|
|
if (lookAt.type === 'movement') return { type: 'movement' };
|
|
if (lookAt.type === 'location' && Number.isFinite(Number(lookAt.x)) && Number.isFinite(Number(lookAt.z))) {
|
|
return { type: 'location', ...clampLocation(lookAt) };
|
|
}
|
|
if (lookAt.type === 'player' && lookAt.targetId && lookAt.targetId !== playerId && players.some((player) => player.id === lookAt.targetId)) {
|
|
return { type: 'player', targetId: lookAt.targetId };
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export function resolveLookAt(play, sequence, playerId, actionIndex, currentLocation = null) {
|
|
const player = play.players.find((candidate) => candidate.id === playerId);
|
|
const track = sequence?.tracks.find((candidate) => candidate.playerId === playerId);
|
|
const action = track?.actions[actionIndex];
|
|
const override = normalizeLookAt(action?.lookAt, playerId, play.players);
|
|
if (override?.type === 'movement') {
|
|
const facing = resolveFacing(track, actionIndex);
|
|
const origin = currentLocation || action.location;
|
|
return { type: 'location', x: origin.x + Math.sin(facing) * 4, z: origin.z + Math.cos(facing) * 4 };
|
|
}
|
|
if (override) return override;
|
|
if (action?.type === 'pass' || action?.type === 'screen') return { type: 'player', targetId: action.targetPlayerId };
|
|
if (action?.type === 'shoot') return { type: 'rim' };
|
|
const ballOwnerId = sequence?.ballOwnerId || play.players.find((candidate) => candidate.team === 'offense')?.id;
|
|
if (player?.team === 'offense') return playerId === ballOwnerId ? { type: 'rim' } : { type: 'ball' };
|
|
if (play.defenseType === 'man-to-man') {
|
|
const match = play.players.find((candidate) => candidate.team === 'offense' && candidate.number === player?.number);
|
|
if (match) return { type: 'player', targetId: match.id };
|
|
}
|
|
return { type: 'ball' };
|
|
}
|
|
|
|
export function lookAtLocation(lookAt, play, locations = {}, sequence = null) {
|
|
if (!lookAt) return null;
|
|
if (lookAt.type === 'rim') return copyLocation(COURT.rim);
|
|
if (lookAt.type === 'location') return copyLocation(lookAt);
|
|
if (lookAt.type === 'ball') { if (sequence?.ballLocation) return copyLocation(sequence.ballLocation); const ownerId = sequence?.ballOwnerId || play.sequences?.[0]?.ballOwnerId || play.players.find((player) => player.team === 'offense')?.id; return copyLocation(locations[ownerId] || play.players.find((player) => player.id === ownerId)?.location || COURT.rim); }
|
|
if (lookAt.type === 'player') return copyLocation(locations[lookAt.targetId] || play.players.find((p) => p.id === lookAt.targetId)?.location || COURT.rim);
|
|
return null;
|
|
}
|
|
|
|
export function shortestAngleLerp(from, to, amount) {
|
|
const twoPi = Math.PI * 2;
|
|
let delta = ((to - from + Math.PI) % twoPi + twoPi) % twoPi - Math.PI;
|
|
return from + delta * Math.max(0, Math.min(1, amount));
|
|
}
|
|
|
|
export { OFFENSE_START, DEFENSE_START };
|