Skip to content

Commit

Permalink
feat(elder): elder has taken revenge event (#656)
Browse files Browse the repository at this point in the history
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit


- **New Features**
- Introduced a new game event: Elder taking revenge, with associated
visuals and audio effects.
- Added functionality to track player attribute alterations in game
history.
	- New options for game setup affecting the Elder's gameplay.
- Enhanced event handling with a dedicated component for Elder's revenge
event.

- **Bug Fixes**
- Improved import statements for clarity and consistency in component
mapping.

- **Documentation**
- Added new localization entries for game events in English, enhancing
narrative depth.

- **Tests**
- Expanded test coverage for game events, particularly focusing on the
Elder's role and related attributes.

- **Chores**
- Streamlined pre-commit and commit message hook scripts for better
efficiency.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
  • Loading branch information
antoinezanardi authored Jul 19, 2024
1 parent 2b6f4be commit 25f46d5
Show file tree
Hide file tree
Showing 35 changed files with 84,090 additions and 80,247 deletions.
2 changes: 0 additions & 2 deletions .husky/commit-msg
Original file line number Diff line number Diff line change
@@ -1,4 +1,2 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npx commitlint -g config/commitlint/.commitlintrc.json --edit $1
2 changes: 0 additions & 2 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npx --no-install validate-branch-name;
npm run prettier:gherkin:fix;
Expand Down
12 changes: 12 additions & 0 deletions .run/Elder Role.run.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Elder Role" type="cucumber.js" factoryName="Cucumber.js" folderName="Tags">
<option name="myFilePath" value="$PROJECT_DIR$/tests/acceptance" />
<option name="myNameFilter" value="" />
<option name="cucumberJsArguments" value="--config config/cucumber/cucumber.json --parallel 1 --tags @elder-role" />
<option name="workingDirectory" value="$PROJECT_DIR$" />
<envs>
<env name="NODE_OPTIONS" value="--import tsx/esm" />
</envs>
<method v="2" />
</configuration>
</component>
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<template>
<GameEventWithTexts
id="game-elder-has-taken-revenge-event"
:texts="gameElderHasTakenRevengeEventTexts"
>
<div class="flex h-full items-center justify-center">
<GameEventFlippingPlayerCard
v-if="elderInPlayers"
id="game-event-flipping-elder-card"
:players="[elderInPlayers]"
svg-icon-path="svg/misc/storm.svg"
/>
</div>
</GameEventWithTexts>
</template>

<script setup lang="ts">
import GameEventFlippingPlayerCard from "~/components/shared/game/game-event/GameEventFlippingPlayerCard/GameEventFlippingPlayerCard.vue";
import GameEventWithTexts from "~/components/shared/game/game-event/GameEventWithTexts/GameEventWithTexts.vue";
import { storeToRefs } from "pinia";
import type { Player } from "~/composables/api/game/types/players/player.class";
import { useGamePlayers } from "~/composables/api/game/useGamePlayers";
import { useAudioStore } from "~/stores/audio/useAudioStore";
import { useGameStore } from "~/stores/game/useGameStore";
const gameStore = useGameStore();
const { game } = storeToRefs(gameStore);
const { getPlayersWithCurrentRole } = useGamePlayers(game);
const { t } = useI18n();
const audioStore = useAudioStore();
const { playSoundEffect } = audioStore;
const elderInPlayers = computed<Player | undefined>(() => getPlayersWithCurrentRole("elder")[0]);
const gameElderHasTakenRevengeEventTexts = computed<string[]>(() => {
if (!elderInPlayers.value) {
return [t("components.GameElderHasTakenRevengeEvent.cantFindElder")];
}
return [
t("components.GameElderHasTakenRevengeEvent.elderHasBeenMurderedByVillager"),
t("components.GameElderHasTakenRevengeEvent.elderHasTakenRevenge"),
];
});
playSoundEffect("thunder");
</script>
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { storeToRefs } from "pinia";
import type { GameEventsMonitorEventTypeComponent } from "~/components/pages/game/GamePlaying/GameEventsMonitor/GameEventsMonitorCurrentEvent/game-events-monitor-current-event.types";
import GameAccursedWolfFatherMayHaveInfectedEvent from "~/components/pages/game/GamePlaying/GameEventsMonitor/GameEventsMonitorCurrentEvent/GameAccursedWolfFatherMayHaveInfectedEvent/GameAccursedWolfFatherMayHaveInfectedEvent.vue";
import GameCupidHasCharmedEvent from "~/components/pages/game/GamePlaying/GameEventsMonitor/GameEventsMonitorCurrentEvent/GameCupidHasCharmedEvent/GameCupidHasCharmedEvent.vue";
import GameElderHasTakenRevengeEvent from "~/components/pages/game/GamePlaying/GameEventsMonitor/GameEventsMonitorCurrentEvent/GameElderHasTakenRevengeEvent/GameElderHasTakenRevengeEvent.vue";
import GameIdiotIsSparedEvent from "~/components/pages/game/GamePlaying/GameEventsMonitor/GameEventsMonitorCurrentEvent/GameIdiotIsSparedEvent/GameIdiotIsSparedEvent.vue";
import GamePhaseStartsEvent from "~/components/pages/game/GamePlaying/GameEventsMonitor/GameEventsMonitorCurrentEvent/GamePhaseStartsEvent/GamePhaseStartsEvent.vue";
import GamePiedPiperHasCharmedEvent from "~/components/pages/game/GamePlaying/GameEventsMonitor/GameEventsMonitorCurrentEvent/GamePiedPiperHasCharmedEvent/GamePiedPiperHasCharmedEvent.vue";
Expand All @@ -34,7 +35,7 @@ import GameSheriffPromotionEvent from "~/components/pages/game/GamePlaying/GameE
import GameStartsEvent from "~/components/pages/game/GamePlaying/GameEventsMonitor/GameEventsMonitorCurrentEvent/GameStartsEvent/GameStartsEvent.vue";
import GameTurnStartsEvent from "~/components/pages/game/GamePlaying/GameEventsMonitor/GameEventsMonitorCurrentEvent/GameTurnStartsEvent/GameTurnStartsEvent.vue";
import GameVillagerVillagerIntroductionEvent from "~/components/pages/game/GamePlaying/GameEventsMonitor/GameEventsMonitorCurrentEvent/GameVillagerVillagerIntroductionEvent/GameVillagerVillagerIntroductionEvent.vue";
import GameWolfHoundHasChosenSide from "~/components/pages/game/GamePlaying/GameEventsMonitor/GameEventsMonitorCurrentEvent/GameWolfHoundHasChosenSideEvent/GameWolfHoundHasChosenSideEvent.vue";
import GameWolfHoundHasChosenSideEvent from "~/components/pages/game/GamePlaying/GameEventsMonitor/GameEventsMonitorCurrentEvent/GameWolfHoundHasChosenSideEvent/GameWolfHoundHasChosenSideEvent.vue";
import type { GameEventType } from "~/stores/game/game-event/types/game-event.types";
import { useGameEventsStore } from "~/stores/game/game-event/useGameEventsStore";
Expand All @@ -54,8 +55,9 @@ const currentGameEventTypeComponent = computed<GameEventsMonitorEventTypeCompone
"accursed-wolf-father-may-have-infected": GameAccursedWolfFatherMayHaveInfectedEvent,
"pied-piper-has-charmed": GamePiedPiperHasCharmedEvent,
"cupid-has-charmed": GameCupidHasCharmedEvent,
"wolf-hound-has-chosen-side": GameWolfHoundHasChosenSide,
"wolf-hound-has-chosen-side": GameWolfHoundHasChosenSideEvent,
"idiot-is-spared": GameIdiotIsSparedEvent,
"elder-has-taken-revenge": GameElderHasTakenRevengeEvent,
};
if (!currentGameEvent.value) {
return undefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,13 @@ const GAME_HISTORY_RECORD_PLAY_VOTING_RESULTS = [
"skipped",
] as const;

export { GAME_HISTORY_RECORD_PLAY_VOTING_RESULTS };
const GAME_HISTORY_RECORD_PLAYER_ATTRIBUTE_ALTERATION_STATUSES = [
"attached",
"detached",
"activated",
] as const;

export {
GAME_HISTORY_RECORD_PLAY_VOTING_RESULTS,
GAME_HISTORY_RECORD_PLAYER_ATTRIBUTE_ALTERATION_STATUSES,
};
100 changes: 100 additions & 0 deletions app/composables/api/game/game-event/useGameEventsGenerator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { useGameLastHistoryRecord } from "~/composables/api/game/game-history-record/useGameLastHistoryRecord";
import { useCurrentGamePlay } from "~/composables/api/game/game-play/useCurrentGamePlay";
import type { GamePlaySourceName } from "~/composables/api/game/types/game-play/game-play-source/game-play-source.types";
import type { GamePlayAction } from "~/composables/api/game/types/game-play/game-play.types";
import type { Game } from "~/composables/api/game/types/game.class";
import { GameEvent } from "~/stores/game/game-event/types/game-event.class";

type UseGameEventsGenerator = {
getLastGameHistoryRecordCharmEvents: (source: GamePlaySourceName) => GameEvent[];
getLastGameActionEvents: (game: Game) => GameEvent[];
getRevealedRolePlayerGameEvents: (game: Game) => GameEvent[];
getDeadPlayerGameEvents: (game: Game) => GameEvent[];
generateGameEventsFromGame: (game: Game) => GameEvent[];
};

function useGameEventsGenerator(): UseGameEventsGenerator {
function getLastGameHistoryRecordCharmEvents(source: GamePlaySourceName): GameEvent[] {
const sourcesGameEvents: Partial<Record<GamePlaySourceName, GameEvent[]>> = {
"pied-piper": [GameEvent.create({ type: "pied-piper-has-charmed" })],
"cupid": [GameEvent.create({ type: "cupid-has-charmed" })],
};

return sourcesGameEvents[source] ?? [];
}

function getLastGameActionEvents(game: Game): GameEvent[] {
const { lastGameHistoryRecord } = game;
if (!lastGameHistoryRecord) {
return [];
}
const { source, action, voting } = lastGameHistoryRecord.play;
if (action === "elect-sheriff" && voting?.result === "sheriff-election" || action === "delegate") {
return [GameEvent.create({ type: "sheriff-promotion" })];
}
const actionsGameEvents: Partial<Record<GamePlayAction, GameEvent[]>> = {
"look": [GameEvent.create({ type: "seer-has-seen" })],
"mark": [GameEvent.create({ type: "scandalmonger-has-marked" })],
"infect": [GameEvent.create({ type: "accursed-wolf-father-may-have-infected" })],
"choose-side": [GameEvent.create({ type: "wolf-hound-has-chosen-side" })],
"charm": getLastGameHistoryRecordCharmEvents(source.name),
};

return actionsGameEvents[action] ?? [];
}

function getRevealedRolePlayerGameEvents(game: Game): GameEvent[] {
const { lastGameHistoryRecord } = game;
if (lastGameHistoryRecord?.revealedPlayers?.some(player => player.role.current === "idiot") === true) {
return [GameEvent.create({ type: "idiot-is-spared" })];
}
return [];
}

function getDeadPlayerGameEvents(game: Game): GameEvent[] {
const { getEligibleTargetsWithInteractionInCurrentGamePlay } = useCurrentGamePlay(game);
const deadPlayers = getEligibleTargetsWithInteractionInCurrentGamePlay("bury");

return deadPlayers.map(player => GameEvent.create({ type: "player-dies", players: [player] }));
}

function getLastGameHistoryRecordPlayerAttributeAlterationEvents(game: Game): GameEvent[] {
const { doesHavePlayerAttributeAlteration } = useGameLastHistoryRecord(game);
const hasElderTakenRevenge = doesHavePlayerAttributeAlteration("powerless", "elder", "attached");
if (hasElderTakenRevenge) {
return [GameEvent.create({ type: "elder-has-taken-revenge" })];
}
return [];
}

function generateGameEventsFromGame(game: Game): GameEvent[] {
const gameEvents: GameEvent[] = [];
if (game.tick === 1) {
gameEvents.push(GameEvent.create({ type: "game-starts" }));
if (game.players.some(player => player.role.current === "villager-villager")) {
gameEvents.push(GameEvent.create({ type: "villager-villager-introduction" }));
}
}
gameEvents.push(...getRevealedRolePlayerGameEvents(game));
gameEvents.push(...getLastGameActionEvents(game));
gameEvents.push(...getLastGameHistoryRecordPlayerAttributeAlterationEvents(game));
if (game.phase.tick === 1 && game.phase.name !== "twilight") {
gameEvents.push(GameEvent.create({ type: "game-phase-starts" }));
}
if (game.currentPlay?.action === "bury-dead-bodies") {
gameEvents.push(...getDeadPlayerGameEvents(game));
}
gameEvents.push(GameEvent.create({ type: "game-turn-starts" }));

return gameEvents;
}
return {
getLastGameHistoryRecordCharmEvents,
getLastGameActionEvents,
getRevealedRolePlayerGameEvents,
getDeadPlayerGameEvents,
generateGameEventsFromGame,
};
}

export { useGameEventsGenerator };
Original file line number Diff line number Diff line change
@@ -1,20 +1,38 @@
import type { Ref } from "vue";
import type { MaybeRef, Ref } from "vue";
import type { GameHistoryRecordPlayerAttributeAlterationStatus } from "~/composables/api/game/types/game-history-record/game-history-record-player-attribute-alteration/game-history-record-player-attribute-alteration.types";
import type { GameHistoryRecord } from "~/composables/api/game/types/game-history-record/game-history-record.class";
import type { Game } from "~/composables/api/game/types/game.class";
import type { GameSource } from "~/composables/api/game/types/game.types";
import type { PlayerAttributeName } from "~/composables/api/game/types/players/player-attribute/player-attribute.types";
import type { Player } from "~/composables/api/game/types/players/player.class";

type UseGameLastHistoryRecord = {
lastTargetedPlayers: Ref<Player[]>;
doesHavePlayerAttributeAlteration: (attributeName: PlayerAttributeName, source: GameSource, status: GameHistoryRecordPlayerAttributeAlterationStatus) => boolean;
};

function useGameLastHistoryRecord(game: Ref<Game>): UseGameLastHistoryRecord {
function useGameLastHistoryRecord(game: MaybeRef<Game>): UseGameLastHistoryRecord {
const lastGameHistoryRecord = computed<GameHistoryRecord | null>(() => (isRef(game) ? game.value.lastGameHistoryRecord : game.lastGameHistoryRecord));

const lastTargetedPlayers = computed<Player[]>(() => {
if (!game.value.lastGameHistoryRecord?.play.targets) {
if (!lastGameHistoryRecord.value?.play.targets) {
return [];
}
return game.value.lastGameHistoryRecord.play.targets.map(target => target.player);
return lastGameHistoryRecord.value.play.targets.map(target => target.player);
});

return { lastTargetedPlayers };
function doesHavePlayerAttributeAlteration(attributeName: PlayerAttributeName, source: GameSource, status: GameHistoryRecordPlayerAttributeAlterationStatus): boolean {
if (!lastGameHistoryRecord.value?.playerAttributeAlterations) {
return false;
}
return lastGameHistoryRecord.value.playerAttributeAlterations.some(playerAttributeAlteration => playerAttributeAlteration.name === attributeName &&
playerAttributeAlteration.source === source &&
playerAttributeAlteration.status === status);
}
return {
lastTargetedPlayers,
doesHavePlayerAttributeAlteration,
};
}

export { useGameLastHistoryRecord };
19 changes: 12 additions & 7 deletions app/composables/api/game/game-play/useCurrentGamePlay.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import type { ComputedRef, Ref } from "vue";
import type { ComputedRef, MaybeRef } from "vue";
import type { GameOptions } from "~/composables/api/game/types/game-options/game-options.class";
import type { GamePlay } from "~/composables/api/game/types/game-play/game-play.class";
import type { GamePlayCause } from "~/composables/api/game/types/game-play/game-play.types";

import type { Game } from "~/composables/api/game/types/game.class";
Expand All @@ -11,11 +13,14 @@ type UseCurrentGamePlay = {
getEligibleTargetsWithInteractionInCurrentGamePlay: (interaction: PlayerInteractionType) => Player[];
};

function useCurrentGamePlay(game: Ref<Game>): UseCurrentGamePlay {
function useCurrentGamePlay(game: MaybeRef<Game>): UseCurrentGamePlay {
const currentPlay = computed<GamePlay | null>(() => (isRef(game) ? game.value.currentPlay : game.currentPlay));
const gameOptions = computed<GameOptions>(() => (isRef(game) ? game.value.options : game.options));

const mustCurrentGamePlayBeSkipped = computed<boolean>(() => {
const isWolfHoundSideRandomlyChosen = game.value.options.roles.wolfHound.isSideRandomlyChosen;
const isWolfHoundSideRandomlyChosen = gameOptions.value.roles.wolfHound.isSideRandomlyChosen;
const stealRoleEligibleTargets = getEligibleTargetsWithInteractionInCurrentGamePlay("steal-role");
const currentGameAction = game.value.currentPlay?.action;
const currentGameAction = currentPlay.value?.action;
const isCurrentActionBuryDeadBodiesAndNoStealRoleEligibleTargets = currentGameAction === "bury-dead-bodies" && !stealRoleEligibleTargets.length;
const isCurrentActionChooseSideAndSideRandomlyChosen = currentGameAction === "choose-side" && isWolfHoundSideRandomlyChosen;

Expand All @@ -29,14 +34,14 @@ function useCurrentGamePlay(game: Ref<Game>): UseCurrentGamePlay {
"stuttering-judge-request",
];

return gamePlayCausesSortedByPriority.find(cause => game.value.currentPlay?.causes?.includes(cause));
return gamePlayCausesSortedByPriority.find(cause => currentPlay.value?.causes?.includes(cause));
});

function getEligibleTargetsWithInteractionInCurrentGamePlay(interactionType: PlayerInteractionType): Player[] {
if (game.value.currentPlay?.source.interactions === undefined) {
if (currentPlay.value?.source.interactions === undefined) {
return [];
}
const interaction = game.value.currentPlay.source.interactions.find(({ type }) => type === interactionType);
const interaction = currentPlay.value.source.interactions.find(({ type }) => type === interactionType);
if (interaction === undefined) {
return [];
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Expose, plainToInstance } from "class-transformer";
import type { GameHistoryRecordPlayerAttributeAlterationStatus } from "~/composables/api/game/types/game-history-record/game-history-record-player-attribute-alteration/game-history-record-player-attribute-alteration.types";
import type { GameSource } from "~/composables/api/game/types/game.types";
import type { PlayerAttributeName } from "~/composables/api/game/types/players/player-attribute/player-attribute.types";
import { DEFAULT_PLAIN_TO_INSTANCE_OPTIONS } from "~/utils/constants/class-transformer.constants";

class GameHistoryRecordPlayerAttributeAlteration {
@Expose()
public name: PlayerAttributeName;

@Expose()
public source: GameSource;

@Expose()
public playerName: string;

@Expose()
public status: GameHistoryRecordPlayerAttributeAlterationStatus;

public static create(gameHistoryRecordPlayerAttributeAlteration: GameHistoryRecordPlayerAttributeAlteration): GameHistoryRecordPlayerAttributeAlteration {
return plainToInstance(GameHistoryRecordPlayerAttributeAlteration, gameHistoryRecordPlayerAttributeAlteration, DEFAULT_PLAIN_TO_INSTANCE_OPTIONS);
}
}

export { GameHistoryRecordPlayerAttributeAlteration };
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type { TupleToUnion } from "type-fest";
import type { GAME_HISTORY_RECORD_PLAYER_ATTRIBUTE_ALTERATION_STATUSES } from "~/composables/api/game/constants/game-history-record/game-history-record.constants";

type GameHistoryRecordPlayerAttributeAlterationStatus = TupleToUnion<typeof GAME_HISTORY_RECORD_PLAYER_ATTRIBUTE_ALTERATION_STATUSES>;

export type { GameHistoryRecordPlayerAttributeAlterationStatus };
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Expose, plainToInstance, Type } from "class-transformer";

import { GameHistoryRecordPlay } from "~/composables/api/game/types/game-history-record/game-history-record-play/game-history-record-play.class";
import { GameHistoryRecordPlayerAttributeAlteration } from "~/composables/api/game/types/game-history-record/game-history-record-player-attribute-alteration/game-history-record-player-attribute-alteration.class";
import { GamePhase } from "~/composables/api/game/types/game-phase/game-phase.class";
import { Player } from "~/composables/api/game/types/players/player.class";
import { DEFAULT_PLAIN_TO_INSTANCE_OPTIONS } from "~/utils/constants/class-transformer.constants";
Expand Down Expand Up @@ -34,6 +35,10 @@ class GameHistoryRecord {
@Type(() => Player)
public deadPlayers?: Player[];

@Expose()
@Type(() => GameHistoryRecordPlayerAttributeAlteration)
public playerAttributeAlterations?: GameHistoryRecordPlayerAttributeAlteration[];

@Expose()
public createdAt: Date;

Expand Down
1 change: 1 addition & 0 deletions app/stores/audio/constants/audio.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const SOUND_EFFECT_NAMES = [
"raven-flying-away",
"supernatural-mood",
"sword",
"thunder",
"time-is-up",
"trumpet-fanfare",
"werewolf-howling",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const GAME_EVENT_TYPES = [
"wolf-hound-has-chosen-side",
"cupid-has-charmed",
"idiot-is-spared",
"elder-has-taken-revenge",
] as const;

export { GAME_EVENT_TYPES };
Loading

0 comments on commit 25f46d5

Please sign in to comment.