major update/remote host the JS and user loader
This commit is contained in:
285
bot_modules/02_state.js
Normal file
285
bot_modules/02_state.js
Normal file
@@ -0,0 +1,285 @@
|
||||
// Push town state to relay
|
||||
// ----------------------------------------------------------------
|
||||
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;
|
||||
console.log("GrepoRemote: Extracted alliance_name =", alliance_name);
|
||||
} catch (e) {
|
||||
console.log("GrepoRemote: Failed to extract alliance_name", 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 -----------------------------------------------
|
||||
// res.storage / res.storage_capacity are always 0 in Grepolis.
|
||||
// Real capacity lives in GameData keyed by the storage building level.
|
||||
let storageCapacity = 0;
|
||||
try {
|
||||
// Strategy 1: dedicated getter (exists on some server versions)
|
||||
storageCapacity = town.getStorageCapacity?.() || 0;
|
||||
|
||||
// Strategy 2: GameData.buildingData.storage.max_storage[level]
|
||||
if (!storageCapacity) {
|
||||
const storageLevel = buildings.storage ?? 0;
|
||||
const gd = uw.GameData?.buildingData?.storage;
|
||||
storageCapacity = gd?.max_storage?.[storageLevel]
|
||||
|| gd?.storage?.[storageLevel]
|
||||
|| 0;
|
||||
}
|
||||
|
||||
// Strategy 3: resource object fallback keys
|
||||
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;
|
||||
|
||||
// Add Trade Office bonus if present
|
||||
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;
|
||||
const s = cost.stone || 0;
|
||||
const i = cost.iron || 0;
|
||||
|
||||
let enough = true;
|
||||
if (res.wood < w || res.stone < s || res.iron < i) enough = false;
|
||||
|
||||
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;
|
||||
let bonuses = {};
|
||||
let wonder_points = 0;
|
||||
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,
|
||||
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); // Wait 1.2s for memory models to update after AJAX finishes
|
||||
}
|
||||
|
||||
if (uw.$) {
|
||||
uw.$(document).ajaxComplete(function (e, xhr, opt) {
|
||||
if (!opt || !opt.url) return;
|
||||
|
||||
// Ignore requests to our own bot server or map data
|
||||
if (opt.url.includes(BASE_URL)) return;
|
||||
if (opt.url.includes('map_tiles')) return;
|
||||
|
||||
// Most game actions use "action=" parameter
|
||||
// Switching towns uses "switch_town"
|
||||
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}`));
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
Reference in New Issue
Block a user