DB addition
This commit is contained in:
@@ -39,7 +39,12 @@
|
|||||||
"Bash(DATABASE_URL=../cursebreaker-parser/cursebreaker.db cargo run:*)",
|
"Bash(DATABASE_URL=../cursebreaker-parser/cursebreaker.db cargo run:*)",
|
||||||
"Bash(identify:*)",
|
"Bash(identify:*)",
|
||||||
"Bash(diesel migration revert:*)",
|
"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": [
|
"additionalDirectories": [
|
||||||
"/home/connor/repos/CBAssets/"
|
"/home/connor/repos/CBAssets/"
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -19,3 +19,4 @@ target/
|
|||||||
|
|
||||||
# Test data (cloned Unity projects for integration tests)
|
# Test data (cloned Unity projects for integration tests)
|
||||||
test_data/
|
test_data/
|
||||||
|
cursebreaker.db
|
||||||
|
|||||||
7
Cargo.lock
generated
7
Cargo.lock
generated
@@ -215,6 +215,12 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base64"
|
||||||
|
version = "0.22.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bit_field"
|
name = "bit_field"
|
||||||
version = "0.10.3"
|
version = "0.10.3"
|
||||||
@@ -363,6 +369,7 @@ name = "cursebreaker-map"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"axum",
|
"axum",
|
||||||
|
"base64",
|
||||||
"cursebreaker-parser",
|
"cursebreaker-parser",
|
||||||
"diesel",
|
"diesel",
|
||||||
"dotenvy",
|
"dotenvy",
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ diesel = { version = "2.1", features = ["sqlite", "returning_clauses_for_sqlite_
|
|||||||
dotenvy = "0.15"
|
dotenvy = "0.15"
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = "0.3"
|
tracing-subscriber = "0.3"
|
||||||
|
base64 = "0.22"
|
||||||
|
|
||||||
[dependencies.cursebreaker-parser]
|
[dependencies.cursebreaker-parser]
|
||||||
path = "../cursebreaker-parser"
|
path = "../cursebreaker-parser"
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use axum::{
|
|||||||
routing::get,
|
routing::get,
|
||||||
Json, Router,
|
Json, Router,
|
||||||
};
|
};
|
||||||
|
use base64::Engine;
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -27,6 +28,27 @@ struct MapBounds {
|
|||||||
max_y: i32,
|
max_y: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct ResourceResponse {
|
||||||
|
resources: Vec<ResourceGroup>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct ResourceGroup {
|
||||||
|
item_id: i32,
|
||||||
|
name: String,
|
||||||
|
skill: String,
|
||||||
|
level: i32,
|
||||||
|
icon_base64: String,
|
||||||
|
positions: Vec<Position>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct Position {
|
||||||
|
x: f32,
|
||||||
|
y: f32,
|
||||||
|
}
|
||||||
|
|
||||||
// Establish database connection
|
// Establish database connection
|
||||||
fn establish_connection(database_url: &str) -> Result<DbConnection, diesel::ConnectionError> {
|
fn establish_connection(database_url: &str) -> Result<DbConnection, diesel::ConnectionError> {
|
||||||
SqliteConnection::establish(database_url)
|
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<Arc<AppState>>,
|
||||||
|
) -> Result<Json<ResourceResponse>, 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<u8>, 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<i32, ResourceGroup> = 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<ResourceGroup> = 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]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
// Initialize tracing
|
// Initialize tracing
|
||||||
@@ -128,6 +218,7 @@ async fn main() {
|
|||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.route("/api/bounds", get(get_bounds))
|
.route("/api/bounds", get(get_bounds))
|
||||||
.route("/api/tiles/:z/:x/:y", get(get_tile))
|
.route("/api/tiles/:z/:x/:y", get(get_tile))
|
||||||
|
.route("/api/resources", get(get_resources))
|
||||||
.nest_service("/", ServeDir::new("static"))
|
.nest_service("/", ServeDir::new("static"))
|
||||||
.layer(CorsLayer::permissive())
|
.layer(CorsLayer::permissive())
|
||||||
.with_state(state);
|
.with_state(state);
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const MapConfig = {
|
|||||||
// Leaflet zoom 1 → Database zoom 1 (2x2 merged)
|
// Leaflet zoom 1 → Database zoom 1 (2x2 merged)
|
||||||
{ leafletZoom: -0.5, dbZoom: 1, mergeFactor: 2, label: "2x2 merged" },
|
{ leafletZoom: -0.5, dbZoom: 1, mergeFactor: 2, label: "2x2 merged" },
|
||||||
// Leaflet zoom 2+ → Database zoom 2 (original tiles)
|
// 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
|
// Leaflet map settings
|
||||||
@@ -23,6 +23,9 @@ const MapConfig = {
|
|||||||
// Debug mode - shows tile boundaries and coordinates
|
// Debug mode - shows tile boundaries and coordinates
|
||||||
debug: true,
|
debug: true,
|
||||||
|
|
||||||
|
// Resource icon configuration
|
||||||
|
resourceIconSize: 48, // Icon size in pixels (configurable)
|
||||||
|
|
||||||
// Get zoom configuration for a specific Leaflet zoom level
|
// Get zoom configuration for a specific Leaflet zoom level
|
||||||
getZoomConfig(leafletZoom) {
|
getZoomConfig(leafletZoom) {
|
||||||
// Find the appropriate config for this zoom level
|
// Find the appropriate config for this zoom level
|
||||||
|
|||||||
@@ -15,7 +15,6 @@
|
|||||||
<div id="app">
|
<div id="app">
|
||||||
<!-- Sidebar -->
|
<!-- Sidebar -->
|
||||||
<div id="sidebar" class="sidebar collapsed">
|
<div id="sidebar" class="sidebar collapsed">
|
||||||
<button id="toggle-sidebar" class="toggle-btn">☰</button>
|
|
||||||
<div class="sidebar-content">
|
<div class="sidebar-content">
|
||||||
<h2>Cursebreaker Map</h2>
|
<h2>Cursebreaker Map</h2>
|
||||||
<div class="info-section">
|
<div class="info-section">
|
||||||
@@ -23,15 +22,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="filters-section">
|
<div class="filters-section">
|
||||||
<h3>Filters</h3>
|
<h3>Resources</h3>
|
||||||
<p class="coming-soon">Coming soon: Filter shops, resources, and more</p>
|
<div class="filter-controls">
|
||||||
|
<button id="select-all-resources" class="filter-btn">Show All</button>
|
||||||
<!-- Placeholder for future filters -->
|
<button id="deselect-all-resources" class="filter-btn">Hide All</button>
|
||||||
<div class="filter-group" style="opacity: 0.5; pointer-events: none;">
|
</div>
|
||||||
<label><input type="checkbox" checked disabled> Shops</label>
|
<div id="resource-filters" class="filter-group">
|
||||||
<label><input type="checkbox" checked disabled> Resources</label>
|
<p class="loading-text">Loading resources...</p>
|
||||||
<label><input type="checkbox" checked disabled> Fast Travel</label>
|
|
||||||
<label><input type="checkbox" checked disabled> Workbenches</label>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -61,5 +58,6 @@
|
|||||||
|
|
||||||
<!-- Custom JS -->
|
<!-- Custom JS -->
|
||||||
<script src="map.js"></script>
|
<script src="map.js"></script>
|
||||||
|
<script src="resources.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -86,10 +86,40 @@ async function initMap() {
|
|||||||
L.control.attribution({
|
L.control.attribution({
|
||||||
position: 'bottomright',
|
position: 'bottomright',
|
||||||
prefix: false
|
prefix: false
|
||||||
}).addHTML('The Black Grimoire: Cursebreaker').addTo(map);
|
}).addAttribution('The Black Grimoire: Cursebreaker').addTo(map);
|
||||||
|
|
||||||
|
// Add sidebar toggle control
|
||||||
|
const SidebarControl = L.Control.extend({
|
||||||
|
options: {
|
||||||
|
position: 'topleft'
|
||||||
|
},
|
||||||
|
|
||||||
|
onAdd: function(map) {
|
||||||
|
const container = L.DomUtil.create('div', 'leaflet-bar leaflet-control');
|
||||||
|
const button = L.DomUtil.create('a', 'leaflet-control-sidebar', container);
|
||||||
|
button.innerHTML = '☰';
|
||||||
|
button.href = '#';
|
||||||
|
button.title = 'Toggle Sidebar';
|
||||||
|
|
||||||
|
L.DomEvent.on(button, 'click', function(e) {
|
||||||
|
L.DomEvent.preventDefault(e);
|
||||||
|
const sidebar = document.getElementById('sidebar');
|
||||||
|
sidebar.classList.toggle('collapsed');
|
||||||
|
});
|
||||||
|
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
map.addControl(new SidebarControl());
|
||||||
|
|
||||||
console.log('Map initialized successfully');
|
console.log('Map initialized successfully');
|
||||||
|
|
||||||
|
// Load resources asynchronously
|
||||||
|
loadResources().catch(error => {
|
||||||
|
console.error('Failed to load resources:', error);
|
||||||
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error initializing map:', error);
|
console.error('Error initializing map:', error);
|
||||||
document.getElementById('map-stats').innerHTML =
|
document.getElementById('map-stats').innerHTML =
|
||||||
@@ -236,11 +266,5 @@ function updateMapInfo(bounds) {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Toggle sidebar
|
|
||||||
document.getElementById('toggle-sidebar').addEventListener('click', function() {
|
|
||||||
const sidebar = document.getElementById('sidebar');
|
|
||||||
sidebar.classList.toggle('collapsed');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initialize map when page loads
|
// Initialize map when page loads
|
||||||
window.addEventListener('DOMContentLoaded', initMap);
|
window.addEventListener('DOMContentLoaded', initMap);
|
||||||
|
|||||||
261
cursebreaker-map/static/resources.js
Normal file
261
cursebreaker-map/static/resources.js
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
// Resource management for Cursebreaker map
|
||||||
|
let resourceLayerGroups = {}; // Map: resource name -> L.layerGroup
|
||||||
|
let resourceIcons = {}; // Map: resource name -> L.icon
|
||||||
|
let resourceData = {}; // Map: resource name -> resource metadata (skill, level, etc.)
|
||||||
|
let filterState = {}; // Map: resource name -> boolean (visible)
|
||||||
|
|
||||||
|
// Load resources from API
|
||||||
|
async function loadResources() {
|
||||||
|
try {
|
||||||
|
console.log('Loading resources from API...');
|
||||||
|
const response = await fetch('/api/resources');
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
console.log(`Received ${data.resources.length} resource types`);
|
||||||
|
|
||||||
|
// Create icons and layer groups for each resource
|
||||||
|
for (const group of data.resources) {
|
||||||
|
createResourceGroup(group);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize filter UI
|
||||||
|
initializeFilterUI();
|
||||||
|
|
||||||
|
// Restore filter state from localStorage
|
||||||
|
restoreFilterState();
|
||||||
|
|
||||||
|
console.log(`Loaded ${data.resources.length} resource types successfully`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading resources:', error);
|
||||||
|
const container = document.getElementById('resource-filters');
|
||||||
|
if (container) {
|
||||||
|
container.innerHTML = '<p style="color: #ff6b6b;">Failed to load resources. Check console for details.</p>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create resource group with icon and markers
|
||||||
|
function createResourceGroup(group) {
|
||||||
|
const config = window.MapConfig;
|
||||||
|
|
||||||
|
// Create icon definition (cached per resource type)
|
||||||
|
const iconUrl = `data:image/webp;base64,${group.icon_base64}`;
|
||||||
|
const icon = L.icon({
|
||||||
|
iconUrl: iconUrl,
|
||||||
|
iconSize: [config.resourceIconSize, config.resourceIconSize],
|
||||||
|
iconAnchor: [config.resourceIconSize / 2, config.resourceIconSize / 2],
|
||||||
|
popupAnchor: [0, -(config.resourceIconSize / 2)],
|
||||||
|
});
|
||||||
|
|
||||||
|
resourceIcons[group.name] = icon;
|
||||||
|
|
||||||
|
// Store metadata
|
||||||
|
resourceData[group.name] = {
|
||||||
|
item_id: group.item_id,
|
||||||
|
skill: group.skill,
|
||||||
|
level: group.level,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create layer group for this resource type
|
||||||
|
const layerGroup = L.layerGroup();
|
||||||
|
|
||||||
|
// Add markers for all positions
|
||||||
|
for (const pos of group.positions) {
|
||||||
|
const marker = L.marker([pos.y, pos.x], {
|
||||||
|
icon: icon,
|
||||||
|
title: group.name,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add popup with resource details
|
||||||
|
marker.bindPopup(
|
||||||
|
`<strong>${group.name}</strong><br/>Position: (${pos.x.toFixed(1)}, ${pos.y.toFixed(1)})`
|
||||||
|
);
|
||||||
|
|
||||||
|
marker.addTo(layerGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add to map (initially visible)
|
||||||
|
layerGroup.addTo(map);
|
||||||
|
resourceLayerGroups[group.name] = layerGroup;
|
||||||
|
filterState[group.name] = true; // Initially visible
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize filter UI with skill grouping
|
||||||
|
function initializeFilterUI() {
|
||||||
|
const container = document.getElementById('resource-filters');
|
||||||
|
if (!container) {
|
||||||
|
console.error('resource-filters container not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.innerHTML = ''; // Clear loading text
|
||||||
|
|
||||||
|
// Group resources by skill
|
||||||
|
const skillGroups = {};
|
||||||
|
for (const name in resourceLayerGroups) {
|
||||||
|
const metadata = resourceData[name];
|
||||||
|
if (!skillGroups[metadata.skill]) {
|
||||||
|
skillGroups[metadata.skill] = [];
|
||||||
|
}
|
||||||
|
skillGroups[metadata.skill].push({
|
||||||
|
name: name,
|
||||||
|
level: metadata.level,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort skills alphabetically
|
||||||
|
const sortedSkills = Object.keys(skillGroups).sort();
|
||||||
|
|
||||||
|
// Create UI for each skill group
|
||||||
|
for (const skill of sortedSkills) {
|
||||||
|
const skillDiv = document.createElement('div');
|
||||||
|
skillDiv.className = 'skill-group';
|
||||||
|
|
||||||
|
const header = document.createElement('div');
|
||||||
|
header.className = 'skill-header';
|
||||||
|
// Capitalize first letter of skill
|
||||||
|
header.textContent = skill.charAt(0).toUpperCase() + skill.slice(1);
|
||||||
|
skillDiv.appendChild(header);
|
||||||
|
|
||||||
|
// Resources are already sorted by level in backend, but sort again to be sure
|
||||||
|
skillGroups[skill].sort((a, b) => a.level - b.level);
|
||||||
|
|
||||||
|
// Create checkbox for each resource
|
||||||
|
for (const resource of skillGroups[skill]) {
|
||||||
|
const label = createFilterLabel(resource.name);
|
||||||
|
skillDiv.appendChild(label);
|
||||||
|
}
|
||||||
|
|
||||||
|
container.appendChild(skillDiv);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attach bulk filter handlers
|
||||||
|
const selectAllBtn = document.getElementById('select-all-resources');
|
||||||
|
const deselectAllBtn = document.getElementById('deselect-all-resources');
|
||||||
|
|
||||||
|
if (selectAllBtn) {
|
||||||
|
selectAllBtn.addEventListener('click', () => {
|
||||||
|
setAllFilters(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deselectAllBtn) {
|
||||||
|
deselectAllBtn.addEventListener('click', () => {
|
||||||
|
setAllFilters(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create filter label with checkbox and icon
|
||||||
|
function createFilterLabel(resourceName) {
|
||||||
|
const label = document.createElement('label');
|
||||||
|
label.className = 'filter-label';
|
||||||
|
|
||||||
|
const checkbox = document.createElement('input');
|
||||||
|
checkbox.type = 'checkbox';
|
||||||
|
checkbox.checked = filterState[resourceName];
|
||||||
|
checkbox.dataset.resource = resourceName;
|
||||||
|
checkbox.addEventListener('change', handleFilterChange);
|
||||||
|
|
||||||
|
const icon = document.createElement('img');
|
||||||
|
icon.src = resourceIcons[resourceName].options.iconUrl;
|
||||||
|
icon.className = 'filter-icon';
|
||||||
|
icon.alt = resourceName;
|
||||||
|
|
||||||
|
const text = document.createElement('span');
|
||||||
|
text.textContent = resourceName;
|
||||||
|
|
||||||
|
label.appendChild(checkbox);
|
||||||
|
label.appendChild(icon);
|
||||||
|
label.appendChild(text);
|
||||||
|
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle filter checkbox change
|
||||||
|
function handleFilterChange(event) {
|
||||||
|
const resourceName = event.target.dataset.resource;
|
||||||
|
const isVisible = event.target.checked;
|
||||||
|
|
||||||
|
filterState[resourceName] = isVisible;
|
||||||
|
|
||||||
|
// Show/hide layer group
|
||||||
|
const layerGroup = resourceLayerGroups[resourceName];
|
||||||
|
if (isVisible) {
|
||||||
|
layerGroup.addTo(map);
|
||||||
|
} else {
|
||||||
|
map.removeLayer(layerGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Persist state
|
||||||
|
saveFilterState();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set all filters to visible or hidden
|
||||||
|
function setAllFilters(visible) {
|
||||||
|
for (const name in filterState) {
|
||||||
|
filterState[name] = visible;
|
||||||
|
const layerGroup = resourceLayerGroups[name];
|
||||||
|
|
||||||
|
if (visible) {
|
||||||
|
layerGroup.addTo(map);
|
||||||
|
} else {
|
||||||
|
map.removeLayer(layerGroup);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update checkboxes
|
||||||
|
document.querySelectorAll('#resource-filters input[type="checkbox"]').forEach((cb) => {
|
||||||
|
cb.checked = visible;
|
||||||
|
});
|
||||||
|
|
||||||
|
saveFilterState();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save filter state to localStorage
|
||||||
|
function saveFilterState() {
|
||||||
|
try {
|
||||||
|
localStorage.setItem('cursebreaker_resource_filters', JSON.stringify(filterState));
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to save filter state to localStorage:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore filter state from localStorage
|
||||||
|
function restoreFilterState() {
|
||||||
|
const saved = localStorage.getItem('cursebreaker_resource_filters');
|
||||||
|
if (!saved) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const savedState = JSON.parse(saved);
|
||||||
|
|
||||||
|
for (const name in savedState) {
|
||||||
|
if (resourceLayerGroups[name]) {
|
||||||
|
filterState[name] = savedState[name];
|
||||||
|
|
||||||
|
const layerGroup = resourceLayerGroups[name];
|
||||||
|
if (!savedState[name]) {
|
||||||
|
map.removeLayer(layerGroup);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update checkboxes after UI is created
|
||||||
|
setTimeout(() => {
|
||||||
|
document.querySelectorAll('#resource-filters input[type="checkbox"]').forEach((cb) => {
|
||||||
|
const name = cb.dataset.resource;
|
||||||
|
if (filterState[name] !== undefined) {
|
||||||
|
cb.checked = filterState[name];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
console.log('Restored filter state from localStorage');
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to restore filter state:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,24 +32,17 @@ body {
|
|||||||
margin-left: -320px;
|
margin-left: -320px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toggle-btn {
|
/* Sidebar toggle control */
|
||||||
position: absolute;
|
.leaflet-control-sidebar {
|
||||||
right: -40px;
|
width: 30px;
|
||||||
top: 10px;
|
height: 30px;
|
||||||
width: 40px;
|
display: flex;
|
||||||
height: 40px;
|
align-items: center;
|
||||||
background: #2a2a2a;
|
justify-content: center;
|
||||||
border: none;
|
font-size: 18px;
|
||||||
border-radius: 0 5px 5px 0;
|
line-height: 30px;
|
||||||
color: #e0e0e0;
|
text-align: center;
|
||||||
font-size: 20px;
|
text-decoration: none;
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.2s;
|
|
||||||
z-index: 1001;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toggle-btn:hover {
|
|
||||||
background: #3a3a3a;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-content {
|
.sidebar-content {
|
||||||
@@ -176,6 +169,96 @@ body {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Filter controls */
|
||||||
|
.filter-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-btn {
|
||||||
|
flex: 1;
|
||||||
|
padding: 6px 12px;
|
||||||
|
background: #3a3a3a;
|
||||||
|
color: #e0e0e0;
|
||||||
|
border: 1px solid #4a4a4a;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-btn:hover {
|
||||||
|
background: #4a4a4a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-btn:active {
|
||||||
|
background: #2a2a2a;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Filter groups by skill */
|
||||||
|
.skill-group {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-header {
|
||||||
|
color: #8b5cf6;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
background: rgba(139, 92, 246, 0.1);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Filter items */
|
||||||
|
.filter-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px 8px;
|
||||||
|
margin-left: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background 0.2s;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-label:hover {
|
||||||
|
background: #3a3a3a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-icon {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
image-rendering: pixelated;
|
||||||
|
image-rendering: -moz-crisp-edges;
|
||||||
|
image-rendering: crisp-edges;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-text {
|
||||||
|
color: #a0a0a0;
|
||||||
|
font-style: italic;
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark theme popups */
|
||||||
|
.leaflet-popup-content-wrapper {
|
||||||
|
background: #2a2a2a;
|
||||||
|
color: #e0e0e0;
|
||||||
|
border: 1px solid #3a3a3a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-popup-content {
|
||||||
|
margin: 8px 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-popup-tip {
|
||||||
|
background: #2a2a2a;
|
||||||
|
}
|
||||||
|
|
||||||
/* Responsive */
|
/* Responsive */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.sidebar {
|
.sidebar {
|
||||||
|
|||||||
@@ -42,14 +42,19 @@ The project provides multiple binaries to handle different parsing tasks. This a
|
|||||||
cargo run --bin xml-parser
|
cargo run --bin xml-parser
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **scene-parser** - Parses Unity scenes and extracts world resource locations
|
2. **scene-parser** - Parses Unity scenes and extracts world objects
|
||||||
- Slow execution (Unity project initialization)
|
- Slow execution (Unity project initialization)
|
||||||
- Extracts InteractableResource components and their positions
|
- Extracts multiple types of interactable components and their positions:
|
||||||
- Saves to `world_resources` table (harvestable_id and 2D coordinates)
|
- **InteractableResource**: Harvestable resources → `world_resources` table
|
||||||
|
- **InteractableTeleporter**: Teleporters with source/destination positions → `world_teleporters` table
|
||||||
|
- **InteractableWorkbench**: Workbenches with workbench ID → `world_workbenches` table
|
||||||
|
- **LootSpawner**: Loot spawners with item, amount, respawn time → `world_loot` table
|
||||||
|
- **MapIcon**: Map icons with type, size, text, etc. → `world_map_icons` table
|
||||||
|
- **MapNameChanger**: Map name changers → `world_map_name_changers` table
|
||||||
- Processes item icons for harvestables:
|
- Processes item icons for harvestables:
|
||||||
- Looks up the first item drop for each harvestable from `harvestable_drops` table
|
- Looks up the first item drop for each harvestable from `harvestable_drops` table
|
||||||
- Loads the icon from `Data/Textures/ItemIcons/{item_id}.png`
|
- Loads the icon from `Data/Textures/ItemIcons/{item_id}.png`
|
||||||
- Applies white outline (1px) and resizes to 64x64
|
- Applies white outline (4px) and resizes to 64x64
|
||||||
- Converts to WebP and stores in `resource_icons` table
|
- Converts to WebP and stores in `resource_icons` table
|
||||||
- Run this when scene files change
|
- Run this when scene files change
|
||||||
```bash
|
```bash
|
||||||
@@ -343,6 +348,11 @@ The parser uses Diesel for database operations with SQLite. Database migrations
|
|||||||
- Harvestable resources and drop tables
|
- Harvestable resources and drop tables
|
||||||
- World resource locations from Unity scenes
|
- World resource locations from Unity scenes
|
||||||
- Resource icons for harvestables (64x64 WebP with white borders)
|
- Resource icons for harvestables (64x64 WebP with white borders)
|
||||||
|
- World teleporters with source/destination coordinates
|
||||||
|
- World workbenches with workbench IDs
|
||||||
|
- World loot spawners with item, amount, and respawn time
|
||||||
|
- Map icons with type, size, text, and hover text
|
||||||
|
- Map name changers with location and map name
|
||||||
- Minimap tiles and metadata
|
- Minimap tiles and metadata
|
||||||
- Shop inventories and pricing
|
- Shop inventories and pricing
|
||||||
- Player houses and locations
|
- Player houses and locations
|
||||||
|
|||||||
140
cursebreaker-parser/examples/verify_world_objects.rs
Normal file
140
cursebreaker-parser/examples/verify_world_objects.rs
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
//! Example: Query world objects from the database
|
||||||
|
//!
|
||||||
|
//! Run with: cargo run --example verify_world_objects
|
||||||
|
|
||||||
|
use diesel::prelude::*;
|
||||||
|
use diesel::sqlite::SqliteConnection;
|
||||||
|
use std::env;
|
||||||
|
|
||||||
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
// Connect to database
|
||||||
|
let database_url = env::var("DATABASE_URL").unwrap_or_else(|_| "../cursebreaker.db".to_string());
|
||||||
|
let mut conn = SqliteConnection::establish(&database_url)?;
|
||||||
|
|
||||||
|
// Query teleporters
|
||||||
|
{
|
||||||
|
use cursebreaker_parser::schema::world_teleporters::dsl::*;
|
||||||
|
|
||||||
|
#[derive(Queryable, Debug)]
|
||||||
|
struct Teleporter {
|
||||||
|
pos_x: f32,
|
||||||
|
pos_y: f32,
|
||||||
|
tp_x: Option<f32>,
|
||||||
|
tp_y: Option<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
let results = world_teleporters.load::<Teleporter>(&mut conn)?;
|
||||||
|
println!("=== World Teleporters ===");
|
||||||
|
println!("Found {} teleporters\n", results.len());
|
||||||
|
for tp in results {
|
||||||
|
print!(" At ({:.2}, {:.2})", tp.pos_x, tp.pos_y);
|
||||||
|
if let (Some(tx), Some(ty)) = (tp.tp_x, tp.tp_y) {
|
||||||
|
println!(" -> teleports to ({:.2}, {:.2})", tx, ty);
|
||||||
|
} else {
|
||||||
|
println!(" -> no destination");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query workbenches
|
||||||
|
{
|
||||||
|
use cursebreaker_parser::schema::world_workbenches::dsl::*;
|
||||||
|
|
||||||
|
#[derive(Queryable, Debug)]
|
||||||
|
struct Workbench {
|
||||||
|
pos_x: f32,
|
||||||
|
pos_y: f32,
|
||||||
|
workbench_id: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
let results = world_workbenches.load::<Workbench>(&mut conn)?;
|
||||||
|
println!("=== World Workbenches ===");
|
||||||
|
println!("Found {} workbenches\n", results.len());
|
||||||
|
for wb in results {
|
||||||
|
println!(" Workbench ID {} at ({:.2}, {:.2})", wb.workbench_id, wb.pos_x, wb.pos_y);
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query loot spawners
|
||||||
|
{
|
||||||
|
use cursebreaker_parser::schema::world_loot::dsl::*;
|
||||||
|
|
||||||
|
#[derive(Queryable, Debug)]
|
||||||
|
struct Loot {
|
||||||
|
pos_x: f32,
|
||||||
|
pos_y: f32,
|
||||||
|
item_id: i32,
|
||||||
|
amount: i32,
|
||||||
|
respawn_time: i32,
|
||||||
|
visibility_checks: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
let results = world_loot.load::<Loot>(&mut conn)?;
|
||||||
|
println!("=== World Loot ===");
|
||||||
|
println!("Found {} loot spawners\n", results.len());
|
||||||
|
for loot in results {
|
||||||
|
println!(" Item {} x{} (respawn: {}s) at ({:.2}, {:.2})",
|
||||||
|
loot.item_id, loot.amount, loot.respawn_time, loot.pos_x, loot.pos_y);
|
||||||
|
if !loot.visibility_checks.is_empty() {
|
||||||
|
println!(" Visibility checks: {}", loot.visibility_checks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query map icons
|
||||||
|
{
|
||||||
|
use cursebreaker_parser::schema::world_map_icons::dsl::*;
|
||||||
|
|
||||||
|
#[derive(Queryable, Debug)]
|
||||||
|
struct MapIcon {
|
||||||
|
pos_x: f32,
|
||||||
|
pos_y: f32,
|
||||||
|
icon_type: i32,
|
||||||
|
icon_size: i32,
|
||||||
|
icon: String,
|
||||||
|
text: String,
|
||||||
|
font_size: i32,
|
||||||
|
hover_text: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
let results = world_map_icons.load::<MapIcon>(&mut conn)?;
|
||||||
|
println!("=== World Map Icons ===");
|
||||||
|
println!("Found {} map icons\n", results.len());
|
||||||
|
for map_icon in results {
|
||||||
|
print!(" Type {} at ({:.2}, {:.2})", map_icon.icon_type, map_icon.pos_x, map_icon.pos_y);
|
||||||
|
if !map_icon.text.is_empty() {
|
||||||
|
print!(" - Text: \"{}\"", map_icon.text);
|
||||||
|
}
|
||||||
|
if !map_icon.hover_text.is_empty() {
|
||||||
|
print!(" - Hover: \"{}\"", map_icon.hover_text);
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query map name changers
|
||||||
|
{
|
||||||
|
use cursebreaker_parser::schema::world_map_name_changers::dsl::*;
|
||||||
|
|
||||||
|
#[derive(Queryable, Debug)]
|
||||||
|
struct MapNameChanger {
|
||||||
|
pos_x: f32,
|
||||||
|
pos_y: f32,
|
||||||
|
map_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
let results = world_map_name_changers.load::<MapNameChanger>(&mut conn)?;
|
||||||
|
println!("=== World Map Name Changers ===");
|
||||||
|
println!("Found {} map name changers\n", results.len());
|
||||||
|
for changer in results {
|
||||||
|
println!(" \"{}\" at ({:.2}, {:.2})", changer.map_name, changer.pos_x, changer.pos_y);
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
-- Drop world scene object tables
|
||||||
|
DROP TABLE world_teleporters;
|
||||||
|
DROP TABLE world_workbenches;
|
||||||
|
DROP TABLE world_loot;
|
||||||
|
DROP TABLE world_map_icons;
|
||||||
|
DROP TABLE world_map_name_changers;
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
-- Create world_teleporters table
|
||||||
|
CREATE TABLE world_teleporters (
|
||||||
|
pos_x REAL NOT NULL,
|
||||||
|
pos_y REAL NOT NULL,
|
||||||
|
tp_x REAL,
|
||||||
|
tp_y REAL,
|
||||||
|
PRIMARY KEY (pos_x, pos_y)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create world_workbenches table
|
||||||
|
CREATE TABLE world_workbenches (
|
||||||
|
pos_x REAL NOT NULL,
|
||||||
|
pos_y REAL NOT NULL,
|
||||||
|
workbench_id INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY (pos_x, pos_y)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create world_loot table
|
||||||
|
CREATE TABLE world_loot (
|
||||||
|
pos_x REAL NOT NULL,
|
||||||
|
pos_y REAL NOT NULL,
|
||||||
|
item_id INTEGER NOT NULL,
|
||||||
|
amount INTEGER NOT NULL,
|
||||||
|
respawn_time INTEGER NOT NULL,
|
||||||
|
visibility_checks TEXT NOT NULL DEFAULT '',
|
||||||
|
PRIMARY KEY (pos_x, pos_y)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create world_map_icons table
|
||||||
|
CREATE TABLE world_map_icons (
|
||||||
|
pos_x REAL NOT NULL,
|
||||||
|
pos_y REAL NOT NULL,
|
||||||
|
icon_type INTEGER NOT NULL,
|
||||||
|
icon_size INTEGER NOT NULL,
|
||||||
|
icon TEXT NOT NULL,
|
||||||
|
text TEXT NOT NULL DEFAULT '',
|
||||||
|
font_size INTEGER NOT NULL,
|
||||||
|
hover_text TEXT NOT NULL DEFAULT '',
|
||||||
|
PRIMARY KEY (pos_x, pos_y)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create world_map_name_changers table
|
||||||
|
CREATE TABLE world_map_name_changers (
|
||||||
|
pos_x REAL NOT NULL,
|
||||||
|
pos_y REAL NOT NULL,
|
||||||
|
map_name TEXT NOT NULL,
|
||||||
|
PRIMARY KEY (pos_x, pos_y)
|
||||||
|
);
|
||||||
@@ -20,11 +20,12 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
info!("🎮 Cursebreaker - Image Parser");
|
info!("🎮 Cursebreaker - Image Parser");
|
||||||
info!("Generates all zoom levels (0, 1, 2) with merged tiles\n");
|
info!("Generates all zoom levels (0, 1, 2) with merged tiles");
|
||||||
|
info!("⚠️ Will override existing database entries\n");
|
||||||
|
|
||||||
// Process minimap tiles
|
// Process minimap tiles
|
||||||
info!("🗺️ Processing minimap tiles...");
|
info!("🗺️ Processing minimap tiles...");
|
||||||
let database_url = env::var("DATABASE_URL").unwrap_or_else(|_| "../cursebreaker.db".to_string());
|
let database_url = env::var("DATABASE_URL").unwrap_or_else(|_| "cursebreaker.db".to_string());
|
||||||
let minimap_db = MinimapDatabase::new(database_url);
|
let minimap_db = MinimapDatabase::new(database_url);
|
||||||
|
|
||||||
let cb_assets_path = env::var("CB_ASSETS_PATH")
|
let cb_assets_path = env::var("CB_ASSETS_PATH")
|
||||||
|
|||||||
@@ -8,7 +8,10 @@
|
|||||||
//! - Saving resource locations to the database
|
//! - Saving resource locations to the database
|
||||||
//! - Processing and saving item icons for resources
|
//! - Processing and saving item icons for resources
|
||||||
|
|
||||||
use cursebreaker_parser::{InteractableResource, ImageProcessor, OutlineConfig};
|
use cursebreaker_parser::{
|
||||||
|
InteractableResource, InteractableTeleporter, InteractableWorkbench,
|
||||||
|
LootSpawner, MapIcon, MapNameChanger, ImageProcessor, OutlineConfig
|
||||||
|
};
|
||||||
use unity_parser::{UnityProject, TypeFilter};
|
use unity_parser::{UnityProject, TypeFilter};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use unity_parser::log::DedupLogger;
|
use unity_parser::log::DedupLogger;
|
||||||
@@ -40,7 +43,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
info!(" • Custom MonoBehaviours: InteractableResource");
|
info!(" • Custom MonoBehaviours: InteractableResource");
|
||||||
let type_filter = TypeFilter::new(
|
let type_filter = TypeFilter::new(
|
||||||
vec!["GameObject", "Transform", "PrefabInstance"],
|
vec!["GameObject", "Transform", "PrefabInstance"],
|
||||||
vec!["InteractableResource"]
|
vec!["InteractableResource", "InteractableTeleporter", "InteractableWorkbench", "LootSpawner", "MapIcon", "MapNameChanger"]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Now parse the scene using the pre-built GUID resolvers with filtering
|
// Now parse the scene using the pre-built GUID resolvers with filtering
|
||||||
@@ -104,6 +107,22 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
// Process and save item icons
|
// Process and save item icons
|
||||||
info!("🎨 Processing item icons...");
|
info!("🎨 Processing item icons...");
|
||||||
process_item_icons(&cb_assets_path, &mut conn, &scene)?;
|
process_item_icons(&cb_assets_path, &mut conn, &scene)?;
|
||||||
|
|
||||||
|
// Save other world objects
|
||||||
|
info!("🗺️ Saving teleporters...");
|
||||||
|
save_teleporters(&mut conn, &scene)?;
|
||||||
|
|
||||||
|
info!("🔨 Saving workbenches...");
|
||||||
|
save_workbenches(&mut conn, &scene)?;
|
||||||
|
|
||||||
|
info!("💰 Saving loot spawners...");
|
||||||
|
save_loot_spawners(&mut conn, &scene)?;
|
||||||
|
|
||||||
|
info!("📍 Saving map icons...");
|
||||||
|
save_map_icons(&mut conn, &scene)?;
|
||||||
|
|
||||||
|
info!("🏷️ Saving map name changers...");
|
||||||
|
save_map_name_changers(&mut conn, &scene)?;
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Parse error: {}", e);
|
error!("Parse error: {}", e);
|
||||||
@@ -141,7 +160,7 @@ fn process_item_icons(
|
|||||||
|
|
||||||
// Create image processor with white outline
|
// Create image processor with white outline
|
||||||
let processor = ImageProcessor::default();
|
let processor = ImageProcessor::default();
|
||||||
let outline_config = OutlineConfig::white(1);
|
let outline_config = OutlineConfig::white(4);
|
||||||
|
|
||||||
let mut processed_count = 0;
|
let mut processed_count = 0;
|
||||||
let mut failed_count = 0;
|
let mut failed_count = 0;
|
||||||
@@ -229,3 +248,204 @@ fn process_item_icons(
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Save teleporter data to database
|
||||||
|
fn save_teleporters(
|
||||||
|
conn: &mut SqliteConnection,
|
||||||
|
scene: &unity_parser::UnityScene,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
use cursebreaker_parser::schema::world_teleporters;
|
||||||
|
|
||||||
|
// Clear existing teleporters
|
||||||
|
diesel::delete(world_teleporters::table).execute(conn)?;
|
||||||
|
|
||||||
|
let mut count = 0;
|
||||||
|
|
||||||
|
// Query all teleporters
|
||||||
|
scene.world
|
||||||
|
.query_all::<(&InteractableTeleporter, &unity_parser::WorldTransform, &unity_parser::GameObject)>()
|
||||||
|
.for_each(|(teleporter, transform, object)| {
|
||||||
|
let world_pos = transform.position();
|
||||||
|
|
||||||
|
// Get the tp_transform position if it exists
|
||||||
|
let (tp_x, tp_y) = if let Some(tp_entity) = teleporter.tp_transform {
|
||||||
|
if let Some(tp_transform) = scene.world.borrow::<unity_parser::WorldTransform>().get(tp_entity) {
|
||||||
|
let tp_pos = tp_transform.position();
|
||||||
|
(Some(tp_pos.x as f32), Some(tp_pos.z as f32))
|
||||||
|
} else {
|
||||||
|
(None, None)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
(None, None)
|
||||||
|
};
|
||||||
|
|
||||||
|
info!(" 🗺️ Teleporter: \"{}\" at ({:.2}, {:.2}) -> ({:?}, {:?})",
|
||||||
|
object.name, world_pos.x, world_pos.z, tp_x, tp_y);
|
||||||
|
|
||||||
|
let _ = diesel::insert_into(world_teleporters::table)
|
||||||
|
.values((
|
||||||
|
world_teleporters::pos_x.eq(world_pos.x as f32),
|
||||||
|
world_teleporters::pos_y.eq(world_pos.z as f32),
|
||||||
|
world_teleporters::tp_x.eq(tp_x),
|
||||||
|
world_teleporters::tp_y.eq(tp_y),
|
||||||
|
))
|
||||||
|
.execute(conn);
|
||||||
|
|
||||||
|
count += 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
info!("✅ Saved {} teleporters to database", count);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Save workbench data to database
|
||||||
|
fn save_workbenches(
|
||||||
|
conn: &mut SqliteConnection,
|
||||||
|
scene: &unity_parser::UnityScene,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
use cursebreaker_parser::schema::world_workbenches;
|
||||||
|
|
||||||
|
// Clear existing workbenches
|
||||||
|
diesel::delete(world_workbenches::table).execute(conn)?;
|
||||||
|
|
||||||
|
let mut count = 0;
|
||||||
|
|
||||||
|
// Query all workbenches
|
||||||
|
scene.world
|
||||||
|
.query_all::<(&InteractableWorkbench, &unity_parser::WorldTransform, &unity_parser::GameObject)>()
|
||||||
|
.for_each(|(workbench, transform, object)| {
|
||||||
|
let world_pos = transform.position();
|
||||||
|
|
||||||
|
info!(" 🔨 Workbench: \"{}\" (ID: {}) at ({:.2}, {:.2})",
|
||||||
|
object.name, workbench.workbench_id, world_pos.x, world_pos.z);
|
||||||
|
|
||||||
|
let _ = diesel::insert_into(world_workbenches::table)
|
||||||
|
.values((
|
||||||
|
world_workbenches::pos_x.eq(world_pos.x as f32),
|
||||||
|
world_workbenches::pos_y.eq(world_pos.z as f32),
|
||||||
|
world_workbenches::workbench_id.eq(workbench.workbench_id as i32),
|
||||||
|
))
|
||||||
|
.execute(conn);
|
||||||
|
|
||||||
|
count += 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
info!("✅ Saved {} workbenches to database", count);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Save loot spawner data to database
|
||||||
|
fn save_loot_spawners(
|
||||||
|
conn: &mut SqliteConnection,
|
||||||
|
scene: &unity_parser::UnityScene,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
use cursebreaker_parser::schema::world_loot;
|
||||||
|
|
||||||
|
// Clear existing loot spawners
|
||||||
|
diesel::delete(world_loot::table).execute(conn)?;
|
||||||
|
|
||||||
|
let mut count = 0;
|
||||||
|
|
||||||
|
// Query all loot spawners
|
||||||
|
scene.world
|
||||||
|
.query_all::<(&LootSpawner, &unity_parser::WorldTransform, &unity_parser::GameObject)>()
|
||||||
|
.for_each(|(loot, transform, object)| {
|
||||||
|
let world_pos = transform.position();
|
||||||
|
|
||||||
|
info!(" 💰 Loot: \"{}\" (Item: {}, Amount: {}, Respawn: {}s) at ({:.2}, {:.2})",
|
||||||
|
object.name, loot.item_id, loot.amount, loot.respawn_time, world_pos.x, world_pos.z);
|
||||||
|
|
||||||
|
let _ = diesel::insert_into(world_loot::table)
|
||||||
|
.values((
|
||||||
|
world_loot::pos_x.eq(world_pos.x as f32),
|
||||||
|
world_loot::pos_y.eq(world_pos.z as f32),
|
||||||
|
world_loot::item_id.eq(loot.item_id as i32),
|
||||||
|
world_loot::amount.eq(loot.amount as i32),
|
||||||
|
world_loot::respawn_time.eq(loot.respawn_time as i32),
|
||||||
|
world_loot::visibility_checks.eq(&loot.visibility_checks),
|
||||||
|
))
|
||||||
|
.execute(conn);
|
||||||
|
|
||||||
|
count += 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
info!("✅ Saved {} loot spawners to database", count);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Save map icon data to database
|
||||||
|
fn save_map_icons(
|
||||||
|
conn: &mut SqliteConnection,
|
||||||
|
scene: &unity_parser::UnityScene,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
use cursebreaker_parser::schema::world_map_icons;
|
||||||
|
|
||||||
|
// Clear existing map icons
|
||||||
|
diesel::delete(world_map_icons::table).execute(conn)?;
|
||||||
|
|
||||||
|
let mut count = 0;
|
||||||
|
|
||||||
|
// Query all map icons
|
||||||
|
scene.world
|
||||||
|
.query_all::<(&MapIcon, &unity_parser::WorldTransform, &unity_parser::GameObject)>()
|
||||||
|
.for_each(|(icon, transform, object)| {
|
||||||
|
let world_pos = transform.position();
|
||||||
|
|
||||||
|
info!(" 📍 MapIcon: \"{}\" (Type: {:?}, Text: \"{}\") at ({:.2}, {:.2})",
|
||||||
|
object.name, icon.icon_type, icon.text, world_pos.x, world_pos.z);
|
||||||
|
|
||||||
|
let _ = diesel::insert_into(world_map_icons::table)
|
||||||
|
.values((
|
||||||
|
world_map_icons::pos_x.eq(world_pos.x as f32),
|
||||||
|
world_map_icons::pos_y.eq(world_pos.z as f32),
|
||||||
|
world_map_icons::icon_type.eq(icon.icon_type as i32),
|
||||||
|
world_map_icons::icon_size.eq(icon.icon_size as i32),
|
||||||
|
world_map_icons::icon.eq(&icon.icon),
|
||||||
|
world_map_icons::text.eq(&icon.text),
|
||||||
|
world_map_icons::font_size.eq(icon.font_size as i32),
|
||||||
|
world_map_icons::hover_text.eq(&icon.hover_text),
|
||||||
|
))
|
||||||
|
.execute(conn);
|
||||||
|
|
||||||
|
count += 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
info!("✅ Saved {} map icons to database", count);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Save map name changer data to database
|
||||||
|
fn save_map_name_changers(
|
||||||
|
conn: &mut SqliteConnection,
|
||||||
|
scene: &unity_parser::UnityScene,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
use cursebreaker_parser::schema::world_map_name_changers;
|
||||||
|
|
||||||
|
// Clear existing map name changers
|
||||||
|
diesel::delete(world_map_name_changers::table).execute(conn)?;
|
||||||
|
|
||||||
|
let mut count = 0;
|
||||||
|
|
||||||
|
// Query all map name changers
|
||||||
|
scene.world
|
||||||
|
.query_all::<(&MapNameChanger, &unity_parser::WorldTransform, &unity_parser::GameObject)>()
|
||||||
|
.for_each(|(changer, transform, object)| {
|
||||||
|
let world_pos = transform.position();
|
||||||
|
|
||||||
|
info!(" 🏷️ MapNameChanger: \"{}\" -> \"{}\" at ({:.2}, {:.2})",
|
||||||
|
object.name, changer.map_name, world_pos.x, world_pos.z);
|
||||||
|
|
||||||
|
let _ = diesel::insert_into(world_map_name_changers::table)
|
||||||
|
.values((
|
||||||
|
world_map_name_changers::pos_x.eq(world_pos.x as f32),
|
||||||
|
world_map_name_changers::pos_y.eq(world_pos.z as f32),
|
||||||
|
world_map_name_changers::map_name.eq(&changer.map_name),
|
||||||
|
))
|
||||||
|
.execute(conn);
|
||||||
|
|
||||||
|
count += 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
info!("✅ Saved {} map name changers to database", count);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
use cursebreaker_parser::ItemDatabase;
|
|
||||||
use diesel::prelude::*;
|
|
||||||
use diesel::sqlite::SqliteConnection;
|
|
||||||
use std::env;
|
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
let database_url = env::var("DATABASE_URL").unwrap_or_else(|_| "../cursebreaker.db".to_string());
|
|
||||||
let mut conn = SqliteConnection::establish(&database_url)?;
|
|
||||||
|
|
||||||
let item_db = ItemDatabase::load_from_db(&mut conn)?;
|
|
||||||
|
|
||||||
println!("✅ Database contains {} items", item_db.len());
|
|
||||||
|
|
||||||
if item_db.len() > 0 {
|
|
||||||
println!("\nFirst 5 items:");
|
|
||||||
for (i, item) in item_db.all_items().iter().take(5).enumerate() {
|
|
||||||
println!(" {}. {} (ID: {})", i + 1, item.item_name, item.type_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
use diesel::prelude::*;
|
|
||||||
use diesel::sqlite::SqliteConnection;
|
|
||||||
use std::env;
|
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
let database_url = env::var("DATABASE_URL").unwrap_or_else(|_| "../cursebreaker.db".to_string());
|
|
||||||
let mut conn = SqliteConnection::establish(&database_url)?;
|
|
||||||
|
|
||||||
// Check items with new columns
|
|
||||||
#[derive(Queryable)]
|
|
||||||
struct ItemInfo {
|
|
||||||
id: Option<i32>,
|
|
||||||
name: String,
|
|
||||||
item_type: String,
|
|
||||||
level: i32,
|
|
||||||
price: i32,
|
|
||||||
max_stack: i32,
|
|
||||||
skill: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
use cursebreaker_parser::schema::items::dsl::*;
|
|
||||||
|
|
||||||
let sample_items = items
|
|
||||||
.select((id, name, item_type, level, price, max_stack, skill))
|
|
||||||
.limit(5)
|
|
||||||
.load::<ItemInfo>(&mut conn)?;
|
|
||||||
|
|
||||||
println!("✅ Sample items with expanded columns:\n");
|
|
||||||
for item in sample_items {
|
|
||||||
println!(
|
|
||||||
" {} - {} (Type: {}, Level: {}, Price: {}, MaxStack: {}, Skill: {})",
|
|
||||||
item.id.unwrap_or(0),
|
|
||||||
item.name,
|
|
||||||
item.item_type,
|
|
||||||
item.level,
|
|
||||||
item.price,
|
|
||||||
item.max_stack,
|
|
||||||
item.skill
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check crafting recipes
|
|
||||||
#[derive(Queryable)]
|
|
||||||
struct RecipeCount {
|
|
||||||
count: i64,
|
|
||||||
}
|
|
||||||
|
|
||||||
use diesel::dsl::count_star;
|
|
||||||
use cursebreaker_parser::schema::crafting_recipes;
|
|
||||||
|
|
||||||
let recipe_count = crafting_recipes::table
|
|
||||||
.select(count_star())
|
|
||||||
.first::<i64>(&mut conn)?;
|
|
||||||
|
|
||||||
println!("\n✅ Total crafting recipes: {}", recipe_count);
|
|
||||||
|
|
||||||
// Show sample recipes
|
|
||||||
if recipe_count > 0 {
|
|
||||||
#[derive(Queryable)]
|
|
||||||
struct RecipeInfo {
|
|
||||||
id: Option<i32>,
|
|
||||||
product_item_id: i32,
|
|
||||||
skill: String,
|
|
||||||
level: i32,
|
|
||||||
workbench_id: i32,
|
|
||||||
}
|
|
||||||
|
|
||||||
let sample_recipes = crafting_recipes::table
|
|
||||||
.select((
|
|
||||||
crafting_recipes::id,
|
|
||||||
crafting_recipes::product_item_id,
|
|
||||||
crafting_recipes::skill,
|
|
||||||
crafting_recipes::level,
|
|
||||||
crafting_recipes::workbench_id,
|
|
||||||
))
|
|
||||||
.limit(3)
|
|
||||||
.load::<RecipeInfo>(&mut conn)?;
|
|
||||||
|
|
||||||
println!("\nSample crafting recipes:");
|
|
||||||
for recipe in sample_recipes {
|
|
||||||
// Get product name
|
|
||||||
let product_name: String = items
|
|
||||||
.filter(id.eq(recipe.product_item_id))
|
|
||||||
.select(name)
|
|
||||||
.first(&mut conn)?;
|
|
||||||
|
|
||||||
// Get ingredient count
|
|
||||||
use cursebreaker_parser::schema::crafting_recipe_items;
|
|
||||||
let ingredient_count: i64 = crafting_recipe_items::table
|
|
||||||
.filter(crafting_recipe_items::recipe_id.eq(recipe.id.unwrap()))
|
|
||||||
.select(count_star())
|
|
||||||
.first(&mut conn)?;
|
|
||||||
|
|
||||||
println!(
|
|
||||||
" Recipe #{}: {} (Skill: {}, Level: {}, Workbench: {}, Ingredients: {})",
|
|
||||||
recipe.id.unwrap(),
|
|
||||||
product_name,
|
|
||||||
recipe.skill,
|
|
||||||
recipe.level,
|
|
||||||
recipe.workbench_id,
|
|
||||||
ingredient_count
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
use diesel::prelude::*;
|
|
||||||
use diesel::sqlite::SqliteConnection;
|
|
||||||
use std::env;
|
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
let database_url = env::var("DATABASE_URL").unwrap_or_else(|_| "../cursebreaker.db".to_string());
|
|
||||||
let mut conn = SqliteConnection::establish(&database_url)?;
|
|
||||||
|
|
||||||
// Check items with images
|
|
||||||
#[derive(Queryable)]
|
|
||||||
struct ItemImageInfo {
|
|
||||||
id: Option<i32>,
|
|
||||||
name: String,
|
|
||||||
icon_large: Option<Vec<u8>>,
|
|
||||||
icon_medium: Option<Vec<u8>>,
|
|
||||||
icon_small: Option<Vec<u8>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
use cursebreaker_parser::schema::items::dsl::*;
|
|
||||||
|
|
||||||
// Count items with images
|
|
||||||
let all_items: Vec<ItemImageInfo> = items
|
|
||||||
.select((id, name, icon_large, icon_medium, icon_small))
|
|
||||||
.load(&mut conn)?;
|
|
||||||
|
|
||||||
let items_with_images = all_items.iter().filter(|item| item.icon_large.is_some()).count();
|
|
||||||
let items_without_images = all_items.len() - items_with_images;
|
|
||||||
|
|
||||||
println!("✅ Image Statistics:\n");
|
|
||||||
println!(" Total items: {}", all_items.len());
|
|
||||||
println!(" Items with icons: {}", items_with_images);
|
|
||||||
println!(" Items without icons: {}", items_without_images);
|
|
||||||
|
|
||||||
// Show sample images with sizes
|
|
||||||
println!("\n📸 Sample items with icons:\n");
|
|
||||||
|
|
||||||
for (i, item) in all_items.iter().filter(|item| item.icon_large.is_some()).take(5).enumerate() {
|
|
||||||
let large_size = item.icon_large.as_ref().map(|v| v.len()).unwrap_or(0);
|
|
||||||
let medium_size = item.icon_medium.as_ref().map(|v| v.len()).unwrap_or(0);
|
|
||||||
let small_size = item.icon_small.as_ref().map(|v| v.len()).unwrap_or(0);
|
|
||||||
let total_size = large_size + medium_size + small_size;
|
|
||||||
|
|
||||||
println!(
|
|
||||||
" {}. {} (ID: {})",
|
|
||||||
i + 1,
|
|
||||||
item.name,
|
|
||||||
item.id.unwrap_or(0)
|
|
||||||
);
|
|
||||||
println!(
|
|
||||||
" Large (256px): {:.1} KB | Medium (64px): {:.1} KB | Small (16px): {:.1} KB | Total: {:.1} KB",
|
|
||||||
large_size as f64 / 1024.0,
|
|
||||||
medium_size as f64 / 1024.0,
|
|
||||||
small_size as f64 / 1024.0,
|
|
||||||
total_size as f64 / 1024.0
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate total storage used by images
|
|
||||||
let total_storage: usize = all_items.iter().map(|item| {
|
|
||||||
item.icon_large.as_ref().map(|v| v.len()).unwrap_or(0) +
|
|
||||||
item.icon_medium.as_ref().map(|v| v.len()).unwrap_or(0) +
|
|
||||||
item.icon_small.as_ref().map(|v| v.len()).unwrap_or(0)
|
|
||||||
}).sum();
|
|
||||||
|
|
||||||
println!("\n💾 Total image storage: {:.2} MB", total_storage as f64 / 1024.0 / 1024.0);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
//! Verify resource_icons table
|
|
||||||
|
|
||||||
use diesel::prelude::*;
|
|
||||||
use diesel::sqlite::SqliteConnection;
|
|
||||||
use std::env;
|
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
use cursebreaker_parser::schema::resource_icons;
|
|
||||||
|
|
||||||
let database_url = env::var("DATABASE_URL").unwrap_or_else(|_| "../cursebreaker.db".to_string());
|
|
||||||
let mut conn = SqliteConnection::establish(&database_url)?;
|
|
||||||
|
|
||||||
// Count total icons
|
|
||||||
let count: i64 = resource_icons::table.count().get_result(&mut conn)?;
|
|
||||||
println!("✅ Database contains {} resource icons\n", count);
|
|
||||||
|
|
||||||
// Get all icons and show details
|
|
||||||
#[derive(Queryable, Debug)]
|
|
||||||
struct ResourceIcon {
|
|
||||||
item_id: i32,
|
|
||||||
name: String,
|
|
||||||
icon_64: Vec<u8>,
|
|
||||||
}
|
|
||||||
|
|
||||||
let icons: Vec<ResourceIcon> = resource_icons::table
|
|
||||||
.load(&mut conn)?;
|
|
||||||
|
|
||||||
if icons.is_empty() {
|
|
||||||
println!("ℹ️ No resource icons found in database.");
|
|
||||||
println!(" This is expected if no item icons were available during scene parsing.");
|
|
||||||
} else {
|
|
||||||
println!("Resource icons:");
|
|
||||||
for icon in icons {
|
|
||||||
println!(" • Item {}: {} ({} bytes)",
|
|
||||||
icon.item_id, icon.name, icon.icon_64.len());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
use diesel::prelude::*;
|
|
||||||
use diesel::sqlite::SqliteConnection;
|
|
||||||
use std::env;
|
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
let database_url = env::var("DATABASE_URL").unwrap_or_else(|_| "../cursebreaker.db".to_string());
|
|
||||||
let mut conn = SqliteConnection::establish(&database_url)?;
|
|
||||||
|
|
||||||
// Count total stats
|
|
||||||
use cursebreaker_parser::schema::item_stats::dsl::*;
|
|
||||||
use diesel::dsl::count_star;
|
|
||||||
|
|
||||||
let total_stats: i64 = item_stats.select(count_star()).first(&mut conn)?;
|
|
||||||
|
|
||||||
println!("✅ Item Stats Statistics:\n");
|
|
||||||
println!(" Total stat entries: {}", total_stats);
|
|
||||||
|
|
||||||
// Get stats breakdown by type
|
|
||||||
#[derive(Queryable)]
|
|
||||||
struct StatTypeCount {
|
|
||||||
stat_type: String,
|
|
||||||
count: i64,
|
|
||||||
}
|
|
||||||
|
|
||||||
let stats_by_type: Vec<StatTypeCount> = item_stats
|
|
||||||
.group_by(stat_type)
|
|
||||||
.select((stat_type, count_star()))
|
|
||||||
.order_by(count_star().desc())
|
|
||||||
.load(&mut conn)?;
|
|
||||||
|
|
||||||
println!("\n📊 Stats breakdown by type:\n");
|
|
||||||
for stat in &stats_by_type {
|
|
||||||
println!(" {}: {} items", stat.stat_type, stat.count);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find items with the most stats
|
|
||||||
#[derive(Queryable)]
|
|
||||||
struct ItemStatCount {
|
|
||||||
item_id: i32,
|
|
||||||
stat_count: i64,
|
|
||||||
}
|
|
||||||
|
|
||||||
let items_with_most_stats: Vec<ItemStatCount> = item_stats
|
|
||||||
.group_by(item_id)
|
|
||||||
.select((item_id, count_star()))
|
|
||||||
.order_by(count_star().desc())
|
|
||||||
.limit(5)
|
|
||||||
.load(&mut conn)?;
|
|
||||||
|
|
||||||
println!("\n🏆 Items with most stats:\n");
|
|
||||||
for item_stat in items_with_most_stats {
|
|
||||||
// Get item name
|
|
||||||
use cursebreaker_parser::schema::items;
|
|
||||||
let item_name: String = items::table
|
|
||||||
.filter(items::id.eq(item_stat.item_id))
|
|
||||||
.select(items::name)
|
|
||||||
.first(&mut conn)?;
|
|
||||||
|
|
||||||
println!(" {} (ID: {}) - {} stats", item_name, item_stat.item_id, item_stat.stat_count);
|
|
||||||
|
|
||||||
// Get the actual stats for this item
|
|
||||||
#[derive(Queryable)]
|
|
||||||
struct ItemStatDetail {
|
|
||||||
stat_type: String,
|
|
||||||
value: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
let stats: Vec<ItemStatDetail> = item_stats
|
|
||||||
.filter(item_id.eq(item_stat.item_id))
|
|
||||||
.select((stat_type, value))
|
|
||||||
.load(&mut conn)?;
|
|
||||||
|
|
||||||
for stat in stats {
|
|
||||||
println!(" {}: {}", stat.stat_type, stat.value);
|
|
||||||
}
|
|
||||||
println!();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show some example stat queries
|
|
||||||
println!("📈 Example queries:\n");
|
|
||||||
|
|
||||||
// Items with high physical damage
|
|
||||||
#[derive(Queryable)]
|
|
||||||
struct ItemWithStat {
|
|
||||||
item_id: i32,
|
|
||||||
value: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
let high_damage_items: Vec<ItemWithStat> = item_stats
|
|
||||||
.filter(stat_type.eq("damage_physical"))
|
|
||||||
.filter(value.gt(50.0))
|
|
||||||
.select((item_id, value))
|
|
||||||
.order_by(value.desc())
|
|
||||||
.limit(5)
|
|
||||||
.load(&mut conn)?;
|
|
||||||
|
|
||||||
if !high_damage_items.is_empty() {
|
|
||||||
println!(" Items with Physical Damage > 50:");
|
|
||||||
for item in high_damage_items {
|
|
||||||
use cursebreaker_parser::schema::items;
|
|
||||||
let item_name: String = items::table
|
|
||||||
.filter(items::id.eq(item.item_id))
|
|
||||||
.select(items::name)
|
|
||||||
.first(&mut conn)?;
|
|
||||||
println!(" {} - {:.1} damage", item_name, item.value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Items with health bonuses
|
|
||||||
let health_items: Vec<ItemWithStat> = item_stats
|
|
||||||
.filter(stat_type.eq("health"))
|
|
||||||
.filter(value.gt(0.0))
|
|
||||||
.select((item_id, value))
|
|
||||||
.order_by(value.desc())
|
|
||||||
.limit(5)
|
|
||||||
.load(&mut conn)?;
|
|
||||||
|
|
||||||
if !health_items.is_empty() {
|
|
||||||
println!("\n Items with Health bonuses:");
|
|
||||||
for item in health_items {
|
|
||||||
use cursebreaker_parser::schema::items;
|
|
||||||
let item_name: String = items::table
|
|
||||||
.filter(items::id.eq(item.item_id))
|
|
||||||
.select(items::name)
|
|
||||||
.first(&mut conn)?;
|
|
||||||
println!(" {} - {:.0} health", item_name, item.value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
@@ -89,7 +89,7 @@ impl MinimapDatabase {
|
|||||||
source_path: relative_path.to_str().unwrap_or(""),
|
source_path: relative_path.to_str().unwrap_or(""),
|
||||||
};
|
};
|
||||||
|
|
||||||
diesel::insert_into(minimap_tiles::table)
|
diesel::replace_into(minimap_tiles::table)
|
||||||
.values(&new_tile)
|
.values(&new_tile)
|
||||||
.execute(&mut conn)?;
|
.execute(&mut conn)?;
|
||||||
|
|
||||||
@@ -223,7 +223,7 @@ impl MinimapDatabase {
|
|||||||
source_path: &source_tiles,
|
source_path: &source_tiles,
|
||||||
};
|
};
|
||||||
|
|
||||||
diesel::insert_into(minimap_tiles::table)
|
diesel::replace_into(minimap_tiles::table)
|
||||||
.values(&new_tile)
|
.values(&new_tile)
|
||||||
.execute(conn)?;
|
.execute(conn)?;
|
||||||
|
|
||||||
|
|||||||
@@ -91,6 +91,12 @@ pub use types::{
|
|||||||
MAX_STACK,
|
MAX_STACK,
|
||||||
// Other types
|
// Other types
|
||||||
InteractableResource,
|
InteractableResource,
|
||||||
|
InteractableTeleporter,
|
||||||
|
InteractableWorkbench,
|
||||||
|
LootSpawner,
|
||||||
|
MapIcon,
|
||||||
|
MapIconType,
|
||||||
|
MapNameChanger,
|
||||||
Npc,
|
Npc,
|
||||||
NpcStat,
|
NpcStat,
|
||||||
NpcLevel,
|
NpcLevel,
|
||||||
|
|||||||
@@ -186,6 +186,38 @@ diesel::table! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
world_loot (pos_x, pos_y) {
|
||||||
|
pos_x -> Float,
|
||||||
|
pos_y -> Float,
|
||||||
|
item_id -> Integer,
|
||||||
|
amount -> Integer,
|
||||||
|
respawn_time -> Integer,
|
||||||
|
visibility_checks -> Text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
world_map_icons (pos_x, pos_y) {
|
||||||
|
pos_x -> Float,
|
||||||
|
pos_y -> Float,
|
||||||
|
icon_type -> Integer,
|
||||||
|
icon_size -> Integer,
|
||||||
|
icon -> Text,
|
||||||
|
text -> Text,
|
||||||
|
font_size -> Integer,
|
||||||
|
hover_text -> Text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
world_map_name_changers (pos_x, pos_y) {
|
||||||
|
pos_x -> Float,
|
||||||
|
pos_y -> Float,
|
||||||
|
map_name -> Text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
diesel::table! {
|
diesel::table! {
|
||||||
world_resources (item_id, pos_x, pos_y) {
|
world_resources (item_id, pos_x, pos_y) {
|
||||||
item_id -> Integer,
|
item_id -> Integer,
|
||||||
@@ -194,6 +226,23 @@ diesel::table! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
world_teleporters (pos_x, pos_y) {
|
||||||
|
pos_x -> Float,
|
||||||
|
pos_y -> Float,
|
||||||
|
tp_x -> Nullable<Float>,
|
||||||
|
tp_y -> Nullable<Float>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
world_workbenches (pos_x, pos_y) {
|
||||||
|
pos_x -> Float,
|
||||||
|
pos_y -> Float,
|
||||||
|
workbench_id -> Integer,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
diesel::joinable!(crafting_recipe_items -> crafting_recipes (recipe_id));
|
diesel::joinable!(crafting_recipe_items -> crafting_recipes (recipe_id));
|
||||||
diesel::joinable!(crafting_recipe_items -> items (item_id));
|
diesel::joinable!(crafting_recipe_items -> items (item_id));
|
||||||
diesel::joinable!(crafting_recipes -> items (product_item_id));
|
diesel::joinable!(crafting_recipes -> items (product_item_id));
|
||||||
@@ -218,5 +267,10 @@ diesel::allow_tables_to_appear_in_same_query!(
|
|||||||
resource_icons,
|
resource_icons,
|
||||||
shops,
|
shops,
|
||||||
traits,
|
traits,
|
||||||
|
world_loot,
|
||||||
|
world_map_icons,
|
||||||
|
world_map_name_changers,
|
||||||
world_resources,
|
world_resources,
|
||||||
|
world_teleporters,
|
||||||
|
world_workbenches,
|
||||||
);
|
);
|
||||||
|
|||||||
30
settings.json
Normal file
30
settings.json
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"ssh_connections": [
|
||||||
|
{
|
||||||
|
"host": "192.168.11.7",
|
||||||
|
"username": "connor",
|
||||||
|
"args": [],
|
||||||
|
"projects": [
|
||||||
|
{
|
||||||
|
"paths": ["/home/connor/repos/cursebreaker-parser-rust"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nickname": "connor-mini"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ui_font_size": 16,
|
||||||
|
"buffer_font_size": 15,
|
||||||
|
"theme": {
|
||||||
|
"mode": "system",
|
||||||
|
"light": "One Light",
|
||||||
|
"dark": "One Dark"
|
||||||
|
},
|
||||||
|
"agent_servers": {
|
||||||
|
"Grok Build Agent": {
|
||||||
|
"type": "custom",
|
||||||
|
"command": "/usr/bin/grok",
|
||||||
|
"args": ["agent", "--yolo", "stdio"],
|
||||||
|
"env": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user