Format/cleanup of Gang UI

This commit is contained in:
Olivier Gagnon 2021-06-17 17:37:05 -04:00
parent 2c7fbc03cf
commit 97fdf7cb7f
18 changed files with 387 additions and 292 deletions

@ -1,16 +1,15 @@
import { IMults } from "./data/upgrades"; import { IMults, UpgradeType } from "./data/upgrades";
export class GangMemberUpgrade { export class GangMemberUpgrade {
name: string; name: string;
cost: number; cost: number;
type: string; type: UpgradeType;
desc: string; desc: string;
mults: IMults; mults: IMults;
constructor(name = "", cost = 0, type = "w", mults: IMults = {}) { constructor(name = "", cost = 0, type: UpgradeType = UpgradeType.Weapon, mults: IMults = {}) {
this.name = name; this.name = name;
this.cost = cost; this.cost = cost;
//w = weapon, a = armor, v = vehicle, r = rootkit, g = Aug
this.type = type; this.type = type;
this.mults = mults; this.mults = mults;
@ -43,15 +42,15 @@ export class GangMemberUpgrade {
// User friendly version of type. // User friendly version of type.
getType(): string { getType(): string {
switch (this.type) { switch (this.type) {
case "w": case UpgradeType.Weapon:
return "Weapon"; return "Weapon";
case "a": case UpgradeType.Armor:
return "Armor"; return "Armor";
case "v": case UpgradeType.Vehicle:
return "Vehicle"; return "Vehicle";
case "r": case UpgradeType.Rootkit:
return "Rootkit"; return "Rootkit";
case "g": case UpgradeType.Augmentation:
return "Augmentation"; return "Augmentation";
default: default:
return ""; return "";

@ -6,4 +6,4 @@ export interface IAscensionResult {
dex: number; dex: number;
agi: number; agi: number;
cha: number; cha: number;
}; }

@ -7,6 +7,14 @@ export interface IMults {
cha?: number; cha?: number;
} }
export enum UpgradeType {
Weapon = "w",
Armor = "a",
Vehicle = "v",
Rootkit = "r",
Augmentation = "g",
}
/** /**
* Defines the parameters that can be used to initialize and describe a GangMemberUpgrade * Defines the parameters that can be used to initialize and describe a GangMemberUpgrade
* (defined in Gang.js) * (defined in Gang.js)
@ -15,7 +23,7 @@ export interface IGangMemberUpgradeMetadata {
cost: number; cost: number;
mults: IMults; mults: IMults;
name: string; name: string;
upgType: string; upgType: UpgradeType;
} }
/** /**
@ -27,192 +35,192 @@ export const gangMemberUpgradesMetadata: IGangMemberUpgradeMetadata[] = [
cost: 1e6, cost: 1e6,
mults: {str: 1.04, def: 1.04}, mults: {str: 1.04, def: 1.04},
name: "Baseball Bat", name: "Baseball Bat",
upgType: "w", upgType: UpgradeType.Weapon,
}, },
{ {
cost: 12e6, cost: 12e6,
mults: {str: 1.08, def: 1.08, dex: 1.08}, mults: {str: 1.08, def: 1.08, dex: 1.08},
name: "Katana", name: "Katana",
upgType: "w", upgType: UpgradeType.Weapon,
}, },
{ {
cost: 25e6, cost: 25e6,
mults: {str: 1.1, def: 1.1, dex: 1.1, agi: 1.1}, mults: {str: 1.1, def: 1.1, dex: 1.1, agi: 1.1},
name: "Glock 18C", name: "Glock 18C",
upgType: "w", upgType: UpgradeType.Weapon,
}, },
{ {
cost: 50e6, cost: 50e6,
mults: {str: 1.12, def: 1.1, agi: 1.1}, mults: {str: 1.12, def: 1.1, agi: 1.1},
name: "P90C", name: "P90C",
upgType: "w", upgType: UpgradeType.Weapon,
}, },
{ {
cost: 60e6, cost: 60e6,
mults: {str: 1.2, def: 1.15}, mults: {str: 1.2, def: 1.15},
name: "Steyr AUG", name: "Steyr AUG",
upgType: "w", upgType: UpgradeType.Weapon,
}, },
{ {
cost: 100e6, cost: 100e6,
mults: {str: 1.25, def: 1.2}, mults: {str: 1.25, def: 1.2},
name: "AK-47", name: "AK-47",
upgType: "w", upgType: UpgradeType.Weapon,
}, },
{ {
cost: 150e6, cost: 150e6,
mults: {str: 1.3, def: 1.25}, mults: {str: 1.3, def: 1.25},
name: "M15A10 Assault Rifle", name: "M15A10 Assault Rifle",
upgType: "w", upgType: UpgradeType.Weapon,
}, },
{ {
cost: 225e6, cost: 225e6,
mults: {str: 1.3, dex: 1.25, agi: 1.3}, mults: {str: 1.3, dex: 1.25, agi: 1.3},
name: "AWM Sniper Rifle", name: "AWM Sniper Rifle",
upgType: "w", upgType: UpgradeType.Weapon,
}, },
{ {
cost: 2e6, cost: 2e6,
mults: {def: 1.04}, mults: {def: 1.04},
name: "Bulletproof Vest", name: "Bulletproof Vest",
upgType: "a", upgType: UpgradeType.Armor,
}, },
{ {
cost: 5e6, cost: 5e6,
mults: {def: 1.08}, mults: {def: 1.08},
name: "Full Body Armor", name: "Full Body Armor",
upgType: "a", upgType: UpgradeType.Armor,
}, },
{ {
cost: 25e6, cost: 25e6,
mults: {def: 1.15, agi: 1.15}, mults: {def: 1.15, agi: 1.15},
name: "Liquid Body Armor", name: "Liquid Body Armor",
upgType: "a", upgType: UpgradeType.Armor,
}, },
{ {
cost: 40e6, cost: 40e6,
mults: {def: 1.2}, mults: {def: 1.2},
name: "Graphene Plating Armor", name: "Graphene Plating Armor",
upgType: "a", upgType: UpgradeType.Armor,
}, },
{ {
cost: 3e6, cost: 3e6,
mults: {agi: 1.04, cha: 1.04}, mults: {agi: 1.04, cha: 1.04},
name: "Ford Flex V20", name: "Ford Flex V20",
upgType: "v", upgType: UpgradeType.Vehicle,
}, },
{ {
cost: 9e6, cost: 9e6,
mults: {agi: 1.08, cha: 1.08}, mults: {agi: 1.08, cha: 1.08},
name: "ATX1070 Superbike", name: "ATX1070 Superbike",
upgType: "v", upgType: UpgradeType.Vehicle,
}, },
{ {
cost: 18e6, cost: 18e6,
mults: {agi: 1.12, cha: 1.12}, mults: {agi: 1.12, cha: 1.12},
name: "Mercedes-Benz S9001", name: "Mercedes-Benz S9001",
upgType: "v", upgType: UpgradeType.Vehicle,
}, },
{ {
cost: 30e6, cost: 30e6,
mults: {agi: 1.16, cha: 1.16}, mults: {agi: 1.16, cha: 1.16},
name: "White Ferrari", name: "White Ferrari",
upgType: "v", upgType: UpgradeType.Vehicle,
}, },
{ {
cost: 5e6, cost: 5e6,
mults: {hack: 1.05}, mults: {hack: 1.05},
name: "NUKE Rootkit", name: "NUKE Rootkit",
upgType: "r", upgType: UpgradeType.Rootkit,
}, },
{ {
cost: 25e6, cost: 25e6,
mults: {hack: 1.1}, mults: {hack: 1.1},
name: "Soulstealer Rootkit", name: "Soulstealer Rootkit",
upgType: "r", upgType: UpgradeType.Rootkit,
}, },
{ {
cost: 75e6, cost: 75e6,
mults: {hack: 1.15}, mults: {hack: 1.15},
name: "Demon Rootkit", name: "Demon Rootkit",
upgType: "r", upgType: UpgradeType.Rootkit,
}, },
{ {
cost: 40e6, cost: 40e6,
mults: {hack: 1.12}, mults: {hack: 1.12},
name: "Hmap Node", name: "Hmap Node",
upgType: "r", upgType: UpgradeType.Rootkit,
}, },
{ {
cost: 75e6, cost: 75e6,
mults: {hack: 1.15}, mults: {hack: 1.15},
name: "Jack the Ripper", name: "Jack the Ripper",
upgType: "r", upgType: UpgradeType.Rootkit,
}, },
{ {
cost: 10e9, cost: 10e9,
mults: {str: 1.3, dex: 1.3}, mults: {str: 1.3, dex: 1.3},
name: "Bionic Arms", name: "Bionic Arms",
upgType: "g", upgType: UpgradeType.Augmentation,
}, },
{ {
cost: 10e9, cost: 10e9,
mults: {agi: 1.6}, mults: {agi: 1.6},
name: "Bionic Legs", name: "Bionic Legs",
upgType: "g", upgType: UpgradeType.Augmentation,
}, },
{ {
cost: 15e9, cost: 15e9,
mults: {str: 1.15, def: 1.15, dex: 1.15, agi: 1.15}, mults: {str: 1.15, def: 1.15, dex: 1.15, agi: 1.15},
name: "Bionic Spine", name: "Bionic Spine",
upgType: "g", upgType: UpgradeType.Augmentation,
}, },
{ {
cost: 20e9, cost: 20e9,
mults: {str: 1.4, def: 1.4}, mults: {str: 1.4, def: 1.4},
name: "BrachiBlades", name: "BrachiBlades",
upgType: "g", upgType: UpgradeType.Augmentation,
}, },
{ {
cost: 12e9, cost: 12e9,
mults: {str: 1.2, def: 1.2}, mults: {str: 1.2, def: 1.2},
name: "Nanofiber Weave", name: "Nanofiber Weave",
upgType: "g", upgType: UpgradeType.Augmentation,
}, },
{ {
cost: 25e9, cost: 25e9,
mults: {str: 1.5, agi: 1.5}, mults: {str: 1.5, agi: 1.5},
name: "Synthetic Heart", name: "Synthetic Heart",
upgType: "g", upgType: UpgradeType.Augmentation,
}, },
{ {
cost: 15e9, cost: 15e9,
mults: {str: 1.3, def: 1.3}, mults: {str: 1.3, def: 1.3},
name: "Synfibril Muscle", name: "Synfibril Muscle",
upgType: "g", upgType: UpgradeType.Augmentation,
}, },
{ {
cost: 5e9, cost: 5e9,
mults: {hack: 1.05}, mults: {hack: 1.05},
name: "BitWire", name: "BitWire",
upgType: "g", upgType: UpgradeType.Augmentation,
}, },
{ {
cost: 10e9, cost: 10e9,
mults: {hack: 1.15}, mults: {hack: 1.15},
name: "Neuralstimulator", name: "Neuralstimulator",
upgType: "g", upgType: UpgradeType.Augmentation,
}, },
{ {
cost: 7.5e9, cost: 7.5e9,
mults: {hack: 1.1}, mults: {hack: 1.1},
name: "DataJack", name: "DataJack",
upgType: "g", upgType: UpgradeType.Augmentation,
}, },
{ {
cost: 50e9, cost: 50e9,
mults: {str: 1.7, def: 1.7}, mults: {str: 1.7, def: 1.7},
name: "Graphene Bone Lacings", name: "Graphene Bone Lacings",
upgType: "g", upgType: UpgradeType.Augmentation,
}, },
]; ];

@ -0,0 +1,43 @@
import React from "react";
import { Gang } from "../Gang";
import { GangMember } from "../GangMember";
import { numeralWrapper } from "../../ui/numeralFormat";
import { removePopup } from "../../ui/React/createPopup";
interface IProps {
member: GangMember;
gang: Gang;
popupId: string;
}
export function AscensionPopup(props: IProps): React.ReactElement {
function confirm(): void {
props.gang.ascendMember(props.member);
removePopup(props.popupId);
}
function cancel(): void {
removePopup(props.popupId);
}
const ascendBenefits = props.member.getAscensionResults();
return (<>
<pre>
Are you sure you want to ascend this member? They will lose all of<br />
their non-Augmentation upgrades and their stats will reset back to 1.<br />
<br />
Furthermore, your gang will lose {numeralWrapper.formatRespect(props.member.earnedRespect)} respect<br />
<br />
In return, they will gain the following permanent boost to stat multipliers:<br />
Hacking: +{numeralWrapper.formatPercentage(ascendBenefits.hack/100)}<br />
Strength: +{numeralWrapper.formatPercentage(ascendBenefits.str/100)}<br />
Defense: +{numeralWrapper.formatPercentage(ascendBenefits.def/100)}<br />
Dexterity: +{numeralWrapper.formatPercentage(ascendBenefits.dex/100)}<br />
Agility: +{numeralWrapper.formatPercentage(ascendBenefits.agi/100)}<br />
Charisma: +{numeralWrapper.formatPercentage(ascendBenefits.cha/100)}<br />
</pre>
<button className="std-button" onClick={confirm}>Ascend</button>
<button className="std-button" onClick={cancel}>Cancel</button>
</>);
}

26
src/Gang/ui/BonusTime.tsx Normal file

@ -0,0 +1,26 @@
import * as React from "react";
import { Gang } from "../Gang";
import { CONSTANTS } from "../../Constants";
import { convertTimeMsToTimeElapsedString } from "../../../utils/StringHelperFunctions";
interface IProps {
gang: Gang;
}
export function BonusTime(props: IProps): React.ReactElement {
const CyclerPerSecond = 1000 / CONSTANTS._idleSpeed;
if (props.gang.storedCycles / CyclerPerSecond*1000 <= 5000) return (<></>);
const bonusMillis = props.gang.storedCycles / CyclerPerSecond * 1000;
return (<>
<p className="tooltip" style={{display: "inline-block"}}>
Bonus time: {convertTimeMsToTimeElapsedString(bonusMillis)}
<span className="tooltiptext">
You gain bonus time while offline or when the game is inactive
(e.g. when the tab is throttled by the browser). Bonus time
makes the Gang mechanic progress faster, up to 5x the normal
speed.
</span>
</p>
<br />
</>);
}

@ -0,0 +1,19 @@
import React from "react";
import { Gang } from "../Gang";
import { GangMember } from "../GangMember";
import { Accordion } from "../../ui/React/Accordion";
import { GangMemberAccordionContent } from "./GangMemberAccordionContent";
interface IProps {
gang: Gang;
member: GangMember;
}
export function GangMemberAccordion(props: IProps): React.ReactElement {
return <Accordion
panelInitiallyOpened={true}
headerContent={<>{props.member.name}</>}
panelContent={<GangMemberAccordionContent
gang={props.gang}
member={props.member} />} />
}

@ -1,7 +1,7 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { Panel1 } from "./Panel1"; import { GangMemberStats } from "./GangMemberStats";
import { Panel2 } from "./Panel2"; import { TaskSelector } from "./TaskSelector";
import { Panel3 } from "./Panel3"; import { TaskDescription } from "./TaskDescription";
import { Gang } from "../Gang"; import { Gang } from "../Gang";
import { GangMember } from "../GangMember"; import { GangMember } from "../GangMember";
@ -14,13 +14,16 @@ export function GangMemberAccordionContent(props: IProps): React.ReactElement {
const setRerender = useState(false)[1]; const setRerender = useState(false)[1];
return (<> return (<>
<div className={"gang-member-info-div tooltip"}> <div className={"gang-member-info-div tooltip"}>
<Panel1 gang={props.gang} member={props.member} /> <GangMemberStats gang={props.gang} member={props.member} />
</div> </div>
<div className={"gang-member-info-div"}> <div className={"gang-member-info-div"}>
<Panel2 onTaskChange={()=>setRerender(old => !old)} gang={props.gang} member={props.member} /> <TaskSelector
onTaskChange={()=>setRerender(old => !old)}
gang={props.gang}
member={props.member} />
</div> </div>
<div className={"gang-member-info-div"}> <div className={"gang-member-info-div"}>
<Panel3 member={props.member} /> <TaskDescription member={props.member} />
</div> </div>
</>); </>);
} }

@ -1,11 +1,11 @@
import React, { useState, useEffect } from "react"; import React, { useState } from "react";
import { Accordion } from "../../ui/React/Accordion"; import { GangMemberUpgradePopup } from "./GangMemberUpgradePopup";
import { GangMemberAccordionContent } from "./GangMemberAccordionContent" import { GangMemberAccordion } from "./GangMemberAccordion";
import { GangMemberUpgradePopup } from "./GangMemberUpgradePopup"
import { createPopup } from "../../ui/React/createPopup"; import { createPopup } from "../../ui/React/createPopup";
import { IPlayer } from "../../PersonObjects/IPlayer"; import { IPlayer } from "../../PersonObjects/IPlayer";
import { Gang } from "../Gang"; import { Gang } from "../Gang";
import { GangMember } from "../GangMember"; import { GangMember } from "../GangMember";
import { RecruitButton } from "./RecruitButton";
interface IProps { interface IProps {
gang: Gang; gang: Gang;
@ -14,6 +14,7 @@ interface IProps {
export function GangMemberList(props: IProps): React.ReactElement { export function GangMemberList(props: IProps): React.ReactElement {
const [filter, setFilter] = useState(""); const [filter, setFilter] = useState("");
const setRerender = useState(false)[1];
function openUpgradePopup(): void { function openUpgradePopup(): void {
const popupId = `gang-upgrade-popup`; const popupId = `gang-upgrade-popup`;
@ -28,19 +29,27 @@ export function GangMemberList(props: IProps): React.ReactElement {
setFilter(event.target.value); setFilter(event.target.value);
} }
function members(): GangMember[] { const members = props.gang.members.filter((member: GangMember) =>
return props.gang.members.filter((member: GangMember) => member.name.indexOf(filter) > -1 || member.task.indexOf(filter) > -1) member.name.indexOf(filter) > -1 || member.task.indexOf(filter) > -1);
}
return (<> return (<>
<input className="text-input" placeholder="Filter gang member" style={{margin: "5px", padding: "5px"}} value={filter} onChange={onChange} /> <RecruitButton
<a className="a-link-button" style={{display: 'inline-block'}} onClick={openUpgradePopup}>Manage Equipment</a> onRecruit={() => setRerender(old => !old)}
gang={props.gang} />
<br />
<input
className="text-input"
placeholder="Filter gang member"
style={{margin: "5px", padding: "5px"}}
value={filter}
onChange={onChange} />
<a
className="a-link-button"
style={{display: 'inline-block'}}
onClick={openUpgradePopup}>Manage Equipment</a>
<ul> <ul>
{members().map((member: GangMember) => <li key={member.name}> {members.map((member: GangMember) => <li key={member.name}>
<Accordion <GangMemberAccordion gang={props.gang} member={member} />
panelInitiallyOpened={true}
headerContent={<>{member.name}</>}
panelContent={<GangMemberAccordionContent gang={props.gang} member={member} />} />
</li>)} </li>)}
</ul> </ul>
</>); </>);

@ -1,59 +1,21 @@
import React, { useState, useEffect } from "react"; import React from "react";
import { dialogBoxCreate } from "../../../utils/DialogBox"; import { dialogBoxCreate } from "../../../utils/DialogBox";
import { formatNumber } from "../../../utils/StringHelperFunctions"; import { formatNumber } from "../../../utils/StringHelperFunctions";
import { numeralWrapper } from "../../ui/numeralFormat"; import { numeralWrapper } from "../../ui/numeralFormat";
import { createPopup, removePopup } from "../../ui/React/createPopup"; import { createPopup, removePopup } from "../../ui/React/createPopup";
import { Gang } from "../Gang"; import { Gang } from "../Gang";
import { GangMember } from "../GangMember"; import { GangMember } from "../GangMember";
import { AscensionPopup } from "./AscensionPopup";
interface IAscendProps {
member: GangMember;
gang: Gang;
popupId: string;
}
function ascendPopup(props: IAscendProps): React.ReactElement {
function confirm(): void {
props.gang.ascendMember(props.member);
removePopup(props.popupId);
}
function cancel(): void {
removePopup(props.popupId);
}
const ascendBenefits = props.member.getAscensionResults();
return (<>
<pre>
Are you sure you want to ascend this member? They will lose all of<br />
their non-Augmentation upgrades and their stats will reset back to 1.<br />
<br />
Furthermore, your gang will lose {numeralWrapper.formatRespect(props.member.earnedRespect)} respect<br />
<br />
In return, they will gain the following permanent boost to stat multipliers:<br />
Hacking: +{numeralWrapper.formatPercentage(ascendBenefits.hack/100)}<br />
Strength: +{numeralWrapper.formatPercentage(ascendBenefits.str/100)}<br />
Defense: +{numeralWrapper.formatPercentage(ascendBenefits.def/100)}<br />
Dexterity: +{numeralWrapper.formatPercentage(ascendBenefits.dex/100)}<br />
Agility: +{numeralWrapper.formatPercentage(ascendBenefits.agi/100)}<br />
Charisma: +{numeralWrapper.formatPercentage(ascendBenefits.cha/100)}<br />
</pre>
<button className="std-button" onClick={confirm}>Ascend</button>
<button className="std-button" onClick={cancel}>Cancel</button>
</>);
}
interface IProps { interface IProps {
member: GangMember; member: GangMember;
gang: Gang; gang: Gang;
} }
export function Panel1(props: IProps): React.ReactElement { export function GangMemberStats(props: IProps): React.ReactElement {
function ascend(): void { function ascend(): void {
const popupId = `gang-management-ascend-member ${props.member.name}`; const popupId = `gang-management-ascend-member ${props.member.name}`;
createPopup(popupId, ascendPopup, { createPopup(popupId, AscensionPopup, {
member: props.member, member: props.member,
gang: props.gang, gang: props.gang,
popupId: popupId, popupId: popupId,
@ -85,12 +47,12 @@ Ag: x{numeralWrapper.formatMultiplier(props.member.agi_mult * props.member.agi_a
Ch: x{numeralWrapper.formatMultiplier(props.member.cha_mult * props.member.cha_asc_mult)}(x{numeralWrapper.formatMultiplier(props.member.cha_mult)} Eq, x{numeralWrapper.formatMultiplier(props.member.cha_asc_mult)} Asc) Ch: x{numeralWrapper.formatMultiplier(props.member.cha_mult * props.member.cha_asc_mult)}(x{numeralWrapper.formatMultiplier(props.member.cha_mult)} Eq, x{numeralWrapper.formatMultiplier(props.member.cha_asc_mult)} Asc)
</span> </span>
<pre id={`${props.member.name}gang-member-stats-text`}> <pre id={`${props.member.name}gang-member-stats-text`}>
Hacking: {formatNumber(props.member.hack, 0)} ({numeralWrapper.formatExp(props.member.hack_exp)} exp)<br /> Hacking: {formatNumber(props.member.hack, 0)} ({numeralWrapper.formatExp(props.member.hack_exp)} exp)<br />
Strength: {formatNumber(props.member.str, 0)} ({numeralWrapper.formatExp(props.member.str_exp)} exp)<br /> Strength: {formatNumber(props.member.str, 0)} ({numeralWrapper.formatExp(props.member.str_exp)} exp)<br />
Defense: {formatNumber(props.member.def, 0)} ({numeralWrapper.formatExp(props.member.def_exp)} exp)<br /> Defense: {formatNumber(props.member.def, 0)} ({numeralWrapper.formatExp(props.member.def_exp)} exp)<br />
Dexterity: {formatNumber(props.member.dex, 0)} ({numeralWrapper.formatExp(props.member.dex_exp)} exp)<br /> Dexterity: {formatNumber(props.member.dex, 0)} ({numeralWrapper.formatExp(props.member.dex_exp)} exp)<br />
Agility: {formatNumber(props.member.agi, 0)} ({numeralWrapper.formatExp(props.member.agi_exp)} exp)<br /> Agility: {formatNumber(props.member.agi, 0)} ({numeralWrapper.formatExp(props.member.agi_exp)} exp)<br />
Charisma: {formatNumber(props.member.cha, 0)} ({numeralWrapper.formatExp(props.member.cha_exp)} exp)<br /> Charisma: {formatNumber(props.member.cha, 0)} ({numeralWrapper.formatExp(props.member.cha_exp)} exp)<br />
</pre> </pre>
<br /> <br />
<button className="accordion-button" onClick={ascend}>Ascend</button> <button className="accordion-button" onClick={ascend}>Ascend</button>

@ -8,6 +8,7 @@ import { Money } from "../../ui/React/Money";
import { removePopup } from "../../ui/React/createPopup"; import { removePopup } from "../../ui/React/createPopup";
import { GangMember } from "../GangMember"; import { GangMember } from "../GangMember";
import { Gang } from "../Gang"; import { Gang } from "../Gang";
import { UpgradeType } from "../data/upgrades";
interface IPanelProps { interface IPanelProps {
member: GangMember; member: GangMember;
@ -17,49 +18,31 @@ interface IPanelProps {
function GangMemberUpgradePanel(props: IPanelProps): React.ReactElement { function GangMemberUpgradePanel(props: IPanelProps): React.ReactElement {
const setRerender = useState(false)[1]; const setRerender = useState(false)[1];
// Upgrade buttons. Only show upgrades that can be afforded function filterUpgrades(list: string[], type: UpgradeType): GangMemberUpgrade[] {
const weaponUpgrades: GangMemberUpgrade[] = []; return Object.keys(GangMemberUpgrades).filter((upgName: string) => {
const armorUpgrades: GangMemberUpgrade[] = [];
const vehicleUpgrades: GangMemberUpgrade[] = [];
const rootkitUpgrades: GangMemberUpgrade[] = [];
const augUpgrades: GangMemberUpgrade[] = [];
for (const upgName in GangMemberUpgrades) {
if (GangMemberUpgrades.hasOwnProperty(upgName)) {
const upg = GangMemberUpgrades[upgName]; const upg = GangMemberUpgrades[upgName];
if (props.player.money.lt(props.gang.getUpgradeCost(upg))) continue; if (props.player.money.lt(props.gang.getUpgradeCost(upg)))
if (props.member.upgrades.includes(upgName) || props.member.augmentations.includes(upgName)) continue; return false;
switch (upg.type) { if(upg.type !== type) return false;
case "w": if(list.includes(upgName)) return false;
weaponUpgrades.push(upg); return true;
break; }).map((upgName: string) => GangMemberUpgrades[upgName]);
case "a":
armorUpgrades.push(upg);
break;
case "v":
vehicleUpgrades.push(upg);
break;
case "r":
rootkitUpgrades.push(upg);
break;
case "g":
augUpgrades.push(upg);
break;
default:
console.error(`ERROR: Invalid Gang Member Upgrade Type: ${upg.type}`);
}
}
} }
const weaponUpgrades = filterUpgrades(props.member.upgrades, UpgradeType.Weapon);
const armorUpgrades = filterUpgrades(props.member.upgrades, UpgradeType.Armor);
const vehicleUpgrades = filterUpgrades(props.member.upgrades, UpgradeType.Vehicle);
const rootkitUpgrades = filterUpgrades(props.member.upgrades, UpgradeType.Rootkit);
const augUpgrades = filterUpgrades(props.member.augmentations, UpgradeType.Augmentation);
function purchased(name: string): React.ReactElement { function purchased(upgName: string): React.ReactElement {
const upg = GangMemberUpgrades[name] const upg = GangMemberUpgrades[upgName]
return (<div key={name} className="gang-owned-upgrade tooltip"> return (<div key={upgName} className="gang-owned-upgrade tooltip">
{upg.name} {upg.name}
<span className="tooltiptext" dangerouslySetInnerHTML={{__html: upg.desc}} /> <span className="tooltiptext" dangerouslySetInnerHTML={{__html: upg.desc}} />
</div>); </div>);
} }
function upgradeButton(upg: GangMemberUpgrade, left = false): React.ReactElement { function upgradeButton(upg: GangMemberUpgrade, left: boolean = false): React.ReactElement {
function onClick(): void { function onClick(): void {
props.member.buyUpgrade(upg, props.player, props.gang); props.member.buyUpgrade(upg, props.player, props.gang);
setRerender(old => !old); setRerender(old => !old);
@ -132,11 +115,25 @@ export function GangMemberUpgradePopup(props: IProps): React.ReactElement {
}, []); }, []);
return (<> return (<>
<input className="text-input" value={filter} placeholder="Filter gang member" onChange={event => setFilter(event.target.value)} /> <input
className="text-input"
value={filter}
placeholder="Filter gang member"
onChange={event => setFilter(event.target.value)} />
<p className="tooltip" style={{marginLeft: '6px', display: 'inline-block'}}> <p className="tooltip" style={{marginLeft: '6px', display: 'inline-block'}}>
Discount: -{numeralWrapper.formatPercentage(1 - 1 / props.gang.getDiscount())} Discount: -{numeralWrapper.formatPercentage(1 - 1 / props.gang.getDiscount())}
<span className="tooltiptext">You get a discount on equipment and upgrades based on your gang's respect and power. More respect and power leads to more discounts.</span> <span className="tooltiptext">
You get a discount on equipment and upgrades based on your
gang's respect and power. More respect and power leads to more
discounts.
</span>
</p> </p>
{props.gang.members.map((member: GangMember) => <GangMemberUpgradePanel key={member.name} player={props.player} gang={props.gang} member={member} />)} {props.gang.members.map((member: GangMember) =>
<GangMemberUpgradePanel
key={member.name}
player={props.player}
gang={props.gang}
member={member} />)
}
</>); </>);
} }

@ -1,125 +1,18 @@
import React, { useState, useEffect } from "react"; import React from "react";
import { Factions } from "../../Faction/Factions"; import { Factions } from "../../Faction/Factions";
import { Gang } from "../Gang"; import { Gang } from "../Gang";
import { import { formatNumber } from "../../../utils/StringHelperFunctions";
formatNumber,
convertTimeMsToTimeElapsedString,
} from "../../../utils/StringHelperFunctions";
import { numeralWrapper } from "../../ui/numeralFormat"; import { numeralWrapper } from "../../ui/numeralFormat";
import { MoneyRate } from "../../ui/React/MoneyRate"; import { MoneyRate } from "../../ui/React/MoneyRate";
import { Reputation } from "../../ui/React/Reputation"; import { Reputation } from "../../ui/React/Reputation";
import { AllGangs } from "../AllGangs"; import { AllGangs } from "../AllGangs";
import { GangConstants } from "../data/Constants"; import { BonusTime } from "./BonusTime";
import { createPopup, removePopup } from "../../ui/React/createPopup";
import { dialogBoxCreate } from "../../../utils/DialogBox";
interface IRecruitPopupProps {
gang: Gang;
popupId: string;
}
function recruitPopup(props: IRecruitPopupProps): React.ReactElement {
const [name, setName] = useState("");
function recruit(): void {
if (name === "") {
dialogBoxCreate("You must enter a name for your Gang member!");
return;
}
if (!props.gang.canRecruitMember()) {
dialogBoxCreate("You cannot recruit another Gang member!");
return;
}
// At this point, the only way this can fail is if you already
// have a gang member with the same name
if (!props.gang.recruitMember(name)) {
dialogBoxCreate("You already have a gang member with this name!");
return;
}
removePopup(props.popupId);
}
function cancel(): void {
removePopup(props.popupId);
}
function onKeyUp(event: React.KeyboardEvent<HTMLInputElement>): void {
if(event.keyCode === 13) recruit();
if(event.keyCode === 27) cancel();
}
function onChange(event: React.ChangeEvent<HTMLInputElement>): void {
setName(event.target.value);
}
return (<>
<p>Enter a name for your new Gang member:</p><br />
<input autoFocus
onKeyUp={onKeyUp}
onChange={onChange}
className="text-input"
type="text"
placeholder="unique name" />
<a className="std-button" onClick={recruit}>Recruit Gang Member</a>
<a className="std-button" onClick={cancel}>Cancel</a>
</>);
}
interface IProps { interface IProps {
gang: Gang; gang: Gang;
} }
function Recruitment(props: IProps): React.ReactElement {
// Toggle the 'Recruit member button' if valid
const numMembers = props.gang.members.length;
const respectCost = props.gang.getRespectNeededToRecruitMember();
if (numMembers >= GangConstants.MaximumGangMembers) {
return (<></>);
} else if (props.gang.canRecruitMember()) {
function onClick(): void {
const popupId = "recruit-gang-member-popup";
createPopup(popupId, recruitPopup, {
gang: props.gang,
popupId: popupId,
});
}
return (<>
<a className="a-link-button"
onClick={onClick}
style={{display: 'inline-block', margin: '10px'}}>
Recruit Gang Member
</a>
</>);
}
return (<>
<a className="a-link-button-inactive"
style={{display: 'inline-block', margin: '10px'}}>
Recruit Gang Member
</a>
<p style={{margin: '10px', color: 'red', display: 'inline-block'}}>
{formatNumber(respectCost, 2)} respect needed to recruit next member
</p>
</>);
}
function BonusTime(props: IProps): React.ReactElement {
const CyclesPerSecond = 1000 / 200;
if (props.gang.storedCycles / CyclesPerSecond*1000 <= 5000) return <></>;
return (<>
<p className="tooltip" style={{display: "inline-block"}}>
Bonus time: {convertTimeMsToTimeElapsedString(props.gang.storedCycles / CyclesPerSecond*1000)}
<span className="tooltiptext">
You gain bonus time while offline or when the game is inactive (e.g. when the tab is throttled by the browser). Bonus time makes the Gang mechanic progress faster, up to 5x the normal speed
</span>
</p>
<br />
</>);
}
export function GangStats(props: IProps): React.ReactElement { export function GangStats(props: IProps): React.ReactElement {
const territoryMult = AllGangs[props.gang.facName].territory * 100; const territoryMult = AllGangs[props.gang.facName].territory * 100;
let territoryStr; let territoryStr;
@ -171,7 +64,5 @@ export function GangStats(props: IProps): React.ReactElement {
</p> </p>
<br /> <br />
<BonusTime gang={props.gang} /> <BonusTime gang={props.gang} />
<br />
<Recruitment gang={props.gang} />
</>); </>);
} }

@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react"; import React from "react";
import { IPlayer } from "../../PersonObjects/IPlayer"; import { IPlayer } from "../../PersonObjects/IPlayer";
import { GangStats } from "./GangStats"; import { GangStats } from "./GangStats";
import { Gang } from "../Gang"; import { Gang } from "../Gang";
@ -12,24 +12,28 @@ interface IProps {
export function ManagementSubpage(props: IProps): React.ReactElement { export function ManagementSubpage(props: IProps): React.ReactElement {
return (<div style={{display: 'block'}}> return (<div style={{display: 'block'}}>
<p style={{width: "70%"}}> <p style={{width: "70%"}}>
This page is used to manage your gang members and get an overview of your gang's stats. This page is used to manage your gang members and get an overview of
your gang's stats.
<br /> <br />
<br /> <br />
If a gang member is not earning much money or respect, the task that you If a gang member is not earning much money or respect, the task that
have assigned to that member might be too difficult. Consider training that you have assigned to that member might be too difficult. Consider
member's stats or choosing an easier task. The tasks closer to the training that member's stats or choosing an easier task. The tasks
top of the dropdown list are generally easier. Alternatively, the gang member's closer to the top of the dropdown list are generally easier.
low production might be due to the fact that your wanted level is too high. Alternatively, the gang member's low production might be due to the
Consider assigning a few members to the '{props.gang.isHackingGang?"Ethical Hacking":"Vigilante Justice"}' fact that your wanted level is too high. Consider assigning a few
members to the '{props.gang.isHackingGang?"Ethical Hacking":"Vigilante Justice"}'
task to lower your wanted level. task to lower your wanted level.
<br /> <br />
<br /> <br />
Installing Augmentations does NOT reset your progress with your Gang. Installing Augmentations does NOT reset your progress with your
Furthermore, after installing Augmentations, you will Gang. Furthermore, after installing Augmentations, you will
automatically be a member of whatever Faction you created your gang with. automatically be a member of whatever Faction you created your gang
with.
<br /> <br />
<br /> <br />
You can also manage your gang programmatically through Netscript using the Gang API You can also manage your gang programmatically through Netscript
using the Gang API
</p> </p>
<br /> <br />
<GangStats gang={props.gang} /> <GangStats gang={props.gang} />

@ -0,0 +1,47 @@
import React from "react";
import { Gang } from "../Gang";
import { RecruitPopup } from "./RecruitPopup";
import { GangConstants } from "../data/Constants";
import { formatNumber } from "../../../utils/StringHelperFunctions";
import { createPopup } from "../../ui/React/createPopup";
interface IProps {
gang: Gang;
onRecruit: () => void;
}
export function RecruitButton(props: IProps): React.ReactElement {
if (props.gang.members.length >= GangConstants.MaximumGangMembers) {
return (<></>);
}
if (!props.gang.canRecruitMember()) {
const respect = props.gang.getRespectNeededToRecruitMember();
return (<>
<a className="a-link-button-inactive"
style={{display: 'inline-block', margin: '10px'}}>
Recruit Gang Member
</a>
<p style={{margin: '10px', color: 'red', display: 'inline-block'}}>
{formatNumber(respect, 2)} respect needed to recruit next member
</p>
</>);
}
function onClick(): void {
const popupId = "recruit-gang-member-popup";
createPopup(popupId, RecruitPopup, {
gang: props.gang,
popupId: popupId,
onRecruit: props.onRecruit,
});
}
return (<>
<a className="a-link-button"
onClick={onClick}
style={{display: 'inline-block', margin: '10px'}}>
Recruit Gang Member
</a>
</>);
}

@ -0,0 +1,60 @@
import React, { useState } from "react";
import { Gang } from "../Gang";
import { removePopup } from "../../ui/React/createPopup";
import { dialogBoxCreate } from "../../../utils/DialogBox";
interface IRecruitPopupProps {
gang: Gang;
popupId: string;
onRecruit: () => void;
}
export function RecruitPopup(props: IRecruitPopupProps): React.ReactElement {
const [name, setName] = useState("");
function recruit(): void {
if (name === "") {
dialogBoxCreate("You must enter a name for your Gang member!");
return;
}
if (!props.gang.canRecruitMember()) {
dialogBoxCreate("You cannot recruit another Gang member!");
return;
}
// At this point, the only way this can fail is if you already
// have a gang member with the same name
if (!props.gang.recruitMember(name)) {
dialogBoxCreate("You already have a gang member with this name!");
return;
}
props.onRecruit();
removePopup(props.popupId);
}
function cancel(): void {
removePopup(props.popupId);
}
function onKeyUp(event: React.KeyboardEvent<HTMLInputElement>): void {
if(event.keyCode === 13) recruit();
if(event.keyCode === 27) cancel();
}
function onChange(event: React.ChangeEvent<HTMLInputElement>): void {
setName(event.target.value);
}
return (<>
<p>Enter a name for your new Gang member:</p><br />
<input autoFocus
onKeyUp={onKeyUp}
onChange={onChange}
className="text-input"
type="text"
placeholder="unique name" />
<a className="std-button" onClick={recruit}>Recruit Gang Member</a>
<a className="std-button" onClick={cancel}>Cancel</a>
</>);
}

@ -27,9 +27,18 @@ export function Root(props: IProps): React.ReactElement {
} }
return (<> return (<>
<a className="a-link-button" style={{display: "inline-block"}} onClick={back}>Back</a> <a className="a-link-button" style={{display: "inline-block"}}
<a className={management?"a-link-button-inactive":"a-link-button"} style={{display: "inline-block"}} onClick={() => setManagement(true)}>Gang Management</a> onClick={back}>Back</a>
<a className={!management?"a-link-button-inactive":"a-link-button"} style={{display: "inline-block"}} onClick={() => setManagement(false)}>Gang Territory</a> <a className={management?"a-link-button-inactive":"a-link-button"}
style={{display: "inline-block"}}
onClick={() => setManagement(true)}>
Gang Management
</a>
<a className={!management?"a-link-button-inactive":"a-link-button"}
style={{display: "inline-block"}}
onClick={() => setManagement(false)}>
Gang Territory
</a>
{management ? {management ?
<ManagementSubpage gang={props.gang} player={props.player} /> : <ManagementSubpage gang={props.gang} player={props.player} /> :
<TerritorySubpage gang={props.gang} />} <TerritorySubpage gang={props.gang} />}

@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react"; import React from "react";
import { GangMemberTasks } from "../GangMemberTasks"; import { GangMemberTasks } from "../GangMemberTasks";
import { GangMember } from "../GangMember"; import { GangMember } from "../GangMember";
@ -6,7 +6,7 @@ interface IProps {
member: GangMember; member: GangMember;
} }
export function Panel3(props: IProps): React.ReactElement { export function TaskDescription(props: IProps): React.ReactElement {
const task = GangMemberTasks[props.member.task]; const task = GangMemberTasks[props.member.task];
const desc = task ? task.desc: GangMemberTasks["Unassigned"].desc; const desc = task ? task.desc: GangMemberTasks["Unassigned"].desc;

@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react"; import React, { useState } from "react";
import { numeralWrapper } from "../../ui/numeralFormat"; import { numeralWrapper } from "../../ui/numeralFormat";
import { StatsTable } from "../../ui/React/StatsTable"; import { StatsTable } from "../../ui/React/StatsTable";
import { MoneyRate } from "../../ui/React/MoneyRate"; import { MoneyRate } from "../../ui/React/MoneyRate";
@ -11,7 +11,7 @@ interface IProps {
onTaskChange: () => void; onTaskChange: () => void;
} }
export function Panel2(props: IProps): React.ReactElement { export function TaskSelector(props: IProps): React.ReactElement {
const [currentTask, setCurrentTask] = useState(props.member.task); const [currentTask, setCurrentTask] = useState(props.member.task);
function onChange(event: React.ChangeEvent<HTMLSelectElement>): void { function onChange(event: React.ChangeEvent<HTMLSelectElement>): void {

@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react"; import React from "react";
import { numeralWrapper } from "../../ui/numeralFormat"; import { numeralWrapper } from "../../ui/numeralFormat";
import { dialogBoxCreate } from "../../../utils/DialogBox"; import { dialogBoxCreate } from "../../../utils/DialogBox";
import { formatNumber } from "../../../utils/StringHelperFunctions"; import { formatNumber } from "../../../utils/StringHelperFunctions";
@ -11,13 +11,13 @@ interface IProps {
export function TerritorySubpage(props: IProps): React.ReactElement { export function TerritorySubpage(props: IProps): React.ReactElement {
function openWarfareHelp(): void { function openWarfareHelp(): void {
dialogBoxCreate("This percentage represents the chance you have of 'clashing' with " + dialogBoxCreate("This percentage represents the chance you have of " +
"with another gang. If you do not wish to gain/lose territory, " + "'clashing' with with another gang. If you do not " +
"then keep this percentage at 0% by not engaging in territory " + "wish to gain/lose territory, then keep this " +
"warfare.") "percentage at 0% by not engaging in territory warfare.");
} }
function formatTerritoryP(n: number): string { function formatTerritory(n: number): string {
const v = n * 100; const v = n * 100;
if (v <= 0) { if (v <= 0) {
return formatNumber(0, 2); return formatNumber(0, 2);
@ -35,7 +35,7 @@ export function TerritorySubpage(props: IProps): React.ReactElement {
return (<span key={name}> return (<span key={name}>
<u>{name}</u><br /> <u>{name}</u><br />
Power: {formatNumber(power, 6)}<br /> Power: {formatNumber(power, 6)}<br />
Territory: {formatTerritoryP(AllGangs[name].territory)}%<br /> Territory: {formatTerritory(AllGangs[name].territory)}%<br />
Chance to win clash with this gang: {numeralWrapper.formatPercentage(clashVictoryChance, 3)}<br /> Chance to win clash with this gang: {numeralWrapper.formatPercentage(clashVictoryChance, 3)}<br />
<br /> <br />
</span>); </span>);
@ -72,8 +72,17 @@ export function TerritorySubpage(props: IProps): React.ReactElement {
<br /> <br />
<br /> <br />
</p> </p>
<input checked={props.gang.territoryWarfareEngaged} id="warfare" type="checkbox" style={{display: "inline-block", margin: "2px"}} onChange={(event)=> props.gang.territoryWarfareEngaged = event.target.checked}/> <input
<label htmlFor="warfare" className="tooltip" style={{color: "white", display: 'inline-block'}}> checked={props.gang.territoryWarfareEngaged}
id="warfare"
type="checkbox"
style={{display: "inline-block", margin: "2px"}}
onChange={(event)=>
props.gang.territoryWarfareEngaged = event.target.checked}/>
<label
htmlFor="warfare"
className="tooltip"
style={{color: "white", display: 'inline-block'}}>
Engage in Territory Warfare Engage in Territory Warfare
<span className="tooltiptext" style={{display: "inline-block"}}> <span className="tooltiptext" style={{display: "inline-block"}}>
Engaging in Territory Warfare sets your clash chance to 100%. Engaging in Territory Warfare sets your clash chance to 100%.
@ -85,10 +94,19 @@ export function TerritorySubpage(props: IProps): React.ReactElement {
<p style={{display: 'inline-block'}}> <p style={{display: 'inline-block'}}>
Territory Clash Chance: {numeralWrapper.formatPercentage(props.gang.territoryClashChance, 3)} Territory Clash Chance: {numeralWrapper.formatPercentage(props.gang.territoryClashChance, 3)}
</p> </p>
<div className="help-tip" style={{display: "inline-block"}} onClick={openWarfareHelp}>?</div> <div
className="help-tip"
style={{display: "inline-block"}}
onClick={openWarfareHelp}>?</div>
<br /> <br />
<input checked={props.gang.notifyMemberDeath} id="notify" type="checkbox" style={{display: "inline-block", margin: "2px"}} onChange={(event)=> props.gang.notifyMemberDeath = event.target.checked}/> <input
checked={props.gang.notifyMemberDeath}
id="notify"
type="checkbox"
style={{display: "inline-block", margin: "2px"}}
onChange={(event)=>
props.gang.notifyMemberDeath = event.target.checked}/>
<label htmlFor="warfare" className="tooltip" style={{color: "white", display: 'inline-block'}}> <label htmlFor="warfare" className="tooltip" style={{color: "white", display: 'inline-block'}}>
Notify about Gang Member Deaths Notify about Gang Member Deaths
<span className="tooltiptext" style={{display: "inline-block"}}> <span className="tooltiptext" style={{display: "inline-block"}}>
@ -101,7 +119,7 @@ export function TerritorySubpage(props: IProps): React.ReactElement {
<p> <p>
<b><u>{props.gang.facName}</u></b><br /> <b><u>{props.gang.facName}</u></b><br />
Power: {formatNumber(AllGangs[props.gang.facName].power, 6)}<br /> Power: {formatNumber(AllGangs[props.gang.facName].power, 6)}<br />
Territory: {formatTerritoryP(AllGangs[props.gang.facName].territory)}%<br /> Territory: {formatTerritory(AllGangs[props.gang.facName].territory)}%<br />
<br /> <br />
{gangNames.map(name => otherGangTerritory(name))} {gangNames.map(name => otherGangTerritory(name))}
</p> </p>