Skip to content

Commit

Permalink
add hit distributions
Browse files Browse the repository at this point in the history
  • Loading branch information
LlemonDuck committed Nov 14, 2023
1 parent 3731cd7 commit 6a05fe7
Show file tree
Hide file tree
Showing 11 changed files with 445 additions and 37 deletions.
2 changes: 1 addition & 1 deletion src/app/components/player/Bonuses.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const Bonuses: React.FC = observer(() => {
<h4 className={'font-serif font-bold'}>Hit Distribution</h4>
</div>
<div className={'mt-2 px-2'}>
<HitDistribution />
<HitDistribution dist={store.calc.loadouts[store.selectedLoadout].dist} />
</div>
</>
)}
Expand Down
18 changes: 18 additions & 0 deletions src/app/components/player/skills/SkillInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const SkillInput: React.FC<SkillInputProps> = observer((props) => {
</label>
</div>
<div>
<div>
<NumberInput
id={id}
required
Expand All @@ -43,6 +44,23 @@ const SkillInput: React.FC<SkillInputProps> = observer((props) => {
}}
/>
</div>
<div>
<NumberInput
id={id}
required
min={-100}
max={100}
value={player.boosts[field]}
onChange={(v) => {
store.updatePlayer({
boosts: {
[field]: v
}
})
}}
/>
</div>
</div>
</div>
)
})
Expand Down
19 changes: 4 additions & 15 deletions src/app/components/results/HitDistribution.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import React, {useCallback} from 'react';
import React from 'react';
import {BarChart, CartesianGrid, XAxis, YAxis, Tooltip, ResponsiveContainer, Bar, TooltipProps} from 'recharts';
import {NameType, ValueType} from 'recharts/types/component/DefaultTooltipContent';
import hitsplat from '@/public/img/hitsplat.webp';
import zero_hitsplat from '@/public/img/zero_hitsplat.png';
import {HistogramEntry} from "@/types/State";

const CustomTooltip: React.FC<TooltipProps<ValueType, NameType>> = ({ active, payload, label }) => {
if (active && payload && payload.length) {
Expand All @@ -28,24 +29,12 @@ const CustomTooltip: React.FC<TooltipProps<ValueType, NameType>> = ({ active, pa
return null;
}

const HitDistribution: React.FC = () => {
const data = useCallback(() => {
let d = [];
for (let i=0; i < 80; i++) {
const min = Math.ceil(0);
const max = Math.floor(1);
const num = Math.random() * (max - min) + min;

d.push({name: i, chance: num});
}
return d;
}, []);

const HitDistribution: React.FC<{ dist: HistogramEntry[] }> = ({dist}) => {
return (
<>
<ResponsiveContainer width={'100%'} height={150}>
<BarChart
data={data()}
data={dist}
>
<CartesianGrid strokeDasharray="5 3" />
<XAxis
Expand Down
21 changes: 17 additions & 4 deletions src/app/components/results/ResultsContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,19 @@ interface IResultRowProps {
calcKey: keyof CalculatedLoadout;
}

const calcKeyToString = (loadout: CalculatedLoadout, calcKey: keyof CalculatedLoadout): string => {
switch (calcKey) {
case "accuracy":
return (loadout[calcKey] * 100).toFixed(2) + '%';

case "dps":
return loadout[calcKey].toFixed(3);

default:
return "" + loadout[calcKey];
}
}

const ResultRow: React.FC<PropsWithChildren<IResultRowProps>> = observer((props) => {
const store = useStore();
const {children, calcKey} = props;
Expand All @@ -20,16 +33,14 @@ const ResultRow: React.FC<PropsWithChildren<IResultRowProps>> = observer((props)
return (
<tr>
<th className={'bg-btns-400 dark:bg-dark-400 w-40'}>{children}</th>
{calc.loadouts.map((l, i) => <th className={'text-center'} key={i}>{l[calcKey] || ''}</th>)}
{calc.loadouts.map((l, i) => <th className={'text-center'} key={i}>{calcKeyToString(l, calcKey)}</th>)}
</tr>
)
})

const ResultsTable: React.FC = observer(() => {
const store = useStore();
const {selectedLoadout, calc} = store;

// TODO: change this to actually show results, this is just proof-of-concept
const {selectedLoadout} = store;

return (
<table className={'min-w-[300px] w-auto mx-auto'}>
Expand All @@ -49,6 +60,8 @@ const ResultsTable: React.FC = observer(() => {
<ResultRow calcKey={'maxHit'}><IconSword className={'inline-block'} /> Max hit</ResultRow>
<ResultRow calcKey={'maxAttackRoll'}><IconDice className={'inline-block'} /> Attack roll</ResultRow>
<ResultRow calcKey={'npcDefRoll'}><IconShield className={'inline-block'} /> NPC def roll</ResultRow>
<ResultRow calcKey={'accuracy'}><IconShield className={'inline-block'} /> Accuracy</ResultRow>
<ResultRow calcKey={'dps'}><IconShield className={'inline-block'} /> DPS</ResultRow>
</tbody>
</table>
)
Expand Down
95 changes: 89 additions & 6 deletions src/lib/CombatCalc.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import {EquipmentPiece, PlayerComputed} from '@/types/Player';
import {Monster} from '@/types/Monster';
import {AttackDistribution, AttackDistributionMode, HitDistribution} from "@/lib/HitDist";

const DEFAULT_ATTACK_SPEED = 4;

export default class CombatCalc {
private player: PlayerComputed;
Expand All @@ -8,6 +11,8 @@ export default class CombatCalc {
// Array of the names of all equipped items (for quick checks)
private allEquippedItems: string[];

private memoizedDist: AttackDistribution | undefined = undefined;

constructor(player: PlayerComputed, monster: Monster) {
this.player = player;
this.monster = monster;
Expand Down Expand Up @@ -78,6 +83,14 @@ export default class CombatCalc {
return this.wearing('Crystal bow') || this.allEquippedItems.some((ei) => ei.includes('Bow of faerdhinen'));
}

private isWearingFang(): boolean {
return this.wearing(["Osmumten's fang", "Osmumten's fang (or)"]);
}

private isWearingScythe(): boolean {
return this.wearing('Scythe of vitur') || this.allEquippedItems.some((ei) => ei.includes('of vitur'));
}

private isUsingGodSpell(): boolean {
return ['Saradomin Strike', 'Claws of Guthix', 'Flames of Zamorak'].includes(this.player.spell.name);
}
Expand All @@ -99,7 +112,9 @@ export default class CombatCalc {
private getPlayerMaxMeleeAttackRoll(): number {
const style = this.player.style;

let effectiveLevel = this.player.skills.atk * 1; // TODO: make this * getPrayerBoost(atk)
const boostedLevel = this.player.skills.atk + this.player.boosts.atk;
const prayerBonus = 1.0; // todo
let effectiveLevel = Math.trunc(boostedLevel * prayerBonus);

if (style.stance === 'Accurate') {
effectiveLevel += 3;
Expand Down Expand Up @@ -171,7 +186,9 @@ export default class CombatCalc {
private getPlayerMaxMeleeHit(): number {
const style = this.player.style;

let effectiveLevel = this.player.skills.str * 1; // TODO: make this * getPrayerBoost(str)
const boostedLevel = this.player.skills.str + this.player.boosts.str;
const prayerBonus = 1.0; // todo
let effectiveLevel = Math.trunc(boostedLevel * prayerBonus);

if (style.stance === 'Aggressive') {
effectiveLevel += 3;
Expand Down Expand Up @@ -260,7 +277,9 @@ export default class CombatCalc {
private getPlayerMaxRangedAttackRoll() {
const style = this.player.style;

let effectiveLevel = this.player.skills.ranged * 1; // TODO: make this * getPrayerBoost(ranged)
const boostedLevel = this.player.skills.ranged + this.player.boosts.ranged;
const prayerBonus = 1.0; // todo
let effectiveLevel = Math.trunc(boostedLevel * prayerBonus);

if (style.stance === 'Accurate') {
effectiveLevel += 3;
Expand Down Expand Up @@ -316,7 +335,9 @@ export default class CombatCalc {
private getPlayerMaxRangedHit() {
const style = this.player.style;

let effectiveLevel = this.player.skills.ranged * 1; // TODO: make this * getPrayerBoost(ranged)
const boostedLevel = this.player.skills.ranged + this.player.boosts.ranged;
const prayerBonus = 1.0; // todo
let effectiveLevel = Math.trunc(boostedLevel * prayerBonus);

if (style.stance === 'Accurate') {
effectiveLevel += 3;
Expand Down Expand Up @@ -362,7 +383,9 @@ export default class CombatCalc {
private getPlayerMaxMagicAttackRoll() {
const style = this.player.style;

let effectiveLevel = this.player.skills.magic * 1; // TODO: make this * getPrayerBoost(ranged)
const boostedLevel = this.player.skills.magic + this.player.boosts.magic;
const prayerBonus = 1.0; // todo
let effectiveLevel = Math.trunc(boostedLevel * prayerBonus);

if (style.stance === 'Accurate') {
effectiveLevel += 2;
Expand Down Expand Up @@ -408,7 +431,7 @@ export default class CombatCalc {
*/
private getPlayerMaxMagicHit() {
let maxHit = 0;
let magicLevel = this.player.skills.magic; // should be the boosted level?
let magicLevel = this.player.skills.magic + this.player.boosts.magic;
const spell = this.player.spell;

// Specific bonuses that are applied from equipment
Expand Down Expand Up @@ -523,4 +546,64 @@ export default class CombatCalc {
return this.getPlayerMaxMagicAttackRoll();
}
}

public getHitChance() {
const atk = this.getMaxAttackRoll();
const def = this.getNPCDefenceRoll();

const hitChance = (atk > def)
? 1 - ((def + 2) / (2 * (atk + 1)))
: atk / (2 * (def + 1));

if (this.isWearingFang()) {
if (this.monster.attributes.includes('toa')) {
return (atk > def)
? 1 - (def + 2) * (2 * def + 3) / (atk + 1) / (atk + 1) / 6
: atk * (4 * atk + 5) / 6 / (atk + 1) / (def + 1);
} else {
return 1 - Math.pow(1 - hitChance, 2);
}
}

return hitChance;
}

public getDistribution(): AttackDistribution {
if (this.memoizedDist !== undefined) {
return this.memoizedDist;
}

const acc = this.getHitChance();
const max = this.getMaxHit();

// todo other dists (fang, keris, etc)
if (this.isWearingFang()) {
return new AttackDistribution(
AttackDistributionMode.UNIFIED,
[HitDistribution.linear(acc, Math.floor(max * 3 / 20), Math.floor(max * 17 / 20))],
)
}

if (this.isWearingScythe()) {
const dists: HitDistribution[] = [];
for (let i = 0; i < Math.min(Math.max(this.monster.size, 1), 3); i++) {
console.log(i + " => " + max + " / " + Math.pow(2, i) + " = " + Math.floor(max / Math.pow(2, i)));
dists.push(HitDistribution.linear(acc, 0, Math.floor(max / (Math.pow(2, i)))));
}
return new AttackDistribution(
AttackDistributionMode.UNIFIED,
dists,
);
}

return this.memoizedDist = new AttackDistribution(
AttackDistributionMode.UNIFIED,
[HitDistribution.linear(this.getHitChance(), 0, this.getMaxHit())],
);
}

public getDps() {
return this.getDistribution().getExpectedDamage() /
(this.player.equipment.weapon?.speed || DEFAULT_ATTACK_SPEED);
}
}
Loading

0 comments on commit 6a05fe7

Please sign in to comment.