Skip to content

Commit

Permalink
Add the ability to execute effects when using optional conditions.
Browse files Browse the repository at this point in the history
GitOrigin-RevId: 3ccf0e7be5023cf7006ddb3b90f312deba33cb5a
  • Loading branch information
cpojer committed May 31, 2024
1 parent 14bd3d8 commit 66aa348
Show file tree
Hide file tree
Showing 6 changed files with 171 additions and 37 deletions.
32 changes: 30 additions & 2 deletions apollo/Condition.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,16 @@ export type GameEndCondition = Readonly<{
value: WinConditionID;
}>;

export type Condition = UnitEqualsCondition | GameEndCondition;
export type OptionalCondition = Readonly<{
type: 'Optional';
value: number;
}>;

export type Condition =
| GameEndCondition
| OptionalCondition
| UnitEqualsCondition;

export type Conditions = ReadonlyArray<Condition>;

const equalsUnit = (
Expand Down Expand Up @@ -67,6 +76,17 @@ const gameEnd = (
return value === 'lose';
};

const optionalCondition = (
previousMap: MapData,
activeMap: MapData,
actionResponse: ActionResponse,
{ value }: OptionalCondition,
) =>
actionResponse.type === 'OptionalCondition' &&
activeMap.getCurrentPlayer().teamId ===
activeMap.getTeam(actionResponse.toPlayer).id &&
value === actionResponse.conditionId;

export function evaluateCondition(
previousMap: MapData,
activeMap: MapData,
Expand All @@ -79,6 +99,13 @@ export function evaluateCondition(
return equalsUnit(previousMap, activeMap, actionResponse, condition);
case 'GameEnd':
return gameEnd(previousMap, activeMap, actionResponse, condition);
case 'Optional':
return optionalCondition(
previousMap,
activeMap,
actionResponse,
condition,
);
default: {
condition satisfies never;
throw new UnknownTypeError('evaluateCondition', type);
Expand Down Expand Up @@ -121,13 +148,14 @@ export function validateCondition(map: MapData, condition: Condition) {
}
return true;
}
case 'Optional':
case 'GameEnd': {
const {
config: { winConditions },
} = map;
const { value } = condition;
return (
WinConditionIDs.has(value) ||
(type === 'GameEnd' && WinConditionIDs.has(value)) ||
(typeof value === 'number' &&
winConditions[value] &&
winConditions[value].type !== WinCriteria.Default)
Expand Down
3 changes: 2 additions & 1 deletion apollo/ConditionMap.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
[
["UnitEquals", [0, ["type", "from", "target"]]],
["GameEnd", [1, ["type", "value"]]]
["GameEnd", [1, ["type", "value"]]],
["Optional", [2, ["type", "value"]]]
]
25 changes: 16 additions & 9 deletions hera/editor/lib/EffectTitle.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
GameEndCondition,
OptionalCondition,
WinConditionID,
} from '@deities/apollo/Condition.tsx';
import { Effect, Effects, EffectTrigger } from '@deities/apollo/Effects.tsx';
Expand Down Expand Up @@ -62,22 +63,28 @@ export default memo(function EffectTitle({
);
}

if (trigger === 'GameEnd') {
const gameEndCondition = effect.conditions?.find(
(condition): condition is GameEndCondition =>
condition.type === 'GameEnd',
if (trigger === 'GameEnd' || trigger === 'OptionalCondition') {
const condition = effect.conditions?.find(
(condition): condition is GameEndCondition | OptionalCondition =>
condition.type === 'GameEnd' || condition.type === 'Optional',
);
return (
<span className={cx(titleStyle, ellipsis)}>
<span>
<fbt desc="Label for 'GameEnd' effect">Game End</fbt>
</span>
{condition && (
<span>
{condition.type === 'GameEnd' ? (
<fbt desc="Label for 'GameEnd' effect">Game End</fbt>
) : (
<fbt desc="Label for 'Optional' effect">Optional Condition</fbt>
)}
</span>
)}
{players}
{gameEndCondition && (
{condition && (
<>
<span>-</span>
<EffectWinConditionTitle
id={gameEndCondition.value}
id={condition.value}
winConditions={winConditions}
/>
</>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Conditions, WinConditionID } from '@deities/apollo/Condition.tsx';

export default function hasGameEndCondition(
export default function hasEffectWinCondition(
type: 'GameEnd' | 'Optional',
id: WinConditionID,
conditions?: Conditions,
) {
return conditions?.some(
(condition) => condition.type === 'GameEnd' && condition.value === id,
(condition) => condition.type === type && condition.value === id,
);
}
5 changes: 3 additions & 2 deletions hera/editor/lib/selectWinConditionEffect.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { WinConditionID } from '@deities/apollo/Condition.tsx';
import { WinCondition, WinCriteria } from '@deities/athena/WinConditions.tsx';
import { EditorState } from '../Types.tsx';
import hasGameEndCondition from './hasGameEndCondition.tsx';
import hasEffectWinCondition from './hasEffectWinCondition.tsx';

export default function selectWinConditionEffect(
editor: EditorState,
Expand All @@ -11,7 +11,8 @@ export default function selectWinConditionEffect(
const effectList = editor.effects.get('GameEnd');
const effect = effectList
? [...effectList].find(({ conditions }) =>
hasGameEndCondition(
hasEffectWinCondition(
'GameEnd',
condition?.type === WinCriteria.Default ? 'win' : conditionIndex,
conditions,
),
Expand Down
138 changes: 117 additions & 21 deletions hera/editor/panels/WinConditionPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Effects } from '@deities/apollo/Effects.tsx';
import dropInactivePlayers from '@deities/athena/lib/dropInactivePlayers.tsx';
import {
getInitialWinCondition,
Expand All @@ -7,18 +8,115 @@ import {
WinCriteria,
WinCriteriaList,
} from '@deities/athena/WinConditions.tsx';
import groupBy from '@deities/hephaestus/groupBy.tsx';
import Box from '@deities/ui/Box.tsx';
import InlineLink from '@deities/ui/InlineLink.tsx';
import Stack from '@deities/ui/Stack.tsx';
import { css } from '@emotion/css';
import { useCallback, useState } from 'react';
import getWinCriteriaName from '../../lib/getWinCriteriaName.tsx';
import { StateWithActions } from '../../Types.tsx';
import hasGameEndCondition from '../lib/hasGameEndCondition.tsx';
import hasEffectWinCondition from '../lib/hasEffectWinCondition.tsx';
import selectWinConditionEffect from '../lib/selectWinConditionEffect.tsx';
import WinConditionCard from '../lib/WinConditionCard.tsx';
import { EditorState, SetEditorStateFunction } from '../Types.tsx';

const maybeRemoveEffect = (
effects: Effects,
condition: WinCondition,
index: number,
setEditorState: SetEditorStateFunction,
) => {
if (condition.type === WinCriteria.Default) {
return;
}

const trigger = condition.optional ? 'OptionalCondition' : 'GameEnd';
const list = effects.get(trigger);
if (list) {
setEditorState({
effects: new Map(effects).set(
trigger,
new Set(
[...list].filter(
({ conditions }) =>
!hasEffectWinCondition(
trigger === 'GameEnd' ? 'GameEnd' : 'Optional',
index,
conditions,
),
),
),
),
});
}
};

const maybeSwapEffect = (
effects: Effects,
condition: WinCondition,
existingCondition: WinCondition,
index: number,
setEditorState: SetEditorStateFunction,
) => {
if (
condition.type === WinCriteria.Default ||
existingCondition.type === WinCriteria.Default ||
existingCondition.optional === condition.optional
) {
return;
}

const trigger = existingCondition.optional ? 'OptionalCondition' : 'GameEnd';
const list = effects.get(trigger);
if (!list) {
return;
}

const newTrigger = trigger === 'GameEnd' ? 'OptionalCondition' : 'GameEnd';
const partition = groupBy(list, ({ conditions }) =>
hasEffectWinCondition(
trigger === 'GameEnd' ? 'GameEnd' : 'Optional',
index,
conditions,
)
? 'target'
: 'origin',
);
const target = partition.get('target')?.map((effect) => ({
...effect,
conditions: effect.conditions?.map((condition) => {
const { type } = condition;
if (type === 'GameEnd' || type === 'Optional') {
const { value } = condition;
if (typeof value === 'number' && value === index) {
return {
...condition,
type: type === 'GameEnd' ? 'Optional' : 'GameEnd',
value,
} as const;
}
}
return condition;
}),
}));

const newEffects = new Map(effects);
if (target) {
newEffects.set(newTrigger, new Set(target));
}

const origin = partition.get('origin');
if (origin) {
newEffects.set(trigger, new Set(origin));
} else {
newEffects.delete(trigger);
}
setEditorState({
effects: newEffects,
});
};

export default function WinConditionPanel({
actions,
editor,
Expand Down Expand Up @@ -48,42 +146,40 @@ export default function WinConditionPanel({
index: number,
) => {
const winConditions = [...conditions];
const existingCondition = winConditions[index];
if (!condition) {
const existingCondition = winConditions[index];
if (existingCondition.type !== WinCriteria.Default) {
const list = editor.effects.get('GameEnd');
const newList = list
? new Set(
[...list].filter(
({ conditions }) => !hasGameEndCondition(index, conditions),
),
)
: null;

if (newList) {
const effects = new Map(editor.effects);
effects.set('GameEnd', newList);
setEditorState({
effects,
});
}
}
maybeRemoveEffect(
editor.effects,
existingCondition,
index,
setEditorState,
);
winConditions.splice(index, 1);
if (!winConditions.length) {
winConditions.push(getInitialWinCondition(map, WinCriteria.Default));
}
// Increment this counter to force re-rendering all list items which resets their state.
setRenders((renders) => renders + 1);
return actions.update({
actions.update({
map: map.copy({
config: map.config.copy({
winConditions,
}),
}),
});

return;
}

if (validate(condition)) {
maybeSwapEffect(
editor.effects,
condition,
existingCondition,
index,
setEditorState,
);

winConditions[index] = condition;
actions.update({
map: map.copy({
Expand Down

0 comments on commit 66aa348

Please sign in to comment.