Skip to content

Commit

Permalink
BLADEBURNER: Move bladeburner team losses to Casualties (#1654)
Browse files Browse the repository at this point in the history
  • Loading branch information
Alpheus authored Sep 24, 2024
1 parent cf2d9b8 commit b86044b
Show file tree
Hide file tree
Showing 8 changed files with 265 additions and 52 deletions.
7 changes: 6 additions & 1 deletion src/Bladeburner/Actions/BlackOperation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ import { BladeburnerActionType, BladeburnerBlackOpName } from "@enums";
import { ActionClass, ActionParams } from "./Action";
import { operationSkillSuccessBonus, operationTeamSuccessBonus } from "./Operation";
import { getEnumHelper } from "../../utils/EnumHelper";
import type { TeamActionWithCasualties } from "./TeamCasualties";

interface BlackOpParams {
name: BladeburnerBlackOpName;
reqdRank: number;
n: number;
}

export class BlackOperation extends ActionClass {
export class BlackOperation extends ActionClass implements TeamActionWithCasualties {
readonly type: BladeburnerActionType.BlackOp = BladeburnerActionType.BlackOp;
readonly name: BladeburnerBlackOpName;
n: number;
Expand Down Expand Up @@ -57,6 +58,10 @@ export class BlackOperation extends ActionClass {
return 1;
}

getMinimumCasualties(): number {
return 1;
}

getTeamSuccessBonus = operationTeamSuccessBonus;

getActionTypeSkillSuccessBonus = operationSkillSuccessBonus;
Expand Down
9 changes: 7 additions & 2 deletions src/Bladeburner/Actions/Operation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,18 @@ import type { ActionIdFor, Availability, SuccessChanceParams } from "../Types";
import { BladeburnerActionType, BladeburnerMultName, BladeburnerOperationName } from "@enums";
import { BladeburnerConstants } from "../data/Constants";
import { ActionClass } from "./Action";
import { Generic_fromJSON, IReviverValue, constructorsForReviver } from "../../utils/JSONReviver";
import { constructorsForReviver, Generic_fromJSON, IReviverValue } from "../../utils/JSONReviver";
import { LevelableActionClass, LevelableActionParams } from "./LevelableAction";
import { clampInteger } from "../../utils/helpers/clampNumber";
import { getEnumHelper } from "../../utils/EnumHelper";
import type { TeamActionWithCasualties } from "./TeamCasualties";

export interface OperationParams extends LevelableActionParams {
name: BladeburnerOperationName;
getAvailability?: (bladeburner: Bladeburner) => Availability;
}

export class Operation extends LevelableActionClass {
export class Operation extends LevelableActionClass implements TeamActionWithCasualties {
readonly type: BladeburnerActionType.Operation = BladeburnerActionType.Operation;
readonly name: BladeburnerOperationName;
teamCount = 0;
Expand Down Expand Up @@ -44,6 +45,10 @@ export class Operation extends LevelableActionClass {

getActionTypeSkillSuccessBonus = operationSkillSuccessBonus;

getMinimumCasualties(): number {
return 0;
}

getChaosSuccessFactor(inst: Bladeburner /*, params: ISuccessChanceParams*/): number {
const city = inst.getCurrentCity();
if (city.chaos > BladeburnerConstants.ChaosThreshold) {
Expand Down
47 changes: 47 additions & 0 deletions src/Bladeburner/Actions/TeamCasualties.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
export enum CasualtyFactor {
LOW_CASUALTIES = 0.5, // 50%
HIGH_CASUALTIES = 1, // 100%
}

export interface OperationTeam {
/** teamSize = Human Team + Supporting Sleeves */
teamSize: number;
teamLost: number;
/** number of supporting sleeves at time of action completion */
sleeveSize: number;

getTeamCasualtiesRoll(low: number, high: number): number;

killRandomSupportingSleeves(sleeveDeaths: number): void;
}

export interface TeamActionWithCasualties {
teamCount: number;

getMinimumCasualties(): number;
}

/**
* Some actions (Operations and Black Operations) use teams for success bonus
* and may result in casualties, reducing the player's hp, killing team members
* and killing sleeves (to shock them, sleeves are immortal) *
*/
export function resolveTeamCasualties(action: TeamActionWithCasualties, team: OperationTeam, success: boolean) {
const severity = success ? CasualtyFactor.LOW_CASUALTIES : CasualtyFactor.HIGH_CASUALTIES;
const radius = action.teamCount * severity;
const worstCase = severity < 1 ? Math.ceil(radius) : Math.floor(radius);
/** Best case is always no deaths */
const deaths = team.getTeamCasualtiesRoll(action.getMinimumCasualties(), worstCase);
const humans = action.teamCount - team.sleeveSize;
const humanDeaths = Math.min(humans, deaths);
/** Supporting Sleeves take damage when they are part of losses,
* e.g. 8 sleeves + 3 team members with 4 losses -> 1 sleeve takes damage */
team.killRandomSupportingSleeves(deaths - humanDeaths);

/** Clamped, bugfix for PR#1659
* "BUGFIX: Wrong team size when all team members die in Bladeburner's action" */
team.teamSize = Math.max(team.teamSize - humanDeaths, team.sleeveSize);
team.teamLost += deaths;

return deaths;
}
70 changes: 22 additions & 48 deletions src/Bladeburner/Bladeburner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ import { Settings } from "../Settings/Settings";
import { formatTime } from "../utils/helpers/formatTime";
import { joinFaction } from "../Faction/FactionHelpers";
import { isSleeveInfiltrateWork } from "../PersonObjects/Sleeve/Work/SleeveInfiltrateWork";
import { isSleeveSupportWork } from "../PersonObjects/Sleeve/Work/SleeveSupportWork";
import { WorkStats, newWorkStats } from "../Work/WorkStats";
import { getEnumHelper } from "../utils/EnumHelper";
import { PartialRecord, createEnumKeyedRecord, getRecordEntries } from "../Types/Record";
Expand All @@ -50,10 +49,12 @@ import { GeneralActions } from "./data/GeneralActions";
import { PlayerObject } from "../PersonObjects/Player/PlayerObject";
import { Sleeve } from "../PersonObjects/Sleeve/Sleeve";
import { autoCompleteTypeShorthand } from "./utils/terminalShorthands";
import { resolveTeamCasualties, type OperationTeam } from "./Actions/TeamCasualties";
import { shuffleArray } from "../Infiltration/ui/BribeGame";

export const BladeburnerPromise: PromisePair<number> = { promise: null, resolve: null };

export class Bladeburner {
export class Bladeburner implements OperationTeam {
numHosp = 0;
moneyLost = 0;
rank = 0;
Expand Down Expand Up @@ -102,6 +103,7 @@ export class Bladeburner {
automateThreshLow = 0;
consoleHistory: string[] = [];
consoleLogs: string[] = ["Bladeburner Console", "Type 'help' to see console commands"];
getTeamCasualtiesRoll = getRandomIntInclusive;

constructor() {
this.contracts = createContracts();
Expand Down Expand Up @@ -750,32 +752,20 @@ export class Bladeburner {
}
}

killRandomSupportingSleeves(n: number) {
const sup = [...Player.sleevesSupportingBladeburner()]; // Explicit shallow copy
shuffleArray(sup);
sup.slice(0, Math.min(sup.length, n)).forEach((sleeve) => sleeve.kill());
}

completeOperation(success: boolean): void {
if (this.action?.type !== BladeburnerActionType.Operation) {
throw new Error("completeOperation() called even though current action is not an Operation");
}
const action = this.getActionObject(this.action);

// Calculate team losses
const teamCount = action.teamCount;
if (teamCount >= 1) {
const maxLosses = success ? Math.ceil(teamCount / 2) : Math.floor(teamCount);
const losses = getRandomIntInclusive(0, maxLosses);
this.teamSize -= losses;
if (this.teamSize < this.sleeveSize) {
const sup = Player.sleeves.filter((x) => isSleeveSupportWork(x.currentWork));
for (let i = 0; i > this.teamSize - this.sleeveSize; i--) {
const r = Math.floor(Math.random() * sup.length);
sup[r].takeDamage(sup[r].hp.max);
sup.splice(r, 1);
}
// If this happens, all team members died and some sleeves took damage. In this case, teamSize = sleeveSize.
this.teamSize = this.sleeveSize;
}
this.teamLost += losses;
if (this.logging.ops && losses > 0) {
this.log("Lost " + formatNumberNoSuffix(losses, 0) + " team members during this " + action.name);
}
const deaths = resolveTeamCasualties(action, this, success);
if (this.logging.ops && deaths > 0) {
this.log("Lost " + formatNumberNoSuffix(deaths, 0) + " team members during this " + action.name);
}

const city = this.getCurrentCity();
Expand Down Expand Up @@ -992,9 +982,7 @@ export class Bladeburner {
this.stamina = 0;
}

// Team loss variables
const teamCount = action.teamCount;
let teamLossMax;
let deaths;

if (action.attempt(this, person)) {
retValue = this.getActionStats(action, person, true);
Expand All @@ -1004,7 +992,8 @@ export class Bladeburner {
rankGain = addOffset(action.rankGain * currentNodeMults.BladeburnerRank, 10);
this.changeRank(person, rankGain);
}
teamLossMax = Math.ceil(teamCount / 2);

deaths = resolveTeamCasualties(action, this, true);

if (this.logging.blackops) {
this.log(
Expand All @@ -1028,7 +1017,8 @@ export class Bladeburner {
this.moneyLost += cost;
}
}
teamLossMax = Math.floor(teamCount);

deaths = resolveTeamCasualties(action, this, false);

if (this.logging.blackops) {
this.log(
Expand All @@ -1042,26 +1032,10 @@ export class Bladeburner {

this.resetAction(); // Stop regardless of success or fail

// Calculate team losses
if (teamCount >= 1) {
const losses = getRandomIntInclusive(1, teamLossMax);
this.teamSize -= losses;
if (this.teamSize < this.sleeveSize) {
const sup = Player.sleeves.filter((x) => isSleeveSupportWork(x.currentWork));
for (let i = 0; i > this.teamSize - this.sleeveSize; i--) {
const r = Math.floor(Math.random() * sup.length);
sup[r].takeDamage(sup[r].hp.max);
sup.splice(r, 1);
}
// If this happens, all team members died and some sleeves took damage. In this case, teamSize = sleeveSize.
this.teamSize = this.sleeveSize;
}
this.teamLost += losses;
if (this.logging.blackops) {
this.log(
`${person.whoAmI()}: You lost ${formatNumberNoSuffix(losses, 0)} team members during ${action.name}.`,
);
}
if (this.logging.blackops && deaths > 0) {
this.log(
`${person.whoAmI()}: You lost ${formatNumberNoSuffix(deaths, 0)} team members during ${action.name}.`,
);
}
break;
}
Expand Down
3 changes: 2 additions & 1 deletion src/Infiltration/ui/BribeGame.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { KeyHandler } from "./KeyHandler";

interface Difficulty {
[key: string]: number;

timer: number;
size: number;
}
Expand Down Expand Up @@ -106,7 +107,7 @@ export function BribeGame(props: IMinigameProps): React.ReactElement {
);
}

function shuffleArray(array: string[]): void {
export function shuffleArray(array: unknown[]): void {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
const temp = array[i];
Expand Down
5 changes: 5 additions & 0 deletions src/PersonObjects/Player/PlayerObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { CONSTANTS } from "../../Constants";
import { Person } from "../Person";
import { isMember } from "../../utils/EnumHelper";
import { PartialRecord } from "../../Types/Record";
import { isSleeveSupportWork } from "../Sleeve/Work/SleeveSupportWork";

export class PlayerObject extends Person implements IPlayer {
// Player-specific properties
Expand Down Expand Up @@ -171,6 +172,10 @@ export class PlayerObject extends Person implements IPlayer {
return "Player";
}

sleevesSupportingBladeburner(): Sleeve[] {
return this.sleeves.filter((s) => isSleeveSupportWork(s.currentWork));
}

/** Serialize the current object to a JSON save state. */
toJSON(): IReviverValue {
return Generic_toJSON("PlayerObject", this);
Expand Down
5 changes: 5 additions & 0 deletions src/PersonObjects/Sleeve/Sleeve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,11 @@ export class Sleeve extends Person implements SleevePerson {
return "sleeves";
}

/** Sleeves are immortal, but we damage them for max hp so they get shocked */
kill() {
return this.takeDamage(this.hp.max);
}

takeDamage(amt: number): boolean {
if (typeof amt !== "number") {
console.warn(`Player.takeDamage() called without a numeric argument: ${amt}`);
Expand Down
Loading

0 comments on commit b86044b

Please sign in to comment.