Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion rust/data/presets/skill-assumptions/Expert+.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"spike_speed_keep_leniency": 4,
"elevator_cf_leniency": 8,
"phantoon_proficiency": 1,
"draygon_proficiency": 0.9,
"draygon_proficiency": 0.85,
"ridley_proficiency": 0.825,
"botwoon_proficiency": 1,
"mother_brain_proficiency": 1,
Expand Down
2 changes: 1 addition & 1 deletion rust/data/presets/skill-assumptions/Expert.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"spike_speed_keep_leniency": 4,
"elevator_cf_leniency": 8,
"phantoon_proficiency": 1,
"draygon_proficiency": 0.9,
"draygon_proficiency": 0.8,
"ridley_proficiency": 0.825,
"botwoon_proficiency": 1,
"mother_brain_proficiency": 1,
Expand Down
2 changes: 1 addition & 1 deletion rust/data/presets/skill-assumptions/Extreme.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"spike_speed_keep_leniency": 4,
"elevator_cf_leniency": 8,
"phantoon_proficiency": 1,
"draygon_proficiency": 0.95,
"draygon_proficiency": 0.9,
"ridley_proficiency": 0.9,
"botwoon_proficiency": 1,
"mother_brain_proficiency": 1,
Expand Down
6 changes: 6 additions & 0 deletions rust/maprando-game/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,9 @@ pub enum Requirement {
},
PhantoonFight {},
DraygonFight {
can_be_patient_tech_idx: usize,
can_be_very_patient_tech_idx: usize,
can_be_extremely_patient_tech_idx: usize,
},
RidleyFight {
can_be_patient_tech_idx: usize,
Expand Down Expand Up @@ -2692,8 +2694,12 @@ impl GameData {
return Ok(Requirement::PhantoonFight {});
} else if enemy_set.contains("Draygon") {
return Ok(Requirement::DraygonFight {
can_be_patient_tech_idx: self.tech_isv.index_by_key
[&TECH_ID_CAN_BE_PATIENT],
can_be_very_patient_tech_idx: self.tech_isv.index_by_key
[&TECH_ID_CAN_BE_VERY_PATIENT],
can_be_extremely_patient_tech_idx: self.tech_isv.index_by_key
[&TECH_ID_CAN_BE_EXTREMELY_PATIENT],
});
} else if enemy_set.contains("Botwoon 1") {
return Ok(Requirement::BotwoonFight {
Expand Down
190 changes: 127 additions & 63 deletions rust/maprando-logic/src/boss_requirements.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,9 @@ pub fn apply_draygon_requirement(
inventory: &Inventory,
local: &mut LocalState,
proficiency: f32,
can_be_patient: bool,
can_be_very_patient: bool,
can_be_extremely_patient: bool,
reverse: bool,
) -> bool {
let mut boss_hp: f32 = 6000.0;
Expand All @@ -87,98 +89,160 @@ pub fn apply_draygon_requirement(
// Assume an accuracy of between 40% (on lowest difficulty) to 100% (on highest).
let accuracy = 0.4 + 0.6 * proficiency;

// Assume a firing rate of between 60% (on lowest difficulty) to 100% (on highest).
let firing_rate = 0.6 + 0.4 * proficiency;
// Assume a firing rate of between 30% (on lowest difficulty) to 100% (on highest).
let firing_skill = 0.3 + 0.7 * proficiency;
let mut firing_rate = firing_skill;
if !inventory.items[Item::Gravity as usize] {
firing_rate *= 0.5;
}
if !inventory.items[Item::Morph as usize] {
firing_rate *= 0.7;
};

const GOOP_CYCLES_PER_SECOND: f32 = 1.0 / 15.0;
const SWOOP_CYCLES_PER_SECOND: f32 = GOOP_CYCLES_PER_SECOND * 2.0;

// Assume a maximum of 1 charge shot per goop phase, and 1 charge shot per swoop.
let charge_firing_rate = (SWOOP_CYCLES_PER_SECOND + GOOP_CYCLES_PER_SECOND) * firing_rate;
let charge_firing_rate = if inventory.items[Item::Plasma as usize] {
// Charge+Plasma will not be blocked by goop so assume 3 charge shots per goop, and 1 charge shot per swoop.
(SWOOP_CYCLES_PER_SECOND + GOOP_CYCLES_PER_SECOND * 3.0) * firing_rate
} else {
// Assume a maximum of 1 charge shot per goop phase, and 1 charge shot per series of swoops.
(0.5 * SWOOP_CYCLES_PER_SECOND + GOOP_CYCLES_PER_SECOND) * firing_rate
};
let charge_damage_rate = charge_firing_rate * charge_damage * accuracy;

let farm_proficiency = 0.2 + 0.8 * proficiency;
let farm_proficiency = 0.1 + 0.2 * proficiency + 0.7 * proficiency * proficiency;
let base_goop_farms_per_cycle = match (
inventory.items[Item::Plasma as usize],
inventory.items[Item::Wave as usize],
) {
(false, _) => 7.0, // Basic beam
(true, false) => 10.0, // Plasma can hit multiple goops at once.
(true, true) => 13.0, // Wave+Plasma can hit even more goops at once.
(true, true) => 11.0, // Wave+Plasma can hit even more goops at once.
};
let goop_farms_per_cycle = if inventory.items[Item::Gravity as usize] {
farm_proficiency * base_goop_farms_per_cycle
} else {
// Without Gravity you can't farm as many goops since you have to spend more time avoiding Draygon.
0.75 * farm_proficiency * base_goop_farms_per_cycle
0.6 * farm_proficiency * base_goop_farms_per_cycle
};
let energy_farm_rate =
GOOP_CYCLES_PER_SECOND * goop_farms_per_cycle * (5.0 * 0.02 + 20.0 * 0.12);
let missile_farm_rate = GOOP_CYCLES_PER_SECOND * goop_farms_per_cycle * (2.0 * 0.44);

let base_hit_dps = if inventory.items[Item::Gravity as usize] {
// With Gravity, assume one Draygon hit per two cycles as the maximum rate of damage to Samus:
160.0 * 0.5 * (GOOP_CYCLES_PER_SECOND + SWOOP_CYCLES_PER_SECOND) * (1.0 - proficiency)
let energy_farm_rate = if 160 / suit_damage_factor(inventory) >= inventory.max_energy {
// Damage is represented in DPS but is actually taken in chunks. If not enough energy capacity
// is available to survive a single hit, then energy farming is not possible.
// Note: this assumes that using low-energy auto-reserves to survive a hit is not realistic enough to model.
// TODO: handle pause abuse case.
0.0
} else {
// Without Gravity, assume one Draygon hit per cycle as the maximum rate of damage to Samus:
160.0 * (GOOP_CYCLES_PER_SECOND + SWOOP_CYCLES_PER_SECOND) * (1.0 - proficiency)
GOOP_CYCLES_PER_SECOND * goop_farms_per_cycle * (5.0 * 0.02 + 20.0 * 0.12)
};
let missile_farms_per_cycle = f32::min(
goop_farms_per_cycle * (2.0 * 0.44),
inventory.max_missiles as f32,
);
let missile_farm_rate = GOOP_CYCLES_PER_SECOND * missile_farms_per_cycle;

let base_hit_dps = match (
inventory.items[Item::Gravity as usize],
inventory.items[Item::Morph as usize],
) {
(false, false) => {
// Without Gravity or Morph, dodging is a challenge...
160.0
* (GOOP_CYCLES_PER_SECOND + SWOOP_CYCLES_PER_SECOND)
* f32::powf(1.0 - 0.9 * proficiency, 0.5)
}
(true, false) => {
// With Gravity, Swoops are more difficult while Goops are manageable
160.0
* (GOOP_CYCLES_PER_SECOND + SWOOP_CYCLES_PER_SECOND)
* f32::powf(1.0 - proficiency, 2.0)
}
(false, true) => {
// With Morph, Goops are difficult while Swoops are manageable
160.0
* (GOOP_CYCLES_PER_SECOND + SWOOP_CYCLES_PER_SECOND)
* f32::powf(1.0 - proficiency, 2.0)
}
(true, true) => {
// With Gravity and Morph, Assume a low hit rate:
160.0
* 0.5
* (GOOP_CYCLES_PER_SECOND + SWOOP_CYCLES_PER_SECOND)
* f32::powf(1.0 - proficiency, 2.0)
}
};

// We assume as many Supers are available can be used immediately (e.g. on the first goop cycle):
// Start by using all Supers
let supers_available = local.supers_available(inventory, reverse);
boss_hp -= (supers_available as f32) * accuracy * 300.0;
if boss_hp < 0.0 {
return true;
}
let supers_needed = boss_hp / 300.0 / accuracy;
let supers_used = f32::min(supers_needed, supers_available as f32);
boss_hp -= supers_used * accuracy * 300.0;

let missiles_available = local.missiles_available(inventory, reverse);
let missile_firing_rate = 20.0 * GOOP_CYCLES_PER_SECOND * firing_rate;
let net_missile_use_rate = missile_firing_rate - missile_farm_rate;
let super_firing_rate =
(5.0 * GOOP_CYCLES_PER_SECOND + 1.0 * SWOOP_CYCLES_PER_SECOND) * firing_rate;
let mut time = supers_used / super_firing_rate;

let initial_missile_damage_rate = 100.0 * missile_firing_rate * accuracy;
let overall_damage_rate = initial_missile_damage_rate + charge_damage_rate;
let time_boss_dead = f32::ceil(boss_hp / overall_damage_rate);
let time_missiles_exhausted = if inventory.max_missiles == 0 {
0.0
} else if net_missile_use_rate > 0.0 {
(missiles_available as f32) / net_missile_use_rate
} else {
f32::INFINITY
};
let mut time = f32::min(time_boss_dead, time_missiles_exhausted);
if time_missiles_exhausted < time_boss_dead {
// Boss is not dead yet after exhausting all Missiles (if any).
// Continue the fight using Missiles only at the lower rate at which they can be farmed (if available).
boss_hp -= time * overall_damage_rate;

let farming_missile_damage_rate = if inventory.max_missiles > 0 {
100.0 * missile_farm_rate * accuracy
} else {
if boss_hp > 0.0 {
let missiles_available = local.missiles_available(inventory, reverse);
let missile_firing_rate =
(12.0 * GOOP_CYCLES_PER_SECOND + 3.0 * SWOOP_CYCLES_PER_SECOND) * firing_rate;
let net_missile_use_rate = missile_firing_rate - missile_farm_rate;

let initial_missile_damage_rate = 100.0 * missile_firing_rate * accuracy;
let time_boss_dead = boss_hp / initial_missile_damage_rate;
let time_missiles_exhausted = if inventory.max_missiles == 0 {
0.0
} else if net_missile_use_rate > 0.0 {
(missiles_available as f32) / net_missile_use_rate
} else {
f32::INFINITY
};
let overall_damage_rate = farming_missile_damage_rate + charge_damage_rate;
if overall_damage_rate == 0.0 {
return false;
let time_missiles = f32::min(time_boss_dead, time_missiles_exhausted);
time += time_missiles;
boss_hp -= time_missiles * initial_missile_damage_rate;

if time_missiles_exhausted < time_boss_dead {
// Boss is not dead yet after exhausting all Missiles (if any).
// Continue the fight using Missiles only at the lower rate at which they can be farmed (if available).
let farming_missile_damage_rate = if inventory.max_missiles > 0 {
100.0 * missile_farm_rate * accuracy
} else {
0.0
};
let overall_damage_rate = f32::max(farming_missile_damage_rate, charge_damage_rate);
if overall_damage_rate == 0.0 {
return false;
}
time += boss_hp / overall_damage_rate;
}
time += boss_hp / overall_damage_rate;
}

if time < 180.0 || can_be_very_patient {
let mut net_dps = base_hit_dps / suit_damage_factor(inventory) as f32 - energy_farm_rate;
if net_dps < 0.0 {
net_dps = 0.0;
}
// Overflow safeguard - bail here if Samus takes calamitous damage.
if net_dps * time > 10000.0 {
return false;
}
// We don't account for resources used, since they can be farmed or picked up after the fight, and we don't
// want the fight to go out of logic due to not saving enough Missiles to open some red doors for example.
// TODO: do something more reasonable here.
local.use_energy((net_dps * time) as Capacity, true, inventory, reverse)
} else {
false
// For determining if patience tech is required:
// `good_time` = hypothetical time based on good execution
let good_time = time * firing_skill * accuracy;
// Without "canBePatient" we tolerate a longer fight compared to other strats
// (180 seconds vs. 90 seconds), since the fight likely only has to be done once and is
// not as boring as other patience-constrained strats.
if good_time >= 150.0 && !can_be_patient
|| good_time >= 240.0 && !can_be_very_patient
|| good_time >= 360.0 && !can_be_extremely_patient
{
// We don't have enough patience to finish the fight:
return false;
}

let mut net_dps = base_hit_dps / suit_damage_factor(inventory) as f32 - energy_farm_rate;
if net_dps < 0.0 {
net_dps = 0.0;
}
// Overflow safeguard - bail here if Samus takes calamitous damage.
if net_dps * time > 10000.0 {
return false;
}
// We don't account for resources used, since they can be farmed or picked up after the fight, and we don't
// want the fight to go out of logic due to not saving enough Missiles to open some red doors for example.
// TODO: do something more reasonable here.
local.use_energy((net_dps * time) as Capacity, true, inventory, reverse)
}

pub fn apply_ridley_requirement(
Expand Down Expand Up @@ -299,7 +363,7 @@ pub fn apply_ridley_requirement(
// `good_time` = hypothetical time based on good but safe execution
// This is 15% slower than the optimized times which are more applicable for short fights.
let good_time = time * firing_rate * accuracy * 1.15;
// With "canBePatient" (Expert) we tolerate a little longer fight compared to other strats
// Without "canBePatient" we tolerate a little longer fight compared to other strats
// (120 seconds vs. 90 seconds), since the fight likely only has to be done once and is
// not as boring as other patience-constrained strats.
if good_time >= 120.0 && !can_be_patient
Expand Down
4 changes: 4 additions & 0 deletions rust/maprando-wasm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ pub fn can_defeat_draygon(
inventory: JsValue,
local: JsValue,
proficiency: f32,
can_be_patient: bool,
can_be_very_patient: bool,
can_be_extremely_patient: bool,
) -> JsValue {
let inventory: Inventory = serde_wasm_bindgen::from_value(inventory).unwrap();
let mut local =
Expand All @@ -44,7 +46,9 @@ pub fn can_defeat_draygon(
&inventory,
&mut local,
proficiency,
can_be_patient,
can_be_very_patient,
can_be_extremely_patient,
false,
) {
serde_wasm_bindgen::to_value(&local).unwrap()
Expand Down
2 changes: 1 addition & 1 deletion rust/maprando-web/templates/logic/boss_calculator.html
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ <h4 class="text-center">Results</h4>
local = can_defeat_phantoon(inventory, local, proficiency);
}
else if (selected_boss == "Draygon") {
local = can_defeat_draygon(inventory, local, proficiency, can_be_very_patient);
local = can_defeat_draygon(inventory, local, proficiency, can_be_patient, can_be_very_patient, can_be_extremely_patient);
}
else if (selected_boss == "Ridley") {
local = can_defeat_ridley(inventory, local, proficiency, can_be_patient, can_be_very_patient, can_be_extremely_patient);
Expand Down
Loading