diff --git a/backend/rorapp/functions/enemy_leader_helper.py b/backend/rorapp/functions/enemy_leader_helper.py index 90b2d32d..ac7b4081 100644 --- a/backend/rorapp/functions/enemy_leader_helper.py +++ b/backend/rorapp/functions/enemy_leader_helper.py @@ -3,8 +3,8 @@ from typing import List, Tuple from django.conf import settings from rorapp.functions.websocket_message_helper import create_websocket_message -from rorapp.models import EnemyLeader, Faction, Game, War -from rorapp.serializers import EnemyLeaderSerializer +from rorapp.models import ActionLog, EnemyLeader, Faction, Game, Step, War +from rorapp.serializers import ActionLogSerializer, EnemyLeaderSerializer, WarSerializer def create_new_enemy_leader( @@ -61,7 +61,33 @@ def create_new_enemy_leader( ) ) - # TODO: Add action log for new enemy leader, which could also reference the matching/activated war + # Create action log for new leader + action_log_index = ( + ActionLog.objects.filter(step__phase__turn__game=game.id) + .order_by("index") + .last() + .index + + 1 + ) + latest_step = ( + Step.objects.filter(phase__turn__game=game_id).order_by("-index").first() + ) + action_log_data = { + "enemy_leader": enemy_leader.id, + "matching_war": matching_war.id, + "activated_the_war": len(activated_war_message) > 0, + "initiating_faction": faction.id, + } + action_log = ActionLog( + index=action_log_index, + step=latest_step, + type="new_enemy_leader", + data=action_log_data, + ) + action_log.save() + messages_to_send.append( + create_websocket_message("action_log", ActionLogSerializer(action_log).data) + ) return messages_to_send @@ -87,5 +113,8 @@ def get_and_activate_matching_war(game: Game, war_name: int) -> Tuple[War, List[ war = matching_wars.first() war.status = "active" war.save() + messages_to_send.append( + create_websocket_message("war", WarSerializer(war).data) + ) return war, messages_to_send diff --git a/backend/rorapp/models/enemy_leader.py b/backend/rorapp/models/enemy_leader.py index ccff6b6d..4ceab469 100644 --- a/backend/rorapp/models/enemy_leader.py +++ b/backend/rorapp/models/enemy_leader.py @@ -1,5 +1,6 @@ from django.db import models from rorapp.models.game import Game +from rorapp.models.war import War # Model for representing enemy leaders @@ -10,5 +11,5 @@ class EnemyLeader(models.Model): disaster_number = models.IntegerField() standoff_number = models.IntegerField() war_name = models.CharField(max_length=10) - current_war = models.ForeignKey("War", on_delete=models.CASCADE) + current_war = models.ForeignKey(War, on_delete=models.CASCADE) dead = models.BooleanField() diff --git a/backend/rorapp/urls.py b/backend/rorapp/urls.py index dc8d70df..994b1961 100644 --- a/backend/rorapp/urls.py +++ b/backend/rorapp/urls.py @@ -20,6 +20,7 @@ router.register('users', views.UserViewSet, basename='user') router.register('waitlist-entries', views.WaitlistEntryViewSet, basename='waitlist-entry') router.register('wars', views.WarViewSet, basename='war') +router.register('enemy-leaders', views.EnemyLeaderViewSet, basename='enemy-leader') app_name = "rorapp" diff --git a/backend/rorapp/views/__init__.py b/backend/rorapp/views/__init__.py index 266c1134..a4c3c6c5 100644 --- a/backend/rorapp/views/__init__.py +++ b/backend/rorapp/views/__init__.py @@ -1,11 +1,12 @@ # Package used to group the view scripts +from .action import ActionViewSet # noqa: F401 +from .action_log import ActionLogViewSet # noqa: F401 +from .enemy_leader import EnemyLeaderViewSet # noqa: F401 from .faction import FactionViewSet # noqa: F401 from .game import GameViewSet # noqa: F401 from .index import index # noqa: F401 -from .action_log import ActionLogViewSet # noqa: F401 -from .player import PlayerViewSet # noqa: F401 from .phase import PhaseViewSet # noqa: F401 -from .action import ActionViewSet # noqa: F401 +from .player import PlayerViewSet # noqa: F401 from .secret import SecretPrivateViewSet, SecretPublicViewSet # noqa: F401 from .senator import SenatorViewSet # noqa: F401 from .senator_action_log import SenatorActionLogViewSet # noqa: F401 diff --git a/backend/rorapp/views/enemy_leader.py b/backend/rorapp/views/enemy_leader.py new file mode 100644 index 00000000..221afcda --- /dev/null +++ b/backend/rorapp/views/enemy_leader.py @@ -0,0 +1,23 @@ +from rest_framework import viewsets +from rest_framework.permissions import IsAuthenticated +from rorapp.models import EnemyLeader +from rorapp.serializers import EnemyLeaderSerializer + + +class EnemyLeaderViewSet(viewsets.ReadOnlyModelViewSet): + """ + Read enemy leaders. + """ + + permission_classes = [IsAuthenticated] + serializer_class = EnemyLeaderSerializer + + def get_queryset(self): + queryset = EnemyLeader.objects.all() + + # Filter against a `game` query parameter in the URL + game_id = self.request.query_params.get("game", None) + if game_id is not None: + queryset = queryset.filter(game__id=game_id) + + return queryset diff --git a/frontend/classes/EnemyLeader.ts b/frontend/classes/EnemyLeader.ts new file mode 100644 index 00000000..ad399765 --- /dev/null +++ b/frontend/classes/EnemyLeader.ts @@ -0,0 +1,37 @@ +interface IEnemyLeader { + id: number + name: string + game: number + strength: number + disaster_number: number + standoff_number: number + war_name: string + current_war: number + dead: boolean +} + +class EnemyLeader { + id: number + name: string + game: number + strength: number + disasterNumber: number + standoffNumber: number + warName: string + currentWar: number + dead: boolean + + constructor(data: IEnemyLeader) { + this.id = data.id + this.name = data.name + this.game = data.game + this.strength = data.strength + this.disasterNumber = data.disaster_number + this.standoffNumber = data.standoff_number + this.warName = data.war_name + this.currentWar = data.current_war + this.dead = data.dead + } +} + +export default EnemyLeader \ No newline at end of file diff --git a/frontend/components/DetailSection.tsx b/frontend/components/DetailSection.tsx index 076d5a86..d7813cb5 100644 --- a/frontend/components/DetailSection.tsx +++ b/frontend/components/DetailSection.tsx @@ -9,7 +9,7 @@ import FactionDetailSection from "@/components/entityDetails/EntityDetail_Factio import { useGameContext } from "@/contexts/GameContext" import HraoTerm from "@/components/terms/Term_Hrao" import RomeConsulTerm from "@/components/terms/Term_RomeConsul" -import SelectedDetail from "@/types/selectedDetail" +import SelectedDetail from "@/types/SelectedDetail" import PriorConsulTerm from "@/components/terms/Term_PriorConsul" import FactionTerm from "@/components/terms/Term_Faction" import SecretTerm from "@/components/terms/Term_Secret" diff --git a/frontend/components/FactionIcon.tsx b/frontend/components/FactionIcon.tsx index 7908a6aa..d8cf7720 100644 --- a/frontend/components/FactionIcon.tsx +++ b/frontend/components/FactionIcon.tsx @@ -1,5 +1,5 @@ import Faction from "@/classes/Faction" -import SelectedDetail from "@/types/selectedDetail" +import SelectedDetail from "@/types/SelectedDetail" import { useGameContext } from "@/contexts/GameContext" interface FactionIconProps { diff --git a/frontend/components/FactionLink.tsx b/frontend/components/FactionLink.tsx index d5611434..7ec58a79 100644 --- a/frontend/components/FactionLink.tsx +++ b/frontend/components/FactionLink.tsx @@ -2,7 +2,7 @@ import { Link } from "@mui/material" import { useGameContext } from "@/contexts/GameContext" import Faction from "@/classes/Faction" -import SelectedDetail from "@/types/selectedDetail" +import SelectedDetail from "@/types/SelectedDetail" import FactionIcon from "@/components/FactionIcon" interface FactionLinkProps { diff --git a/frontend/components/GamePage.tsx b/frontend/components/GamePage.tsx index b0720641..04dc1e1f 100644 --- a/frontend/components/GamePage.tsx +++ b/frontend/components/GamePage.tsx @@ -42,6 +42,7 @@ import SenatorActionLog from "@/classes/SenatorActionLog" import Secret from "@/classes/Secret" import War from "@/classes/War" import WarfareTab from "@/components/WarfareTab" +import EnemyLeader from "@/classes/EnemyLeader" const webSocketURL: string = process.env.NEXT_PUBLIC_WS_URL ?? "" @@ -87,6 +88,7 @@ const GamePage = (props: GamePageProps) => { setNotifications, setAllSecrets, setWars, + setEnemyLeaders, } = useGameContext() const [latestActions, setLatestActions] = useState>( new Collection() @@ -252,6 +254,11 @@ const GamePage = (props: GamePageProps) => { fetchAndSetCollection(War, setWars, url) }, [props.gameId, setWars, fetchAndSetCollection]) + const fetchEnemyLeaders = useCallback(async () => { + const url = `enemy-leaders/?game=${props.gameId}` + fetchAndSetCollection(EnemyLeader, setEnemyLeaders, url) + }, [props.gameId, setEnemyLeaders, fetchAndSetCollection]) + const fetchTitles = useCallback(async () => { const url = `titles/?game=${props.gameId}&relevant` fetchAndSetCollection(Title, setAllTitles, url) @@ -403,6 +410,7 @@ const GamePage = (props: GamePageProps) => { fetchNotifications(), fetchSecrets(), fetchWars(), + fetchEnemyLeaders(), ] const results = await Promise.all(requestsBatch1) const updatedLatestStep: Step | null = results[0] as Step | null @@ -434,6 +442,7 @@ const GamePage = (props: GamePageProps) => { fetchNotifications, fetchSecrets, fetchWars, + fetchEnemyLeaders, ]) // Function to handle instance updates @@ -513,6 +522,7 @@ const GamePage = (props: GamePageProps) => { handleCollectionUpdate, ], war: [setWars, War, handleCollectionUpdate], + enemy_leader: [setEnemyLeaders, EnemyLeader, handleCollectionUpdate], }), [ handleCollectionUpdate, @@ -527,6 +537,7 @@ const GamePage = (props: GamePageProps) => { setNotifications, setSenatorActionLogs, setWars, + setEnemyLeaders, ] ) @@ -555,16 +566,6 @@ const GamePage = (props: GamePageProps) => { lastMessage, game?.id, classUpdateMap, - setLatestTurn, - setLatestPhase, - setLatestStep, - setLatestActions, - setAllFactions, - setAllTitles, - setAllSenators, - setNotifications, - setActionLogs, - setSenatorActionLogs, ]) // Remove old actions (i.e. actions from a step that is no longer the latest step) diff --git a/frontend/components/MetaSection.tsx b/frontend/components/MetaSection.tsx index a83be3e8..6117a4a1 100644 --- a/frontend/components/MetaSection.tsx +++ b/frontend/components/MetaSection.tsx @@ -20,7 +20,7 @@ import VotesIcon from "@/images/icons/votes.svg" import SecretsIcon from "@/images/icons/secrets.svg" import AttributeFlex, { Attribute } from "@/components/AttributeFlex" import Collection from "@/classes/Collection" -import SelectedDetail from "@/types/selectedDetail" +import SelectedDetail from "@/types/SelectedDetail" // Section showing meta info about the game const MetaSection = () => { diff --git a/frontend/components/ProgressSection.tsx b/frontend/components/ProgressSection.tsx index 4ad5377c..e55ff26d 100644 --- a/frontend/components/ProgressSection.tsx +++ b/frontend/components/ProgressSection.tsx @@ -4,17 +4,17 @@ import EastIcon from "@mui/icons-material/East" import Collection from "@/classes/Collection" import Action from "@/classes/Action" -import Actions from "@/data/actions.json" +import ActionDataCollection from "@/data/actions.json" import FactionIcon from "@/components/FactionIcon" import { useGameContext } from "@/contexts/GameContext" import { useAuthContext } from "@/contexts/AuthContext" import ActionDialog from "@/components/actionDialogs/ActionDialog" -import ActionsType from "@/types/actions" +import ActionDataCollectionType from "@/types/Action" import Faction from "@/classes/Faction" import FactionLink from "@/components/FactionLink" import NotificationList from "@/components/NotificationList" -const typedActions: ActionsType = Actions +const typedActionDataCollection: ActionDataCollectionType = ActionDataCollection const SEQUENTIAL_PHASES = ["Forum"] @@ -64,7 +64,7 @@ const ProgressSection = ({ latestActions }: ProgressSectionProps) => { waitingForDesc = ( Waiting for {pendingActions.length} factions to{" "} - {typedActions[firstPotentialAction.type]["sentence"]} + {typedActionDataCollection[firstPotentialAction.type]["sentence"]} ) } else if (pendingActions.length === 1) { @@ -80,7 +80,7 @@ const ProgressSection = ({ latestActions }: ProgressSectionProps) => { ) : ( )}{" "} - to {typedActions[firstPotentialAction.type]["sentence"]} + to {typedActionDataCollection[firstPotentialAction.type]["sentence"]} ) } @@ -131,7 +131,7 @@ const ProgressSection = ({ latestActions }: ProgressSectionProps) => { {thisFactionsPendingActions.allIds.length > 0 && requiredAction ? (
{ - const { wars } = useGameContext() + const { wars, enemyLeaders } = useGameContext() const getWarStatus = (war: War) => { switch (war.status) { @@ -92,6 +91,13 @@ const WarfareTab = () => { ))} +
    + {enemyLeaders.asArray.map((leader) => ( +
  • + {leader.name} +
  • + ))} +
) } diff --git a/frontend/components/actionLogs/ActionLog.tsx b/frontend/components/actionLogs/ActionLog.tsx index fa4d5119..218e0dbb 100644 --- a/frontend/components/actionLogs/ActionLog.tsx +++ b/frontend/components/actionLogs/ActionLog.tsx @@ -6,6 +6,7 @@ import NewTurnNotification from "./ActionLog_NewTurn" import NewFamilyNotification from "./ActionLog_NewFamily" import NewWarNotification from "./ActionLog_NewWar" import MatchedWarNotification from "./ActionLog_MatchedWar" +import NewEnemyLeaderNotification from "./ActionLog_NewEnemyLeader" interface NotificationItemProps { notification: ActionLog @@ -20,6 +21,7 @@ const notifications: { [key: string]: React.ComponentType } = { new_war: NewWarNotification, select_faction_leader: SelectFactionLeaderNotification, temporary_rome_consul: TemporaryRomeConsulNotification, + new_enemy_leader: NewEnemyLeaderNotification, } // Container for a notification, which determines the type of notification to render diff --git a/frontend/components/actionLogs/ActionLog_MatchedWar.tsx b/frontend/components/actionLogs/ActionLog_MatchedWar.tsx index fb545830..870ce2e7 100644 --- a/frontend/components/actionLogs/ActionLog_MatchedWar.tsx +++ b/frontend/components/actionLogs/ActionLog_MatchedWar.tsx @@ -12,7 +12,7 @@ interface NotificationProps { // Notification for when an existing war is matched during the forum phase const MatchedWarNotification = ({ notification }: NotificationProps) => { - const { allFactions, wars } = useGameContext() + const { wars } = useGameContext() // Get notification-specific data const war: War | null = notification.data diff --git a/frontend/components/actionLogs/ActionLog_NewEnemyLeader.tsx b/frontend/components/actionLogs/ActionLog_NewEnemyLeader.tsx new file mode 100644 index 00000000..7c77196f --- /dev/null +++ b/frontend/components/actionLogs/ActionLog_NewEnemyLeader.tsx @@ -0,0 +1,61 @@ +import Image from "next/image" +import { Alert } from "@mui/material" +import MilitaryIcon from "@/images/icons/military.svg" +import ActionLog from "@/classes/ActionLog" +import { useGameContext } from "@/contexts/GameContext" +import War from "@/classes/War" +import EnemyLeader from "@/classes/EnemyLeader" +import Faction from "@/classes/Faction" +import EnemyLeaderDataCollection from "@/data/enemyLeaders.json" +import EnemyLeaderDataCollectionType from "@/types/EnemyLeader" +import FactionLink from "@/components/FactionLink" + +const typedEnemyLeaderDataCollection: EnemyLeaderDataCollectionType = + EnemyLeaderDataCollection + +interface NotificationProps { + notification: ActionLog +} + +// Notification for when a new war appears during the forum phase +const NewEnemyLeaderNotification = ({ notification }: NotificationProps) => { + const { allFactions, enemyLeaders, wars } = useGameContext() + + // Get notification-specific data + const enemyLeader: EnemyLeader | null = notification.data + ? enemyLeaders.byId[notification.data.enemy_leader] ?? null + : null + const matching_war: War | null = notification.data + ? wars.byId[notification.data.matching_war] ?? null + : null + const activatedTheWar: boolean = notification.data + ? notification.data.activated_the_war + : false + const initiatingFaction: Faction | null = notification.data + ? allFactions.byId[notification.data.initiating_faction] ?? null + : null + + const getIcon = () => ( +
+ War Icon +
+ ) + + if (!enemyLeader || !initiatingFaction) return null + + return ( + + New Enemy Leader +

+ In {typedEnemyLeaderDataCollection[enemyLeader.name]["location"]}, a + talented commander named {enemyLeader.name} has{" "} + {typedEnemyLeaderDataCollection[enemyLeader.name]["new_description"]}.{" "} + + Situation initiated by + +

+
+ ) +} + +export default NewEnemyLeaderNotification diff --git a/frontend/components/actionLogs/ActionLog_NewWar.tsx b/frontend/components/actionLogs/ActionLog_NewWar.tsx index bb2d43c8..e6c9b991 100644 --- a/frontend/components/actionLogs/ActionLog_NewWar.tsx +++ b/frontend/components/actionLogs/ActionLog_NewWar.tsx @@ -20,15 +20,16 @@ const NewWarNotification = ({ notification }: NotificationProps) => { const newWar: War | null = notification.data ? wars.byId[notification.data.war] ?? null : null - const initiatingFaction: Faction | null = notification.data - ? allFactions.byId[notification.data.initiating_faction] ?? null - : null const initialStatus = notification.data ? notification.data.initial_status : null - const matchingWars = notification.data ? notification.data.matching_wars ?? [] : [] + const initiatingFaction: Faction | null = notification.data + ? allFactions.byId[notification.data.initiating_faction] ?? null + : null + const matchingWars = notification.data + ? notification.data.matching_wars ?? [] + : [] const isMatchedByMultiple = matchingWars.length > 1 - console.log(matchingWars.length) const getIcon = () => (
@@ -39,7 +40,9 @@ const NewWarNotification = ({ notification }: NotificationProps) => { const getStatusAndExplanation = () => { switch (initialStatus) { case "imminent": - return `Imminent due to ${isMatchedByMultiple ? "Matching Wars": "a Matching War"}` + return `Imminent due to ${ + isMatchedByMultiple ? "Matching Wars" : "a Matching War" + }` default: return capitalize(initialStatus) } diff --git a/frontend/contexts/GameContext.tsx b/frontend/contexts/GameContext.tsx index 3426e022..aea3a274 100644 --- a/frontend/contexts/GameContext.tsx +++ b/frontend/contexts/GameContext.tsx @@ -13,13 +13,14 @@ import Collection from "@/classes/Collection" import Title from "@/classes/Title" import Game from "@/classes/Game" import Step from "@/classes/Step" -import SelectedDetail from "@/types/selectedDetail" +import SelectedDetail from "@/types/SelectedDetail" import ActionLog from "@/classes/ActionLog" import SenatorActionLog from "@/classes/SenatorActionLog" import Phase from "@/classes/Phase" import Turn from "@/classes/Turn" import Secret from "@/classes/Secret" import War from "@/classes/War" +import EnemyLeader from "@/classes/EnemyLeader" interface GameContextType { game: Game | null @@ -56,6 +57,8 @@ interface GameContextType { setAllSecrets: Dispatch>> wars: Collection setWars: Dispatch>> + enemyLeaders: Collection + setEnemyLeaders: Dispatch>> } const GameContext = createContext(null) @@ -109,6 +112,9 @@ export const GameProvider = (props: GameProviderProps): JSX.Element => { new Collection() ) const [wars, setWars] = useState>(new Collection()) + const [enemyLeaders, setEnemyLeaders] = useState>( + new Collection() + ) return ( { setAllSecrets, wars, setWars, + enemyLeaders, + setEnemyLeaders, }} > {props.children} diff --git a/frontend/data/enemyLeaders.json b/frontend/data/enemyLeaders.json new file mode 100644 index 00000000..b420d1b1 --- /dev/null +++ b/frontend/data/enemyLeaders.json @@ -0,0 +1,22 @@ +{ + "Hamilcar": { + "title": "General", + "location": "Carthage", + "new_description": "risen to prominence" + }, + "Hannibal": { + "title": "General", + "location": "Carthage", + "new_description": "risen to prominence" + }, + "Antiochus III": { + "title": "King", + "location": "the Seleucid Empire", + "new_description": "inherited the throne" + }, + "Philip V": { + "title": "King", + "location": "Macedonia", + "new_description": "inherited the throne" + } +} diff --git a/frontend/types/actions.ts b/frontend/types/Action.ts similarity index 51% rename from frontend/types/actions.ts rename to frontend/types/Action.ts index e232822c..9b207568 100644 --- a/frontend/types/actions.ts +++ b/frontend/types/Action.ts @@ -1,10 +1,10 @@ // JSON action data is typed so that it can be used in TypeScript -interface ActionType { +interface ActionData { sentence: string title: string } -export default interface ActionsType { - [key: string]: ActionType +export default interface ActionDataCollection { + [key: string]: ActionData } diff --git a/frontend/types/EnemyLeader.ts b/frontend/types/EnemyLeader.ts new file mode 100644 index 00000000..a5963b02 --- /dev/null +++ b/frontend/types/EnemyLeader.ts @@ -0,0 +1,11 @@ +// JSON enemy leader data is typed so that it can be used in TypeScript + +interface EnemyLeaderData { + title: string + location: string + new_description: string +} + +export default interface EnemyLeaderDataCollection { + [key: string]: EnemyLeaderData +}