Skip to content

Commit

Permalink
Symmetrical Drawing Mode in the Map Editor (#19)
Browse files Browse the repository at this point in the history
Co-authored-by: cpojer <[email protected]>
  • Loading branch information
dkratz and cpojer authored May 19, 2024
1 parent 33fbbd4 commit e893189
Show file tree
Hide file tree
Showing 10 changed files with 492 additions and 106 deletions.
24 changes: 19 additions & 5 deletions hera/GameMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ import { resetBehavior, setBaseClass } from './behavior/Behavior.tsx';
import MenuBehavior from './behavior/Menu.tsx';
import NullBehavior from './behavior/NullBehavior.tsx';
import Cursor from './Cursor.tsx';
import MapEditorExtraCursors from './editor/MapEditorMirrorCursors.tsx';
import { EditorState } from './editor/Types.tsx';
import addEndTurnAnimations from './lib/addEndTurnAnimations.tsx';
import animateSupply from './lib/animateSupply.tsx';
Expand Down Expand Up @@ -1721,12 +1722,25 @@ export default class GameMap extends Component<Props, State> {
{(propsShowCursor || propsShowCursor == null) &&
showCursor &&
!replayState.isReplaying && (
<Cursor
position={position}
size={tileSize}
zIndex={zIndex - 4}
/>
<>
<Cursor
position={position}
size={tileSize}
zIndex={zIndex - 4}
/>
{editor?.mode === 'design' && (
<MapEditorExtraCursors
color="red"
drawingMode={editor?.drawingMode}
mapSize={map.size}
origin={position}
size={tileSize}
zIndex={zIndex}
/>
)}
</>
)}

<MapAnimations
actions={this._actions}
animationComplete={this._animationComplete}
Expand Down
1 change: 1 addition & 0 deletions hera/editor/MapEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ const getEditorBaseState = (
effects = new Map([...effects, ['Start', startScenario]]);
}
return {
drawingMode: 'regular',
effects,
isDrawing: false,
isErasing: false,
Expand Down
28 changes: 28 additions & 0 deletions hera/editor/MapEditorMirrorCursors.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import Vector from '@deities/athena/map/Vector.tsx';
import { SizeVector } from '@deities/athena/MapData.tsx';
import { ComponentProps } from 'react';
import Cursor from '../Cursor.tsx';
import getSymmetricPositions from './lib/getSymmetricPositions.ts';
import { DrawingMode } from './Types.tsx';

export default function MapEditorMirrorCursors({
drawingMode,
mapSize,
origin: orign,
...props
}: {
drawingMode: DrawingMode | undefined;
mapSize: SizeVector;
origin: Vector | null;
} & Omit<ComponentProps<typeof Cursor>, 'position'>) {
if (!orign || !drawingMode || drawingMode === 'regular') {
return null;
}

const vectors = getSymmetricPositions(orign, drawingMode, mapSize);
return vectors.size
? [...vectors].map((vector) => (
<Cursor {...props} key={vector.toString()} position={vector} />
))
: null;
}
7 changes: 7 additions & 0 deletions hera/editor/Types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,16 @@ type UndoKey =

export type UndoEntry = readonly [UndoKey, MapData];
export type UndoStack = ReadonlyArray<UndoEntry>;
export type DrawingMode =
| 'regular'
| 'horizontal'
| 'vertical'
| 'horizontal-vertical'
| 'diagonal';

export type EditorState = Readonly<{
condition?: readonly [WinConditionsWithVectors, number];
drawingMode: DrawingMode;
effects: Effects;
isDrawing: boolean;
isErasing: boolean;
Expand Down
69 changes: 57 additions & 12 deletions hera/editor/behavior/DesignBehavior.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import verifyTiles from '@deities/athena/lib/verifyTiles.tsx';
import Building from '@deities/athena/map/Building.tsx';
import { getDecoratorLimit } from '@deities/athena/map/Configuration.tsx';
import Entity from '@deities/athena/map/Entity.tsx';
import { PlayerID, PlayerIDs } from '@deities/athena/map/Player.tsx';
import Unit from '@deities/athena/map/Unit.tsx';
import Vector from '@deities/athena/map/Vector.tsx';
import MapData from '@deities/athena/MapData.tsx';
Expand All @@ -48,6 +49,7 @@ import {
} from '../../Types.tsx';
import FlashFlyout from '../../ui/FlashFlyout.tsx';
import { FlyoutItem } from '../../ui/Flyout.tsx';
import getSymmetricPositions from '../lib/getSymmetricPositions.ts';
import updateUndoStack from '../lib/updateUndoStack.tsx';
import { EditorState } from '../Types.tsx';

Expand Down Expand Up @@ -162,7 +164,11 @@ export default class DesignBehavior {
subVector?: Vector,
): StateLike | null {
if (editor?.isDrawing && editor.selected) {
return this.put(vector, state, actions, editor);
const vectors = [
vector,
...getSymmetricPositions(vector, editor.drawingMode, state.map.size),
];
return this.draw(vectors, state, actions, editor);
}

const { animations, map } = state;
Expand Down Expand Up @@ -228,11 +234,46 @@ export default class DesignBehavior {
return null;
}

private draw(
vectors: Array<Vector>,
state: State,
actions: Actions,
editor: EditorState,
): StateLike | null {
let newState: StateLike | null = null;
const players = Array.from(
new Set([...state.map.active, ...PlayerIDs.filter((id) => id !== 0)]),
).slice(0, vectors.length);
vectors.forEach((vector, index) => {
const currentPlayerIndex = players.indexOf(
state.map.getCurrentPlayer().id,
);
const playerId =
players[
((currentPlayerIndex >= 0 ? currentPlayerIndex : 0) + index) %
players.length
];

newState = {
...newState,
...this.put(
vector,
{ ...state, ...newState },
actions,
editor,
playerId,
),
};
});
return newState;
}

private put(
vector: Vector,
state: State,
actions: Actions,
editor: EditorState,
playerId: PlayerID,
): StateLike | null {
if (shouldPlaceDecorator(editor)) {
return null;
Expand Down Expand Up @@ -299,7 +340,14 @@ export default class DesignBehavior {
);
} else if (selected.unit) {
this.previous = null;
return this.putUnit(selected.unit, vector, state, actions, editor);
return this.putUnit(
selected.unit,
vector,
state,
actions,
editor,
playerId,
);
} else if (selected.building) {
this.previous = null;
return this.putBuilding(
Expand All @@ -308,6 +356,7 @@ export default class DesignBehavior {
state,
actions,
editor,
playerId,
);
}
return null;
Expand Down Expand Up @@ -503,6 +552,7 @@ export default class DesignBehavior {
state: State,
actions: Actions,
editor: EditorState,
playerId: PlayerID,
): StateLike | null {
const { map } = state;
const { units } = map;
Expand All @@ -521,12 +571,7 @@ export default class DesignBehavior {
...spawn(
actions,
state,
[
[
vector,
unit.removeLeader().setPlayer(map.getCurrentPlayer().id),
],
],
[[vector, unit.removeLeader().setPlayer(playerId)]],
null,
({ map }) => {
updateUndoStack(actions, editor, [
Expand All @@ -551,6 +596,7 @@ export default class DesignBehavior {
state: State,
actions: Actions,
editor: EditorState,
playerId: PlayerID,
): StateLike | null {
const { animations, map } = state;
const { buildings, units } = map;
Expand All @@ -568,14 +614,13 @@ export default class DesignBehavior {
const config = map.config.copy({
blocklistedBuildings: new Set(),
});
const player = map.getCurrentPlayer();
const isAlwaysNeutral = building.info.isStructure();

const tryToPlaceBuilding = (state: State): StateLike | null => {
let { map } = state;
map = map.copy({
active: getActivePlayers(map),
buildings: map.buildings.set(vector, building.setPlayer(player.id)),
buildings: map.buildings.set(vector, building.setPlayer(playerId)),
});

const { editorPlaceOn, placeOn } = building.info.configuration;
Expand Down Expand Up @@ -618,7 +663,7 @@ export default class DesignBehavior {
return canBuild(
getTemporaryMapForBuilding(temporaryMap, vector, building),
building.info,
isAlwaysNeutral ? 0 : player,
isAlwaysNeutral ? 0 : playerId,
vector,
true,
) && !(building.info.isHQ() && map.currentPlayer === 0)
Expand All @@ -636,7 +681,7 @@ export default class DesignBehavior {
return newState;
},
type: 'createBuilding',
variant: isAlwaysNeutral ? 0 : player.id,
variant: isAlwaysNeutral ? 0 : playerId,
}),
map: state.map.copy({ buildings: buildings.delete(vector) }),
}
Expand Down
139 changes: 139 additions & 0 deletions hera/editor/lib/__tests__/getSymmetricPositions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import vec from '@deities/athena/map/vec.tsx';
import { SizeVector } from '@deities/athena/MapData.tsx';
import { expect, test } from 'vitest';
import getSymmetricPositions from '../getSymmetricPositions.ts';

test('`getSymmetricPositions` regular', () => {
expect(
getSymmetricPositions(vec(4, 4), 'regular', new SizeVector(10, 10)),
).toMatchInlineSnapshot(`Set {}`);
});

test('`getSymmetricPositions` horizontal', () => {
expect(getSymmetricPositions(vec(4, 4), 'horizontal', new SizeVector(10, 10)))
.toMatchInlineSnapshot(`
Set {
[
7,
4,
],
}
`);
expect(getSymmetricPositions(vec(8, 8), 'horizontal', new SizeVector(10, 10)))
.toMatchInlineSnapshot(`
Set {
[
3,
8,
],
}
`);
});

test('`getSymmetricPositions` vertical', () => {
expect(getSymmetricPositions(vec(4, 4), 'vertical', new SizeVector(10, 10)))
.toMatchInlineSnapshot(`
Set {
[
4,
7,
],
}
`);
expect(getSymmetricPositions(vec(8, 8), 'vertical', new SizeVector(10, 10)))
.toMatchInlineSnapshot(`
Set {
[
8,
3,
],
}
`);
});

test('`getSymmetricPositions` diagonal', () => {
expect(getSymmetricPositions(vec(4, 4), 'diagonal', new SizeVector(10, 10)))
.toMatchInlineSnapshot(`
Set {
[
7,
7,
],
}
`);
expect(getSymmetricPositions(vec(8, 8), 'diagonal', new SizeVector(10, 10)))
.toMatchInlineSnapshot(`
Set {
[
3,
3,
],
}
`);
});

test('`getSymmetricPositions` horizontal-vertical', () => {
expect(
getSymmetricPositions(
vec(4, 4),
'horizontal-vertical',
new SizeVector(10, 10),
),
).toMatchInlineSnapshot(`
Set {
[
7,
4,
],
[
4,
7,
],
[
7,
7,
],
}
`);
expect(
getSymmetricPositions(
vec(8, 8),
'horizontal-vertical',
new SizeVector(10, 10),
),
).toMatchInlineSnapshot(`
Set {
[
3,
8,
],
[
8,
3,
],
[
3,
3,
],
}
`);
});

test('`getSymmetricPositions` does not include vector itself', () => {
expect(
getSymmetricPositions(vec(3, 2), 'horizontal', new SizeVector(5, 5)),
).toMatchInlineSnapshot(`Set {}`);
expect(
getSymmetricPositions(vec(2, 3), 'vertical', new SizeVector(5, 5)),
).toMatchInlineSnapshot(`Set {}`);
expect(
getSymmetricPositions(vec(3, 3), 'diagonal', new SizeVector(5, 5)),
).toMatchInlineSnapshot(`Set {}`);
expect(
getSymmetricPositions(
vec(3, 3),
'horizontal-vertical',
new SizeVector(5, 5),
),
).toMatchInlineSnapshot(`Set {}`);
});
Loading

0 comments on commit e893189

Please sign in to comment.