Last active
February 6, 2024 15:49
-
-
Save ImaginaryDevelopment/2c01309f21c84b07612d97b0c0f946ad to your computer and use it in GitHub Desktop.
OneBit Adventure Damage calc
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// level 58 test | |
// expected hit 3267; calc 3265 | |
// expected crit 10,633; calc 10626 | |
// expected pc 8821; calc 8815 | |
// expected crit pc 28,710; calc 28691 | |
type Crit = private { critc: decimal } with | |
static member ValidateCritRate crit = | |
if crit<0.0m || crit > 1.0m then Error () | |
else Ok crit | |
static member Create value = | |
Crit.TryCreate value | |
|> function | |
| Ok v -> v | |
| Error e -> failwithf "Crit create error: %A" e | |
static member TryCreate value = | |
Crit.ValidateCritRate value | |
|> Result.map(fun x -> {critc=x}) | |
member x.CritChance = x.critc | |
override x.ToString() = if x.critc > 0.999m then "100%" else $"%0.2f{x.critc * 100.0m}%%" | |
type Weapon = { | |
Name: string | |
Dmg: int | |
Crit: Crit | |
CritDmg: decimal | |
SlotsRemaining: int | |
} | |
type OptimizableWeapon = { | |
W: Weapon | |
DmgAdd: int | |
CritAdd: Crit | |
CritDmgAdd: decimal | |
} | |
type WeaponOptimization = { | |
OW: OptimizableWeapon | |
DmgAdds: int | |
CritAdds: int | |
CritDmgAdds: int | |
} | |
type WeaponSummary = { | |
Name: string | |
// option makes output display messy | |
AvgPerfectComboDmg: int option | |
AvgDmg: int | |
SlotsRemaining: int | |
Hit: int | |
CritHit: int | |
Dmg: int | |
Crit: string | |
CritDmg: decimal | |
PerfectComboDmg: int | |
CritPerfectComboDmg: int | |
} | |
// varies by class and level | |
// hit unarmed to check? | |
//let unarmed= 931 // perfect combo 170% made 931 -> 2514 | |
let characterBaseDmg = 338 // thief 58-> 338, 71 -> 506 | |
// perfect Combo starts at 100% so it is effectively + 1 | |
let includeBaseDmg, perfectComboOpt = true, None // Some 1.7m | |
let critBuff = Some 0.15m | |
let baseAtk = 1.75m // + 1.0m // baseAtk value is + 1 | |
printfn "BaseAtk Bonus%%: %0.2f" baseAtk | |
if includeBaseDmg then | |
printfn "BaseDmg: %i" characterBaseDmg | |
perfectComboOpt | |
|> Option.iter(printfn "%0.2f Perfect Combo%%") | |
let nextWeapon = | |
{ | |
W= | |
{ Name="Longsword" | |
Dmg=995 | |
Crit=Crit.Create 0.1673m | |
CritDmg=79.58m | |
SlotsRemaining=8 | |
} | |
DmgAdd= 235 | |
CritAdd= Crit.Create 0.1m | |
CritDmgAdd=15.0m | |
} | |
let calcPerfectCombo hit = | |
match perfectComboOpt with | |
| Some pc -> decimal hit * (1.0m + pc) |> Some | |
| None -> None | |
let addCharacterBase dmg = | |
if includeBaseDmg then (decimal characterBaseDmg * (1.0m + baseAtk)) else (1.0m + baseAtk) | |
|> int | |
|> (+) dmg | |
let weightCrits critMod (crit:Crit) dmg = | |
let hitWeight = 1.0m - crit.CritChance | |
let critWeight = crit.CritChance | |
let avgHit = decimal dmg * hitWeight + decimal (decimal dmg * critMod) * critWeight |> int | |
avgHit | |
let summarize (w: Weapon) = | |
if w.Crit.CritChance < 0.0m || w.Crit.CritChance > 1.0m then failwith $"Unexpected crit rate %s{w.Name}:%0.2f{w.Crit.CritChance}" | |
let hit = w.Dmg |> addCharacterBase |> int | |
let critMod = (200.0m + w.CritDmg) / 100.0m | |
let critChance = | |
match critBuff with | |
| None -> w.Crit.CritChance | |
| Some cb -> | |
let x = w.Crit.CritChance + cb | |
let value = min x 1.0m | |
//(w.Name,x,value).Dump("x") | |
value | |
let hitWeight = 1.0m - critChance | |
let critWeight = critChance | |
let pcd = calcPerfectCombo hit | |
let hits = | |
match pcd with | |
| None -> 1 | |
| Some _ -> 3 | |
let avgHit, avgPHit = | |
let cc = | |
match Crit.TryCreate critChance with | |
| Error _ -> Crit.Create 1.0m | |
| Ok cc -> cc | |
let nonPcHit = weightCrits critMod cc hit | |
match pcd with | |
| None -> nonPcHit, None | |
| Some pcd -> | |
nonPcHit, weightCrits critMod cc (nonPcHit * 2 + int pcd) / 3 |> Some | |
//decimal hit * hitWeight + decimal (decimal hit * critMod) * critWeight |> int | |
//(w.Crit,critWeight, avgHit).Dump() | |
//summarize avgHit hit critMod w | |
let cHit = decimal hit * critMod |> int | |
{ | |
Name=w.Name | |
Hit=hit | |
SlotsRemaining=w.SlotsRemaining | |
AvgDmg= avgHit | |
AvgPerfectComboDmg=avgPHit | |
Dmg= w.Dmg | |
Crit= string w.Crit | |
CritDmg= w.CritDmg | |
CritHit= cHit | |
PerfectComboDmg= pcd |> Option.map int |> Option.defaultValue 0 | |
CritPerfectComboDmg= pcd |> Option.map((*) critMod) |> Option.map int |> Option.defaultValue 0 | |
} | |
#if LINQPAD | |
let toDump(o:obj):obj = | |
match o with | |
| :? Option<int> as oi -> | |
match oi with | |
| None -> "_" | |
| Some v -> string v | |
| x -> x | |
( | |
// customize dump output for option int | |
let f: Func<obj,obj> = toDump | |
f.AddDumpCustomizer() | |
) | |
#endif | |
let weapons = [ | |
{ // at 58, 175% base, 170% perfect combo this weapon is hitting for 3267/8821 perfect combo/10633 crit/28710 p crit | |
Name="Jagged Blade" | |
Dmg=2_336 | |
Crit=Crit.Create 0.4424m | |
SlotsRemaining=0 | |
CritDmg=125.47m } | |
{ | |
Name="Dagger" | |
Dmg=369 | |
Crit= Crit.Create 0.6563m | |
SlotsRemaining=0 | |
CritDmg=437.99m | |
} | |
//nextWeapon | |
//nextWeapon |> addDmg 238 5 | |
//match nextWeapon |> addCrit (Crit.Create 0.0977m) 3 |> Seq.tryHead with | |
//| None -> () | |
//| Some w -> w |> addDmg 238 4 | |
//nextWeapon |> addCritDmg 19.93m 6 | |
{ Name="test" | |
Dmg=100 | |
SlotsRemaining=0 | |
Crit=Crit.Create 1.0m | |
CritDmg=100.0m | |
} | |
] | |
module Adds = | |
let asOptimizing (ow:OptimizableWeapon): WeaponOptimization = {OW=ow;DmgAdds=0;CritAdds=0;CritDmgAdds=0} | |
let modifySlots count (w:Weapon) = { w with SlotsRemaining = w.SlotsRemaining - count} | |
let addDmgName title count (w:Weapon) = { w with Name = w.Name + $" %s{title}%i{count}"} | |
let updateWeaponName (ow:WeaponOptimization) : Weapon = | |
(ow.OW.W, [ow.DmgAdds, "Dmg"; ow.CritAdds, "Crit"; ow.CritDmgAdds, "CritDmg"]) | |
||> Seq.fold(fun w (count,title) -> | |
if count > 0 then | |
w |> addDmgName title count | |
else w | |
) | |
let replaceWeapon w (ow: WeaponOptimization) = | |
{ ow with OW= {ow.OW with W = w}} | |
let addDmg (ow: WeaponOptimization): WeaponOptimization = | |
let w = {ow.OW.W with Dmg=ow.OW.W.Dmg + ow.OW.DmgAdd } |> modifySlots 1 | |
{ow with DmgAdds = ow.DmgAdds + 1} | |
|> replaceWeapon w | |
let addCrit (ow: WeaponOptimization) : WeaponOptimization option = | |
let setupNewWeapon nextCrit = { ow.OW.W with Crit = nextCrit } | |
// add as much crit as we can without going over | |
ow.OW.W.Crit.CritChance + ow.OW.CritAdd.CritChance |> Crit.TryCreate | |
|> function | |
| Ok nextCrit -> | |
replaceWeapon (setupNewWeapon nextCrit |> modifySlots 1) ow |> fun ow -> {ow with CritAdds= ow.CritAdds + 1} |> Some | |
| Error _ -> | |
None | |
let addCritDmg (ow: WeaponOptimization) = | |
let nextW = | |
{ ow.OW.W with CritDmg= ow.OW.W.CritDmg + ow.OW.CritDmgAdd } | |
|> modifySlots 1 | |
{ replaceWeapon nextW ow with CritDmgAdds = ow.CritDmgAdds + 1} | |
let optimize (ow: OptimizableWeapon) = | |
if ow.W.SlotsRemaining < 1 then | |
List.empty | |
else | |
let ow = {OW=ow;DmgAdds=0;CritAdds=0;CritDmgAdds=0} | |
(ow,[0..ow.OW.W.SlotsRemaining - 1]) | |
||> Seq.fold(fun ow _ -> | |
[ | |
addDmg ow | |
match addCrit ow with | |
| None -> () | |
| Some w -> w | |
addCritDmg ow | |
] | |
|> Seq.maxBy(fun x -> (summarize x.OW.W).AvgPerfectComboDmg) | |
) | |
|> List.singleton | |
() | |
[ | |
yield! weapons | |
yield nextWeapon.W | |
if nextWeapon.W.SlotsRemaining > 0 then | |
let ow = Adds.asOptimizing nextWeapon | |
yield ow |> Adds.addCritDmg |> Adds.updateWeaponName | |
yield ow |> Adds.addDmg |> Adds.updateWeaponName | |
yield! Adds.optimize nextWeapon |> List.map Adds.updateWeaponName | |
] | |
|> List.map(fun w -> | |
// unless crit % can go over 100%, let's fail if we find one | |
//(w.Crit,critWeight, avgHit).Dump() | |
summarize w | |
) | |
|> List.sortByDescending(fun ws -> ws.AvgDmg, ws.Name) | |
|> Dump | |
|> ignore | |
printfn "Level 58 thief, 175%% base, 170%% perfect combo" | |
printfn "Jagged Blade: 2,336dmg, 44.24%% crit, 125.47%% crit damage" | |
printfn "game gives 3267 hit, 10633 crit" | |
printfn "Perfect Combos: 8821, 28710 crit" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public static List<Func<object,object>> MyDumpHooks = new (); | |
public static class MyExtensions | |
{ | |
// Write custom extension methods here. They will be available to all queries. | |
public static void AddDumpCustomizer(this Func<object,object> tester) => MyDumpHooks.Add(tester); | |
} | |
public static object ToDump(object value) { | |
foreach (var k in MyDumpHooks) | |
{ | |
if (k(value) is {} v) return v; | |
} | |
return value; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// level 58 test | |
// expected hit 3267; calc 3265 | |
// expected crit 10,633; calc 10626 | |
// expected pc 8821; calc 8815 | |
// expected crit pc 28,710; calc 28691 | |
type Crit = private { critc: decimal } with | |
static member ValidateCritRate crit = | |
if crit<0.0m || crit > 1.0m then Error () | |
else Ok crit | |
static member Create value = | |
Crit.TryCreate value | |
|> function | |
| Ok v -> v | |
| Error e -> failwithf "Crit create error: %A" e | |
static member TryCreate value = | |
Crit.ValidateCritRate value | |
|> Result.map(fun x -> {critc=x}) | |
member x.CritChance = x.critc | |
override x.ToString() = if x.critc > 0.999m then "100%" else $"%0.2f{x.critc * 100.0m}%%" | |
type Weapon = { | |
Name: string | |
Dmg: int | |
Crit: Crit | |
CritDmg: decimal | |
SlotsRemaining: int | |
} | |
type WeaponSummary = { | |
Name: string | |
AvgDmg: int | |
SlotsRemaining: int | |
Hit: int | |
CritHit: int | |
Dmg: int | |
Crit: string | |
CritDmg: decimal | |
PerfectComboDmg: int | |
CritPerfectComboDmg: int | |
} | |
// varies by class and level | |
// hit unarmed to check? | |
//let unarmed= 931 // perfect combo 170% made 931 -> 2514 | |
let characterBaseDmg = 338 // thief 58-> 338, 71 -> 506 | |
// perfect Combo starts at 100% so it is effectively + 1 | |
let includeBaseDmg, perfectComboOpt = true, Some 1.7m | |
let critBuff = Some 0.15m | |
let baseAtk = 1.75m // + 1.0m // baseAtk value is + 1 | |
printfn "BaseAtk Bonus%%: %0.2f" baseAtk | |
if includeBaseDmg then | |
printfn "BaseDmg: %i" characterBaseDmg | |
perfectComboOpt | |
|> Option.iter(printfn "%0.2f Perfect Combo") | |
let nextWeapon = | |
{ Name="Scimitar" | |
Dmg=984 | |
Crit=Crit.Create 0.3149m | |
CritDmg=92.21m | |
SlotsRemaining=8 | |
} | |
let addDmg dmg count (w: Weapon) = { | |
w with Name = w.Name + $" Dmg%i{count}"; Dmg=w.Dmg + dmg * count | |
} | |
let addCrit (crit:Crit) count (w: Weapon) = | |
let addSomeCrit count = w.Crit.CritChance + (crit.CritChance) * decimal count |> Crit.TryCreate | |
let setupNewWeapon nextCrit count = | |
{ | |
w with Name = w.Name + $" Crit%i{count}"; Crit = nextCrit | |
} | |
[ count .. -1 .. 0] | |
|> Seq.choose(fun count -> | |
addSomeCrit count | |
|> function | |
| Ok nextCrit -> | |
setupNewWeapon nextCrit count |> Some | |
| Error _ -> | |
None | |
) | |
let addCritDmg cDmg count (w: Weapon) = { | |
w with Name = w.Name + $" CritDmg%i{count}"; CritDmg= w.CritDmg + cDmg * decimal count | |
} | |
let calcPerfectCombo hit = | |
match perfectComboOpt with | |
| Some pc -> decimal hit * (1.0m + pc) |> Some | |
| None -> None | |
let summarize avghit (hit:int) (critMod:decimal) (w: Weapon) = | |
let pcd = calcPerfectCombo hit | |
let cHit = decimal hit * critMod |> int | |
{ | |
Name=w.Name | |
Hit=hit | |
SlotsRemaining=w.SlotsRemaining | |
AvgDmg= avghit | |
Dmg= w.Dmg | |
Crit= string w.Crit | |
CritDmg= w.CritDmg | |
CritHit= cHit | |
PerfectComboDmg= pcd |> Option.map int |> Option.defaultValue hit | |
CritPerfectComboDmg= pcd |> Option.map((*) critMod) |> Option.map int |> Option.defaultValue cHit | |
} | |
let weapons = [ | |
{ // at 58, 175% base, 170% perfect combo this weapon is hitting for 3267/8821 perfect combo/10633 crit/28710 p crit | |
Name="Jagged Blade" | |
Dmg=2_336 | |
Crit=Crit.Create 0.4424m | |
SlotsRemaining=0 | |
CritDmg=125.47m } | |
{ | |
Name="Dagger" | |
Dmg=369 | |
Crit= Crit.Create 0.6563m | |
SlotsRemaining=0 | |
CritDmg=437.99m | |
} | |
nextWeapon | |
nextWeapon |> addDmg 54 10 | |
match nextWeapon |> addCrit (Crit.Create 0.0977m) 5 |> Seq.tryHead with | |
| None -> () | |
| Some w -> w | |
nextWeapon |> addCritDmg 45.07m 10 | |
{ Name="test" | |
Dmg=100 | |
SlotsRemaining=0 | |
Crit=Crit.Create 1.0m | |
CritDmg=100.0m | |
} | |
] | |
let addCharacterBase dmg = | |
if includeBaseDmg then (decimal characterBaseDmg * (1.0m + baseAtk)) else (1.0m + baseAtk) | |
|> int | |
|> (+) dmg | |
weapons | |
|> List.map(fun w -> | |
// unless crit % can go over 100%, let's fail if we find one | |
if w.Crit.CritChance < 0.0m || w.Crit.CritChance > 1.0m then failwith $"Unexpected crit rate %s{w.Name}:%0.2f{w.Crit.CritChance}" | |
let hit = w.Dmg |> addCharacterBase |> int | |
let critMod = (200.0m + w.CritDmg) / 100.0m | |
let critChance = | |
match critBuff with | |
| None -> w.Crit.CritChance | |
| Some cb -> | |
let x = w.Crit.CritChance + cb | |
let value = min x 1.0m | |
//(w.Name,x,value).Dump("x") | |
value | |
let hitWeight = 1.0m - critChance | |
let critWeight = critChance | |
let avgHit = decimal hit * hitWeight + decimal (decimal hit * critMod) * critWeight |> int | |
//(w.Crit,critWeight, avgHit).Dump() | |
summarize avgHit hit critMod w | |
) | |
|> List.sortByDescending(fun ws -> ws.AvgDmg, ws.Name) | |
|> Dump | |
|> ignore | |
printfn "Level 58 thief, 175%% base, 170%% perfect combo" | |
printfn "Jagged Blade: 2,336dmg, 44.24%% crit, 125.47%% crit damage" | |
printfn "game gives 3267 hit, 10633 crit" | |
printfn "Perfect Combos: 8821, 28710 crit" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment