Files
game/assets/game-api.js
T
2026-09-10 19:23:18 +08:00

494 lines
19 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 宇森游戏 · 前端与 Odoo 通信层
// 本地调试:前端 8080/8888 -> Odoo 8069(跨域,带调试 CORS 头)
// 生产部署:前端与 Odoo 同域,由 nginx 代理 /game/*。
// 库名说明:本地 Odoo 用 odoo.conf 钉死 yt_game;线上游戏库名为 game(dbfilter 不含 yt_game)。
// game/* 走统一 API_CONFIG.db;yt_world/* 单独带 ?db=,按 WORLD_DB 区分本地/线上,避免 git 同步互相覆盖。
const _isLocalDev = () =>
location.hostname === '127.0.0.1' || location.hostname === 'localhost';
// 本地开发:统一走前端开发服务器(dev-server 默认 8080)的反向代理到 Odoo,
// 这样无论页面由 dev-server 同源提供,还是从本机其它端口(如预览窗)打开,
// API 都能经 8080 代理抵达 Odoo;_json 会回显 Origin 以支持跨域带 cookie。
// 线上(非 localhost)则同源走 nginx,base 为空。
const _isDevPort = (port) =>
location.port === String(port) ||
// 某些环境(如 Python http.server)可能不设 port(默认 80)或用空串
(port === 80 && (location.port === '' || location.port === '80'));
const API_CONFIG = {
base: _isLocalDev()
? ((_isDevPort(8080) || _isDevPort(8888)) ? '' : `http://${location.hostname}:8080`)
: '',
db: _isLocalDev() ? 'study' : 'game',
};
// 世界(yt_world)所在库:本地 Odoo 钉 yt_game;线上游戏库名为 game。按域名自适应,git 同步不冲突。
const WORLD_DB = _isLocalDev() ? 'yt_game' : 'game';
// Odoo 后端基础 URL(世界 API 专用,兼容本地跨域 / 生产同域)
const WORLD_BASE = API_CONFIG.base || '';
const _cred = API_CONFIG.base ? 'include' : 'same-origin';
const _wcred = WORLD_BASE ? 'include' : 'same-origin';
function _url(path) {
return `${API_CONFIG.base}${path}?db=${API_CONFIG.db}`;
}
function _worldUrl(path) {
return `${WORLD_BASE}${path}?db=${WORLD_DB}`;
}
const API = {
async me() {
return (await fetch(_url('/game/api/me'), { credentials: _cred })).json();
},
async login(login, password) {
return (await fetch(_url('/game/api/login'), {
method: 'POST', credentials: _cred,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ login, password }),
})).json();
},
async register(payload) {
return (await fetch(_url('/game/api/register'), {
method: 'POST', credentials: _cred,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
})).json();
},
async logout() {
return (await fetch(_url('/game/api/logout'), {
method: 'POST', credentials: _cred,
})).json();
},
async achievements() {
return (await fetch(_url('/game/api/achievements'), { credentials: _cred })).json();
},
async mails() {
return (await fetch(_url('/game/api/mails'), { credentials: _cred })).json();
},
async mailRead(mailId) {
return (await fetch(_url('/game/api/mail/read'), {
method: 'POST', credentials: _cred,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ mail_id: mailId }),
})).json();
},
async codex() {
return (await fetch(_url('/game/api/codex'), { credentials: _cred })).json();
},
async rules() {
return (await fetch(_url('/game/api/rules'), { credentials: _cred })).json();
},
async currencies() {
return (await fetch(_url('/game/api/currencies'), { credentials: _cred })).json();
},
async pages() {
return (await fetch(_url('/game/api/pages'), { credentials: _cred })).json();
},
async threads() {
return (await fetch(_url('/game/api/threads'), { credentials: _cred })).json();
},
async announcements() {
return (await fetch(_url('/game/api/announcements'), { credentials: _cred })).json();
},
async story() {
return (await fetch(_url('/game/api/story'), { credentials: _cred })).json();
},
async storyChapter(cid) {
return (await fetch(_url('/game/api/story/chapter/' + cid), { credentials: _cred })).json();
},
// ---------- 沙盘记忆(yt_world 模块) ----------
async world() {
return (await fetch(_worldUrl('/yt_world/api/world'), { credentials: _wcred })).json();
},
async saveTiles(tiles) {
return (await fetch(_worldUrl('/yt_world/api/tiles/save'), {
method: 'POST', credentials: _wcred,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tiles }),
})).json();
},
async saveSettings(settings) {
return (await fetch(_worldUrl('/yt_world/api/settings'), {
method: 'POST', credentials: _wcred,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ settings }),
})).json();
},
async renameWorld(name) {
return (await fetch(_worldUrl('/yt_world/api/world/rename'), {
method: 'POST', credentials: _wcred,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name }),
})).json();
},
// 微观世界命名(按地块坐标):命名后由后端持久化到 yt.world.tile.micro.name
async renameMicro(q, r, name) {
try {
const r0 = await fetch(_worldUrl('/yt_world/api/micro/name'), {
method: 'POST', credentials: _wcred,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ q, r, name }),
});
if (r0.redirected || (r0.url && r0.url.indexOf('/web/login') >= 0)) {
return { ok: false, error: 'auth' };
}
const j = await r0.json();
return j && j.ok === false ? j : (j || { ok: true });
} catch (e) {
return { ok: false, error: 'network', detail: String(e && e.message || e) };
}
},
// 微观建造布置保存(按地块坐标)
// 返回结构化结果:{ok:true,...} 或 {ok:false, error:'auth'|'network', detail}
// —— auth='user' 路由未登录会 303 跳转到 /web/login,fetch 跟随后得到 HTML,
// .json() 会抛错;这里捕获并明确区分「未登录」与「网络不可达」。
async saveMicroBuild(q, r, items) {
try {
const r0 = await fetch(_worldUrl('/yt_world/api/micro/build/save'), {
method: 'POST', credentials: _wcred,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ q, r, items }),
});
if (r0.redirected || (r0.url && r0.url.indexOf('/web/login') >= 0)) {
return { ok: false, error: 'auth' };
}
const j = await r0.json();
return j && j.ok === false ? j : (j || { ok: true });
} catch (e) {
return { ok: false, error: 'network', detail: String(e && e.message || e) };
}
},
// 建造扣费(落库前调用):扣成功才允许放置建筑。
// 返回 {ok:true, cost, gold} 或 {ok:false, error:'no_gold'|'auth'|'network', need, have}
async chargeBuild(code, count = 1) {
try {
const r0 = await fetch(_worldUrl('/yt_world/api/micro/build/charge'), {
method: 'POST', credentials: _wcred,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code, count }),
});
if (r0.redirected || (r0.url && r0.url.indexOf('/web/login') >= 0)) {
return { ok: false, error: 'auth' };
}
const j = await r0.json();
return j && j.ok === false ? j : (j || { ok: true });
} catch (e) {
return { ok: false, error: 'network', detail: String(e && e.message || e) };
}
},
// ---------- 世界数据 / 包裹(后端接口预留:yt_world 暂未实现路由,前端 mock 兜底) ----------
async worldData() {
try {
const r = await fetch(_worldUrl('/yt_world/api/data'), { credentials: _wcred });
if (!r.ok) return null;
return await r.json();
} catch (e) { return null; }
},
async inventory() {
try {
const r = await fetch(_worldUrl('/yt_world/api/inventory'), { credentials: _wcred });
if (!r.ok) return null;
return await r.json();
} catch (e) { return null; }
},
// ---------- 游戏化系统:角色 / 生物 / 订单 / 建筑交互 ----------
async char() {
try {
const r = await fetch(_worldUrl('/yt_world/api/char'), { credentials: _wcred });
if (!r.ok) return null;
return await r.json();
} catch (e) { return null; }
},
async saveChar(patch) {
try {
const r = await fetch(_worldUrl('/yt_world/api/char/save'), {
method: 'POST', credentials: _wcred,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(patch || {}),
});
return await r.json();
} catch (e) { return null; }
},
async creatures() {
try {
const r = await fetch(_worldUrl('/yt_world/api/creatures'), { credentials: _wcred });
if (!r.ok) return null;
return await r.json();
} catch (e) { return null; }
},
async saveCreatures(list) {
try {
const r = await fetch(_worldUrl('/yt_world/api/creatures/save'), {
method: 'POST', credentials: _wcred,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ creatures: list || [] }),
});
return await r.json();
} catch (e) { return null; }
},
async races() {
try {
const r = await fetch(_worldUrl('/yt_world/api/races'), { credentials: _wcred });
if (!r.ok) return { ok: false, error: 'http_' + r.status };
const j = await r.json();
return j && j.ok === false ? j : (j || { ok: false, error: 'empty' });
} catch (e) { return { ok: false, error: 'network' }; }
},
async equipCatalog() {
try {
const r = await fetch(_worldUrl('/yt_world/api/equip/catalog'), { credentials: _wcred });
if (!r.ok) return { ok: false, error: 'http_' + r.status };
const j = await r.json();
return j && j.ok === false ? j : (j || { ok: false, error: 'empty' });
} catch (e) { return { ok: false, error: 'network' }; }
},
async creature(cid) {
try {
const r = await fetch(_worldUrl('/yt_world/api/creature/' + encodeURIComponent(cid)), { credentials: _wcred });
if (!r.ok) return { ok: false, error: 'http_' + r.status };
const j = await r.json();
return j && j.ok === false ? j : (j || { ok: false, error: 'empty' });
} catch (e) { return { ok: false, error: 'network' }; }
},
async creatureAction(payload) {
try {
const r = await fetch(_worldUrl('/yt_world/api/creature/action'), {
method: 'POST', credentials: _wcred,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload || {}),
});
const j = await r.json();
return j && j.ok === false ? j : (j || { ok: true });
} catch (e) { return { ok: false, error: 'network' }; }
},
async buildingAction(payload) {
try {
const r = await fetch(_worldUrl('/yt_world/api/building/action'), {
method: 'POST', credentials: _wcred,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload || {}),
});
const j = await r.json();
return j && j.ok === false ? j : (j || { ok: true });
} catch (e) {
return { ok: false, error: 'network', detail: String(e && e.message || e) };
}
},
// ---------- 微观世界角色(玩家放置的种族角色,绑定用户持久化) ----------
// 以下两个已重定向到世界生物表(yt.world.creature),保留旧名以兼容 characters.js
async microChars() {
return this.creatures();
},
async saveMicroChars(chars, deleted) {
try {
const r = await fetch(_worldUrl('/yt_world/api/creatures/save'), {
method: 'POST', credentials: _wcred,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ creatures: chars || [], deleted: deleted || [] }),
});
const j = await r.json();
return j && j.ok === false ? j : (j || { ok: true });
} catch (e) {
return { ok: false, error: 'network', detail: String(e && e.message || e) };
}
},
// 以下旧名重定向到世界生物表(yt.world.creature),保留以兼容历史调用
async microEquipCatalog() { return this.equipCatalog(); },
async microChar(cid) { return this.creature(cid); },
async microCharAction(payload) { return this.creatureAction(payload); },
// ---------- 图鉴(codex) ----------
async codexResources() {
return (await fetch(_worldUrl('/yt_world/api/codex/resources'), { credentials: _wcred })).json();
},
async codexCreatures() {
return (await fetch(_worldUrl('/yt_world/api/codex/creatures'), { credentials: _wcred })).json();
},
async codexMonsters() {
return (await fetch(_worldUrl('/yt_world/api/codex/monsters'), { credentials: _wcred })).json();
},
async codexRaces() {
return (await fetch(_worldUrl('/yt_world/api/codex/races'), { credentials: _wcred })).json();
},
async codexTalents() {
return (await fetch(_worldUrl('/yt_world/api/codex/talents'), { credentials: _wcred })).json();
},
async charTalents() {
const r = await fetch(_worldUrl('/yt_world/api/char/talents'), { credentials: _wcred });
if (!r.ok) return { ok: false, error: 'http_' + r.status };
return await r.json();
},
async activateTalent(code) {
const r = await fetch(_worldUrl('/yt_world/api/char/talent/activate'), {
method: 'POST', credentials: _wcred,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code }),
});
if (r.redirected || (r.url && r.url.indexOf('/web/login') >= 0)) {
return { ok: false, error: 'auth' };
}
return await r.json();
},
async upgradeTalent(code) {
const r = await fetch(_worldUrl('/yt_world/api/char/talent/upgrade'), {
method: 'POST', credentials: _wcred,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code }),
});
if (r.redirected || (r.url && r.url.indexOf('/web/login') >= 0)) {
return { ok: false, error: 'auth' };
}
return await r.json();
},
// ---------- 新手抽天赋 ----------
async talentDraw() {
const r = await fetch(_worldUrl('/yt_world/api/char/talent/draw'), {
method: 'POST', credentials: _wcred,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({}),
});
if (r.redirected || (r.url && r.url.indexOf('/web/login') >= 0)) return { ok: false, error: 'auth' };
return await r.json();
},
async talentDrawActivate(code) {
const r = await fetch(_worldUrl('/yt_world/api/char/talent/draw/activate'), {
method: 'POST', credentials: _wcred,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code }),
});
if (r.redirected || (r.url && r.url.indexOf('/web/login') >= 0)) return { ok: false, error: 'auth' };
return await r.json();
},
// ---------- 新手主线任务 ----------
async tasks() {
const r = await fetch(_worldUrl('/yt_world/api/tasks'), { credentials: _wcred });
if (!r.ok) return { ok: false, error: 'http_' + r.status };
return await r.json();
},
async taskComplete(code) {
try {
const r = await fetch(_worldUrl('/yt_world/api/task/complete'), {
method: 'POST', credentials: _wcred,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code }),
});
if (r.redirected || (r.url && r.url.indexOf('/web/login') >= 0)) {
return { ok: false, error: 'auth' };
}
const j = await r.json();
return j && j.ok === false ? j : (j || { ok: false, error: 'empty' });
} catch (e) {
return { ok: false, error: 'network', detail: String(e && e.message || e) };
}
},
// 发射游戏事件,推进任务进度(服务端事件总线)
async taskProgress(event, payload) {
try {
const r = await fetch(_worldUrl('/yt_world/api/task/progress'), {
method: 'POST', credentials: _wcred,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ event, payload: payload || {} }),
});
if (r.redirected || (r.url && r.url.indexOf('/web/login') >= 0)) {
return { ok: false, error: 'auth' };
}
const j = await r.json();
return j && j.ok === false ? j : (j || { ok: false, error: 'empty' });
} catch (e) {
return { ok: false, error: 'network', detail: String(e && e.message || e) };
}
},
// 暴露后端 base,供图片等静态资源 URL 拼接(本地跨域→8069;线上同域→'')
base: API_CONFIG.base,
// ---------- 资源系统(库存 / 结算 / 离线囤积回收) ----------
async worldResources(q, r) {
try {
const qs = (q !== undefined && r !== undefined) ? ('?q=' + q + '&r=' + r) : '';
const r0 = await fetch(_worldUrl('/yt_world/api/world/resources') + qs, { credentials: _wcred });
if (!r0.ok) return { ok: false, error: 'http_' + r0.status };
return await r0.json();
} catch (e) {
return { ok: false, error: 'network', detail: String(e && e.message || e) };
}
},
async worldSettle(auto_collect = false, force_collect = false) {
try {
const r0 = await fetch(_worldUrl('/yt_world/api/world/settle'), {
method: 'POST', credentials: _wcred,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ auto_collect, force_collect }),
});
if (r0.redirected || (r0.url && r0.url.indexOf('/web/login') >= 0)) {
return { ok: false, error: 'auth' };
}
return await r0.json();
} catch (e) {
return { ok: false, error: 'network', detail: String(e && e.message || e) };
}
},
async collectBuilding(q, r, i, j) {
try {
const r0 = await fetch(_worldUrl('/yt_world/api/building/collect'), {
method: 'POST', credentials: _wcred,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ q, r, i, j }),
});
if (r0.redirected || (r0.url && r0.url.indexOf('/web/login') >= 0)) {
return { ok: false, error: 'auth' };
}
return await r0.json();
} catch (e) {
return { ok: false, error: 'network', detail: String(e && e.message || e) };
}
},
// ---------- 宝箱系统 ----------
async chests(q, r) {
try {
const qs = (q !== undefined && r !== undefined) ? ('&q=' + q + '&r=' + r) : '';
const r0 = await fetch(_worldUrl('/yt_world/api/chests') + qs, { credentials: _wcred });
if (!r0.ok) return { ok: false, error: 'http_' + r0.status };
return await r0.json();
} catch (e) {
return { ok: false, error: 'network', detail: String(e && e.message || e) };
}
},
async chestGenerate(q, r, grid_n = 20) {
try {
const r0 = await fetch(_worldUrl('/yt_world/api/chest/generate'), {
method: 'POST', credentials: _wcred,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ q, r, grid_n: grid_n }),
});
if (r0.redirected || (r0.url && r0.url.indexOf('/web/login') >= 0)) {
return { ok: false, error: 'auth' };
}
const j = await r0.json();
return j && j.ok === false ? j : (j || { ok: true });
} catch (e) {
return { ok: false, error: 'network', detail: String(e && e.message || e) };
}
},
async chestClaim(chest_id, cid) {
try {
const r0 = await fetch(_worldUrl('/yt_world/api/chest/claim'), {
method: 'POST', credentials: _wcred,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ chest_id, cid }),
});
if (r0.redirected || (r0.url && r0.url.indexOf('/web/login') >= 0)) {
return { ok: false, error: 'auth' };
}
const j = await r0.json();
return j && j.ok === false ? j : (j || { ok: true });
} catch (e) {
return { ok: false, error: 'network', detail: String(e && e.message || e) };
}
},
};