'use strict'; const chunkPos = require('./chunkPos'); /** * 领土模型:聚合所有邦国的领土状态,并提供全局查询能力。 * * 维护三份数据: * - states:邦国名 → 邦国领土状态(含区块集合、王城、颜色) * - allClaimed:所有邦国已占领区块的并集(判断区块是否已被占用) * - sessionClaims:本次会话新占领的区块(刷新地图时不会被覆盖) * * 区块集合一律用 `chunkPos.key(x, z)` 生成的整数键存进 Set, * 避免数万区块产生海量临时对象。 */ /** API 数据独有邦国在地图上的默认颜色(紫 #FF43DE,ARGB 0xFF43DE) */ const API_DEFAULT_COLOR = 0xff43de; class TerritoryModel { /** * @param {Map, color: number}>} [states] */ constructor(states) { /** @type {Map, color: number}>} */ this.states = new Map(states || []); /** @type {Set} 所有邦国已占领区块的并集 */ this.allClaimed = new Set(); for (const state of this.states.values()) { for (const k of state.chunks) this.allClaimed.add(k); } /** @type {Set} 本次会话新占领的区块 */ this.sessionClaims = new Set(); /** @type {string} 当前选中的己方邦国名称 */ this.selfStateName = ''; /** @type {Map>|null} 飞地缓存 */ this._flyingByState = null; /** @type {Set|null} 飞地并集缓存 */ this._allFlying = null; /** 数据来源时间戳,便于 Web 控制台展示新鲜度 */ this.updatedAt = 0; } /** @returns {TerritoryModel} 空模型(无任何邦国数据) */ static empty() { return new TerritoryModel(new Map()); } /** @returns {IterableIterator<{name: string, capital: {x: number, z: number}|null, chunks: Set, color: number}>} 所有邦国领土状态 */ statesIter() { return this.states.values(); } /** @returns {number} 邦国数量 */ get stateCount() { return this.states.size; } /** * 按名称查询邦国领土状态。 * @param {string} name * @returns {{name: string, capital: {x: number, z: number}|null, chunks: Set, color: number}|undefined} */ state(name) { return this.states.get(name); } /** @returns {Set} 所有邦国已占领区块的并集(勿直接修改) */ allClaimedChunks() { return this.allClaimed; } /** @returns {Set} 本次会话新占领的区块(勿直接修改) */ sessionClaimsSet() { return this.sessionClaims; } /** @returns {string} 当前选中的己方邦国名称 */ selfStateNameValue() { return this.selfStateName; } /** * 设置当前选中的己方邦国名称。 * @param {string|null} name */ setSelfStateName(name) { this.selfStateName = name || ''; } /** @returns {object|null} 己方邦国的领土状态;未设置或不存在时为 null */ selfState() { return this.states.get(this.selfStateName) || null; } /** * 查找包含指定区块的邦国。 * @param {{x: number, z: number}} chunk * @returns {object|null} */ findContainingState(chunk) { const k = chunkPos.key(chunk.x, chunk.z); for (const state of this.states.values()) { if (state.chunks.has(k)) return state; } return null; } /** * 所有邦国的飞地(疏漏区块)并集。 * * 飞地指「四周(四个正方向)都被同一邦国占领、但自身未被占领」的区块, * 通常是自动扩地时漏掉的疏漏,需要在地图上标出并优先填补。 * * @returns {Set} */ flyingChunks() { if (this._allFlying) return this._allFlying; const result = new Set(); for (const chunks of this._flyingByStateMap().values()) { for (const k of chunks) result.add(k); } this._allFlying = result; return result; } /** @returns {Set} 己方邦国的飞地(疏漏区块);未选择邦国时为空集 */ selfFlyingChunks() { return this._flyingByStateMap().get(this.selfStateName) || EMPTY_SET; } /** @returns {Map>} 各邦国飞地区块(懒计算并缓存) */ _flyingByStateMap() { if (this._flyingByState) return this._flyingByState; const map = new Map(); for (const state of this.states.values()) { map.set(state.name, this._computeFlyingChunks(state.chunks)); } this._flyingByState = map; return map; } /** * 计算某邦国的全部飞地区块:在该邦国的外接矩形内, * 找出「未被任何邦国占领、但四个正方向均属于该邦国」的区块。 * @param {Set} own * @returns {Set} */ _computeFlyingChunks(own) { const result = new Set(); if (own.size === 0) return result; let minX = Infinity; let maxX = -Infinity; let minZ = Infinity; let maxZ = -Infinity; for (const k of own) { const { x, z } = chunkPos.decode(k); if (x < minX) minX = x; if (x > maxX) maxX = x; if (z < minZ) minZ = z; if (z > maxZ) maxZ = z; } for (let x = minX; x <= maxX; x++) { for (let z = minZ; z <= maxZ; z++) { const k = chunkPos.key(x, z); if (own.has(k) || this.allClaimed.has(k)) continue; if (own.has(chunkPos.key(x + 1, z)) && own.has(chunkPos.key(x - 1, z)) && own.has(chunkPos.key(x, z + 1)) && own.has(chunkPos.key(x, z - 1))) { result.add(k); } } } return result; } /** * 记录一次会话内新占领的区块:加入会话集合与全局已占领集合, * 并同步更新己方邦国的区块集合。 * @param {{x: number, z: number}} chunk */ addSessionClaim(chunk) { const k = chunkPos.key(chunk.x, chunk.z); this.sessionClaims.add(k); this.allClaimed.add(k); this._invalidateFlying(); const self = this.states.get(this.selfStateName); if (self) { self.chunks.add(k); } } /** 失效飞地缓存 */ _invalidateFlying() { this._flyingByState = null; this._allFlying = null; } /** * 合并 SimpCore 开放 API 的领土数据(在模型发布前、单线程内调用)。 * * 合并原则(「任意一个数据源显示被占领即算占领」): * - API 中出现、Dynmap 中没有的区块 → 新增一个 API 邦国状态(或并入已有同名邦国); * - 两边的区块取并集,颜色以 Dynmap 优先(保持地图观感一致); * - 王城坐标:Dynmap 优先,API 数据兜底。 * * API 数据块本身有服务端缓存(最多延迟 1 小时),因此只做增量补充,绝不覆盖 Dynmap 已确认的数据。 * * @param {Map>} apiChunks API 返回的 邦国名 → 区块键集合 * @param {Map} [apiCapitals] API 返回的 邦国名 → 王城区块 * @param {number} [apiColor] API 独有邦国使用的颜色 */ mergeApiData(apiChunks, apiCapitals, apiColor) { if (!apiChunks || apiChunks.size === 0) return; const color = apiColor || API_DEFAULT_COLOR; const rebuilt = new Map(); for (const [name, chunkKeys] of apiChunks) { if (!name) continue; const apiCapital = apiCapitals ? apiCapitals.get(name) : null; const existing = this.states.get(name); if (!existing) { const chunks = new Set(chunkKeys); let capital = apiCapital; if (!capital) { const first = chunks.values().next(); capital = first.done ? { x: 0, z: 0 } : chunkPos.decode(first.value); } rebuilt.set(name, { name, capital, chunks, color }); } else { const chunks = new Set(existing.chunks); for (const k of chunkKeys) chunks.add(k); let capital = existing.capital; if (apiCapital && (!capital || (capital.x === 0 && capital.z === 0))) { capital = apiCapital; } rebuilt.set(name, { name, capital, chunks, color: existing.color }); } } // 未被 API 覆盖的邦国原样保留 for (const [name, state] of this.states) { if (!rebuilt.has(name)) rebuilt.set(name, state); } this.states = rebuilt; this.allClaimed = new Set(); for (const state of rebuilt.values()) { for (const k of state.chunks) this.allClaimed.add(k); } this._invalidateFlying(); } } /** 复用的空集合,避免重复分配 */ const EMPTY_SET = new Set(); module.exports = { TerritoryModel, API_DEFAULT_COLOR };