Files
game/assets/game-api.js
T
李鹏宇 106d49f6de feat(game): 游戏前后端整合与素材预置备份
后端(Odoo addons):
- game_base: 迁入 关于 功能(game.page/game.page.item/yusen.asset 模型、page_views/asset_views、6条权限、menu_website_about 左侧应用入口),新增 chat 模块(AI世界聊天: chat.py/chat_api.py/chat_data.xml/chat_views.xml)
- yt_world: 合并 game_ai_npc(npc 模型/数据/controller),修复菜单 NotNullViolation
- yusen_website: 移除已迁出的 page/asset,背景菜单改挂 game_base.action_game_page
- 新增 game_asset_backup 纯数据模块: 备份前端素材到 addons(game_assets/),预置 图鉴(game.codex.entry)9条(过场CG/动物) + 素材管理(yusen.asset)120条(地形/建筑/瓦片/幻灯片/背景/路/UI),按类分 tags

前端(assets + HTML):
- 新增 chat.js / game-api.js、chat.html / buildings_default_demo.html
- 清理旧 slides/website 冗余文件,更新 GENERATION_RULES.md 与动物九宫格素材
2026-08-11 10:02:19 +08:00

227 lines
8.6 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();
},
// 微观建造布置保存(按地块坐标)
// 返回结构化结果:{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) };
}
},
// ---------- 世界数据 / 包裹(后端接口预留: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 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) };
}
},
// ---------- 图鉴(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();
},
// 暴露后端 base,供图片等静态资源 URL 拼接(本地跨域→8069;线上同域→'')
base: API_CONFIG.base,
};