part 1 of converting gang to react

This commit is contained in:
Olivier Gagnon 2021-06-14 15:42:38 -04:00
parent 67e5e413e4
commit 25f546c691
18 changed files with 1251 additions and 267 deletions

@ -172,7 +172,7 @@ export class CodingContract {
async prompt(): Promise<CodingContractResult> {
const popupId = `coding-contract-prompt-popup-${this.fn}`;
return new Promise<CodingContractResult>((resolve) => {
const popup = new CodingContractPopup({
createPopup(popupId, CodingContractPopup, {
c: this,
popupId: popupId,
onClose: () => {
@ -188,7 +188,6 @@ export class CodingContract {
removePopup(popupId);
},
});
createPopup(popupId, CodingContractPopup, popup.props);
});
}

@ -4,8 +4,8 @@
* balance point to keep them from running out of control
*/
import { gangMemberTasksMetadata } from "./data/gangmembertasks";
import { gangMemberUpgradesMetadata } from "./data/gangmemberupgrades";
import { gangMemberTasksMetadata } from "./Gang/data/tasks";
import { gangMemberUpgradesMetadata } from "./Gang/data/upgrades";
import { Engine } from "./engine";
import { Faction } from "./Faction/Faction";
@ -42,16 +42,19 @@ import { Money } from "./ui/React/Money";
import { MoneyRate } from "./ui/React/MoneyRate";
import { Reputation } from "./ui/React/Reputation";
// import { GangMember as GM } from "./Gang/GangMember";
import { GangMemberUpgrade } from "./Gang/GangMemberUpgrade";
import { GangMemberUpgrades } from "./Gang/GangMemberUpgrades";
import { GangConstants } from "./Gang/data/Constants";
import { GangMemberTasks } from "./Gang/GangMemberTasks";
import { GangMemberTask } from "./Gang/GangMemberTask";
import { Panel1 } from "./Gang/ui/Panel1";
import React from "react";
import ReactDOM from "react-dom";
import { renderToStaticMarkup } from "react-dom/server"
// Constants
const GangRespectToReputationRatio = 5; // Respect is divided by this to get rep gain
const MaximumGangMembers = 30;
const CyclesPerTerritoryAndPowerUpdate = 100;
const AscensionMultiplierRatio = 15 / 100; // Portion of upgrade multiplier that is kept after ascending
// Switch between territory and management screen with 1 and 2
$(document).keydown(function(event) {
if (routing.isOn(Page.Gang) && event.altKey) {
@ -245,7 +248,7 @@ Gang.prototype.processGains = function(numCycles=1, player) {
dialogBoxCreate("ERROR: Could not get Faction associates with your gang. This is a bug, please report to game dev");
} else {
let favorMult = 1 + (fac.favor / 100);
fac.playerReputation += ((player.faction_rep_mult * gain * favorMult) / GangRespectToReputationRatio);
fac.playerReputation += ((player.faction_rep_mult * gain * favorMult) / GangConstants.GangRespectToReputationRatio);
}
// Keep track of respect gained per member
@ -289,8 +292,8 @@ function calculateTerritoryGain(winGang, loseGang) {
Gang.prototype.processTerritoryAndPowerGains = function(numCycles=1) {
this.storedTerritoryAndPowerCycles += numCycles;
if (this.storedTerritoryAndPowerCycles < CyclesPerTerritoryAndPowerUpdate) { return; }
this.storedTerritoryAndPowerCycles -= CyclesPerTerritoryAndPowerUpdate;
if (this.storedTerritoryAndPowerCycles < GangConstants.CyclesPerTerritoryAndPowerUpdate) { return; }
this.storedTerritoryAndPowerCycles -= GangConstants.CyclesPerTerritoryAndPowerUpdate;
// Process power first
const gangName = this.facName;
@ -380,7 +383,7 @@ Gang.prototype.processTerritoryAndPowerGains = function(numCycles=1) {
}
Gang.prototype.canRecruitMember = function() {
if (this.members.length >= MaximumGangMembers) { return false; }
if (this.members.length >= GangConstants.MaximumGangMembers) { return false; }
return (this.respect >= this.getRespectNeededToRecruitMember());
}
@ -848,12 +851,12 @@ GangMember.prototype.getAscensionResults = function() {
// Subtract 1 because we're only interested in the actual "bonus" part
const eff = this.getAscensionEfficiency();
return {
hack: (Math.max(0, hack - 1) * AscensionMultiplierRatio * eff.hack),
str: (Math.max(0, str - 1) * AscensionMultiplierRatio * eff.str),
def: (Math.max(0, def - 1) * AscensionMultiplierRatio * eff.def),
dex: (Math.max(0, dex - 1) * AscensionMultiplierRatio * eff.dex),
agi: (Math.max(0, agi - 1) * AscensionMultiplierRatio * eff.agi),
cha: (Math.max(0, cha - 1) * AscensionMultiplierRatio * eff.cha),
hack: (Math.max(0, hack - 1) * GangConstants.AscensionMultiplierRatio * eff.hack),
str: (Math.max(0, str - 1) * GangConstants.AscensionMultiplierRatio * eff.str),
def: (Math.max(0, def - 1) * GangConstants.AscensionMultiplierRatio * eff.def),
dex: (Math.max(0, dex - 1) * GangConstants.AscensionMultiplierRatio * eff.dex),
agi: (Math.max(0, agi - 1) * GangConstants.AscensionMultiplierRatio * eff.agi),
cha: (Math.max(0, cha - 1) * GangConstants.AscensionMultiplierRatio * eff.cha),
}
}
@ -894,136 +897,6 @@ GangMember.fromJSON = function(value) {
Reviver.constructors.GangMember = GangMember;
// Defines tasks that Gang Members can work on
function GangMemberTask(name="", desc="", isHacking=false, isCombat=false,
params={baseRespect: 0, baseWanted: 0, baseMoney: 0,
hackWeight: 0, strWeight: 0, defWeight: 0,
dexWeight: 0, agiWeight: 0, chaWeight: 0,
difficulty: 0}) {
this.name = name;
this.desc = desc;
// Flags that describe whether this Task is applicable for Hacking/Combat gangs
this.isHacking = isHacking;
this.isCombat = isCombat;
// Base gain rates for respect/wanted/money
this.baseRespect = params.baseRespect ? params.baseRespect : 0;
this.baseWanted = params.baseWanted ? params.baseWanted : 0;
this.baseMoney = params.baseMoney ? params.baseMoney : 0;
// Weighting for the effect that each stat has on the tasks effectiveness.
// Weights must add up to 100
this.hackWeight = params.hackWeight ? params.hackWeight : 0;
this.strWeight = params.strWeight ? params.strWeight : 0;
this.defWeight = params.defWeight ? params.defWeight : 0;
this.dexWeight = params.dexWeight ? params.dexWeight : 0;
this.agiWeight = params.agiWeight ? params.agiWeight : 0;
this.chaWeight = params.chaWeight ? params.chaWeight : 0;
if (Math.round(this.hackWeight + this.strWeight + this.defWeight + this.dexWeight + this.agiWeight + this.chaWeight) != 100) {
console.error(`GangMemberTask ${this.name} weights do not add up to 100`);
}
// 1 - 100
this.difficulty = params.difficulty ? params.difficulty : 1;
// Territory Factors. Exponential factors that dictate how territory affects gains
// Formula: Territory Mutiplier = (Territory * 100) ^ factor / 100
// So factor should be > 1 if something should scale exponentially with territory
// and should be < 1 if it should have diminshing returns
this.territory = params.territory ? params.territory : {money: 1, respect: 1, wanted: 1};
}
GangMemberTask.prototype.toJSON = function() {
return Generic_toJSON("GangMemberTask", this);
}
GangMemberTask.fromJSON = function(value) {
return Generic_fromJSON(GangMemberTask, value.data);
}
Reviver.constructors.GangMemberTask = GangMemberTask;
export const GangMemberTasks = {};
function addGangMemberTask(name, desc, isHacking, isCombat, params) {
GangMemberTasks[name] = new GangMemberTask(name, desc, isHacking, isCombat, params);
}
gangMemberTasksMetadata.forEach((e) => {
addGangMemberTask(e.name, e.desc, e.isHacking, e.isCombat, e.params);
});
function GangMemberUpgrade(name="", cost=0, type="w", mults={}) {
this.name = name;
this.cost = cost;
this.type = type; //w = weapon, a = armor, v = vehicle, r = rootkit, g = Aug
this.mults = mults;
this.createDescription();
}
GangMemberUpgrade.prototype.getCost = function(gang) {
const discount = gang.getDiscount();
return this.cost / discount;
}
GangMemberUpgrade.prototype.createDescription = function() {
const lines = ["Increases:"];
if (this.mults.str != null) {
lines.push(`* Strength by ${Math.round((this.mults.str - 1) * 100)}%`);
}
if (this.mults.def != null) {
lines.push(`* Defense by ${Math.round((this.mults.def - 1) * 100)}%`);
}
if (this.mults.dex != null) {
lines.push(`* Dexterity by ${Math.round((this.mults.dex - 1) * 100)}%`);
}
if (this.mults.agi != null) {
lines.push(`* Agility by ${Math.round((this.mults.agi - 1) * 100)}%`);
}
if (this.mults.cha != null) {
lines.push(`* Charisma by ${Math.round((this.mults.cha - 1) * 100)}%`);
}
if (this.mults.hack != null) {
lines.push(`* Hacking by ${Math.round((this.mults.hack - 1) * 100)}%`);
}
this.desc = lines.join("<br>");
}
// Passes in a GangMember object
GangMemberUpgrade.prototype.apply = function(member) {
if (this.mults.str != null) { member.str_mult *= this.mults.str; }
if (this.mults.def != null) { member.def_mult *= this.mults.def; }
if (this.mults.dex != null) { member.dex_mult *= this.mults.dex; }
if (this.mults.agi != null) { member.agi_mult *= this.mults.agi; }
if (this.mults.cha != null) { member.cha_mult *= this.mults.cha; }
if (this.mults.hack != null) { member.hack_mult *= this.mults.hack; }
return;
}
GangMemberUpgrade.prototype.toJSON = function() {
return Generic_toJSON("GangMemberUpgrade", this);
}
GangMemberUpgrade.fromJSON = function(value) {
return Generic_fromJSON(GangMemberUpgrade, value.data);
}
Reviver.constructors.GangMemberUpgrade = GangMemberUpgrade;
// Initialize Gang Member Upgrades
export const GangMemberUpgrades = {}
function addGangMemberUpgrade(name, cost, type, mults) {
GangMemberUpgrades[name] = new GangMemberUpgrade(name, cost, type, mults);
}
gangMemberUpgradesMetadata.forEach((e) => {
addGangMemberUpgrade(e.name, e.cost, e.upgType, e.mults);
});
// Create a pop-up box that lets player purchase upgrades
Gang.prototype.createGangMemberUpgradeBox = function(player, initialFilter="") {
const boxId = "gang-member-upgrade-popup-box";
@ -1721,7 +1594,7 @@ Gang.prototype.updateGangContent = function() {
const respectCost = this.getRespectNeededToRecruitMember();
const btn = UIElems.gangRecruitMemberButton;
if (numMembers >= MaximumGangMembers) {
if (numMembers >= GangConstants.MaximumGangMembers) {
btn.className = "a-link-button-inactive";
UIElems.gangRecruitRequirementText.style.display = "inline-block";
UIElems.gangRecruitRequirementText.innerHTML = "You have reached the maximum amount of gang members";

309
src/Gang/GangMember.ts Normal file

@ -0,0 +1,309 @@
import { Page, routing } from "../ui/navigationTracking";
import { Generic_fromJSON, Generic_toJSON, Reviver } from "../../utils/JSONReviver";
export class GangMember {
name: string;
task: string = "Unassigned";
earnedRespect: number = 0;
hack: number = 1;
str: number = 1;
def: number = 1;
dex: number = 1;
agi: number = 1;
cha: number = 1;
hack_exp: number = 0;
str_exp: number = 0;
def_exp: number = 0;
dex_exp: number = 0;
agi_exp: number = 0;
cha_exp: number = 0;
hack_mult: number = 1;
str_mult: number = 1;
def_mult: number = 1;
dex_mult: number = 1;
agi_mult: number = 1;
cha_mult: number = 1;
hack_asc_mult: number = 1;
str_asc_mult: number = 1;
def_asc_mult: number = 1;
dex_asc_mult: number = 1;
agi_asc_mult: number = 1;
cha_asc_mult: number = 1;
upgrades: string[] = []; // Names of upgrades
augmentations: string[] = []; // Names of augmentations only
constructor(name: string = "") {
this.name = name;
}
// // Same skill calculation formula as Player
// calculateSkill(exp: number, mult: number = 1): number {
// return Math.max(Math.floor(mult * (32 * Math.log(exp + 534.5) - 200)), 1);
// }
// updateSkillLevels(): void {
// this.hack = this.calculateSkill(this.hack_exp, this.hack_mult * this.hack_asc_mult);
// this.str = this.calculateSkill(this.str_exp, this.str_mult * this.str_asc_mult);
// this.def = this.calculateSkill(this.def_exp, this.def_mult * this.def_asc_mult);
// this.dex = this.calculateSkill(this.dex_exp, this.dex_mult * this.dex_asc_mult);
// this.agi = this.calculateSkill(this.agi_exp, this.agi_mult * this.agi_asc_mult);
// this.cha = this.calculateSkill(this.cha_exp, this.cha_mult * this.cha_asc_mult);
// }
// calculatePower(): number {
// return (this.hack + this.str + this.def + this.dex + this.agi + this.cha) / 95;
// }
// assignToTask(taskName: string): boolean {
// if (GangMemberTasks.hasOwnProperty(taskName)) {
// this.task = taskName;
// return true;
// } else {
// this.task = "Unassigned";
// return false;
// }
// }
// unassignFromTask(): string {
// this.task = "Unassigned";
// }
// getTask(): string {
// // Backwards compatibility
// if (this.task instanceof GangMemberTask) {
// this.task = this.task.name;
// }
// if (GangMemberTasks.hasOwnProperty(this.task)) {
// return GangMemberTasks[this.task];
// }
// return GangMemberTasks["Unassigned"];
// }
// // Gains are per cycle
// calculateRespectGain(gang: any): number {
// const task = this.getTask();
// if (task == null || !(task instanceof GangMemberTask) || task.baseRespect === 0) {return 0;}
// let statWeight = (task.hackWeight/100) * this.hack +
// (task.strWeight/100) * this.str +
// (task.defWeight/100) * this.def +
// (task.dexWeight/100) * this.dex +
// (task.agiWeight/100) * this.agi +
// (task.chaWeight/100) * this.cha;
// statWeight -= (4 * task.difficulty);
// if (statWeight <= 0) { return 0; }
// const territoryMult = Math.pow(AllGangs[gang.facName].territory * 100, task.territory.respect) / 100;
// if (isNaN(territoryMult) || territoryMult <= 0) { return 0; }
// const respectMult = gang.getWantedPenalty();
// return 11 * task.baseRespect * statWeight * territoryMult * respectMult;
// }
// calculateWantedLevelGain(gang: any): number {
// const task = this.getTask();
// if (task == null || !(task instanceof GangMemberTask) || task.baseWanted === 0) { return 0; }
// let statWeight = (task.hackWeight / 100) * this.hack +
// (task.strWeight / 100) * this.str +
// (task.defWeight / 100) * this.def +
// (task.dexWeight / 100) * this.dex +
// (task.agiWeight / 100) * this.agi +
// (task.chaWeight / 100) * this.cha;
// statWeight -= (3.5 * task.difficulty);
// if (statWeight <= 0) { return 0; }
// const territoryMult = Math.pow(AllGangs[gang.facName].territory * 100, task.territory.wanted) / 100;
// if (isNaN(territoryMult) || territoryMult <= 0) { return 0; }
// if (task.baseWanted < 0) {
// return 0.4 * task.baseWanted * statWeight * territoryMult;
// } else {
// const calc = 7 * task.baseWanted / (Math.pow(3 * statWeight * territoryMult, 0.8));
// // Put an arbitrary cap on this to prevent wanted level from rising too fast if the
// // denominator is very small. Might want to rethink formula later
// return Math.min(100, calc);
// }
// }
// calculateMoneyGain(gang: any): number {
// const task = this.getTask();
// if (task == null || !(task instanceof GangMemberTask) || task.baseMoney === 0) {return 0;}
// let statWeight = (task.hackWeight/100) * this.hack +
// (task.strWeight/100) * this.str +
// (task.defWeight/100) * this.def +
// (task.dexWeight/100) * this.dex +
// (task.agiWeight/100) * this.agi +
// (task.chaWeight/100) * this.cha;
// statWeight -= (3.2 * task.difficulty);
// if (statWeight <= 0) { return 0; }
// const territoryMult = Math.pow(AllGangs[gang.facName].territory * 100, task.territory.money) / 100;
// if (isNaN(territoryMult) || territoryMult <= 0) { return 0; }
// const respectMult = gang.getWantedPenalty();
// return 5 * task.baseMoney * statWeight * territoryMult * respectMult;
// }
// gainExperience(numCycles: number = 1): void {
// const task = this.getTask();
// if (task == null || !(task instanceof GangMemberTask) || task === GangMemberTasks["Unassigned"]) {return;}
// const difficultyMult = Math.pow(task.difficulty, 0.9);
// const difficultyPerCycles = difficultyMult * numCycles;
// const weightDivisor = 1500;
// this.hack_exp += (task.hackWeight / weightDivisor) * difficultyPerCycles;
// this.str_exp += (task.strWeight / weightDivisor) * difficultyPerCycles;
// this.def_exp += (task.defWeight / weightDivisor) * difficultyPerCycles;
// this.dex_exp += (task.dexWeight / weightDivisor) * difficultyPerCycles;
// this.agi_exp += (task.agiWeight / weightDivisor) * difficultyPerCycles;
// this.cha_exp += (task.chaWeight / weightDivisor) * difficultyPerCycles;
// }
// recordEarnedRespect(numCycles: number = 1, gang: any): void {
// this.earnedRespect += (this.calculateRespectGain(gang) * numCycles);
// }
// ascend(): any {
// const res = this.getAscensionResults();
// const hackAscMult = res.hack;
// const strAscMult = res.str;
// const defAscMult = res.def;
// const dexAscMult = res.dex;
// const agiAscMult = res.agi;
// const chaAscMult = res.cha;
// this.hack_asc_mult += hackAscMult;
// this.str_asc_mult += strAscMult;
// this.def_asc_mult += defAscMult;
// this.dex_asc_mult += dexAscMult;
// this.agi_asc_mult += agiAscMult;
// this.cha_asc_mult += chaAscMult;
// // Remove upgrades. Then re-calculate multipliers and stats
// this.upgrades.length = 0;
// this.hack_mult = 1;
// this.str_mult = 1;
// this.def_mult = 1;
// this.dex_mult = 1;
// this.agi_mult = 1;
// this.cha_mult = 1;
// for (let i = 0; i < this.augmentations.length; ++i) {
// let aug = GangMemberUpgrades[this.augmentations[i]];
// aug.apply(this);
// }
// // Clear exp and recalculate stats
// this.hack_exp = 0;
// this.str_exp = 0;
// this.def_exp = 0;
// this.dex_exp = 0;
// this.agi_exp = 0;
// this.cha_exp = 0;
// this.updateSkillLevels();
// const respectToDeduct = this.earnedRespect;
// this.earnedRespect = 0;
// return {
// respect: respectToDeduct,
// hack: hackAscMult,
// str: strAscMult,
// def: defAscMult,
// dex: dexAscMult,
// agi: agiAscMult,
// cha: chaAscMult,
// };
// }
// getAscensionEfficiency(): any {
// function formula(mult) {
// return 1/(1+Math.log(mult)/Math.log(20));
// }
// return {
// hack: formula(this.hack_asc_mult),
// str: formula(this.str_asc_mult),
// def: formula(this.def_asc_mult),
// dex: formula(this.dex_asc_mult),
// agi: formula(this.agi_asc_mult),
// cha: formula(this.cha_asc_mult),
// };
// }
// // Returns the multipliers that would be gained from ascension
// getAscensionResults(): any {
// /**
// * Calculate ascension bonus to stat multipliers.
// * This is based on the current number of multipliers from Non-Augmentation upgrades
// * + Ascension Bonus = N% of current bonus from Augmentations
// */
// let hack = 1;
// let str = 1;
// let def = 1;
// let dex = 1;
// let agi = 1;
// let cha = 1;
// for (let i = 0; i < this.upgrades.length; ++i) {
// const upg = GangMemberUpgrades[this.upgrades[i]];
// if (upg.mults.hack != null) { hack *= upg.mults.hack; }
// if (upg.mults.str != null) { str *= upg.mults.str; }
// if (upg.mults.def != null) { def *= upg.mults.def; }
// if (upg.mults.dex != null) { dex *= upg.mults.dex; }
// if (upg.mults.agi != null) { agi *= upg.mults.agi; }
// if (upg.mults.cha != null) { cha *= upg.mults.cha; }
// }
// // Subtract 1 because we're only interested in the actual "bonus" part
// const eff = this.getAscensionEfficiency();
// return {
// hack: (Math.max(0, hack - 1) * AscensionMultiplierRatio * eff.hack),
// str: (Math.max(0, str - 1) * AscensionMultiplierRatio * eff.str),
// def: (Math.max(0, def - 1) * AscensionMultiplierRatio * eff.def),
// dex: (Math.max(0, dex - 1) * AscensionMultiplierRatio * eff.dex),
// agi: (Math.max(0, agi - 1) * AscensionMultiplierRatio * eff.agi),
// cha: (Math.max(0, cha - 1) * AscensionMultiplierRatio * eff.cha),
// }
// }
// buyUpgrade(upg: any, player: any, gang: any): boolean {
// if (typeof upg === 'string') {
// upg = GangMemberUpgrades[upg];
// }
// if (!(upg instanceof GangMemberUpgrade)) {
// return false;
// }
// // Prevent purchasing of already-owned upgrades
// if (this.augmentations.includes(upg.name) || this.upgrades.includes(upg.name)) {
// return false;
// }
// if (player.money.lt(upg.getCost(gang))) { return false; }
// player.loseMoney(upg.getCost(gang));
// if (upg.type === "g") {
// this.augmentations.push(upg.name);
// } else {
// this.upgrades.push(upg.name);
// }
// upg.apply(this);
// if (routing.isOn(Page.Gang) && UIElems.gangMemberUpgradeBoxOpened) {
// var initFilterValue = UIElems.gangMemberUpgradeBoxFilter.value.toString();
// gang.createGangMemberUpgradeBox(player, initFilterValue);
// }
// return true;
// }
/**
* Serialize the current object to a JSON save state.
*/
toJSON(): any {
return Generic_toJSON("GangMember", this);
}
/**
* Initiatizes a GangMember object from a JSON save state.
*/
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
static fromJSON(value: any): GangMember {
return Generic_fromJSON(GangMember, value.data);
}
}
Reviver.constructors.GangMember = GangMember;

@ -0,0 +1,81 @@
interface ITerritory {
money: number;
respect: number;
wanted: number;
}
export interface ITaskParams {
baseRespect?: number;
baseWanted?: number;
baseMoney?: number;
hackWeight?: number;
strWeight?: number;
defWeight?: number;
dexWeight?: number;
agiWeight?: number;
chaWeight?: number;
difficulty?: number;
territory?: ITerritory;
}
export class GangMemberTask {
name: string;
desc: string;
isHacking: boolean;
isCombat: boolean;
baseRespect: number;
baseWanted: number;
baseMoney: number;
hackWeight: number;
strWeight: number;
defWeight: number;
dexWeight: number;
agiWeight: number;
chaWeight: number;
difficulty: number;
territory: ITerritory;
// Defines tasks that Gang Members can work on
constructor(name: string, desc: string, isHacking: boolean, isCombat: boolean, params: ITaskParams) {
this.name = name;
this.desc = desc;
// Flags that describe whether this Task is applicable for Hacking/Combat gangs
this.isHacking = isHacking;
this.isCombat = isCombat;
// Base gain rates for respect/wanted/money
this.baseRespect = params.baseRespect ? params.baseRespect : 0;
this.baseWanted = params.baseWanted ? params.baseWanted : 0;
this.baseMoney = params.baseMoney ? params.baseMoney : 0;
// Weighting for the effect that each stat has on the tasks effectiveness.
// Weights must add up to 100
this.hackWeight = params.hackWeight ? params.hackWeight : 0;
this.strWeight = params.strWeight ? params.strWeight : 0;
this.defWeight = params.defWeight ? params.defWeight : 0;
this.dexWeight = params.dexWeight ? params.dexWeight : 0;
this.agiWeight = params.agiWeight ? params.agiWeight : 0;
this.chaWeight = params.chaWeight ? params.chaWeight : 0;
if (Math.round(this.hackWeight + this.strWeight + this.defWeight + this.dexWeight + this.agiWeight + this.chaWeight) != 100) {
console.error(`GangMemberTask ${this.name} weights do not add up to 100`);
}
// 1 - 100
this.difficulty = params.difficulty ? params.difficulty : 1;
// Territory Factors. Exponential factors that dictate how territory affects gains
// Formula: Territory Mutiplier = (Territory * 100) ^ factor / 100
// So factor should be > 1 if something should scale exponentially with territory
// and should be < 1 if it should have diminshing returns
this.territory = params.territory ? params.territory : {money: 1, respect: 1, wanted: 1};
}
}

@ -0,0 +1,12 @@
import { gangMemberTasksMetadata } from "./data/tasks";
import { GangMemberTask } from "./GangMemberTask";
export const GangMemberTasks: {
[key: string]: GangMemberTask;
} = {};
(function() {
gangMemberTasksMetadata.forEach((e) => {
GangMemberTasks[e.name] = new GangMemberTask(e.name, e.desc, e.isHacking, e.isCombat, e.params);
});
})();

@ -0,0 +1,56 @@
import { IMults } from "./data/upgrades";
export class GangMemberUpgrade {
name: string;
cost: number;
type: string;
desc: string;
mults: IMults;
constructor(name: string = "", cost: number = 0, type: string = "w", mults: IMults = {}) {
this.name = name;
this.cost = cost;
//w = weapon, a = armor, v = vehicle, r = rootkit, g = Aug
this.type = type;
this.mults = mults;
this.desc = this.createDescription();
}
getCost(gang: any) {
return this.cost / gang.getDiscount();
}
createDescription(): string {
const lines = ["Increases:"];
if (this.mults.str != null) {
lines.push(`* Strength by ${Math.round((this.mults.str - 1) * 100)}%`);
}
if (this.mults.def != null) {
lines.push(`* Defense by ${Math.round((this.mults.def - 1) * 100)}%`);
}
if (this.mults.dex != null) {
lines.push(`* Dexterity by ${Math.round((this.mults.dex - 1) * 100)}%`);
}
if (this.mults.agi != null) {
lines.push(`* Agility by ${Math.round((this.mults.agi - 1) * 100)}%`);
}
if (this.mults.cha != null) {
lines.push(`* Charisma by ${Math.round((this.mults.cha - 1) * 100)}%`);
}
if (this.mults.hack != null) {
lines.push(`* Hacking by ${Math.round((this.mults.hack - 1) * 100)}%`);
}
return lines.join("<br>");
}
// Passes in a GangMember object
apply(member: any) {
if (this.mults.str != null) { member.str_mult *= this.mults.str; }
if (this.mults.def != null) { member.def_mult *= this.mults.def; }
if (this.mults.dex != null) { member.dex_mult *= this.mults.dex; }
if (this.mults.agi != null) { member.agi_mult *= this.mults.agi; }
if (this.mults.cha != null) { member.cha_mult *= this.mults.cha; }
if (this.mults.hack != null) { member.hack_mult *= this.mults.hack; }
}
}

@ -0,0 +1,12 @@
import { gangMemberUpgradesMetadata } from "./data/upgrades";
import { GangMemberUpgrade } from "./GangMemberUpgrade";
export const GangMemberUpgrades: {
[key: string]: GangMemberUpgrade;
} = {};
(function() {
gangMemberUpgradesMetadata.forEach((e) => {
GangMemberUpgrades[e.name] = new GangMemberUpgrade(e.name, e.cost, e.upgType, e.mults);
});
})();

@ -0,0 +1,13 @@
export const GangConstants: {
GangRespectToReputationRatio: number;
MaximumGangMembers: number;
CyclesPerTerritoryAndPowerUpdate: number;
AscensionMultiplierRatio: number;
} = {
// Respect is divided by this to get rep gain
GangRespectToReputationRatio: 5,
MaximumGangMembers: 30,
CyclesPerTerritoryAndPowerUpdate: 100,
// Portion of upgrade multiplier that is kept after ascending
AscensionMultiplierRatio : 15,
};

284
src/Gang/data/tasks.ts Normal file

@ -0,0 +1,284 @@
/* tslint:disable:max-line-length */
/**
* Defines the parameters that can be used to initialize and describe a GangMemberTask
* (defined in Gang.js)
*/
export interface IGangMemberTaskMetadata {
/**
* Description of the task
*/
desc: string;
/**
* Whether or not this task is meant for Combat-type gangs
*/
isCombat: boolean;
/**
* Whether or not this task is for Hacking-type gangs
*/
isHacking: boolean;
/**
* Name of the task
*/
name: string;
/**
* An object containing weighting parameters for the task. These parameters are used for
* various calculations (respect gain, wanted gain, etc.)
*/
params?: any;
}
/**
* Array of metadata for all Gang Member tasks. Used to construct the global GangMemberTask
* objects in Gang.js
*/
export const gangMemberTasksMetadata: IGangMemberTaskMetadata[] = [
{
desc: "This gang member is currently idle",
isCombat: true,
isHacking: true,
name: "Unassigned",
params: {hackWeight: 100}, // This is just to get by the weight check in the GangMemberTask constructor
},
{
desc: "Assign this gang member to create and distribute ransomware<br><br>Earns money - Slightly increases respect - Slightly increases wanted level",
isCombat: false,
isHacking: true,
name: "Ransomware",
params: {baseRespect: 0.00005, baseWanted: 0.0001, baseMoney: 1, hackWeight: 100, difficulty: 1},
},
{
desc: "Assign this gang member to attempt phishing scams and attacks<br><br>Earns money - Slightly increases respect - Slightly increases wanted level",
isCombat: false,
isHacking: true,
name: "Phishing",
params: {baseRespect: 0.00008, baseWanted: 0.003, baseMoney: 2.5, hackWeight: 85, chaWeight: 15, difficulty: 3.5},
},
{
desc: "Assign this gang member to attempt identity theft<br><br>Earns money - Increases respect - Increases wanted level",
isCombat: false,
isHacking: true,
name: "Identity Theft",
params: {baseRespect: 0.0001, baseWanted: 0.075, baseMoney: 6, hackWeight: 80, chaWeight: 20, difficulty: 5},
},
{
desc: "Assign this gang member to carry out DDoS attacks<br><br>Increases respect - Increases wanted level",
isCombat: false,
isHacking: true,
name: "DDoS Attacks",
params: {baseRespect: 0.0004, baseWanted: 0.2, hackWeight: 100, difficulty: 8},
},
{
desc: "Assign this gang member to create and distribute malicious viruses<br><br>Increases respect - Increases wanted level",
isCombat: false,
isHacking: true,
name: "Plant Virus",
params: {baseRespect: 0.0006, baseWanted: 0.4, hackWeight: 100, difficulty: 12},
},
{
desc: "Assign this gang member to commit financial fraud and digital counterfeiting<br><br>Earns money - Slightly increases respect - Slightly increases wanted level",
isCombat: false,
isHacking: true,
name: "Fraud & Counterfeiting",
params: {baseRespect: 0.0004, baseWanted: 0.3, baseMoney: 15, hackWeight: 80, chaWeight: 20, difficulty: 20},
},
{
desc: "Assign this gang member to launder money<br><br>Earns money - Increases respect - Increases wanted level",
isCombat: false,
isHacking: true,
name: "Money Laundering",
params: {baseRespect: 0.001, baseWanted: 1.25, baseMoney: 120, hackWeight: 75, chaWeight: 25, difficulty: 25},
},
{
desc: "Assign this gang member to commit acts of cyberterrorism<br><br>Greatly increases respect - Greatly increases wanted level",
isCombat: false,
isHacking: true,
name: "Cyberterrorism",
params: {baseRespect: 0.01, baseWanted: 6, hackWeight: 80, chaWeight: 20, difficulty: 36},
},
{
desc: "Assign this gang member to be an ethical hacker for corporations<br><br>Earns money - Lowers wanted level",
isCombat: false,
isHacking: true,
name: "Ethical Hacking",
params: {baseWanted: -0.001, baseMoney: 1, hackWeight: 90, chaWeight: 10, difficulty: 1},
},
{
desc: "Assign this gang member to mug random people on the streets<br><br>Earns money - Slightly increases respect - Very slightly increases wanted level",
isCombat: true,
isHacking: false,
name: "Mug People",
params: {
baseRespect: 0.00005, baseWanted: 0.00005, baseMoney: 1.2,
strWeight: 25, defWeight: 25, dexWeight: 25, agiWeight: 10, chaWeight: 15,
difficulty: 1,
},
},
{
desc: "Assign this gang member to sell drugs<br><br>Earns money - Slightly increases respect - Slightly increases wanted level - Scales slightly with territory",
isCombat: true,
isHacking: false,
name: "Deal Drugs",
params: {
baseRespect: 0.00006, baseWanted: 0.002, baseMoney: 5,
agiWeight: 20, dexWeight: 20, chaWeight: 60,
difficulty: 3.5,
territory: {
money: 1.2,
respect: 1,
wanted: 1.15,
},
},
},
{
desc: "Assign this gang member to extort civilians in your territory<br><br>Earns money - Slightly increases respect - Increases wanted - Scales heavily with territory",
isCombat: true,
isHacking: false,
name: "Strongarm Civilians",
params: {
baseRespect: 0.00004, baseWanted: 0.02, baseMoney: 2.5,
hackWeight: 10, strWeight: 25, defWeight: 25, dexWeight: 20, agiWeight: 10, chaWeight: 10,
difficulty: 5,
territory: {
money: 1.6,
respect: 1.1,
wanted: 1.5,
},
},
},
{
desc: "Assign this gang member to run cons<br><br>Earns money - Increases respect - Increases wanted level",
isCombat: true,
isHacking: false,
name: "Run a Con",
params: {
baseRespect: 0.00012, baseWanted: 0.05, baseMoney: 15,
strWeight: 5, defWeight: 5, agiWeight: 25, dexWeight: 25, chaWeight: 40,
difficulty: 14,
},
},
{
desc: "Assign this gang member to commit armed robbery on stores, banks and armored cars<br><br>Earns money - Increases respect - Increases wanted level",
isCombat: true,
isHacking: false,
name: "Armed Robbery",
params: {
baseRespect: 0.00014, baseWanted: 0.1, baseMoney: 38,
hackWeight: 20, strWeight: 15, defWeight: 15, agiWeight: 10, dexWeight: 20, chaWeight: 20,
difficulty: 20,
},
},
{
desc: "Assign this gang member to traffick illegal arms<br><br>Earns money - Increases respect - Increases wanted level - Scales heavily with territory",
isCombat: true,
isHacking: false,
name: "Traffick Illegal Arms",
params: {
baseRespect: 0.0002, baseWanted: 0.24, baseMoney: 58,
hackWeight: 15, strWeight: 20, defWeight: 20, dexWeight: 20, chaWeight: 25,
difficulty: 32,
territory: {
money: 1.4,
respect: 1.3,
wanted: 1.25,
},
},
},
{
desc: "Assign this gang member to threaten and black mail high-profile targets<br><br>Earns money - Slightly increases respect - Slightly increases wanted level",
isCombat: true,
isHacking: false,
name: "Threaten & Blackmail",
params: {
baseRespect: 0.0002, baseWanted: 0.125, baseMoney: 24,
hackWeight: 25, strWeight: 25, dexWeight: 25, chaWeight: 25,
difficulty: 28,
},
},
{
desc: "Assign this gang member to engage in human trafficking operations<br><br>Earns money - Increases respect - Increases wanted level - Scales heavily with territory",
isCombat: true,
isHacking: false,
name: "Human Trafficking",
params: {
baseRespect: 0.004, baseWanted: 1.25, baseMoney: 120,
hackWeight: 30, strWeight: 5, defWeight: 5, dexWeight: 30, chaWeight: 30,
difficulty: 36,
territory: {
money: 1.5,
respect: 1.5,
wanted: 1.6,
},
},
},
{
desc: "Assign this gang member to commit acts of terrorism<br><br>Greatly increases respect - Greatly increases wanted level - Scales heavily with territory",
isCombat: true,
isHacking: false,
name: "Terrorism",
params: {
baseRespect: 0.01, baseWanted: 6,
hackWeight: 20, strWeight: 20, defWeight: 20, dexWeight: 20, chaWeight: 20,
difficulty: 36,
territory: {
money: 1,
respect: 2,
wanted: 2,
},
},
},
{
desc: "Assign this gang member to be a vigilante and protect the city from criminals<br><br>Decreases wanted level",
isCombat: true,
isHacking: true,
name: "Vigilante Justice",
params: {
baseWanted: -0.001,
hackWeight: 20, strWeight: 20, defWeight: 20, dexWeight: 20, agiWeight: 20,
difficulty: 1,
territory: {
money: 1,
respect: 1,
wanted: 0.9, // Gets harder with more territory
},
},
},
{
desc: "Assign this gang member to increase their combat stats (str, def, dex, agi)",
isCombat: true,
isHacking: true,
name: "Train Combat",
params: {
strWeight: 25, defWeight: 25, dexWeight: 25, agiWeight: 25,
difficulty: 5,
},
},
{
desc: "Assign this gang member to train their hacking skills",
isCombat: true,
isHacking: true,
name: "Train Hacking",
params: {hackWeight: 100, difficulty: 8},
},
{
desc: "Assign this gang member to train their charisma",
isCombat: true,
isHacking: true,
name: "Train Charisma",
params: {chaWeight: 100, difficulty: 8},
},
{
desc: "Assign this gang member to engage in territorial warfare with other gangs. Members assigned to this task will help increase your gang's territory and will defend your territory from being taken.",
isCombat: true,
isHacking: true,
name: "Territory Warfare",
params: {
hackWeight: 15, strWeight: 20, defWeight: 20, dexWeight: 20, agiWeight: 20, chaWeight: 5,
difficulty: 5,
},
},
];

218
src/Gang/data/upgrades.ts Normal file

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

143
src/Gang/ui/Panel1.tsx Normal file

@ -0,0 +1,143 @@
import * as React from "react";
import { GangMember } from "../GangMember";
import { dialogBoxCreate } from "../../../utils/DialogBox";
import { formatNumber } from "../../../utils/StringHelperFunctions";
import { numeralWrapper } from "../../ui/numeralFormat";
import { createPopup, removePopup } from "../../ui/React/createPopup";
interface IAscendProps {
member: any;
gang: any;
popupId: string;
}
function ascendPopup(props: IAscendProps): React.ReactElement {
function confirm() {
props.gang.ascendMember(props.member);
props.gang.updateGangMemberDisplayElement(props.member);
removePopup(props.popupId);
return false;
}
function cancel() {
removePopup(props.popupId);
return false;
}
const ascendBenefits = props.member.getAscensionResults();
return (<>
<pre>
Are you sure you want to ascend this member? They will lose all of
their non-Augmentation upgrades and their stats will reset back to 1.
Furthermore, your gang will lose {numeralWrapper.formatRespect(props.member.earnedRespect)} respect
In return, they will gain the following permanent boost to stat multipliers:
Hacking: +{numeralWrapper.formatPercentage(ascendBenefits.hack)}
Strength: +{numeralWrapper.formatPercentage(ascendBenefits.str)}
Defense: +{numeralWrapper.formatPercentage(ascendBenefits.def)}
Dexterity: +{numeralWrapper.formatPercentage(ascendBenefits.dex)}
Agility: +{numeralWrapper.formatPercentage(ascendBenefits.agi)}
Charisma: +{numeralWrapper.formatPercentage(ascendBenefits.cha)}
</pre>
<button className="std-button" onClick={confirm}>Ascend</button>
<button className="std-button" onClick={cancel}>Cancel</button>
</>);
}
interface IProps {
member: any;
gang: any;
}
export function Panel1(props: IProps): React.ReactElement {
function ascend() {
const popupId = `gang-management-ascend-member ${props.member.name}`;
createPopup(popupId, ascendPopup, {
member: props.member,
gang: props.gang,
popupId: popupId,
});
}
return (<>
<pre>
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 />
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 />
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 />
</pre>
<br />
<button className="accordion-button" onClick={ascend}>Ascend</button>
</>);
}
/*
const ascendButton = createElement("button", {
class: "accordion-button",
innerText: "Ascend",
clickListener: () => {
const popupId = `gang-management-ascend-member ${memberObj.name}`;
const ascendBenefits = memberObj.getAscensionResults();
const txt = createElement("pre", {
innerText: ["Are you sure you want to ascend this member? They will lose all of",
"their non-Augmentation upgrades and their stats will reset back to 1.",
"",
`Furthermore, your gang will lose ${numeralWrapper.formatRespect(memberObj.earnedRespect)} respect`,
"",
"In return, they will gain the following permanent boost to stat multipliers:\n",
`Hacking: +${numeralWrapper.formatPercentage(ascendBenefits.hack)}`,
`Strength: +${numeralWrapper.formatPercentage(ascendBenefits.str)}`,
`Defense: +${numeralWrapper.formatPercentage(ascendBenefits.def)}`,
`Dexterity: +${numeralWrapper.formatPercentage(ascendBenefits.dex)}`,
`Agility: +${numeralWrapper.formatPercentage(ascendBenefits.agi)}`,
`Charisma: +${numeralWrapper.formatPercentage(ascendBenefits.cha)}`].join("\n"),
});
const confirmBtn = createElement("button", {
class: "std-button",
clickListener: () => {
this.ascendMember(memberObj);
this.updateGangMemberDisplayElement(memberObj);
removePopup(popupId);
return false;
},
innerText: "Ascend",
});
const cancelBtn = createElement("button", {
class: "std-button",
clickListener: () => {
removePopup(popupId);
return false;
},
innerText: "Cancel",
});
createPopup(popupId, [txt, confirmBtn, cancelBtn]);
},
});
const ascendHelpTip = createElement("div", {
class: "help-tip",
clickListener: () => {
dialogBoxCreate(["Ascending a Gang Member resets the member's progress and stats in exchange",
"for a permanent boost to their stat multipliers.",
"<br><br>The additional stat multiplier that the Gang Member gains upon ascension",
"is based on the amount of multipliers the member has from non-Augmentation Equipment.",
"<br><br>Upon ascension, the member will lose all of its non-Augmentation Equipment and your",
"gang will lose respect equal to the total respect earned by the member."].join(" "));
},
innerText: "?",
marginTop: "5px",
});
statsDiv.appendChild(statsP);
statsDiv.appendChild(brElement);
statsDiv.appendChild(ascendButton);
statsDiv.appendChild(ascendHelpTip);
*/

@ -440,4 +440,4 @@ export const LocationsMetadata: IConstructorParams[] = [
name: LocationName.WorldStockExchange,
types: [LocationType.StockMarket],
},
];
];

@ -31,10 +31,10 @@ import {
import { calculateServerGrowth } from "./Server/formulas/grow";
import {
AllGangs,
GangMemberUpgrades,
GangMemberTasks,
Gang,
} from "./Gang";
import { GangMemberTasks } from "./Gang/GangMemberTasks";
import { GangMemberUpgrades } from "./Gang/GangMemberUpgrades";
import { Factions, factionExists } from "./Faction/Factions";
import { joinFaction, purchaseAugmentation } from "./Faction/FactionHelpers";
import { FactionWorkType } from "./Faction/FactionWorkTypeEnum";

@ -13,6 +13,5 @@ export const BaseCostPerSleeve = 10e12;
export const PopupId = "covenant-sleeve-purchases-popup";
export function createSleevePurchasesFromCovenantPopup(p: IPlayer): void {
const removePopupFn = removePopup.bind(null, PopupId);
createPopup(PopupId, CovenantPurchasesRoot, { p: p, closeFn: removePopupFn });
createPopup(PopupId, CovenantPurchasesRoot, { p: p, closeFn: () => removePopup(PopupId) });
}

@ -2,7 +2,7 @@
* Root React component for the popup that lets player purchase Duplicate
* Sleeves and Sleeve-related upgrades from The Covenant
*/
import * as React from "react";
import React, { useState } from 'react';
import { CovenantSleeveUpgrades } from "./CovenantSleeveUpgrades";
@ -21,91 +21,73 @@ import { dialogBoxCreate } from "../../../../utils/DialogBox";
interface IProps {
closeFn: () => void;
p: IPlayer;
rerender: () => void;
}
interface IState {
update: number;
}
export class CovenantPurchasesRoot extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {
update: 0,
}
this.rerender = this.rerender.bind(this);
}
export function CovenantPurchasesRoot(props: IProps): React.ReactElement {
const [update, setUpdate] = useState(0);
/**
* Get the cost to purchase a new Duplicate Sleeve
*/
purchaseCost(): number {
return (this.props.p.sleevesFromCovenant + 1) * BaseCostPerSleeve;
function purchaseCost(): number {
return (props.p.sleevesFromCovenant + 1) * BaseCostPerSleeve;
}
/**
* Force a rerender by just changing an arbitrary state value
*/
rerender(): void {
this.setState((state: IState) => ({
update: state.update + 1,
}));
function rerender(): void {
setUpdate(update + 1);
}
render(): React.ReactNode {
// Purchasing a new Duplicate Sleeve
let purchaseDisabled = false;
if (!this.props.p.canAfford(this.purchaseCost())) {
purchaseDisabled = true;
}
if (this.props.p.sleevesFromCovenant >= MaxSleevesFromCovenant) {
purchaseDisabled = true;
}
const purchaseOnClick = (): void => {
if (this.props.p.sleevesFromCovenant >= MaxSleevesFromCovenant) { return; }
if (this.props.p.canAfford(this.purchaseCost())) {
this.props.p.loseMoney(this.purchaseCost());
this.props.p.sleevesFromCovenant += 1;
this.props.p.sleeves.push(new Sleeve(this.props.p));
this.rerender();
} else {
dialogBoxCreate(`You cannot afford to purchase a Duplicate Sleeve`, false);
}
}
// Purchasing a new Duplicate Sleeve
let purchaseDisabled = false;
if (!props.p.canAfford(purchaseCost())) {
purchaseDisabled = true;
}
if (props.p.sleevesFromCovenant >= MaxSleevesFromCovenant) {
purchaseDisabled = true;
}
// Purchasing Upgrades for Sleeves
const upgradePanels = [];
for (let i = 0; i < this.props.p.sleeves.length; ++i) {
const sleeve = this.props.p.sleeves[i];
upgradePanels.push(
<CovenantSleeveUpgrades {...this.props} sleeve={sleeve} index={i} rerender={this.rerender} key={i} />,
)
function purchaseOnClick(): void {
if (props.p.sleevesFromCovenant >= MaxSleevesFromCovenant) return;
if (props.p.canAfford(purchaseCost())) {
props.p.loseMoney(purchaseCost());
props.p.sleevesFromCovenant += 1;
props.p.sleeves.push(new Sleeve(props.p));
rerender();
} else {
dialogBoxCreate(`You cannot afford to purchase a Duplicate Sleeve`, false);
}
}
return (
<div>
<PopupCloseButton popup={PopupId} text={"Close"} />
<p>
Would you like to purchase an additional Duplicate Sleeve from The Covenant
for {Money(this.purchaseCost())}?
</p>
<br />
<p>
These Duplicate Sleeves are permanent (they persist through BitNodes). You can
purchase a total of {MaxSleevesFromCovenant} from The Covenant.
</p>
<StdButton disabled={purchaseDisabled} onClick={purchaseOnClick} text={"Purchase"} />
<br /><br />
<p>
Here, you can also purchase upgrades for your Duplicate Sleeves. These upgrades
are also permanent, meaning they persist across BitNodes.
</p>
{upgradePanels}
</div>
// Purchasing Upgrades for Sleeves
const upgradePanels = [];
for (let i = 0; i < props.p.sleeves.length; ++i) {
const sleeve = props.p.sleeves[i];
upgradePanels.push(
<CovenantSleeveUpgrades {...props} sleeve={sleeve} index={i} rerender={rerender} key={i} />,
)
}
return (<div>
<PopupCloseButton popup={PopupId} text={"Close"} />
<p>
Would you like to purchase an additional Duplicate Sleeve from The Covenant
for {Money(purchaseCost())}?
</p>
<br />
<p>
These Duplicate Sleeves are permanent (they persist through BitNodes). You can
purchase a total of {MaxSleevesFromCovenant} from The Covenant.
</p>
<StdButton disabled={purchaseDisabled} onClick={purchaseOnClick} text={"Purchase"} />
<br /><br />
<p>
Here, you can also purchase upgrades for your Duplicate Sleeves. These upgrades
are also permanent, meaning they persist across BitNodes.
</p>
{upgradePanels}
</div>);
}

@ -1,4 +1,4 @@
import * as React from "react";
import React, { useState } from 'react';
import { KEY } from "../../../utils/helpers/keyCodes";
import { CodingContract, CodingContractType, CodingContractTypes } from "../../CodingContracts";
@ -16,19 +16,14 @@ type IState = {
answer: string;
}
export class CodingContractPopup extends React.Component<IProps, IState>{
constructor(props: IProps) {
super(props);
this.state = { answer: ''};
this.setAnswer = this.setAnswer.bind(this);
this.onInputKeydown = this.onInputKeydown.bind(this);
export function CodingContractPopup(props: IProps): React.ReactElement {
const [answer, setAnswer] = useState("");
function onChange(event: React.ChangeEvent<HTMLInputElement>): void {
setAnswer(event.target.value);
}
setAnswer(event: React.ChangeEvent<HTMLInputElement>): void {
this.setState({ answer: event.target.value });
}
onInputKeydown(event: React.KeyboardEvent<HTMLInputElement>): void {
function onKeyDown(event: React.KeyboardEvent<HTMLInputElement>): void {
// React just won't cooperate on this one.
// "React.KeyboardEvent<HTMLInputElement>" seems like the right type but
// whatever ...
@ -36,31 +31,41 @@ export class CodingContractPopup extends React.Component<IProps, IState>{
if (event.keyCode === KEY.ENTER && value !== "") {
event.preventDefault();
this.props.onAttempt(this.state.answer);
props.onAttempt(answer);
} else if (event.keyCode === KEY.ESC) {
event.preventDefault();
this.props.onClose();
props.onClose();
}
}
render(): React.ReactNode {
const contractType: CodingContractType = CodingContractTypes[this.props.c.type];
const description = [];
for (const [i, value] of contractType.desc(this.props.c.data).split('\n').entries())
description.push(<span key={i} dangerouslySetInnerHTML={{__html: value+'<br />'}}></span>);
return (
<div>
<CopyableText value={this.props.c.type} tag={ClickableTag.Tag_h1} />
<br/><br/>
<p>You are attempting to solve a Coding Contract. You have {this.props.c.getMaxNumTries() - this.props.c.tries} tries remaining, after which the contract will self-destruct.</p>
<br/>
<p>{description}</p>
<br/>
<input className="text-input" style={{ width:"50%",marginTop:"8px" }} autoFocus={true} placeholder="Enter Solution here" value={this.state.answer}
onChange={this.setAnswer} onKeyDown={this.onInputKeydown} />
<PopupCloseButton popup={this.props.popupId} onClose={() => this.props.onAttempt(this.state.answer)} text={"Solve"} />
<PopupCloseButton popup={this.props.popupId} onClose={this.props.onClose} text={"Close"} />
</div>
)
}
const contractType: CodingContractType = CodingContractTypes[props.c.type];
const description = [];
for (const [i, value] of contractType.desc(props.c.data).split('\n').entries())
description.push(<span key={i} dangerouslySetInnerHTML={{__html: value+'<br />'}}></span>);
return (
<div>
<CopyableText value={props.c.type} tag={ClickableTag.Tag_h1} />
<br/><br/>
<p>You are attempting to solve a Coding Contract. You have {props.c.getMaxNumTries() - props.c.tries} tries remaining, after which the contract will self-destruct.</p>
<br/>
<p>{description}</p>
<br/>
<input
className="text-input"
style={{ width:"50%",marginTop:"8px" }}
autoFocus={true}
placeholder="Enter Solution here"
value={answer}
onChange={onChange}
onKeyDown={onKeyDown} />
<PopupCloseButton
popup={props.popupId}
onClose={() => props.onAttempt(answer)}
text={"Solve"} />
<PopupCloseButton
popup={props.popupId}
onClose={props.onClose}
text={"Close"} />
</div>
)
}

@ -5,15 +5,13 @@
*/
import * as React from "react";
type ReactComponent = new(...args: any[]) => React.Component<any, any>
interface IProps {
content: ReactComponent;
interface IProps<T> {
content: (props: T) => React.ReactElement;
id: string;
props: any;
props: T;
}
export function Popup(props: IProps): React.ReactElement {
export function Popup<T>(props: IProps<T>): React.ReactElement {
return (
<div className={"popup-box-content"} id={`${props.id}-content`}>
{React.createElement(props.content, props.props)}

@ -31,7 +31,7 @@ function getGameContainer(): void {
document.addEventListener("DOMContentLoaded", getGameContainer);
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export function createPopup(id: string, rootComponent: ReactComponent, props: any): HTMLElement | null {
export function createPopup<T>(id: string, rootComponent: (props: T) => React.ReactElement, props: T): HTMLElement | null {
let container = document.getElementById(id);
if (container == null) {
container = createElement("div", {