use std::{collections::VecDeque, time::Duration};
use ahash::AHashMap;
use bevy::prelude::*;
use de_core::{
gamestate::GameState,
objects::{Local, ObjectTypeComponent},
player::PlayerComponent,
state::AppState,
};
use de_index::SpatialQuery;
use de_objects::SolidObjects;
use de_pathing::{PathQueryProps, PathTarget};
use de_signs::{
LineLocation, UpdateLineEndEvent, UpdateLineLocationEvent, UpdatePoleLocationEvent,
};
use de_spawner::{ObjectCounter, SpawnLocalActiveEvent};
use de_types::{
objects::{ActiveObjectType, ObjectType, UnitType, PLAYER_MAX_UNITS},
player::Player,
projection::{ToAltitude, ToFlat},
};
use parry2d::bounding_volume::Aabb;
use parry3d::math::Isometry;
const MANUFACTURING_TIME: Duration = Duration::from_secs(2);
const DEFAULT_TARGET_DISTANCE: f32 = 20.;
pub(crate) struct ManufacturingPlugin;
impl Plugin for ManufacturingPlugin {
fn build(&self, app: &mut App) {
app.add_event::<EnqueueAssemblyEvent>()
.add_event::<ChangeDeliveryLocationEvent>()
.add_event::<DeliverEvent>()
.add_systems(
PreUpdate,
(
change_locations.in_set(ManufacturingSet::ChangeLocations),
check_spawn_locations.before(ManufacturingSet::Produce),
produce.in_set(ManufacturingSet::Produce),
deliver
.after(ManufacturingSet::ChangeLocations)
.after(ManufacturingSet::Produce),
)
.run_if(in_state(GameState::Playing)),
)
.add_systems(Update, enqueue.run_if(in_state(GameState::Playing)))
.add_systems(PostUpdate, configure.run_if(in_state(AppState::InGame)));
}
}
#[derive(Copy, Clone, Hash, Debug, PartialEq, Eq, SystemSet)]
enum ManufacturingSet {
ChangeLocations,
Produce,
}
#[derive(Event)]
pub struct ChangeDeliveryLocationEvent {
factory: Entity,
position: Vec2,
}
impl ChangeDeliveryLocationEvent {
pub fn new(factory: Entity, position: Vec2) -> Self {
Self { factory, position }
}
fn factory(&self) -> Entity {
self.factory
}
fn position(&self) -> Vec2 {
self.position
}
}
#[derive(Event)]
pub struct EnqueueAssemblyEvent {
factory: Entity,
unit: UnitType,
}
impl EnqueueAssemblyEvent {
pub fn new(factory: Entity, unit: UnitType) -> Self {
Self { factory, unit }
}
fn factory(&self) -> Entity {
self.factory
}
fn unit(&self) -> UnitType {
self.unit
}
}
#[derive(Event)]
struct DeliverEvent {
factory: Entity,
unit: UnitType,
}
impl DeliverEvent {
fn new(factory: Entity, unit: UnitType) -> Self {
Self { factory, unit }
}
fn factory(&self) -> Entity {
self.factory
}
fn unit(&self) -> UnitType {
self.unit
}
}
#[derive(Component)]
struct DeliveryLocation(Vec2);
impl DeliveryLocation {
fn initial(local_aabb: Aabb, transform: &Transform) -> Self {
let target = Vec2::new(
local_aabb.maxs.x + DEFAULT_TARGET_DISTANCE,
0.5 * (local_aabb.mins.y + local_aabb.maxs.y),
);
Self(transform.transform_point(target.to_msl()).to_flat())
}
}
#[derive(Component, Default)]
pub struct AssemblyLine {
blocks: Blocks,
queue: VecDeque<ProductionItem>,
}
impl AssemblyLine {
fn blocks_mut(&mut self) -> &mut Blocks {
&mut self.blocks
}
fn current(&self) -> Option<UnitType> {
self.queue.front().map(|item| item.unit())
}
fn enqueue(&mut self, unit: UnitType, time: Duration) {
let mut item = ProductionItem::new(unit);
if self.queue.is_empty() {
item.restart(time);
}
self.queue.push_back(item);
}
fn produce(&mut self, time: Duration) -> Option<UnitType> {
if let Some(time_past) = self.queue.front().and_then(|item| item.finished(time)) {
if self.blocks.blocked() {
self.queue.front_mut().unwrap().block(time);
None
} else {
let item = self.queue.pop_front().unwrap();
if item.is_active() {
if let Some(next) = self.queue.front_mut() {
next.restart(time - time_past);
}
}
Some(item.unit())
}
} else {
None
}
}
}
#[derive(Default)]
struct Blocks {
spawn_location: bool,
map_capacity: bool,
}
impl Blocks {
fn blocked(&self) -> bool {
self.spawn_location || self.map_capacity
}
}
struct ProductionItem {
accumulated: Duration,
restarted: Option<Duration>,
unit: UnitType,
}
impl ProductionItem {
fn new(unit: UnitType) -> Self {
Self {
accumulated: Duration::ZERO,
restarted: None,
unit,
}
}
fn unit(&self) -> UnitType {
self.unit
}
fn is_active(&self) -> bool {
self.restarted.is_some()
}
fn restart(&mut self, time: Duration) {
self.stop(time);
self.restarted = Some(time);
}
fn stop(&mut self, time: Duration) {
if let Some(last) = self.restarted {
self.accumulated += time - last;
if self.accumulated > MANUFACTURING_TIME {
self.accumulated = MANUFACTURING_TIME;
}
}
self.restarted = None;
}
fn block(&mut self, time: Duration) {
if self.progress(time) >= MANUFACTURING_TIME {
self.accumulated = MANUFACTURING_TIME;
self.restarted = Some(time);
}
}
fn finished(&self, time: Duration) -> Option<Duration> {
let progress = self.progress(time);
if progress >= MANUFACTURING_TIME {
Some(progress - MANUFACTURING_TIME)
} else {
None
}
}
fn progress(&self, time: Duration) -> Duration {
self.accumulated
+ self
.restarted
.map_or(Duration::ZERO, |restarted| time - restarted)
}
}
fn configure(
mut commands: Commands,
solids: SolidObjects,
new: Query<(Entity, &Transform, &ObjectTypeComponent), Added<Local>>,
mut pole_events: EventWriter<UpdatePoleLocationEvent>,
mut line_events: EventWriter<UpdateLineLocationEvent>,
) {
for (entity, transform, &object_type) in new.iter() {
let solid = solids.get(*object_type);
if let Some(factory) = solid.factory() {
let start = transform.transform_point(factory.position().to_msl());
let local_aabb = solid.ichnography().local_aabb();
let delivery_location = DeliveryLocation::initial(local_aabb, transform);
pole_events.send(UpdatePoleLocationEvent::new(entity, delivery_location.0));
let end = delivery_location.0.to_msl();
line_events.send(UpdateLineLocationEvent::new(
entity,
LineLocation::new(start, end),
));
commands
.entity(entity)
.insert((AssemblyLine::default(), delivery_location));
}
}
}
fn change_locations(
mut events: EventReader<ChangeDeliveryLocationEvent>,
mut locations: Query<&mut DeliveryLocation>,
mut pole_events: EventWriter<UpdatePoleLocationEvent>,
mut line_events: EventWriter<UpdateLineEndEvent>,
) {
for event in events.read() {
if let Ok(mut location) = locations.get_mut(event.factory()) {
let owner = event.factory();
location.0 = event.position();
pole_events.send(UpdatePoleLocationEvent::new(owner, event.position()));
let end = event.position().to_msl();
line_events.send(UpdateLineEndEvent::new(owner, end));
}
}
}
fn enqueue(
time: Res<Time>,
mut events: EventReader<EnqueueAssemblyEvent>,
mut lines: Query<&mut AssemblyLine>,
) {
for event in events.read() {
let Ok(mut line) = lines.get_mut(event.factory()) else {
continue;
};
info!(
"Enqueueing manufacturing of {} in {:?}.",
event.unit(),
event.factory()
);
line.enqueue(event.unit(), time.elapsed());
}
}
fn check_spawn_locations(
solids: SolidObjects,
space: SpatialQuery<Entity>,
mut factories: Query<(Entity, &ObjectTypeComponent, &Transform, &mut AssemblyLine)>,
) {
for (entity, &object_type, transform, mut line) in factories.iter_mut() {
line.blocks_mut().spawn_location = match line.current() {
Some(unit_type) => {
let factory = solids.get(*object_type).factory().unwrap();
let collider = solids
.get(ObjectType::Active(ActiveObjectType::Unit(unit_type)))
.collider();
let spawn_point = transform.transform_point(factory.position().to_msl());
let isometry = Isometry::translation(spawn_point.x, spawn_point.y, spawn_point.z);
let mut aabb = collider.aabb().transform_by(&isometry);
aabb.mins.y = f32::NEG_INFINITY;
aabb.maxs.y = f32::INFINITY;
space.query_aabb(&aabb, Some(entity)).next().is_some()
}
None => false,
};
}
}
fn produce(
time: Res<Time>,
counter: Res<ObjectCounter>,
mut factories: Query<(Entity, &PlayerComponent, &mut AssemblyLine)>,
mut deliver_events: EventWriter<DeliverEvent>,
) {
let mut counts: AHashMap<Player, u32> = AHashMap::from_iter(
counter
.counters()
.map(|(&player, counter)| (player, counter.unit_count())),
);
for (factory, &player, mut assembly) in factories.iter_mut() {
let player_count = counts.entry(*player).or_default();
loop {
assembly.blocks_mut().map_capacity = *player_count >= PLAYER_MAX_UNITS;
let Some(unit_type) = assembly.produce(time.elapsed()) else {
break;
};
*player_count += 1;
deliver_events.send(DeliverEvent::new(factory, unit_type));
}
}
}
fn deliver(
solids: SolidObjects,
mut deliver_events: EventReader<DeliverEvent>,
mut spawn_active_events: EventWriter<SpawnLocalActiveEvent>,
factories: Query<(
&Transform,
&ObjectTypeComponent,
&PlayerComponent,
&DeliveryLocation,
)>,
) {
for delivery in deliver_events.read() {
info!(
"Manufacturing of {} in {:?} just finished.",
delivery.unit(),
delivery.factory()
);
let (transform, &factory_object_type, &player, delivery_location) =
factories.get(delivery.factory()).unwrap();
let object_type = ActiveObjectType::Unit(delivery.unit());
let factory = solids.get(*factory_object_type).factory().unwrap();
debug_assert!(factory.products().contains(&delivery.unit()));
let spawn_point = transform.transform_point(factory.position().to_msl());
let path_target = PathTarget::new(
delivery_location.0,
PathQueryProps::new(0., f32::INFINITY),
false,
);
spawn_active_events.send(SpawnLocalActiveEvent::new(
object_type,
Transform::from_translation(spawn_point),
*player,
Some(path_target),
));
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_assembly_line() {
let mut line = AssemblyLine::default();
assert!(line.produce(Duration::from_secs(20)).is_none());
line.enqueue(UnitType::Attacker, Duration::from_secs(21));
line.enqueue(UnitType::Attacker, Duration::from_secs(21));
assert!(line.produce(Duration::from_secs(22)).is_none());
line.blocks_mut().map_capacity = true;
assert!(line.produce(Duration::from_secs(25)).is_none());
line.blocks_mut().map_capacity = false;
assert_eq!(
line.produce(Duration::from_secs(26)).unwrap(),
UnitType::Attacker
);
assert!(line.produce(Duration::from_secs(26)).is_none());
assert_eq!(
line.produce(Duration::from_secs(27)).unwrap(),
UnitType::Attacker
);
assert!(line.produce(Duration::from_secs(30)).is_none());
line.enqueue(UnitType::Attacker, Duration::from_secs(50));
line.enqueue(UnitType::Attacker, Duration::from_secs(51));
assert!(line.produce(Duration::from_secs(51)).is_none());
assert_eq!(
line.produce(Duration::from_secs(61)).unwrap(),
UnitType::Attacker
);
assert_eq!(
line.produce(Duration::from_secs(63)).unwrap(),
UnitType::Attacker
);
assert!(line.produce(Duration::from_secs(90)).is_none());
}
}