loot
This commit is contained in:
@@ -227,6 +227,46 @@ let beginner = harvestable_db.get_by_level_range(1, 10);
|
|||||||
let advanced = harvestable_db.get_by_level_range(50, 100);
|
let advanced = harvestable_db.get_by_level_range(50, 100);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Loading Loot Tables
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use cursebreaker_parser::LootDatabase;
|
||||||
|
|
||||||
|
let loot_db = LootDatabase::load_from_xml("Data/XMLs/Loot/Loot.xml")?;
|
||||||
|
println!("Loaded {} loot tables", loot_db.len());
|
||||||
|
```
|
||||||
|
|
||||||
|
### Querying Loot Tables
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Get all loot tables for a specific NPC
|
||||||
|
let npc_id = 45;
|
||||||
|
let tables = loot_db.get_tables_for_npc(npc_id);
|
||||||
|
|
||||||
|
// Get all drops for a specific NPC
|
||||||
|
let drops = loot_db.get_drops_for_npc(npc_id);
|
||||||
|
for drop in drops {
|
||||||
|
println!("Item ID: {}, Rate: {:?}", drop.item, drop.rate);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find which NPCs drop a specific item
|
||||||
|
let item_id = 180;
|
||||||
|
let npcs = loot_db.get_npcs_dropping_item(item_id);
|
||||||
|
println!("Item {} drops from {} NPCs", item_id, npcs.len());
|
||||||
|
|
||||||
|
// Get all tables with conditional drops (checks field)
|
||||||
|
let conditional = loot_db.get_conditional_tables();
|
||||||
|
|
||||||
|
// Get all tables with guaranteed drops (rate = 1)
|
||||||
|
let guaranteed = loot_db.get_tables_with_guaranteed_drops();
|
||||||
|
|
||||||
|
// Get all unique item IDs that can drop
|
||||||
|
let droppable_items = loot_db.get_all_droppable_items();
|
||||||
|
|
||||||
|
// Get all NPCs that have loot tables
|
||||||
|
let npcs_with_loot = loot_db.get_all_npcs_with_loot();
|
||||||
|
```
|
||||||
|
|
||||||
## Cross-referencing Data
|
## Cross-referencing Data
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
@@ -257,6 +297,30 @@ for harvestable in harvestable_db.all_harvestables() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Find what items an NPC drops
|
||||||
|
let npc_id = 45;
|
||||||
|
if let Some(npc) = npc_db.get_by_id(npc_id) {
|
||||||
|
let drops = loot_db.get_drops_for_npc(npc_id);
|
||||||
|
println!("NPC '{}' drops {} items:", npc.name, drops.len());
|
||||||
|
for drop in drops {
|
||||||
|
if let Some(item) = item_db.get_by_id(drop.item) {
|
||||||
|
println!(" - {}", item.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find which NPCs drop a specific item
|
||||||
|
let item_id = 180;
|
||||||
|
if let Some(item) = item_db.get_by_id(item_id) {
|
||||||
|
let npcs = loot_db.get_npcs_dropping_item(item_id);
|
||||||
|
println!("Item '{}' drops from:", item.name);
|
||||||
|
for npc_id in npcs {
|
||||||
|
if let Some(npc) = npc_db.get_by_id(npc_id) {
|
||||||
|
println!(" - {}", npc.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Statistics from XML Files
|
## Statistics from XML Files
|
||||||
@@ -299,6 +363,13 @@ When loaded from `/home/connor/repos/CBAssets/Data/XMLs/`:
|
|||||||
- **Level 51-100**: 28
|
- **Level 51-100**: 28
|
||||||
- **Unique Items from Harvestables**: 98
|
- **Unique Items from Harvestables**: 98
|
||||||
|
|
||||||
|
### Loot/Loot.xml
|
||||||
|
- **Total Loot Tables**: 175
|
||||||
|
- **NPCs with Loot**: 267
|
||||||
|
- **Droppable Items**: 405
|
||||||
|
- **Tables with Conditional Drops**: 33
|
||||||
|
- **Tables with Guaranteed Drops**: Multiple tables include guaranteed (rate=1) drops
|
||||||
|
|
||||||
## File Structure
|
## File Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -312,12 +383,14 @@ cursebreaker-parser/
|
|||||||
│ │ ├── npc.rs # NPC data structures
|
│ │ ├── npc.rs # NPC data structures
|
||||||
│ │ ├── quest.rs # Quest data structures
|
│ │ ├── quest.rs # Quest data structures
|
||||||
│ │ ├── harvestable.rs # Harvestable data structures
|
│ │ ├── harvestable.rs # Harvestable data structures
|
||||||
|
│ │ ├── loot.rs # Loot table data structures
|
||||||
│ │ └── interactable_resource.rs
|
│ │ └── interactable_resource.rs
|
||||||
│ ├── xml_parser.rs # XML parsing logic (all types)
|
│ ├── xml_parser.rs # XML parsing logic (all types)
|
||||||
│ ├── item_database.rs # ItemDatabase for runtime access
|
│ ├── item_database.rs # ItemDatabase for runtime access
|
||||||
│ ├── npc_database.rs # NpcDatabase for runtime access
|
│ ├── npc_database.rs # NpcDatabase for runtime access
|
||||||
│ ├── quest_database.rs # QuestDatabase for runtime access
|
│ ├── quest_database.rs # QuestDatabase for runtime access
|
||||||
│ └── harvestable_database.rs # HarvestableDatabase for runtime access
|
│ ├── harvestable_database.rs # HarvestableDatabase for runtime access
|
||||||
|
│ └── loot_database.rs # LootDatabase for runtime access
|
||||||
└── examples/
|
└── examples/
|
||||||
├── item_database_demo.rs # Items usage example
|
├── item_database_demo.rs # Items usage example
|
||||||
└── game_data_demo.rs # Full game data example
|
└── game_data_demo.rs # Full game data example
|
||||||
@@ -339,12 +412,12 @@ thiserror = "1.0" # Error handling
|
|||||||
- ✅ NPCs (`/XMLs/Npcs/NPCInfo.xml`)
|
- ✅ NPCs (`/XMLs/Npcs/NPCInfo.xml`)
|
||||||
- ✅ Quests (`/XMLs/Quests/Quests.xml`)
|
- ✅ Quests (`/XMLs/Quests/Quests.xml`)
|
||||||
- ✅ Harvestables (`/XMLs/Harvestables/HarvestableInfo.xml`)
|
- ✅ Harvestables (`/XMLs/Harvestables/HarvestableInfo.xml`)
|
||||||
|
- ✅ Loot tables (`/XMLs/Loot/Loot.xml`)
|
||||||
|
|
||||||
## Future Enhancements
|
## Future Enhancements
|
||||||
|
|
||||||
The same pattern can be extended to parse other XML files:
|
The same pattern can be extended to parse other XML files:
|
||||||
|
|
||||||
- [ ] Loot tables (`/XMLs/Loot/*.xml`)
|
|
||||||
- [ ] Maps (`/XMLs/Maps/*.xml`)
|
- [ ] Maps (`/XMLs/Maps/*.xml`)
|
||||||
- [ ] Dialogue (`/XMLs/Dialogue/*.xml`)
|
- [ ] Dialogue (`/XMLs/Dialogue/*.xml`)
|
||||||
- [ ] Events (`/XMLs/Events/*.xml`)
|
- [ ] Events (`/XMLs/Events/*.xml`)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
//!
|
//!
|
||||||
//! Run with: cargo run --example game_data_demo
|
//! Run with: cargo run --example game_data_demo
|
||||||
|
|
||||||
use cursebreaker_parser::{ItemDatabase, NpcDatabase, QuestDatabase, HarvestableDatabase};
|
use cursebreaker_parser::{ItemDatabase, NpcDatabase, QuestDatabase, HarvestableDatabase, LootDatabase};
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
println!("🎮 Cursebreaker Game Data Demo\n");
|
println!("🎮 Cursebreaker Game Data Demo\n");
|
||||||
@@ -13,11 +13,13 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
let npc_db = NpcDatabase::load_from_xml("/home/connor/repos/CBAssets/Data/XMLs/Npcs/NPCInfo.xml")?;
|
let npc_db = NpcDatabase::load_from_xml("/home/connor/repos/CBAssets/Data/XMLs/Npcs/NPCInfo.xml")?;
|
||||||
let quest_db = QuestDatabase::load_from_xml("/home/connor/repos/CBAssets/Data/XMLs/Quests/Quests.xml")?;
|
let quest_db = QuestDatabase::load_from_xml("/home/connor/repos/CBAssets/Data/XMLs/Quests/Quests.xml")?;
|
||||||
let harvestable_db = HarvestableDatabase::load_from_xml("/home/connor/repos/CBAssets/Data/XMLs/Harvestables/HarvestableInfo.xml")?;
|
let harvestable_db = HarvestableDatabase::load_from_xml("/home/connor/repos/CBAssets/Data/XMLs/Harvestables/HarvestableInfo.xml")?;
|
||||||
|
let loot_db = LootDatabase::load_from_xml("/home/connor/repos/CBAssets/Data/XMLs/Loot/Loot.xml")?;
|
||||||
|
|
||||||
println!("✅ Loaded {} items", item_db.len());
|
println!("✅ Loaded {} items", item_db.len());
|
||||||
println!("✅ Loaded {} NPCs", npc_db.len());
|
println!("✅ Loaded {} NPCs", npc_db.len());
|
||||||
println!("✅ Loaded {} quests", quest_db.len());
|
println!("✅ Loaded {} quests", quest_db.len());
|
||||||
println!("✅ Loaded {} harvestables\n", harvestable_db.len());
|
println!("✅ Loaded {} harvestables", harvestable_db.len());
|
||||||
|
println!("✅ Loaded {} loot tables\n", loot_db.len());
|
||||||
|
|
||||||
// =======================================================================
|
// =======================================================================
|
||||||
// Items
|
// Items
|
||||||
@@ -191,6 +193,74 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
println!(" • Level 51-100: {}", high_level.len());
|
println!(" • Level 51-100: {}", high_level.len());
|
||||||
println!();
|
println!();
|
||||||
|
|
||||||
|
// =======================================================================
|
||||||
|
// Loot Tables
|
||||||
|
// =======================================================================
|
||||||
|
println!("=== Loot Tables ===");
|
||||||
|
let all_tables = loot_db.all_tables();
|
||||||
|
let conditional_tables = loot_db.get_conditional_tables();
|
||||||
|
let guaranteed_tables = loot_db.get_tables_with_guaranteed_drops();
|
||||||
|
|
||||||
|
println!("Statistics:");
|
||||||
|
println!(" • Total loot tables: {}", loot_db.len());
|
||||||
|
println!(" • NPCs with loot: {}", loot_db.get_all_npcs_with_loot().len());
|
||||||
|
println!(" • Droppable items: {}", loot_db.get_all_droppable_items().len());
|
||||||
|
println!(" • Tables with conditional drops: {}", conditional_tables.len());
|
||||||
|
println!(" • Tables with guaranteed drops: {}", guaranteed_tables.len());
|
||||||
|
|
||||||
|
// Sample loot table
|
||||||
|
if let Some(table) = all_tables.first() {
|
||||||
|
println!("\nSample loot table:");
|
||||||
|
if let Some(name) = &table.name {
|
||||||
|
println!(" Name: {}", name);
|
||||||
|
}
|
||||||
|
println!(" NPCs: {:?}", table.npc_ids);
|
||||||
|
println!(" Drops: {} items", table.drops.len());
|
||||||
|
|
||||||
|
// Show first few drops
|
||||||
|
println!(" Sample drops:");
|
||||||
|
for drop in table.drops.iter().take(3) {
|
||||||
|
if let Some(item) = item_db.get_by_id(drop.item) {
|
||||||
|
let rate_str = drop.rate.map(|r| r.to_string()).unwrap_or_else(|| "N/A".to_string());
|
||||||
|
let amount_str = if let (Some(min), Some(max)) = (drop.minamount, drop.maxamount) {
|
||||||
|
format!("{}x{}", min, max)
|
||||||
|
} else {
|
||||||
|
"1x1".to_string()
|
||||||
|
};
|
||||||
|
println!(" - {} ({}, rate: {})", item.name, amount_str, rate_str);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cross-reference: Find what an NPC drops
|
||||||
|
println!("\nSample NPC drops:");
|
||||||
|
if let Some(npc) = npc_db.get_hostile().first() {
|
||||||
|
println!(" NPC: {} (ID: {})", npc.name, npc.id);
|
||||||
|
let drops = loot_db.get_drops_for_npc(npc.id);
|
||||||
|
if !drops.is_empty() {
|
||||||
|
println!(" Drops {} different items:", drops.len());
|
||||||
|
for drop in drops.iter().take(5) {
|
||||||
|
if let Some(item) = item_db.get_by_id(drop.item) {
|
||||||
|
println!(" - {}", item.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
println!(" No drops configured");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cross-reference: Find what NPCs drop an item
|
||||||
|
if let Some(item) = item_db.get_by_id(180) {
|
||||||
|
println!("\nItem '{}' drops from:", item.name);
|
||||||
|
let npcs = loot_db.get_npcs_dropping_item(180);
|
||||||
|
for npc_id in npcs.iter().take(5) {
|
||||||
|
if let Some(npc) = npc_db.get_by_id(*npc_id) {
|
||||||
|
println!(" • {}", npc.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
|
||||||
// =======================================================================
|
// =======================================================================
|
||||||
// Cross-referencing data
|
// Cross-referencing data
|
||||||
// =======================================================================
|
// =======================================================================
|
||||||
|
|||||||
@@ -55,15 +55,18 @@ mod item_database;
|
|||||||
mod npc_database;
|
mod npc_database;
|
||||||
mod quest_database;
|
mod quest_database;
|
||||||
mod harvestable_database;
|
mod harvestable_database;
|
||||||
|
mod loot_database;
|
||||||
|
|
||||||
pub use item_database::ItemDatabase;
|
pub use item_database::ItemDatabase;
|
||||||
pub use npc_database::NpcDatabase;
|
pub use npc_database::NpcDatabase;
|
||||||
pub use quest_database::QuestDatabase;
|
pub use quest_database::QuestDatabase;
|
||||||
pub use harvestable_database::HarvestableDatabase;
|
pub use harvestable_database::HarvestableDatabase;
|
||||||
|
pub use loot_database::LootDatabase;
|
||||||
pub use types::{
|
pub use types::{
|
||||||
Item, ItemStat, CraftingRecipe, AnimationSet, GenerateRule, InteractableResource,
|
Item, ItemStat, CraftingRecipe, AnimationSet, GenerateRule, InteractableResource,
|
||||||
Npc, NpcStat, NpcLevel, RightClick, BarkGroup, Bark, QuestMarker, NpcAnimationSet,
|
Npc, NpcStat, NpcLevel, RightClick, BarkGroup, Bark, QuestMarker, NpcAnimationSet,
|
||||||
Quest, QuestPhase, QuestReward,
|
Quest, QuestPhase, QuestReward,
|
||||||
Harvestable, HarvestableDrop,
|
Harvestable, HarvestableDrop,
|
||||||
|
LootTable, LootDrop,
|
||||||
};
|
};
|
||||||
pub use xml_parser::XmlParseError;
|
pub use xml_parser::XmlParseError;
|
||||||
|
|||||||
168
cursebreaker-parser/src/loot_database.rs
Normal file
168
cursebreaker-parser/src/loot_database.rs
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
use crate::types::{LootTable, LootDrop};
|
||||||
|
use crate::xml_parser::{parse_loot_xml, XmlParseError};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
/// A database for managing Loot Tables loaded from XML files
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct LootDatabase {
|
||||||
|
tables: Vec<LootTable>,
|
||||||
|
// Map NPC ID -> list of table indices that apply to this NPC
|
||||||
|
tables_by_npc: HashMap<i32, Vec<usize>>,
|
||||||
|
// Map item ID -> list of table indices that drop this item
|
||||||
|
tables_by_item: HashMap<i32, Vec<usize>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LootDatabase {
|
||||||
|
/// Create a new empty LootDatabase
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
tables: Vec::new(),
|
||||||
|
tables_by_npc: HashMap::new(),
|
||||||
|
tables_by_item: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load loot tables from an XML file
|
||||||
|
pub fn load_from_xml<P: AsRef<Path>>(path: P) -> Result<Self, XmlParseError> {
|
||||||
|
let tables = parse_loot_xml(path)?;
|
||||||
|
let mut db = Self::new();
|
||||||
|
db.add_tables(tables);
|
||||||
|
Ok(db)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add loot tables to the database
|
||||||
|
pub fn add_tables(&mut self, tables: Vec<LootTable>) {
|
||||||
|
for table in tables {
|
||||||
|
let index = self.tables.len();
|
||||||
|
|
||||||
|
// Index by NPC IDs
|
||||||
|
for &npc_id in &table.npc_ids {
|
||||||
|
self.tables_by_npc
|
||||||
|
.entry(npc_id)
|
||||||
|
.or_insert_with(Vec::new)
|
||||||
|
.push(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Index by item IDs
|
||||||
|
for drop in &table.drops {
|
||||||
|
self.tables_by_item
|
||||||
|
.entry(drop.item)
|
||||||
|
.or_insert_with(Vec::new)
|
||||||
|
.push(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.tables.push(table);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all loot tables that apply to a specific NPC ID
|
||||||
|
pub fn get_tables_for_npc(&self, npc_id: i32) -> Vec<&LootTable> {
|
||||||
|
self.tables_by_npc
|
||||||
|
.get(&npc_id)
|
||||||
|
.map(|indices| {
|
||||||
|
indices
|
||||||
|
.iter()
|
||||||
|
.filter_map(|&idx| self.tables.get(idx))
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all loot tables that drop a specific item
|
||||||
|
pub fn get_tables_with_item(&self, item_id: i32) -> Vec<&LootTable> {
|
||||||
|
self.tables_by_item
|
||||||
|
.get(&item_id)
|
||||||
|
.map(|indices| {
|
||||||
|
indices
|
||||||
|
.iter()
|
||||||
|
.filter_map(|&idx| self.tables.get(idx))
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all possible drops for a specific NPC
|
||||||
|
pub fn get_drops_for_npc(&self, npc_id: i32) -> Vec<&LootDrop> {
|
||||||
|
let mut drops = Vec::new();
|
||||||
|
for table in self.get_tables_for_npc(npc_id) {
|
||||||
|
drops.extend(&table.drops);
|
||||||
|
}
|
||||||
|
drops
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all NPCs that can drop a specific item
|
||||||
|
pub fn get_npcs_dropping_item(&self, item_id: i32) -> Vec<i32> {
|
||||||
|
let mut npcs = std::collections::HashSet::new();
|
||||||
|
|
||||||
|
if let Some(table_indices) = self.tables_by_item.get(&item_id) {
|
||||||
|
for &idx in table_indices {
|
||||||
|
if let Some(table) = self.tables.get(idx) {
|
||||||
|
npcs.extend(&table.npc_ids);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
npcs.into_iter().collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all loot tables
|
||||||
|
pub fn all_tables(&self) -> &[LootTable] {
|
||||||
|
&self.tables
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get tables with conditional drops (that have checks)
|
||||||
|
pub fn get_conditional_tables(&self) -> Vec<&LootTable> {
|
||||||
|
self.tables
|
||||||
|
.iter()
|
||||||
|
.filter(|t| !t.get_conditional_drops().is_empty())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get tables with guaranteed drops (rate = 1)
|
||||||
|
pub fn get_tables_with_guaranteed_drops(&self) -> Vec<&LootTable> {
|
||||||
|
self.tables
|
||||||
|
.iter()
|
||||||
|
.filter(|t| !t.get_guaranteed_drops().is_empty())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all unique item IDs that can drop
|
||||||
|
pub fn get_all_droppable_items(&self) -> Vec<i32> {
|
||||||
|
self.tables_by_item.keys().copied().collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all unique NPC IDs that have loot tables
|
||||||
|
pub fn get_all_npcs_with_loot(&self) -> Vec<i32> {
|
||||||
|
self.tables_by_npc.keys().copied().collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get number of loot tables in database
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.tables.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if database is empty
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.tables.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prepare loot tables for SQL insertion
|
||||||
|
/// Returns a vector of tuples (npc_ids_json, name, json_data)
|
||||||
|
pub fn prepare_for_sql(&self) -> Vec<(String, Option<String>, String)> {
|
||||||
|
self.tables
|
||||||
|
.iter()
|
||||||
|
.map(|table| {
|
||||||
|
let npc_ids_json = serde_json::to_string(&table.npc_ids).unwrap_or_else(|_| "[]".to_string());
|
||||||
|
let json = serde_json::to_string(table).unwrap_or_else(|_| "{}".to_string());
|
||||||
|
(npc_ids_json, table.name.clone(), json)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for LootDatabase {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
//! 3. Extracting typeId and transform positions
|
//! 3. Extracting typeId and transform positions
|
||||||
//! 4. Writing resource data to an output file
|
//! 4. Writing resource data to an output file
|
||||||
|
|
||||||
use cursebreaker_parser::{ItemDatabase, NpcDatabase, QuestDatabase, HarvestableDatabase, InteractableResource};
|
use cursebreaker_parser::{ItemDatabase, NpcDatabase, QuestDatabase, HarvestableDatabase, LootDatabase, InteractableResource};
|
||||||
use unity_parser::UnityProject;
|
use unity_parser::UnityProject;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use unity_parser::log::DedupLogger;
|
use unity_parser::log::DedupLogger;
|
||||||
@@ -41,6 +41,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
let harvestable_db = HarvestableDatabase::load_from_xml(harvestables_path)?;
|
let harvestable_db = HarvestableDatabase::load_from_xml(harvestables_path)?;
|
||||||
info!("✅ Loaded {} harvestables", harvestable_db.len());
|
info!("✅ Loaded {} harvestables", harvestable_db.len());
|
||||||
|
|
||||||
|
let loot_path = "/home/connor/repos/CBAssets/Data/XMLs/Loot/Loot.xml";
|
||||||
|
let loot_db = LootDatabase::load_from_xml(loot_path)?;
|
||||||
|
info!("✅ Loaded {} loot tables", loot_db.len());
|
||||||
|
|
||||||
// Print statistics
|
// Print statistics
|
||||||
info!("\n📊 Game Data Statistics:");
|
info!("\n📊 Game Data Statistics:");
|
||||||
info!(" Items:");
|
info!(" Items:");
|
||||||
@@ -58,6 +62,11 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
info!(" • Mining: {}", harvestable_db.get_by_skill("mining").len());
|
info!(" • Mining: {}", harvestable_db.get_by_skill("mining").len());
|
||||||
info!(" • Fishing: {}", harvestable_db.get_by_skill("Fishing").len());
|
info!(" • Fishing: {}", harvestable_db.get_by_skill("Fishing").len());
|
||||||
info!(" • Alchemy: {}", harvestable_db.get_by_skill("Alchemy").len());
|
info!(" • Alchemy: {}", harvestable_db.get_by_skill("Alchemy").len());
|
||||||
|
info!(" Loot:");
|
||||||
|
info!(" • Total tables: {}", loot_db.len());
|
||||||
|
info!(" • NPCs with loot: {}", loot_db.get_all_npcs_with_loot().len());
|
||||||
|
info!(" • Droppable items: {}", loot_db.get_all_droppable_items().len());
|
||||||
|
info!(" • Tables with conditional drops: {}", loot_db.get_conditional_tables().len());
|
||||||
|
|
||||||
// Initialize Unity project once - scans entire project for GUID mappings
|
// Initialize Unity project once - scans entire project for GUID mappings
|
||||||
let project_root = Path::new("/home/connor/repos/CBAssets");
|
let project_root = Path::new("/home/connor/repos/CBAssets");
|
||||||
|
|||||||
76
cursebreaker-parser/src/types/loot.rs
Normal file
76
cursebreaker-parser/src/types/loot.rs
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct LootTable {
|
||||||
|
// NPC IDs this table applies to (can be multiple, comma-separated in XML)
|
||||||
|
pub npc_ids: Vec<i32>,
|
||||||
|
|
||||||
|
// Optional name/description of the loot table
|
||||||
|
pub name: Option<String>,
|
||||||
|
|
||||||
|
// List of possible drops
|
||||||
|
pub drops: Vec<LootDrop>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct LootDrop {
|
||||||
|
// Item ID that can drop
|
||||||
|
pub item: i32,
|
||||||
|
|
||||||
|
// Drop rate (higher = rarer, e.g., rate=1 means common, rate=100 means very rare)
|
||||||
|
pub rate: Option<i32>,
|
||||||
|
|
||||||
|
// Amount range
|
||||||
|
pub minamount: Option<i32>,
|
||||||
|
pub maxamount: Option<i32>,
|
||||||
|
|
||||||
|
// Optional requirements/checks
|
||||||
|
pub checks: Option<String>,
|
||||||
|
|
||||||
|
// Optional comment/description
|
||||||
|
pub comment: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LootTable {
|
||||||
|
pub fn new(npc_ids: Vec<i32>) -> Self {
|
||||||
|
Self {
|
||||||
|
npc_ids,
|
||||||
|
name: None,
|
||||||
|
drops: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if this loot table applies to a given NPC ID
|
||||||
|
pub fn applies_to_npc(&self, npc_id: i32) -> bool {
|
||||||
|
self.npc_ids.contains(&npc_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all item IDs that can drop from this table
|
||||||
|
pub fn get_drop_item_ids(&self) -> Vec<i32> {
|
||||||
|
self.drops.iter().map(|d| d.item).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get drops that have conditional checks
|
||||||
|
pub fn get_conditional_drops(&self) -> Vec<&LootDrop> {
|
||||||
|
self.drops.iter().filter(|d| d.checks.is_some()).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get guaranteed drops (rate = 1)
|
||||||
|
pub fn get_guaranteed_drops(&self) -> Vec<&LootDrop> {
|
||||||
|
self.drops.iter().filter(|d| d.rate == Some(1)).collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LootDrop {
|
||||||
|
/// Check if this drop has requirements
|
||||||
|
pub fn has_requirements(&self) -> bool {
|
||||||
|
self.checks.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the average drop amount
|
||||||
|
pub fn average_amount(&self) -> f32 {
|
||||||
|
let min = self.minamount.unwrap_or(1) as f32;
|
||||||
|
let max = self.maxamount.unwrap_or(1) as f32;
|
||||||
|
(min + max) / 2.0
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,9 +3,11 @@ mod item;
|
|||||||
mod npc;
|
mod npc;
|
||||||
mod quest;
|
mod quest;
|
||||||
mod harvestable;
|
mod harvestable;
|
||||||
|
mod loot;
|
||||||
|
|
||||||
pub use interactable_resource::InteractableResource;
|
pub use interactable_resource::InteractableResource;
|
||||||
pub use item::{Item, ItemStat, CraftingRecipe, AnimationSet, GenerateRule};
|
pub use item::{Item, ItemStat, CraftingRecipe, AnimationSet, GenerateRule};
|
||||||
pub use npc::{Npc, NpcStat, NpcLevel, RightClick, BarkGroup, Bark, QuestMarker, NpcAnimationSet};
|
pub use npc::{Npc, NpcStat, NpcLevel, RightClick, BarkGroup, Bark, QuestMarker, NpcAnimationSet};
|
||||||
pub use quest::{Quest, QuestPhase, QuestReward};
|
pub use quest::{Quest, QuestPhase, QuestReward};
|
||||||
pub use harvestable::{Harvestable, HarvestableDrop};
|
pub use harvestable::{Harvestable, HarvestableDrop};
|
||||||
|
pub use loot::{LootTable, LootDrop};
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use crate::types::{
|
|||||||
Npc, NpcStat, NpcLevel, RightClick, BarkGroup, QuestMarker, NpcAnimationSet,
|
Npc, NpcStat, NpcLevel, RightClick, BarkGroup, QuestMarker, NpcAnimationSet,
|
||||||
Quest, QuestPhase, QuestReward,
|
Quest, QuestPhase, QuestReward,
|
||||||
Harvestable, HarvestableDrop,
|
Harvestable, HarvestableDrop,
|
||||||
|
LootTable, LootDrop,
|
||||||
};
|
};
|
||||||
use quick_xml::events::Event;
|
use quick_xml::events::Event;
|
||||||
use quick_xml::reader::Reader;
|
use quick_xml::reader::Reader;
|
||||||
@@ -649,3 +650,86 @@ pub fn parse_harvestables_xml<P: AsRef<Path>>(path: P) -> Result<Vec<Harvestable
|
|||||||
|
|
||||||
Ok(harvestables)
|
Ok(harvestables)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Loot Parser
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
pub fn parse_loot_xml<P: AsRef<Path>>(path: P) -> Result<Vec<LootTable>, XmlParseError> {
|
||||||
|
let file = File::open(path)?;
|
||||||
|
let buf_reader = BufReader::new(file);
|
||||||
|
let mut reader = Reader::from_reader(buf_reader);
|
||||||
|
reader.config_mut().trim_text(true);
|
||||||
|
|
||||||
|
let mut loot_tables = Vec::new();
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
let mut current_table: Option<LootTable> = None;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match reader.read_event_into(&mut buf) {
|
||||||
|
Ok(Event::Start(ref e)) | Ok(Event::Empty(ref e)) => {
|
||||||
|
match e.name().as_ref() {
|
||||||
|
b"table" => {
|
||||||
|
let attrs = parse_attributes(e)?;
|
||||||
|
|
||||||
|
// Parse npcid - can be comma-separated like "45,459"
|
||||||
|
let npc_ids = if let Some(npcid_str) = attrs.get("npcid") {
|
||||||
|
npcid_str
|
||||||
|
.split(',')
|
||||||
|
.filter_map(|s| s.trim().parse::<i32>().ok())
|
||||||
|
.collect::<Vec<i32>>()
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut table = LootTable::new(npc_ids);
|
||||||
|
|
||||||
|
// Parse optional name
|
||||||
|
if let Some(v) = attrs.get("name") {
|
||||||
|
table.name = Some(v.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
current_table = Some(table);
|
||||||
|
}
|
||||||
|
b"drop" if current_table.is_some() => {
|
||||||
|
if let Some(ref mut table) = current_table {
|
||||||
|
let attrs = parse_attributes(e)?;
|
||||||
|
|
||||||
|
// Parse item ID (required for a drop)
|
||||||
|
if let Some(item_str) = attrs.get("item") {
|
||||||
|
if let Ok(item) = item_str.parse::<i32>() {
|
||||||
|
let drop = LootDrop {
|
||||||
|
item,
|
||||||
|
rate: attrs.get("rate").and_then(|v| v.parse().ok()),
|
||||||
|
minamount: attrs.get("minamount").and_then(|v| v.parse().ok()),
|
||||||
|
maxamount: attrs.get("maxamount").and_then(|v| v.parse().ok()),
|
||||||
|
checks: attrs.get("checks").cloned(),
|
||||||
|
comment: attrs.get("comment").cloned(),
|
||||||
|
};
|
||||||
|
table.drops.push(drop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Event::End(ref e)) => {
|
||||||
|
match e.name().as_ref() {
|
||||||
|
b"table" => {
|
||||||
|
if let Some(table) = current_table.take() {
|
||||||
|
loot_tables.push(table);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Event::Eof) => break,
|
||||||
|
Err(e) => return Err(XmlParseError::XmlError(e)),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
buf.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(loot_tables)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user