diff --git a/.claude/settings.local.json b/.claude/settings.local.json index fd49278..f4274d0 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -39,7 +39,12 @@ "Bash(DATABASE_URL=../cursebreaker-parser/cursebreaker.db cargo run:*)", "Bash(identify:*)", "Bash(diesel migration revert:*)", - "Bash(xargs:*)" + "Bash(xargs:*)", + "Bash(ss:*)", + "Bash(timeout 10 cargo run:*)", + "Bash(timeout 60 cargo run:*)", + "Bash(DATABASE_URL=../cursebreaker.db diesel print-schema:*)", + "Bash(DATABASE_URL=../cursebreaker.db diesel database:*)" ], "additionalDirectories": [ "/home/connor/repos/CBAssets/" diff --git a/.gitignore b/.gitignore index a9554cd..7b0fb7f 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,4 @@ target/ # Test data (cloned Unity projects for integration tests) test_data/ +cursebreaker.db diff --git a/Cargo.lock b/Cargo.lock index 32e403a..b006622 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -215,6 +215,12 @@ dependencies = [ "tracing", ] +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bit_field" version = "0.10.3" @@ -363,6 +369,7 @@ name = "cursebreaker-map" version = "0.1.0" dependencies = [ "axum", + "base64", "cursebreaker-parser", "diesel", "dotenvy", diff --git a/cursebreaker-map/Cargo.toml b/cursebreaker-map/Cargo.toml index 84638c1..7339866 100644 --- a/cursebreaker-map/Cargo.toml +++ b/cursebreaker-map/Cargo.toml @@ -14,6 +14,7 @@ diesel = { version = "2.1", features = ["sqlite", "returning_clauses_for_sqlite_ dotenvy = "0.15" tracing = "0.1" tracing-subscriber = "0.3" +base64 = "0.22" [dependencies.cursebreaker-parser] path = "../cursebreaker-parser" diff --git a/cursebreaker-map/src/main.rs b/cursebreaker-map/src/main.rs index 271fbec..9381252 100644 --- a/cursebreaker-map/src/main.rs +++ b/cursebreaker-map/src/main.rs @@ -5,6 +5,7 @@ use axum::{ routing::get, Json, Router, }; +use base64::Engine; use diesel::prelude::*; use serde::Serialize; use std::sync::Arc; @@ -27,6 +28,27 @@ struct MapBounds { max_y: i32, } +#[derive(Serialize)] +struct ResourceResponse { + resources: Vec, +} + +#[derive(Serialize)] +struct ResourceGroup { + item_id: i32, + name: String, + skill: String, + level: i32, + icon_base64: String, + positions: Vec, +} + +#[derive(Serialize)] +struct Position { + x: f32, + y: f32, +} + // Establish database connection fn establish_connection(database_url: &str) -> Result { SqliteConnection::establish(database_url) @@ -111,6 +133,74 @@ async fn get_tile( } } +// Get all resources with icons from database +async fn get_resources( + State(state): State>, +) -> Result, StatusCode> { + use cursebreaker_parser::schema::{harvestables, resource_icons, world_resources}; + + let mut conn = establish_connection(&state.database_url).map_err(|e| { + tracing::error!("Database connection error: {}", e); + StatusCode::INTERNAL_SERVER_ERROR + })?; + + // Query with three-way join + let results = world_resources::table + .inner_join( + resource_icons::table.on(world_resources::item_id.eq(resource_icons::item_id)), + ) + .inner_join(harvestables::table.on(resource_icons::item_id.eq(harvestables::id))) + .select(( + resource_icons::item_id, + resource_icons::name, + harvestables::skill, + harvestables::level, + resource_icons::icon_64, + world_resources::pos_x, + world_resources::pos_y, + )) + .order_by((harvestables::skill, harvestables::level)) + .load::<(i32, String, String, i32, Vec, f32, f32)>(&mut conn) + .map_err(|e| { + tracing::error!("Error querying resources: {}", e); + StatusCode::INTERNAL_SERVER_ERROR + })?; + + // Group results by item_id + use std::collections::HashMap; + let mut grouped: HashMap = HashMap::new(); + + for (item_id, name, skill, level, icon_bytes, pos_x, pos_y) in results { + let entry = grouped.entry(item_id).or_insert_with(|| { + // Convert icon to base64 (only once per resource type) + let icon_base64 = base64::engine::general_purpose::STANDARD.encode(&icon_bytes); + + ResourceGroup { + item_id, + name, + skill, + level, + icon_base64, + positions: Vec::new(), + } + }); + + // Add position with multiplier applied + entry.positions.push(Position { + x: pos_x * 5.12, + y: pos_y * 5.12, + }); + } + + // Convert to vec and sort by skill and level + let mut resources: Vec = grouped.into_values().collect(); + resources.sort_by(|a, b| a.skill.cmp(&b.skill).then(a.level.cmp(&b.level))); + + info!("Returning {} resource types", resources.len()); + + Ok(Json(ResourceResponse { resources })) +} + #[tokio::main] async fn main() { // Initialize tracing @@ -128,6 +218,7 @@ async fn main() { let app = Router::new() .route("/api/bounds", get(get_bounds)) .route("/api/tiles/:z/:x/:y", get(get_tile)) + .route("/api/resources", get(get_resources)) .nest_service("/", ServeDir::new("static")) .layer(CorsLayer::permissive()) .with_state(state); diff --git a/cursebreaker-map/static/config.js b/cursebreaker-map/static/config.js index 8e8179b..241c1d2 100644 --- a/cursebreaker-map/static/config.js +++ b/cursebreaker-map/static/config.js @@ -10,7 +10,7 @@ const MapConfig = { // Leaflet zoom 1 → Database zoom 1 (2x2 merged) { leafletZoom: -0.5, dbZoom: 1, mergeFactor: 2, label: "2x2 merged" }, // Leaflet zoom 2+ → Database zoom 2 (original tiles) - { leafletZoom: 1.5, dbZoom: 2, mergeFactor: 1, label: "original" }, + { leafletZoom: 1, dbZoom: 2, mergeFactor: 1, label: "original" }, ], // Leaflet map settings @@ -23,6 +23,9 @@ const MapConfig = { // Debug mode - shows tile boundaries and coordinates debug: true, + // Resource icon configuration + resourceIconSize: 48, // Icon size in pixels (configurable) + // Get zoom configuration for a specific Leaflet zoom level getZoomConfig(leafletZoom) { // Find the appropriate config for this zoom level diff --git a/cursebreaker-map/static/index.html b/cursebreaker-map/static/index.html index 6f205c2..557b3ba 100644 --- a/cursebreaker-map/static/index.html +++ b/cursebreaker-map/static/index.html @@ -15,7 +15,6 @@