persisting changes
This commit is contained in:
@@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
build_manager::on_building_create(&unit, &mut locked_state);
|
if unit.get_type().is_building() {
|
||||||
|
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) {}
|
||||||
|
|||||||
@@ -31,13 +31,19 @@ pub fn get_build_stages() -> Vec<BuildStage> {
|
|||||||
.with_unit(UnitType::Terran_Supply_Depot, 2)
|
.with_unit(UnitType::Terran_Supply_Depot, 2)
|
||||||
.with_unit(UnitType::Terran_Barracks, 1)
|
.with_unit(UnitType::Terran_Barracks, 1)
|
||||||
.with_unit(UnitType::Terran_Refinery, 1),
|
.with_unit(UnitType::Terran_Refinery, 1),
|
||||||
// Stage 2: Defense bunker
|
|
||||||
BuildStage::new("Defense Bunker")
|
BuildStage::new("Defense Bunker")
|
||||||
.with_unit(UnitType::Terran_SCV, 16)
|
.with_unit(UnitType::Terran_SCV, 16)
|
||||||
.with_unit(UnitType::Terran_Supply_Depot, 3)
|
.with_unit(UnitType::Terran_Supply_Depot, 3)
|
||||||
.with_unit(UnitType::Terran_Command_Center, 1)
|
.with_unit(UnitType::Terran_Command_Center, 1)
|
||||||
.with_unit(UnitType::Terran_Barracks, 1)
|
.with_unit(UnitType::Terran_Barracks, 1)
|
||||||
.with_unit(UnitType::Terran_Refinery, 1)
|
.with_unit(UnitType::Terran_Refinery, 1),
|
||||||
.with_unit(UnitType::Terran_Bunker, 2),
|
|
||||||
|
BuildStage::new("Mid Game")
|
||||||
|
.with_unit(UnitType::Terran_SCV, 20)
|
||||||
|
.with_unit(UnitType::Terran_Supply_Depot, 4)
|
||||||
|
.with_unit(UnitType::Terran_Command_Center, 2)
|
||||||
|
.with_unit(UnitType::Terran_Barracks, 2)
|
||||||
|
.with_unit(UnitType::Terran_Refinery, 2)
|
||||||
|
.with_unit(UnitType::Terran_Missile_Turret, 2),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,39 +13,53 @@ pub fn on_frame(game: &Game, player: &Player, state: &mut GameState) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_building_create(unit: &Unit, state: &mut GameState) {
|
pub fn on_building_create(unit: &Unit, state: &mut GameState) {
|
||||||
|
// When a building is created, clear any intended command for the worker that was assigned to build it.
|
||||||
if let Some(entry) = state
|
if let Some(entry) = state
|
||||||
.unit_build_history
|
.unit_build_history
|
||||||
.iter()
|
.iter()
|
||||||
.rev()
|
.rev()
|
||||||
.find(|e| e.unit_type == Some(unit.get_type()))
|
.find(|e| e.unit_type == Some(unit.get_type()))
|
||||||
{
|
{
|
||||||
if let Some(probe_id) = entry.assigned_unit_id {
|
if let Some(builder_id) = entry.assigned_unit_id {
|
||||||
state.intended_commands.remove(&probe_id);
|
state.intended_commands.remove(&builder_id);
|
||||||
println!(
|
println!(
|
||||||
"Building {} started. Removed assignment for probe {}",
|
"Building {} started. Removed assignment for worker {}",
|
||||||
unit.get_type().name(),
|
unit.get_type().name(),
|
||||||
probe_id
|
builder_id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called when a non‑building unit (e.g., a trained unit) is created.
|
||||||
|
/// This clears any pending assignment for the building that trained it.
|
||||||
|
pub fn on_unit_create(unit: &Unit, state: &mut GameState) {
|
||||||
|
// Find the most recent history entry for this unit type.
|
||||||
|
if let Some(entry) = state
|
||||||
|
.unit_build_history
|
||||||
|
.iter()
|
||||||
|
.rev()
|
||||||
|
.find(|e| e.unit_type == Some(unit.get_type()))
|
||||||
|
{
|
||||||
|
if let Some(builder_id) = entry.assigned_unit_id {
|
||||||
|
// For training, we didn't store an intended command, but clear just in case.
|
||||||
|
state.intended_commands.remove(&builder_id);
|
||||||
|
println!(
|
||||||
|
"Unit {} created. Cleared assignment for builder {}",
|
||||||
|
unit.get_type().name(),
|
||||||
|
builder_id
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cleanup_stale_commands(player: &Player, state: &mut GameState) {
|
fn cleanup_stale_commands(player: &Player, state: &mut GameState) {
|
||||||
|
// Retain intended commands as long as the unit still exists.
|
||||||
|
// The command will be cleared when the building/unit is created (on_building_create) or when the unit dies.
|
||||||
let unit_ids: Vec<usize> = player.get_units().iter().map(|u| u.get_id()).collect();
|
let unit_ids: Vec<usize> = player.get_units().iter().map(|u| u.get_id()).collect();
|
||||||
state.intended_commands.retain(|unit_id, cmd| {
|
state
|
||||||
if !unit_ids.contains(unit_id) {
|
.intended_commands
|
||||||
return false;
|
.retain(|unit_id, _cmd| unit_ids.contains(unit_id));
|
||||||
}
|
|
||||||
if let Some(unit) = player.get_units().iter().find(|u| u.get_id() == *unit_id) {
|
|
||||||
if cmd.order == Order::PlaceBuilding {
|
|
||||||
return unit.is_constructing() || unit.get_order() == Order::PlaceBuilding;
|
|
||||||
}
|
|
||||||
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) {
|
||||||
@@ -71,29 +85,16 @@ fn try_start_next_build(game: &Game, player: &Player, state: &mut GameState) {
|
|||||||
};
|
};
|
||||||
state.unit_build_history.push(entry);
|
state.unit_build_history.push(entry);
|
||||||
let current_stage = &state.build_stages[state.current_stage_index];
|
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 {
|
fn should_start_next_build(_game: &Game, player: &Player, state: &mut GameState) -> bool {
|
||||||
!has_pending_assignment(state)
|
// Only start a new build if there are no ongoing constructions or training actions.
|
||||||
|
!has_ongoing_constructions(state, player)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn has_pending_assignment(state: &GameState) -> bool {
|
// Removed: pending assignment tracking now relies on actual unit state via has_ongoing_constructions.
|
||||||
state.unit_build_history.iter().any(|entry| {
|
|
||||||
if let Some(unit_id) = entry.assigned_unit_id {
|
|
||||||
state.intended_commands.contains_key(&unit_id)
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn has_ongoing_constructions(state: &GameState, player: &Player) -> bool {
|
fn has_ongoing_constructions(state: &GameState, player: &Player) -> bool {
|
||||||
state.unit_build_history.iter().any(|entry| {
|
state.unit_build_history.iter().any(|entry| {
|
||||||
@@ -237,12 +238,7 @@ fn assign_builder_to_construct(
|
|||||||
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,
|
|
||||||
target_position: Some(pos.to_position()),
|
|
||||||
target_unit: None,
|
|
||||||
};
|
|
||||||
state.intended_commands.insert(builder_id, intended_cmd);
|
|
||||||
Some((true, Some(pos)))
|
Some((true, Some(pos)))
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -261,12 +257,7 @@ fn assign_builder_to_construct(
|
|||||||
} 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,
|
|
||||||
target_position: None,
|
|
||||||
target_unit: None,
|
|
||||||
};
|
|
||||||
state.intended_commands.insert(builder_id, intended_cmd);
|
|
||||||
Some((true, None))
|
Some((true, None))
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -357,91 +348,4 @@ pub fn print_debug_build_status(game: &Game, player: &Player, state: &GameState)
|
|||||||
y += 10;
|
y += 10;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let has_pending = state.unit_build_history.iter().any(|entry| {
|
|
||||||
if let Some(unit_id) = entry.assigned_unit_id {
|
|
||||||
state.intended_commands.contains_key(&unit_id)
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
});
|
|
||||||
for entry in &state.unit_build_history {
|
|
||||||
if let (Some(unit_type), Some(tile_pos), Some(unit_id)) =
|
|
||||||
(entry.unit_type, entry.tile_position, entry.assigned_unit_id)
|
|
||||||
{
|
|
||||||
if state.intended_commands.contains_key(&unit_id) {
|
|
||||||
let pixel_x = tile_pos.x * 32;
|
|
||||||
let pixel_y = tile_pos.y * 32;
|
|
||||||
let width = unit_type.tile_width() * 32;
|
|
||||||
let height = unit_type.tile_height() * 32;
|
|
||||||
use rsbwapi::{Color, Position};
|
|
||||||
let top_left = Position {
|
|
||||||
x: pixel_x,
|
|
||||||
y: pixel_y,
|
|
||||||
};
|
|
||||||
let top_right = Position {
|
|
||||||
x: pixel_x + width,
|
|
||||||
y: pixel_y,
|
|
||||||
};
|
|
||||||
let bottom_left = Position {
|
|
||||||
x: pixel_x,
|
|
||||||
y: pixel_y + height,
|
|
||||||
};
|
|
||||||
let bottom_right = Position {
|
|
||||||
x: pixel_x + width,
|
|
||||||
y: pixel_y + height,
|
|
||||||
};
|
|
||||||
let color = Color::Yellow;
|
|
||||||
game.draw_line_map(top_left, top_right, color);
|
|
||||||
game.draw_line_map(top_right, bottom_right, color);
|
|
||||||
game.draw_line_map(bottom_right, bottom_left, color);
|
|
||||||
game.draw_line_map(bottom_left, top_left, color);
|
|
||||||
let center = Position {
|
|
||||||
x: pixel_x + width / 2,
|
|
||||||
y: pixel_y + height / 2,
|
|
||||||
};
|
|
||||||
game.draw_text_map(center, &format!("Pending: {}", unit_type.name()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if has_pending {
|
|
||||||
use rsbwapi::{Color, Position, TilePosition};
|
|
||||||
if let Some(nexus) = player
|
|
||||||
.get_units()
|
|
||||||
.iter()
|
|
||||||
.find(|u| u.get_type() == UnitType::Protoss_Nexus)
|
|
||||||
{
|
|
||||||
let nexus_tile = nexus.get_tile_position();
|
|
||||||
let radius = 30;
|
|
||||||
for dx in -radius..=radius {
|
|
||||||
for dy in -radius..=radius {
|
|
||||||
let tile_x = nexus_tile.x + dx;
|
|
||||||
let tile_y = nexus_tile.y + dy;
|
|
||||||
if tile_x < 0 || tile_y < 0 {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let tile_pos = TilePosition {
|
|
||||||
x: tile_x,
|
|
||||||
y: tile_y,
|
|
||||||
};
|
|
||||||
let dist_sq = (dx * dx + dy * dy) as f32;
|
|
||||||
if dist_sq > (radius * radius) as f32 {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let is_buildable = game.is_buildable(tile_pos);
|
|
||||||
if !is_buildable {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let has_power = game.has_power(tile_pos, (1, 1));
|
|
||||||
let pixel_x = tile_x * 32;
|
|
||||||
let pixel_y = tile_y * 32;
|
|
||||||
let center = Position {
|
|
||||||
x: pixel_x + 16,
|
|
||||||
y: pixel_y + 16,
|
|
||||||
};
|
|
||||||
let color = if has_power { Color::Green } else { Color::Blue };
|
|
||||||
game.draw_circle_map(center, 3, color, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,13 +45,6 @@ fn assign_worker_to_mineral(game: &Game, worker: &Unit, state: &mut GameState) {
|
|||||||
|
|
||||||
state.intended_commands.insert(worker_id, intended_cmd);
|
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 at {:?}",
|
"Assigned worker {} to mine from mineral at {:?}",
|
||||||
|
|||||||
@@ -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,46 +234,52 @@
|
|||||||
</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');
|
||||||
|
|
||||||
async function fetchCurrentSpeed() {
|
function highlightButton(speed) {
|
||||||
try {
|
presetButtons.forEach(btn => {
|
||||||
const response = await fetch("http://127.0.0.1:3333/api/speed");
|
if (Number(btn.dataset.speed) === speed) {
|
||||||
if (response.ok) {
|
btn.classList.add('active');
|
||||||
const data = await response.json();
|
} else {
|
||||||
updateSpeedDisplay(data.speed);
|
btn.classList.remove('active');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
showStatus("Unable to connect to bot", "error");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateSpeedDisplay(speed) {
|
|
||||||
currentSpeedDisplay.textContent = speed;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function setSpeed(speed) {
|
|
||||||
try {
|
|
||||||
const response = await fetch("http://127.0.0.1:3333/api/speed", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ speed: speed }),
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchCurrentSpeed() {
|
||||||
|
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);
|
||||||
showStatus(`Speed set to ${data.speed}`, "success");
|
// Clear any previous error message
|
||||||
|
statusDiv.textContent = '';
|
||||||
|
statusDiv.className = 'status';
|
||||||
} else {
|
} else {
|
||||||
showStatus("Failed to update speed", "error");
|
showStatus('Unable to connect to bot', 'error');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
showStatus("Connection error", "error");
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
async function setSpeed(speed) {
|
||||||
|
const response = await fetch('http://127.0.0.1:3333/api/speed', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ speed: speed }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
highlightButton(data.speed);
|
||||||
|
// Clear any previous status message on success
|
||||||
|
statusDiv.textContent = '';
|
||||||
|
statusDiv.className = 'status';
|
||||||
|
} else {
|
||||||
|
showStatus('Failed to update speed', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function showStatus(message, type) {
|
function showStatus(message, type) {
|
||||||
statusDiv.textContent = message;
|
statusDiv.textContent = message;
|
||||||
|
|||||||
Reference in New Issue
Block a user