// ================================================================ // 02_state.js — Gather & push town state to the relay server // Depends on: uw, BASE_URL, log, paused (00_config.js / 01_ui.js) // ================================================================ function gatherState() { const towns = uw.ITowns?.towns || {}; const player = uw.Game?.player_name || ''; const player_id = uw.Game?.player_id ?? null; const alliance_id = uw.Game?.alliance_id ?? null; let alliance_name = null; try { const pm = uw.MM.getModels().Player[player_id]; if (pm && pm.attributes) alliance_name = pm.attributes.alliance_name; } catch (e) {} const total_points = uw.Game?.player_points ?? 0; const world = uw.Game?.world_id || ''; const townList = Object.values(towns).map(town => { const res = town.resources(); const buildings = town.buildings()?.attributes ?? {}; const unitsObj = {}; try { const units = town.units(); if (units) { Object.keys(units).forEach(k => { unitsObj[k] = typeof units[k] === 'number' ? units[k] : (units[k]?.getAmount?.() ?? 0); }); } } catch (e) {} let buildQueue = []; try { const bo = town.buildingOrders?.(); if (bo?.models) buildQueue = bo.models.map(m => m.attributes); } catch (e) {} let buildDataMap = {}; try { const buildDataRaw = uw.MM?.getModels?.()?.BuildingBuildData?.[town.id]?.attributes?.building_data || {}; for (const k in buildDataRaw) { buildDataMap[k] = { buildable: buildDataRaw[k].buildable, dependencies: buildDataRaw[k].dependencies_fulfilled !== false, wood: buildDataRaw[k].resources_for?.wood || 0, stone: buildDataRaw[k].resources_for?.stone || 0, iron: buildDataRaw[k].resources_for?.iron || 0, pop: buildDataRaw[k].population_for || 0, build_time: buildDataRaw[k].building_time || '', can_upgrade: !!buildDataRaw[k].can_upgrade, enough_resources: !!buildDataRaw[k].enough_resources, missing_dependencies: buildDataRaw[k].missing_dependencies || [], has_max_level: !!buildDataRaw[k].has_max_level }; } } catch (e) { log(`Failed to gather build data: ${e}`); } // ---- Storage capacity ----------------------------------------------- let storageCapacity = 0; try { storageCapacity = town.getStorageCapacity?.() || 0; if (!storageCapacity) { const storageLevel = buildings.storage ?? 0; const gd = uw.GameData?.buildingData?.storage; storageCapacity = gd?.max_storage?.[storageLevel] || gd?.storage?.[storageLevel] || 0; } if (!storageCapacity) { storageCapacity = res.capacity || res.storage_capacity || res.storage || 0; } } catch (e) { log(`storage capacity lookup failed: ${e}`); } // ---- Market / Trade capacity ----------------------------------------- let marketCapacity = 0; try { const marketLevel = buildings.market ?? 0; const gd = uw.GameData?.buildingData?.market; marketCapacity = gd?.capacity_per_level?.[marketLevel] || 0; if (buildings.trade_office && buildings.trade_office > 0) { marketCapacity += (uw.GameData?.buildingData?.trade_office?.capacity_extra_per_level || 500) * marketLevel; } } catch (e) { log(`market capacity lookup failed: ${e}`); } // ---- Coordinates & sea zone ----------------------------------------- let x = null, y = null, sea = null; try { x = town.getIslandCoordinateX?.() ?? null; y = town.getIslandCoordinateY?.() ?? null; if (typeof x === 'number' && typeof y === 'number') { sea = Math.floor(x / 100) * 10 + Math.floor(y / 100); } } catch (e) {} // ---- Researches ----------------------------------------------------- let researches = {}; try { const r = town.researches?.(); if (r) researches = r.attributes ?? (typeof r === 'object' ? r : {}); } catch (e) { log(`[Debug] town.researches() failed: ${e}`); } // ---- Unit Data (Costs & Dependencies) ------------------------------- let unitDataMap = {}; try { const gdUnits = uw.GameData?.units || {}; for (const u in gdUnits) { if (u === 'militia') continue; const reqBuildings = gdUnits[u].building_dependencies || {}; const reqResearch = gdUnits[u].research_dependencies || []; let missing_deps = {}; for (const reqB in reqBuildings) { if ((buildings[reqB] || 0) < reqBuildings[reqB]) { missing_deps[reqB] = { name: reqB, needed_level: reqBuildings[reqB] }; } } for (const reqR of reqResearch) { if (!researches[reqR]) { missing_deps[reqR] = { name: reqR, needed_level: 'Έρευνα' }; } } const cost = gdUnits[u].resources || {}; const w = cost.wood || 0, s = cost.stone || 0, i = cost.iron || 0; let enough = !(res.wood < w || res.stone < s || res.iron < i); unitDataMap[u] = { wood: w, stone: s, iron: i, pop: gdUnits[u].population || 0, build_time: gdUnits[u].build_time || 0, enough_resources: enough, missing_dependencies: missing_deps }; } } catch (e) { log(`Failed to gather unit data: ${e}`); } // ---- Farm town data ----------------------------------------------- let farms = []; try { const farmCollection = uw.MM.getOnlyCollectionByName('FarmTown'); const relCollection = uw.MM.getOnlyCollectionByName('FarmTownPlayerRelation'); if (farmCollection && relCollection) { const ix = town.getIslandCoordinateX?.(); const iy = town.getIslandCoordinateY?.(); farmCollection.models.forEach(farm => { if (farm.attributes.island_x !== ix || farm.attributes.island_y !== iy) return; relCollection.models.forEach(rel => { if (rel.attributes.farm_town_id === farm.attributes.id && rel.attributes.relation_status >= 1) { farms.push({ farm_town_id: farm.attributes.id, farm_name: farm.attributes.name || '', relation_id: rel.id, relation_status: rel.attributes.relation_status, expansion_stage: rel.attributes.expansion_stage || 0, expansion_at: rel.attributes.expansion_at || 0, lootable_at: rel.attributes.lootable_at || 0 }); } }); }); } } catch (e) {} // ---- Extra town flags ----------------------------------------------- let has_premium = false; try { has_premium = uw.GameDataPremium?.isAdvisorActivated?.('curator') || false; } catch (e) {} return { town_id: town.id, town_name: town.name, x, y, sea, wood: res.wood, stone: res.stone, iron: res.iron, storage: storageCapacity, market_capacity: marketCapacity, population: res.population, points: town.getPoints?.() ?? 0, god: town.god?.() ?? null, buildings, units: unitsObj, buildingOrder: buildQueue, buildData: buildDataMap, unitData: unitDataMap, researches, has_premium, bonuses: {}, wonder_points: 0, alliance_name, farms, }; }); return { player, player_id, alliance_id, total_points, world_id: world, towns: townList }; } function pushState() { if (paused) return; try { const payload = gatherState(); fetch(`${BASE_URL}/api/state`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }) .then(() => log(`State pushed — ${payload.towns.length} towns`)) .catch(e => log(`State push failed: ${e}`)); } catch (e) { log(`gatherState error: ${e}`); } } // ---- AJAX Interceptor for Zero-Delay Push ---------------------------- let pushTimeout = null; function debouncedPushState() { if (paused) return; if (pushTimeout) clearTimeout(pushTimeout); pushTimeout = setTimeout(() => { log('⚡ State change detected (AJAX). Syncing to Remote...'); pushState(); }, 1200); } if (uw.$) { uw.$(document).ajaxComplete(function (e, xhr, opt) { if (!opt || !opt.url) return; if (opt.url.includes(BASE_URL)) return; if (opt.url.includes('map_tiles')) return; if (opt.url.includes('action=') || opt.url.includes('switch_town')) { debouncedPushState(); } }); } // ---- Report command result back to relay ----------------------------- function reportResult(cmdId, status, message) { fetch(`${BASE_URL}/api/commands/${cmdId}/result`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status, message }) }).catch(e => log(`reportResult failed: ${e}`)); }