官网首页:去掉 YUSEN 小标、右上角登录/注册、进入游戏走登录闸、补 favicon;补充 game_base 菜单图标与 yt_world 沙盘记忆模块
This commit is contained in:
parent
63449893ca
commit
3d34fbc7cc
@ -1,8 +1,9 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<odoo>
|
<odoo>
|
||||||
|
|
||||||
<!-- ============ 顶层菜单 ============ -->
|
<!-- ============ 顶层菜单(带品牌 logo) ============ -->
|
||||||
<menuitem id="menu_game_root" name="宇森游戏" sequence="10"/>
|
<menuitem id="menu_game_root" name="宇森游戏" sequence="10"
|
||||||
|
web_icon="game_base,static/description/icon.png"/>
|
||||||
|
|
||||||
<!-- ============ 图鉴(按分类 + 子级分组) ============ -->
|
<!-- ============ 图鉴(按分类 + 子级分组) ============ -->
|
||||||
<record id="action_game_codex" model="ir.actions.act_window">
|
<record id="action_game_codex" model="ir.actions.act_window">
|
||||||
|
|||||||
2
addons/yt_world/__init__.py
Normal file
2
addons/yt_world/__init__.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
from . import models
|
||||||
|
from . import controllers
|
||||||
14
addons/yt_world/__manifest__.py
Normal file
14
addons/yt_world/__manifest__.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
'name': '宇森世界 - 沙盘与微观世界',
|
||||||
|
'version': '0.2',
|
||||||
|
'summary': '地表存储(沙盘记忆) + 微观世界(1:1预留) + 资源(框架)',
|
||||||
|
'description': '每个玩家拥有一个确定性生成的世界;仅持久化玩家改过的地表地块(种子+增量覆盖)。微观世界独立为 1:1 模型仅预留,资源模型为框架。',
|
||||||
|
'category': 'Game',
|
||||||
|
'depends': ['base'],
|
||||||
|
'data': [
|
||||||
|
'security/ir.model.access.csv',
|
||||||
|
'views/yt_world_views.xml',
|
||||||
|
],
|
||||||
|
'application': True,
|
||||||
|
'license': 'AGPL-3',
|
||||||
|
}
|
||||||
1
addons/yt_world/controllers/__init__.py
Normal file
1
addons/yt_world/controllers/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from . import world_api
|
||||||
61
addons/yt_world/controllers/world_api.py
Normal file
61
addons/yt_world/controllers/world_api.py
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import json
|
||||||
|
from odoo import http
|
||||||
|
from odoo.http import request, Response
|
||||||
|
|
||||||
|
|
||||||
|
def _json(data):
|
||||||
|
"""返回 UTF-8 JSON 响应(与 game_base 一致)。"""
|
||||||
|
return Response(
|
||||||
|
json.dumps(data, ensure_ascii=False),
|
||||||
|
content_type='application/json; charset=utf-8',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _post_body():
|
||||||
|
try:
|
||||||
|
raw = request.httprequest.get_data(as_text=True) or '{}'
|
||||||
|
return json.loads(raw)
|
||||||
|
except Exception:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
class YtWorldApi(http.Controller):
|
||||||
|
"""前端沙盘记忆接口(JSON over HTTP)。auth=user:需已登录 Odoo。
|
||||||
|
前端统一带 ?db=game 且 credentials:'same-origin',故路由须为 type='http'
|
||||||
|
并以 _json 返回原始 JSON(与 game-api.js 的 fetch().json() 对齐)。"""
|
||||||
|
|
||||||
|
@http.route('/yt_world/api/world', type='http', auth='user',
|
||||||
|
methods=['GET', 'OPTIONS'], csrf=False)
|
||||||
|
def world(self, **kw):
|
||||||
|
"""取或建当前用户世界,导出 seed + 地块覆盖。"""
|
||||||
|
user = request.env.user
|
||||||
|
rec = request.env['yt.world'].sudo().get_or_create_for_user(user)
|
||||||
|
return _json(rec.export_state())
|
||||||
|
|
||||||
|
@http.route('/yt_world/api/tiles/save', type='http', auth='user',
|
||||||
|
methods=['POST', 'OPTIONS'], csrf=False)
|
||||||
|
def save_tiles(self, **kw):
|
||||||
|
"""增量覆盖保存地块。body: {"tiles": [...]}"""
|
||||||
|
user = request.env.user
|
||||||
|
world = request.env['yt.world'].sudo().search(
|
||||||
|
[('user_id', '=', user.id)], limit=1)
|
||||||
|
if not world:
|
||||||
|
return _json({'ok': False, 'error': 'no_world'})
|
||||||
|
body = _post_body()
|
||||||
|
tiles = body.get('tiles') or []
|
||||||
|
world.import_tiles(tiles)
|
||||||
|
return _json({'ok': True, 'saved': len(tiles)})
|
||||||
|
|
||||||
|
@http.route('/yt_world/api/settings', type='http', auth='user',
|
||||||
|
methods=['POST', 'OPTIONS'], csrf=False)
|
||||||
|
def save_settings(self, **kw):
|
||||||
|
"""保存相机/UI 偏好。body: {"settings": {...}}"""
|
||||||
|
user = request.env.user
|
||||||
|
world = request.env['yt.world'].sudo().search(
|
||||||
|
[('user_id', '=', user.id)], limit=1)
|
||||||
|
body = _post_body()
|
||||||
|
settings = body.get('settings')
|
||||||
|
if world and settings is not None:
|
||||||
|
world.settings = settings
|
||||||
|
return _json({'ok': True})
|
||||||
4
addons/yt_world/models/__init__.py
Normal file
4
addons/yt_world/models/__init__.py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
from . import yt_world
|
||||||
|
from . import yt_world_tile
|
||||||
|
from . import yt_world_tile_micro
|
||||||
|
from . import yt_resource
|
||||||
15
addons/yt_world/models/constants.py
Normal file
15
addons/yt_world/models/constants.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
TERRAIN_SELECTION = [
|
||||||
|
('water', '水域'),
|
||||||
|
('desert', '荒漠'),
|
||||||
|
('plain', '平原'),
|
||||||
|
('forest', '森林'),
|
||||||
|
('mountain', '山脉'),
|
||||||
|
('snow', '雪地'),
|
||||||
|
]
|
||||||
|
|
||||||
|
RESOURCE_TYPE_SELECTION = [
|
||||||
|
('wood', '木材'),
|
||||||
|
('food', '食物'),
|
||||||
|
('stone', '石料'),
|
||||||
|
('fish', '鱼获'),
|
||||||
|
]
|
||||||
20
addons/yt_world/models/yt_resource.py
Normal file
20
addons/yt_world/models/yt_resource.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from odoo import models, fields
|
||||||
|
from .constants import RESOURCE_TYPE_SELECTION
|
||||||
|
|
||||||
|
|
||||||
|
class YtResource(models.Model):
|
||||||
|
"""资源(框架模型)。本期仅结构,不接入玩法产出/消耗。"""
|
||||||
|
_name = 'yt.resource'
|
||||||
|
_description = '资源(框架)'
|
||||||
|
|
||||||
|
tile_id = fields.Many2one('yt.world.tile', string='所属地块', ondelete='cascade')
|
||||||
|
micro_id = fields.Many2one('yt.world.tile.micro', string='所属微观世界', ondelete='cascade')
|
||||||
|
type = fields.Selection(RESOURCE_TYPE_SELECTION, string='资源类型')
|
||||||
|
amount = fields.Float(string='数量', default=0.0)
|
||||||
|
capacity = fields.Float(string='容量上限', default=100.0) # [PLACEHOLDER] 调参
|
||||||
|
|
||||||
|
_sql_constraints = [
|
||||||
|
('uniq_res_tile', 'unique(tile_id, type)',
|
||||||
|
'同地块同类型资源唯一'),
|
||||||
|
]
|
||||||
88
addons/yt_world/models/yt_world.py
Normal file
88
addons/yt_world/models/yt_world.py
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from odoo import models, fields
|
||||||
|
import random
|
||||||
|
|
||||||
|
|
||||||
|
class YtWorld(models.Model):
|
||||||
|
"""玩家世界:每个用户一条。seed 再生基础地图,tile_ids 存被改地块。"""
|
||||||
|
_name = 'yt.world'
|
||||||
|
_description = '玩家世界'
|
||||||
|
|
||||||
|
name = fields.Char(string='世界名称', translate=True)
|
||||||
|
user_id = fields.Many2one('res.users', string='拥有者',
|
||||||
|
required=True, ondelete='cascade', index=True)
|
||||||
|
seed = fields.Integer(string='生成种子', required=True, index=True)
|
||||||
|
world_version = fields.Integer(string='生成算法版本', default=1)
|
||||||
|
settings = fields.Json(string='世界设置') # 相机位/缩放/UI 偏好
|
||||||
|
tile_ids = fields.One2many('yt.world.tile', 'world_id', string='地表地块')
|
||||||
|
created_at = fields.Datetime(string='创建时间', default=fields.Datetime.now)
|
||||||
|
last_active = fields.Datetime(string='最后活跃')
|
||||||
|
|
||||||
|
# ---------- 接口方法(供 controller 调用) ----------
|
||||||
|
|
||||||
|
def get_or_create_for_user(self, user):
|
||||||
|
"""取当前用户的世界;没有则按随机 seed 新建。"""
|
||||||
|
rec = self.search([('user_id', '=', user.id)], limit=1)
|
||||||
|
if not rec:
|
||||||
|
rec = self.create({
|
||||||
|
'user_id': user.id,
|
||||||
|
'seed': random.randint(1, 2_000_000_000),
|
||||||
|
'name': '我的世界',
|
||||||
|
})
|
||||||
|
rec.last_active = fields.Datetime.now()
|
||||||
|
return rec
|
||||||
|
|
||||||
|
def export_state(self):
|
||||||
|
"""导出前端需要的世界状态:seed + 地块覆盖列表。"""
|
||||||
|
self.ensure_one()
|
||||||
|
tiles = []
|
||||||
|
for t in self.tile_ids:
|
||||||
|
tiles.append({
|
||||||
|
'q': t.q,
|
||||||
|
'r': t.r,
|
||||||
|
'terrain': t.terrain or None,
|
||||||
|
'height': t.height or None,
|
||||||
|
'flags': t.flags or {},
|
||||||
|
'micro_world_id': t.micro_world_id.id if t.micro_world_id else None,
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
'world_id': self.id,
|
||||||
|
'seed': self.seed,
|
||||||
|
'world_version': self.world_version,
|
||||||
|
'settings': self.settings or {},
|
||||||
|
'tiles': tiles,
|
||||||
|
}
|
||||||
|
|
||||||
|
def import_tiles(self, tiles):
|
||||||
|
"""增量覆盖:upsert 地块。terrain/height/flags 全回退底图则删除覆盖行。"""
|
||||||
|
self.ensure_one()
|
||||||
|
tile_model = self.env['yt.world.tile']
|
||||||
|
for tv in (tiles or []):
|
||||||
|
q, r = tv.get('q'), tv.get('r')
|
||||||
|
if q is None or r is None:
|
||||||
|
continue
|
||||||
|
terrain = tv.get('terrain')
|
||||||
|
height = tv.get('height')
|
||||||
|
flags = tv.get('flags')
|
||||||
|
# 全部回退底图 → 删覆盖行,保持最小集(种子+增量覆盖原则)
|
||||||
|
if not terrain and height is None and not (flags or {}):
|
||||||
|
existing = tile_model.search([
|
||||||
|
('world_id', '=', self.id), ('q', '=', q), ('r', '=', r)])
|
||||||
|
if existing:
|
||||||
|
existing.unlink()
|
||||||
|
continue
|
||||||
|
vals = {}
|
||||||
|
if terrain is not None:
|
||||||
|
vals['terrain'] = terrain
|
||||||
|
if height is not None:
|
||||||
|
vals['height'] = height
|
||||||
|
if flags is not None:
|
||||||
|
vals['flags'] = flags
|
||||||
|
existing = tile_model.search([
|
||||||
|
('world_id', '=', self.id), ('q', '=', q), ('r', '=', r)])
|
||||||
|
if existing:
|
||||||
|
existing.write(vals)
|
||||||
|
else:
|
||||||
|
vals.update({'q': q, 'r': r, 'world_id': self.id})
|
||||||
|
tile_model.create(vals) # create 钩子自动建微观占位
|
||||||
|
self.last_active = fields.Datetime.now()
|
||||||
52
addons/yt_world/models/yt_world_tile.py
Normal file
52
addons/yt_world/models/yt_world_tile.py
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from odoo import models, fields, api
|
||||||
|
from .constants import TERRAIN_SELECTION
|
||||||
|
|
||||||
|
|
||||||
|
class YtWorldTile(models.Model):
|
||||||
|
"""地表地块:本期实际持久化内容(坐标 + 高度 + 属性 + 微观世界关联位)。"""
|
||||||
|
_name = 'yt.world.tile'
|
||||||
|
_description = '地表地块'
|
||||||
|
_rec_name = 'coord'
|
||||||
|
|
||||||
|
world_id = fields.Many2one('yt.world', string='所属世界',
|
||||||
|
required=True, ondelete='cascade')
|
||||||
|
q = fields.Integer(string='轴向坐标Q', required=True)
|
||||||
|
r = fields.Integer(string='轴向坐标R', required=True)
|
||||||
|
terrain = fields.Selection(TERRAIN_SELECTION, string='地形', index=True)
|
||||||
|
height = fields.Integer(string='高度等级') # heightLevel 1-12
|
||||||
|
flags = fields.Json(string='地表标记') # {claimed, decorated}
|
||||||
|
micro_world_id = fields.Many2one('yt.world.tile.micro',
|
||||||
|
string='微观世界', ondelete='set null')
|
||||||
|
updated_at = fields.Datetime(string='更新时间', default=fields.Datetime.now)
|
||||||
|
coord = fields.Char(string='坐标标识', compute='_compute_coord', store=True)
|
||||||
|
|
||||||
|
_sql_constraints = [
|
||||||
|
('uniq_tile', 'unique(world_id, q, r)',
|
||||||
|
'同一世界内 (坐标) 的地块必须唯一'),
|
||||||
|
]
|
||||||
|
|
||||||
|
@api.depends('q', 'r')
|
||||||
|
def _compute_coord(self):
|
||||||
|
for t in self:
|
||||||
|
t.coord = "%s,%s" % (t.q, t.r)
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def create(self, vals):
|
||||||
|
rec = super(YtWorldTile, self).create(vals)
|
||||||
|
rec._ensure_micro()
|
||||||
|
return rec
|
||||||
|
|
||||||
|
def _ensure_micro(self):
|
||||||
|
"""新建地块时同步建 1:1 微观世界占位。"""
|
||||||
|
for t in self:
|
||||||
|
if not t.micro_world_id:
|
||||||
|
m = self.env['yt.world.tile.micro'].create({'tile_id': t.id})
|
||||||
|
t.micro_world_id = m
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def write(self, vals):
|
||||||
|
# 任何编辑都刷新 updated_at
|
||||||
|
if 'updated_at' not in vals:
|
||||||
|
vals['updated_at'] = fields.Datetime.now()
|
||||||
|
return super(YtWorldTile, self).write(vals)
|
||||||
22
addons/yt_world/models/yt_world_tile_micro.py
Normal file
22
addons/yt_world/models/yt_world_tile_micro.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from odoo import models, fields
|
||||||
|
|
||||||
|
|
||||||
|
class YtWorldTileMicro(models.Model):
|
||||||
|
"""微观世界(预留 1:1 模型)。当前仅骨架,内容与资源后续开发。"""
|
||||||
|
_name = 'yt.world.tile.micro'
|
||||||
|
_description = '微观世界(预留)'
|
||||||
|
|
||||||
|
tile_id = fields.Many2one('yt.world.tile', string='所属地块',
|
||||||
|
required=True, ondelete='cascade')
|
||||||
|
name = fields.Char(string='微观世界名称', translate=True)
|
||||||
|
# ===== 预留字段(后续放开,本期不实现) =====
|
||||||
|
# people = fields.Json(string='居民') # [{id, name, job, mood, x, y}]
|
||||||
|
# buildings = fields.Json(string='建筑') # [{type, level, x, y, rot}]
|
||||||
|
# resources = fields.Json(string='资源') # {wood, food, stone, fish}
|
||||||
|
# ===========================================
|
||||||
|
|
||||||
|
_sql_constraints = [
|
||||||
|
('uniq_micro', 'unique(tile_id)',
|
||||||
|
'一个地块只能有一个微观世界(一对一)'),
|
||||||
|
]
|
||||||
5
addons/yt_world/security/ir.model.access.csv
Normal file
5
addons/yt_world/security/ir.model.access.csv
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||||
|
access_yt_world,yt.world,model_yt_world,base.group_user,1,1,1,1
|
||||||
|
access_yt_world_tile,yt.world.tile,model_yt_world_tile,base.group_user,1,1,1,1
|
||||||
|
access_yt_world_tile_micro,yt.world.tile.micro,model_yt_world_tile_micro,base.group_user,1,1,1,1
|
||||||
|
access_yt_resource,yt.resource,model_yt_resource,base.group_user,1,1,1,1
|
||||||
|
BIN
addons/yt_world/static/description/icon.png
Normal file
BIN
addons/yt_world/static/description/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 89 KiB |
111
addons/yt_world/views/yt_world_views.xml
Normal file
111
addons/yt_world/views/yt_world_views.xml
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<!-- ============ 顶层菜单(带品牌 logo,直接打开玩家世界列表) ============ -->
|
||||||
|
<menuitem id="menu_yt_world_root"
|
||||||
|
name="宇森世界"
|
||||||
|
sequence="20"
|
||||||
|
action="action_yt_world"
|
||||||
|
web_icon="yt_world,static/description/icon.png"/>
|
||||||
|
|
||||||
|
<!-- ============ 玩家世界 ============ -->
|
||||||
|
<record id="action_yt_world" model="ir.actions.act_window">
|
||||||
|
<field name="name">玩家世界</field>
|
||||||
|
<field name="res_model">yt.world</field>
|
||||||
|
<field name="view_mode">list,form</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_yt_world_list" model="ir.ui.view">
|
||||||
|
<field name="name">yt.world.list</field>
|
||||||
|
<field name="model">yt.world</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="user_id"/>
|
||||||
|
<field name="seed"/>
|
||||||
|
<field name="world_version"/>
|
||||||
|
<field name="last_active"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_yt_world_form" model="ir.ui.view">
|
||||||
|
<field name="name">yt.world.form</field>
|
||||||
|
<field name="model">yt.world</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form>
|
||||||
|
<sheet>
|
||||||
|
<group>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="user_id"/>
|
||||||
|
<field name="seed"/>
|
||||||
|
<field name="world_version"/>
|
||||||
|
<field name="settings" widget="json"/>
|
||||||
|
<field name="last_active"/>
|
||||||
|
</group>
|
||||||
|
<notebook>
|
||||||
|
<page string="地表地块">
|
||||||
|
<field name="tile_ids">
|
||||||
|
<list>
|
||||||
|
<field name="q"/>
|
||||||
|
<field name="r"/>
|
||||||
|
<field name="terrain"/>
|
||||||
|
<field name="height"/>
|
||||||
|
<field name="micro_world_id"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</page>
|
||||||
|
</notebook>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ============ 地表地块 ============ -->
|
||||||
|
<menuitem id="menu_yt_world_tile" name="地表地块"
|
||||||
|
parent="menu_yt_world_root" action="action_yt_world_tile" sequence="10"/>
|
||||||
|
|
||||||
|
<record id="action_yt_world_tile" model="ir.actions.act_window">
|
||||||
|
<field name="name">地表地块</field>
|
||||||
|
<field name="res_model">yt.world.tile</field>
|
||||||
|
<field name="view_mode">list,form</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_yt_world_tile_list" model="ir.ui.view">
|
||||||
|
<field name="name">yt.world.tile.list</field>
|
||||||
|
<field name="model">yt.world.tile</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list>
|
||||||
|
<field name="world_id"/>
|
||||||
|
<field name="q"/>
|
||||||
|
<field name="r"/>
|
||||||
|
<field name="terrain"/>
|
||||||
|
<field name="height"/>
|
||||||
|
<field name="micro_world_id"/>
|
||||||
|
<field name="updated_at"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_yt_world_tile_form" model="ir.ui.view">
|
||||||
|
<field name="name">yt.world.tile.form</field>
|
||||||
|
<field name="model">yt.world.tile</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form>
|
||||||
|
<sheet>
|
||||||
|
<group>
|
||||||
|
<field name="world_id"/>
|
||||||
|
<field name="q"/>
|
||||||
|
<field name="r"/>
|
||||||
|
<field name="terrain"/>
|
||||||
|
<field name="height"/>
|
||||||
|
<field name="flags" widget="json"/>
|
||||||
|
<field name="micro_world_id"/>
|
||||||
|
<field name="updated_at"/>
|
||||||
|
</group>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
@ -39,4 +39,22 @@ const API = {
|
|||||||
async announcements() {
|
async announcements() {
|
||||||
return (await fetch('/game/api/announcements?db=game', { credentials: 'same-origin' })).json();
|
return (await fetch('/game/api/announcements?db=game', { credentials: 'same-origin' })).json();
|
||||||
},
|
},
|
||||||
|
// ---------- 沙盘记忆(yt_world 模块) ----------
|
||||||
|
async world() {
|
||||||
|
return (await fetch('/yt_world/api/world?db=game', { credentials: 'same-origin' })).json();
|
||||||
|
},
|
||||||
|
async saveTiles(tiles) {
|
||||||
|
return (await fetch('/yt_world/api/tiles/save?db=game', {
|
||||||
|
method: 'POST', credentials: 'same-origin',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ tiles }),
|
||||||
|
})).json();
|
||||||
|
},
|
||||||
|
async saveSettings(settings) {
|
||||||
|
return (await fetch('/yt_world/api/settings?db=game', {
|
||||||
|
method: 'POST', credentials: 'same-origin',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ settings }),
|
||||||
|
})).json();
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
60
index.html
60
index.html
@ -7,6 +7,9 @@
|
|||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&family=ZCOOL+QingKe+HuangYou&family=Noto+Sans+SC:wght@300;400;500;700&display=swap" rel="stylesheet" />
|
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&family=ZCOOL+QingKe+HuangYou&family=Noto+Sans+SC:wght@300;400;500;700&display=swap" rel="stylesheet" />
|
||||||
|
<link rel="icon" type="image/png" sizes="48x48" href="assets/ui/logo_planet_48.png?v=20260723">
|
||||||
|
<link rel="icon" type="image/png" sizes="256x256" href="assets/ui/logo_planet_256.png?v=20260723">
|
||||||
|
<link rel="apple-touch-icon" href="assets/ui/logo_planet_256.png?v=20260723">
|
||||||
<style>
|
<style>
|
||||||
:root{
|
:root{
|
||||||
--bg-0:#080c1a; --bg-1:#0e1730; --bg-2:#15213f;
|
--bg-0:#080c1a; --bg-1:#0e1730; --bg-2:#15213f;
|
||||||
@ -45,6 +48,10 @@
|
|||||||
.user-box .ub-name{color:#fff;font-weight:500;}
|
.user-box .ub-name{color:#fff;font-weight:500;}
|
||||||
.user-box .ub-logout{font-family:inherit;font-size:12px;color:var(--text-dim);background:rgba(255,255,255,.06);border:1px solid var(--card-bd);padding:5px 12px;border-radius:8px;cursor:pointer;transition:.2s;}
|
.user-box .ub-logout{font-family:inherit;font-size:12px;color:var(--text-dim);background:rgba(255,255,255,.06);border:1px solid var(--card-bd);padding:5px 12px;border-radius:8px;cursor:pointer;transition:.2s;}
|
||||||
.user-box .ub-logout:hover{color:#fff;background:rgba(139,92,246,.18);}
|
.user-box .ub-logout:hover{color:#fff;background:rgba(139,92,246,.18);}
|
||||||
|
.user-box .auth-link{color:var(--text-dim);text-decoration:none;font-size:14px;transition:.2s;padding:5px 8px;}
|
||||||
|
.user-box .auth-link:hover{color:#fff;}
|
||||||
|
.user-box .auth-btn{font-family:inherit;font-size:13px;color:#0b1020;background:linear-gradient(120deg,var(--gold),#f0d49a);border:none;padding:6px 16px;border-radius:20px;cursor:pointer;font-weight:600;transition:.2s;}
|
||||||
|
.user-box .auth-btn:hover{filter:brightness(1.1);box-shadow:0 0 16px rgba(231,184,92,.4);}
|
||||||
.cta:hover{filter:brightness(1.08);}
|
.cta:hover{filter:brightness(1.08);}
|
||||||
|
|
||||||
.content{flex:1;position:relative;overflow:hidden;}
|
.content{flex:1;position:relative;overflow:hidden;}
|
||||||
@ -178,7 +185,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<div class="app">
|
<div class="app">
|
||||||
<header class="topbar">
|
<header class="topbar">
|
||||||
<div class="logo"><span class="cn">宇森</span><span class="en">YUSEN</span></div>
|
<div class="logo"><span class="cn">宇森</span></div>
|
||||||
<nav class="nav">
|
<nav class="nav">
|
||||||
<button data-tab="intro" class="active">介绍</button>
|
<button data-tab="intro" class="active">介绍</button>
|
||||||
<button data-tab="codex">图鉴</button>
|
<button data-tab="codex">图鉴</button>
|
||||||
@ -540,31 +547,44 @@
|
|||||||
// ============ 登录态 / 右上角个人信息(从 Odoo 取) ============
|
// ============ 登录态 / 右上角个人信息(从 Odoo 取) ============
|
||||||
(function(){
|
(function(){
|
||||||
const userBox = document.getElementById('userBox');
|
const userBox = document.getElementById('userBox');
|
||||||
async function refreshUserBox(){
|
let cachedMe = null;
|
||||||
|
|
||||||
|
function renderLoggedOut(){
|
||||||
if(!userBox) return;
|
if(!userBox) return;
|
||||||
|
userBox.innerHTML = '<a href="login.html?next=world.html" class="auth-link">登录</a><a href="login.html?next=world.html" class="auth-btn">注册</a>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderLoggedIn(me){
|
||||||
|
if(!userBox) return;
|
||||||
|
userBox.innerHTML = '<span class="ub-name"></span><button class="ub-logout" id="ubLogout">退出</button>';
|
||||||
|
const nm = userBox.querySelector('.ub-name');
|
||||||
|
if(nm) nm.textContent = me.name || me.login;
|
||||||
|
const lb = document.getElementById('ubLogout');
|
||||||
|
if(lb) lb.addEventListener('click', async ()=>{
|
||||||
|
try { await API.logout(); } catch(e){}
|
||||||
|
location.href = 'index.html';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshUserBox(){
|
||||||
try{
|
try{
|
||||||
const me = await API.me();
|
cachedMe = await API.me();
|
||||||
if(me && me.uid){
|
if(cachedMe && cachedMe.uid) renderLoggedIn(cachedMe);
|
||||||
userBox.innerHTML = '<span class="ub-name"></span><button class="ub-logout" id="ubLogout">退出</button>';
|
else renderLoggedOut();
|
||||||
const nm = userBox.querySelector('.ub-name');
|
}catch(e){ renderLoggedOut(); }
|
||||||
if(nm) nm.textContent = me.name || me.login;
|
|
||||||
const lb = document.getElementById('ubLogout');
|
|
||||||
if(lb) lb.addEventListener('click', async ()=>{
|
|
||||||
await API.logout();
|
|
||||||
location.href = 'index.html';
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
userBox.innerHTML = ''; // 未登录:右上角不显示登录入口,登录经“进入游戏”引导
|
|
||||||
}
|
|
||||||
}catch(e){ userBox.innerHTML = ''; }
|
|
||||||
}
|
}
|
||||||
refreshUserBox();
|
refreshUserBox();
|
||||||
|
|
||||||
// ============ 进入游戏:已登录→世界页;未登录→提示登录 ============
|
// ============ 进入游戏:已登录→世界页;未登录→登录页 ============
|
||||||
// TEMP: 本地临时默认直接进入游戏,跳过 Odoo 登录校验;打通后端后改回 API.me() 判断
|
|
||||||
const enterBtn = document.getElementById('enterGame');
|
const enterBtn = document.getElementById('enterGame');
|
||||||
if(enterBtn) enterBtn.addEventListener('click', ()=>{
|
if(enterBtn) enterBtn.addEventListener('click', async ()=>{
|
||||||
location.href = 'world.html';
|
try {
|
||||||
|
const me = cachedMe || await API.me();
|
||||||
|
if (me && me.uid) location.href = 'world.html';
|
||||||
|
else location.href = 'login.html?next=world.html';
|
||||||
|
} catch(e) {
|
||||||
|
location.href = 'login.html?next=world.html';
|
||||||
|
}
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user