Skip to content
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

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from 2 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
138 changes: 116 additions & 22 deletions hera/editor/MapEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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';
Expand All @@ -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;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why return undefined instead of null?


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) =>
Expand All @@ -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
Expand All @@ -133,6 +163,7 @@ const getEditorBaseState = (
if (!effects.has('Start')) {
effects = new Map([...effects, ['Start', startScenario]]);
}

return {
effects,
isDrawing: false,
Expand All @@ -143,8 +174,8 @@ const getEditorBaseState = (
selected: {
tile: Plain.id,
},
undoStack: [['initial', map]],
undoStackIndex: null,
undoStack: undoStack ?? [['initial', map]],
undoStackIndex: undoStackIndex ?? null,
};
};

Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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);

Copy link
Contributor

Choose a reason for hiding this comment

The 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];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if this ends up being -1? It will crash, right?


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,
);
},
[],
);
Expand Down Expand Up @@ -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
Expand All @@ -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(
{
Expand All @@ -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,
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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(
Expand All @@ -722,6 +814,8 @@ export default function MapEditor({
},
editor.mode,
editor.scenario,
previousState.undoStack,
previousState.undoStackIndex,
),
);
}
Expand Down
4 changes: 4 additions & 0 deletions hera/editor/Types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Copy link
Contributor

Choose a reason for hiding this comment

The 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 slug, that means you'll always have a unique id to work with.

map: MapData;
mapName: string;
tags: ReadonlyArray<string>;
Expand Down Expand Up @@ -110,6 +112,8 @@ export type MapObject = Readonly<{
export type PreviousMapEditorState = Readonly<{
effects: string;
map: MapData;
undoStack?: UndoStack;
undoStackIndex?: number;
}>;

export type MapEditorSaveMessageId =
Expand Down
13 changes: 13 additions & 0 deletions hera/editor/lib/updateUndoStack.tsx
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'}`;

Copy link
Contributor

@cpojer cpojer May 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since these are functions they should be getUndoStackKey and getUndoStackIndexKey.

export default function updateUndoStack(
{ setEditorState }: { setEditorState: SetEditorStateFunction },
{ undoStack, undoStackIndex }: EditorState,
entry: UndoEntry,
id?: string,
) {
const lastKey = undoStack.at(
undoStackIndex != null ? undoStackIndex : -1,
Expand All @@ -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));
}
}
4 changes: 4 additions & 0 deletions ui/Storage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ export default {
return localStorage.getItem(`${namespace}${key}`);
},

removeItem(key: string) {
return localStorage.removeItem(`${namespace}${key}`);
},

setItem(key: string, value: string) {
if (value === null) {
localStorage.removeItem(`${namespace}${key}`);
Expand Down