diff --git a/.vscode/settings.json b/.vscode/settings.json index c93d2779a..aca913d3d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -46,6 +46,7 @@ "Thanos", "Tidyable", "tungstenite", + "uncategorized", "unclicked", "Unswappable", "Uzumaki", diff --git a/client/package-lock.json b/client/package-lock.json index 8b1f69227..d002fec18 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -22,6 +22,7 @@ "react-dom": "^18.2.0", "react-helmet": "^6.1.0", "react-resizable-panels": "^2.1.6", + "react-responsive-masonry": "^2.6.0", "react-scripts": "^5.0.1", "react-virtuoso": "^4.12.0", "typescript": "^4", @@ -29,7 +30,8 @@ }, "devDependencies": { "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", - "@types/react-helmet": "^6.1.11" + "@types/react-helmet": "^6.1.11", + "@types/react-responsive-masonry": "^2.1.3" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -4310,6 +4312,15 @@ "@types/react": "*" } }, + "node_modules/@types/react-responsive-masonry": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@types/react-responsive-masonry/-/react-responsive-masonry-2.1.3.tgz", + "integrity": "sha512-aOFUtv3QwNMmy0BgpQpvivQ/+vivMTB6ARrzf9eTSXsLzXpVnfEtjpHpSknYDnr8KaQmlgeauAj8E7wo/qMOTg==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/resolve": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", @@ -14616,6 +14627,11 @@ "react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, + "node_modules/react-responsive-masonry": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/react-responsive-masonry/-/react-responsive-masonry-2.6.0.tgz", + "integrity": "sha512-aD1NMUOoRHoL2PT6k4b/+MtH8ZbSLxk8nr6O04HVLXjy7hFRrVXcHNTHscuTtBC70w0hEsJTATHHUxToQsY3PA==" + }, "node_modules/react-scripts": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz", @@ -20917,6 +20933,15 @@ "@types/react": "*" } }, + "@types/react-responsive-masonry": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@types/react-responsive-masonry/-/react-responsive-masonry-2.1.3.tgz", + "integrity": "sha512-aOFUtv3QwNMmy0BgpQpvivQ/+vivMTB6ARrzf9eTSXsLzXpVnfEtjpHpSknYDnr8KaQmlgeauAj8E7wo/qMOTg==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, "@types/resolve": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", @@ -28338,6 +28363,11 @@ "integrity": "sha512-oIqo/7pp2TsR+Dp1qZMr1l4RBDV4Zz/0HEG5zxliBJoHqqFnG0MbmFbk+5Q1VMGfPQ4uhXxefunLC1o7v38PDQ==", "requires": {} }, + "react-responsive-masonry": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/react-responsive-masonry/-/react-responsive-masonry-2.6.0.tgz", + "integrity": "sha512-aD1NMUOoRHoL2PT6k4b/+MtH8ZbSLxk8nr6O04HVLXjy7hFRrVXcHNTHscuTtBC70w0hEsJTATHHUxToQsY3PA==" + }, "react-scripts": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz", diff --git a/client/package.json b/client/package.json index 74fa3140e..19afc49b2 100644 --- a/client/package.json +++ b/client/package.json @@ -17,6 +17,7 @@ "react-dom": "^18.2.0", "react-helmet": "^6.1.0", "react-resizable-panels": "^2.1.6", + "react-responsive-masonry": "^2.6.0", "react-scripts": "^5.0.1", "react-virtuoso": "^4.12.0", "typescript": "^4", @@ -24,7 +25,8 @@ }, "devDependencies": { "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", - "@types/react-helmet": "^6.1.11" + "@types/react-helmet": "^6.1.11", + "@types/react-responsive-masonry": "^2.1.3" }, "scripts": { "start": "react-scripts start", diff --git a/client/src/components/Wiki.tsx b/client/src/components/Wiki.tsx index 7d958b2ea..538d819ea 100644 --- a/client/src/components/Wiki.tsx +++ b/client/src/components/Wiki.tsx @@ -1,15 +1,19 @@ -import React, { ReactElement, useCallback, useEffect, useState } from "react"; +import React, { ReactElement, useCallback, useEffect, useMemo, useRef, useState } from "react"; import translate from "../game/lang"; import "./wiki.css"; import { Role, getMainRoleSetFromRole } from "../game/roleState.d"; import GAME_MANAGER, { regEscape } from ".."; -import WikiArticle, { getSearchStrings } from "./WikiArticle"; -import { ARTICLES, WikiArticleLink, getArticleTitle } from "./WikiArticleLink"; +import WikiArticle, { getSearchStrings, PageCollection } from "./WikiArticle"; +import { ARTICLES, WikiArticleLink, getArticleTitle, wikiPageIsEnabled } from "./WikiArticleLink"; import StyledText from "./StyledText"; import Icon from "./Icon"; import { ContentMenu, MenuController } from "../menu/game/GameScreen"; import { AnchorController } from "../menu/Anchor"; import WikiCoverCard from "./WikiCoverCard"; +import { getAllRoles } from "../game/roleListState.d"; +import { useLobbyOrGameState } from "./useHooks"; +import { MODIFIERS, ModifierType } from "../game/gameState.d"; +import Masonry from "react-responsive-masonry"; export function setWikiSearchPage(page: WikiArticleLink, anchorController: AnchorController, menuController?: MenuController) { @@ -28,7 +32,8 @@ export function setWikiSearchPage(page: WikiArticleLink, anchorController: Ancho export default function Wiki(props: Readonly<{ - enabledRoles?: Role[], + enabledRoles: Role[], + enabledModifiers: ModifierType[], initialWikiPage?: WikiArticleLink, onPageChange?: (page: WikiArticleLink | null) => void, }>): ReactElement { @@ -76,10 +81,6 @@ export default function Wiki(props: Readonly<{ } } - - - - return
: @@ -101,12 +102,12 @@ export default function Wiki(props: Readonly<{
} -function WikiSearchBar(props: { +function WikiSearchBar(props: Readonly<{ searchQuery: string, onSearchChange: (search: string) => void, onBack: () => void, onClear: () => void, -}): ReactElement { +}>): ReactElement { return
} -function WikiSearchResults(props: { +function WikiSearchResults(props: Readonly<{ searchQuery: string, - article: WikiArticleLink | null, - enabledRoles?: Role[], + enabledRoles: Role[], + enabledModifiers: ModifierType[], onChooseArticle: (article: WikiArticleLink) => void -}): ReactElement { +}>): ReactElement { + const enabledRoles = useLobbyOrGameState( + gameState => gameState.enabledRoles, + ["enabledRoles"], + getAllRoles() + )!; + const enabledModifiers = useLobbyOrGameState( + gameState => gameState.enabledModifiers, + ["enabledRoles"], + MODIFIERS as any as ModifierType[] + )!; - function getSearchResults(search: string): WikiArticleLink[] { + const getSearchResults = useCallback((search: string) => { const out = [ ...ARTICLES.filter((page) => {return RegExp(regEscape(search.trim()), 'i').test(getArticleTitle(page))}), ...ARTICLES.filter((page) => {return getSearchStrings(page).some((str) => RegExp(regEscape(search.trim()), 'i').test(str))}) ]; return out.filter((item, index) => out.indexOf(item) === index); - } + }, []); - let searchResultsHtml = []; - const results = getSearchResults(props.searchQuery); - if (props.searchQuery === ""){ - let standardHeaderAdded = false; - let lastArticleRoleFaction = null; - for(let page of results){ + const results = useMemo(() => + getSearchResults(props.searchQuery), + [props.searchQuery, getSearchResults]) - let articleType = page.split("/")[0]; + return
+ {props.searchQuery === "" + ? + : results.map(page => { + let className = undefined; + if(( + page.includes("role/") && + !enabledRoles.map(role => `role/${role}`).includes(page) + ) || ( + page.includes("modifier/") && + !enabledModifiers.map(modifier => `modifier/${modifier}`).includes(page) + )) { + className = "keyword-disabled"; + } + + return props.onChooseArticle(page)} + /> + })} +
+} - if(!standardHeaderAdded && articleType === "standard"){ - searchResultsHtml.push(

{translate(articleType)}

); - standardHeaderAdded = true; - } +function WikiMainPage(props: Readonly<{ + articles: WikiArticleLink[], + enabledRoles: Role[], + enabledModifiers: ModifierType[] + onChooseArticle: (article: WikiArticleLink) => void +}>): ReactElement { + const articlePartitions = useMemo(() => + partitionWikiPages(props.articles, props.enabledRoles, props.enabledModifiers), + [props.articles, props.enabledRoles, props.enabledModifiers]); - if(articleType === "role"){ - const role = page.split("/")[1] as Role; - const faction = getMainRoleSetFromRole(role); + const ref = useRef(null); - if(faction !== lastArticleRoleFaction){ - searchResultsHtml.push(

{translate(faction)}

); - lastArticleRoleFaction = faction; - } - } + const [columnCount, setColumnCount] = useState(1); - let className = undefined; - if( - page.includes("role/") && - props.enabledRoles !== undefined && props.enabledRoles.length !== 0 && !props.enabledRoles.map(role => `role/${role}`).includes(page) - ) { - className = "keyword-disabled"; + useEffect(() => { + const redetermineColumnWidths = () => { + if (ref.current) { + setColumnCount(Math.max(Math.floor(ref.current.clientWidth / 300), 1)) } + } + + const resizeObserver = new ResizeObserver(redetermineColumnWidths) + + redetermineColumnWidths() + + setTimeout(() => { + resizeObserver.observe(ref.current!); + }) + return resizeObserver.unobserve(ref.current!) + }, [ref]) + + return
+ + {Object.entries(articlePartitions).filter(([category]) => category !== "uncategorized").map(([category, pages]) => { + return
+ +
+ })} +
+ +
+} + +export const WIKI_CATEGORIES = [ + "categories", "town", "mafia", "cult", "neutral", "minions", "fiends", "modifiers", "abilities", "strategies" +] as const; +export type WikiCategory = (typeof WIKI_CATEGORIES)[number] - searchResultsHtml.push( - props.onChooseArticle(page)}/> - ); +type WikiPagePartitions = Record; + +export function partitionWikiPages( + wikiPages: WikiArticleLink[], + enabledRoles: Role[], + enabledModifiers: ModifierType[] +): WikiPagePartitions { + const partitions: WikiPagePartitions = Object.fromEntries([ + ...WIKI_CATEGORIES.map(a => [a, []]), + ["uncategorized", []] + ]) as WikiPagePartitions; + + for (const wikiPage of wikiPages) { + const articleType = wikiPage.split("/")[0]; + + let category: WikiCategory | "uncategorized" | null = null; + + if (articleType === "role") { + const role = wikiPage.split("/")[1] as Role; + category = getCategoryForRole(role); + + } else if (articleType === "modifier") { + category = "modifiers" + } else if (articleType === "category") { + category = "categories" } - }else{ - for(let page of results){ - - let className = undefined; - if( - page.includes("role/") && - props.enabledRoles !== undefined && props.enabledRoles.length !== 0 && !props.enabledRoles.map(role => `role/${role}`).includes(page) - ) { - className = "keyword-disabled"; - } - searchResultsHtml.push( - props.onChooseArticle(page)}/> - ); + if (wikiPage === "standard/mafia") { + category = "mafia" + } else if (wikiPage === "standard/cult") { + category = "cult" + } + + if ([ + "standard/backup", "standard/block", "standard/convert", "standard/douse", "standard/forged", + "standard/frame", "standard/haunt", "standard/hypnotize", "standard/interview", "standard/jail", + "standard/loveLinked", "standard/marionette", "standard/obscured", "standard/possess", + "standard/protect", "standard/rampage", "standard/report", "standard/roleblock", "standard/silenced", + "standard/spiral", "standard/syndicateGunItem", "standard/transport", "standard/ward", + "standard/forfeitVote", "standard/aura", "standard/fastForward", "standard/appearedVisit", + "standard/defense", "standard/confused" + ].includes(wikiPage)) { + category = "abilities" } + + if ([ + "standard/claim", "standard/claimswap", "standard/vfr" + ].includes(wikiPage)) { + category = "strategies" + } + + if (category === null) { + category = "uncategorized" + } + + partitions[category].push(wikiPage) } - + const sortFunction = getWikiPageSortFunction(enabledRoles, enabledModifiers); - return
- {searchResultsHtml} -
+ for (const category of Object.keys(partitions) as WikiCategory[]) { + partitions[category].sort(sortFunction); + } + + return partitions; +} + +function getCategoryForRole(role: Role): WikiCategory { + return getMainRoleSetFromRole(role) as WikiCategory; +} + +function getWikiPageSortFunction( + enabledRole: Role[], + enabledModifiers: ModifierType[] +): (first: WikiArticleLink, second: WikiArticleLink) => number { + return (first, second) => wikiPageSortFunction(first, second, enabledRole, enabledModifiers) } -function WikiSearchResult(props: { +function wikiPageSortFunction( + first: WikiArticleLink, + second: WikiArticleLink, + enabledRoles: Role[], + enabledModifiers: ModifierType[] +): number { + const isPageEnabled = (page: WikiArticleLink) => wikiPageIsEnabled(page, enabledRoles, enabledModifiers); + + if (isPageEnabled(first) && !isPageEnabled(second)) { + return -1; + } else if (!isPageEnabled(first) && isPageEnabled(second)) { + return 1 + } else { + return getArticleTitle(first).localeCompare(getArticleTitle(second)) + } +} + +function WikiSearchResult(props: Readonly<{ page: WikiArticleLink, className?: string, onChooseArticle: (article: WikiArticleLink) => void -}): ReactElement { +}>): ReactElement { return + })} + +} + function GeneratedArticleElement(props: Readonly<{ article: GeneratedArticle }>): ReactElement { switch(props.article){ @@ -127,54 +188,65 @@ function GeneratedArticleElement(props: Readonly<{ article: GeneratedArticle }>) function RoleSetArticle(): ReactElement { const enabledRoles = useLobbyOrGameState( state => state.enabledRoles, - ["enabledRoles"] - ); - - let mainElements = [ -
- {"# "+translate("wiki.article.generated.roleSet.title")} -
- ]; - - for(let set of ROLE_SETS){ - let elements = getRolesFromRoleSet(set).map((role)=>{ + ["enabledRoles"], + getAllRoles() + )!; + + const ref = useRef(null); - let className = ""; - if(enabledRoles !== undefined && !enabledRoles.includes(role)) { - className = "keyword-disabled"; + const [columnCount, setColumnCount] = useState(1); + + useEffect(() => { + const redetermineColumnWidths = () => { + if (ref.current) { + setColumnCount(Math.max(Math.floor(ref.current.clientWidth / 300), 1)) } + } - return - }); - - mainElements.push(
- {"### "+translate(set)} -
); - mainElements.push(
- {elements} -
); - } - mainElements.push( + const resizeObserver = new ResizeObserver(redetermineColumnWidths) + + redetermineColumnWidths() + + setTimeout(() => { + resizeObserver.observe(ref.current!); + }) + return resizeObserver.unobserve(ref.current!) + }, [ref]) + + return
+
+ {"# "+translate("wiki.article.generated.roleSet.title")} +
+ + {ROLE_SETS.map(set => { + const description = translateChecked(`${set}.description`); + return
+ `role/${role}` as WikiArticleLink)} + enabledRoles={enabledRoles} + enabledModifiers={[]} + > + {description &&

{description}

} +
+
+ })} +
{translate("wiki.article.generated.roleSet.extra", Object.keys(roleJsonData()).length)} - ); - - return
{mainElements}
; +
; } function getSearchStringsGenerated(article: GeneratedArticle): string[]{ switch(article){ - case "roleSet": + case "roleSet": { let out = [translate("wiki.article.generated.roleSet.title")]; for(let set of ROLE_SETS){ out.push(translate(set)); } return out; + } case "all_text": return []; } @@ -184,8 +256,7 @@ export function getSearchStrings(article: WikiArticleLink): string[]{ const path = article.split('/'); switch (path[0]) { - case "role": - + case "role": { const role = path[1] as Role; const roleData = roleJsonData()[role]; let out = []; @@ -217,15 +288,17 @@ export function getSearchStrings(article: WikiArticleLink): string[]{ out.push(translate("wiki.article.standard.roleLimit.title")); return out; - - case "standard": + } + case "modifiers": + case "standard": { return [ - translate(`wiki.article.standard.${path[1]}.title`), - translate(`wiki.article.standard.${path[1]}.text`), + translate(`wiki.article.${path[0]}.${path[1]}.title`), + translate(`wiki.article.${path[0]}.${path[1]}.text`), ] + } case "generated": return getSearchStringsGenerated(path[1] as GeneratedArticle); - default: + default: // Categories don't show up in search results return []; } } \ No newline at end of file diff --git a/client/src/components/WikiArticleLink.tsx b/client/src/components/WikiArticleLink.tsx index 150fdd8bc..9e848ac62 100644 --- a/client/src/components/WikiArticleLink.tsx +++ b/client/src/components/WikiArticleLink.tsx @@ -1,9 +1,14 @@ +import { MODIFIERS, ModifierType } from "../game/gameState.d"; import translate, { langJson } from "../game/lang"; -import { Role, roleJsonData } from "../game/roleState.d"; +import { Role } from "../game/roleState.d"; +import { WIKI_CATEGORIES, WikiCategory } from "./Wiki"; import "./wiki.css"; +import { getAllRoles } from "../game/roleListState.d" export type WikiArticleLink = `role/${Role}` | + `modifier/${ModifierType}` | + `category/${WikiCategory}` | `standard/${StandardArticle}` | `generated/${GeneratedArticle}`; @@ -16,7 +21,9 @@ const GENERATED_ARTICLES = ["roleSet", "all_text"] as const; export type GeneratedArticle = typeof GENERATED_ARTICLES[number]; export const ARTICLES: WikiArticleLink[] = - Object.keys(roleJsonData()).map(role => `role/${role}`) + WIKI_CATEGORIES.map(category => `category/${category}`) + .concat(getAllRoles().map(role => `role/${role}`)) + .concat(MODIFIERS.map(modifier => `modifier/${modifier}`)) .concat(STANDARD_ARTICLES.map(article => `standard/${article}`)) .concat(GENERATED_ARTICLES.map(article => `generated/${article}`)) as WikiArticleLink[]; @@ -28,6 +35,10 @@ export function getArticleLangKey(page: WikiArticleLink): string { switch (path[0]) { case "role": return `role.${path[1]}.name`; + case "modifier": + return `wiki.article.modifier.${path[1]}.title`; + case "category": + return `wiki.category.${path[1]}`; case "standard": return `wiki.article.standard.${path[1]}.title`; case "generated": @@ -40,4 +51,19 @@ export function getArticleLangKey(page: WikiArticleLink): string { export function getArticleTitle(page: WikiArticleLink): string { return translate(getArticleLangKey(page)); +} + +export function wikiPageIsEnabled( + page: WikiArticleLink, + enabledRoles: Role[], + enabledModifiers: ModifierType[] +): boolean { + switch (page.split("/")[0]) { + case "role": + return enabledRoles.map(role => `role/${role}`).includes(page) + case "modifiers": + return enabledModifiers.map(modifier => `modifier/${modifier}`).includes(page) + default: + return true; + } } \ No newline at end of file diff --git a/client/src/components/WikiCoverCard.tsx b/client/src/components/WikiCoverCard.tsx index 893047747..355e4d0e5 100644 --- a/client/src/components/WikiCoverCard.tsx +++ b/client/src/components/WikiCoverCard.tsx @@ -4,6 +4,8 @@ import Wiki from '../components/Wiki'; import "./wiki.css"; import { WikiArticleLink } from './WikiArticleLink'; import { useLobbyOrGameState } from './useHooks'; +import { MODIFIERS, ModifierType } from '../game/gameState.d'; +import { getAllRoles } from '../game/roleListState.d'; export default function WikiCoverCard(props: Readonly<{ initialWikiPage?: WikiArticleLink @@ -11,11 +13,16 @@ export default function WikiCoverCard(props: Readonly<{ const enabledRoles = useLobbyOrGameState( state => state.enabledRoles, ["enabledRoles"], - [] + getAllRoles() + )!; + const enabledModifiers = useLobbyOrGameState( + state => state.enabledModifiers, + ["enabledModifiers"], + MODIFIERS as any as ModifierType[] )!; return

{translate("menu.wiki.title")}

- +
; } diff --git a/client/src/components/gameModeSettings/OutlineSelector.tsx b/client/src/components/gameModeSettings/OutlineSelector.tsx index 588632232..4b0c14bf9 100644 --- a/client/src/components/gameModeSettings/OutlineSelector.tsx +++ b/client/src/components/gameModeSettings/OutlineSelector.tsx @@ -1,7 +1,7 @@ -import React, { useContext } from "react"; +import React, { ReactElement, useCallback, useContext } from "react"; import "./outlineSelector.css"; import translate from "../../game/lang"; -import { ROLE_SETS, RoleList, RoleOutline, RoleOutlineOption, simplifyRoleOutline, translateRoleOutline, translateRoleOutlineOption} from "../../game/roleListState.d"; +import { getAllRoles, getRolesFromRoleSet, ROLE_SETS, RoleList, RoleOutline, RoleOutlineOption, simplifyRoleOutline, translateRoleOutline, translateRoleOutlineOption} from "../../game/roleListState.d"; import { Role, roleJsonData } from "../../game/roleState.d"; import Icon from "../Icon"; import { DragAndDrop } from "../DragAndDrop"; @@ -9,6 +9,7 @@ import { GameModeContext } from "./GameModesEditor"; import Select, { SelectOptionsSearch } from "../Select"; import StyledText from "../StyledText"; import { Button } from "../Button"; +import { useLobbyOrGameState } from "../useHooks"; type RoleOutlineSelectorProps = { roleOutline: RoleOutline, @@ -114,7 +115,13 @@ export default class RoleOutlineSelector extends React.Component void, -}) - -function translateRoleOutlineOptionOrAny(roleOutlineOption: RoleOutlineOption | "any"): string { - if(roleOutlineOption === "any") { - return translate("any"); - }else - return translateRoleOutlineOption(roleOutlineOption); -} -export class RoleOutlineOptionSelector extends React.Component { - - - render(): React.ReactNode { - - - const optionsSearch: SelectOptionsSearch = new Map(); - - optionsSearch.set("any", [ +})>): ReactElement { + const enabledRoles = useLobbyOrGameState( + state => state.enabledRoles, + ["enabledRoles"], + getAllRoles() + )!; + + const isRoleEnabled = useCallback((role: Role) => { + return enabledRoles.includes(role) + }, [enabledRoles]) + + const optionsSearch: SelectOptionsSearch = new Map(); + + optionsSearch.set("any", [ + + {translate("any")} + , + translate("any") + ]); + + ROLE_SETS.forEach((roleSet) => { + optionsSearch.set(JSON.stringify({type: "roleSet", roleSet: roleSet}), [ !isRoleEnabled(role)) ? "keyword-disabled" : ""} > - {translate("any")} + {translateRoleOutlineOptionOrAny({type: "roleSet", roleSet: roleSet})} , - translate("any") + translateRoleOutlineOptionOrAny({type: "roleSet", roleSet: roleSet})] + ); + }); + + getAllRoles().forEach((role) => { + optionsSearch.set(JSON.stringify({type: "role", role: role}), [ + + {translateRoleOutlineOptionOrAny({type: "role", role})} + , + translateRoleOutlineOptionOrAny({type: "role", role}) ]); - - ROLE_SETS.forEach((roleSet) => { - optionsSearch.set(JSON.stringify({type: "roleSet", roleSet: roleSet}), [ - - {translateRoleOutlineOptionOrAny({type: "roleSet", roleSet: roleSet})} - , - translateRoleOutlineOptionOrAny({type: "roleSet", roleSet: roleSet})] + }); + + return { - this.props.onChange( - value === "any" ? "any" : JSON.parse(value) - ); - }} - optionsSearch={optionsSearch} - /> - } + }} + optionsSearch={optionsSearch} + /> } export function OutlineListSelector(props: { diff --git a/client/src/components/wiki.css b/client/src/components/wiki.css index 0df52e92e..4e7c4ba55 100644 --- a/client/src/components/wiki.css +++ b/client/src/components/wiki.css @@ -23,6 +23,9 @@ flex: 1 1; padding-bottom: 1rem; } +.wiki-search .masonry-item { + width: 100%; +} .wiki-message-section { display: flex; flex-direction: column; @@ -97,4 +100,7 @@ } .wiki-cover-card, .anchor-cover-card .wiki-article { max-width: 50rem; +} +.role-set-article .masonry-item { + text-align: center; } \ No newline at end of file diff --git a/client/src/game/roleListState.d.tsx b/client/src/game/roleListState.d.tsx index 61e30d99e..42666c40d 100644 --- a/client/src/game/roleListState.d.tsx +++ b/client/src/game/roleListState.d.tsx @@ -16,8 +16,7 @@ export function getRolesFromRoleList(roleList: RoleList): Role[] { } export function getRolesComplement(roleList: Role[]): Role[] { - let roles = Object.keys(roleJsonData()) as Role[]; - return roles.filter((role) => { + return getAllRoles().filter((role) => { return !roleList.includes(role); }); } @@ -85,7 +84,7 @@ export function translateRoleOutlineOption(roleOutlineOption: RoleOutlineOption) export function getRolesFromOutline(roleOutline: RoleOutline): Role[] { switch(roleOutline.type){ case "any": - return Object.keys(roleJsonData()) as Role[]; + return getAllRoles(); case "roleOutlineOptions": return roleOutline.options.flatMap((option) => getRolesFromOutlineOption(option)); } @@ -130,5 +129,8 @@ function outlineOptionCompare(optionA: RoleOutlineOption, optionB: RoleOutlineOp } export function getAllRoles(): Role[] { - return Object.keys(roleJsonData()) as Role[]; + return Object.entries(roleJsonData()) + .sort((a, b) => translate(`role.${a[0]}.name`).localeCompare(translate(`role.${b[0]}.name`))) + .sort((a, b) => ROLE_SETS.indexOf(a[1].mainRoleSet) - ROLE_SETS.indexOf(b[1].mainRoleSet)) + .map((a) => a[0]) as Role[]; } \ No newline at end of file diff --git a/client/src/index.css b/client/src/index.css index 070ea4c46..40bddbf63 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -97,7 +97,7 @@ s { *{ user-select: none; - text-shadow: .0625rem .0625rem .13rem rgb(0, 0, 0); + /* text-shadow: .0625rem .0625rem .13rem rgb(0, 0, 0); */ text-align: center; box-sizing: border-box; @@ -342,17 +342,17 @@ code, .code { } .wiki-menu-colors { - --background-color: #301f1b; - --fade-color: #150e0b; - --primary-color: #412720; - --secondary-color: #422821; + --background-color: #2C2124; + --fade-color: #0F0D15; + --primary-color: #3A2A31; + --secondary-color: #3A2A31; --text-color: #ffffff; - --primary-border-color: #5a3b32; - --primary-border-shadow-color: #2b1b16; - --background-border-color: #503830; - --background-border-shadow-color: #251813; - --hover-color: #4c2d23; - --focus-outline-color: #a98e84; + --primary-border-color: #533C44; + --primary-border-shadow-color: #261C26; + --background-border-color: #493942; + --background-border-shadow-color: #201924; + --hover-color: #442F35; + --focus-outline-color: #A18E99; } diff --git a/client/src/menu/Settings.tsx b/client/src/menu/Settings.tsx index fbd075353..4ebe32438 100644 --- a/client/src/menu/Settings.tsx +++ b/client/src/menu/Settings.tsx @@ -3,8 +3,9 @@ import "./settings.css"; import translate, { Language, languageName, LANGUAGES, switchLanguage } from "../game/lang"; import StyledText, { computeKeywordData } from "../components/StyledText"; import Icon from "../components/Icon"; -import { loadSettingsParsed, saveSettings } from "../game/localStorage"; -import { AnchorControllerContext, ANCHOR_CONTROLLER } from "./Anchor"; +import { loadSettingsParsed, RoleSpecificMenuType, saveSettings } from "../game/localStorage"; +import { MobileContext, AnchorControllerContext, ANCHOR_CONTROLLER } from "./Anchor"; +import { Role } from "../game/roleState.d"; import AudioController from "./AudioController"; import CheckBox from "../components/CheckBox"; import { DragAndDrop } from "../components/DragAndDrop"; diff --git a/client/src/menu/game/gameScreenContent/WikiMenu.tsx b/client/src/menu/game/gameScreenContent/WikiMenu.tsx index 0f538b269..00d6e29b7 100644 --- a/client/src/menu/game/gameScreenContent/WikiMenu.tsx +++ b/client/src/menu/game/gameScreenContent/WikiMenu.tsx @@ -4,19 +4,26 @@ import "./wikiMenu.css" import translate from "../../../game/lang"; import Wiki from "../../../components/Wiki"; import { useLobbyOrGameState } from "../../../components/useHooks"; +import { getAllRoles } from "../../../game/roleListState.d"; +import { MODIFIERS, ModifierType } from "../../../game/gameState.d"; export default function WikiMenu(): ReactElement { const enabledRoles = useLobbyOrGameState( state => state.enabledRoles, ["enabledRoles"], - [] + getAllRoles() + )!; + const enabledModifiers = useLobbyOrGameState( + state => state.enabledModifiers, + ["enabledModifiers"], + MODIFIERS as any as ModifierType[] )!; return
{translate("menu.wiki.title")}
- +
} \ No newline at end of file diff --git a/client/src/menu/main/StandaloneWiki.tsx b/client/src/menu/main/StandaloneWiki.tsx index 79312c5a4..51aff11f8 100644 --- a/client/src/menu/main/StandaloneWiki.tsx +++ b/client/src/menu/main/StandaloneWiki.tsx @@ -3,6 +3,8 @@ import Wiki from "../../components/Wiki"; import translate from "../../game/lang"; import "./standaloneWiki.css"; import { WikiArticleLink } from "../../components/WikiArticleLink"; +import { MODIFIERS, ModifierType } from "../../game/gameState.d"; +import { getAllRoles } from "../../game/roleListState.d"; export default function StandaloneWiki(props: Readonly<{ initialWikiPage?: WikiArticleLink @@ -13,6 +15,8 @@ export default function StandaloneWiki(props: Readonly<{
{ if (page !== null) { diff --git a/client/src/resources/keywords.json b/client/src/resources/keywords.json index b2c697785..bc63dad5b 100644 --- a/client/src/resources/keywords.json +++ b/client/src/resources/keywords.json @@ -20,8 +20,10 @@ "minions": { "style": "keyword-minions","link": "standard/minions"}, "town": { "style": "keyword-good", "link": "standard/town"}, "mafia": { "style": "keyword-evil", "link": "standard/mafia"}, + "wiki.article.standard.mafia.title": { "style": "keyword-evil", "link": "standard/mafia" }, "neutral": { "style": "keyword-neutral", "link": "standard/neutral"}, "cult": { "style": "keyword-cult", "link": "standard/cult"}, + "wiki.article.standard.cult.title": { "style": "keyword-cult", "link": "standard/cult" }, "fiends": { "style": "keyword-fiends", "link": "standard/fiends"}, "niceList": { "style": "keyword-christmas", "link": "role/santaClaus" }, "naughtyList": { "style": "keyword-christmas keyword-naughty", "link": "role/santaClaus" }, @@ -34,10 +36,14 @@ "defense.3": { "style": "keyword-info", "link": "standard/defense"}, "dead": { "style": "keyword-evil", "link": "standard/dead"}, "wiki.article.role.reminder": { "style": "keyword-info"}, - "wiki.article.role.abilities": { "style": "keyword-info"}, "wiki.article.role.attributes": { "style": "keyword-info"}, "wiki.article.role.extra": { "style": "keyword-info"}, - "wiki.article.role.guide": { "style": "keyword-info"}, + "wiki.category.town": { "style": "keyword-good", "link": "category/town" }, + "wiki.category.mafia": { "style": "keyword-evil", "link": "category/mafia" }, + "wiki.category.fiends": { "style": "keyword-fiends", "link": "category/fiends" }, + "wiki.category.minions": { "style": "keyword-minions", "link": "category/minions" }, + "wiki.category.cult": { "style": "keyword-cult", "link": "category/cult" }, + "wiki.category.neutral": { "style": "keyword-neutral", "link": "category/neutral" }, "grave.deathCause.execution": { "style": "keyword-info"}, "grave.deathCause.leftTown": { "style": "keyword-info"}, "grave.killer.suicide": { "style": "keyword-info"}, diff --git a/client/src/resources/lang/en_us.json b/client/src/resources/lang/en_us.json index 2963fc0dd..fcea2055b 100644 --- a/client/src/resources/lang/en_us.json +++ b/client/src/resources/lang/en_us.json @@ -102,9 +102,9 @@ "menu.will.add": "Add", "menu.will.subtract": "Subtract", - "menu.wiki.title": "Encyclopedia", + "menu.wiki.title": "Midnight Manual", "menu.wiki.icon": "📖", - "menu.wiki.search.placeholder": "Search the encyclopedia...", + "menu.wiki.search.placeholder": "Search the manual...", "menu.playerList.title": "Players", "menu.playerList.icon": "đŸ•”đŸŸ", @@ -578,6 +578,9 @@ "controllerId.role.krampus.1.name": "Investigate", "obscured": "Obscured", + "obscured:var.0": "Obscure", + "obscured:var.1": "Obscures", + "obscured:var.2": "Obscuring", "grave.deathCause.execution": "Execution", "grave.deathCause.execution:var.0": "Execute", "grave.deathCause.execution:var.1": "Executed", @@ -617,6 +620,7 @@ "townCommon":"Town Common", "townCommon:var.0":"TC", + "townCommon.description":"All Town roles except Jailor, Villager, and Drunk.", "townInvestigative":"Town Investigative", "townInvestigative:var.0":"TI", "townProtective":"Town Protective", @@ -688,7 +692,7 @@ "controllerId.wardenLiveOrDie.boolean.false": "Die", - "standard":"Standard", + "standard":"Uncategorized", "union": "âˆȘ", "onTrial": "On Trial", @@ -714,6 +718,7 @@ "dead:var.0": "Died", "dead:var.1": "Die", "dead:var.2": "Dies", + "dead:var.3": "Death", "any": "Any", "attack": "Attack", "none": "None", @@ -990,17 +995,45 @@ "kiraResult.wrongSpot": "Somebody has this role, but not this player", "kiraResult.notInGame": "Nobody has this role", + "wiki.category.uncategorized": "Other", + "wiki.category.categories": "Composite pages", + "wiki.category.categories.text": "These are the composite pages used to help organize the midnight manual.", + "wiki.category.town": "Town", + "wiki.category.town:var.0": "Town", + "wiki.category.town.text": "These are the roles in the Town role set. By default, most people with these roles are town loyalists, and not insiders.", + "wiki.category.mafia": "Syndicate", + "wiki.category.mafia.text": "These are the roles in the Syndicate role set. By default, most people with these roles are syndicate loyalists, syndicate insiders, and are part of the Syndicate hierarchy.", + "wiki.category.neutral": "Neutral", + "wiki.category.neutral.text": "These are the roles in the Neutral role set. By default, most people with these roles are not insiders and don't win based on the game's conclusion. For example, the Jester wins when it dies.", + "wiki.category.minions": "Minion", + "wiki.category.minions.text": "These are the roles in the Minion role set. By default, most people with these roles win when town loses, and are not insiders.", + "wiki.category.cult": "Cult", + "wiki.category.cult.text": "These are the roles in the cult role set. By default, most people with these roles are cult loyalists, cult insiders, and are part of the Cult hierarchy.", + "wiki.category.fiends": "Fiend", + "wiki.category.fiends.text": "These are the roles in the fiends role set. By default, most people with these roles are fiends loyalists and insiders.", + "wiki.category.modifiers": "Modifiers", + "wiki.category.modifiers.text": "These are options that can be enabled which change the behavior of the game.", + "wiki.category.abilities":"Abilities", + "wiki.category.abilities:var.0":"Ability", + "wiki.category.abilities.text":"Abilities are how players perform actions in the game. To see what actions you can do, see the ability menu. The Ability menu is where the controls for most abilities are. Most abilities happen during the night, but some abilities happen during the day.", + "wiki.category.strategies":"Strategies", + "wiki.category.strategies:var.0":"Strategy", + "wiki.category.strategies.text":"There are a lot of helpful strategies that players use in order to help them win. New strategies are created every day.", + "wiki.article.standard.town.title": "Town", "wiki.article.standard.town.text": "Town is the main good faction. Usually, the majority of the players in each game are town loyalists. They don't start knowing who each other are, unlike the syndicate or the cult. Town wins by eliminating all opposing teams (syndicate, cult, arsonist, werewolf, etc). The townies need to work together to find out who the evils are. Their main power is in communication and voting, so if you are a townie who does not communicate, you are not helping your team win.", - "wiki.article.standard.mafia.title": "Syndicate", - "wiki.article.standard.mafia.title:var.0": "Mafia", - "wiki.article.standard.mafia.text": "The Syndicate is the main evil faction. Syndicate members are told who each other are so they can communicate at night. The syndicate wins by eliminating all opposing teams. The syndicate kills and lies to get people to execute innocent townies in order to get rid of the town. The syndicate can communicate at night to create strategies and help each other lie. Most syndicate roles can't select each other to use their ability. The syndicate always has a godfather who attacks someone each night. If the godfather dies, a new syndicate member becomes the godfather.", + "wiki.article.standard.mafia.title": "Syndicate Hierarchy", + "wiki.article.standard.mafia.title:var.0": "Syndicate", + "wiki.article.standard.mafia.title:var.1": "Mafia Hierarchy", + "wiki.article.standard.mafia.title:var.2": "Mafia", + "wiki.article.standard.mafia.text": "The syndicate hierarchy is the system which keeps the syndicate being able to kill every night. The syndicate hierarchy is a collection of players who are usually also syndicate insiders and syndicate loyalists. The system follows two rules:\n* If there is a Syndicate Killing role in the hierarchy - such as Godfather or Impostor - when they die, another member of the hierarchy will inherit their role.\n* If there is no Syndicate Killing role in the hierarchy, a syndicate member is given the syndicate gun. When the player with the syndicate gun dies, another member of the hierarchy inherits it.\n\nPlayers that are in the syndicate hierarchy tend to also be syndicate insiders and loyalists. This means they win by eliminating every other team and can talk together during night and obituary.", "wiki.article.standard.neutral.title": "Neutral", "wiki.article.standard.neutral.text": "Neutrals are not all on the same team. Each neutral role has special win conditions. Most of them are against both the town and the syndicate. Some neutral roles can win before the game ends like the Jester, Revolutionary, and Doomsayer.", "wiki.article.standard.fiends.title": "Fiends", "wiki.article.standard.fiends.text": "Fiends win by eliminating all opposing teams. Fiends are on the same team, but they can't communicate at night. Fiends are usually very powerful and can kill multiple players in one night. Unlike syndicate and cult, most games should have at most one fiend, in addition to the town and syndicate.", - "wiki.article.standard.cult.title": "Cult", - "wiki.article.standard.cult.text": "Cult is an evil faction. They win by eliminating all opposing teams, they can communicate at night, and they know who each other are. The cult as a whole has 2 abilities, kill and convert. They alternate, converting first, and killing after.\n- The cult can only kill after they have successfully converted someone, after that, they can only convert after successfully killing someone. If for some reason the ability doesn't work (Protection or Roleblock), then it is tried again the next night. It is not inherently every other night.\n\n- Apostle. If there is a cult, there is always an apostle. The apostle uses their ability to convert players. If there is no zealot, then the apostle kills.\n- Disciple. The disciple has no ability.\n- Zealot. The zealot is always be the role of the newest created cultist member. The zealot will do the kills instead of the apostle if a zealot exits.", + "wiki.article.standard.cult.title": "Cult Hierarchy", + "wiki.article.standard.cult.title:var.0": "Cult", + "wiki.article.standard.cult.text": "The cult hierarchy is the system which keeps the cult being able to either kill or convert each night. The cult hierarchy is a collection of players who are usually also cult insiders and cult loyalists. The cult hierarchy maintains 2 abilities, kill and convert. They alternate, converting first, and killing after.\n* The cult can only kill after they have successfully converted someone. After that, they can only convert after successfully killing someone.\n* If for some reason a kill or convert doesn't work (e.g. the killer/converter was blocked), then the same ability is tried again the next night - it doesn't switch. Because of this, killing/converting is not inherently \"every other night\".\n\nThese are how the cult roles execute the duties of the cult:\n* Apostle: The cult hierarchy guarantees there is always an apostle. If the apostle dies, the player who has been in the cult hierarchy the longest becomes the apostle. The apostle's ability is to convert players, however if there is no zealot, then the apostle kills.\n* Disciple: The disciple has no ability. A disciple may become an apostle if the apostle dies.\n* Zealot: The player who has been in the cult hierarchy the shortest is the only zealot. If there is a zealot, it will perform killings rather than the apostle.", "wiki.article.standard.minions.title": "Minions", "wiki.article.standard.minions.text": "Minions is a Role Set that contains roles which win only if town loses. They are not loyal to any conclusion. They leave the game if all their enemies are dead. They aren't insiders to any group, so they don't communicate at night.\n\nSome Minions include:\n* Witch\n* Scarecrow\n* Kidnapper\n* Warper", "wiki.article.standard.whisper.title": "Whisper", @@ -1026,7 +1059,8 @@ "wiki.article.standard.interview.text": "A reporter can interview a player during the night, allowing the reporter to speak with them. The player being interviewed may not know who the reporter is, but everyone knows the reporter is on the town team.", "wiki.article.standard.report.title": "Report", "wiki.article.standard.report.text": "The reporter has a report which is posted publicly every obituary the reporter chooses to. Everyone knows the report was written by the reporter so they know the information is trustworthy. The reporter may interview a player each night to gain information to include in the report.", - "wiki.article.standard.forged.title": "Forged", + "wiki.article.standard.forged.title": "Forge", + "wiki.article.standard.forged.title:var.0": "Forged", "wiki.article.standard.forged.text": "A player who is forged had their alibi and role changed by the forger on their grave.", "wiki.article.standard.fullMoon.title": "Full Moon", "wiki.article.standard.fullMoon.text": "Every night is a full moon night except night one and night three. The Werewolf has a different ability depending on whether or not it is a full moon night.", @@ -1111,11 +1145,11 @@ "wiki.article.standard.protect.text": "Protect means to increase someone's defense to protected. The protection will only last the night unless otherwise indicated. A protected player is told when they are attacked but saved by protection", "wiki.article.standard.dead.title": "Dead", "wiki.article.standard.dead.text": "If a player is dead, they can't vote, their grave will likely be in the graveyard, and they usually can't use their ability anymore. Dead players can only speak with other dead players with the exception of the Medium and its target. A player needs to die to become dead. All players start the game alive! (What did you expect
)", - "wiki.article.standard.silenced.title": "Silenced", + "wiki.article.standard.silenced.title": "Silence", "wiki.article.standard.silenced.title:var.0": "Silences", "wiki.article.standard.silenced.title:var.1": "Silencer", "wiki.article.standard.silenced.title:var.2": "Silencing", - "wiki.article.standard.silenced.title:var.3": "Silence", + "wiki.article.standard.silenced.title:var.3": "Silenced", "wiki.article.standard.silenced.text": "A player who is silenced can not talk or otherwise communicate during that day.\n- They can't nominate during the nomination phase because they are forced to forfeit their vote\n- They can still vote during the judgement phase\n- If the silenced player is put on trial, they still can't talk\n- If the players are talking out loud, silenced players should not speak or use any type of communication, including signals or facial expressions. The rule is, of a player can't use chat, they are not allowed to communicate\n- Roles like deputy mayor and jailor can still use their abilities normally during the day even if silenced\n\n A Blackmailer can silence a player each night.", "wiki.article.standard.friends.title": "Friends", "wiki.article.standard.friends.title:var.0": "Friend", @@ -1156,8 +1190,8 @@ "wiki.article.standard.guilty.text": "A guilty vote during the judgement phase is a vote for the player on trial to be executed. If there are more Guilty votes than Innocent votes, the player on trial is executed.", "wiki.article.standard.abstain.title": "Abstain", "wiki.article.standard.abstain.text": "Abstaining is when a player does not vote during a Judgment phase. If all players Abstain, then the player on trial is not executed.", - "wiki.article.standard.obscured.title": "Obscured", - "wiki.article.standard.obscured.title:var.0": "Obscure", + "wiki.article.standard.obscured.title": "Obscure", + "wiki.article.standard.obscured.title:var.0": "Obscured", "wiki.article.standard.obscured.title:var.1": "Obscures", "wiki.article.standard.obscured.title:var.2": "Obscuring", "wiki.article.standard.obscured.text": "A player who is obscured had their role and alibi erased from their grave. A Mortician can tag players for obscuring, then when they die, they are obscured.", @@ -1188,9 +1222,11 @@ "wiki.article.standard.confused.title:var.1": "Confusing", "wiki.article.standard.confused.title:var.2": "Confuse", "wiki.article.standard.confused.text": "The role of a player who is confused might not function correctly. Confused affects each role differently. You might not be told that you are confused.\n How this affects every role:\n - Snoop: You are always told that you can't tell\n - Detective: You are always told innocent\n - Philosopher: You are always told friends\n - Gossip: You are always told that your target didn't visit an enemy\n - Psychic: You are told a random list of 3 players for your evil vision, You are told a random list of 2 players for your good vision.\n - Tally Clerk: You are told either 1 more or 1 less than the correct number you should be told. If it would exceed the number of players who voted guilty, it is that number instead. If it were to go under 0, it is 0 instead.\n - Auditor: You get random roles, they might be correct or incorrect. You are only told roles that could have come from that slot.", - "wiki.article.standard.marionette.title": "Marionette", - "wiki.article.standard.marionette.title:var.0": "Marionettes", - "wiki.article.standard.marionette.title:var.1": "Puppet", + "wiki.article.standard.marionette.title": "String Up", + "wiki.article.standard.marionette.title:var.0": "String", + "wiki.article.standard.marionette.title:var.1": "Marionette", + "wiki.article.standard.marionette.title:var.2": "Marionettes", + "wiki.article.standard.marionette.title:var.3": "Puppet", "wiki.article.standard.marionette.text": "A player who is a marionette has joined a different team\n- They keep their old role when they become a marionette\n- They are attacked every night by the role that recruited them with a protection piercing attack(EX. Recruiter or Puppeteer)\n- If there are no players with the role that recruited them, the marionette is still attacked\n- The marionette no longer wins with their original team but wins with the new team instead", "wiki.article.standard.aura.title": "Aura", "wiki.article.standard.aura.title:var.0": "Auras", @@ -1224,9 +1260,9 @@ "wiki.article.standard.forfeitVote.title:var.2": "Forfeits Vote", "wiki.article.standard.forfeitVote.title:var.3": "Forfeit Vote", "wiki.article.standard.forfeitVote.text": "Forfeiting your vote means you are not able to vote during the nomination phase but you can still vote during the judgement phase. During discussion, players can choose to forfeit their vote using the button at the top of the playerlist. Once Nomination starts, they can't change their mind.\n- Everyone learns who forfeited their vote when nomination starts in the form of a tag\n- For each player who forfeits their vote, the amount of votes needed for a trial decreases accordingly\n- Players who are silenced are forced to forfeit their vote\n\n It is strongly recommended to never ever forfeit your vote unless you are silenced or pretending to be silenced", - "wiki.article.standard.loveLinked.title": "Love Linked", + "wiki.article.standard.loveLinked.title": "Love Link", "wiki.article.standard.loveLinked.title:var.0": "Broken Heart", - "wiki.article.standard.loveLinked.title:var.1": "Love Link", + "wiki.article.standard.loveLinked.title:var.1": "Love Linked", "wiki.article.standard.loveLinked.title:var.2": "Love Links", "wiki.article.standard.loveLinked.title:var.3": "Broken Hearts", "wiki.article.standard.loveLinked.title:var.4": "Love Linking", @@ -1235,11 +1271,11 @@ "wiki.article.standard.howToPlay.title:var.0":"Phase", "wiki.article.standard.howToPlay.title:var.1":"Phases", "wiki.article.standard.howToPlay.title:var.2":"Help", - "wiki.article.standard.howToPlay.text":"The goal of the game depends on your role. Most roles win if all players from opposing teams are eliminated\nThe teams are Town, Syndicate, Cult, Fiends\n- Town should try to find out who the syndicate is in order to vote them out\n- Syndicate, cult, and fiends, should try to stay hidden while killing townies\n\nThe game runs in phases of day and night\n- During night, each player uses their roles abilities\n- During the day, players discuss information they learned and vote on a player to kill\n\nThere are subphases within day and night\n\n# Night\nMost role abilities work at night. Here are some examples\n- Each night, the godfather can select a player to kill them\n- Each night, the detective can select a player. The detective is told if the player they select is innocent or suspicious. This would help the detective find out who the syndicate is. If they are suspicious they are a town enemy\n- Each night, the framer can select a player to make them register falsely to the detective. So a townie could register as suspicious to a detective falsely if a framer used their ability\n\n# Obituary\nDuring the obituary phase, everyone is told who died, along with the role and alibi of those who died.\nAll players should keep their alibi note page to keep track of all the information they learned during the game. The obituary phase is a perfect time to write down in your alibi what you learned last night. For example:\n\n- If you're a detective who learned that @5 is suspicious on night 1. You could write your alibi to look like this\n\nDetective\nNight 1: @5 - suspicious\n\nIt is also important for non townie players to keep a fake alibi to pretend that they are a townie\n\n# Discussion\nThe discussion phase is where players discuss the information they learned and try to find out who the syndicate, fiends, and cult are\nPlayers will ask each other to claim, this means they want to know the information that the other player learned\nMost players send their alibi in chat during this phase, to share the information efficiently in response to being asked to claim. It will likely be considered suspicious to not have an alibi prepared when asked for one. So its very important to have one prepared, no matter who you are\n\n# Nomination\nDuring nomination, players can vote other players on to trial in order to execute them\nThe best way for townies to kill syndicate members is to use their vote, even if you aren’t certain someone is evil, voting them onto trial and threatening them will often give information to help find syndicate\n- If more than half of the living players vote for one player, that player is put on trial\n- If there aren’t enough votes in time, the game skips directly to dusk\n\n# Testimony\n- The player voted on trail is the only player allowed to speak and chat during this phase, they should certainly claim, send their alibi and say what their role is\n\n# Judgement\n- Everyone votes innocent or guilty (or abstains)\n- If more players vote guilty than innocent, the player on trial will be executed\n- If the player on trial isn’t executed, there is another trail (with a max of three)\n\n# Final words\n- The player who was on trial should be allowed to say some final words or notes they have for the town\n\n# Dusk\n- A prenight phase for everyone to get a last word in before the next night\n\n# Final thoughts\nThe game runs in this loop until there is one team remaining\nThe encyclopedia has a list of all roles and their abilities. It is recommended to read up on your role during the beginning briefing phase\nIf your evil, you should try to learn how at least one townie role works so you can pretend to be that role\nNeutral roles all have different win conditions and are not on a team. For example: Jester wins if they are voted out during a trail", + "wiki.article.standard.howToPlay.text":"The goal of the game depends on your role. Most roles win if all players from opposing teams are eliminated\nThe teams are Town, Syndicate, Cult, Fiends\n- Town should try to find out who the syndicate is in order to vote them out\n- Syndicate, cult, and fiends, should try to stay hidden while killing townies\n\nThe game runs in phases of day and night\n- During night, each player uses their roles abilities\n- During the day, players discuss information they learned and vote on a player to kill\n\nThere are subphases within day and night\n\n# Night\nMost role abilities work at night. Here are some examples\n- Each night, the godfather can select a player to kill them\n- Each night, the detective can select a player. The detective is told if the player they select is innocent or suspicious. This would help the detective find out who the syndicate is. If they are suspicious they are a town enemy\n- Each night, the framer can select a player to make them register falsely to the detective. So a townie could register as suspicious to a detective falsely if a framer used their ability\n\n# Obituary\nDuring the obituary phase, everyone is told who died, along with the role and alibi of those who died.\nAll players should keep their alibi note page to keep track of all the information they learned during the game. The obituary phase is a perfect time to write down in your alibi what you learned last night. For example:\n\n- If you're a detective who learned that @5 is suspicious on night 1. You could write your alibi to look like this\n\nDetective\nNight 1: @5 - suspicious\n\nIt is also important for non townie players to keep a fake alibi to pretend that they are a townie\n\n# Discussion\nThe discussion phase is where players discuss the information they learned and try to find out who the syndicate, fiends, and cult are\nPlayers will ask each other to claim, this means they want to know the information that the other player learned\nMost players send their alibi in chat during this phase, to share the information efficiently in response to being asked to claim. It will likely be considered suspicious to not have an alibi prepared when asked for one. So its very important to have one prepared, no matter who you are\n\n# Nomination\nDuring nomination, players can vote other players on to trial in order to execute them\nThe best way for townies to kill syndicate members is to use their vote, even if you aren’t certain someone is evil, voting them onto trial and threatening them will often give information to help find syndicate\n- If more than half of the living players vote for one player, that player is put on trial\n- If there aren’t enough votes in time, the game skips directly to dusk\n\n# Testimony\n- The player voted on trail is the only player allowed to speak and chat during this phase, they should certainly claim, send their alibi and say what their role is\n\n# Judgement\n- Everyone votes innocent or guilty (or abstains)\n- If more players vote guilty than innocent, the player on trial will be executed\n- If the player on trial isn’t executed, there is another trail (with a max of three)\n\n# Final words\n- The player who was on trial should be allowed to say some final words or notes they have for the town\n\n# Dusk\n- A prenight phase for everyone to get a last word in before the next night\n\n# Final thoughts\nThe game runs in this loop until there is one team remaining\nThe midnight manual has a list of all roles and their abilities. It is recommended to read up on your role during the beginning briefing phase\nIf your evil, you should try to learn how at least one townie role works so you can pretend to be that role\nNeutral roles all have different win conditions and are not on a team. For example: Jester wins if they are voted out during a trail", "wiki.article.standard.priority.title":"Priority", "wiki.article.standard.priority.title:var.0":"Priorities", - "wiki.article.standard.priority.text":"***Some information in this article may be old, or incorrect***\n\nAbilities are used in a specific order called priority order. If 2 players have abilities that go on the same priority, the player with the lower player number will go first. It is recommended that you read a roles encyclopedia page before this page. Some abilities go before night starts. Attacking someone doesn't kill them until the end of the night. Here are all the priorities and the abilities that happen in them.\n\n**Before Night (Not a priority)**\n\n- Jailor chooses who to jail\n- Reporter chooses who to interview\n- Spy finds if an apostle can convert\n- Create Visits\n - All visits are decided by who is selected by each players. During each priority, each associated ability is activated using who is being visited by who, which is not necessarily who the player selected (in the case of a transporter, witch, etc...).\n\n**1: Top Priority**\n\n- Ojo (Visit every player with certain role)\n- Doomsayer (Kill)\n- Revolutionary (Suicide)\n- Jester (Haunt)\n- Veteran (Decide to Rampage)\n- Vigilante (Suicide)\n\n**2: Transporter**\n\n- Transporter (Swap Visits)\n\n**3: Possess**\n\n- Syndicate Witch (Possess)\n\n**4: Necromancy**\n\n- Retributionist (Possess/Change Visits)\n- Necromancer (Possess/Change Visits)\n\n**5: roleblock**\n\n- Bouncer (roleblock)\n- Hypnotist (roleblock)\n- Escort (roleblock)\n- Jailor (Invisible roleblock)\n\n**6: Deception**\n\n- Hypnotist (Hypnotize)\n- Framer (Frame)\n- Blackmailer (Silence)\n- Mortician (Tag)\n- Arsonist (Douse)\n\n**7: Bodyguard**\n\n- Bodyguard (Change visits)\n\n**8: Heal**\n\n- Engineer (Intentionally dismantle, set, or use trap)\n- Bodyguard (Defend themselves)\n- Sentinel (Protect target)\n- Guardian (Defend)\n- Veteran (Defend themselves)\n\n**9: Kill**\n\n- Ojo (Kill)\n- Apostle (Kill)\n- Zealot (Kill)\n- Martyr (Kill)\n- Engineer (Indirect Kill)\n- Arsonist (Ignite)\n- Bodyguard (Attack attacker)\n- Sentinel (Attack Visitor)\n- Godfather (Attack or Make Backup Attack)\n- Jailor (Attack)\n- Mafioso (Attack)\n- Veteran (Rampage)\n- Vigilante (Attack)\n- Werewolf (Rampage and attacks)\n\n**10: Investigative**\n\n- Ojo (See)\n- Reporter (Add Report Message)\n- Psychic (Vision)\n- Engineer (Traps report info)\n- Engineer (Dismantle if triggered) \n- Arsonist (Find who is doused)\n- Bodyguard (Find if protected)\n- Informant (Investigate)\n- Sentinel (Find if protected)\n- Guardian (Find if protected)\n- Mortician (Find role and alibi)\n- Forger (Find role and alibi)\n- Lookout (Investigate)\n- Philosopher (Investigate)\n- Detective (Investigate)\n- Spy (Find who the Syndicate Visits)\n- Tracker (Investigate)\n- Werewolf (Track players scent)\n\n**11: Spy Bug**\n\n- Spy (Bug)\n\n**12: Steal Messages**\n\n- Syndicate Witch (Get target messages)\n- Necromancer (Get target messages)\n- Retributionist (Get target messages)\n\n**13: Convert**\n\n- Apostle (Convert)", + "wiki.article.standard.priority.text":"***Some information in this article may be old, or incorrect***\n\nAbilities are used in a specific order called priority order. If 2 players have abilities that go on the same priority, the player with the lower player number will go first. It is recommended that you read a role's manual page before this page. Some abilities go before night starts. Attacking someone doesn't kill them until the end of the night. Here are all the priorities and the abilities that happen in them.\n\n**Before Night (Not a priority)**\n\n- Jailor chooses who to jail\n- Reporter chooses who to interview\n- Spy finds if an apostle can convert\n- Create Visits\n - All visits are decided by who is selected by each players. During each priority, each associated ability is activated using who is being visited by who, which is not necessarily who the player selected (in the case of a transporter, witch, etc...).\n\n**1: Top Priority**\n\n- Ojo (Visit every player with certain role)\n- Doomsayer (Kill)\n- Revolutionary (Suicide)\n- Jester (Haunt)\n- Veteran (Decide to Rampage)\n- Vigilante (Suicide)\n\n**2: Transporter**\n\n- Transporter (Swap Visits)\n\n**3: Possess**\n\n- Syndicate Witch (Possess)\n\n**4: Necromancy**\n\n- Retributionist (Possess/Change Visits)\n- Necromancer (Possess/Change Visits)\n\n**5: roleblock**\n\n- Bouncer (roleblock)\n- Hypnotist (roleblock)\n- Escort (roleblock)\n- Jailor (Invisible roleblock)\n\n**6: Deception**\n\n- Hypnotist (Hypnotize)\n- Framer (Frame)\n- Blackmailer (Silence)\n- Mortician (Tag)\n- Arsonist (Douse)\n\n**7: Bodyguard**\n\n- Bodyguard (Change visits)\n\n**8: Heal**\n\n- Engineer (Intentionally dismantle, set, or use trap)\n- Bodyguard (Defend themselves)\n- Sentinel (Protect target)\n- Guardian (Defend)\n- Veteran (Defend themselves)\n\n**9: Kill**\n\n- Ojo (Kill)\n- Apostle (Kill)\n- Zealot (Kill)\n- Martyr (Kill)\n- Engineer (Indirect Kill)\n- Arsonist (Ignite)\n- Bodyguard (Attack attacker)\n- Sentinel (Attack Visitor)\n- Godfather (Attack or Make Backup Attack)\n- Jailor (Attack)\n- Mafioso (Attack)\n- Veteran (Rampage)\n- Vigilante (Attack)\n- Werewolf (Rampage and attacks)\n\n**10: Investigative**\n\n- Ojo (See)\n- Reporter (Add Report Message)\n- Psychic (Vision)\n- Engineer (Traps report info)\n- Engineer (Dismantle if triggered) \n- Arsonist (Find who is doused)\n- Bodyguard (Find if protected)\n- Informant (Investigate)\n- Sentinel (Find if protected)\n- Guardian (Find if protected)\n- Mortician (Find role and alibi)\n- Forger (Find role and alibi)\n- Lookout (Investigate)\n- Philosopher (Investigate)\n- Detective (Investigate)\n- Spy (Find who the Syndicate Visits)\n- Tracker (Investigate)\n- Werewolf (Track players scent)\n\n**11: Spy Bug**\n\n- Spy (Bug)\n\n**12: Steal Messages**\n\n- Syndicate Witch (Get target messages)\n- Necromancer (Get target messages)\n- Retributionist (Get target messages)\n\n**13: Convert**\n\n- Apostle (Convert)", "wiki.article.standard.chat.title:var.0":"Chat Messages", "wiki.article.standard.chat.title":"Chat", @@ -1251,43 +1287,37 @@ "wiki.article.standard.alibi.title":"Alibi", "wiki.article.standard.alibi.title:var.0":"Alibis", "wiki.article.standard.alibi.text":"In order to find out who the syndicate is, townies need to share information they have in order to deduce who is lying about their role. This is done with the use of alibis, which are records of the abilities you have used and information you have gathered. Alibis are posted in the chat and graveyard when the player dies. Here is an example of a typical alibi that a Detective would have.\n\n> Detective\nNight 1: Deodat Lawson - Innocent\nNight 2: Samuel Parris - Suspicious\n\nWhen the Detective posts this alibi in chat, they provide evidence to other players that on the second night, they found Samuel Parris as suspicious, implying that Samuel Parris is a syndicate member. If the town believes the Detective, they will likely vote the suspicious player out.\n\nFor a syndicate member to pretend to be a town role, they need to write a fake alibi to blend in with the town roles. _IF A PLAYER DOES NOT HAVE AN ALIBI, THEY ARE LIKELY A SYNDICATE MEMBER_ who doesn't know how to write a fake alibi, and they should be directed to this page. ", - "wiki.article.standard.abilityMenu.title":"Ability", - "wiki.article.standard.abilityMenu.text":"The Ability menu is where the controls for most abilities is. Players should come here during night to decide how to use their abilities.", "wiki.article.standard.gameMode.title":"Gamemode", "wiki.article.standard.gameMode.title:var.0":"Game Mode", "wiki.article.standard.gameMode.text":"The game mode menu lets you keep track of the game settings during the game.\n- Outline list\n- Enabled Roles\n- Enabled Modifiers", - "wiki.article.standard.modifiers.title":"Modifiers", - "wiki.article.standard.modifiers.title:var.0":"Modifier", - "wiki.article.standard.modifiers.text":"A game modifier is a setting set before the game that changes the game somehow. Some examples are the obscured graves modifier and the random love links modifier.", - - "wiki.article.standard.obscuredGraves.title":"Obscured Graves", - "wiki.article.standard.obscuredGraves.text":"Obscured Graves is a game modifier that makes it so all graves are obscured.", - "wiki.article.standard.randomLoveLinks.title":"Random Love Links", - "wiki.article.standard.randomLoveLinks.text":"Random Love Links is a game modifier that makes it so all players are love linked to a random player. The game is set up such that all players are love linked to at least 1 other player, and at most 1 player is love linked to more than 1 other player.", - "wiki.article.standard.deadCanChat.title":"Dead Can Chat", - "wiki.article.standard.deadCanChat.text":"Dead Can Chat is a game modifier that changes the game such that dead players can still use chats as if they were alive. Dead players can not whisper or be whispered to.", - "wiki.article.standard.scheduledNominations.title":"Scheduled Nominations", - "wiki.article.standard.scheduledNominations.text":"Scheduled Nominations is a game modifier that changes the game such that nomination is split into 3 identical separate phases.\n\n- At the end of a nomination phase (when time runs out), whoever has the most votes is put to trial.\n - If there is a tie, nobody is put to trial\n - If the player with the most votes doesnt have at least the minimum votes required for a trial, nobody is put to trial\n - If nobody is put to trial, the game skips to the next nomination phase, or if there are no trials left, the game skips to dusk\n- After a player is voted guilty, and their trial ends, the game skips to dusk", - "wiki.article.standard.noAbstaining.title":"No Abstaining", - "wiki.article.standard.noAbstaining.text":"No Abstaining is a game modifier that changes the game such that no players can abstain during judgement. It will default to voting innocent instead of defaulting to abstaining.", - "wiki.article.standard.noDeathCause.title":"No Death Cause", - "wiki.article.standard.noDeathCause.text":"No Death Cause is a game modifier that changes the game such that every grave does not have a death cause listed.", - "wiki.article.standard.roleSetGraveKillers.title":"Role Set Grave Killers", - "wiki.article.standard.roleSetGraveKillers.text":"Role Set Grave Killers is a game modifier that changes the game such that every grave killer which is a role is replaced with one of:\n* Syndicate;\n* Town;\n* Minions;\n* Neutral;\n* Fiends; or\n* Cult.\n\nOr, if the killer's role is not in one of these role sets, it is not changed.", - "wiki.article.standard.autoGuilty.title": "Auto-Guilty", - "wiki.article.standard.autoGuilty.text": "Auto-Guilty is a game modifier that changes the game such that every nominated player will be executed without judgement.", - "wiki.article.standard.twoThirdsMajority.title": "Two-thirds Majority", - "wiki.article.standard.twoThirdsMajority.text": "Two-thirds Majority is a game modifier that changes the game such that during nomination and judgement, a two-thirds majority (rounded up) is required, rather than a simple majority.", - "wiki.article.standard.noTrialPhases.title": "No Trial Phases", - "wiki.article.standard.noTrialPhases.text": "No Trial Phases is a game modifier that changes the game such that the trial phases Nomination, Testimony, and Judgement no longer happen.", - "wiki.article.standard.noWhispers.title": "No Whispers", - "wiki.article.standard.noWhispers.text": "No Whispers is a game modifier that changes the game such that no players can whisper each other.", - "wiki.article.standard.noNightChat.title": "No Night Chat", - "wiki.article.standard.noNightChat.text": "No Night Chat is a game modifier that changes the game such that all living players cannot send messages during night - even if they are part of an insider group.", - "wiki.article.standard.noChat.title": "No Chat", - "wiki.article.standard.noChat.text": "No Chat is a game modifier that changes the game such that no players can send messages.", + "wiki.article.modifier.obscuredGraves.title":"Obscured Graves", + "wiki.article.modifier.obscuredGraves.text":"Obscured Graves is a game modifier that makes it so all graves are obscured.", + "wiki.article.modifier.randomLoveLinks.title":"Random Love Links", + "wiki.article.modifier.randomLoveLinks.text":"Random Love Links is a game modifier that makes it so all players are love linked to a random player. The game is set up such that all players are love linked to at least 1 other player, and at most 1 player is love linked to more than 1 other player.", + "wiki.article.modifier.deadCanChat.title":"Dead Can Chat", + "wiki.article.modifier.deadCanChat.text":"Dead Can Chat is a game modifier that changes the game such that dead players can still use chats as if they were alive. Dead players can not whisper or be whispered to.", + "wiki.article.modifier.scheduledNominations.title":"Scheduled Nominations", + "wiki.article.modifier.scheduledNominations.text":"Scheduled Nominations is a game modifier that changes the game such that nomination is split into 3 identical separate phases.\n\n- At the end of a nomination phase (when time runs out), whoever has the most votes is put to trial.\n - If there is a tie, nobody is put to trial\n - If the player with the most votes doesnt have at least the minimum votes required for a trial, nobody is put to trial\n - If nobody is put to trial, the game skips to the next nomination phase, or if there are no trials left, the game skips to dusk\n- After a player is voted guilty, and their trial ends, the game skips to dusk", + "wiki.article.modifier.noAbstaining.title":"No Abstaining", + "wiki.article.modifier.noAbstaining.text":"No Abstaining is a game modifier that changes the game such that no players can abstain during judgement. It will default to voting innocent instead of defaulting to abstaining.", + "wiki.article.modifier.noDeathCause.title":"No Death Cause", + "wiki.article.modifier.noDeathCause.text":"No Death Cause is a game modifier that changes the game such that every grave does not have a death cause listed.", + "wiki.article.modifier.roleSetGraveKillers.title":"Role Set Grave Killers", + "wiki.article.modifier.roleSetGraveKillers.text":"Role Set Grave Killers is a game modifier that changes the game such that every grave killer which is a role is replaced with one of:\n* Syndicate;\n* Town;\n* Minions;\n* Neutral;\n* Fiends; or\n* Cult.\n\nOr, if the killer's role is not in one of these role sets, it is not changed.", + "wiki.article.modifier.autoGuilty.title": "Auto-Guilty", + "wiki.article.modifier.autoGuilty.text": "Auto-Guilty is a game modifier that changes the game such that every nominated player will be executed without judgement.", + "wiki.article.modifier.twoThirdsMajority.title": "Two-thirds Majority", + "wiki.article.modifier.twoThirdsMajority.text": "Two-thirds Majority is a game modifier that changes the game such that during nomination and judgement, a two-thirds majority (rounded up) is required, rather than a simple majority.", + "wiki.article.modifier.noTrialPhases.title": "No Trial Phases", + "wiki.article.modifier.noTrialPhases.text": "No Trial Phases is a game modifier that changes the game such that the trial phases Nomination, Testimony, and Judgement no longer happen.", + "wiki.article.modifier.noWhispers.title": "No Whispers", + "wiki.article.modifier.noWhispers.text": "No Whispers is a game modifier that changes the game such that no players can whisper each other.", + "wiki.article.modifier.noNightChat.title": "No Night Chat", + "wiki.article.modifier.noNightChat.text": "No Night Chat is a game modifier that changes the game such that all living players cannot send messages during night - even if they are part of an insider group.", + "wiki.article.modifier.noChat.title": "No Chat", + "wiki.article.modifier.noChat.text": "No Chat is a game modifier that changes the game such that no players can send messages.", "wiki.article.standard.syndicateGunItem.title":"Syndicate Gun", "wiki.article.standard.syndicateGunItem.text":"Syndicate Gun is an ability a syndicate member gets when theres no syndicate killing role present\n- A syndicate member is given a gun they can use to kill a player with a basic attack at night\n- The gun can't be used on the first night\n- The gun can be passed around between syndicate members, so a different syndicate member can shoot it every night\n- The gun still uses visits, so getting roleblocked still erases that visit, and transports still move it\n- The killer appears to be syndicate on the grave of the player who died to the gun", "syndicateGunItem.description":"Choose a player to shoot with the syndicate gun. Or choose to give it to another syndicate insider.", @@ -1320,7 +1350,8 @@ "wiki.article.standard.shwompt.text":"Shwompt \nPronunciation: ʃwɒmpt \nA sudden whooshing sound that contains a siblant conclusion. This describes the sound that is made when objects phase through a solid wall. \nOther forms: Shwompter, Shwompting, Shwompted, Shwomptee", - "wiki.article.generated.roleSet.title":"Role Set", + "wiki.article.generated.roleSet.title":"Role Sets", + "wiki.article.generated.roleSet.title:var.0":"Role Set", "wiki.article.generated.roleSet.extra":"There are a total of \\0 roles.", "wiki.article.generated.all_text.title":"All text",