From e5c54fadf8d77ca795c5a57fccd696cd1f52badc Mon Sep 17 00:00:00 2001 From: Michael McKenzie Date: Sat, 13 Dec 2025 08:26:56 -0330 Subject: [PATCH 1/8] Update Draygon for Hard --- rust/maprando-logic/src/boss_requirements.rs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/rust/maprando-logic/src/boss_requirements.rs b/rust/maprando-logic/src/boss_requirements.rs index 5e5df6ea7..394396f3b 100644 --- a/rust/maprando-logic/src/boss_requirements.rs +++ b/rust/maprando-logic/src/boss_requirements.rs @@ -82,7 +82,12 @@ pub fn apply_draygon_requirement( reverse: bool, ) -> bool { let mut boss_hp: f32 = 6000.0; - let charge_damage = get_charge_damage(inventory); + // Charge without Plasma is mostly a way to do the fight without missiles. + let charge_damage = if proficiency >= 0.8 || inventory.items[Item::Plasma as usize] { + get_charge_damage(inventory) + } else { + 0.0 + }; // Assume an accuracy of between 40% (on lowest difficulty) to 100% (on highest). let accuracy = 0.4 + 0.6 * proficiency; @@ -97,20 +102,24 @@ pub fn apply_draygon_requirement( let charge_firing_rate = (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 = if proficiency <= 0.5 { + 0.0 + } else { + 0.2 + 0.8 * 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); From 9f175580fd217f3d81f1c454299eaf9694a8a437 Mon Sep 17 00:00:00 2001 From: Michael McKenzie Date: Sat, 13 Dec 2025 12:53:16 -0330 Subject: [PATCH 2/8] Update Draygon for Hard --- rust/maprando-logic/src/boss_requirements.rs | 66 +++++++++++++------- 1 file changed, 44 insertions(+), 22 deletions(-) diff --git a/rust/maprando-logic/src/boss_requirements.rs b/rust/maprando-logic/src/boss_requirements.rs index 394396f3b..4de6d0967 100644 --- a/rust/maprando-logic/src/boss_requirements.rs +++ b/rust/maprando-logic/src/boss_requirements.rs @@ -92,21 +92,26 @@ 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_rate = if inventory.items[Item::Gravity as usize] { + 0.3 + 0.7 * proficiency + } else { + 0.5 * 0.3 + 0.7 * proficiency + }; 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_damage_rate = charge_firing_rate * charge_damage * accuracy; - - let farm_proficiency = if proficiency <= 0.5 { - 0.0 + 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 { - 0.2 + 0.8 * proficiency + // Assume a maximum of 1 charge shot per goop phase, and 1 charge shot per swoop. + (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 base_goop_farms_per_cycle = match ( inventory.items[Item::Plasma as usize], inventory.items[Item::Wave as usize], @@ -125,23 +130,39 @@ pub fn apply_draygon_requirement( 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) - } 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) + 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 * 3.0 * (GOOP_CYCLES_PER_SECOND + SWOOP_CYCLES_PER_SECOND) * (1.5 - proficiency) + } + (true, false) => { + // With Gravity, Swoops are more difficult while Goops are manageable + 160.0 * (GOOP_CYCLES_PER_SECOND + SWOOP_CYCLES_PER_SECOND) * (1.0 - proficiency) + } + (false, true) => { + // With Morph, Goops are difficult while Swoops are manageable + 160.0 * (GOOP_CYCLES_PER_SECOND + SWOOP_CYCLES_PER_SECOND) * (1.0 - proficiency) + } + (true, true) => { + // With Gravity and Morph, Assume a low hit rate: + 160.0 * 0.25 * (GOOP_CYCLES_PER_SECOND + SWOOP_CYCLES_PER_SECOND) * (1.0 - proficiency) + } }; - // 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 super_firing_rate = 4.0 * GOOP_CYCLES_PER_SECOND * firing_rate; + let time_supers_exhausted = supers_used * super_firing_rate; let missiles_available = local.missiles_available(inventory, reverse); - let missile_firing_rate = 20.0 * GOOP_CYCLES_PER_SECOND * firing_rate; + let missile_firing_rate = 10.0 * GOOP_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; @@ -154,7 +175,8 @@ pub fn apply_draygon_requirement( } else { f32::INFINITY }; - let mut time = f32::min(time_boss_dead, time_missiles_exhausted); + let mut time = time_supers_exhausted + 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). From 6d1ba211471389292db00a489b6aef2296589680 Mon Sep 17 00:00:00 2001 From: Michael McKenzie Date: Sat, 13 Dec 2025 13:04:03 -0330 Subject: [PATCH 3/8] Return farming floor --- rust/maprando-logic/src/boss_requirements.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/rust/maprando-logic/src/boss_requirements.rs b/rust/maprando-logic/src/boss_requirements.rs index 4de6d0967..2167ee08a 100644 --- a/rust/maprando-logic/src/boss_requirements.rs +++ b/rust/maprando-logic/src/boss_requirements.rs @@ -111,7 +111,12 @@ pub fn apply_draygon_requirement( }; let charge_damage_rate = charge_firing_rate * charge_damage * accuracy; - let farm_proficiency = 0.2 + 0.8 * proficiency; + // There is a minimum amount of proficiency needed to gain resources from farming + let farm_proficiency = if proficiency <= 0.6 { + 0.1 + } else { + 0.2 + 0.8 * proficiency + }; let base_goop_farms_per_cycle = match ( inventory.items[Item::Plasma as usize], inventory.items[Item::Wave as usize], From d2ea1ea70966075b5023dd5477ccee3ac0488152 Mon Sep 17 00:00:00 2001 From: Michael McKenzie Date: Sat, 13 Dec 2025 13:12:31 -0330 Subject: [PATCH 4/8] Saving Expert from Draygon --- rust/data/presets/skill-assumptions/Expert+.json | 2 +- rust/data/presets/skill-assumptions/Expert.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/rust/data/presets/skill-assumptions/Expert+.json b/rust/data/presets/skill-assumptions/Expert+.json index 8c5715ce6..6a93486cc 100644 --- a/rust/data/presets/skill-assumptions/Expert+.json +++ b/rust/data/presets/skill-assumptions/Expert+.json @@ -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, diff --git a/rust/data/presets/skill-assumptions/Expert.json b/rust/data/presets/skill-assumptions/Expert.json index 91d9ec04c..78e24fcc3 100644 --- a/rust/data/presets/skill-assumptions/Expert.json +++ b/rust/data/presets/skill-assumptions/Expert.json @@ -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, From a14639dd365004eff517dbfc5116063f5440b1fe Mon Sep 17 00:00:00 2001 From: Brent Kerby Date: Sat, 13 Dec 2025 18:36:14 -0700 Subject: [PATCH 5/8] second pass --- .../presets/skill-assumptions/Extreme.json | 2 +- rust/maprando-game/src/lib.rs | 6 + rust/maprando-logic/src/boss_requirements.rs | 149 ++++++++++-------- rust/maprando-wasm/src/lib.rs | 4 + .../templates/logic/boss_calculator.html | 2 +- rust/maprando/src/bin/debug.rs | 78 +++++---- rust/maprando/src/traverse.rs | 8 +- 7 files changed, 151 insertions(+), 98 deletions(-) diff --git a/rust/data/presets/skill-assumptions/Extreme.json b/rust/data/presets/skill-assumptions/Extreme.json index 05c5f90eb..53b7b89ce 100644 --- a/rust/data/presets/skill-assumptions/Extreme.json +++ b/rust/data/presets/skill-assumptions/Extreme.json @@ -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, diff --git a/rust/maprando-game/src/lib.rs b/rust/maprando-game/src/lib.rs index 63af46209..4223bbf42 100644 --- a/rust/maprando-game/src/lib.rs +++ b/rust/maprando-game/src/lib.rs @@ -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, @@ -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 { diff --git a/rust/maprando-logic/src/boss_requirements.rs b/rust/maprando-logic/src/boss_requirements.rs index 2167ee08a..2c00e1836 100644 --- a/rust/maprando-logic/src/boss_requirements.rs +++ b/rust/maprando-logic/src/boss_requirements.rs @@ -78,32 +78,32 @@ 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; - // Charge without Plasma is mostly a way to do the fight without missiles. - let charge_damage = if proficiency >= 0.8 || inventory.items[Item::Plasma as usize] { - get_charge_damage(inventory) - } else { - 0.0 - }; + let charge_damage = get_charge_damage(inventory); // 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 30% (on lowest difficulty) to 100% (on highest). - let firing_rate = if inventory.items[Item::Gravity as usize] { - 0.3 + 0.7 * proficiency - } else { - 0.5 * 0.3 + 0.7 * proficiency + 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; 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. + // 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 swoop. @@ -111,12 +111,7 @@ pub fn apply_draygon_requirement( }; let charge_damage_rate = charge_firing_rate * charge_damage * accuracy; - // There is a minimum amount of proficiency needed to gain resources from farming - let farm_proficiency = if proficiency <= 0.6 { - 0.1 - } else { - 0.2 + 0.8 * proficiency - }; + let farm_proficiency = 0.1 + 0.3 * proficiency + 0.6 * proficiency * proficiency; let base_goop_farms_per_cycle = match ( inventory.items[Item::Plasma as usize], inventory.items[Item::Wave as usize], @@ -141,19 +136,28 @@ pub fn apply_draygon_requirement( ) { (false, false) => { // Without Gravity or Morph, dodging is a challenge... - 160.0 * 3.0 * (GOOP_CYCLES_PER_SECOND + SWOOP_CYCLES_PER_SECOND) * (1.5 - proficiency) + 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) * (1.0 - proficiency) + 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) * (1.0 - proficiency) + 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.25 * (GOOP_CYCLES_PER_SECOND + SWOOP_CYCLES_PER_SECOND) * (1.0 - proficiency) + 160.0 + * 0.5 + * (GOOP_CYCLES_PER_SECOND + SWOOP_CYCLES_PER_SECOND) + * f32::powf(1.0 - proficiency, 2.0) } }; @@ -163,58 +167,71 @@ pub fn apply_draygon_requirement( let supers_used = f32::min(supers_needed, supers_available as f32); boss_hp -= supers_used * accuracy * 300.0; - let super_firing_rate = 4.0 * GOOP_CYCLES_PER_SECOND * firing_rate; - let time_supers_exhausted = supers_used * super_firing_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 missiles_available = local.missiles_available(inventory, reverse); - let missile_firing_rate = 10.0 * GOOP_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 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 = time_supers_exhausted + 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; + 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 farming_missile_damage_rate = if inventory.max_missiles > 0 { - 100.0 * missile_farm_rate * accuracy - } else { + 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 >= 180.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( @@ -335,7 +352,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 diff --git a/rust/maprando-wasm/src/lib.rs b/rust/maprando-wasm/src/lib.rs index 7cee62709..aa1a27336 100644 --- a/rust/maprando-wasm/src/lib.rs +++ b/rust/maprando-wasm/src/lib.rs @@ -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 = @@ -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() diff --git a/rust/maprando-web/templates/logic/boss_calculator.html b/rust/maprando-web/templates/logic/boss_calculator.html index 9af2da014..e360d57d9 100644 --- a/rust/maprando-web/templates/logic/boss_calculator.html +++ b/rust/maprando-web/templates/logic/boss_calculator.html @@ -207,7 +207,7 @@

Results

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); diff --git a/rust/maprando/src/bin/debug.rs b/rust/maprando/src/bin/debug.rs index ff3c6dd8c..e21859930 100644 --- a/rust/maprando/src/bin/debug.rs +++ b/rust/maprando/src/bin/debug.rs @@ -10,8 +10,8 @@ use maprando::{ traverse::{LockedDoorData, apply_requirement, simple_cost_config}, }; use maprando_game::{ - Capacity, GameData, Item, NodeId, Requirement, RidleyStuck, RoomId, - TECH_ID_CAN_BE_EXTREMELY_PATIENT, TECH_ID_CAN_BE_PATIENT, TECH_ID_CAN_BE_VERY_PATIENT, + Capacity, GameData, Item, NodeId, Requirement, RoomId, TECH_ID_CAN_BE_EXTREMELY_PATIENT, + TECH_ID_CAN_BE_PATIENT, TECH_ID_CAN_BE_VERY_PATIENT, }; use maprando_logic::{GlobalState, Inventory, LocalState}; use rand::SeedableRng; @@ -22,7 +22,7 @@ fn run_scenario( missile_cnt: Capacity, super_cnt: Capacity, item_loadout: &[&'static str], - patience: bool, + patience: i32, settings: &RandomizerSettings, mut difficulty: DifficultyConfig, game_data: &GameData, @@ -95,33 +95,31 @@ fn run_scenario( let objectives = get_objectives(settings, None, game_data, &mut rng); difficulty.draygon_proficiency = proficiency; difficulty.ridley_proficiency = proficiency; - difficulty.tech[game_data.tech_isv.index_by_key[&TECH_ID_CAN_BE_VERY_PATIENT]] = patience; - difficulty.tech[game_data.tech_isv.index_by_key[&TECH_ID_CAN_BE_EXTREMELY_PATIENT]] = patience; - // let new_local_state_opt = apply_requirement( - // &Requirement::DraygonFight { - // can_be_very_patient_tech_idx: game_data.tech_isv.index_by_key - // [&TECH_ID_CAN_BE_VERY_PATIENT], - // }, - // &global_state, - // local_state, - // false, - // settings, - // &difficulty, - // game_data, - // &locked_door_data, - // &objectives, - // ); + match patience { + 0 => { /* do nothing */ } + 1 => { + difficulty.tech[game_data.tech_isv.index_by_key[&TECH_ID_CAN_BE_PATIENT]] = true; + } + 2 => { + difficulty.tech[game_data.tech_isv.index_by_key[&TECH_ID_CAN_BE_PATIENT]] = true; + difficulty.tech[game_data.tech_isv.index_by_key[&TECH_ID_CAN_BE_VERY_PATIENT]] = true; + } + 3 => { + difficulty.tech[game_data.tech_isv.index_by_key[&TECH_ID_CAN_BE_PATIENT]] = true; + difficulty.tech[game_data.tech_isv.index_by_key[&TECH_ID_CAN_BE_VERY_PATIENT]] = true; + difficulty.tech[game_data.tech_isv.index_by_key[&TECH_ID_CAN_BE_EXTREMELY_PATIENT]] = + true; + } + _ => panic!("unrecognized patience value {}", patience), + } let cost_config = simple_cost_config(); let new_local_state_opt = apply_requirement( - &Requirement::RidleyFight { + &Requirement::DraygonFight { can_be_patient_tech_idx: game_data.tech_isv.index_by_key[&TECH_ID_CAN_BE_PATIENT], can_be_very_patient_tech_idx: game_data.tech_isv.index_by_key [&TECH_ID_CAN_BE_VERY_PATIENT], can_be_extremely_patient_tech_idx: game_data.tech_isv.index_by_key [&TECH_ID_CAN_BE_EXTREMELY_PATIENT], - power_bombs: true, - g_mode: false, - stuck: RidleyStuck::None, }, &global_state, local_state, @@ -134,12 +132,34 @@ fn run_scenario( &objectives, &cost_config, ); + // let new_local_state_opt = apply_requirement( + // &Requirement::RidleyFight { + // can_be_patient_tech_idx: game_data.tech_isv.index_by_key[&TECH_ID_CAN_BE_PATIENT], + // can_be_very_patient_tech_idx: game_data.tech_isv.index_by_key + // [&TECH_ID_CAN_BE_VERY_PATIENT], + // can_be_extremely_patient_tech_idx: game_data.tech_isv.index_by_key + // [&TECH_ID_CAN_BE_EXTREMELY_PATIENT], + // power_bombs: true, + // g_mode: false, + // stuck: RidleyStuck::None, + // }, + // &global_state, + // local_state, + // false, + // settings, + // &difficulty, + // game_data, + // &door_map, + // &locked_door_data, + // &objectives, + // &cost_config, + // ); let outcome = new_local_state_opt .map(|x| format!("{:?}", x.energy)) .unwrap_or("n/a".to_string()); println!( - "proficiency={proficiency}, items={item_loadout:?}, missiles={missile_cnt}, patience={patience}: {outcome}" + "proficiency={proficiency}, items={item_loadout:?}, missiles={missile_cnt}, supers={super_cnt}, patience={patience}: {outcome}" ); } @@ -160,16 +180,18 @@ fn main() -> Result<()> { settings.skill_assumption_settings = preset_data.skill_presets.last().unwrap().clone(); let difficulty = preset_data.difficulty_tiers.last().unwrap(); - let proficiencies = vec![0.0, 0.3, 0.5, 0.7, 0.8, 0.825, 0.85, 0.9, 0.95, 1.0]; - let missile_counts = vec![60]; + // let proficiencies = vec![0.0, 0.3, 0.5, 0.7, 0.8, 0.825, 0.85, 0.9, 0.95, 1.0]; + let proficiencies = vec![0.85]; + let missile_counts = vec![5, 10, 20, 30, 40, 50, 60, 70, 80]; let super_counts = vec![0]; - let item_loadouts = vec![vec!["M", "V", "C"]]; + let patience_values = vec![0]; // 0 = no patience tech, 1 = canBePatient, 2 = canBeVeryPatient, 3 = canBeExtremelyPatient + let item_loadouts = vec![vec!["M"]]; for &proficiency in &proficiencies { for &missile_cnt in &missile_counts { for &super_cnt in &super_counts { for beam_loadout in &item_loadouts { - for patience in [true, false] { + for &patience in &patience_values { run_scenario( proficiency, missile_cnt, diff --git a/rust/maprando/src/traverse.rs b/rust/maprando/src/traverse.rs index 0f49ccad4..021c58b30 100644 --- a/rust/maprando/src/traverse.rs +++ b/rust/maprando/src/traverse.rs @@ -1768,12 +1768,16 @@ fn apply_requirement_simple( ) .into(), Requirement::DraygonFight { - can_be_very_patient_tech_idx: can_be_very_patient_tech_id, + can_be_patient_tech_idx, + can_be_very_patient_tech_idx, + can_be_extremely_patient_tech_idx, } => apply_draygon_requirement( &cx.global.inventory, local, cx.difficulty.draygon_proficiency, - cx.difficulty.tech[*can_be_very_patient_tech_id], + cx.difficulty.tech[*can_be_patient_tech_idx], + cx.difficulty.tech[*can_be_very_patient_tech_idx], + cx.difficulty.tech[*can_be_extremely_patient_tech_idx], cx.reverse, ) .into(), From b74d541899e1318d9eff6d7242c5afe02f777c36 Mon Sep 17 00:00:00 2001 From: Michael McKenzie Date: Sun, 14 Dec 2025 05:50:22 -0330 Subject: [PATCH 6/8] missiles & charge & initial hit --- rust/maprando-logic/src/boss_requirements.rs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/rust/maprando-logic/src/boss_requirements.rs b/rust/maprando-logic/src/boss_requirements.rs index 2c00e1836..062d0163f 100644 --- a/rust/maprando-logic/src/boss_requirements.rs +++ b/rust/maprando-logic/src/boss_requirements.rs @@ -106,12 +106,12 @@ pub fn apply_draygon_requirement( // 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 swoop. - (SWOOP_CYCLES_PER_SECOND + GOOP_CYCLES_PER_SECOND) * firing_rate + // 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.1 + 0.3 * proficiency + 0.6 * proficiency * 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], @@ -128,7 +128,11 @@ pub fn apply_draygon_requirement( }; 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 missle_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 * missle_farms_per_cycle; let base_hit_dps = match ( inventory.items[Item::Gravity as usize], @@ -161,6 +165,11 @@ pub fn apply_draygon_requirement( } }; + // Damage is represented in DPS but is actually taken in chunks. Ensure 1 hit can be survived: + if base_hit_dps > 0.0 && 160 / suit_damage_factor(inventory) > inventory.max_energy { + return false; + } + // Start by using all Supers let supers_available = local.supers_available(inventory, reverse); let supers_needed = boss_hp / 300.0 / accuracy; @@ -177,7 +186,7 @@ pub fn apply_draygon_requirement( (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 initial_missile_damage_rate = 100.0 * missile_firing_rate * accuracy * accuracy; let time_boss_dead = boss_hp / initial_missile_damage_rate; let time_missiles_exhausted = if inventory.max_missiles == 0 { 0.0 From ec20e4076d14876041c54b5545ef7c735c9a490f Mon Sep 17 00:00:00 2001 From: Brent Kerby Date: Sun, 14 Dec 2025 06:10:02 -0700 Subject: [PATCH 7/8] zero energy_farm_rate if can't survive hit --- rust/maprando-logic/src/boss_requirements.rs | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/rust/maprando-logic/src/boss_requirements.rs b/rust/maprando-logic/src/boss_requirements.rs index 062d0163f..ad57ae7ec 100644 --- a/rust/maprando-logic/src/boss_requirements.rs +++ b/rust/maprando-logic/src/boss_requirements.rs @@ -126,13 +126,20 @@ pub fn apply_draygon_requirement( // Without Gravity you can't farm as many goops since you have to spend more time avoiding Draygon. 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 missle_farms_per_cycle = f32::min( + 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 { + 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 * missle_farms_per_cycle; + let missile_farm_rate = GOOP_CYCLES_PER_SECOND * missile_farms_per_cycle; let base_hit_dps = match ( inventory.items[Item::Gravity as usize], @@ -165,11 +172,6 @@ pub fn apply_draygon_requirement( } }; - // Damage is represented in DPS but is actually taken in chunks. Ensure 1 hit can be survived: - if base_hit_dps > 0.0 && 160 / suit_damage_factor(inventory) > inventory.max_energy { - return false; - } - // Start by using all Supers let supers_available = local.supers_available(inventory, reverse); let supers_needed = boss_hp / 300.0 / accuracy; From bc12555210fc8de47b4c2a3d1e7598da5ffcc0d9 Mon Sep 17 00:00:00 2001 From: Michael McKenzie Date: Mon, 15 Dec 2025 17:51:30 -0330 Subject: [PATCH 8/8] lower patience --- rust/maprando-logic/src/boss_requirements.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rust/maprando-logic/src/boss_requirements.rs b/rust/maprando-logic/src/boss_requirements.rs index ad57ae7ec..59cb186c8 100644 --- a/rust/maprando-logic/src/boss_requirements.rs +++ b/rust/maprando-logic/src/boss_requirements.rs @@ -188,7 +188,7 @@ pub fn apply_draygon_requirement( (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 * accuracy; + 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 @@ -223,7 +223,7 @@ pub fn apply_draygon_requirement( // 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 >= 180.0 && !can_be_patient + if good_time >= 150.0 && !can_be_patient || good_time >= 240.0 && !can_be_very_patient || good_time >= 360.0 && !can_be_extremely_patient {