1use std::collections::HashMap;
4use glam::Vec3;
5use crate::crafting::recipes::{Recipe, CraftResult, CraftingCalculator};
6
7#[derive(Debug, Clone, PartialEq, Eq, Hash)]
12pub enum WorkbenchType {
13 Forge,
14 AlchemyTable,
15 CookingPot,
16 EnchantingTable,
17 Workbench,
18 Loom,
19 Jeweler,
20}
21
22impl WorkbenchType {
23 pub fn label(&self) -> &'static str {
24 match self {
25 WorkbenchType::Forge => "Forge",
26 WorkbenchType::AlchemyTable => "Alchemy Table",
27 WorkbenchType::CookingPot => "Cooking Pot",
28 WorkbenchType::EnchantingTable => "Enchanting Table",
29 WorkbenchType::Workbench => "Workbench",
30 WorkbenchType::Loom => "Loom",
31 WorkbenchType::Jeweler => "Jeweler",
32 }
33 }
34
35 pub fn requires_fuel(&self) -> bool {
37 matches!(
38 self,
39 WorkbenchType::Forge | WorkbenchType::CookingPot
40 )
41 }
42
43 pub fn base_quality_bonus(&self) -> u32 {
45 match self {
46 WorkbenchType::Forge => 5,
47 WorkbenchType::AlchemyTable => 8,
48 WorkbenchType::CookingPot => 3,
49 WorkbenchType::EnchantingTable => 12,
50 WorkbenchType::Workbench => 4,
51 WorkbenchType::Loom => 4,
52 WorkbenchType::Jeweler => 10,
53 }
54 }
55}
56
57#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
62pub enum WorkbenchTier {
63 Basic,
64 Improved,
65 Advanced,
66 Master,
67}
68
69impl WorkbenchTier {
70 pub fn quality_multiplier(&self) -> f32 {
72 match self {
73 WorkbenchTier::Basic => 1.0,
74 WorkbenchTier::Improved => 1.25,
75 WorkbenchTier::Advanced => 1.60,
76 WorkbenchTier::Master => 2.20,
77 }
78 }
79
80 pub fn speed_multiplier(&self) -> f32 {
82 match self {
83 WorkbenchTier::Basic => 1.0,
84 WorkbenchTier::Improved => 1.15,
85 WorkbenchTier::Advanced => 1.35,
86 WorkbenchTier::Master => 1.60,
87 }
88 }
89
90 pub fn upgrade_cost(&self) -> u64 {
92 match self {
93 WorkbenchTier::Basic => 500,
94 WorkbenchTier::Improved => 2000,
95 WorkbenchTier::Advanced => 8000,
96 WorkbenchTier::Master => 0, }
98 }
99
100 pub fn label(&self) -> &'static str {
101 match self {
102 WorkbenchTier::Basic => "Basic",
103 WorkbenchTier::Improved => "Improved",
104 WorkbenchTier::Advanced => "Advanced",
105 WorkbenchTier::Master => "Master",
106 }
107 }
108}
109
110#[derive(Debug, Clone)]
115pub enum WorkbenchState {
116 Idle,
117 Crafting {
118 job_id: u64,
119 elapsed: f32,
120 duration: f32,
121 },
122 Broken {
123 repair_cost: u64,
124 },
125}
126
127impl WorkbenchState {
128 pub fn is_idle(&self) -> bool {
129 matches!(self, WorkbenchState::Idle)
130 }
131
132 pub fn is_crafting(&self) -> bool {
133 matches!(self, WorkbenchState::Crafting { .. })
134 }
135
136 pub fn is_broken(&self) -> bool {
137 matches!(self, WorkbenchState::Broken { .. })
138 }
139}
140
141#[derive(Debug, Clone)]
147pub struct CraftingJob {
148 pub id: u64,
149 pub recipe_id: String,
150 pub ingredients_consumed: Vec<(String, u32)>,
152 pub started_at: f32,
154 pub duration: f32,
156 pub quantity: u32,
158 pub owner_id: String,
160}
161
162impl CraftingJob {
163 pub fn new(
164 id: u64,
165 recipe_id: impl Into<String>,
166 ingredients_consumed: Vec<(String, u32)>,
167 started_at: f32,
168 duration: f32,
169 quantity: u32,
170 owner_id: impl Into<String>,
171 ) -> Self {
172 Self {
173 id,
174 recipe_id: recipe_id.into(),
175 ingredients_consumed,
176 started_at,
177 duration,
178 quantity,
179 owner_id: owner_id.into(),
180 }
181 }
182
183 pub fn progress(&self, current_time: f32) -> f32 {
185 if self.duration <= 0.0 {
186 return 1.0;
187 }
188 ((current_time - self.started_at) / self.duration).clamp(0.0, 1.0)
189 }
190
191 pub fn is_complete(&self, current_time: f32) -> bool {
193 current_time >= self.started_at + self.duration
194 }
195}
196
197const MAX_QUEUE_SLOTS: usize = 8;
202
203#[derive(Debug, Clone)]
205pub struct CraftingQueue {
206 jobs: Vec<CraftingJob>,
207 next_job_id: u64,
208}
209
210impl CraftingQueue {
211 pub fn new() -> Self {
212 Self {
213 jobs: Vec::with_capacity(MAX_QUEUE_SLOTS),
214 next_job_id: 1,
215 }
216 }
217
218 pub fn has_capacity(&self) -> bool {
220 self.jobs.len() < MAX_QUEUE_SLOTS
221 }
222
223 pub fn len(&self) -> usize {
225 self.jobs.len()
226 }
227
228 pub fn is_empty(&self) -> bool {
229 self.jobs.is_empty()
230 }
231
232 pub fn enqueue(
234 &mut self,
235 recipe_id: impl Into<String>,
236 ingredients_consumed: Vec<(String, u32)>,
237 started_at: f32,
238 duration: f32,
239 quantity: u32,
240 owner_id: impl Into<String>,
241 ) -> Option<u64> {
242 if !self.has_capacity() {
243 return None;
244 }
245 let id = self.next_job_id;
246 self.next_job_id += 1;
247 self.jobs.push(CraftingJob::new(
248 id,
249 recipe_id,
250 ingredients_consumed,
251 started_at,
252 duration,
253 quantity,
254 owner_id,
255 ));
256 Some(id)
257 }
258
259 pub fn front(&self) -> Option<&CraftingJob> {
261 self.jobs.first()
262 }
263
264 pub fn dequeue(&mut self) -> Option<CraftingJob> {
266 if self.jobs.is_empty() {
267 None
268 } else {
269 Some(self.jobs.remove(0))
270 }
271 }
272
273 pub fn cancel(&mut self, job_id: u64) -> Option<CraftingJob> {
275 if let Some(pos) = self.jobs.iter().position(|j| j.id == job_id) {
276 Some(self.jobs.remove(pos))
277 } else {
278 None
279 }
280 }
281
282 pub fn all_jobs(&self) -> &[CraftingJob] {
284 &self.jobs
285 }
286}
287
288impl Default for CraftingQueue {
289 fn default() -> Self {
290 Self::new()
291 }
292}
293
294#[derive(Debug, Clone)]
299pub enum WorkbenchEvent {
300 JobStarted {
301 job_id: u64,
302 recipe_id: String,
303 },
304 JobCompleted {
305 job_id: u64,
306 results: Vec<(String, u32, u8)>, },
308 JobFailed {
309 job_id: u64,
310 reason: String,
311 },
312 FuelEmpty,
313 RepairNeeded {
314 repair_cost: u64,
315 },
316 QueueFull,
317}
318
319#[derive(Debug, Clone, PartialEq, Eq, Hash)]
324pub enum FuelType {
325 Coal,
326 Wood,
327 MagicCrystal,
328}
329
330impl FuelType {
331 pub fn burn_rate(&self) -> f32 {
333 match self {
334 FuelType::Coal => 0.5,
335 FuelType::Wood => 1.0,
336 FuelType::MagicCrystal => 0.1,
337 }
338 }
339
340 pub fn heat_quality_bonus(&self) -> u32 {
342 match self {
343 FuelType::Coal => 3,
344 FuelType::Wood => 1,
345 FuelType::MagicCrystal => 15,
346 }
347 }
348
349 pub fn fuel_value(&self) -> f32 {
351 match self {
352 FuelType::Coal => 60.0,
353 FuelType::Wood => 20.0,
354 FuelType::MagicCrystal => 300.0,
355 }
356 }
357
358 pub fn label(&self) -> &'static str {
359 match self {
360 FuelType::Coal => "Coal",
361 FuelType::Wood => "Wood",
362 FuelType::MagicCrystal => "Magic Crystal",
363 }
364 }
365}
366
367#[derive(Debug, Clone)]
369pub struct FuelSystem {
370 pub fuel_level: f32,
371 pub max_fuel: f32,
372 pub current_fuel_type: FuelType,
373 pub is_burning: bool,
374}
375
376impl FuelSystem {
377 pub fn new(max_fuel: f32) -> Self {
378 Self {
379 fuel_level: 0.0,
380 max_fuel,
381 current_fuel_type: FuelType::Coal,
382 is_burning: false,
383 }
384 }
385
386 pub fn add_fuel(&mut self, fuel_type: FuelType, items: u32) -> u32 {
388 let units = fuel_type.fuel_value() * items as f32;
389 let available_space = self.max_fuel - self.fuel_level;
390 if units <= available_space {
391 self.fuel_level += units;
392 self.current_fuel_type = fuel_type;
393 0
394 } else {
395 self.fuel_level = self.max_fuel;
396 let fuel_value = fuel_type.fuel_value();
397 self.current_fuel_type = fuel_type;
398 let overflow_units = units - available_space;
399 let overflow_items = (overflow_units / fuel_value).ceil() as u32;
400 overflow_items
401 }
402 }
403
404 pub fn consume(&mut self, dt: f32) -> bool {
406 if !self.is_burning {
407 return true;
408 }
409 let consumed = self.current_fuel_type.burn_rate() * dt;
410 if self.fuel_level >= consumed {
411 self.fuel_level -= consumed;
412 true
413 } else {
414 self.fuel_level = 0.0;
415 self.is_burning = false;
416 false
417 }
418 }
419
420 pub fn heat_quality_bonus(&self) -> u32 {
422 if self.is_burning && self.fuel_level > 0.0 {
423 self.current_fuel_type.heat_quality_bonus()
424 } else {
425 0
426 }
427 }
428
429 pub fn ignite(&mut self) {
430 if self.fuel_level > 0.0 {
431 self.is_burning = true;
432 }
433 }
434
435 pub fn extinguish(&mut self) {
436 self.is_burning = false;
437 }
438
439 pub fn is_empty(&self) -> bool {
440 self.fuel_level <= 0.0
441 }
442
443 pub fn level_fraction(&self) -> f32 {
445 if self.max_fuel <= 0.0 {
446 return 0.0;
447 }
448 (self.fuel_level / self.max_fuel).clamp(0.0, 1.0)
449 }
450}
451
452#[derive(Debug, Clone)]
458pub struct Workbench {
459 pub id: u64,
460 pub position: Vec3,
461 pub bench_type: WorkbenchType,
462 pub tier: WorkbenchTier,
463 pub state: WorkbenchState,
464 pub queue: CraftingQueue,
465 pub fuel: FuelSystem,
466 pub efficiency: f32,
468 pub wear: f32,
470 pub operator_skill: u32,
472 events_buffer: Vec<WorkbenchEvent>,
474 current_time: f32,
476}
477
478impl Workbench {
479 pub fn new(id: u64, position: Vec3, bench_type: WorkbenchType, tier: WorkbenchTier) -> Self {
480 let needs_fuel = bench_type.requires_fuel();
481 let max_fuel = if needs_fuel { 600.0 } else { 0.0 };
482 Self {
483 id,
484 position,
485 bench_type,
486 tier,
487 state: WorkbenchState::Idle,
488 queue: CraftingQueue::new(),
489 fuel: FuelSystem::new(max_fuel),
490 efficiency: 1.0,
491 wear: 0.0,
492 operator_skill: 1,
493 events_buffer: Vec::new(),
494 current_time: 0.0,
495 }
496 }
497
498 fn try_start_next_job(&mut self) {
500 if !self.state.is_idle() {
501 return;
502 }
503 if self.state.is_broken() {
504 return;
505 }
506 if let Some(job) = self.queue.front() {
507 let job_id = job.id;
508 let duration = job.duration;
509 let recipe_id = job.recipe_id.clone();
510 self.state = WorkbenchState::Crafting {
511 job_id,
512 elapsed: 0.0,
513 duration,
514 };
515 self.events_buffer.push(WorkbenchEvent::JobStarted { job_id, recipe_id });
516
517 if self.bench_type.requires_fuel() {
519 self.fuel.ignite();
520 }
521 }
522 }
523
524 pub fn tick(&mut self, dt: f32) -> Vec<WorkbenchEvent> {
526 self.events_buffer.clear();
527 self.current_time += dt;
528
529 if self.state.is_broken() {
530 return self.events_buffer.clone();
531 }
532
533 if self.state.is_idle() && !self.queue.is_empty() {
535 self.try_start_next_job();
536 }
537
538 match self.state.clone() {
539 WorkbenchState::Idle => {}
540 WorkbenchState::Broken { .. } => {}
541 WorkbenchState::Crafting { job_id, elapsed, duration } => {
542 if self.bench_type.requires_fuel() {
544 let fuel_ok = self.fuel.consume(dt);
545 if !fuel_ok {
546 self.state = WorkbenchState::Idle;
548 self.events_buffer.push(WorkbenchEvent::FuelEmpty);
549 return self.events_buffer.clone();
550 }
551 }
552
553 let new_elapsed = (elapsed + dt * self.efficiency).min(duration);
554 self.state = WorkbenchState::Crafting {
555 job_id,
556 elapsed: new_elapsed,
557 duration,
558 };
559
560 self.wear += dt * 0.0002;
562 if self.wear >= 1.0 {
563 self.wear = 1.0;
564 let repair_cost = self.compute_repair_cost();
565 self.state = WorkbenchState::Broken { repair_cost };
566 self.events_buffer.push(WorkbenchEvent::RepairNeeded { repair_cost });
567 return self.events_buffer.clone();
568 }
569
570 if new_elapsed >= duration {
572 if let Some(job) = self.queue.dequeue() {
574 let results = self.compute_job_results(&job);
575 self.events_buffer.push(WorkbenchEvent::JobCompleted {
576 job_id: job.id,
577 results,
578 });
579 }
580 self.state = WorkbenchState::Idle;
581 self.try_start_next_job();
583 }
584 }
585 }
586
587 self.events_buffer.clone()
588 }
589
590 fn compute_job_results(&self, job: &CraftingJob) -> Vec<(String, u32, u8)> {
592 let quality_bonus = self.tier_quality_bonus();
593 let fuel_bonus = self.fuel.heat_quality_bonus();
594 let mut results = Vec::new();
595
596 for _ in 0..job.quantity {
598 let base_quality: u8 = 80;
600 let computed_quality = CraftingCalculator::calculate_quality(
601 base_quality,
602 self.operator_skill,
603 quality_bonus + fuel_bonus,
604 );
605 let item_id = format!("{}_product", job.recipe_id);
606 results.push((item_id, 1, computed_quality));
607 }
608 results
609 }
610
611 pub fn compute_results_for_recipe(
613 &self,
614 recipe: &Recipe,
615 quantity: u32,
616 rng_values: &[f32],
617 ) -> Vec<(String, u32, u8)> {
618 let quality_bonus = self.tier_quality_bonus();
619 let fuel_bonus = self.fuel.heat_quality_bonus();
620 let mut results = Vec::new();
621 let mut rng_idx = 0;
622
623 for _ in 0..quantity {
624 for craft_result in &recipe.results {
625 let rng = rng_values.get(rng_idx).copied().unwrap_or(0.5);
626 rng_idx += 1;
627
628 if CraftingCalculator::evaluate_chance(craft_result, self.operator_skill, rng) {
629 let computed_quality = CraftingCalculator::calculate_quality(
630 craft_result.quality,
631 self.operator_skill,
632 quality_bonus + fuel_bonus,
633 );
634 let computed_quantity = CraftingCalculator::calculate_quantity(
635 craft_result.quantity,
636 self.operator_skill,
637 0.0,
638 );
639 results.push((craft_result.item_id.clone(), computed_quantity, computed_quality));
640 }
641 }
642 }
643 results
644 }
645
646 pub fn tier_quality_bonus(&self) -> u32 {
648 let base = self.bench_type.base_quality_bonus();
649 let multiplied = base as f32 * self.tier.quality_multiplier();
650 multiplied.round() as u32
651 }
652
653 pub fn submit_job(
657 &mut self,
658 recipe_id: impl Into<String>,
659 ingredients: Vec<(String, u32)>,
660 duration: f32,
661 quantity: u32,
662 owner_id: impl Into<String>,
663 ) -> Result<u64, WorkbenchEvent> {
664 if self.state.is_broken() {
665 let repair_cost = match &self.state {
666 WorkbenchState::Broken { repair_cost } => *repair_cost,
667 _ => 0,
668 };
669 return Err(WorkbenchEvent::RepairNeeded { repair_cost });
670 }
671 if !self.queue.has_capacity() {
672 return Err(WorkbenchEvent::QueueFull);
673 }
674 let adjusted_duration = duration / self.tier.speed_multiplier();
675 let job_id = self.queue.enqueue(
676 recipe_id,
677 ingredients,
678 self.current_time,
679 adjusted_duration,
680 quantity,
681 owner_id,
682 );
683 match job_id {
684 Some(id) => {
685 self.try_start_next_job();
686 Ok(id)
687 }
688 None => Err(WorkbenchEvent::QueueFull),
689 }
690 }
691
692 pub fn repair(&mut self) {
694 self.wear = 0.0;
695 self.state = WorkbenchState::Idle;
696 self.efficiency = 1.0;
697 }
698
699 pub fn compute_repair_cost(&self) -> u64 {
701 let base: u64 = match self.tier {
702 WorkbenchTier::Basic => 100,
703 WorkbenchTier::Improved => 400,
704 WorkbenchTier::Advanced => 1200,
705 WorkbenchTier::Master => 3500,
706 };
707 let wear_factor = (self.wear * 3.0) as u64;
708 base + wear_factor * 10
709 }
710
711 pub fn upgrade_cost(&self) -> Option<u64> {
713 match self.tier {
714 WorkbenchTier::Master => None,
715 _ => Some(self.tier.upgrade_cost()),
716 }
717 }
718
719 pub fn upgrade(&mut self) {
721 self.tier = match self.tier {
722 WorkbenchTier::Basic => WorkbenchTier::Improved,
723 WorkbenchTier::Improved => WorkbenchTier::Advanced,
724 WorkbenchTier::Advanced => WorkbenchTier::Master,
725 WorkbenchTier::Master => WorkbenchTier::Master,
726 };
727 }
728
729 pub fn cancel_job(&mut self, job_id: u64) -> Option<CraftingJob> {
731 if let WorkbenchState::Crafting { job_id: active_id, .. } = &self.state {
733 if *active_id == job_id {
734 self.state = WorkbenchState::Idle;
735 }
736 }
737 self.queue.cancel(job_id)
738 }
739
740 pub fn current_progress(&self) -> Option<f32> {
742 match &self.state {
743 WorkbenchState::Crafting { elapsed, duration, .. } => {
744 if *duration <= 0.0 {
745 Some(1.0)
746 } else {
747 Some((elapsed / duration).clamp(0.0, 1.0))
748 }
749 }
750 _ => None,
751 }
752 }
753}
754
755#[derive(Debug, Clone)]
761pub struct CraftingStation {
762 pub id: u64,
763 pub name: String,
764 pub position: Vec3,
765 pub benches: Vec<Workbench>,
766 pub inventory: HashMap<String, u32>,
768}
769
770impl CraftingStation {
771 pub fn new(id: u64, name: impl Into<String>, position: Vec3) -> Self {
772 Self {
773 id,
774 name: name.into(),
775 position,
776 benches: Vec::new(),
777 inventory: HashMap::new(),
778 }
779 }
780
781 pub fn add_bench(&mut self, bench: Workbench) {
783 self.benches.push(bench);
784 }
785
786 pub fn add_item(&mut self, item_id: impl Into<String>, quantity: u32) {
788 *self.inventory.entry(item_id.into()).or_insert(0) += quantity;
789 }
790
791 pub fn remove_item(&mut self, item_id: &str, quantity: u32) -> bool {
793 if let Some(stock) = self.inventory.get_mut(item_id) {
794 if *stock >= quantity {
795 *stock -= quantity;
796 return true;
797 }
798 }
799 false
800 }
801
802 pub fn item_count(&self, item_id: &str) -> u32 {
804 self.inventory.get(item_id).copied().unwrap_or(0)
805 }
806
807 pub fn tick(&mut self, dt: f32) -> Vec<(u64, WorkbenchEvent)> {
809 let mut all_events = Vec::new();
810 for bench in &mut self.benches {
811 let bench_id = bench.id;
812 let events = bench.tick(dt);
813 for event in events {
814 if let WorkbenchEvent::JobCompleted { ref results, .. } = event {
816 for (item_id, qty, _quality) in results {
817 *self.inventory.entry(item_id.clone()).or_insert(0) += qty;
818 }
819 }
820 all_events.push((bench_id, event));
821 }
822 }
823 all_events
824 }
825
826 pub fn find_bench_of_type(&self, bench_type: &WorkbenchType) -> Option<&Workbench> {
828 self.benches.iter().find(|b| &b.bench_type == bench_type)
829 }
830
831 pub fn find_bench_of_type_mut(&mut self, bench_type: &WorkbenchType) -> Option<&mut Workbench> {
833 self.benches.iter_mut().find(|b| &b.bench_type == bench_type)
834 }
835}
836
837#[derive(Debug, Clone)]
843pub struct AutoCraftConfig {
844 pub recipe_id: String,
845 pub target_quantity: u32,
846 pub owner_id: String,
847 pub stop_at_stock: u32,
849}
850
851impl AutoCraftConfig {
852 pub fn new(
853 recipe_id: impl Into<String>,
854 target_quantity: u32,
855 owner_id: impl Into<String>,
856 ) -> Self {
857 Self {
858 recipe_id: recipe_id.into(),
859 target_quantity,
860 owner_id: owner_id.into(),
861 stop_at_stock: u32::MAX,
862 }
863 }
864}
865
866#[derive(Debug, Clone)]
868pub struct AutoCrafter {
869 pub config: AutoCraftConfig,
870 pub produced: u32,
871 pub running: bool,
872 pub last_submit_time: f32,
873 pub submit_interval: f32,
875}
876
877impl AutoCrafter {
878 pub fn new(config: AutoCraftConfig) -> Self {
879 Self {
880 config,
881 produced: 0,
882 running: true,
883 last_submit_time: 0.0,
884 submit_interval: 1.0,
885 }
886 }
887
888 pub fn tick(
892 &mut self,
893 current_time: f32,
894 station: &mut CraftingStation,
895 recipe_duration: f32,
896 ingredients: Vec<(String, u32)>,
897 ) -> u32 {
898 if !self.running {
899 return 0;
900 }
901 if self.produced >= self.config.target_quantity {
902 self.running = false;
903 return 0;
904 }
905 if current_time - self.last_submit_time < self.submit_interval {
906 return 0;
907 }
908
909 let recipe_output_key = format!("{}_product", self.config.recipe_id);
911 let current_stock = station.item_count(&recipe_output_key);
912 if current_stock >= self.config.stop_at_stock {
913 self.running = false;
914 return 0;
915 }
916
917 let mut submitted = 0u32;
918 let bench_count = station.benches.len();
919 let mut i = 0;
920 while i < bench_count {
921 if self.produced >= self.config.target_quantity { break; }
922 let remaining = self.config.target_quantity - self.produced;
923 if !station.benches[i].queue.has_capacity() { i += 1; continue; }
924 let can_craft = ingredients.iter().all(|(item_id, qty)| {
925 station.inventory.get(item_id).copied().unwrap_or(0) >= *qty
926 });
927 if !can_craft { break; }
928 for (item_id, qty) in &ingredients {
929 if let Some(stock) = station.inventory.get_mut(item_id) {
930 *stock = stock.saturating_sub(*qty);
931 }
932 }
933 let batch = remaining.min(8);
934 let result = station.benches[i].submit_job(
935 self.config.recipe_id.clone(),
936 ingredients.clone(),
937 recipe_duration,
938 batch,
939 self.config.owner_id.clone(),
940 );
941 if result.is_ok() {
942 self.produced += batch;
943 submitted += 1;
944 self.last_submit_time = current_time;
945 }
946 i += 1;
947 }
948 submitted
949 }
950
951 pub fn stop(&mut self) {
952 self.running = false;
953 }
954
955 pub fn restart(&mut self) {
956 self.running = true;
957 self.produced = 0;
958 }
959}