feat: 世界聊天敏感词过滤+菜单整合, 部署脚本绑定 yt_game
This commit is contained in:
@@ -2,5 +2,6 @@ from . import game_employee
|
||||
from . import game_achievement
|
||||
from . import game_mail
|
||||
from . import chat
|
||||
from . import sensitive_word
|
||||
from . import page
|
||||
from . import asset
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
from odoo import models, fields
|
||||
from odoo import api, models, fields
|
||||
|
||||
# 游戏助手的展示昵称(前后端共用,落库消息的作者名)
|
||||
GUIDE_NAME = '游戏助手'
|
||||
@@ -29,6 +29,24 @@ class GameChatChannel(models.Model):
|
||||
creator_id = fields.Many2one('res.users', string='创建者')
|
||||
member_ids = fields.Many2many('res.users', string='成员')
|
||||
message_ids = fields.One2many('game.chat.message', 'channel_id', string='消息')
|
||||
message_count = fields.Integer(string='消息数量', compute='_compute_message_count')
|
||||
|
||||
@api.depends('message_ids')
|
||||
def _compute_message_count(self):
|
||||
for rec in self:
|
||||
rec.message_count = len(rec.message_ids)
|
||||
|
||||
def action_view_messages(self):
|
||||
"""从频道列表跳转查看该频道全部消息。"""
|
||||
self.ensure_one()
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': '频道消息',
|
||||
'res_model': 'game.chat.message',
|
||||
'view_mode': 'list,form',
|
||||
'domain': [('channel_id', '=', self.id)],
|
||||
'context': dict(self.env.context),
|
||||
}
|
||||
|
||||
def generate_guide_reply(self, question):
|
||||
"""内置游戏知识引擎(零外部依赖):按关键词检索 game.rule / game.rule.category 作答。
|
||||
@@ -104,3 +122,12 @@ class GameChatMessage(models.Model):
|
||||
author_id = fields.Many2one('res.users', string='发送者')
|
||||
author_name = fields.Char(string='昵称')
|
||||
body = fields.Text(string='内容', required=True)
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
"""创建消息时自动过滤敏感词。"""
|
||||
sanitize = self.env['game.sensitive.word'].sanitize
|
||||
for vals in vals_list:
|
||||
if vals.get('body'):
|
||||
vals['body'] = sanitize(vals['body'])
|
||||
return super(GameChatMessage, self).create(vals_list)
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
from odoo import api, models, fields
|
||||
|
||||
|
||||
class GameSensitiveWord(models.Model):
|
||||
_name = 'game.sensitive.word'
|
||||
_description = '聊天敏感词'
|
||||
_order = 'name'
|
||||
|
||||
name = fields.Char(string='敏感词', required=True, index=True)
|
||||
active = fields.Boolean(string='启用', default=True)
|
||||
replacement = fields.Char(string='替换为', default='**')
|
||||
|
||||
_sql_constraints = [
|
||||
('name_uniq', 'unique(name)', '敏感词不能重复'),
|
||||
]
|
||||
|
||||
@api.model
|
||||
def sanitize(self, text):
|
||||
"""将文本中的敏感词替换为对应替换字符(默认 **)。"""
|
||||
if not text:
|
||||
return text
|
||||
words = self.search([('active', '=', True)])
|
||||
if not words:
|
||||
return text
|
||||
result = text
|
||||
for w in words:
|
||||
pat = re.escape(w.name)
|
||||
repl = w.replacement or '**'
|
||||
result = re.sub(pat, repl, result, flags=re.IGNORECASE)
|
||||
return result
|
||||
@@ -9,6 +9,8 @@ access_game_chat_channel,game.chat.channel public,model_game_chat_channel,base.g
|
||||
access_game_chat_channel_user,game.chat.channel user,model_game_chat_channel,base.group_user,1,1,1,1
|
||||
access_game_chat_message,game.chat.message public,model_game_chat_message,base.group_public,1,0,0,0
|
||||
access_game_chat_message_user,game.chat.message user,model_game_chat_message,base.group_user,1,1,1,1
|
||||
access_game_sensitive_word,game.sensitive.word public,model_game_sensitive_word,base.group_public,1,0,0,0
|
||||
access_game_sensitive_word_user,game.sensitive.word user,model_game_sensitive_word,base.group_user,1,1,1,1
|
||||
access_game_page,game.page public,model_game_page,base.group_public,1,0,0,0
|
||||
access_game_page_user,game.page user,model_game_page,base.group_user,1,1,1,1
|
||||
access_game_page_item,game.page.item public,model_game_page_item,base.group_public,1,0,0,0
|
||||
|
||||
|
@@ -1,10 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<!-- ============ 世界聊天 · 顶级菜单 ============ -->
|
||||
<!-- ============ 世界聊天 · 并入宇森游戏根菜单 ============ -->
|
||||
<record id="menu_game_chat_root" model="ir.ui.menu">
|
||||
<field name="name">世界聊天</field>
|
||||
<field name="sequence">10</field>
|
||||
<field name="parent_id" ref="game_base.menu_game_root"/>
|
||||
<field name="sequence">95</field>
|
||||
</record>
|
||||
|
||||
<!-- ============ 世界聊天 · 频道 ============ -->
|
||||
@@ -28,7 +29,8 @@
|
||||
<list>
|
||||
<field name="name"/>
|
||||
<field name="channel_type"/>
|
||||
<field name="sequence"/>
|
||||
<field name="message_count"/>
|
||||
<button name="action_view_messages" type="object" string="消息" icon="fa-comments"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
@@ -108,4 +110,44 @@
|
||||
<field name="sequence">20</field>
|
||||
</record>
|
||||
|
||||
<!-- ============ 世界聊天 · 敏感词 ============ -->
|
||||
<record id="action_game_sensitive_word" model="ir.actions.act_window">
|
||||
<field name="name">敏感词</field>
|
||||
<field name="res_model">game.sensitive.word</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
</record>
|
||||
|
||||
<record id="view_game_sensitive_word_list" model="ir.ui.view">
|
||||
<field name="name">game.sensitive.word.list</field>
|
||||
<field name="model">game.sensitive.word</field>
|
||||
<field name="arch" type="xml">
|
||||
<list editable="bottom">
|
||||
<field name="name"/>
|
||||
<field name="replacement"/>
|
||||
<field name="active"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_game_sensitive_word_form" model="ir.ui.view">
|
||||
<field name="name">game.sensitive.word.form</field>
|
||||
<field name="model">game.sensitive.word</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<group>
|
||||
<field name="name"/>
|
||||
<field name="replacement"/>
|
||||
<field name="active"/>
|
||||
</group>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="menu_game_sensitive_word" model="ir.ui.menu">
|
||||
<field name="name">敏感词</field>
|
||||
<field name="parent_id" ref="menu_game_chat_root"/>
|
||||
<field name="action" ref="action_game_sensitive_word"/>
|
||||
<field name="sequence">30</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
|
||||
@@ -124,6 +124,39 @@ class YtWorldApi(http.Controller):
|
||||
tile.micro_world_id.build_items = items
|
||||
return _json({'ok': True, 'count': len(items)})
|
||||
|
||||
@http.route('/yt_world/api/micro/name', type='http', auth='user',
|
||||
methods=['POST', 'OPTIONS'], csrf=False)
|
||||
def rename_micro(self, **kw):
|
||||
"""修改某地块微观世界的名称。body: {"q":int,"r":int,"name":"..."}
|
||||
找不到地块时自动建(保证种子地块也能落库记忆);与 rename_world 同样的校验。"""
|
||||
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()
|
||||
q, r, name = body.get('q'), body.get('r'), (body.get('name') or '').strip()
|
||||
if q is None or r is None:
|
||||
return _json({'ok': False, 'error': 'bad_params'})
|
||||
if not name:
|
||||
return _json({'ok': False, 'error': 'empty'})
|
||||
if len(name) > 24:
|
||||
return _json({'ok': False, 'error': 'too_long'})
|
||||
if re.search(r'[\x00-\x1f\x7f]', name):
|
||||
return _json({'ok': False, 'error': 'invalid_char'})
|
||||
tile = request.env['yt.world.tile'].sudo().search(
|
||||
[('world_id', '=', world.id), ('q', '=', q), ('r', '=', r)], limit=1)
|
||||
if not tile:
|
||||
tile = request.env['yt.world.tile'].sudo().create(
|
||||
{'world_id': world.id, 'q': q, 'r': r})
|
||||
if not tile.micro_world_id:
|
||||
tile._ensure_micro()
|
||||
old = tile.micro_world_id.name
|
||||
tile.micro_world_id.name = name
|
||||
_logger.info('yt.world.tile.micro rename uid=%s (%s,%s) %r -> %r',
|
||||
user.id, q, r, old, name)
|
||||
return _json({'ok': True, 'name': name, 'q': q, 'r': r})
|
||||
|
||||
@http.route('/yt_world/api/micro/catalog', type='http', auth='user',
|
||||
methods=['GET', 'OPTIONS'], csrf=False)
|
||||
def micro_catalog(self, **kw):
|
||||
|
||||
@@ -84,6 +84,7 @@ class YtWorld(models.Model):
|
||||
'height': t.height or None,
|
||||
'flags': t.flags or {},
|
||||
'micro_world_id': t.micro_world_id.id if t.micro_world_id else None,
|
||||
'micro_name': (t.micro_world_id.name or '') if t.micro_world_id else '',
|
||||
'build_items': (t.micro_world_id.build_items or []) if t.micro_world_id else [],
|
||||
})
|
||||
return {
|
||||
|
||||
@@ -56,9 +56,4 @@
|
||||
parent="menu_website_config"
|
||||
action="game_base.action_game_achievement_template" sequence="93"/>
|
||||
|
||||
<!-- 8. 世界聊天 -->
|
||||
<menuitem id="menu_website_world_chat" name="世界聊天"
|
||||
parent="game_base.menu_game_root"
|
||||
action="game_base.action_game_chat_channel" sequence="95"/>
|
||||
|
||||
</odoo>
|
||||
|
||||
@@ -125,6 +125,23 @@ const API = {
|
||||
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,
|
||||
|
||||
+2
-2
@@ -6,7 +6,7 @@ set -e
|
||||
# ===== 按实际情况修改这几个变量 =====
|
||||
REPO_DIR="/opt/yuthon/game" # git 仓库目录(含 world.html 与 addons/)
|
||||
ODOO_CONF="/etc/odoo/odoo.conf" # odoo.conf 路径
|
||||
ODOO_DB="game" # 线上数据库名(务必与前端 ?db= 一致)
|
||||
ODOO_DB="yt_game" # 线上数据库名(game.pengyuthon.cn 对应 yt_game;勿改,避免影响 school)
|
||||
ODOO_SERVICE="odoo" # systemctl 服务名
|
||||
ODOO_BIN="/usr/bin/odoo" # odoo-bin 路径(或 which odoo)
|
||||
# ===================================
|
||||
@@ -36,5 +36,5 @@ sleep 3
|
||||
systemctl is-active "$ODOO_SERVICE" || { echo "Odoo 未起来,检查: journalctl -u $ODOO_SERVICE -n 50"; exit 1; }
|
||||
|
||||
echo "== [5/5] 冒烟验证 =="
|
||||
curl -s -o /dev/null -w "codex 接口 HTTP %{http_code}\n" "https://game.pengyuthon.cn/game/api/codex?db=game"
|
||||
curl -s -o /dev/null -w "codex 接口 HTTP %{http_code}\n" "https://game.pengyuthon.cn/game/api/codex?db=yt_game"
|
||||
echo "完成。前端为静态文件,nginx 指向 $REPO_DIR 即可,无需构建。"
|
||||
|
||||
@@ -47,6 +47,8 @@
|
||||
.sl-arrow:hover{background:rgba(139,92,246,.25);border-color:rgba(139,92,246,.4);transform:translateY(-50%) scale(1.08);}
|
||||
.sl-arrow.prev{left:16px;}
|
||||
.sl-arrow.next{right:16px;}
|
||||
/* 隐藏左右翻页与轮播点,底层自动轮播/键盘/滑动切换逻辑保留 */
|
||||
.sl-arrow, .sl-dots{ display:none !important; }
|
||||
|
||||
/* 起源历史(右上竖排) */
|
||||
.slide-origin{position:absolute;right:72px;top:48px;z-index:4;pointer-events:none;writing-mode:vertical-rl;text-orientation:upright;
|
||||
|
||||
+8
-7
@@ -31,17 +31,18 @@
|
||||
.wrap{position:relative;z-index:2;width:min(420px,92vw);}
|
||||
.brand{text-align:center;margin-bottom:26px;}
|
||||
.brand .cn{font-family:"PingFang SC","Microsoft YaHei",system-ui,sans-serif;font-weight:900;font-size:42px;letter-spacing:8px;color:#fff;text-shadow:0 0 18px rgba(139,92,246,.6);}
|
||||
.brand .en{font-size:16px;color:var(--magenta);letter-spacing:5px;margin-top:8px;font-weight:600;text-shadow:0 1px 6px rgba(0,0,0,.7);}
|
||||
.brand .en{font-size:18px;color:#fff;letter-spacing:6px;margin-top:8px;font-weight:700;text-shadow:0 2px 10px rgba(0,0,0,.9),0 0 18px rgba(139,92,246,.45);}
|
||||
.brand .tag{font-size:13px;color:var(--text-dim);margin-top:10px;}
|
||||
|
||||
.card{background:var(--card);border:1px solid var(--card-bd);border-radius:18px;padding:26px 26px 30px;backdrop-filter:blur(10px);box-shadow:var(--glow);}
|
||||
.card{background:rgba(8,12,26,.78);border:1px solid rgba(255,255,255,.18);border-radius:18px;padding:26px 26px 30px;backdrop-filter:blur(14px);box-shadow:0 8px 32px rgba(0,0,0,.45),var(--glow);}
|
||||
.tabs{display:flex;gap:6px;margin-bottom:20px;background:rgba(8,12,26,.5);border-radius:12px;padding:5px;}
|
||||
.tabs button{flex:1;font-family:inherit;font-size:14px;font-weight:500;color:var(--text-dim);background:transparent;border:none;padding:9px;border-radius:9px;cursor:pointer;transition:.2s;}
|
||||
.tabs button.active{color:#fff;background:rgba(139,92,246,.28);box-shadow:var(--glow);}
|
||||
.tabs button{flex:1;font-family:inherit;font-size:15px;font-weight:600;color:#c4cde5;background:transparent;border:none;padding:10px;border-radius:9px;cursor:pointer;transition:.2s;}
|
||||
.tabs button.active{color:#fff;background:rgba(139,92,246,.35);box-shadow:var(--glow);}
|
||||
|
||||
.field{margin-bottom:14px;}
|
||||
.field label{display:block;font-size:12px;color:var(--text-dim);margin-bottom:6px;letter-spacing:1px;}
|
||||
.field input{width:100%;background:rgba(8,12,26,.6);border:1px solid var(--card-bd);border-radius:10px;color:var(--text);padding:11px 13px;font-family:inherit;font-size:14px;outline:none;transition:.2s;}
|
||||
.field{margin-bottom:16px;}
|
||||
.field label{display:block;font-size:14px;color:#fff;margin-bottom:8px;letter-spacing:1px;font-weight:600;text-shadow:0 1px 4px rgba(0,0,0,.8);}
|
||||
.field input{width:100%;background:rgba(0,0,0,.45);border:1px solid rgba(255,255,255,.22);border-radius:10px;color:var(--text);padding:12px 14px;font-family:inherit;font-size:15px;outline:none;transition:.2s;}
|
||||
.field input::placeholder{color:rgba(233,237,246,.55);}
|
||||
.field input:focus{border-color:var(--violet);box-shadow:0 0 0 3px rgba(139,92,246,.18);}
|
||||
.pw-wrap{position:relative;}
|
||||
.pw-wrap input{padding-right:40px;}
|
||||
|
||||
+71
-9
@@ -42,7 +42,7 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa
|
||||
}
|
||||
#topBar .title .worldName.editable { cursor: pointer; padding: 2px 8px; border-radius: 5px; transition: background .15s; }
|
||||
#topBar .title .worldName.editable:hover { background: rgba(255,255,255,.08); }
|
||||
#topBar .title .worldName-input {
|
||||
.worldName-input {
|
||||
background: transparent; border: 1px solid rgba(124,199,255,.6); color: #cfe1f3;
|
||||
font-size: 15px; font-weight: 600; padding: 2px 6px; border-radius: 5px; width: 180px;
|
||||
font-family: inherit; outline: none;
|
||||
@@ -172,7 +172,7 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa
|
||||
|
||||
/* 世界等级经验条(位于 bottomBar 内最左侧,加大加清晰) */
|
||||
#worldExpBarWrap{
|
||||
flex:0 0 auto; width:380px; height:100%;
|
||||
flex:0 0 auto; width:520px; height:100%;
|
||||
display:flex; flex-direction:column; justify-content:center; gap:7px;
|
||||
padding:0 16px 0 0; background:transparent; border:none;
|
||||
}
|
||||
@@ -180,8 +180,9 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa
|
||||
.we-lv{font-size:16px; font-weight:800; color:#c4b5fd; text-shadow:0 0 10px rgba(139,92,246,.5);}
|
||||
.we-title{font-size:13px; color:#9fb0c4; max-width:130px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
|
||||
.we-exp-txt{margin-left:auto; font-size:13px; font-weight:600; color:#cfe1f3;}
|
||||
.we-bar{width:100%; height:12px; background:rgba(255,255,255,.10); border-radius:6px; overflow:hidden;}
|
||||
.we-fill{height:100%; width:0%; background:linear-gradient(90deg,#8b5cf6,#4af); border-radius:6px; transition:width .3s ease; box-shadow:0 0 12px rgba(139,92,246,.45);}
|
||||
.we-pct{margin-left:2px; font-size:13px; font-weight:700; color:#c4b5fd; min-width:34px; text-align:right;}
|
||||
.we-bar{width:100%; height:14px; background:rgba(255,255,255,.10); border-radius:7px; overflow:hidden;}
|
||||
.we-fill{height:100%; width:0%; background:linear-gradient(90deg,#8b5cf6,#4af); border-radius:7px; transition:width .3s ease; box-shadow:0 0 12px rgba(139,92,246,.45);}
|
||||
|
||||
/* 主沙盘底部功能框架(预留区:放功能按钮 / 素材槽,比微观边框更高) */
|
||||
#bottomBar {
|
||||
@@ -238,6 +239,8 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa
|
||||
}
|
||||
#microHeader .backBtn:hover { background: rgba(100,200,255,0.25); }
|
||||
#microHeader .microTitle { margin-left: 16px; font-size: 16px; font-weight: bold; color: #e0e0e0; }
|
||||
#microHeader .microTitle.editable { cursor: pointer; padding: 2px 8px; border-radius: 5px; transition: background .15s; }
|
||||
#microHeader .microTitle.editable:hover { background: rgba(255,255,255,.08); }
|
||||
#microHeader .microSubtitle { display: none; }
|
||||
|
||||
#microBody { margin-top: 96px; flex: 1; display: flex; flex-direction: column; position: relative; overflow: hidden; }
|
||||
@@ -715,7 +718,7 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa
|
||||
@media (max-width:420px){
|
||||
#topBar .title .worldName{display:none;}
|
||||
#bottomBar .bb-btn{padding:7px 9px;font-size:11px;}
|
||||
#worldExpBarWrap{width:220px;gap:5px;}
|
||||
#worldExpBarWrap{width:280px;gap:5px;}
|
||||
#worldExpBarWrap .we-title{display:none;}
|
||||
#sceneToolbar{right:8px;gap:5px;padding:6px 7px;top:calc(58px + env(safe-area-inset-top) + 6px);}
|
||||
#sceneToolbar .top-icon{width:34px;height:34px;font-size:16px;}
|
||||
@@ -818,6 +821,7 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa
|
||||
<span class="we-lv">世界 Lv.<b id="weLv">--</b></span>
|
||||
<span class="we-title" id="weTitle">--</span>
|
||||
<span class="we-exp-txt" id="weExpTxt">EXP -- / --</span>
|
||||
<span class="we-pct" id="weExpPct">--%</span>
|
||||
</div>
|
||||
<div class="we-bar"><div class="we-fill" id="weExpFill" style="width:0%"></div></div>
|
||||
</div>
|
||||
@@ -1690,6 +1694,7 @@ let ambParticles = [];
|
||||
|
||||
// 微观世界
|
||||
let microState = null;
|
||||
let microNames = {}; // 地块 key("q,r") -> 该微观世界自定义名称(后端持久化,记忆)
|
||||
let microBuildRevealAt = 0; // 建筑显示时间戳:过渡结束 + 200ms 后才浮现
|
||||
// 游戏化系统全局状态(与 Odoo 后端打通)
|
||||
let microChar = null; // 玩家角色:{name,level,exp,gold,inventory,equip}
|
||||
@@ -2972,6 +2977,15 @@ function pickTransitionImage() {
|
||||
return TRANSITION_IMAGES[idx];
|
||||
}
|
||||
|
||||
// ============ 微观世界标题:有自定义名显示「层 · 自定义名」,否则默认「层 · 地形村落」 ============
|
||||
function microTitleText(tile) {
|
||||
const key = tile.q + ',' + tile.r;
|
||||
const custom = microNames[key];
|
||||
const layerLabel = { earth: '🌍 大地', under: '🕳 地下', core: '🔥 地心', sky: '🌤 天空', star: '✦ 星空' }[tile.layer] || '';
|
||||
if (custom) return `${layerLabel} · ${custom}`;
|
||||
return `${layerLabel} · ${TERRAIN[tile.terrain].name}村落`;
|
||||
}
|
||||
|
||||
// ============ 进入微观世界 ============
|
||||
function enterMicroWorld(tile) {
|
||||
if (!tile) return;
|
||||
@@ -3012,8 +3026,7 @@ function enterMicroWorld(tile) {
|
||||
microZoom = 1; microPanX = 0; microPanY = 0;
|
||||
microViewOpen = true; // 暂停主世界重绘
|
||||
microView.classList.add('active'); // 入场动画(缩放+去模糊)在遮罩掩护下播放,揭幕时早已归位
|
||||
const layerLabel = { earth:'🌍 大地', under:'🕳 地下', core:'🔥 地心', sky:'🌤 天空', star:'✦ 星空' }[tile.layer] || '';
|
||||
document.getElementById('microTitle').textContent = `${layerLabel} · ${TERRAIN[tile.terrain].name}村落`;
|
||||
document.getElementById('microTitle').textContent = microTitleText(tile);
|
||||
document.getElementById('microSubtitle').textContent = `坐标(${tile.q}, ${tile.r}) · ${microState.units.length}位居民`;
|
||||
applyMicroAccent(tile.terrain);
|
||||
updateSidebar();
|
||||
@@ -3682,8 +3695,7 @@ function setMicroTile(tile) {
|
||||
? [{ ...BUILDINGS[tile.buildingType], type: tile.buildingType, level: tile.buildingLevel, hp: 80 + tile.buildingLevel * 10 }]
|
||||
: [];
|
||||
microState.resources = calcResources(tile);
|
||||
const layerLabel = { earth: '🌍 大地', under: '🕳 地下', core: '🔥 地心', sky: '🌤 天空', star: '✦ 星空' }[tile.layer] || '';
|
||||
document.getElementById('microTitle').textContent = `${layerLabel} · ${TERRAIN[tile.terrain].name}村落`;
|
||||
document.getElementById('microTitle').textContent = microTitleText(tile);
|
||||
document.getElementById('microSubtitle').textContent = `坐标(${tile.q}, ${tile.r}) · ${microState.units.length}位居民`;
|
||||
updateSidebar();
|
||||
microAutoRoadCells(); // 切地块时重新计算自动道路贴图
|
||||
@@ -5044,6 +5056,7 @@ function applyWorldState(ws) {
|
||||
if (ov.terrain) t.terrain = ov.terrain;
|
||||
if (ov.height != null) { t.height = ov.height; t.tier = heightToTier(ov.height); }
|
||||
if (ov.flags) t.flags = ov.flags;
|
||||
if (ov.micro_name != null) microNames[ov.q + ',' + ov.r] = ov.micro_name; // 恢复微观世界命名记忆
|
||||
}
|
||||
// 微观建造布置:按 'q,r' 恢复(覆盖所有层,进入地块时即显示)
|
||||
for (const ov of ws.tiles) {
|
||||
@@ -5946,6 +5959,51 @@ function setupWorldNameEdit() {
|
||||
});
|
||||
}
|
||||
|
||||
// ============ 微观世界标题:点击进入编辑,回车/失焦保存到后端,Esc 取消 ============
|
||||
function setupMicroNameEdit() {
|
||||
const mt = document.getElementById('microTitle');
|
||||
if (!mt) return;
|
||||
mt.classList.add('editable');
|
||||
mt.title = '点击重命名此微观世界';
|
||||
mt.addEventListener('click', () => {
|
||||
if (!microState || !microState.tile) return; // 仅在微观视图内可编辑
|
||||
if (mt.querySelector('input')) return; // 已在编辑
|
||||
const tile = microState.tile;
|
||||
const key = tile.q + ',' + tile.r;
|
||||
const current = microNames[key] || '';
|
||||
const input = document.createElement('input');
|
||||
input.type = 'text';
|
||||
input.value = current;
|
||||
input.placeholder = '输入此微观世界名称';
|
||||
input.maxLength = 24;
|
||||
input.className = 'worldName-input';
|
||||
input.spellcheck = false;
|
||||
mt.textContent = '';
|
||||
mt.appendChild(input);
|
||||
input.focus();
|
||||
input.select();
|
||||
let done = false;
|
||||
const restore = () => { if (done) return; done = true; mt.textContent = microTitleText(tile); };
|
||||
const commit = async () => {
|
||||
if (done) return;
|
||||
const next = input.value.trim();
|
||||
if (!next || next === current) { restore(); return; }
|
||||
restore(); // 先本地显示,再异步写后端
|
||||
microNames[key] = next;
|
||||
try {
|
||||
const r = await API.renameMicro(tile.q, tile.r, next);
|
||||
if (r && r.ok && r.name) { microNames[key] = r.name; }
|
||||
else console.warn('微观世界重命名后端未响应,已本地保存', r);
|
||||
} catch (e) { console.warn('微观世界重命名失败,已本地保存', e); }
|
||||
};
|
||||
input.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') { e.preventDefault(); commit(); }
|
||||
else if (e.key === 'Escape') { e.preventDefault(); restore(); }
|
||||
});
|
||||
input.addEventListener('blur', commit);
|
||||
});
|
||||
}
|
||||
|
||||
function init() {
|
||||
W = canvas.width = window.innerWidth;
|
||||
H = canvas.height = window.innerHeight - BOTTOM_BAR_H;
|
||||
@@ -5960,6 +6018,8 @@ function init() {
|
||||
setupBuildUI();
|
||||
// 顶栏世界名称点击编辑
|
||||
setupWorldNameEdit();
|
||||
// 微观世界标题点击编辑(按地块独立命名,持久化到后端)
|
||||
setupMicroNameEdit();
|
||||
// 钟表已移走;时间文字由 syncTimeUI 每 100ms 节流更新
|
||||
// 世界切换导航
|
||||
document.querySelectorAll('#worldNav .wn-item').forEach(el => {
|
||||
@@ -6245,6 +6305,8 @@ if (typeof fitWorld === 'function') fitWorld();
|
||||
if (weExpFill) weExpFill.style.width = worldExpPct + '%';
|
||||
const weExpTxt = document.getElementById('weExpTxt');
|
||||
if (weExpTxt) weExpTxt.textContent = 'EXP ' + worldExp + ' / ' + worldExpMax;
|
||||
const weExpPctEl = document.getElementById('weExpPct');
|
||||
if (weExpPctEl) weExpPctEl.textContent = Math.floor(worldExpPct) + '%';
|
||||
// 邮件未读角标(顶部图标栏)
|
||||
const topMailBadge = document.getElementById('topMailBadge');
|
||||
if (topMailBadge) {
|
||||
|
||||
Reference in New Issue
Block a user