-
-
Notifications
You must be signed in to change notification settings - Fork 126
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add undo-stack on a per map basis #22
Changes from 2 commits
a2cc8f7
d8c6d8d
c6f4a04
2c773b5
f5de162
3ce9fed
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -26,6 +26,7 @@ import validateMap from '@deities/athena/lib/validateMap.tsx'; | |
import withModifiers from '@deities/athena/lib/withModifiers.tsx'; | ||
import { Biome, Biomes } from '@deities/athena/map/Biome.tsx'; | ||
import { DoubleSize, TileSize } from '@deities/athena/map/Configuration.tsx'; | ||
import { PlainMap } from '@deities/athena/map/PlainMap.tsx'; | ||
import { Bot, HumanPlayer, PlayerID } from '@deities/athena/map/Player.tsx'; | ||
import { toTeamArray } from '@deities/athena/map/Team.tsx'; | ||
import MapData, { SizeVector } from '@deities/athena/MapData.tsx'; | ||
|
@@ -89,7 +90,10 @@ import useZoom from './hooks/useZoom.tsx'; | |
import BiomeIcon from './lib/BiomeIcon.tsx'; | ||
import canFillTile from './lib/canFillTile.tsx'; | ||
import getMapValidationErrorText from './lib/getMapValidationErrorText.tsx'; | ||
import updateUndoStack from './lib/updateUndoStack.tsx'; | ||
import updateUndoStack, { | ||
UNDO_STACK_INDEX_KEY, | ||
UNDO_STACK_KEY, | ||
} from './lib/updateUndoStack.tsx'; | ||
import ZoomButton from './lib/ZoomButton.tsx'; | ||
import MapEditorControlPanel from './panels/MapEditorControlPanel.tsx'; | ||
import ResizeHandle from './ResizeHandle.tsx'; | ||
|
@@ -103,15 +107,39 @@ import { | |
PreviousMapEditorState, | ||
SaveMapFunction, | ||
SetMapFunction, | ||
UndoStack, | ||
} from './Types.tsx'; | ||
|
||
const generateName = nameGenerator(); | ||
|
||
export function decodeUndoStack( | ||
encodedUndoStack: string | null, | ||
): UndoStack | undefined { | ||
if (!encodedUndoStack) { | ||
return undefined; | ||
} | ||
|
||
try { | ||
return JSON.parse(encodedUndoStack).map( | ||
([key, value]: [string, PlainMap]) => [key, MapData.fromObject(value)], | ||
); | ||
} catch { | ||
return undefined; | ||
} | ||
} | ||
|
||
const getUndoStack = (id?: string) => | ||
decodeUndoStack(Storage.getItem(UNDO_STACK_KEY(id))); | ||
|
||
const getUndoStackIndex = (id?: string) => { | ||
const index = Storage.getItem(UNDO_STACK_INDEX_KEY(id)); | ||
return index ? Number.parseInt(index, 10) : undefined; | ||
}; | ||
|
||
const startAction = { | ||
type: 'Start', | ||
} as const; | ||
|
||
const MAP_KEY = 'map-editor-previous-map'; | ||
const EFFECTS_KEY = 'map-editor-previous-effects'; | ||
|
||
const getDefaultScenario = (effects: Effects) => | ||
|
@@ -125,6 +153,8 @@ const getEditorBaseState = ( | |
mapObject: Pick<MapObject, 'effects'> | null = null, | ||
mode: EditorMode = 'design', | ||
scenario?: Scenario, | ||
undoStack?: UndoStack, | ||
undoStackIndex?: number, | ||
): EditorState => { | ||
const startScenario = new Set([{ actions: [] }]); | ||
let effects: Effects = mapObject?.effects | ||
|
@@ -133,6 +163,7 @@ const getEditorBaseState = ( | |
if (!effects.has('Start')) { | ||
effects = new Map([...effects, ['Start', startScenario]]); | ||
} | ||
|
||
return { | ||
effects, | ||
isDrawing: false, | ||
|
@@ -143,8 +174,8 @@ const getEditorBaseState = ( | |
selected: { | ||
tile: Plain.id, | ||
}, | ||
undoStack: [['initial', map]], | ||
undoStackIndex: null, | ||
undoStack: undoStack ?? [['initial', map]], | ||
undoStackIndex: undoStackIndex ?? null, | ||
}; | ||
}; | ||
|
||
|
@@ -241,7 +272,14 @@ export default function MapEditor({ | |
usePlayMusic(map.config.biome); | ||
|
||
const [editor, _setEditorState] = useState<EditorState>(() => | ||
getEditorBaseState(map, mapObject, mode, scenario), | ||
getEditorBaseState( | ||
map, | ||
mapObject, | ||
mode, | ||
scenario, | ||
getUndoStack(mapObject?.id), | ||
getUndoStackIndex(mapObject?.id), | ||
), | ||
); | ||
const [previousEffects, setPreviousEffects] = useState(editor.effects); | ||
|
||
|
@@ -319,40 +357,59 @@ export default function MapEditor({ | |
const [previousState, setPreviousState] = | ||
useState<PreviousMapEditorState | null>(() => { | ||
try { | ||
const effects = Storage.getItem(EFFECTS_KEY) || ''; | ||
const undoStack = getUndoStack(mapObject?.id); | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's remove this one empty line. There are too many empty lines here :D |
||
if (!undoStack) { | ||
return null; | ||
} | ||
|
||
const undoStackIndex = getUndoStackIndex(mapObject?.id); | ||
const map = undoStack[undoStackIndex ?? -1][1]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What if this ends up being |
||
|
||
return { | ||
effects: Storage.getItem(EFFECTS_KEY) || '', | ||
map: MapData.fromJSON(Storage.getItem(MAP_KEY) || ''), | ||
effects, | ||
map, | ||
undoStack, | ||
undoStackIndex, | ||
}; | ||
// eslint-disable-next-line no-empty | ||
} catch {} | ||
return null; | ||
}); | ||
|
||
const setMap: SetMapFunction = useCallback( | ||
(type, map) => { | ||
(type, map, undoStack) => { | ||
_setMap(map); | ||
if (type !== 'cleanup') { | ||
updateUndoStack({ setEditorState }, editor, [ | ||
type === 'resize' | ||
? `resize-${map.size.height}-${map.size.width}` | ||
: type === 'biome' | ||
? `biome-${map.config.biome}` | ||
: 'checkpoint', | ||
map, | ||
]); | ||
updateUndoStack( | ||
{ setEditorState }, | ||
undoStack ? { ...editor, undoStack } : editor, | ||
[ | ||
type === 'resize' | ||
? `resize-${map.size.height}-${map.size.width}` | ||
: type === 'biome' | ||
? `biome-${map.config.biome}` | ||
: 'checkpoint', | ||
map, | ||
], | ||
mapObject?.id, | ||
); | ||
} | ||
setInternalMapID(internalMapID + 1); | ||
}, | ||
[editor, internalMapID, setEditorState], | ||
[editor, internalMapID, setEditorState, mapObject?.id], | ||
); | ||
|
||
const updatePreviousMap = useCallback( | ||
(map?: MapData, editorEffects?: Effects) => { | ||
(map?: MapData, editorEffects?: Effects, editorUndoStack?: UndoStack) => { | ||
const effects = JSON.stringify( | ||
editorEffects ? encodeEffects(editorEffects) : '', | ||
); | ||
Storage.setItem(MAP_KEY, JSON.stringify(map)); | ||
setPreviousState(map ? { effects, map } : null); | ||
|
||
setPreviousState( | ||
map ? { effects, map, undoStack: editorUndoStack } : null, | ||
); | ||
}, | ||
[], | ||
); | ||
|
@@ -457,10 +514,18 @@ export default function MapEditor({ | |
|
||
const isNew = type === 'New' || !mapObject?.id; | ||
setSaveState(null); | ||
|
||
const stack = editor.undoStack.map(([key, value]) => [ | ||
key, | ||
value.toJSON(), | ||
]); | ||
|
||
if (isNew) { | ||
const id = generateName(); | ||
createMap( | ||
{ | ||
effects: editor.effects, | ||
id, | ||
map, | ||
mapName, | ||
tags: mapObject?.id | ||
|
@@ -469,6 +534,15 @@ export default function MapEditor({ | |
}, | ||
setSaveState, | ||
); | ||
Storage.removeItem(UNDO_STACK_KEY('')); | ||
Storage.removeItem(UNDO_STACK_INDEX_KEY('')); | ||
Storage.setItem(UNDO_STACK_KEY(id), JSON.stringify(stack)); | ||
if (editor.undoStackIndex !== null) { | ||
Storage.setItem( | ||
UNDO_STACK_INDEX_KEY(id), | ||
`${editor?.undoStackIndex}`, | ||
); | ||
} | ||
} else { | ||
updateMap( | ||
{ | ||
|
@@ -481,14 +555,23 @@ export default function MapEditor({ | |
type, | ||
setSaveState, | ||
); | ||
Storage.setItem(UNDO_STACK_KEY(mapObject?.id), JSON.stringify(stack)); | ||
if (editor.undoStackIndex !== null) { | ||
Storage.setItem( | ||
UNDO_STACK_INDEX_KEY(mapObject?.id), | ||
`${editor?.undoStackIndex}`, | ||
); | ||
} | ||
} | ||
}, | ||
[ | ||
createMap, | ||
editor.effects, | ||
editor.undoStackIndex, | ||
mapName, | ||
mapObject?.id, | ||
setMap, | ||
editor.undoStack, | ||
tags, | ||
updateMap, | ||
withHumanPlayer, | ||
|
@@ -574,6 +657,7 @@ export default function MapEditor({ | |
undoStack.length, | ||
), | ||
); | ||
Storage.setItem(UNDO_STACK_INDEX_KEY(mapObject?.id), `${index}`); | ||
if (index > -1 && index < undoStack.length) { | ||
const [, newMap] = undoStack.at(index) || []; | ||
if (newMap) { | ||
|
@@ -659,7 +743,15 @@ export default function MapEditor({ | |
document.removeEventListener('keydown', keydownListener); | ||
document.removeEventListener('keyup', keyupListener); | ||
}; | ||
}, [editor, saveMap, setEditorState, setMap, tilted, toggleDeleteEntity]); | ||
}, [ | ||
editor, | ||
saveMap, | ||
setEditorState, | ||
setMap, | ||
tilted, | ||
toggleDeleteEntity, | ||
mapObject?.id, | ||
]); | ||
|
||
useInput( | ||
'select', | ||
|
@@ -712,7 +804,7 @@ export default function MapEditor({ | |
|
||
const restorePreviousState = useCallback(() => { | ||
if (previousState) { | ||
setMap('reset', previousState.map); | ||
setMap('reset', previousState.map, previousState.undoStack); | ||
setPreviousState(null); | ||
_setEditorState( | ||
getEditorBaseState( | ||
|
@@ -722,6 +814,8 @@ export default function MapEditor({ | |
}, | ||
editor.mode, | ||
editor.scenario, | ||
previousState.undoStack, | ||
previousState.undoStackIndex, | ||
), | ||
); | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -65,11 +65,13 @@ export type SetEditorStateFunction = (editor: Partial<EditorState>) => void; | |
export type SetMapFunction = ( | ||
type: 'biome' | 'cleanup' | 'heal' | 'reset' | 'resize' | 'teams', | ||
map: MapData, | ||
undoStack?: UndoStack, | ||
) => void; | ||
|
||
type MapSaveType = 'New' | 'Update' | 'Disk' | 'Export'; | ||
export type MapCreateVariables = Readonly<{ | ||
effects: Effects; | ||
id: string; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's remove this. When you rebase, you'll be able to find this: https://github.com/nkzw-tech/athena-crisis/blob/main/docs/content/examples/map-editor.tsx#L93 Let's change that line to set the id to the same as |
||
map: MapData; | ||
mapName: string; | ||
tags: ReadonlyArray<string>; | ||
|
@@ -110,6 +112,8 @@ export type MapObject = Readonly<{ | |
export type PreviousMapEditorState = Readonly<{ | ||
effects: string; | ||
map: MapData; | ||
undoStack?: UndoStack; | ||
undoStackIndex?: number; | ||
}>; | ||
|
||
export type MapEditorSaveMessageId = | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,17 @@ | ||
import Storage from '@deities/ui/Storage.tsx'; | ||
import { EditorState, SetEditorStateFunction, UndoEntry } from '../Types.tsx'; | ||
|
||
export const UNDO_STACK_KEY = (id: string | undefined) => | ||
`map-editor-undo-stack-${id ? `${id}` : 'fallback'}`; | ||
|
||
export const UNDO_STACK_INDEX_KEY = (id: string | undefined) => | ||
`map-editor-undo-stack-index-${id ? `${id}` : 'fallback'}`; | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since these are functions they should be |
||
export default function updateUndoStack( | ||
{ setEditorState }: { setEditorState: SetEditorStateFunction }, | ||
{ undoStack, undoStackIndex }: EditorState, | ||
entry: UndoEntry, | ||
id?: string, | ||
) { | ||
const lastKey = undoStack.at( | ||
undoStackIndex != null ? undoStackIndex : -1, | ||
|
@@ -24,4 +32,9 @@ export default function updateUndoStack( | |
], | ||
undoStackIndex: null, | ||
}); | ||
|
||
if (id) { | ||
const stack = undoStack.map(([key, value]) => [key, value.toJSON()]); | ||
Storage.setItem(UNDO_STACK_KEY(id), JSON.stringify(stack)); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why return
undefined
instead ofnull
?