Skip to content

Instantly share code, notes, and snippets.

@ImaginaryDevelopment
Last active February 6, 2024 15:49
Show Gist options
  • Save ImaginaryDevelopment/2c01309f21c84b07612d97b0c0f946ad to your computer and use it in GitHub Desktop.
Save ImaginaryDevelopment/2c01309f21c84b07612d97b0c0f946ad to your computer and use it in GitHub Desktop.
OneBit Adventure Damage calc
// 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"
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;
}
// 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