///- --package .
:# combat static =
    :< testing

:< keyed_rng

:| combat_roll =
    | ArmorProbe
    | BoutSwing
    | StatsDraw
    | ArrowShot
    | ArrowFire

:+ purpose_code combat_roll =
    \ r =>
        ?= r ?|
        | ArmorProbe => 4u32
        | BoutSwing => 7u32
        | StatsDraw => 9u32
        | ArrowShot => 11u32
        | ArrowFire => 12u32

:== roll p = keyed_rng::roll_key p

:= combat_key (s : u32) (t : u32) (e : u32) (r : combat_roll) : roll combat_roll =
    keyed_rng::roll_key (keyed_rng::seed s) (keyed_rng::tick t) (keyed_rng::entity e) r

:= unit_scale () : i32 = 4096

:= uniform_unit \* p {purpose_code}. (key : roll p) (index : u32) : i32 =
    conv (keyed_rng::draw (keyed_rng::sub_key key index) % 4096u32)

:= irwin_hall_twelve \* p {purpose_code}. (key : roll p) (attempt : u32) : i32 =
    :~ ^ sum (i : u32) (total : i32) : i32 =
        ? i >= 12u32 ?> total |: sum (i + 1u32) (total + uniform_unit key (attempt * 12u32 + i))
    sum 0u32 0i32

:= normal_attempt \* p {purpose_code}. (key : roll p) (n : u32) : i32 =
    2048i32 + (irwin_hall_twelve key n - 24576i32) / 4i32

:= clamped_to_unit (v : i32) : i32 = ? v < 0i32 ?> 0i32 |: unit_scale ()

:= in_unit (v : i32) : bool = v >= 0i32 && v <= unit_scale ()

:= truncated_normal_unit \* p {purpose_code}. (key : roll p) : i32 =
    :~ ^ attempt (n : u32) : i32 =
        := v = normal_attempt key n
        ? in_unit v ?> v
        |? n >= 7u32 ?> clamped_to_unit v
        |: attempt (n + 1u32)
    attempt 0u32

:== draw_stats = { total : i32; low : i32; high : i32 }

:= with_draw (v : i32) (d : draw_stats) : draw_stats =
    { total = d::total + v; low = (? v < d::low ?> v |: d::low); high = (? v > d::high ?> v |: d::high) }

:= stats_draw (seed : u32) (i : i32) : i32 = truncated_normal_unit (combat_key seed (conv i) 0u32 StatsDraw)

:= no_draws () : draw_stats = { total = 0i32; low = unit_scale (); high = 0i32 }

:~ ^ draws_from (seed : u32) (count : i32) (i : i32) (d : draw_stats) : draw_stats =
    ? i >= count ?> d |: draws_from seed count (i + 1i32) (with_draw (stats_draw seed i) d)

:= unit_draw_stats (seed : u32) (count : i32) : i32 * i32 * i32 =
    := d = draws_from seed count 0i32 (no_draws ())
    d::total / count, d::low, d::high

:= melee_roll \* p {purpose_code}. (low : i32) (high : i32) (key : roll p) : i32 =
    low + (truncated_normal_unit key * (high - low) + 2048i32) / unit_scale ()

:= uniform_between \* p {purpose_code}. (low : i32) (high : i32) (key : roll p) (index : u32) : i32 =
    conv (keyed_rng::roll_between (conv low) (conv high) (keyed_rng::sub_key key index))

:= defense_block \* p {purpose_code}. (defense : i32) (key : roll p) : i32 =
    uniform_between (defense / 2i32) defense key 200u32

:= armor_block \* p {purpose_code}. (armor : i32) (key : roll p) : i32 =
    ? armor > 3i32 ?> uniform_between (armor / 2i32) (armor - (armor % 2i32 + 1i32)) key 201u32
    |? armor > 0i32 ?> 1i32
    |: 0i32

:= after_armor \* p {purpose_code}. (hit : i32) (armor : i32) (key : roll p) : i32 =
    := left = hit - armor_block armor key
    ? left <= 0i32 ?> 0i32 |: left

:= blocked_hit \* p {purpose_code}. (raw : i32) (defense : i32) (armor : i32) (charged : bool) (key : roll p) : i32 =
    := left = ? charged ?> raw - defense_block defense key |: raw
    ? left <= 0i32 ?> 0i32 |: after_armor left armor key

:== gear = { armor : i32; defense : i32 }

:= balanced_defense_percent () : i32 = 75i32
:= shield_scaling_per_mille () : i32 = 160i32

:= shielded_defense (shield_skill : i32) (shield_defense : i32) : i32 =
    (shield_skill * 25i32 + 223i32)
    * shield_defense
    * balanced_defense_percent ()
    * shield_scaling_per_mille ()
    / 10000000i32

:= knight_sample_armor () : i32 =
    := brass_helmet = 3i32
    := brass_armor = 8i32
    := brass_legs = 5i32
    := leather_boots = 1i32
    brass_helmet + brass_armor + brass_legs + leather_boots

:= knight_sample_gear () : gear =
    := dwarven_shield = 26i32
    { armor = knight_sample_armor (); defense = shielded_defense 11i32 dwarven_shield }

:= max_block_charges () : i32 = 2i32
:= block_charge_ms () : i32 = 1000i32

:= creature_hit \* p {purpose_code}. (melee_max : i32) (defender : gear) (shielded : bool) (key : roll p) : i32 =
    blocked_hit (melee_roll 0i32 melee_max key) defender::defense defender::armor shielded key

:== ranged_attack = { chance_percent : i32; max_damage : i32; range : i32; every_ms : i32 }

:= hunter_arrow () : ranged_attack = { chance_percent = 50; max_damage = 100; range = 7; every_ms = 2000 }

:= ranged_in_reach (attack : ranged_attack) (distance : i32) (sight_clear : bool) : bool =
    distance <= attack::range && sight_clear

:= ranged_fires \* p {purpose_code}. (attack : ranged_attack) (key : roll p) : bool =
    uniform_between 1i32 100i32 key 202u32 <= attack::chance_percent

:= charge_left (charges : i32) : i32 = ? charges > 0i32 ?> charges - 1i32 |: 0i32

:= ranged_hit_spending_a_charge \* p {purpose_code}.
    (attack : ranged_attack) (defender : gear) (charges : i32) (key : roll p)
    : i32 * i32
    =
    blocked_hit (melee_roll 0i32 attack::max_damage key) defender::defense defender::armor !- key, charge_left charges

:== swinger = { mode : damage::fight_mode; level : i32; attack_value : i32; attack_skill : i32 }

:= swing_floor (s : swinger) : i32 = ? s::attack_value > 0i32 ?> s::level / 5i32 |: 0i32

:= swing_ceiling (s : swinger) : i32 = damage::max_melee_damage s::mode s::attack_value s::attack_skill s::level

:= melee_hit \* p {purpose_code}. (s : swinger) (target : gear) (key : roll p) : i32 =
    blocked_hit (melee_roll (swing_floor s) (swing_ceiling s) key) target::defense target::armor !+ key

:== bout = { s : swinger; target : gear; seed : u32; swings : i32 }

:= bout_of (s : swinger) (target : gear) (seed : u32) (swings : i32) : bout =
    { s = s; target = target; seed = seed; swings = swings }

:= bout_hit (b : bout) (i : i32) : i32 = melee_hit b::s b::target (combat_key b::seed (conv i) 1u32 BoutSwing)

:= landed_after (hit : i32) (landed : i32) : i32 = ? hit > 0i32 ?> landed + 1i32 |: landed

:== swing_tally = { landed : i32; total : i32 }

:= tallied_swing (hit : i32) (t : swing_tally) : swing_tally =
    { landed = landed_after hit t::landed; total = t::total + hit }

:~ ^ swings_from (b : bout) (i : i32) (t : swing_tally) : swing_tally =
    ? i >= b::swings ?> t |: swings_from b (i + 1i32) (tallied_swing (bout_hit b i) t)

:= swing_stats (s : swinger) (target : gear) (seed : u32) (swings : i32) : i32 * i32 =
    := t = swings_from (bout_of s target seed swings) 0i32 { landed = 0i32; total = 0i32 }
    t::landed, t::total

:== capped_tally = { health : i32; landed : i32; total : i32 }

:= taken_from (hit : i32) (health : i32) : i32 = ? hit > health ?> health |: hit

:= health_after (max_health : i32) (left : i32) : i32 = ? left <= 0i32 ?> max_health |: left

:= capped_swing (max_health : i32) (hit : i32) (t : capped_tally) : capped_tally =
    := taken = taken_from hit t::health
    := health = health_after max_health (t::health - taken)
    { health = health; landed = landed_after taken t::landed; total = t::total + taken }

:~ ^ capped_from (b : bout) (max_health : i32) (i : i32) (t : capped_tally) : capped_tally =
    ? i >= b::swings ?> t |: capped_from b max_health (i + 1i32) (capped_swing max_health (bout_hit b i) t)

:= fresh_capped (max_health : i32) : capped_tally = { health = max_health; landed = 0i32; total = 0i32 }

:= capped_hit_stats (s : swinger) (target : gear) (max_health : i32) (seed : u32) (swings : i32) : i32 * i32 =
    := t = capped_from (bout_of s target seed swings) max_health 0i32 (fresh_capped max_health)
    t::landed, t::total

:= level_8_axe (mode : damage::fight_mode) (skill : i32) : swinger =
    { mode = mode; level = 8i32; attack_value = 21i32; attack_skill = skill }

:= armor_and_defense_10 () : gear = { armor = 10i32; defense = 10i32 }

:= troll_swings (mode : damage::fight_mode) (skill : i32) (seed : u32) : i32 * i32 =
    swing_stats (level_8_axe mode skill) (armor_and_defense_10 ()) seed 2000i32

:= capped_troll_swings (mode : damage::fight_mode) (skill : i32) (health : i32) (seed : u32) : i32 * i32 =
    capped_hit_stats (level_8_axe mode skill) (armor_and_defense_10 ()) health seed 2000i32

:= arrow_key (i : i32) : roll combat_roll = combat_key 9u32 (conv i) 2u32 ArrowShot

:= one_if (b : bool) : i32 = ? b ?> 1i32 |: 0i32

:= arrow_outcome (g : gear) (i : i32) : i32 * i32 * i32 =
    := key = arrow_key i
    := hurt, left = ranged_hit_spending_a_charge (hunter_arrow ()) g 2i32 key
    one_if (hurt <> creature_hit (hunter_arrow ())::max_damage g !- key), one_if (left = 1i32), hurt

:= summed (a : i32 * i32 * i32) (b : i32 * i32 * i32) : i32 * i32 * i32 =
    := a1, a2, a3 = a
    := b1, b2, b3 = b
    a1 + b1, a2 + b2, a3 + b3

:~ ^ arrow_totals (g : gear) (shots : i32) (i : i32) (acc : i32 * i32 * i32) : i32 * i32 * i32 =
    ? i >= shots ?> acc |: arrow_totals g shots (i + 1i32) (summed acc (arrow_outcome g i))

:= arrow_with_no_charge (g : gear) : i32 =
    := _, left = ranged_hit_spending_a_charge (hunter_arrow ()) g 0i32 (arrow_key 0i32)
    left

:= arrow_facts (shots : i32) : i32 * i32 * i32 * bool =
    := g = knight_sample_gear ()
    := differ, spent, total = arrow_totals g shots 0i32 (0i32, 0i32, 0i32)
    differ, spent, arrow_with_no_charge g, total > shots * 30i32

:~ ^ fired (arrow : ranged_attack) (i : i32) (count : i32) : i32 =
    ? i >= 10000i32 ?> count
    |: fired arrow (i + 1i32) (? ranged_fires arrow (combat_key 3u32 (conv i) 4u32 ArrowFire) ?> count + 1i32 |: count)

:= reach_of (arrow : ranged_attack) (range : i32) : bool * bool * bool =
    ranged_in_reach arrow range !+, ranged_in_reach arrow (range + 1i32) !+, ranged_in_reach arrow 3i32 !-

:= armor_blocks \* p {purpose_code}. (armor : i32) (key : roll p) : bool * i32 * i32 =
    := block = armor_block armor key
    (block >= 5i32 && block <= 9i32), armor_block (armor - 7i32) key, armor_block (armor - 10i32) key

:= fires_about_half (arrow : ranged_attack) : bool =
    := shots = fired arrow 0i32 0i32
    shots > 4800i32 && shots < 5200i32

///> c
:# hunter_arrows_skip_the_shield_but_spend_a_block_charge_like_canary_distance_spells =
    := ~shots = 4000i32
    arrow_facts shots |> _assert_eq (0i32, shots, 0i32, !+)

///> c
:# hunter_arrows_need_range_7_and_clear_sight_and_fire_half_the_time =
    := ~range = 7i32
    := near, beyond, close = reach_of (hunter_arrow ()) range
    (near, beyond, close, fires_about_half (hunter_arrow ())) |> _assert_eq (!+, !-, !-, !+)

///> c
:# armor_block_follows_canary_bounds =
    := ~armor = 10i32
    armor_blocks armor (combat_key 1u32 2u32 3u32 ArmorProbe) |> _assert_eq (!+, 1i32, 0i32)

///> c
:# truncated_normal_stays_in_unit_range_with_mean_near_half =
    := ~seed = 11u32
    unit_draw_stats seed 4000i32 |> _assert_eq (2055i32, 0i32, 4093i32)

///> c
:# level_8_knight_rarely_hurts_a_troll =
    := ~seed = 5u32
    troll_swings (:< damage ;; Offensive) 11i32 seed |> _assert_eq (435i32, 1393i32)

///> c
:# axe_skill_30_hurts_a_troll_often =
    := ~seed = 5u32
    troll_swings (:< damage ;; Offensive) 30i32 seed |> _assert_eq (1680i32, 28222i32)

///> c
:# balanced_axe_skill_30_matches_live_troll_hits =
    := ~seed = 5u32
    troll_swings (:< damage ;; Balanced) 30i32 seed |> _assert_eq (1435i32, 15398i32)

///> wgsl
:# balanced_swing_stats_on_gpu =
    := ~seed = 5u32
    ?= troll_swings (:< damage ;; Balanced) 30i32 seed ?|
    | landed, total => ? landed = 1435i32 && total = 15398i32 ?> 0i32 |: 1i32

///> c
:# hits_capped_at_remaining_health_match_live_skeleton_log =
    := ~seed = 5u32
    capped_troll_swings (:< damage ;; Balanced) 30i32 50i32 seed |> _assert_eq (1435i32, 13451i32)
