feat: 前端 UI 精修 + 后端视图调整 + 资源预置

前端:
- world.html 游戏内聊天组件重构:表情选择器改固定方格+自适应列+去滚动条;
  输入框清空占位文字+隐藏滚动条;聊天面板整体高度缩短约 1/3;
  顶部用户区改为纯文字并点开个人信息弹窗(头像/头像框/性别/说明/称号/注册)
- chat.html 聊天输入框压扁(46→34px)
- codex.html 新增神话品质、天赋/技能书/书籍分类与预置数据
- rules.html 规则文档更新

后端(Odoo):
- game_base 附件改为看板视图(图片/名称/备注/大小)
- yt_world 玩家菜单直接指向 hr.employee
- game_base 员工表单隐藏原生联系人/组织架构页签
- chat 模型/接口/视图与规则数据扩充

资源:生成 4x4 资源精灵表(树木/石材/芦苇/宝箱)
This commit is contained in:
李鹏宇
2026-08-14 14:27:15 +08:00
parent d713221581
commit 73d764938d
16 changed files with 1713 additions and 388 deletions
+29 -5
View File
@@ -2,6 +2,7 @@
from odoo import http
from odoo.http import request
from .game_api import _json, _post_body
from ..models.chat import GUIDE_NAME
class ChatApiController(http.Controller):
@@ -12,13 +13,14 @@ class ChatApiController(http.Controller):
@http.route('/game/api/chat/channels', type='http', auth='public',
methods=['GET'], csrf=False)
def chat_channels(self, **kw):
"""当前用户可见频道:世界频道(公开) + 自己参与的私聊/公会。"""
"""当前用户可见频道:世界频道 + AI向导频道(均公开) + 自己参与的私聊/公会。"""
uid = request.session.uid
Channel = request.env['game.chat.channel'].sudo()
domain = [('channel_type', '=', 'world')]
public = [('channel_type', 'in', ['world', 'guide'])]
if uid:
domain = ['|', ('channel_type', '=', 'world'),
('member_ids', 'in', [uid])]
domain = ['|'] + public + [('member_ids', 'in', [uid])]
else:
domain = public
recs = Channel.search_read(domain, ['id', 'name', 'channel_type'],
order='sequence, id')
return _json({'items': [{
@@ -60,6 +62,7 @@ class ChatApiController(http.Controller):
'create_date': str(r['create_date']),
'is_self': bool(uid and r.get('author_id')
and r['author_id'][0] == uid),
'is_guide': bool(r['author_name'] == GUIDE_NAME),
} for r in recs]
return _json({'items': items})
@@ -92,6 +95,26 @@ class ChatApiController(http.Controller):
'author_name': author_name,
'body': body,
})
# AI 向导自动回复:仅对 guide 类型频道生效,与世界频道隔离
guide_reply = None
ch = request.env['game.chat.channel'].sudo().browse(channel_id)
if ch.exists() and ch.channel_type == 'guide':
reply_text = ch.generate_guide_reply(body)
if reply_text:
reply = request.env['game.chat.message'].sudo().create({
'channel_id': channel_id,
'author_id': False,
'author_name': GUIDE_NAME,
'body': reply_text,
})
guide_reply = {
'id': reply.id,
'author_name': GUIDE_NAME,
'body': reply_text,
'create_date': str(reply.create_date),
'is_self': False,
'is_guide': True,
}
return _json({
'ok': True,
'message': {
@@ -100,5 +123,6 @@ class ChatApiController(http.Controller):
'body': body,
'create_date': str(msg.create_date),
'is_self': True,
}
},
'guide_reply': guide_reply,
})
+11 -3
View File
@@ -22,10 +22,18 @@ def _json(data):
def _post_body():
try:
raw = request.httprequest.get_data(as_text=True) or '{}'
return json.loads(raw)
# Odoo 官方解析 JSON 请求体(正确处理 UTF-8),优先使用。
return request.get_json_data()
except Exception:
return {}
try:
# 兜底:显式按 UTF-8 解码原始字节,避免 Werkzeug 在 application/json
# 无 charset 时按 latin1 解码中文导致乱码。
raw = request.httprequest.get_data(cache=True) or b'{}'
if isinstance(raw, str):
raw = raw.encode('utf-8')
return json.loads(raw.decode('utf-8'))
except Exception:
return {}
class GameApiController(http.Controller):
+9 -2
View File
@@ -1,11 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<!-- 默认世界频道:所有玩家可见、可发言 -->
<data>
<!-- 默认世界频道:所有玩家可见、可发言(纯净玩家聊天,不混入 AI 向导) -->
<record model="game.chat.channel" id="channel_world">
<field name="name">世界频道</field>
<field name="channel_type">world</field>
<field name="sequence">10</field>
</record>
<!-- 默认游戏助手频道:与世界频道物理隔离,玩家提问、系统自动回复(知识引擎) -->
<record model="game.chat.channel" id="channel_guide">
<field name="name">游戏助手</field>
<field name="channel_type">guide</field>
<field name="sequence">20</field>
</record>
</data>
</odoo>
+75
View File
@@ -1,6 +1,17 @@
# -*- coding: utf-8 -*-
import re
from odoo import models, fields
# 游戏助手的展示昵称(前后端共用,落库消息的作者名)
GUIDE_NAME = '游戏助手'
def _plain(text):
"""去掉 HTML 标签,用于把 rule.desc(Html) 当作纯文本兜底。"""
if not text:
return ''
return re.sub(r'<[^>]+>', '', text).strip()
class GameChatChannel(models.Model):
_name = 'game.chat.channel'
@@ -12,12 +23,76 @@ class GameChatChannel(models.Model):
('world', '世界频道'),
('guild', '公会'),
('pm', '私聊'),
('guide', 'AI向导'),
], string='类型', default='world', index=True)
sequence = fields.Integer(string='排序', default=10)
creator_id = fields.Many2one('res.users', string='创建者')
member_ids = fields.Many2many('res.users', string='成员')
message_ids = fields.One2many('game.chat.message', 'channel_id', string='消息')
def generate_guide_reply(self, question):
"""内置游戏知识引擎(零外部依赖):按关键词检索 game.rule / game.rule.category 作答。
预留扩展点:若需接入真实 LLM,替换本方法实现、返回模型生成的文本即可,其余调用方不变。
"""
q = (question or '').strip()
if not q:
return '你好,我是游戏助手。你可以问我关于「属性」「等级」「货币」「制度」的问题~'
env = self.env
Rule = env['game.rule']
Cat = env['game.rule.category']
def rules_of(key):
c = Cat.search([('key', '=', key)], limit=1)
return Rule.search([('category_id', '=', c.id)], order='name') if c else []
def line(r):
return f'· {r.name}:{r.summary or _plain(r.desc)}'
# 问候
if re.search(r'(你好|您好|hi|hello|在吗|嗨|早上好|晚上好)', q, re.I):
return ('你好,我是游戏助手 🤖 随时为你解答宇森世界的玩法。\n'
'可以问我:属性怎么算?怎么升级?货币有什么用?有哪些制度?')
# 属性(关键词覆盖攻击/物攻/法攻/防御/暴击等;穿透/韧性已移除)
_attr_re = re.search(r'(属性|攻击|物攻|法攻|防御|物防|法防|血量|生命|法力|速度|攻速|暴击|爆伤|命中|闪避|格挡|抗性|吸血|幸运)', q)
if _attr_re:
rules = list(rules_of('attribute_basic')) + list(rules_of('attribute_advanced'))
if rules:
head = '【属性规则】不同属性决定你的战斗表现,核心属性如下:'
return head + '\n' + '\n'.join(line(r) for r in rules[:12]) + '\n\n想深入了解某项,直接问我它的名字~'
# 等级
if re.search(r'(等级|升级|经验|世界等级|角色等级|怎么变强|变强)', q):
for key in ('world', 'character'):
rules = rules_of(key)
if rules:
c = Cat.search([('key', '=', key)], limit=1)
head = f'【{c.name}】'
lines = [f'· {r.name}(Lv{r.level}):{r.summary or ""}' for r in rules[:6]]
return head + '\n' + '\n'.join(l for l in lines if l.strip())
# 货币
if re.search(r'(货币|金币|钱|元宝|钻石|充值|兑换|经济)', q):
rules = rules_of('currency')
if rules:
head = '【货币规则】游戏内货币与获取/消耗方式:'
return head + '\n' + '\n'.join(line(r) for r in rules[:8])
# 制度 / 规则
if re.search(r'(制度|规定|规则|条款|公约|不能|禁止|惩罚|封号)', q):
rules = rules_of('system')
if rules:
head = '【制度说明】请遵守以下基本规则:'
return head + '\n' + '\n'.join(line(r) for r in rules[:8])
# 兜底:给出可问的方向
cats = Cat.search(
[('key', 'in', ['world', 'character', 'currency', 'attribute', 'system'])],
order='sequence')
tips = '、'.join(c.name for c in cats) if cats else '属性、等级、货币、制度'
return ('我是游戏助手,这个问题我还在学习中 🤔\n'
f'你可以问我这些方向:{tips}。\n例如:「攻击和物防怎么算?」「怎么升级最快?」')
class GameChatMessage(models.Model):
_name = 'game.chat.message'
+43 -1
View File
@@ -2,10 +2,52 @@
<odoo>
<!-- ============ 附件(Odoo 原生 ir.attachment) ============ -->
<!-- ============ 附件(Odoo 原生 ir.attachment)看板视图 ============ -->
<record id="yusen_attachment_kanban_view" model="ir.ui.view">
<field name="name">yusen.attachment.kanban</field>
<field name="model">ir.attachment</field>
<field name="arch" type="xml">
<kanban>
<field name="name"/>
<field name="description"/>
<field name="file_size"/>
<field name="mimetype"/>
<templates>
<t t-name="kanban-box">
<div class="oe_kanban_global_click oe_kanban_card d-flex flex-column p-2" style="width:160px;">
<div class="o_kanban_image mb-2 text-center" style="height:100px;line-height:100px;background:rgba(0,0,0,.06);border-radius:6px;overflow:hidden;font-size:40px;">
<t t-if="record.mimetype.raw_value and 'image' in record.mimetype.raw_value">🖼️</t>
<t t-else="">📎</t>
</div>
<div class="oe_kanban_details text-center">
<strong class="o_kanban_record_title d-block text-truncate" title="%s"><field name="name"/></strong>
<div class="text-muted small text-truncate mt-1" style="min-height:1.2em;">
<t t-if="record.description.raw_value"><field name="description"/></t>
<t t-else="">—</t>
</div>
<div class="small mt-1" style="color:#6c757d;">
<t t-if="record.file_size.raw_value &gt;= 1048576">
<t t-esc="(record.file_size.raw_value / 1048576).toFixed(2)"/> MB
</t>
<t t-elif="record.file_size.raw_value &gt;= 1024">
<t t-esc="(record.file_size.raw_value / 1024).toFixed(1)"/> KB
</t>
<t t-else="">
<t t-esc="record.file_size.raw_value"/> B
</t>
</div>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<record id="action_yusen_attachment" model="ir.actions.act_window">
<field name="name">附件</field>
<field name="res_model">ir.attachment</field>
<field name="view_mode">list,form</field>
<field name="view_mode">kanban,list,form</field>
<field name="context">{'search_default_filter_website': 0}</field>
</record>
+63 -1
View File
@@ -1,13 +1,26 @@
<?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>
</record>
<!-- ============ 世界聊天 · 频道 ============ -->
<record id="action_game_chat_channel" model="ir.actions.act_window">
<field name="name">世界聊天</field>
<field name="name">频道</field>
<field name="res_model">game.chat.channel</field>
<field name="view_mode">list,form</field>
</record>
<record id="menu_game_chat_channel" model="ir.ui.menu">
<field name="name">频道</field>
<field name="parent_id" ref="menu_game_chat_root"/>
<field name="action" ref="action_game_chat_channel"/>
<field name="sequence">10</field>
</record>
<record id="view_game_chat_channel_list" model="ir.ui.view">
<field name="name">game.chat.channel.list</field>
<field name="model">game.chat.channel</field>
@@ -46,4 +59,53 @@
</field>
</record>
<!-- ============ 世界聊天 · 消息(后台记录与查看) ============ -->
<record id="action_game_chat_message" model="ir.actions.act_window">
<field name="name">聊天消息</field>
<field name="res_model">game.chat.message</field>
<field name="view_mode">list,form</field>
<field name="context">{'search_default_group_channel': 1}</field>
</record>
<record id="view_game_chat_message_list" model="ir.ui.view">
<field name="name">game.chat.message.list</field>
<field name="model">game.chat.message</field>
<field name="arch" type="xml">
<list string="聊天消息" multi_edit="1">
<field name="channel_id"/>
<field name="author_name"/>
<field name="body" string="内容"/>
<field name="create_date"/>
</list>
</field>
</record>
<record id="view_game_chat_message_form" model="ir.ui.view">
<field name="name">game.chat.message.form</field>
<field name="model">game.chat.message</field>
<field name="arch" type="xml">
<form>
<group>
<field name="channel_id"/>
<field name="author_id"/>
<field name="author_name"/>
</group>
<group>
<field name="create_date"/>
</group>
<group string="内容">
<field name="body" nolabel="1"/>
</group>
</form>
</field>
</record>
<!-- 频道菜单下挂消息子项,方便后台同步查看聊天记录 -->
<record id="menu_game_chat_message" model="ir.ui.menu">
<field name="name">聊天消息</field>
<field name="parent_id" ref="menu_game_chat_root"/>
<field name="action" ref="action_game_chat_message"/>
<field name="sequence">20</field>
</record>
</odoo>
@@ -7,6 +7,14 @@
<field name="model">hr.employee</field>
<field name="inherit_id" ref="hr.view_employee_form"/>
<field name="arch" type="xml">
<!-- 隐藏原生「公开信息」(联系人) 和「HR 设置」(组织架构) 页签 -->
<xpath expr="//page[@name='public']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
<xpath expr="//page[@name='hr_settings']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
<!-- 追加「游戏数据」页签(后续世界数据等关联也放这里) -->
<xpath expr="//notebook" position="inside">
<page string="游戏数据" name="game_data">
<group>
+5 -6
View File
@@ -442,15 +442,14 @@
</field>
</record>
<!-- ============ 玩家(res.users) ============ -->
<!-- 独立顶级菜单:用 res.users 直接管理玩家,比 hr.employee 语义更准;
view_mode 默认 list 起步,不进入"新建"表单。 -->
<!-- ============ 玩家(hr.employee) ============ -->
<!-- 直接用 hr.employee 管理玩家,点进去就是员工列表,无需经 res.users 中转 -->
<record id="action_yt_player" model="ir.actions.act_window">
<field name="name">玩家</field>
<field name="res_model">res.users</field>
<field name="res_model">hr.employee</field>
<field name="view_mode">list,form</field>
<field name="context">{'search_default_active': 1, 'search_default_share': 0}</field>
<field name="help">游戏玩家列表。点「玩家世界」菜单可按用户筛其下属的全部世界/地块/微观/角色/生物等数据。</field>
<field name="context">{'search_default_active': 1}</field>
<field name="help">游戏玩家(员工)列表。每位玩家对应一个 hr.employee,关联其 Odoo 登录账户。</field>
</record>
<record id="view_yt_player_search" model="ir.ui.view">
+128
View File
@@ -124,4 +124,132 @@
<field name="note"><![CDATA[至高货币,不可增发。总量由世界本源锚定,仅可通过特定事件获取。]]></field>
</record>
<!-- ====================================================================
属性规则(战斗 / 成长核心数值维度)
说明:前端「规则」页的「属性规则」页签从此处读取。
为支持「基础 / 高级」分区,属性拆为两个类别:
attribute_basic (基础属性,seq=50)
attribute_advanced(高级属性,seq=51)
前端会把两者合并到同一个「属性规则」页签内,分两组展示。
每条属性 = 一条 game.rule:
name = 属性中文名(前端按名映射 emoji 图标)
desc = 给玩家的说明(是什么、怎么影响战局)
summary = 设计师基准(成长来源 / 公式提示,后台可见)
物攻 / 法攻决定输出,防御类(防御 / 物防 / 法防)按伤害类型减伤,
其余为速度、暴击、爆伤、命中闪避等衍生维度,共同构成单位战力模型。
==================================================================== -->
<record id="rule_category_attribute_basic" model="game.rule.category">
<field name="key">attribute_basic</field>
<field name="name">基础属性</field>
<field name="sequence">50</field>
</record>
<record id="rule_category_attribute_advanced" model="game.rule.category">
<field name="key">attribute_advanced</field>
<field name="name">高级属性</field>
<field name="sequence">51</field>
</record>
<!-- ===== 基础属性 ===== -->
<!-- 生存 -->
<record id="attr_hp" model="game.rule">
<field name="category_id" ref="rule_category_attribute_basic"/><field name="level">0</field><field name="name">生命值</field>
<field name="desc"><![CDATA[单位存活的血量上限。受到伤害时优先扣除,归零则该单位倒下。随等级、装备与回复技能提升。]]></field>
<field name="summary"><![CDATA[基础值随等级成长,装备/天赋加成]]></field>
</record>
<record id="attr_mp" model="game.rule">
<field name="category_id" ref="rule_category_attribute_basic"/><field name="level">0</field><field name="name">法力值</field>
<field name="desc"><![CDATA[释放技能与法术的消耗资源。普攻不耗蓝,技能按强度扣蓝;蓝量不足无法施法,脱战自然回复。]]></field>
<field name="summary"><![CDATA[技能消耗资源,脱战回复]]></field>
</record>
<!-- 攻击 -->
<record id="attr_atk" model="game.rule">
<field name="category_id" ref="rule_category_attribute_basic"/><field name="level">0</field><field name="name">物攻</field>
<field name="desc"><![CDATA[物理输出能力,决定普通攻击与物理系技能的基础伤害。越高打出越疼。]]></field>
<field name="summary"><![CDATA[物理伤害基数]]></field>
</record>
<record id="attr_mat" model="game.rule">
<field name="category_id" ref="rule_category_attribute_basic"/><field name="level">0</field><field name="name">法攻</field>
<field name="desc"><![CDATA[法术输出能力,决定法术技能的基础伤害。与物攻相互独立,受法强装备加成。]]></field>
<field name="summary"><![CDATA[法术伤害基数]]></field>
</record>
<!-- 防御 -->
<record id="attr_def" model="game.rule">
<field name="category_id" ref="rule_category_attribute_basic"/><field name="level">0</field><field name="name">防御</field>
<field name="desc"><![CDATA[通用物理减伤能力,是对物理伤害的笼统防护,多数装备与体质提供。与「物防」同源,物防为其专精项。]]></field>
<field name="summary"><![CDATA[物理减伤(通用)]]></field>
</record>
<record id="attr_pdef" model="game.rule">
<field name="category_id" ref="rule_category_attribute_basic"/><field name="level">0</field><field name="name">物防</field>
<field name="desc"><![CDATA[针对物理伤害的专项减免,由护甲、盾牌等装备提供。面对物理系敌人时主要看此项。]]></field>
<field name="summary"><![CDATA[物理伤害减免]]></field>
</record>
<record id="attr_mdef" model="game.rule">
<field name="category_id" ref="rule_category_attribute_basic"/><field name="level">0</field><field name="name">法防</field>
<field name="desc"><![CDATA[针对法术伤害的专项减免,由法抗装备、抗性符文提供。面对法师 / 术士时主要看此项。]]></field>
<field name="summary"><![CDATA[法术伤害减免]]></field>
</record>
<!-- 节奏 -->
<record id="attr_spd" model="game.rule">
<field name="category_id" ref="rule_category_attribute_basic"/><field name="level">0</field><field name="name">速度</field>
<field name="desc"><![CDATA[行动先后顺序与出手频率。速度高者先手,回合制中更频繁行动;也影响部分先攻技能触发。]]></field>
<field name="summary"><![CDATA[先攻 / 行动顺序]]></field>
</record>
<record id="attr_aspd" model="game.rule">
<field name="category_id" ref="rule_category_attribute_basic"/><field name="level">0</field><field name="name">攻速</field>
<field name="desc"><![CDATA[两次攻击的间隔,越短出手越快、单位时间输出越高。影响普攻与部分技能频率。]]></field>
<field name="summary"><![CDATA[攻击间隔]]></field>
</record>
<!-- ===== 高级属性 ===== -->
<!-- 暴击 -->
<record id="attr_crt" model="game.rule">
<field name="category_id" ref="rule_category_attribute_advanced"/><field name="level">0</field><field name="name">暴击</field>
<field name="desc"><![CDATA[造成暴击的概率(百分比)。暴击时伤害按「爆伤」倍率提升。]]></field>
<field name="summary"><![CDATA[暴击率 %]]></field>
</record>
<record id="attr_crtdmg" model="game.rule">
<field name="category_id" ref="rule_category_attribute_advanced"/><field name="level">0</field><field name="name">爆伤</field>
<field name="desc"><![CDATA[暴击时的伤害倍率(默认 150%)。暴击率越高,堆此项收益越大。]]></field>
<field name="summary"><![CDATA[暴击倍率 %]]></field>
</record>
<!-- 命中 / 闪避 -->
<record id="attr_acc" model="game.rule">
<field name="category_id" ref="rule_category_attribute_advanced"/><field name="level">0</field><field name="name">命中</field>
<field name="desc"><![CDATA[攻击命中的概率。面对高闪避目标需足够命中,否则出现「未命中」。]]></field>
<field name="summary"><![CDATA[命中率 %]]></field>
</record>
<record id="attr_eva" model="game.rule">
<field name="category_id" ref="rule_category_attribute_advanced"/><field name="level">0</field><field name="name">闪避</field>
<field name="desc"><![CDATA[躲避攻击的概率。高闪避可让对手攻击落空,但受对方命中压制。]]></field>
<field name="summary"><![CDATA[闪避率 %]]></field>
</record>
<!-- 减伤 -->
<record id="attr_blk" model="game.rule">
<field name="category_id" ref="rule_category_attribute_advanced"/><field name="level">0</field><field name="name">格挡</field>
<field name="desc"><![CDATA[受击时触发格挡、按比例减免伤害的概率(需持盾 / 特定姿态)。格挡成功大幅减伤。]]></field>
<field name="summary"><![CDATA[格挡率 %]]></field>
</record>
<!-- 异常 / 续航 -->
<record id="attr_res" model="game.rule">
<field name="category_id" ref="rule_category_attribute_advanced"/><field name="level">0</field><field name="name">抗性</field>
<field name="desc"><![CDATA[对灼烧、冰冻、中毒等异常状态的抵抗概率。高抗性降低被控与持续掉血风险。]]></field>
<field name="summary"><![CDATA[异常抵抗 %]]></field>
</record>
<record id="attr_lifesteal" model="game.rule">
<field name="category_id" ref="rule_category_attribute_advanced"/><field name="level">0</field><field name="name">吸血</field>
<field name="desc"><![CDATA[造成伤害时按百分比回复自身生命。持续作战与单挑续航的关键属性。]]></field>
<field name="summary"><![CDATA[吸血 %]]></field>
</record>
<record id="attr_luck" model="game.rule">
<field name="category_id" ref="rule_category_attribute_advanced"/><field name="level">0</field><field name="name">幸运</field>
<field name="desc"><![CDATA[影响暴击、掉落与稀有事件触发概率的隐性维度。高幸运带来更多意外之喜与关键转折,是冒险与博弈中的变数之源。]]></field>
<field name="summary"><![CDATA[幸运值(影响暴击/掉落/稀有事件)]]></field>
</record>
</odoo>
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

+12 -4
View File
@@ -6,9 +6,10 @@
let channels = [];
let activeChannel = null; // {id, name}
let lastId = 0; // 已渲染的最大消息 id(增量轮询基准)
let renderedIds = new Set(); // 已渲染消息 id,避免即时渲染与轮询重复
let pollTimer = null;
const TYPE_ICON = { world: '🌐', guild: '🛡️', pm: '💬' };
const TYPE_ICON = { world: '🌐', guild: '🛡️', pm: '💬', guide: '🤖' };
// ---------- 工具 ----------
function fmtTime(s) {
@@ -54,9 +55,12 @@
}
function renderMessage(m, prepend) {
if (renderedIds.has(m.id)) return; // 去重:即时渲染与轮询可能重复拉到同一消息
renderedIds.add(m.id);
const isGuide = !!m.is_guide;
const row = document.createElement('div');
row.className = 'row' + (m.is_self ? ' self' : '');
const initial = (m.author_name || '?').slice(0, 1);
row.className = 'row' + (m.is_self ? ' self' : '') + (isGuide ? ' guide' : '');
const initial = isGuide ? '🤖' : (m.author_name || '?').slice(0, 1);
row.innerHTML = `
<div class="ava">${escapeHtml(initial)}</div>
<div class="bubble-wrap">
@@ -99,8 +103,12 @@
async function selectChannel(ch) {
activeChannel = ch;
lastId = 0;
renderedIds.clear();
$('messages').innerHTML = '';
$('chTitle').textContent = ch.name;
$('chSub').textContent = ch.type === 'guide'
? '向宇森向导提问,了解属性 / 等级 / 货币 / 制度'
: '和大家聊聊宇森世界吧';
renderChannels();
setStatus('加载中…');
const data = await api(`/game/api/chat/messages?channel_id=${ch.id}&limit=50`);
@@ -138,8 +146,8 @@
btn.disabled = false;
if (data.ok) {
ta.value = '';
// 本地立即渲染(轮询也会拉到,靠 id 去重:renderMessage 只更新 lastId,不重复插入)
renderMessage(data.message, false);
if (data.guide_reply) renderMessage(data.guide_reply, false);
scrollBottom();
} else {
setStatus(data.error || '发送失败');
+18 -13
View File
@@ -81,43 +81,48 @@
.chat-head .sub { font-size: 12px; color: var(--txt-dim); }
.messages {
flex: 1; overflow-y: auto; padding: 22px 22px 8px;
display: flex; flex-direction: column; gap: 14px;
flex: 1 1 auto; max-height: 290px; overflow-y: auto; padding: 16px 22px 6px;
display: flex; flex-direction: column; gap: 10px;
scroll-behavior: smooth;
}
.row { display: flex; gap: 10px; max-width: 76%; }
.row { display: flex; gap: 8px; max-width: 76%; }
.row.self { align-self: flex-end; flex-direction: row-reverse; }
.row .ava {
width: 36px; height: 36px; border-radius: 10px; flex-shrink: 0;
width: 34px; height: 34px; border-radius: 10px; flex-shrink: 0;
background: var(--bg-card); display: flex; align-items: center; justify-content: center;
font-size: 13px; color: var(--txt-dim); border: 1px solid var(--line);
}
.row.self .ava { background: linear-gradient(135deg, var(--accent), #3b82f6); color: #06121a; border: none; }
.bubble-wrap { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.bubble-wrap { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.row.self .bubble-wrap { align-items: flex-end; }
.meta { font-size: 11px; color: var(--txt-dim); display: flex; gap: 8px; }
.bubble {
padding: 10px 14px; border-radius: 14px; line-height: 1.55;
padding: 8px 12px; border-radius: 13px; line-height: 1.5;
background: var(--bubble-other); color: var(--txt); font-size: 14px;
word-break: break-word; white-space: pre-wrap;
}
.row.self .bubble { background: var(--bubble-self); color: #06121a; font-weight: 500; }
/* AI 向导消息:紫色区分,与世界频道玩家消息隔离 */
.row.guide .ava { background: linear-gradient(135deg, #a78bfa, #7c3aed); color: #fff; border: none; }
.row.guide .bubble { background: linear-gradient(135deg, #2a2150, #3b2f63); color: #ede9fe; border: 1px solid #4c3a82; }
.row.guide .meta span:first-child { color: #c4b5fd; }
/* 输入区 */
.composer {
border-top: 1px solid var(--line); padding: 14px 18px;
display: flex; gap: 12px; align-items: flex-end; background: var(--bg-soft);
border-top: 1px solid var(--line); padding: 8px 14px;
display: flex; gap: 8px; align-items: flex-end; background: var(--bg-soft);
}
.composer textarea {
flex: 1; resize: none; height: 46px; max-height: 120px;
background: var(--bg-card); border: 1px solid var(--line); border-radius: 12px;
color: var(--txt); padding: 12px 14px; font-size: 14px; font-family: inherit;
flex: 1; resize: none; height: 34px; max-height: 120px;
background: var(--bg-card); border: 1px solid var(--line); border-radius: 10px;
color: var(--txt); padding: 7px 11px; font-size: 13.5px; font-family: inherit;
outline: none; transition: border-color .18s;
}
.composer textarea:focus { border-color: var(--accent); }
.composer button {
height: 46px; padding: 0 22px; border: none; border-radius: 12px;
background: var(--bubble-self); color: #06121a; font-weight: 600; font-size: 14px;
height: 34px; padding: 0 16px; border: none; border-radius: 10px;
background: var(--bubble-self); color: #06121a; font-weight: 600; font-size: 13px;
cursor: pointer; transition: transform .12s, filter .12s;
}
.composer button:hover { filter: brightness(1.06); }
+216 -33
View File
@@ -58,13 +58,18 @@ body::before{
}
.searchBox .clear:hover{opacity:1;color:#ffb4b4;}
/* 分类标签 */
.catTab{
padding:5px 14px;border-radius:999px;font-size:14px;cursor:pointer;
background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.08);color:#9fb3cc;transition:.18s;
/* ===== 分类页签(与规则页统一风格) ===== */
.rule-tabs{
display:flex;gap:10px;flex-wrap:wrap;justify-content:flex-start;
padding:6px 0 8px;
}
.catTab:hover{background:rgba(139,92,246,.14);color:#d0dfff;border-color:rgba(139,92,246,.3);}
.catTab.active{background:rgba(139,92,246,.2);color:#fff;border-color:rgba(139,92,246,.5);box-shadow:0 0 12px rgba(139,92,246,.2);}
.rule-tab{
padding:8px 20px;border-radius:999px;font-size:15px;cursor:pointer;user-select:none;
background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.08);color:#9fb3cc;transition:.18s;
font-family:"ZCOOL QingKe HuangYou",sans-serif;letter-spacing:1px;
}
.rule-tab:hover{background:rgba(139,92,246,.14);color:#d0dfff;border-color:rgba(139,92,246,.3);}
.rule-tab.active{background:rgba(139,92,246,.2);color:#fff;border-color:rgba(139,92,246,.5);box-shadow:0 0 12px rgba(139,92,246,.25);}
/* 品质筛点 */
.ql{display:inline-flex;align-items:center;gap:6px;cursor:pointer;padding:4px 11px;border-radius:8px;transition:.15s;}
@@ -107,12 +112,14 @@ body::before{
.card.q-rare::before{background:linear-gradient(90deg,#3b82f6,#60a5fa);}
.card.q-epic::before{background:linear-gradient(90deg,#a855f7,#c084fc);}
.card.q-legendary::before{background:linear-gradient(90deg,#f59e0b,#fbbf24);}
.card.q-mythic::before{background:linear-gradient(90deg,#dc2626,#f59e0b);}
/* 品质光晕 */
.card.q-common:hover{box-shadow:0 8px 24px rgba(0,0,0,.4),0 0 20px rgba(156,163,175,.08);}
.card.q-uncommon:hover{box-shadow:0 8px 24px rgba(0,0,0,.4),0 0 20px rgba(34,197,94,.12);}
.card.q-rare:hover{box-shadow:0 8px 24px rgba(0,0,0,.4),0 0 20px rgba(59,130,246,.12);}
.card.q-epic:hover{box-shadow:0 8px 24px rgba(0,0,0,.4),0 0 20px rgba(168,85,247,.12);}
.card.q-legendary:hover{box-shadow:0 8px 24px rgba(0,0,0,.4),0 0 20px rgba(245,158,11,.12);}
.card.q-mythic:hover{box-shadow:0 8px 24px rgba(0,0,0,.4),0 0 24px rgba(220,38,38,.18),0 0 12px rgba(245,158,11,.1);}
/* 品质角标 */
.card .qbadge{
@@ -125,6 +132,7 @@ body::before{
.qbadge.rare{background:rgba(59,130,246,.15);color:#60a5fa;border:1px solid rgba(59,130,246,.3);}
.qbadge.epic{background:rgba(168,85,247,.15);color:#c084fc;border:1px solid rgba(168,85,247,.3);}
.qbadge.legendary{background:rgba(245,158,11,.15);color:#fbbf24;border:1px solid rgba(245,158,11,.3);}
.qbadge.mythic{background:linear-gradient(135deg,rgba(220,38,38,.18),rgba(245,158,11,.18));color:#f87171;border:1px solid rgba(220,38,38,.35);text-shadow:0 0 6px rgba(245,158,11,.4);}
/* 生物/野怪/种族 专属样式 */
.card.creature{border-style:solid;border-color:rgba(100,200,255,.12);}
@@ -249,23 +257,19 @@ body::before{
<!-- ===== 筛选区 ===== -->
<div class="filters" id="filters">
<div class="filterRow">
<span class="filterLabel">分类</span>
<span class="catTab active" data-cat="all" onclick="setCat('all')">🏛️ 全部</span>
<span class="catTab" data-cat="material" onclick="setCat('material')">🪵 材料</span>
<span class="catTab" data-cat="processed" onclick="setCat('processed')">🏭 加工品</span>
<span class="catTab" data-cat="creature" onclick="setCat('creature')">🐾 生物</span>
<span class="catTab" data-cat="monster" onclick="setCat('monster')">👹 野怪</span>
<span class="catTab" data-cat="race" onclick="setCat('race')">🌍 种族</span>
<span class="catTab" data-cat="special" onclick="setCat('special')">✨ 特殊</span>
<div class="searchBox">
<span class="icon">🔍</span>
<input id="searchInput" type="text" placeholder="搜索…" />
<span class="clear" id="searchClear" onclick="clearSearch()" title="清空" style="display:none;">✕</span>
</div>
<div class="rule-tabs" id="catTabs">
<span class="rule-tab active" data-cat="all" onclick="setCat('all')">🏛️ 全部</span>
<span class="rule-tab" data-cat="material" onclick="setCat('material')">🪵 材料</span>
<span class="rule-tab" data-cat="processed" onclick="setCat('processed')">🏭 加工品</span>
<span class="rule-tab" data-cat="creature" onclick="setCat('creature')">🐾 生物</span>
<span class="rule-tab" data-cat="monster" onclick="setCat('monster')">👹 野怪</span>
<span class="rule-tab" data-cat="race" onclick="setCat('race')">🌍 种族</span>
<span class="rule-tab" data-cat="talent" onclick="setCat('talent')">🌟 天赋</span>
<span class="rule-tab" data-cat="skill_book" onclick="setCat('skill_book')">📖 技能书</span>
<span class="rule-tab" data-cat="book" onclick="setCat('book')">📚 书籍</span>
<span class="rule-tab" data-cat="special" onclick="setCat('special')">✨ 特殊</span>
</div>
<div class="filterRow">
<span class="filterLabel">品质</span>
<span class="ql active" data-ql="all" onclick="setQual('all')">
<span class="dot" style="background:linear-gradient(135deg,#9ca3af,#fbbf24);"></span>
<span class="lbl">全部</span>
@@ -290,6 +294,15 @@ body::before{
<span class="dot" style="background:#f59e0b;"></span>
<span class="lbl">传说</span>
</span>
<span class="ql" data-ql="mythic" onclick="setQual('mythic')">
<span class="dot" style="background:linear-gradient(135deg,#dc2626,#f59e0b);"></span>
<span class="lbl">神话</span>
</span>
<div class="searchBox">
<span class="icon">🔍</span>
<input id="searchInput" type="text" placeholder="搜索…" />
<span class="clear" id="searchClear" onclick="clearSearch()" title="清空" style="display:none;">✕</span>
</div>
</div>
</div>
@@ -306,10 +319,110 @@ body::before{
// 宇森图鉴 — 全部数据来自后端 Odoo (yt_world 模块),前端无硬编码默认数据
// =====================================================================
const QUALITIES = ['common','uncommon','rare','epic','legendary'];
const QUALITY_NAMES = { common:'普通', uncommon:'优秀', rare:'精良', epic:'史诗', legendary:'传说' };
const QUALITIES = ['common','uncommon','rare','epic','legendary','mythic'];
const QUALITY_NAMES = { common:'普通', uncommon:'优秀', rare:'精良', epic:'史诗', legendary:'传说', mythic:'神话' };
const TIER_NAMES = { normal:'普通野怪', elite:'精英野怪', boss:'Boss' };
const CAT_ORDER = ['material','processed','creature','monster','race','special'];
const CAT_ORDER = ['material','processed','creature','monster','race','talent','skill_book','book','special'];
// ===== 天赋预置数据(前端硬编码,后端就绪后迁移) =====
const TALENT_DATA = [
{ code:'talent_ancient_blood', name:'先民血脉', icon:'🩸', quality:'mythic',
trigger:'被动', effect:'生命回复+20%;濒死时触发先民护盾,5秒内免伤',
desc:'流淌着远古先民的血脉,身体拥有超乎寻常的自愈与守护之力。传说中先民以此血脉抵御了灭世之灾。' },
{ code:'talent_dragon_soul', name:'龙裔之魂', icon:'🐉', quality:'mythic',
trigger:'被动', effect:'火焰抗性+50%;攻击附带灼烧效果',
desc:'体内沉睡着巨龙的灵魂碎片,对火焰有着天然的亲和与免疫,攻击时能唤出龙息灼烧敌人。' },
{ code:'talent_star_resonance', name:'星辰共鸣', icon:'✨', quality:'legendary',
trigger:'被动', effect:'法力回复+15%;夜间额外+10%',
desc:'与星辰之力产生共鸣,能够汲取星光转化为法力。夜幕降临时,这份共鸣尤为强烈。' },
{ code:'talent_beast_king', name:'兽王本能', icon:'🐾', quality:'epic',
trigger:'被动', effect:'驯养生物好感度+30%;生物产出+20%',
desc:'天生与野兽亲和,能感知它们的情绪与需求。驯养的生物更加忠诚,产出也更加丰厚。' },
{ code:'talent_smith_hand', name:'铁匠之手', icon:'🔨', quality:'rare',
trigger:'被动', effect:'加工品品质+1阶;加工成功率+10%',
desc:'双手有着与生俱来的锻造天赋,加工材料时更加精准,成品的品质也往往超出预期。' },
{ code:'talent_forest_walker', name:'林间行者', icon:'🌿', quality:'rare',
trigger:'被动', effect:'森林地形移速+25%;隐蔽+15%',
desc:'在密林中如鱼得水,脚步轻盈无声,仿佛与森林融为一体。敌人很难在林间发现你的踪迹。' },
{ code:'talent_ore_sense', name:'矿脉感应', icon:'⛏️', quality:'uncommon',
trigger:'被动', effect:'发现稀有矿脉概率+20%',
desc:'对地底的矿脉有着敏锐的直觉,总能在别人忽略的地方发现珍贵的矿石。' },
{ code:'talent_harvest_blessing', name:'丰收祝福', icon:'🌾', quality:'uncommon',
trigger:'被动', effect:'农作物产量+15%;生长速度+10%',
desc:'受到大地之灵的祝福,经手种植的作物总是长势喜人,收获时也比旁人更加丰盈。' },
];
// ===== 技能书预置数据(前端硬编码,后端就绪后迁移) =====
const SKILL_BOOK_DATA = [
{ code:'sb_inferno_v1', name:'烈焰术·卷一', icon:'🔥', quality:'rare',
skillType:'主动', learnLevel:5, cost:'法力 20',
effect:'释放火焰法术,对前方3格内敌人造成灼烧伤害,持续4秒',
desc:'记载着基础火焰法术的入门卷轴。虽然只是卷一,但修习者能感受到火焰之力的澎湃。' },
{ code:'sb_frost_domain_frag', name:'冰霜领域·残卷', icon:'❄️', quality:'epic',
skillType:'主动', learnLevel:15, cost:'法力 50',
effect:'冻结周围3格内所有敌人,持续3秒,破冰时受到额外伤害',
desc:'残缺的古老卷轴,记载着冰霜领域的部分奥义。虽不完整,但冰封万物的威力已然惊人。' },
{ code:'sb_thunder_judgment_full', name:'雷霆审判·全本', icon:'⚡', quality:'mythic',
skillType:'主动', learnLevel:30, cost:'法力 80',
effect:'召唤天雷轰击目标区域,无视火焰/冰霜抗性,造成毁灭性雷击伤害',
desc:'完整的雷霆审判秘典。传说修习至大成者可引天雷降世,审判一切罪恶。非大毅力者不可修习。' },
{ code:'sb_ironwall_secret', name:'铁壁守护·秘典', icon:'🛡️', quality:'rare',
skillType:'被动', learnLevel:8, cost:'—',
effect:'物理减伤+15%;格挡率+10%',
desc:'铁壁一脉代代相传的守护秘典。修习者身如铁壁,能为同伴抵挡致命一击。' },
{ code:'sb_gale_step_frag', name:'疾风步·残页', icon:'💨', quality:'uncommon',
skillType:'主动', learnLevel:3, cost:'法力 10',
effect:'短暂提升移速40%,持续5秒,冷却15秒',
desc:'仅存数页的残破秘籍,记载着疾风步的基本步法。虽不完整,但足以让修习者步履如风。' },
{ code:'sb_life_drain_forbidden', name:'生命汲取·禁典', icon:'💀', quality:'legendary',
skillType:'被动', learnLevel:20, cost:'—',
effect:'攻击时回复造成伤害10%的生命;击杀时额外回复15%最大生命',
desc:'被列为禁术的黑暗典籍。修习者以杀养身,越战越强,但也容易迷失在嗜血的渴望中。慎之。' },
];
// ===== 书籍预置数据(前端硬编码,后端就绪后迁移) =====
const BOOK_DATA = [
{ code:'book_genesis', name:'宇森创世纪', icon:'📖', bookType:'历史',
author:'佚名·先民', chapters:12,
desc:'记载世界诞生的远古文献。从混沌初开到万物生长,从先民觉醒到百族兴起,宇森世界的起源尽在此书中。' },
{ code:'book_hundred_wars', name:'百族大战纪略', icon:'📜', bookType:'历史',
author:'史官·凌岳', chapters:8,
desc:'记录各族群之间那场旷日持久的远古战争。战争的起因、过程与结局,以及那些被遗忘的英雄与罪人。' },
{ code:'book_star_child', name:'星辰之子传说', icon:'✨', bookType:'传说',
author:'游吟诗人·散人', chapters:6,
desc:'流传于民间的英雄史诗。讲述一个诞生于星光之下的孩子,如何走出荒野、拯救苍生的故事。真伪已不可考。' },
{ code:'book_vein_walk', name:'地脉行走指南', icon:'🗺️', bookType:'秘闻',
author:'探险家·石锤', chapters:15,
desc:'记录地底矿脉走向与秘境入口的探险手册。地脉行走者的必备指南,据说按图索骥可寻得珍稀矿藏。' },
{ code:'book_codex_vol1', name:'万物图鉴·初卷', icon:'📔', bookType:'图鉴',
author:'图鉴师·青木', chapters:20,
desc:'汇编已知生物与资源的初步图鉴。虽只是初卷,却已涵盖数百种生灵与材料,是图鉴师的入门必读。' },
{ code:'book_ancient_remnant', name:'先民遗书·残篇', icon:'🔏', bookType:'秘闻',
author:'先民·佚名', chapters:3,
desc:'先民留下的残缺手稿,字迹斑驳难辨。隐约可见关于古老力量与禁忌之地的记载,暗藏着不为人知的秘密。' },
];
function normTalent(t){
return {
type:'talent', id:t.code, cat:'talent', name:t.name, icon:t.icon||'🌟',
qual:t.quality||'rare', trigger:t.trigger||'被动', effect:t.effect||'',
desc:t.description||t.desc||''
};
}
function normSkillBook(s){
return {
type:'skill_book', id:s.code, cat:'skill_book', name:s.name, icon:s.icon||'📖',
qual:s.quality||'common', skillType:s.skillType||'主动', learnLevel:s.learnLevel||1,
cost:s.cost||'—', effect:s.effect||'', desc:s.description||s.desc||''
};
}
function normBook(b){
return {
type:'book', id:b.code, cat:'book', name:b.name, icon:b.icon||'📚',
bookType:b.bookType||'其他', author:b.author||'佚名', chapters:b.chapters||0,
desc:b.description||b.desc||''
};
}
let ALL_ENTRIES = []; // 归一化后的全部条目(来自后端)
let currentCat = 'all';
@@ -319,7 +432,7 @@ let searchTerm = '';
// ---------- 筛选交互 ----------
function setCat(cat){
currentCat = cat;
document.querySelectorAll('.catTab').forEach(el=>el.classList.toggle('active',el.dataset.cat===cat));
document.querySelectorAll('.rule-tab').forEach(el=>el.classList.toggle('active',el.dataset.cat===cat));
renderGrid();
}
function setQual(ql){
@@ -392,6 +505,9 @@ async function loadCodexData(){
...(cre||[]).map(normCreature),
...(mon||[]).map(normMonster),
...(rac||[]).map(normRace),
...TALENT_DATA.map(normTalent),
...SKILL_BOOK_DATA.map(normSkillBook),
...BOOK_DATA.map(normBook),
];
renderGrid();
}catch(err){
@@ -406,12 +522,14 @@ function renderGrid(){
let filtered = ALL_ENTRIES.filter(e=>{
if(currentCat!=='all' && e.cat!==currentCat) return false;
if(q){
const hay = [e.name, e.desc, (e.products||[]).join(' '), (e.drops||[]).join(' '),
const hay = [e.name, e.desc, e.effect||'', e.trigger||'', e.skillType||'',
e.bookType||'', e.author||'', e.cost||'',
(e.products||[]).join(' '), (e.drops||[]).join(' '),
(e.traits||[]).join(' '), e.hab||'', e.layer||''].join(' ');
if(!hay.toLowerCase().includes(q)) return false;
}
if(currentQual!=='all'){
if(e.type!=='resource') return false;
if(e.type!=='resource' && e.type!=='talent' && e.type!=='skill_book') return false;
if(e.qual!==currentQual) return false;
}
return true;
@@ -470,6 +588,37 @@ function renderGrid(){
<span class="tag">${e.layer||''}</span>
</div>
`;
} else if(e.type==='talent'){
div.classList.add('q-'+e.qual, 'talent');
div.innerHTML = `
<span class="qbadge ${e.qual}">${QUALITY_NAMES[e.qual]}</span>
<div class="icon">${e.icon}</div>
<div class="name">${e.name}</div>
<div class="tags">
<span class="tag">${e.trigger||'被动'}</span>
</div>
`;
} else if(e.type==='skill_book'){
div.classList.add('q-'+e.qual, 'skill_book');
div.innerHTML = `
<span class="qbadge ${e.qual}">${QUALITY_NAMES[e.qual]}</span>
<div class="icon">${e.icon}</div>
<div class="name">${e.name}</div>
<div class="tags">
<span class="tag">${e.skillType||'主动'}</span>
<span class="tag">Lv ${e.learnLevel||1}</span>
</div>
`;
} else if(e.type==='book'){
div.classList.add('book');
div.innerHTML = `
<div class="icon">${e.icon}</div>
<div class="name">${e.name}</div>
<div class="tags">
<span class="tag">${e.bookType||'其他'}</span>
<span class="tag">${(e.chapters||0)}章</span>
</div>
`;
}
div.addEventListener('click',()=>showDetail(e));
@@ -502,11 +651,12 @@ function showDetail(e){
html += `<div class="md-header">`;
html += `<div class="md-icon">${e.icon}</div>`;
html += `<div class="md-info"><div class="md-title">${e.name}</div><div class="md-meta">`;
if(e.type==='resource'){
const c = e.qual==='common'?'156,163,175':e.qual==='uncommon'?'34,197,94':e.qual==='rare'?'59,130,246':e.qual==='epic'?'168,85,247':'245,158,11';
const t = e.qual==='common'?'#9ca3af':e.qual==='uncommon'?'#4ade80':e.qual==='rare'?'#60a5fa':e.qual==='epic'?'#c084fc':'#fbbf24';
if(e.type==='resource' || e.type==='talent' || e.type==='skill_book'){
const c = e.qual==='common'?'156,163,175':e.qual==='uncommon'?'34,197,94':e.qual==='rare'?'59,130,246':e.qual==='epic'?'168,85,247':e.qual==='legendary'?'245,158,11':'220,38,38';
const t = e.qual==='common'?'#9ca3af':e.qual==='uncommon'?'#4ade80':e.qual==='rare'?'#60a5fa':e.qual==='epic'?'#c084fc':e.qual==='legendary'?'#fbbf24':'#f87171';
html += `<span class="mt" style="background:rgba(${c},.15);color:${t}">${QUALITY_NAMES[e.qual]}</span>`;
html += `<span class="mt">${e.cat==='material'?'材料':e.cat==='processed'?'加工品':'特殊'}</span>`;
const catLabel = e.type==='talent'?'🌟 天赋':e.type==='skill_book'?'📖 技能书':e.cat==='material'?'材料':e.cat==='processed'?'加工品':'特殊';
html += `<span class="mt">${catLabel}</span>`;
} else if(e.type==='creature'){
html += `<span class="mt">🐾 生物</span><span class="mt">${e.hab||''}</span><span class="mt">${e.diet||''}</span><span class="mt">${e.tame?'✅ 可驯养':'❌ 野性'}</span>`;
} else if(e.type==='monster'){
@@ -519,8 +669,8 @@ function showDetail(e){
}
html += `</div></div></div>`;
// 品质等级条(资源)
if(e.type==='resource'){
// 品质等级条(资源 / 天赋 / 技能书)
if(e.type==='resource' || e.type==='talent' || e.type==='skill_book'){
html += `<div class="qualityBar">`;
QUALITIES.forEach((q,i)=>{ html += `<div class="qlv ${i<=qi?'on':''}" title="${QUALITY_NAMES[q]}"></div>`; });
html += `</div>`;
@@ -581,6 +731,39 @@ function showDetail(e){
}
}
// 天赋详情
if(e.type==='talent'){
html += `<div class="md-section"><h3>📋 属性</h3><div class="md-grid">`;
html += `<div class="gi"><span class="gl">触发方式</span><span class="gv">${e.trigger||'被动'}</span></div>`;
html += `<div class="gi"><span class="gl">品质</span><span class="gv">${QUALITY_NAMES[e.qual]||'—'}</span></div>`;
html += `</div></div>`;
if(e.effect){
html += `<div class="md-section"><h3>⚡ 效果</h3><div class="md-desc" style="color:#f0e6d3;">${e.effect}</div></div>`;
}
}
// 技能书详情
if(e.type==='skill_book'){
html += `<div class="md-section"><h3>📋 属性</h3><div class="md-grid">`;
html += `<div class="gi"><span class="gl">技能类型</span><span class="gv">${e.skillType||'主动'}</span></div>`;
html += `<div class="gi"><span class="gl">学习等级</span><span class="gv">Lv ${e.learnLevel||1}</span></div>`;
html += `<div class="gi"><span class="gl">消耗</span><span class="gv">${e.cost||'—'}</span></div>`;
html += `<div class="gi"><span class="gl">品质</span><span class="gv">${QUALITY_NAMES[e.qual]||'—'}</span></div>`;
html += `</div></div>`;
if(e.effect){
html += `<div class="md-section"><h3>⚡ 技能效果</h3><div class="md-desc" style="color:#f0e6d3;">${e.effect}</div></div>`;
}
}
// 书籍详情
if(e.type==='book'){
html += `<div class="md-section"><h3>📋 属性</h3><div class="md-grid">`;
html += `<div class="gi"><span class="gl">类型</span><span class="gv">${e.bookType||'其他'}</span></div>`;
html += `<div class="gi"><span class="gl">作者</span><span class="gv">${e.author||'佚名'}</span></div>`;
html += `<div class="gi"><span class="gl">章节数</span><span class="gv">${e.chapters||0} 章</span></div>`;
html += `</div></div>`;
}
html += '</div>';
box.innerHTML = html;
overlay.classList.add('open');
+176 -105
View File
@@ -8,13 +8,25 @@
<link rel="icon" type="image/png" sizes="48x48" href="assets/ui/logo_planet_48.png?v=20260723">
<link rel="stylesheet" href="assets/common.css" />
<style>
.rules-scroll{max-width:1080px;width:100%;margin:0 auto;display:flex;flex-direction:column;gap:34px;padding:30px 48px;}
.rule-block{}
.rule-block h3{position:relative;display:inline-block;font-family:"ZCOOL QingKe HuangYou",sans-serif;font-size:28px;color:#fff;letter-spacing:3px;margin:0 0 16px;padding-bottom:11px;
text-shadow:0 2px 14px rgba(139,92,246,.35);}
.rule-block h3::after{content:'';position:absolute;left:0;bottom:0;width:100%;height:3px;border-radius:3px;
background:linear-gradient(90deg,var(--gold),var(--violet));}
.rule-intro{font-size:17px;color:var(--text-dim);margin-bottom:18px;line-height:1.95;max-width:860px;}
/* ===== 页签 ===== */
.rule-tabs{
display:flex;gap:10px;flex-wrap:wrap;justify-content:flex-start;
max-width:1080px;width:100%;margin:24px auto 0;padding:0 24px;
}
.rule-tab{
padding:8px 22px;border-radius:999px;font-size:15px;cursor:pointer;user-select:none;
background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.08);color:#9fb3cc;transition:.18s;
font-family:"ZCOOL QingKe HuangYou",sans-serif;letter-spacing:1px;
}
.rule-tab:hover{background:rgba(139,92,246,.14);color:#d0dfff;border-color:rgba(139,92,246,.3);}
.rule-tab.active{background:rgba(139,92,246,.2);color:#fff;border-color:rgba(139,92,246,.5);box-shadow:0 0 12px rgba(139,92,246,.25);}
.rules-scroll{max-width:1080px;width:100%;margin:0 auto;display:flex;flex-direction:column;gap:34px;padding:26px 48px 40px;}
.rule-panel{display:none;}
.rule-panel.active{display:block;}
.rule-block{margin-top:6px;}
.rule-intro{font-size:17px;color:var(--text-dim);margin-bottom:18px;line-height:1.95;max-width:880px;}
.rule-intro b{color:#fff;}
.tier-list{display:flex;flex-direction:column;gap:10px;}
.tier-row{display:grid;grid-template-columns:104px 124px 1fr;gap:16px;align-items:center;background:var(--card);border:1px solid var(--card-bd);border-radius:12px;padding:14px 18px;transition:.2s;}
@@ -23,9 +35,23 @@
.tier-row .tname{font-size:18px;color:#fff;font-weight:600;}
.tier-row .tdesc{font-size:16.5px;color:var(--text-dim);line-height:1.7;}
/* ===== 属性卡片网格 ===== */
.attr-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(248px,1fr));gap:14px;margin-top:4px;}
.attr-sub{margin:22px 0 10px;}
.attr-sub:first-of-type{margin-top:14px;}
.attr-sub-label{display:inline-block;font-family:"ZCOOL QingKe HuangYou",sans-serif;font-size:21px;color:#fff;letter-spacing:2px;padding-bottom:6px;border-bottom:2px solid rgba(139,92,246,.45);text-shadow:0 2px 12px rgba(139,92,246,.3);}
.attr-card{background:var(--card);border:1px solid var(--card-bd);border-radius:14px;padding:16px 18px;transition:.2s;}
.attr-card:hover{border-color:var(--violet);background:rgba(139,92,246,.08);box-shadow:var(--glow);transform:translateY(-2px);}
.attr-card .ah{display:flex;align-items:center;gap:11px;margin-bottom:9px;}
.attr-card .ai{font-size:28px;line-height:1;}
.attr-card .an{font-size:18px;font-weight:700;color:#fff;}
.attr-card .as{font-size:11px;color:var(--cyan);margin-bottom:7px;font-family:"Press Start 2P",monospace;letter-spacing:.5px;}
.attr-card .ad{font-size:14.5px;color:var(--text-dim);line-height:1.72;}
@media (max-width:768px){
.rules-scroll{padding:20px 18px;}
.tier-row{grid-template-columns:72px 92px 1fr;gap:8px;}
.attr-grid{grid-template-columns:repeat(auto-fill,minmax(150px,1fr));}
}
@media (max-width:480px){
.tier-row{grid-template-columns:60px 76px 1fr;gap:6px;}
@@ -39,7 +65,7 @@
<nav class="nav">
<a href="announce.html">公告</a>
<a href="background.html">背景</a>
<a href="rules.html">规则</a>
<a href="rules.html" class="active">规则</a>
<a href="codex.html">图鉴</a>
<a href="story.html">剧情</a>
<a href="forum.html" class="auth-only">论坛</a>
@@ -50,112 +76,157 @@
</header>
<main class="page-content hide-scroll">
<div class="rules-scroll" id="rulesBody">
<!-- JS 渲染:优先后端 API.rules(),兜底硬编码 -->
</div>
<div class="rule-tabs" id="ruleTabs"></div>
<div class="rules-scroll" id="rulePanels"><!-- JS 渲染:优先后端 API.rules(),兜底硬编码 --></div>
</main>
</div>
<script src="assets/game-api.js"></script>
<script src="assets/common.js"></script>
<script>
// 规则数据:先尝试从后端拉取,失败则用本地硬编码
async function loadRules(){
const el = document.getElementById('rulesBody');
if (!el) return;
// 属性名 -> emoji(前端映射,后台只存 name/desc/summary)
const ATTR_ICON = {
'生命值':'❤️','法力值':'🔷','物攻':'⚔️','法攻':'🔥','防御':'🛡️','物防':'🪖','法防':'🔮',
'速度':'💨','攻速':'⚡','暴击':'💥','爆伤':'💢','命中':'🎯','闪避':'🌫️',
'格挡':'🛡','抗性':'🌟','吸血':'🩸','幸运':'🍀'
};
// 类别 key -> 页签名
const TAB_LABEL = { world:'世界等级', character:'角色等级', currency:'货币等级', attribute_basic:'基础属性', attribute_advanced:'高级属性' };
// 尝试后端
const SYS_HTML = `<div class="rule-block">
<p class="rule-intro">三者相互独立又彼此加成:<b style="color:#fff">世界等级</b>是舞台上限,决定能发生什么;<b style="color:#fff">势力等级</b>是群体进度,决定能调动什么;<b style="color:#fff">角色等级</b>是个体成长,决定能做成什么。园丁的核心策略,是在三者间分配有限的引导资源,让文明自洽地向上演化。<br><br>跃迁逻辑:角色历练与装备积累→提升势力实力→推动世界阶段跃迁→解锁更深内容(更深的地下层、更远的星空层、更强的神器)。每一级提升都会扩大可叙事的事件池,世界因此越活越厚。</p></div>`;
// 硬编码兜底(与后端 rule_data.xml 对齐;后端不可达时使用)
const FALLBACK = {
groups: [
{ key:'world', label:'世界等级', items:[
{level:1,name:'鸿荒',desc:'万物生灵尚处蒙昧凡土,天地灵气稀薄,各族散居荒野,无族群联盟与政权,只求狩猎采集求生,一切秩序遵循自然法则。'},
{level:2,name:'启明',desc:'天地灵气复苏觉醒,各族聚拢形成聚落,诞生劳作分工、屋舍营建与物物交换,不同族群率先凝聚部族,世间初代势力自此诞生。'},
{level:3,name:'鼎世',desc:'各族部落兼并融合,广袤大地形成多元族群共治的完整疆域,征伐、道统、造物三大发展路线并行;各族大能、上古秘闻等传奇事件不断现世,鼎世文明迎来鼎盛割据时代。'},
{level:4,name:'寰界',desc:'生灵突破地表桎梏,打通地底深渊与浩瀚星空,天地上下各界尽数纳入族群管辖;各类上古神器、本命法器完整解锁,文明疆域遍布天地穹苍。'},
{level:5,name:'归元',desc:'各族顶尖文明勘破这片天地的诞生本源,挣脱本世界规则束缚,可踏上飞升之路、往复轮回,前往更高维度开启全新文明演化。'},
]},
{ key:'character', label:'人物角色等级', items:[
{level:1,name:'赤子',desc:'初入世界的无名者。'},{level:2,name:'稚民',desc:'掌握基础技艺。'},
{level:3,name:'劳者',desc:'可独立完成任务。'},{level:4,name:'巧匠',desc:'在某领域崭露头角。'},
{level:5,name:'贤能',desc:'群体中的中坚。'},{level:6,name:'巨擘',desc:'技艺臻于化境。'},
{level:7,name:'宗师',desc:'开宗立派的人物。'},{level:8,name:'传奇',desc:'事迹被载入编年史。'},
{level:9,name:'神话',desc:'近乎传说的存在。'},{level:10,name:'半神',desc:'触及世界法则。'},
{level:11,name:'圣灵',desc:'被信仰托举的意志。'},{level:12,name:'创世',desc:'超越生死的世界锚点。'},
]},
{ key:'currency', label:'货币等级', items:[
{level:1,name:'铜币',desc:'凡俗通用零钱,以贝壳、骨片、碎石铸成。100 铜币 = 1 银币。'},
{level:2,name:'银币',desc:'蕴含微量灵气的金属铸币,聚落以上势力日常使用。100 银币 = 1 金币。'},
{level:3,name:'金币',desc:'刻有鼎纹的稀有锭块,用于高价值交易与功法图纸。100 金币 = 1 紫金。'},
{level:4,name:'紫金',desc:'产自地底深渊的幽暗玉石,蕴含浓郁能量。100 紫金 = 1 星辉。'},
{level:5,name:'星辉',desc:'来自星空矿脉的稀有鎏金晶体,跨层贸易硬通货。100 星辉 = 1 宇玺。'},
{level:6,name:'宇玺',desc:'追溯世界本源凝聚而成的至高货币,可兑换神器、传说事件与维度权限。'},
]},
{ key:'attribute_basic', label:'基础属性', items:[
{name:'生命值',summary:'基础值随等级成长,装备/天赋加成',desc:'单位存活的血量上限。受到伤害时优先扣除,归零则该单位倒下。随等级、装备与回复技能提升。'},
{name:'法力值',summary:'技能消耗资源,脱战回复',desc:'释放技能与法术的消耗资源。普攻不耗蓝,技能按强度扣蓝;蓝量不足无法施法,脱战自然回复。'},
{name:'物攻',summary:'物理伤害基数',desc:'物理输出能力,决定普通攻击与物理系技能的基础伤害。越高打出越疼。'},
{name:'法攻',summary:'法术伤害基数',desc:'法术输出能力,决定法术技能的基础伤害。与物攻相互独立,受法强装备加成。'},
{name:'防御',summary:'物理减伤(通用)',desc:'通用物理减伤能力,是对物理伤害的笼统防护,多数装备与体质提供。与「物防」同源,物防为其专精项。'},
{name:'物防',summary:'物理伤害减免',desc:'针对物理伤害的专项减免,由护甲、盾牌等装备提供。面对物理系敌人时主要看此项。'},
{name:'法防',summary:'法术伤害减免',desc:'针对法术伤害的专项减免,由法抗装备、抗性符文提供。面对法师 / 术士时主要看此项。'},
{name:'速度',summary:'先攻 / 行动顺序',desc:'行动先后顺序与出手频率。速度高者先手,回合制中更频繁行动;也影响部分先攻技能触发。'},
{name:'攻速',summary:'攻击间隔',desc:'两次攻击的间隔,越短出手越快、单位时间输出越高。影响普攻与部分技能频率。'},
]},
{ key:'attribute_advanced', label:'高级属性', items:[
{name:'暴击',summary:'暴击率 %',desc:'造成暴击的概率(百分比)。暴击时伤害按「爆伤」倍率提升。'},
{name:'爆伤',summary:'暴击倍率 %',desc:'暴击时的伤害倍率(默认 150%)。暴击率越高,堆此项收益越大。'},
{name:'命中',summary:'命中率 %',desc:'攻击命中的概率。面对高闪避目标需足够命中,否则出现「未命中」。'},
{name:'闪避',summary:'闪避率 %',desc:'躲避攻击的概率。高闪避可让对手攻击落空,但受对方命中压制。'},
{name:'格挡',summary:'格挡率 %',desc:'受击时触发格挡、按比例减免伤害的概率(需持盾 / 特定姿态)。格挡成功大幅减伤。'},
{name:'抗性',summary:'异常抵抗 %',desc:'对灼烧、冰冻、中毒等异常状态的抵抗概率。高抗性降低被控与持续掉血风险。'},
{name:'吸血',summary:'吸血 %',desc:'造成伤害时按百分比回复自身生命。持续作战与单挑续航的关键属性。'},
{name:'幸运',summary:'幸运值(影响暴击/掉落/稀有事件)',desc:'影响暴击、掉落与稀有事件触发概率的隐性维度。高幸运带来更多意外之喜与关键转折,是冒险与博弈中的变数之源。'},
]},
]
};
function stripHtml(s){ return (s||'').replace(/<[^>]+>/g,''); }
function renderTierPanel(g, active){
const rows = (g.items||[]).map(it=>{
const lvl = (it.level != null && it.level>0) ? ('Lv.'+it.level) : (g.key==='world' ? '阶段' : '');
return `<div class="tier-row"><span class="tn">${lvl}</span><span class="tname">${it.name}</span><span class="tdesc">${stripHtml(it.desc)}</span></div>`;
}).join('');
return `<div class="rule-panel${active}" data-panel="${g.key}">
<div class="rule-block">
<div class="tier-list">${rows}</div></div></div>`;
}
function renderAttrCard(it){
const icon = ATTR_ICON[it.name] || '📊';
const sum = stripHtml(it.summary);
return `<div class="attr-card"><div class="ah"><span class="ai">${icon}</span><span class="an">${it.name}</span></div>`
+ (sum?`<div class="as">${sum}</div>`:'')
+ `<div class="ad">${stripHtml(it.desc)}</div></div>`;
}
function renderAttrPanel(attrGroups, active){
const basic = (attrGroups.find(g=>g.key==='attribute_basic')||{}).items || [];
const adv = (attrGroups.find(g=>g.key==='attribute_advanced')||{}).items || [];
let html = `<div class="rule-panel${active}" data-panel="attribute"><div class="rule-block">`
+ `<p class="rule-intro">战斗与成长的核心数值维度。物攻 / 法攻决定输出,防御类(防御 / 物防 / 法防)按伤害类型减伤,辅以速度、暴击、爆伤、命中闪避等衍生维度,共同构成单位的战力模型。</p>`;
if (basic.length){
html += `<div class="attr-sub"><span class="attr-sub-label">基础属性</span></div><div class="attr-grid">`
+ basic.map(renderAttrCard).join('') + `</div>`;
}
if (adv.length){
html += `<div class="attr-sub"><span class="attr-sub-label">高级属性</span></div><div class="attr-grid">`
+ adv.map(renderAttrCard).join('') + `</div>`;
}
html += `</div></div>`;
return html;
}
function renderGroups(groups){
let tabs = '', panels = '';
const attrGroups = groups.filter(g => g.key==='attribute_basic' || g.key==='attribute_advanced');
const otherGroups = groups.filter(g => g.key!=='attribute_basic' && g.key!=='attribute_advanced');
let first = true;
otherGroups.forEach(g=>{
const label = TAB_LABEL[g.key] || g.label || g.key;
const active = first ? ' active' : '';
tabs += `<span class="rule-tab${active}" data-key="${g.key}" onclick="switchRuleTab('${g.key}')">${label}</span>`;
panels += renderTierPanel(g, active);
first = false;
});
// 属性规则:单页签内分区(基础 / 高级)
if (attrGroups.length){
const active = first ? ' active' : '';
tabs += `<span class="rule-tab${active}" data-key="attribute" onclick="switchRuleTab('attribute')">属性规则</span>`;
panels += renderAttrPanel(attrGroups, active);
first = false;
}
// 制度说明(静态 tab)
tabs += `<span class="rule-tab" data-key="__sys" onclick="switchRuleTab('__sys')">制度说明</span>`;
panels += `<div class="rule-panel" data-panel="__sys">${SYS_HTML}</div>`;
document.getElementById('ruleTabs').innerHTML = tabs;
document.getElementById('rulePanels').innerHTML = panels;
}
function switchRuleTab(key){
document.querySelectorAll('.rule-tab').forEach(el=>el.classList.toggle('active', el.dataset.key===key));
document.querySelectorAll('.rule-panel').forEach(el=>el.classList.toggle('active', el.dataset.panel===key));
}
async function loadRules(){
let groups = [];
try {
const d = await API.rules();
const groups = (d && d.groups) || [];
const ruleGroups = groups.filter(g => (g.key||'').toLowerCase() !== 'faction');
if (ruleGroups.length && ruleGroups.some(g => (g.items||[]).length > 0)) {
let html = ruleGroups.map(g => {
const title = g.label || g.title || g.key;
const rows = (g.items || []).map(it => {
const lvl = (it.level != null) ? ('Lv.' + it.level) : (g.key === 'world' ? '阶段' : '');
// desc 可能含 <p> 标签,提取纯文本
const desc = (it.desc || '').replace(/<[^>]+>/g, '');
return `<div class="tier-row"><span class="tn">${lvl}</span><span class="tname">${it.name}</span><span class="tdesc">${desc}</span></div>`;
}).join('');
const intro = (g.intro || '').replace(/<[^>]+>/g, '');
return `<div class="rule-block"><h3>${title}</h3>` +
(intro ? `<p class="rule-intro">${intro}</p>` : '') +
`<div class="tier-list">${rows}</div></div>`;
}).join('');
// 补上后端没有的「制度说明」段
html += `
<div class="rule-block">
<h3>④ 制度说明</h3>
<p class="rule-intro">三者相互独立又彼此加成:<b style="color:#fff">世界等级</b>是舞台上限,决定能发生什么;<b style="color:#fff">势力等级</b>是群体进度,决定能调动什么;<b style="color:#fff">角色等级</b>是个体成长,决定能做成什么。园丁的核心策略,是在三者间分配有限的引导资源,让文明自洽地向上演化。<br><br>跃迁逻辑:角色历练与装备积累→提升势力实力→推动世界阶段跃迁→解锁更深内容(更深的地下层、更远的星空层、更强的神器)。每一级提升都会扩大可叙事的事件池,世界因此越活越厚。</p>
</div>`;
el.innerHTML = html;
return;
}
} catch(e){ /* 后端失败走兜底 */ }
// 本地硬编码兜底(完整中文内容)
el.innerHTML = `
<div class="rule-block">
<h3>① 世界等级 · 五阶段</h3>
<p class="rule-intro">世界从鸿荒到归元的整体进化阶段,决定可解锁的内容深度与事件池上限。园丁对世界施加的影响会推动阶段跃迁——这是文明能"发生什么"的舞台上限。</p>
<div class="tier-list">
<div class="tier-row"><span class="tn">阶段一</span><span class="tname">鸿荒</span><span class="tdesc">万物蒙昧,文明种子落地未醒;园丁穷神开局,仅有创世原一簇火的可能。</span></div>
<div class="tier-row"><span class="tn">阶段二</span><span class="tname">启明</span><span class="tdesc">聚落与火光初现,名与语、工与棚成形,第一代秩序萌芽。</span></div>
<div class="tier-row"><span class="tn">阶段三</span><span class="tname">鼎世</span><span class="tdesc">多族共治、疆域成形,十五大系统解锁,文明可自循环。</span></div>
<div class="tier-row"><span class="tn">阶段四</span><span class="tname">寰界</span><span class="tdesc">天地贯通,地下深渊与浩瀚星空尽纳管辖,跨界面作战。</span></div>
<div class="tier-row"><span class="tn">阶段五</span><span class="tname">归元</span><span class="tdesc">勘破本源、超脱规则束缚,可创世飞升,往更高维度演化。</span></div>
</div>
</div>
<!-- 势力等级(暂时隐藏:② 势力等级整段暂隐,待用户要求恢复时取消此注释即可;后端 game_base_data.xml 的 faction 数据未动)
<div class="rule-block">
<h3>② 势力等级</h3>
<p class="rule-intro">文明内部群体的聚合度,由人口、领土、影响力累计。势力等级越高,可调动的世界资源与外交权重越大——决定文明"能调动什么"。</p>
<div class="tier-list">
<div class="tier-row"><span class="tn">Lv.1</span><span class="tname">鸿荒·种族</span><span class="tdesc">血缘小群体,蛮荒无制度。</span></div>
<div class="tier-row"><span class="tn">Lv.2</span><span class="tname">启明·部盟</span><span class="tdesc">灵气聚落联盟,初步治理与贸易。</span></div>
<div class="tier-row"><span class="tn">Lv.3</span><span class="tname">鼎世·王国</span><span class="tdesc">疆域整合,军法、道规体系完善。</span></div>
<div class="tier-row"><span class="tn">Lv.4</span><span class="tname">大同·联邦</span><span class="tdesc">跨族群共治,制度与传奇并行。</span></div>
<div class="tier-row"><span class="tn">Lv.5</span><span class="tname">寰界·天盟</span><span class="tdesc">天地大陆霸主,穿梭星空深渊作战。</span></div>
<div class="tier-row"><span class="tn">Lv.6</span><span class="tname">归元·宇盟</span><span class="tdesc">多维文明联合体,触及世界本源博弈。</span></div>
</div>
</div>
-->
<div class="rule-block">
<h3>③ 人物角色等级 · 十二阶段</h3>
<p class="rule-intro">个体(英雄 / 领袖 / 特殊 NPC)的成长阶序,通过历练、天赋点亮与装备积累提升,影响可承担的职责与战力——决定个体"能做成什么"。</p>
<div class="tier-list">
<div class="tier-row"><span class="tn">Lv.1</span><span class="tname">赤子</span><span class="tdesc">初入世界的无名者。</span></div>
<div class="tier-row"><span class="tn">Lv.2</span><span class="tname">稚民</span><span class="tdesc">习得基础生存。</span></div>
<div class="tier-row"><span class="tn">Lv.3</span><span class="tname">劳者</span><span class="tdesc">能独立劳作。</span></div>
<div class="tier-row"><span class="tn">Lv.4</span><span class="tname">巧匠</span><span class="tdesc">某一技之长崭露。</span></div>
<div class="tier-row"><span class="tn">Lv.5</span><span class="tname">贤能</span><span class="tdesc">群体里的骨干。</span></div>
<div class="tier-row"><span class="tn">Lv.6</span><span class="tname">巨擘</span><span class="tdesc">一方领袖。</span></div>
<div class="tier-row"><span class="tn">Lv.7</span><span class="tname">宗师</span><span class="tdesc">开宗立派的人物。</span></div>
<div class="tier-row"><span class="tn">Lv.8</span><span class="tname">传奇</span><span class="tdesc">事迹载入编年史。</span></div>
<div class="tier-row"><span class="tn">Lv.9</span><span class="tname">神话</span><span class="tdesc">近乎传说的存在。</span></div>
<div class="tier-row"><span class="tn">Lv.10</span><span class="tname">半神</span><span class="tdesc">触及世界法则。</span></div>
<div class="tier-row"><span class="tn">Lv.11</span><span class="tname">圣灵</span><span class="tdesc">被信仰托举的意志。</span></div>
<div class="tier-row"><span class="tn">Lv.12</span><span class="tname">创世</span><span class="tdesc">超越生死的世界锚点。</span></div>
</div>
</div>
<div class="rule-block">
<h3>④ 制度说明</h3>
<p class="rule-intro">三者相互独立又彼此加成:<b style="color:#fff">世界等级</b>是舞台上限,决定能发生什么;<b style="color:#fff">势力等级</b>是群体进度,决定能调动什么;<b style="color:#fff">角色等级</b>是个体成长,决定能做成什么。园丁的核心策略,是在三者间分配有限的引导资源,让文明自洽地向上演化。<br><br>跃迁逻辑:角色历练与装备积累→提升势力实力→推动世界阶段跃迁→解锁更深内容(更深的地下层、更远的星空层、更强的神器)。每一级提升都会扩大可叙事的事件池,世界因此越活越厚。</p>
</div>
<div class="rule-block">
<h3>⑤ 货币等级 · 六档</h3>
<p class="rule-intro">文明流通的价值尺度,从凡俗零钱到本源货币,越稀有的货币可交易的资源层级越高——决定文明"能交换什么"。<br><br><b style="color:#fff">兑换规则:</b>100 低等级货币 = 1 高等级货币。</p>
<div class="tier-list">
<div class="tier-row"><span class="tn">Lv.1</span><span class="tname">铜币</span><span class="tdesc">凡俗零钱,铜铸。<b>100 铜币 = 1 银币</b></span></div>
<div class="tier-row"><span class="tn">Lv.2</span><span class="tname">银币</span><span class="tdesc">聚落通用,银锭。<b>100 银币 = 1 金币</b></span></div>
<div class="tier-row"><span class="tn">Lv.3</span><span class="tname">金币</span><span class="tdesc">疆域通行,足金。<b>100 金币 = 1 紫金</b></span></div>
<div class="tier-row"><span class="tn">Lv.4</span><span class="tname">紫金</span><span class="tdesc">地下深层贵金,泛紫光。<b>100 紫金 = 1 星辉</b></span></div>
<div class="tier-row"><span class="tn">Lv.5</span><span class="tname">星辉</span><span class="tdesc">星空层交易,凝星辉之力。<b>100 星辉 = 1 宇玺</b></span></div>
<div class="tier-row"><span class="tn">Lv.6</span><span class="tname">宇玺</span><span class="tdesc">本源结晶铸的最高货币,仅用于传说级交易与飞升仪式。</span></div>
</div>
</div>
`;
groups = (d && d.groups) || [];
} catch(e){ groups = []; }
// 排除刻意隐藏的「势力等级」,且无条目的类别不显示
const visible = groups.filter(g => (g.key||'') !== 'faction' && (g.items||[]).length);
if (!visible.length) {
renderGroups(FALLBACK.groups);
return;
}
renderGroups(visible);
}
loadRules();
</script>
+847 -215
View File
File diff suppressed because it is too large Load Diff
+73
View File
@@ -0,0 +1,73 @@
# yuthon 微世界 · 最小可玩闭环规划
> 目标:把"已定义但没接活"的规则(属性/等级/货币)真正驱动玩法,让玩家登录后有 **目标 → 动作 → 可感知成长** 的循环。后台让运营能配置规则、管理玩家进度,而不是只能看静态文案。
> 原则:先打通 P0 最小闭环(可玩性),再补 P1 后台运营能力,P2 扩展社交/战斗。
---
## 一、最小可玩闭环(单一场景讲清)
玩家每天登录 → 收到 1 个每日任务(如"建造 1 个建筑 / 训练 1 只生物")→ 去沙盘或微观世界做动作 → 系统检测进度完成 → 发经验+金币 → 经验满则角色升级(升级按规则消耗金币、并按属性规则加属性点)→ 员工卡/图鉴上的"战力"数字实时涨了 → 看到成长,第二天还想来。
这条链路当前断了 4 处:经验不累加、升级不耗币、属性不生效、没有任务给目标。P0 就是补这 4 处。
---
## 二、P0 最小闭环(必须先做,可独立验收)
### 1. 经验/等级/货币 接通(后端)
- 文件:`game_base` 或 `yt_world` 的 `hr.employee` 扩展(`game_employee.py`,已有 `game_exp/game_level/game_coins` 字段但无逻辑)。
- 新增方法 `add_exp(amount)`:
- 累加 `game_exp`;按"角色等级"规则里的经验曲线判断升级;
- 升级时调用 `GameRule.convert()` 消耗金币(接通当前从未被调用的货币换算);
- 升级按"属性规则"给基础属性点(让属性第一次进入数值)。
- 等级规则种子当前 `noupdate=1` 只读 → 改为可编辑(`rule_data.xml` 去掉 noupdate 或单独建可配表),后台能调曲线。
### 2. 战力计算(让属性有意义)
- 新增 `compute_power(employee)`:用基础属性(物攻/法攻/防御/物防/法防/速度/生命/法力等)加权求和得到"战力"。
- 员工卡(`world.html` 员工卡区块)、图鉴、AI 向导问答都展示战力,让升级反馈可见。
### 3. 每日任务机制(给目标+奖励)
- 新增模型 `game.daily.task`(任务定义:动作类型、目标次数、奖励经验/金币)+ `game.player.task`(玩家每日进度)。
- 接口:`API.dailyTask`(拉今日任务+进度)、`completeTask`(领奖)。
- 在现有动作完成时挂钩:建筑动作(`world_api.py buildingAction`)、生物训练(`microUnitTrain`)→ 上报进度。
### 4. 前端成长反馈
- `world.html` 员工卡加:战力数字、经验进度条、今日任务面板(API 对接)。
- 升级/完成任务时弹"获得经验+XX / 金币+XX / 战力+XX"的轻提示。
**P0 验收标准**:登录 → 看到今日任务 → 做个建造/训练 → 任务完成领经验金币 → 经验满升级(扣金币、属性+战力涨)→ 员工卡数字变化可见。全程无需管理员手动改数据。
---
## 三、P1 后台功能优化(运营可配置)
- **任务管理后台**:`game.daily.task` 的表单/列表视图(定义任务、奖励、周期),运营可增删改,不必改代码。
- **经济/等级曲线后台**:把升级所需经验、货币消耗、属性增益做成可编辑规则(替代当前硬编码种子),运营调数值立刻生效。
- **属性生效验证**:员工表单加 smart button 显示"当前战力/属性明细",后台能验证规则确实驱动了数值(解决当前"升级实际效果后台不可验证")。
- **签到 / 简单抽奖**:P1 轻量正反馈,签到发币、抽奖给属性点/图鉴碎片,进一步拉日活。
- **玩家进度管理**:运营可查看/修正单个玩家等级、货币、任务进度(客服/调试用)。
---
## 四、P2 扩展(闭环跑通后再做)
- 战斗系统(用属性+战力做单位对战,当前 `yt.world.creature` 已有养成骨架)。
- AI NPC 深度互动(已有 `game_npc` 模型与知识视图,接对话触发任务)。
- 社交玩法(公会/排行榜,复用 `channel_type` 体系)。
---
## 五、执行建议(分期,每期可验收)
1. **第 1 期(P0 后端)**:`add_exp` + 战力计算 + 每日任务模型 + 规则可编辑。升级 Odoo 落库。
2. **第 2 期(P0 前端)**:员工卡战力/经验条 + 任务面板 + 升级提示。本地联调闭环跑通。
3. **第 3 期(P1 后台)**:任务管理视图 + 经济曲线后台 + 属性验证 smart button + 签到/抽奖。
4. 每期做完停下给你验收,不一次性闷头跑完。
---
## 附:当前已具备、无需重做的资产
- 登录/会话、员工卡展示、沙盘建造、世界+AI向导聊天、图鉴、邮件/成就、建筑经济结算、生物训练升级(均已在 `world.html` / `yt_world` / `game_base` 落地)。
- 规则文案(属性18条已分基础/高级、等级/货币/制度)已有,只需从"展示"改为"被逻辑读取"。
- 后台图鉴/规则/聊天/公告/员工等视图已较完善,缺的是任务/经济/属性生效三类管理界面。