Compare commits
5 Commits
321e3ebb76
...
terran
| Author | SHA1 | Date | |
|---|---|---|---|
| 810fc4a3d4 | |||
| 9956c930cd | |||
| dd24f686b3 | |||
| 997cbdb48e | |||
| 0dfdb6a6df |
@@ -70,16 +70,16 @@ impl AiModule for ProtosBot {
|
|||||||
}
|
}
|
||||||
println!("unit created: {:?}", unit.get_type());
|
println!("unit created: {:?}", unit.get_type());
|
||||||
|
|
||||||
// Check if the created unit is a building
|
// If the created unit is a building, handle building creation; otherwise handle unit creation (e.g., trained units).
|
||||||
if !unit.get_type().is_building() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let Ok(mut locked_state) = self.game_state.lock() else {
|
let Ok(mut locked_state) = self.game_state.lock() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if unit.get_type().is_building() {
|
||||||
build_manager::on_building_create(&unit, &mut locked_state);
|
build_manager::on_building_create(&unit, &mut locked_state);
|
||||||
|
} else {
|
||||||
|
build_manager::on_unit_create(&unit, &mut locked_state);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_unit_morph(&mut self, _game: &Game, _unit: Unit) {}
|
fn on_unit_morph(&mut self, _game: &Game, _unit: Unit) {}
|
||||||
|
|||||||
@@ -24,23 +24,26 @@ impl BuildStage {
|
|||||||
pub fn get_build_stages() -> Vec<BuildStage> {
|
pub fn get_build_stages() -> Vec<BuildStage> {
|
||||||
vec![
|
vec![
|
||||||
BuildStage::new("Start")
|
BuildStage::new("Start")
|
||||||
.with_unit(UnitType::Protoss_Probe, 10)
|
.with_unit(UnitType::Terran_SCV, 10)
|
||||||
.with_unit(UnitType::Protoss_Pylon, 1),
|
.with_unit(UnitType::Terran_Supply_Depot, 1),
|
||||||
|
|
||||||
BuildStage::new("Basic Production")
|
BuildStage::new("Basic Production")
|
||||||
.with_unit(UnitType::Protoss_Probe, 12)
|
.with_unit(UnitType::Terran_SCV, 12)
|
||||||
.with_unit(UnitType::Protoss_Pylon, 2)
|
.with_unit(UnitType::Terran_Supply_Depot, 2)
|
||||||
.with_unit(UnitType::Protoss_Gateway, 1)
|
.with_unit(UnitType::Terran_Barracks, 1)
|
||||||
.with_unit(UnitType::Protoss_Forge, 1),
|
.with_unit(UnitType::Terran_Refinery, 1),
|
||||||
|
BuildStage::new("Defense Bunker")
|
||||||
|
.with_unit(UnitType::Terran_SCV, 16)
|
||||||
|
.with_unit(UnitType::Terran_Supply_Depot, 3)
|
||||||
|
.with_unit(UnitType::Terran_Command_Center, 1)
|
||||||
|
.with_unit(UnitType::Terran_Barracks, 1)
|
||||||
|
.with_unit(UnitType::Terran_Refinery, 1),
|
||||||
|
|
||||||
|
BuildStage::new("Mid Game")
|
||||||
// Stage 2: Defense cannons
|
.with_unit(UnitType::Terran_SCV, 20)
|
||||||
BuildStage::new("Defense Cannons")
|
.with_unit(UnitType::Terran_Supply_Depot, 4)
|
||||||
.with_unit(UnitType::Protoss_Probe, 16)
|
.with_unit(UnitType::Terran_Command_Center, 2)
|
||||||
.with_unit(UnitType::Protoss_Pylon, 3)
|
.with_unit(UnitType::Terran_Barracks, 2)
|
||||||
.with_unit(UnitType::Protoss_Nexus, 1)
|
.with_unit(UnitType::Terran_Refinery, 2)
|
||||||
.with_unit(UnitType::Protoss_Gateway, 1)
|
.with_unit(UnitType::Terran_Missile_Turret, 2),
|
||||||
.with_unit(UnitType::Protoss_Forge, 1)
|
|
||||||
.with_unit(UnitType::Protoss_Photon_Cannon, 4),
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
use rsbwapi::{Order, Position, Unit, UnitType, UpgradeType};
|
use rsbwapi::{UnitType, UpgradeType};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use crate::state::build_stages::BuildStage;
|
use crate::state::build_stages::BuildStage;
|
||||||
|
|
||||||
pub struct GameState {
|
pub struct GameState {
|
||||||
pub intended_commands: HashMap<usize, IntendedCommand>,
|
|
||||||
pub unit_build_history: Vec<BuildHistoryEntry>,
|
pub unit_build_history: Vec<BuildHistoryEntry>,
|
||||||
pub build_stages: Vec<BuildStage>,
|
pub build_stages: Vec<BuildStage>,
|
||||||
pub current_stage_index: usize,
|
pub current_stage_index: usize,
|
||||||
@@ -15,7 +14,6 @@ pub struct GameState {
|
|||||||
impl Default for GameState {
|
impl Default for GameState {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
intended_commands: HashMap::new(),
|
|
||||||
unit_build_history: Vec::new(),
|
unit_build_history: Vec::new(),
|
||||||
build_stages: crate::state::build_stages::get_build_stages(),
|
build_stages: crate::state::build_stages::get_build_stages(),
|
||||||
current_stage_index: 0,
|
current_stage_index: 0,
|
||||||
@@ -25,13 +23,6 @@ impl Default for GameState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct IntendedCommand {
|
|
||||||
pub order: Order,
|
|
||||||
pub target_position: Option<Position>,
|
|
||||||
pub target_unit: Option<Unit>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum BuildStatus {
|
pub enum BuildStatus {
|
||||||
Assigned,
|
Assigned,
|
||||||
@@ -43,5 +34,6 @@ pub struct BuildHistoryEntry {
|
|||||||
pub unit_type: Option<UnitType>,
|
pub unit_type: Option<UnitType>,
|
||||||
pub upgrade_type: Option<UpgradeType>,
|
pub upgrade_type: Option<UpgradeType>,
|
||||||
pub assigned_unit_id: Option<usize>,
|
pub assigned_unit_id: Option<usize>,
|
||||||
|
pub tile_position: Option<rsbwapi::TilePosition>,
|
||||||
// pub status: BuildStatus,
|
// pub status: BuildStatus,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,48 +1,46 @@
|
|||||||
use rsbwapi::{Game, TilePosition, Unit, UnitType};
|
use rsbwapi::{Game, Player, TilePosition, Unit, UnitType};
|
||||||
|
|
||||||
pub fn find_build_location(
|
pub fn find_build_location(
|
||||||
game: &Game,
|
game: &Game,
|
||||||
|
_player: &Player,
|
||||||
builder: &Unit,
|
builder: &Unit,
|
||||||
building_type: UnitType,
|
building_type: UnitType,
|
||||||
max_range: i32,
|
max_range: i32,
|
||||||
) -> Option<TilePosition> {
|
) -> Option<TilePosition> {
|
||||||
let start_tile = builder.get_tile_position();
|
if building_type.is_refinery() {
|
||||||
let map_width = game.map_width();
|
for geyser in game.get_geysers() {
|
||||||
let map_height = game.map_height();
|
let tp = geyser.get_tile_position();
|
||||||
|
if let Ok(true) = game.can_build_here(Some(builder), tp, building_type, false) {
|
||||||
for distance in 0..max_range {
|
return Some(tp);
|
||||||
for dx in -distance..=distance {
|
}
|
||||||
for dy in -distance..=distance {
|
}
|
||||||
if dx.abs() != distance && dy.abs() != distance {
|
return None;
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let tile = TilePosition {
|
let start = builder.get_tile_position();
|
||||||
x: start_tile.x + dx,
|
for radius in 0..=max_range {
|
||||||
y: start_tile.y + dy,
|
for dx in -radius..=radius {
|
||||||
|
let dy = radius - dx.abs();
|
||||||
|
for &dy_sign in &[-1, 1] {
|
||||||
|
let cand = TilePosition {
|
||||||
|
x: start.x + dx,
|
||||||
|
y: start.y + dy * dy_sign,
|
||||||
};
|
};
|
||||||
|
if let Ok(true) = game.can_build_here(Some(builder), cand, building_type, false) {
|
||||||
if tile.x < 0 || tile.y < 0 || tile.x >= map_width || tile.y >= map_height {
|
return Some(cand);
|
||||||
|
}
|
||||||
|
if dy == 0 {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
let cand2 = TilePosition {
|
||||||
if is_valid_build_location(game, building_type, tile, builder) {
|
x: start.x + dx,
|
||||||
return Some(tile);
|
y: start.y - dy * dy_sign,
|
||||||
|
};
|
||||||
|
if let Ok(true) = game.can_build_here(Some(builder), cand2, building_type, false) {
|
||||||
|
return Some(cand2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_valid_build_location(
|
|
||||||
game: &Game,
|
|
||||||
building_type: UnitType,
|
|
||||||
position: TilePosition,
|
|
||||||
builder: &Unit,
|
|
||||||
) -> bool {
|
|
||||||
game
|
|
||||||
.can_build_here(builder, position, building_type, false)
|
|
||||||
.unwrap_or(false)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,91 +1,100 @@
|
|||||||
use rsbwapi::{Game, Order, Player, Unit, UnitType};
|
use rsbwapi::{Game, Player, Unit, UnitType};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
state::game_state::{BuildHistoryEntry, GameState, IntendedCommand},
|
state::game_state::{BuildHistoryEntry, GameState},
|
||||||
utils::build_location_utils,
|
utils::build_location_utils,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn on_frame(game: &Game, player: &Player, state: &mut GameState) {
|
pub fn on_frame(game: &Game, player: &Player, state: &mut GameState) {
|
||||||
cleanup_stale_commands(player, state);
|
// No intended command tracking; directly manage builds and stages.
|
||||||
check_and_advance_stage(player, state);
|
check_and_advance_stage(player, state);
|
||||||
state.stage_item_status = get_status_for_stage_items(game, player, state);
|
state.stage_item_status = get_status_for_stage_items(game, player, state);
|
||||||
|
|
||||||
try_start_next_build(game, player, state);
|
try_start_next_build(game, player, state);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_building_create(unit: &Unit, state: &mut GameState) {
|
pub fn on_building_create(unit: &Unit, state: &mut GameState) {
|
||||||
if let Some(entry) = state
|
// When a building is created, remove the corresponding build history entry so the next
|
||||||
.unit_build_history
|
// build can be started without waiting for the current one to finish.
|
||||||
.iter()
|
if let Some(pos) = state.unit_build_history.iter().position(|entry| {
|
||||||
.rev()
|
entry
|
||||||
.find(|e| e.unit_type == Some(unit.get_type()))
|
.unit_type
|
||||||
{
|
.map(|ut| ut == unit.get_type())
|
||||||
if let Some(probe_id) = entry.assigned_unit_id {
|
.unwrap_or(false)
|
||||||
// Remove the probe's intended command (PlaceBuilding order)
|
}) {
|
||||||
state.intended_commands.remove(&probe_id);
|
state.unit_build_history.remove(pos);
|
||||||
println!(
|
|
||||||
"Building {} started. Removed assignment for probe {}",
|
|
||||||
unit.get_type().name(),
|
|
||||||
probe_id
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cleanup_stale_commands(player: &Player, state: &mut GameState) {
|
/// Called when a non‑building unit (e.g., a trained unit) is created.
|
||||||
let unit_ids: Vec<usize> = player.get_units().iter().map(|u| u.get_id()).collect();
|
/// This clears any pending assignment for the building that trained it.
|
||||||
|
pub fn on_unit_create(_unit: &Unit, _state: &mut GameState) {
|
||||||
state.intended_commands.retain(|unit_id, cmd| {
|
// No intended command tracking needed for unit creation.
|
||||||
// Remove if unit no longer exists
|
|
||||||
if !unit_ids.contains(unit_id) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the unit
|
|
||||||
if let Some(unit) = player.get_units().iter().find(|u| u.get_id() == *unit_id) {
|
|
||||||
// For PlaceBuilding orders, check if unit is actually constructing or idle
|
|
||||||
if cmd.order == Order::PlaceBuilding {
|
|
||||||
// Keep command only if unit is moving to build location or constructing
|
|
||||||
return unit.is_constructing() || unit.get_order() == Order::PlaceBuilding;
|
|
||||||
}
|
|
||||||
// For Train orders, check if the building is training
|
|
||||||
if cmd.order == Order::Train {
|
|
||||||
return unit.is_training();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
false
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn try_start_next_build(game: &Game, player: &Player, state: &mut GameState) {
|
fn try_start_next_build(game: &Game, player: &Player, state: &mut GameState) {
|
||||||
|
if !should_start_next_build(game, player, state) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let Some(unit_type) = get_next_thing_to_build(game, player, state) else {
|
let Some(unit_type) = get_next_thing_to_build(game, player, state) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some(builder) = find_builder_for_unit(player, unit_type, state) else {
|
let Some(builder) = find_builder_for_unit(player, unit_type, state) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let builder_id = builder.get_id();
|
let builder_id = builder.get_id();
|
||||||
|
if let Some((success, tile_pos)) =
|
||||||
if assign_builder_to_construct(game, &builder, unit_type, state) {
|
assign_builder_to_construct(game, player, &builder, unit_type, state)
|
||||||
|
{
|
||||||
|
if success {
|
||||||
let entry = BuildHistoryEntry {
|
let entry = BuildHistoryEntry {
|
||||||
unit_type: Some(unit_type),
|
unit_type: Some(unit_type),
|
||||||
upgrade_type: None,
|
upgrade_type: None,
|
||||||
assigned_unit_id: Some(builder_id),
|
assigned_unit_id: Some(builder_id),
|
||||||
|
tile_position: tile_pos,
|
||||||
};
|
};
|
||||||
|
|
||||||
state.unit_build_history.push(entry);
|
state.unit_build_history.push(entry);
|
||||||
|
|
||||||
let current_stage = &state.build_stages[state.current_stage_index];
|
|
||||||
println!(
|
|
||||||
"Started building {} with unit {} (Stage: {})",
|
|
||||||
unit_type.name(),
|
|
||||||
builder_id,
|
|
||||||
current_stage.name
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_start_next_build(_game: &Game, player: &Player, state: &mut GameState) -> bool {
|
||||||
|
// Do not start a new build if there is a pending build (assigned but not yet constructing/training).
|
||||||
|
if pending_build_exists(state, player) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Ensure there is a builder available for the next thing to build.
|
||||||
|
if let Some(unit_type) = get_next_thing_to_build(_game, player, state) {
|
||||||
|
find_builder_for_unit(player, unit_type, state).is_some()
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_ongoing_constructions(state: &GameState, player: &Player) -> bool {
|
||||||
|
// Consider a construction ongoing if there is a build history entry with an assigned unit that
|
||||||
|
// is currently constructing or training. This covers the period after a build command is issued
|
||||||
|
// but before the unit starts the actual constructing state, preventing multiple workers from
|
||||||
|
// being assigned to the same build command.
|
||||||
|
state.unit_build_history.iter().any(|entry| {
|
||||||
|
if let Some(unit_id) = entry.assigned_unit_id {
|
||||||
|
if let Some(unit) = player.get_units().iter().find(|u| u.get_id() == unit_id) {
|
||||||
|
return unit.is_constructing() || unit.is_training();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns true if there is a build history entry with an assigned builder that has not yet started constructing or training.
|
||||||
|
fn pending_build_exists(state: &GameState, player: &Player) -> bool {
|
||||||
|
state.unit_build_history.iter().any(|entry| {
|
||||||
|
if let Some(unit_id) = entry.assigned_unit_id {
|
||||||
|
if let Some(unit) = player.get_units().iter().find(|u| u.get_id() == unit_id) {
|
||||||
|
return !(unit.is_constructing() || unit.is_training());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_status_for_stage_items(
|
fn get_status_for_stage_items(
|
||||||
@@ -94,15 +103,12 @@ fn get_status_for_stage_items(
|
|||||||
state: &GameState,
|
state: &GameState,
|
||||||
) -> std::collections::HashMap<String, String> {
|
) -> std::collections::HashMap<String, String> {
|
||||||
let mut status_map = std::collections::HashMap::new();
|
let mut status_map = std::collections::HashMap::new();
|
||||||
|
|
||||||
let Some(current_stage) = state.build_stages.get(state.current_stage_index) else {
|
let Some(current_stage) = state.build_stages.get(state.current_stage_index) else {
|
||||||
return status_map;
|
return status_map;
|
||||||
};
|
};
|
||||||
|
|
||||||
for (unit_type, &desired_count) in ¤t_stage.desired_counts {
|
for (unit_type, &desired_count) in ¤t_stage.desired_counts {
|
||||||
let unit_name = unit_type.name().to_string();
|
let unit_name = unit_type.name().to_string();
|
||||||
let current_count = count_units_of_type(player, state, *unit_type);
|
let current_count = count_units_of_type(player, state, *unit_type);
|
||||||
|
|
||||||
if current_count >= desired_count {
|
if current_count >= desired_count {
|
||||||
status_map.insert(
|
status_map.insert(
|
||||||
unit_name,
|
unit_name,
|
||||||
@@ -110,7 +116,6 @@ fn get_status_for_stage_items(
|
|||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if !can_afford_unit(player, *unit_type) {
|
if !can_afford_unit(player, *unit_type) {
|
||||||
let minerals_short = unit_type.mineral_price() - player.minerals();
|
let minerals_short = unit_type.mineral_price() - player.minerals();
|
||||||
let gas_short = unit_type.gas_price() - player.gas();
|
let gas_short = unit_type.gas_price() - player.gas();
|
||||||
@@ -126,7 +131,6 @@ fn get_status_for_stage_items(
|
|||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if unit_type.is_building() {
|
if unit_type.is_building() {
|
||||||
if find_builder_for_unit(player, *unit_type, state).is_none() {
|
if find_builder_for_unit(player, *unit_type, state).is_none() {
|
||||||
status_map.insert(
|
status_map.insert(
|
||||||
@@ -136,79 +140,59 @@ fn get_status_for_stage_items(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
status_map.insert(
|
status_map.insert(
|
||||||
unit_name,
|
unit_name,
|
||||||
format!("Ready to build ({}/{})", current_count, desired_count),
|
format!("Ready to build ({}/{})", current_count, desired_count),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
status_map
|
status_map
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_next_thing_to_build(game: &Game, player: &Player, state: &GameState) -> Option<UnitType> {
|
fn get_next_thing_to_build(game: &Game, player: &Player, state: &GameState) -> Option<UnitType> {
|
||||||
let current_stage = state.build_stages.get(state.current_stage_index)?;
|
let current_stage = state.build_stages.get(state.current_stage_index)?;
|
||||||
|
|
||||||
if let Some(pylon) = check_need_more_supply(game, player, state) {
|
if let Some(pylon) = check_need_more_supply(game, player, state) {
|
||||||
return Some(pylon);
|
return Some(pylon);
|
||||||
}
|
}
|
||||||
|
|
||||||
let status_map = get_status_for_stage_items(game, player, state);
|
let status_map = get_status_for_stage_items(game, player, state);
|
||||||
let mut candidates = Vec::new();
|
let mut candidates = Vec::new();
|
||||||
|
|
||||||
for (unit_type, &desired_count) in ¤t_stage.desired_counts {
|
for (unit_type, &desired_count) in ¤t_stage.desired_counts {
|
||||||
let current_count = count_units_of_type(player, state, *unit_type);
|
let current_count = count_units_of_type(player, state, *unit_type);
|
||||||
|
|
||||||
if current_count >= desired_count {
|
if current_count >= desired_count {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let status = status_map.get(&unit_type.name().to_string());
|
let status = status_map.get(&unit_type.name().to_string());
|
||||||
if status.is_some() && status.unwrap().starts_with("Ready to build") {
|
if status.is_some() && status.unwrap().starts_with("Ready to build") {
|
||||||
candidates.push(*unit_type);
|
candidates.push(*unit_type);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
candidates
|
candidates
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.max_by_key(|unit_type| unit_type.mineral_price() + unit_type.gas_price())
|
.max_by_key(|unit_type| unit_type.mineral_price() + unit_type.gas_price())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_need_more_supply(game: &Game, player: &Player, state: &GameState) -> Option<UnitType> {
|
fn check_need_more_supply(_game: &Game, player: &Player, _state: &GameState) -> Option<UnitType> {
|
||||||
let supply_used = player.supply_used();
|
let supply_used = player.supply_used();
|
||||||
let supply_total = player.supply_total();
|
let supply_total = player.supply_total();
|
||||||
|
|
||||||
if supply_total == 0 {
|
if supply_total == 0 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let supply_remaining = supply_total - supply_used;
|
let supply_remaining = supply_total - supply_used;
|
||||||
let threshold = ((supply_total as f32) * 0.15).ceil() as i32;
|
let threshold = ((supply_total as f32) * 0.15).ceil() as i32;
|
||||||
|
|
||||||
if supply_remaining <= threshold && supply_total < 400 {
|
if supply_remaining <= threshold && supply_total < 400 {
|
||||||
let pylon_type = UnitType::Protoss_Pylon;
|
let pylon_type = UnitType::Protoss_Pylon;
|
||||||
|
|
||||||
if can_afford_unit(player, pylon_type) {
|
if can_afford_unit(player, pylon_type) {
|
||||||
if let Some(builder) = find_builder_for_unit(player, pylon_type, state) {
|
|
||||||
let build_location =
|
|
||||||
build_location_utils::find_build_location(game, &builder, pylon_type, 25);
|
|
||||||
if build_location.is_some() {
|
|
||||||
return Some(pylon_type);
|
return Some(pylon_type);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_builder_for_unit(
|
fn find_builder_for_unit(
|
||||||
player: &Player,
|
player: &Player,
|
||||||
unit_type: UnitType,
|
unit_type: UnitType,
|
||||||
state: &GameState,
|
_state: &GameState,
|
||||||
) -> Option<rsbwapi::Unit> {
|
) -> Option<rsbwapi::Unit> {
|
||||||
let builder_type = unit_type.what_builds().0;
|
let builder_type = unit_type.what_builds().0;
|
||||||
|
|
||||||
player
|
player
|
||||||
.get_units()
|
.get_units()
|
||||||
.iter()
|
.iter()
|
||||||
@@ -217,45 +201,37 @@ fn find_builder_for_unit(
|
|||||||
&& !u.is_constructing()
|
&& !u.is_constructing()
|
||||||
&& !u.is_training()
|
&& !u.is_training()
|
||||||
&& (u.is_idle() || u.is_gathering_minerals() || u.is_gathering_gas())
|
&& (u.is_idle() || u.is_gathering_minerals() || u.is_gathering_gas())
|
||||||
&& !state.intended_commands.contains_key(&u.get_id())
|
|
||||||
})
|
})
|
||||||
.cloned()
|
.cloned()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn assign_builder_to_construct(
|
fn assign_builder_to_construct(
|
||||||
game: &Game,
|
game: &Game,
|
||||||
|
player: &Player,
|
||||||
builder: &rsbwapi::Unit,
|
builder: &rsbwapi::Unit,
|
||||||
unit_type: UnitType,
|
unit_type: UnitType,
|
||||||
state: &mut GameState,
|
state: &mut GameState,
|
||||||
) -> bool {
|
) -> Option<(bool, Option<rsbwapi::TilePosition>)> {
|
||||||
let builder_id = builder.get_id();
|
let builder_id = builder.get_id();
|
||||||
|
|
||||||
if unit_type.is_building() {
|
if unit_type.is_building() {
|
||||||
let build_location = build_location_utils::find_build_location(game, builder, unit_type, 25);
|
let build_location =
|
||||||
|
build_location_utils::find_build_location(game, player, builder, unit_type, 42);
|
||||||
if let Some(pos) = build_location {
|
if let Some(pos) = build_location {
|
||||||
println!(
|
println!(
|
||||||
"Attempting to build {} at {:?} with worker {} (currently at {:?})",
|
"Attempting to build {} at {:?} with worker {}",
|
||||||
unit_type.name(),
|
unit_type.name(),
|
||||||
pos,
|
pos,
|
||||||
builder_id,
|
builder_id,
|
||||||
builder.get_position()
|
|
||||||
);
|
);
|
||||||
|
|
||||||
match builder.build(unit_type, pos) {
|
match builder.build(unit_type, pos) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
println!("Build command succeeded for {}", unit_type.name());
|
println!("Build command succeeded for {}", unit_type.name());
|
||||||
let intended_cmd = IntendedCommand {
|
// No intended command for building actions; the builder (worker) will be tracked via ongoing constructions.
|
||||||
order: Order::PlaceBuilding,
|
Some((true, Some(pos)))
|
||||||
target_position: Some(pos.to_position()),
|
|
||||||
target_unit: None,
|
|
||||||
};
|
|
||||||
state.intended_commands.insert(builder_id, intended_cmd);
|
|
||||||
true
|
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
println!("Build command FAILED for {}: {:?}", unit_type.name(), e);
|
println!("Build command FAILED for {}: {:?}", unit_type.name(), e);
|
||||||
false
|
Some((false, None))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -264,41 +240,33 @@ fn assign_builder_to_construct(
|
|||||||
unit_type.name(),
|
unit_type.name(),
|
||||||
builder.get_id()
|
builder.get_id()
|
||||||
);
|
);
|
||||||
false
|
Some((false, None))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
match builder.train(unit_type) {
|
match builder.train(unit_type) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
let intended_cmd = IntendedCommand {
|
// No intended command for training; the building will be tracked via ongoing constructions.
|
||||||
order: Order::Train,
|
Some((true, None))
|
||||||
target_position: None,
|
|
||||||
target_unit: None,
|
|
||||||
};
|
|
||||||
state.intended_commands.insert(builder_id, intended_cmd);
|
|
||||||
true
|
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
println!("Train command FAILED for {}: {:?}", unit_type.name(), e);
|
println!("Train command FAILED for {}: {:?}", unit_type.name(), e);
|
||||||
false
|
Some((false, None))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn count_units_of_type(player: &Player, _state: &GameState, unit_type: UnitType) -> i32 {
|
fn count_units_of_type(player: &Player, _state: &GameState, unit_type: UnitType) -> i32 {
|
||||||
let existing = player
|
player
|
||||||
.get_units()
|
.get_units()
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|u| u.get_type() == unit_type)
|
.filter(|u| u.get_type() == unit_type)
|
||||||
.count() as i32;
|
.count() as i32
|
||||||
|
|
||||||
existing
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn can_afford_unit(player: &Player, unit_type: UnitType) -> bool {
|
fn can_afford_unit(player: &Player, unit_type: UnitType) -> bool {
|
||||||
let minerals = player.minerals();
|
let minerals = player.minerals();
|
||||||
let gas = player.gas();
|
let gas = player.gas();
|
||||||
|
|
||||||
minerals >= unit_type.mineral_price() && gas >= unit_type.gas_price()
|
minerals >= unit_type.mineral_price() && gas >= unit_type.gas_price()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -306,7 +274,6 @@ fn check_and_advance_stage(player: &Player, state: &mut GameState) {
|
|||||||
let Some(current_stage) = state.build_stages.get(state.current_stage_index) else {
|
let Some(current_stage) = state.build_stages.get(state.current_stage_index) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let stage_complete = current_stage
|
let stage_complete = current_stage
|
||||||
.desired_counts
|
.desired_counts
|
||||||
.iter()
|
.iter()
|
||||||
@@ -314,10 +281,8 @@ fn check_and_advance_stage(player: &Player, state: &mut GameState) {
|
|||||||
let current_count = count_units_of_type(player, state, *unit_type);
|
let current_count = count_units_of_type(player, state, *unit_type);
|
||||||
current_count >= desired_count
|
current_count >= desired_count
|
||||||
});
|
});
|
||||||
|
|
||||||
if stage_complete {
|
if stage_complete {
|
||||||
let next_stage_index = state.current_stage_index + 1;
|
let next_stage_index = state.current_stage_index + 1;
|
||||||
|
|
||||||
if next_stage_index < state.build_stages.len() {
|
if next_stage_index < state.build_stages.len() {
|
||||||
println!(
|
println!(
|
||||||
"Stage '{}' complete! Advancing to stage {}",
|
"Stage '{}' complete! Advancing to stage {}",
|
||||||
@@ -331,7 +296,6 @@ fn check_and_advance_stage(player: &Player, state: &mut GameState) {
|
|||||||
pub fn print_debug_build_status(game: &Game, player: &Player, state: &GameState) {
|
pub fn print_debug_build_status(game: &Game, player: &Player, state: &GameState) {
|
||||||
let mut y = 10;
|
let mut y = 10;
|
||||||
let x = 3;
|
let x = 3;
|
||||||
|
|
||||||
if let Some(current_stage) = state.build_stages.get(state.current_stage_index) {
|
if let Some(current_stage) = state.build_stages.get(state.current_stage_index) {
|
||||||
let next_build = get_next_thing_to_build(game, player, state);
|
let next_build = get_next_thing_to_build(game, player, state);
|
||||||
let next_build_str = if let Some(unit_type) = next_build {
|
let next_build_str = if let Some(unit_type) = next_build {
|
||||||
@@ -348,7 +312,6 @@ pub fn print_debug_build_status(game: &Game, player: &Player, state: &GameState)
|
|||||||
};
|
};
|
||||||
game.draw_text_screen((x, y), &next_build_str);
|
game.draw_text_screen((x, y), &next_build_str);
|
||||||
y += 10;
|
y += 10;
|
||||||
|
|
||||||
if let Some(last_entry) = state.unit_build_history.last() {
|
if let Some(last_entry) = state.unit_build_history.last() {
|
||||||
let unit_name = if let Some(unit_type) = last_entry.unit_type {
|
let unit_name = if let Some(unit_type) = last_entry.unit_type {
|
||||||
unit_type.name()
|
unit_type.name()
|
||||||
@@ -362,10 +325,8 @@ pub fn print_debug_build_status(game: &Game, player: &Player, state: &GameState)
|
|||||||
game.draw_text_screen((x, y), "Last Built: None");
|
game.draw_text_screen((x, y), "Last Built: None");
|
||||||
}
|
}
|
||||||
y += 10;
|
y += 10;
|
||||||
|
|
||||||
game.draw_text_screen((x, y), "Stage Progress:");
|
game.draw_text_screen((x, y), "Stage Progress:");
|
||||||
y += 10;
|
y += 10;
|
||||||
|
|
||||||
for (unit_type, &desired_count) in ¤t_stage.desired_counts {
|
for (unit_type, &desired_count) in ¤t_stage.desired_counts {
|
||||||
let current_count = count_units_of_type(player, state, *unit_type);
|
let current_count = count_units_of_type(player, state, *unit_type);
|
||||||
game.draw_text_screen(
|
game.draw_text_screen(
|
||||||
|
|||||||
@@ -1,17 +1,28 @@
|
|||||||
use rsbwapi::{Game, Order, Player, Unit};
|
use rsbwapi::{Game, Player, Unit};
|
||||||
|
|
||||||
use crate::state::game_state::{GameState, IntendedCommand};
|
use crate::state::game_state::GameState;
|
||||||
|
|
||||||
pub fn assign_idle_workers_to_minerals(game: &Game, player: &Player, state: &mut GameState) {
|
pub fn assign_idle_workers_to_minerals(game: &Game, player: &Player, state: &mut GameState) {
|
||||||
let all_units = player.get_units();
|
let all_units = player.get_units();
|
||||||
let workers: Vec<Unit> = all_units
|
let workers: Vec<Unit> = all_units
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|u| u.get_type().is_worker() && u.is_completed())
|
.filter(|u| {
|
||||||
|
// Worker must be a completed worker unit
|
||||||
|
u.get_type().is_worker() && u.is_completed()
|
||||||
|
})
|
||||||
.cloned()
|
.cloned()
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// Assign idle workers to mining
|
// Assign idle workers that are not already assigned in the build history
|
||||||
for worker in workers {
|
for worker in workers {
|
||||||
|
// Skip if this worker is already recorded as assigned in any ongoing build history entry
|
||||||
|
let already_assigned = state.unit_build_history.iter().any(|entry| {
|
||||||
|
entry.assigned_unit_id == Some(worker.get_id())
|
||||||
|
&& entry.unit_type.map(|ut| ut.is_building()).unwrap_or(false)
|
||||||
|
});
|
||||||
|
if already_assigned {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
assign_worker_to_mineral(game, &worker, state);
|
assign_worker_to_mineral(game, &worker, state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -19,76 +30,42 @@ pub fn assign_idle_workers_to_minerals(game: &Game, player: &Player, state: &mut
|
|||||||
fn assign_worker_to_mineral(game: &Game, worker: &Unit, state: &mut GameState) {
|
fn assign_worker_to_mineral(game: &Game, worker: &Unit, state: &mut GameState) {
|
||||||
let worker_id = worker.get_id();
|
let worker_id = worker.get_id();
|
||||||
|
|
||||||
if let Some(cmd) = state.intended_commands.get(&worker_id) {
|
// Skip if the worker is currently assigned to an in‑progress building construction.
|
||||||
if cmd.order != Order::MiningMinerals {
|
let has_in_progress_build = state.unit_build_history.iter().any(|entry| {
|
||||||
return;
|
entry.assigned_unit_id == Some(worker_id)
|
||||||
}
|
&& entry.unit_type.map(|ut| ut.is_building()).unwrap_or(false)
|
||||||
}
|
});
|
||||||
|
if has_in_progress_build {
|
||||||
if !worker.is_idle() {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if worker.is_gathering_minerals() || worker.is_gathering_gas() {
|
// Worker must be idle and not already gathering resources.
|
||||||
|
if !worker.is_idle() || worker.is_gathering_minerals() || worker.is_gathering_gas() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Find a mineral patch to assign.
|
||||||
let Some(mineral) = find_available_mineral(game, worker, state) else {
|
let Some(mineral) = find_available_mineral(game, worker, state) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let intended_cmd = IntendedCommand {
|
// Assign worker to gather minerals (ignore intended command tracking).
|
||||||
order: Order::MiningMinerals,
|
|
||||||
target_position: None,
|
|
||||||
target_unit: Some(mineral.clone()),
|
|
||||||
};
|
|
||||||
|
|
||||||
state.intended_commands.insert(worker_id, intended_cmd);
|
|
||||||
|
|
||||||
println!(
|
|
||||||
"Worker {} current order: {:?}, assigning to mine from mineral at {:?}",
|
|
||||||
worker_id,
|
|
||||||
worker.get_order(),
|
|
||||||
mineral.get_position()
|
|
||||||
);
|
|
||||||
|
|
||||||
if worker.gather(&mineral).is_ok() {
|
if worker.gather(&mineral).is_ok() {
|
||||||
println!(
|
println!("Assigned worker {} to mine from mineral", worker_id,);
|
||||||
"Assigned worker {} to mine from mineral at {:?}",
|
|
||||||
worker_id,
|
|
||||||
mineral.get_position()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_available_mineral(game: &Game, worker: &Unit, state: &GameState) -> Option<Unit> {
|
fn find_available_mineral(game: &Game, worker: &Unit, _state: &GameState) -> Option<Unit> {
|
||||||
let worker_pos = worker.get_position();
|
let worker_pos = worker.get_position();
|
||||||
let minerals = game.get_static_minerals();
|
let minerals = game.get_static_minerals();
|
||||||
let mut mineral_list: Vec<Unit> = minerals.iter().filter(|m| m.exists()).cloned().collect();
|
let mut mineral_list: Vec<Unit> = minerals.iter().filter(|m| m.exists()).cloned().collect();
|
||||||
|
|
||||||
|
// Sort minerals by distance to the worker.
|
||||||
mineral_list.sort_by_key(|m| {
|
mineral_list.sort_by_key(|m| {
|
||||||
let pos = m.get_position();
|
let pos = m.get_position();
|
||||||
((pos.x - worker_pos.x).pow(2) + (pos.y - worker_pos.y).pow(2)) as i32
|
((pos.x - worker_pos.x).pow(2) + (pos.y - worker_pos.y).pow(2)) as i32
|
||||||
});
|
});
|
||||||
|
|
||||||
for mineral in mineral_list.iter() {
|
// Return the closest mineral, ignoring any intended command tracking.
|
||||||
let mineral_id: usize = mineral.get_id();
|
|
||||||
|
|
||||||
let worker_count = state
|
|
||||||
.intended_commands
|
|
||||||
.values()
|
|
||||||
.filter(|cmd| {
|
|
||||||
if let Some(target) = &cmd.target_unit {
|
|
||||||
target.get_id() == mineral_id
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.count();
|
|
||||||
|
|
||||||
if worker_count < 2 {
|
|
||||||
return Some(mineral.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mineral_list.first().cloned()
|
mineral_list.first().cloned()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,13 +31,6 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
|
||||||
color: #d4af37;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
font-size: 28px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.control-group {
|
.control-group {
|
||||||
margin-bottom: 30px;
|
margin-bottom: 30px;
|
||||||
}
|
}
|
||||||
@@ -60,13 +53,17 @@
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #d4af37;
|
color: #d4af37;
|
||||||
display: block;
|
display: block;
|
||||||
|
transition: color 0.3s;
|
||||||
|
}
|
||||||
|
.speed-fast {
|
||||||
|
color: #4ade80; /* green */
|
||||||
|
}
|
||||||
|
.speed-slow {
|
||||||
|
color: #f87171; /* red */
|
||||||
}
|
}
|
||||||
|
|
||||||
.speed-label {
|
.speed-label {
|
||||||
color: #999;
|
display: none; /* hide label for compact view */
|
||||||
font-size: 12px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 1px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.preset-buttons {
|
.preset-buttons {
|
||||||
@@ -87,6 +84,10 @@
|
|||||||
background: #2d2d2d;
|
background: #2d2d2d;
|
||||||
color: #d4af37;
|
color: #d4af37;
|
||||||
}
|
}
|
||||||
|
button.active {
|
||||||
|
background: #d4af37;
|
||||||
|
color: #1e1e1e;
|
||||||
|
}
|
||||||
|
|
||||||
button small {
|
button small {
|
||||||
display: block;
|
display: block;
|
||||||
@@ -201,27 +202,28 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>⚡ Game Speed Control</h1>
|
|
||||||
|
|
||||||
<div class="speed-display">
|
|
||||||
<span class="speed-value" id="currentSpeed">42</span>
|
|
||||||
<span class="speed-label">Current Speed</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<label>Select Speed</label>
|
|
||||||
<div class="preset-buttons">
|
<div class="preset-buttons">
|
||||||
<button onclick="setSpeed(42)">42<br /><small>Slowest</small></button>
|
<button data-speed="42" onclick="setSpeed(42)">
|
||||||
<button onclick="setSpeed(1)">1<br /><small>Fast</small></button>
|
42<br /><small>Slowest</small>
|
||||||
<button onclick="setSpeed(0)">0<br /><small>Fastest</small></button>
|
</button>
|
||||||
<button onclick="setSpeed(-1)">-1<br /><small>Max</small></button>
|
<button data-speed="1" onclick="setSpeed(1)">
|
||||||
|
1<br /><small>Fast</small>
|
||||||
|
</button>
|
||||||
|
<button data-speed="0" onclick="setSpeed(0)">
|
||||||
|
0<br /><small>Fastest</small>
|
||||||
|
</button>
|
||||||
|
<button data-speed="-1" onclick="setSpeed(-1)">
|
||||||
|
-1<br /><small>Max</small>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="status" class="status"></div>
|
<div id="status" class="status"></div>
|
||||||
|
|
||||||
<div class="build-status-section">
|
<div class="build-status-section">
|
||||||
<h2>📋 Build Status</h2>
|
|
||||||
<div class="stage-info">
|
<div class="stage-info">
|
||||||
<div class="stage-name" id="stageName">Loading...</div>
|
<div class="stage-name" id="stageName">Loading...</div>
|
||||||
<ul class="build-items" id="buildItems">
|
<ul class="build-items" id="buildItems">
|
||||||
@@ -232,47 +234,53 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script id="game-speed-script">
|
<script id="game-speed-script">
|
||||||
const currentSpeedDisplay = document.getElementById("currentSpeed");
|
const presetButtons = document.querySelectorAll('.preset-buttons button');
|
||||||
const statusDiv = document.getElementById("status");
|
const statusDiv = document.getElementById('status');
|
||||||
|
|
||||||
|
function highlightButton(speed) {
|
||||||
|
presetButtons.forEach(btn => {
|
||||||
|
if (Number(btn.dataset.speed) === speed) {
|
||||||
|
btn.classList.add('active');
|
||||||
|
} else {
|
||||||
|
btn.classList.remove('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchCurrentSpeed() {
|
async function fetchCurrentSpeed() {
|
||||||
try {
|
const response = await fetch('http://127.0.0.1:3333/api/speed');
|
||||||
const response = await fetch("http://127.0.0.1:3333/api/speed");
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
updateSpeedDisplay(data.speed);
|
highlightButton(data.speed);
|
||||||
|
// Clear any previous error message
|
||||||
|
statusDiv.textContent = '';
|
||||||
|
statusDiv.className = 'status';
|
||||||
|
} else {
|
||||||
|
showStatus('Unable to connect to bot', 'error');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
showStatus("Unable to connect to bot", "error");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateSpeedDisplay(speed) {
|
|
||||||
currentSpeedDisplay.textContent = speed;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setSpeed(speed) {
|
async function setSpeed(speed) {
|
||||||
try {
|
const response = await fetch('http://127.0.0.1:3333/api/speed', {
|
||||||
const response = await fetch("http://127.0.0.1:3333/api/speed", {
|
method: 'POST',
|
||||||
method: "POST",
|
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ speed: speed }),
|
body: JSON.stringify({ speed: speed }),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
updateSpeedDisplay(data.speed);
|
highlightButton(data.speed);
|
||||||
showStatus(`Speed set to ${data.speed}`, "success");
|
// Clear any previous status message on success
|
||||||
|
statusDiv.textContent = '';
|
||||||
|
statusDiv.className = 'status';
|
||||||
} else {
|
} else {
|
||||||
showStatus("Failed to update speed", "error");
|
showStatus('Failed to update speed', 'error');
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
showStatus("Connection error", "error");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function showStatus(message, type) {
|
function showStatus(message, type) {
|
||||||
statusDiv.textContent = message;
|
statusDiv.textContent = message;
|
||||||
statusDiv.className = `status ${type} visible`;
|
statusDiv.className = `status ${type} visible`;
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ map: maps/BroodWar/(4)CircuitBreaker.scx
|
|||||||
# map: maps/(2)Boxer.scm
|
# map: maps/(2)Boxer.scm
|
||||||
|
|
||||||
# player_race: Zerg
|
# player_race: Zerg
|
||||||
# player_race: Terran
|
player_race: Terran
|
||||||
player_race: Protoss
|
# player_race: Protoss
|
||||||
# player_race: Random
|
# player_race: Random
|
||||||
|
|
||||||
# enemy_count: 1
|
# enemy_count: 1
|
||||||
|
|||||||
Reference in New Issue
Block a user