Skip to content

Commit

Permalink
Merge pull request #736 from mafia-rust/generic_ability
Browse files Browse the repository at this point in the history
Generic ability
  • Loading branch information
ItsSammyM authored Dec 6, 2024
2 parents f24058f + e477141 commit b54d500
Show file tree
Hide file tree
Showing 132 changed files with 4,714 additions and 2,149 deletions.
19 changes: 13 additions & 6 deletions client/src/ListMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,28 @@ export type ListMapData<K, V> = [K, V][]

export default class ListMap<K, V> {
list: ListMapData<K, V>;
constructor(list: ListMapData<K, V> = []) {
this.list = list
equalsFn: ((k1: K, k2: K)=>boolean);
constructor(
list: ListMapData<K, V> = [],
equalsFn: ((k1: K, k2: K)=>boolean) = (k1, k2)=>k1 === k2
) {
this.list = list;
this.equalsFn = equalsFn;
}
get(key: K): V | null {
for (const [k, v] of this.list) {
if (k === key) {
if (this.equalsFn(k, key)) {
return v
}
}
return null
}
set(key: K, value: V) {
this.list = this.list.filter(([k, v]) => k !== key);
insert(key: K, value: V) {
this.list = this.list.filter(([k, v]) => !this.equalsFn(k, key));
this.list.push([key, value])
}
delete(key: K) {
this.list = this.list.filter(([k, v]) => k !== key)
this.list = this.list.filter(([k, v]) => !this.equalsFn(k, key))
}
entries(): ListMapData<K, V> {
return this.list
Expand All @@ -29,4 +34,6 @@ export default class ListMap<K, V> {
values(): V[] {
return this.list.map(([k, v]) => v)
}


}
191 changes: 127 additions & 64 deletions client/src/components/ChatMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@ import DOMPurify from "dompurify";
import GraveComponent from "./grave";
import { RoleOutline, translateRoleOutline } from "../game/roleListState.d";
import { CopyButton } from "./ClipboardButtons";
import { useLobbyOrGameState, usePlayerState } from "./useHooks";
import { KiraGuess, KiraGuessResult, kiraGuessTranslate } from "../menu/game/gameScreenContent/AbilityMenu/RoleSpecificMenus/KiraMenu";
import { useGameState, useLobbyOrGameState, usePlayerState } from "./useHooks";
import { KiraResult, KiraResultDisplay } from "../menu/game/gameScreenContent/AbilityMenu/AbilitySelectionTypes/KiraSelectionMenu";
import { AuditorResult } from "../menu/game/gameScreenContent/AbilityMenu/RoleSpecificMenus/AuditorMenu";
import { PuppeteerAction } from "../menu/game/gameScreenContent/AbilityMenu/RoleSpecificMenus/SmallPuppeteerMenu";
import { RecruiterAction } from "../menu/game/gameScreenContent/AbilityMenu/RoleSpecificMenus/RecruiterMenu";
import { ControllerID, AbilitySelection, translateControllerID } from "../game/abilityInput";
import DetailsSummary from "./DetailsSummary";

const ChatElement = React.memo((
Expand All @@ -29,6 +30,11 @@ const ChatElement = React.memo((
playerState => playerState.roleState,
["yourRoleState"]
);

const roleList = useGameState(
state => state.roleList,
["roleList"]
);

const [mouseHovering, setMouseHovering] = React.useState(false);

Expand Down Expand Up @@ -80,59 +86,52 @@ const ChatElement = React.memo((
<StyledText className={"chat-message " + style}
playerKeywordData={props.playerKeywordData}
>
{(chatGroupIcon??"")} {translateChatMessage(message.variant, playerNames)}
{(chatGroupIcon??"")} {translateChatMessage(message.variant, playerNames, roleList)}
</StyledText>
<ChatElement {...props} message={{
variant: message.variant.message,
chatGroup: message.chatGroup,
}}/>
</span>
</div>
case "kiraResult":
let out = [];

let sortedPlayerIndexes = Object.keys(message.variant.result.guesses).map((k)=>{return Number.parseInt(k)}).sort();

for(let playerIndex of sortedPlayerIndexes){
let resultStyle = "";
let resultIcon = "";
let resultString = "";

if(message.variant.result.guesses[playerIndex][1] === "correct"){
resultStyle = "correct";
resultIcon = "🟩";
resultString = translate("kiraResult.correct");
}else if(message.variant.result.guesses[playerIndex][1] === "wrongSpot"){
resultStyle = "wrongSpot";
resultIcon = "🟨";
resultString = translate("kiraResult.wrongSpot");
}else if(message.variant.result.guesses[playerIndex][1] === "notInGame"){
resultStyle = "notInGame";
resultIcon = "🟥";
resultString = translate("kiraResult.notInGame");
}

if(message.variant.result.guesses[playerIndex][0] === "none"){
resultStyle = "";
resultIcon = "";
resultString = "";
}

out.push(<div key={playerIndex} className={"kira-guess-result "+resultStyle}>
<StyledText
playerKeywordData={props.playerKeywordData}
>
{playerNames[playerIndex]} {kiraGuessTranslate(message.variant.result.guesses[playerIndex][0])} {resultIcon} {resultString}
</StyledText>
</div>)
case "reporterReport":
style += " block";
break;
case "abilityUsed":
switch (message.variant.selection.type){
case "kira":
return <div className={"chat-message-div chat-message kira-guess-results " + style}>
<StyledText
className="chat-message result"
playerKeywordData={props.playerKeywordData}
>{chatGroupIcon ?? ""} {translate("chatMessage.kiraSelection")}</StyledText>
<KiraResultDisplay
map={{
type: "selection",
map: message.variant.selection.selection
}}
playerKeywordData={props.playerKeywordData}
playerNames={playerNames}
/>
</div>
case "string":
style += " block"
}

break;
case "kiraResult":
return <div className={"chat-message-div chat-message kira-guess-results " + style}>
<StyledText
className="chat-message result"
playerKeywordData={props.playerKeywordData}
>{chatGroupIcon ?? ""} {translate("chatMessage.kiraResult")}</StyledText>
{out}
<KiraResultDisplay
map={{
type: "reuslt",
map: message.variant.result.guesses
}}
playerKeywordData={props.playerKeywordData}
playerNames={playerNames}
/>
</div>
case "playerDied":

Expand Down Expand Up @@ -172,13 +171,13 @@ const ChatElement = React.memo((
onMouseOut={() => setMouseHovering(false)}
>
<StyledText className={"chat-message " + style} playerKeywordData={props.playerKeywordData}>
{(chatGroupIcon??"")} {translateChatMessage(message.variant, playerNames)}
{(chatGroupIcon??"")} {translateChatMessage(message.variant, playerNames, roleList)}
</StyledText>
{
mouseHovering && ( roleState?.type === "forger" || roleState?.type === "counterfeiter")
&& <CopyButton
className="chat-message-div-copy-button"
text={translateChatMessage(message.variant, playerNames)}
text={translateChatMessage(message.variant, playerNames, roleList)}
/>
}
</div>;
Expand Down Expand Up @@ -264,7 +263,7 @@ function NormalChatMessage(props: Readonly<{
<StyledText
playerKeywordData={props.playerKeywordData}
>
{translateChatMessage(props.message.variant, props.playerNames)}
{translateChatMessage(props.message.variant, props.playerNames, undefined)}
</StyledText>
</span>
{
Expand Down Expand Up @@ -327,7 +326,11 @@ export function sanitizePlayerMessage(text: string): string {
});
}

export function translateChatMessage(message: ChatMessageVariant, playerNames?: string[]): string {
export function translateChatMessage(
message: ChatMessageVariant,
playerNames?: string[],
roleList?: RoleOutline[]
): string {

if (playerNames === undefined) {
playerNames = GAME_MANAGER.getPlayerNames();
Expand Down Expand Up @@ -456,6 +459,77 @@ export function translateChatMessage(message: ChatMessageVariant, playerNames?:
playerNames[message.targeter],
);
}
case "abilityUsed":

let out;

switch (message.selection.type) {
case "unit":
out = translate("chatMessage.abilityUsed.selection.unit");
break;
case "boolean":
if(message.selection.selection){
out = " "+translate("on");
}else{
out = " "+translate("off");
}
break;
case "onePlayerOption":
out = translate("chatMessage.abilityUsed.selection.onePlayerOption",
message.selection.selection===null?translate("nobody"):playerNames[message.selection.selection],
);
break;
case "twoPlayerOption":
out = translate("chatMessage.abilityUsed.selection.twoPlayerOption",
message.selection.selection[0]===null?translate("nobody"):playerNames[message.selection.selection[0]],
message.selection.selection[1]===null?translate("nobody"):playerNames[message.selection.selection[1]],
);
break;
case "threePlayerOption":
out = translate("chatMessage.abilityUsed.selection.threePlayerOption",
message.selection.selection[0]===null?translate("nobody"):playerNames[message.selection.selection[0]],
message.selection.selection[1]===null?translate("nobody"):playerNames[message.selection.selection[1]],
message.selection.selection[2]===null?translate("nobody"):playerNames[message.selection.selection[2]],
);
break;
case "roleOption":
out = translate("chatMessage.abilityUsed.selection.roleOption",
message.selection.selection===null?translate("none"):translate("role."+message.selection.selection+".name")
);
break;
case "twoRoleOption":
out = translate("chatMessage.abilityUsed.selection.twoRoleOption",
message.selection.selection[0]===null?translate("none"):translate("role."+message.selection.selection[0]+".name"),
message.selection.selection[1]===null?translate("none"):translate("role."+message.selection.selection[1]+".name"),
);
break;
case "twoRoleOutlineOption":
let first = message.selection.selection[0] === null ?
translate("none") :
roleList === undefined ?
message.selection.selection[0].toString() :
translateRoleOutline(roleList[message.selection.selection[0]]);

let second = message.selection.selection[1] === null ?
translate("none") :
roleList === undefined ?
message.selection.selection[1].toString() :
translateRoleOutline(roleList[message.selection.selection[1]]);



out = translate("chatMessage.abilityUsed.selection.twoRoleOutlineOption", first, second);
break;
case "string":
out = translate("chatMessage.abilityUsed.selection.string", sanitizePlayerMessage(replaceMentions(message.selection.selection)));
break;
default:
out = "";
}

let abilityIdString = translateControllerID(message.abilityId);

return translate("chatMessage.abilityUsed", playerNames[message.player], abilityIdString, out);
case "mayorRevealed":
return translate("chatMessage.mayorRevealed",
playerNames[message.playerIndex],
Expand Down Expand Up @@ -614,11 +688,6 @@ export function translateChatMessage(message: ChatMessageVariant, playerNames?:
return translate("chatMessage.puppeteerActionChosen."+message.action);
case "recruiterActionChosen":
return translate("chatMessage.recruiterActionChosen."+message.action);
case "marksmanChosenMarks":
if(message.marks.length === 0){
return translate("chatMessage.marksmanChosenMarks.none");
}
return translate("chatMessage.marksmanChosenMarks", playerListToString(message.marks, playerNames));
case "silenced":
return translate("chatMessage.silenced");
case "mediumHauntStarted":
Expand All @@ -645,11 +714,6 @@ export function translateChatMessage(message: ChatMessageVariant, playerNames?:
return translate("chatMessage.youAreLoveLinked", playerNames[message.player]);
case "playerDiedOfABrokenHeart":
return translate("chatMessage.playerDiedOfBrokenHeart", playerNames[message.player], playerNames[message.lover]);
case "syndicateGunTarget":
return translate("chatMessage.syndicateGunTarget",
playerNames[message.shooter],
message.target===null?translate("nobody"):playerNames[message.target]
);
case "chronokaiserSpeedUp":
return translate("chatMessage.chronokaiserSpeedUp", message.percent);
case "deputyShotYou":
Expand Down Expand Up @@ -781,6 +845,12 @@ export type ChatMessageVariant = {
type: "targeted",
targeter: PlayerIndex,
targets: PlayerIndex[]
} | {
type: "abilityUsed",
player: PlayerIndex,
abilityId: ControllerID,
selection: AbilitySelection

} | {
type: "phaseFastForwarded"
} |
Expand Down Expand Up @@ -919,10 +989,6 @@ export type ChatMessageVariant = {
type: "playerDiedOfABrokenHeart",
player: PlayerIndex
lover: PlayerIndex
} | {
type: "syndicateGunTarget",
shooter: PlayerIndex
target: PlayerIndex | null
} | {
type: "youWereProtected"
} | {
Expand Down Expand Up @@ -963,9 +1029,6 @@ export type ChatMessageVariant = {
} | {
type: "recruiterActionChosen",
action: RecruiterAction,
} | {
type: "marksmanChosenMarks",
marks: PlayerIndex[],
} | {
type: "targetIsPossessionImmune"
} | {
Expand Down Expand Up @@ -1000,7 +1063,7 @@ export type ChatMessageVariant = {
} | {
type: "kiraResult",
result: {
guesses: Record<PlayerIndex, [KiraGuess, KiraGuessResult]>
guesses: KiraResult
}
} | {
type: "martyrFailed"
Expand Down
4 changes: 3 additions & 1 deletion client/src/components/DetailsSummary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ export default function DetailsSummary(props: Readonly<{
open?: boolean,
disabled?: boolean,
onClick?: () => void
className?: string
summaryClassName?: string
}>): ReactElement {

const [openState, setOpen] = React.useState(props.defaultOpen??false);
Expand All @@ -21,7 +23,7 @@ export default function DetailsSummary(props: Readonly<{
return openState;
}, [props.open, openState, props.disabled]);

return <div className="details-summary-container">
return <div className={"details-summary-container "+(props.className??"")}>
<div className="details-summary-summary-container"
onClick={() => {
if(props.disabled) return;
Expand Down
Loading

0 comments on commit b54d500

Please sign in to comment.