Initial commit: courtlab tactical board with team management and MP4 export
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
import { addAction, addSequence, createInitialPlay, removeAction, removeSequence, restoreTrackActionSnapshot, setPlayerStartLocation, trackActionSnapshot } from './domain.js';
|
||||
import { assertValidPlay } from './playRepository.js';
|
||||
|
||||
export function createAppState(name = '새 전술', defenseType = 'man-to-man') {
|
||||
const play = createInitialPlay(name, defenseType);
|
||||
return { play, selectedPlayerId: 'offense-1', selectedSequence: 0, selectedAction: -1, mode: 'start', view: 'tactical', playing: false, history: createSequenceHistory(play) };
|
||||
}
|
||||
export function createAppStateFromPlay(sourcePlay) {
|
||||
assertValidPlay(sourcePlay); const play = cloneValue(sourcePlay); const selectedPlayerId = play.players.some((player) => player.id === 'offense-1') ? 'offense-1' : play.players[0]?.id || null;
|
||||
return { play, selectedPlayerId, selectedSequence: 0, selectedAction: -1, mode: 'move', view: 'tactical', playing: false, history: createSequenceHistory(play) };
|
||||
}
|
||||
export function serializePlay(play) { return JSON.stringify(play); }
|
||||
function cloneValue(value) { return JSON.parse(JSON.stringify(value)); }
|
||||
function samePlay(a, b) { return serializePlay(a) === serializePlay(b); }
|
||||
|
||||
export function createSequenceHistory(play, limit = 50) {
|
||||
return { limit, bySequenceId: Object.fromEntries((play?.sequences || []).map((sequence) => [sequence.id, Object.fromEntries((play.players || []).map((player) => [player.id, { past: [], future: [] }]))])) };
|
||||
}
|
||||
function normalizeActionHistory(history, play) {
|
||||
const limit = history?.limit || 50; const bySequenceId = {};
|
||||
for (const sequence of play?.sequences || []) {
|
||||
const oldTracks = history?.bySequenceId?.[sequence.id] || {}; bySequenceId[sequence.id] = Object.fromEntries((play.players || []).map((player) => { const entry = oldTracks[player.id]; return [player.id, { past: cloneValue(entry?.past || []).slice(-limit), future: cloneValue(entry?.future || []) }]; }));
|
||||
}
|
||||
return { limit, bySequenceId };
|
||||
}
|
||||
export function recordActionHistory(history, play, sequenceId, playerId) {
|
||||
const snapshot = trackActionSnapshot(play, sequenceId, playerId); if (!snapshot) return normalizeActionHistory(history, play);
|
||||
const next = normalizeActionHistory(history, play); const entry = next.bySequenceId[sequenceId][playerId]; entry.past = [...entry.past, snapshot].slice(-next.limit); entry.future = []; return next;
|
||||
}
|
||||
export function commitActionEdit(state, nextPlay, sequenceIndex = state.selectedSequence, playerId = state.selectedPlayerId) {
|
||||
const sequence = state.play.sequences[sequenceIndex]; const before = sequence?.tracks.find((track) => track.playerId === playerId); const after = nextPlay?.sequences?.find((candidate) => candidate.id === sequence?.id)?.tracks.find((track) => track.playerId === playerId);
|
||||
if (!before || !after) return state;
|
||||
if (serializePlay(before.actions) === serializePlay(after.actions)) return samePlay(state.play, nextPlay) ? state : { ...state, play: nextPlay };
|
||||
return { ...state, play: nextPlay, history: recordActionHistory(state.history, state.play, sequence.id, playerId) };
|
||||
}
|
||||
function restoreActionHistoryState(state, result) { return { ...state, play: result.play, history: result.history, selectedAction: -1 }; }
|
||||
function actionHistoryResult(history, currentPlay, sequenceId, playerId, direction) {
|
||||
const normalized = normalizeActionHistory(history, currentPlay); const entry = normalized.bySequenceId?.[sequenceId]?.[playerId]; if (!entry) return { play: currentPlay, history };
|
||||
if (direction === 'undo') {
|
||||
if (!entry.past.length) return { play: currentPlay, history }; const snapshot = entry.past.at(-1); const nextPlay = restoreTrackActionSnapshot(currentPlay, snapshot); if (nextPlay === currentPlay) return { play: currentPlay, history }; entry.past = entry.past.slice(0, -1); entry.future = [trackActionSnapshot(currentPlay, sequenceId, playerId), ...entry.future]; return { play: nextPlay, history: normalized };
|
||||
}
|
||||
if (!entry.future.length) return { play: currentPlay, history }; const snapshot = entry.future[0]; const nextPlay = restoreTrackActionSnapshot(currentPlay, snapshot); if (nextPlay === currentPlay) return { play: currentPlay, history }; entry.future = entry.future.slice(1); entry.past = [...entry.past, trackActionSnapshot(currentPlay, sequenceId, playerId)].slice(-normalized.limit); return { play: nextPlay, history: normalized };
|
||||
}
|
||||
export function canUndoActionEdit(state) { const sequenceId = state.play.sequences[state.selectedSequence]?.id; const entry = state.history?.bySequenceId?.[sequenceId]?.[state.selectedPlayerId]; const snapshot = entry?.past?.at(-1); return Boolean(snapshot && restoreTrackActionSnapshot(state.play, snapshot) !== state.play); }
|
||||
export function canRedoActionEdit(state) { const sequenceId = state.play.sequences[state.selectedSequence]?.id; const entry = state.history?.bySequenceId?.[sequenceId]?.[state.selectedPlayerId]; const snapshot = entry?.future?.[0]; return Boolean(snapshot && restoreTrackActionSnapshot(state.play, snapshot) !== state.play); }
|
||||
export function undoActionEdit(state) { const sequenceId = state.play.sequences[state.selectedSequence]?.id; const result = actionHistoryResult(state.history, state.play, sequenceId, state.selectedPlayerId, 'undo'); return result.play === state.play ? state : restoreActionHistoryState(state, result); }
|
||||
export function redoActionEdit(state) { const sequenceId = state.play.sequences[state.selectedSequence]?.id; const result = actionHistoryResult(state.history, state.play, sequenceId, state.selectedPlayerId, 'redo'); return result.play === state.play ? state : restoreActionHistoryState(state, result); }
|
||||
|
||||
export function addMoveAction(state, location) { const play = addAction(state.play, state.selectedSequence, state.selectedPlayerId, location); const track = play.sequences[state.selectedSequence]?.tracks.find((candidate) => candidate.playerId === state.selectedPlayerId); return track && play !== state.play ? { ...commitActionEdit(state, play), selectedAction: track.actions.length - 1 } : state; }
|
||||
export function setSelectedPlayerStart(state, location) { const play = setPlayerStartLocation(state.play, state.selectedPlayerId, location); return { ...state, play, selectedAction: -1 }; }
|
||||
export function addPlaySequence(state) { const play = addSequence(state.play); return { ...state, play, history: normalizeActionHistory(state.history, play), selectedSequence: play.sequences.length - 1, selectedAction: -1 }; }
|
||||
export function deletePlaySequence(state, index = state.selectedSequence) {
|
||||
const play = removeSequence(state.play, index); if (samePlay(state.play, play)) return state; const selectedSequence = index < state.selectedSequence ? state.selectedSequence - 1 : Math.min(state.selectedSequence, play.sequences.length - 1); return { ...state, play, history: normalizeActionHistory(state.history, play), selectedSequence: Math.max(0, selectedSequence), selectedAction: -1 };
|
||||
}
|
||||
export function deleteSelectedAction(state, index = state.selectedAction) { const play = removeAction(state.play, state.selectedSequence, state.selectedPlayerId, index); return { ...commitActionEdit(state, play), selectedAction: -1 }; }
|
||||
Reference in New Issue
Block a user