diff --git a/addons/yt_world/__manifest__.py b/addons/yt_world/__manifest__.py index 14a1d3e..1813702 100644 --- a/addons/yt_world/__manifest__.py +++ b/addons/yt_world/__manifest__.py @@ -11,6 +11,7 @@ 'data/codex_creature.xml', 'data/codex_monster.xml', 'data/codex_race.xml', + 'data/codex_talent.xml', 'data/yt_world_build_item.xml', 'data/yt_world_task.xml', 'views/yt_world_views.xml', diff --git a/addons/yt_world/controllers/world_api.py b/addons/yt_world/controllers/world_api.py index e6a01ba..57037d4 100644 --- a/addons/yt_world/controllers/world_api.py +++ b/addons/yt_world/controllers/world_api.py @@ -2,7 +2,8 @@ import json import logging import re -from odoo import http +import random +from odoo import http, fields from odoo.http import request, Response _logger = logging.getLogger(__name__) @@ -297,6 +298,245 @@ class YtWorldApi(http.Controller): return self._records_json('yt.codex.race', ['code','name','icon','layer','traits','description']) + @http.route('/yt_world/api/codex/talents', type='http', auth='public', + methods=['GET', 'OPTIONS'], csrf=False) + def codex_talents(self, **kw): + """返回天赋图鉴配置(含前置天赋 code)。""" + recs = request.env['yt.codex.talent'].sudo().search([], order='sequence, id') + out = [] + for r in recs: + out.append({ + 'code': r.code, + 'name': r.name, + 'icon': r.icon or '🌟', + 'quality': r.quality or 'common', + 'trigger': r.trigger or '被动', + 'effect': r.effect or '', + 'description': r.description or '', + 'max_level': r.max_level or 1, + 'cost_type': r.cost_type or 'point', + 'cost_value': r.cost_value or 0, + 'unlock_condition': r.unlock_condition or '', + 'prerequisite_code': r.prerequisite_talent_id.code if r.prerequisite_talent_id else None, + 'sequence': r.sequence or 10, + }) + return _json(out) + + # ---------- 角色天赋系统 ---------- + + def _serialize_char_talents(self, char): + """把角色已拥有的天赋序列化成前端可用结构。""" + owned = {} + for t in char.talent_ids: + owned[t.talent_id.code] = { + 'id': t.id, + 'level': t.level or 1, + 'active': t.active, + 'acquired_at': fields.Datetime.to_string(t.acquired_at) if t.acquired_at else None, + } + all_talents = request.env['yt.codex.talent'].sudo().search([]) + talents = [] + for tal in all_talents: + info = owned.get(tal.code, {}) + talents.append({ + 'code': tal.code, + 'name': tal.name, + 'icon': tal.icon or '🌟', + 'quality': tal.quality or 'common', + 'trigger': tal.trigger or '被动', + 'effect': tal.effect or '', + 'description': tal.description or '', + 'max_level': tal.max_level or 1, + 'cost_type': tal.cost_type or 'point', + 'cost_value': tal.cost_value or 0, + 'unlock_condition': tal.unlock_condition or '', + 'prerequisite_code': tal.prerequisite_talent_id.code if tal.prerequisite_talent_id else None, + 'owned': bool(info), + 'level': info.get('level', 0), + 'active': info.get('active', False), + }) + return talents + + @http.route('/yt_world/api/char/talents', type='http', auth='public', + methods=['GET', 'OPTIONS'], csrf=False) + def char_talents(self, **kw): + """取当前角色天赋状态(图鉴 + 拥有情况)。""" + auth_err = self._require_user() + if auth_err: + return _json(auth_err) + world = self._get_world() + if not world: + return _json({'ok': False, 'error': 'no_world'}) + char = self._get_char(world) + return _json({ + 'ok': True, + 'talent_points': char.talent_points or 0, + 'gold': char.gold or 0, + 'talents': self._serialize_char_talents(char), + }) + + @http.route('/yt_world/api/char/talent/activate', type='http', auth='public', + methods=['POST', 'OPTIONS'], csrf=False) + def char_talent_activate(self, **kw): + """激活天赋 body: {"code": "talent_xxx"}。""" + auth_err = self._require_user() + if auth_err: + return _json(auth_err) + world = self._get_world() + if not world: + return _json({'ok': False, 'error': 'no_world'}) + body = _post_body() + code = (body.get('code') or '').strip() + if not code: + return _json({'ok': False, 'error': 'bad_params'}) + + char = self._get_char(world) + Talent = request.env['yt.codex.talent'].sudo() + CharTalent = request.env['yt.world.character.talent'].sudo() + talent = Talent.search([('code', '=', code)], limit=1) + if not talent: + return _json({'ok': False, 'error': 'talent_not_found', 'code': code}) + + existing = CharTalent.search([ + ('character_id', '=', char.id), + ('talent_id', '=', talent.id) + ], limit=1) + if existing: + return _json({'ok': False, 'error': 'already_owned', 'code': code}) + + # 前置天赋校验 + if talent.prerequisite_talent_id: + pre = CharTalent.search([ + ('character_id', '=', char.id), + ('talent_id', '=', talent.prerequisite_talent_id.id), + ('active', '=', True) + ], limit=1) + if not pre: + return _json({'ok': False, 'error': 'prereq_unmet', + 'prerequisite_code': talent.prerequisite_talent_id.code}) + + cost = talent.cost_value or 0 + if talent.cost_type == 'point': + if (char.talent_points or 0) < cost: + return _json({'ok': False, 'error': 'no_points', + 'need': cost, 'have': char.talent_points or 0}) + char.talent_points = (char.talent_points or 0) - cost + elif talent.cost_type == 'gold': + if (char.gold or 0) < cost: + return _json({'ok': False, 'error': 'no_gold', + 'need': cost, 'have': char.gold or 0}) + char.gold = (char.gold or 0) - cost + + CharTalent.create({ + 'character_id': char.id, + 'talent_id': talent.id, + 'level': 1, + 'active': True, + }) + + _logger.info('yt.talent.activate uid=%s code=%s cost=%s', + request.session.uid, code, cost) + return _json({ + 'ok': True, + 'code': code, + 'level': 1, + 'talent_points': char.talent_points or 0, + 'gold': char.gold or 0, + }) + + @http.route('/yt_world/api/char/talent/upgrade', type='http', auth='public', + methods=['POST', 'OPTIONS'], csrf=False) + def char_talent_upgrade(self, **kw): + """升级天赋 body: {"code": "talent_xxx"}。""" + auth_err = self._require_user() + if auth_err: + return _json(auth_err) + world = self._get_world() + if not world: + return _json({'ok': False, 'error': 'no_world'}) + body = _post_body() + code = (body.get('code') or '').strip() + if not code: + return _json({'ok': False, 'error': 'bad_params'}) + + char = self._get_char(world) + Talent = request.env['yt.codex.talent'].sudo() + CharTalent = request.env['yt.world.character.talent'].sudo() + talent = Talent.search([('code', '=', code)], limit=1) + if not talent: + return _json({'ok': False, 'error': 'talent_not_found', 'code': code}) + + rec = CharTalent.search([ + ('character_id', '=', char.id), + ('talent_id', '=', talent.id) + ], limit=1) + if not rec: + return _json({'ok': False, 'error': 'not_owned', 'code': code}) + + res = rec.do_upgrade() + if not res.get('ok'): + return _json(res) + return _json({ + 'ok': True, + 'code': code, + 'level': res['level'], + 'talent_points': char.talent_points or 0, + 'gold': char.gold or 0, + }) + + @http.route('/yt_world/api/char/talent/draw', type='http', auth='public', + methods=['POST', 'OPTIONS'], csrf=False) + def char_talent_draw(self, **kw): + """新手抽天赋:返回 5 张随机未拥有的天赋牌(前端先展示牌背,翻牌时再免费激活)。""" + auth_err = self._require_user() + if auth_err: + return _json(auth_err) + world = self._get_world() + if not world: + return _json({'ok': False, 'error': 'no_world'}) + char = self._get_char(world) + owned_codes = char.talent_ids.mapped('talent_id.code') + pool = request.env['yt.codex.talent'].sudo().search([('code', 'not in', owned_codes)]) + data = pool.read(['code', 'name', 'icon', 'quality', 'trigger', + 'effect', 'description', 'max_level']) + random.shuffle(data) + return _json({'ok': True, 'deck': data[:5]}) + + @http.route('/yt_world/api/char/talent/draw/activate', type='http', auth='public', + methods=['POST', 'OPTIONS'], csrf=False) + def char_talent_draw_activate(self, **kw): + """新手抽卡翻牌激活:免费、免前置、免消耗。body: {"code": "talent_xxx"}。""" + auth_err = self._require_user() + if auth_err: + return _json(auth_err) + world = self._get_world() + if not world: + return _json({'ok': False, 'error': 'no_world'}) + body = _post_body() + code = (body.get('code') or '').strip() + if not code: + return _json({'ok': False, 'error': 'bad_params'}) + char = self._get_char(world) + Talent = request.env['yt.codex.talent'].sudo() + CharTalent = request.env['yt.world.character.talent'].sudo() + talent = Talent.search([('code', '=', code)], limit=1) + if not talent: + return _json({'ok': False, 'error': 'talent_not_found', 'code': code}) + existing = CharTalent.search([ + ('character_id', '=', char.id), + ('talent_id', '=', talent.id) + ], limit=1) + if existing: + return _json({'ok': False, 'error': 'already_owned', 'code': code}) + CharTalent.create({ + 'character_id': char.id, + 'talent_id': talent.id, + 'level': 1, + 'active': True, + }) + return _json({'ok': True, 'code': code, 'level': 1, + 'name': talent.name, 'icon': talent.icon or '🌟'}) + # ============================================================ # 游戏化系统:角色 / 生物 / 订单 / 建筑交互(前端-后端打通) # ============================================================ diff --git a/addons/yt_world/data/codex_talent.xml b/addons/yt_world/data/codex_talent.xml new file mode 100644 index 0000000..4cedddf --- /dev/null +++ b/addons/yt_world/data/codex_talent.xml @@ -0,0 +1,114 @@ + + + + talent_ancient_blood + 先民血脉 + 🩸 + mythic + 被动 + 生命回复+20%;濒死时触发先民护盾,5秒内免伤 + 流淌着远古先民的血脉,身体拥有超乎寻常的自愈与守护之力。传说中先民以此血脉抵御了灭世之灾。 + 5 + point + 1 + 10 + + + + talent_dragon_soul + 龙裔之魂 + 🐉 + mythic + 被动 + 火焰抗性+50%;攻击附带灼烧效果 + 体内沉睡着巨龙的灵魂碎片,对火焰有着天然的亲和与免疫,攻击时能唤出龙息灼烧敌人。 + 5 + point + 1 + 20 + + + + talent_star_resonance + 星辰共鸣 + ✨ + legendary + 被动 + 法力回复+15%;夜间额外+10% + 与星辰之力产生共鸣,能够汲取星光转化为法力。夜幕降临时,这份共鸣尤为强烈。 + 5 + point + 1 + 30 + + + + talent_beast_king + 兽王本能 + 🐾 + epic + 被动 + 驯养生物好感度+30%;生物产出+20% + 天生与野兽亲和,能感知它们的情绪与需求。驯养的生物更加忠诚,产出也更加丰厚。 + 5 + point + 1 + 40 + + + + talent_smith_hand + 铁匠之手 + 🔨 + rare + 被动 + 加工品品质+1阶;加工成功率+10% + 双手有着与生俱来的锻造天赋,加工材料时更加精准,成品的品质也往往超出预期。 + 5 + point + 1 + 50 + + + + talent_forest_walker + 林间行者 + 🌿 + rare + 被动 + 森林地形移速+25%;隐蔽+15% + 在密林中如鱼得水,脚步轻盈无声,仿佛与森林融为一体。敌人很难在林间发现你的踪迹。 + 5 + point + 1 + 60 + + + + talent_ore_sense + 矿脉感应 + ⛏️ + uncommon + 被动 + 发现稀有矿脉概率+20% + 对地底的矿脉有着敏锐的直觉,总能在别人忽略的地方发现珍贵的矿石。 + 5 + point + 1 + 70 + + + + talent_harvest_blessing + 丰收祝福 + 🌾 + uncommon + 被动 + 农作物产量+15%;生长速度+10% + 受到大地之灵的祝福,经手种植的作物总是长势喜人,收获时也比旁人更加丰盈。 + 5 + point + 1 + 80 + + diff --git a/addons/yt_world/models/__init__.py b/addons/yt_world/models/__init__.py index 768baee..7916d3f 100644 --- a/addons/yt_world/models/__init__.py +++ b/addons/yt_world/models/__init__.py @@ -7,6 +7,8 @@ from . import yt_world_game from . import yt_world_task from . import yt_resource from . import yt_codex +from . import yt_codex_talent +from . import yt_world_character_talent from . import npc from . import npc_relationship from . import npc_behavior diff --git a/addons/yt_world/models/yt_codex_talent.py b/addons/yt_world/models/yt_codex_talent.py new file mode 100644 index 0000000..b3d1ad5 --- /dev/null +++ b/addons/yt_world/models/yt_codex_talent.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +from odoo import models, fields + + +class YtCodexTalent(models.Model): + """图鉴-天赋(全局配置)。每个玩家在 yt.world.character.talent 中拥有 + 自己的激活记录与等级,但配置本身全局共享。""" + _name = 'yt.codex.talent' + _description = '图鉴-天赋' + _order = 'sequence, id' + + name = fields.Char(string='名称', required=True, translate=True) + code = fields.Char(string='编码', required=True, index=True) + icon = fields.Char(string='图标', default='🌟') + quality = fields.Selection([ + ('common', '普通'), + ('uncommon', '优秀'), + ('rare', '精良'), + ('epic', '史诗'), + ('legendary', '传说'), + ('mythic', '神话'), + ], string='品质', default='common', required=True) + trigger = fields.Char(string='触发方式', default='被动') + effect = fields.Char(string='效果') + description = fields.Char(string='描述') + max_level = fields.Integer(string='最大等级', default=1) + cost_type = fields.Selection([ + ('point', '天赋点'), + ('gold', '金币'), + ('exp', '经验'), + ('item', '道具'), + ], string='升级消耗类型', default='point') + cost_value = fields.Integer(string='每级消耗', default=0) + unlock_condition = fields.Char(string='解锁条件') + prerequisite_talent_id = fields.Many2one( + 'yt.codex.talent', string='前置天赋', ondelete='set null', + help='激活本天赋前必须先激活的天赋' + ) + sequence = fields.Integer(string='排序', default=10) diff --git a/addons/yt_world/models/yt_world_character_talent.py b/addons/yt_world/models/yt_world_character_talent.py new file mode 100644 index 0000000..d4d78b7 --- /dev/null +++ b/addons/yt_world/models/yt_world_character_talent.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +from odoo import models, fields, api + + +class YtWorldCharacterTalent(models.Model): + """玩家角色已拥有的天赋记录。绑定 yt.world.character,每个角色独立。""" + _name = 'yt.world.character.talent' + _description = '玩家角色天赋' + _order = 'character_id, talent_id' + + character_id = fields.Many2one( + 'yt.world.character', string='角色', required=True, + ondelete='cascade', index=True + ) + talent_id = fields.Many2one( + 'yt.codex.talent', string='天赋', required=True, index=True + ) + level = fields.Integer(string='等级', default=1) + active = fields.Boolean(string='已激活', default=True) + acquired_at = fields.Datetime(string='获得时间', default=fields.Datetime.now) + + _sql_constraints = [ + ('uniq_char_talent', 'unique(character_id, talent_id)', '角色不能重复拥有同一天赋'), + ] + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + if vals.get('level', 1) < 1: + vals['level'] = 1 + return super().create(vals_list) + + def can_upgrade(self): + """检查是否还能升级:未达最大等级、角色有足够天赋点。""" + self.ensure_one() + char = self.character_id + talent = self.talent_id + if self.level >= (talent.max_level or 1): + return False, 'max_level' + cost = talent.cost_value or 0 + if talent.cost_type == 'point': + if (char.talent_points or 0) < cost: + return False, 'no_points' + elif talent.cost_type == 'gold': + if (char.gold or 0) < cost: + return False, 'no_gold' + return True, None + + def do_upgrade(self): + """执行升级:扣资源、等级+1。调用前须先用 can_upgrade 校验。""" + self.ensure_one() + ok, reason = self.can_upgrade() + if not ok: + return {'ok': False, 'error': reason} + talent = self.talent_id + cost = talent.cost_value or 0 + char = self.character_id + if talent.cost_type == 'point': + char.talent_points = (char.talent_points or 0) - cost + elif talent.cost_type == 'gold': + char.gold = (char.gold or 0) - cost + self.level += 1 + return {'ok': True, 'level': self.level} diff --git a/addons/yt_world/models/yt_world_game.py b/addons/yt_world/models/yt_world_game.py index 873756a..737986d 100644 --- a/addons/yt_world/models/yt_world_game.py +++ b/addons/yt_world/models/yt_world_game.py @@ -36,10 +36,15 @@ class YtWorldCharacter(models.Model): exp = fields.Integer(string='经验', default=0) exp_max = fields.Integer(string='升级所需经验', default=100) gold = fields.Integer(string='金币', default=100) + talent_points = fields.Integer(string='天赋点', default=0, + help='用于激活/升级天赋的专用点数') inventory = fields.Json(string='背包', default=dict) inventory_display = fields.Text(string='背包(可读)', compute='_compute_displays') equip = fields.Json(string='装备', default=dict) equip_display = fields.Text(string='装备(可读)', compute='_compute_displays') + talent_ids = fields.One2many( + 'yt.world.character.talent', 'character_id', string='已拥有天赋' + ) # 任务进度:[{code, progress:{obj_id:have}, state, ts}] # state ∈ in_progress / claimable / done(locked 为前端按前置派生,不落库) tasks = fields.Json(string='任务进度', default=list) diff --git a/addons/yt_world/security/ir.model.access.csv b/addons/yt_world/security/ir.model.access.csv index d431109..77e1a92 100644 --- a/addons/yt_world/security/ir.model.access.csv +++ b/addons/yt_world/security/ir.model.access.csv @@ -1,4 +1,10 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_yt_codex_talent_user,yt.codex.talent user,model_yt_codex_talent,base.group_user,1,0,0,0 +access_yt_codex_talent_public,yt.codex.talent public,model_yt_codex_talent,base.group_public,1,0,0,0 +access_yt_codex_talent_portal,yt.codex.talent portal,model_yt_codex_talent,base.group_portal,1,0,0,0 +access_yt_world_character_talent_user,yt.world.character.talent user,model_yt_world_character_talent,base.group_user,1,1,1,1 +access_yt_world_character_talent_public,yt.world.character.talent public,model_yt_world_character_talent,base.group_public,1,0,0,0 +access_yt_world_character_talent_portal,yt.world.character.talent portal,model_yt_world_character_talent,base.group_portal,1,1,1,1 access_yt_world_user,yt.world user,model_yt_world,base.group_user,1,1,1,1 access_yt_world_public,yt.world public,model_yt_world,base.group_public,1,1,1,1 access_yt_world_portal,yt.world portal,model_yt_world,base.group_portal,1,1,1,1 diff --git a/assets/game-api.js b/assets/game-api.js index 22ed2eb..58e8669 100644 --- a/assets/game-api.js +++ b/assets/game-api.js @@ -280,6 +280,55 @@ const API = { async codexRaces() { return (await fetch(_worldUrl('/yt_world/api/codex/races'), { credentials: _wcred })).json(); }, + async codexTalents() { + return (await fetch(_worldUrl('/yt_world/api/codex/talents'), { credentials: _wcred })).json(); + }, + async charTalents() { + const r = await fetch(_worldUrl('/yt_world/api/char/talents'), { credentials: _wcred }); + if (!r.ok) return { ok: false, error: 'http_' + r.status }; + return await r.json(); + }, + async activateTalent(code) { + const r = await fetch(_worldUrl('/yt_world/api/char/talent/activate'), { + method: 'POST', credentials: _wcred, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ code }), + }); + if (r.redirected || (r.url && r.url.indexOf('/web/login') >= 0)) { + return { ok: false, error: 'auth' }; + } + return await r.json(); + }, + async upgradeTalent(code) { + const r = await fetch(_worldUrl('/yt_world/api/char/talent/upgrade'), { + method: 'POST', credentials: _wcred, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ code }), + }); + if (r.redirected || (r.url && r.url.indexOf('/web/login') >= 0)) { + return { ok: false, error: 'auth' }; + } + return await r.json(); + }, + // ---------- 新手抽天赋 ---------- + async talentDraw() { + const r = await fetch(_worldUrl('/yt_world/api/char/talent/draw'), { + method: 'POST', credentials: _wcred, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({}), + }); + if (r.redirected || (r.url && r.url.indexOf('/web/login') >= 0)) return { ok: false, error: 'auth' }; + return await r.json(); + }, + async talentDrawActivate(code) { + const r = await fetch(_worldUrl('/yt_world/api/char/talent/draw/activate'), { + method: 'POST', credentials: _wcred, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ code }), + }); + if (r.redirected || (r.url && r.url.indexOf('/web/login') >= 0)) return { ok: false, error: 'auth' }; + return await r.json(); + }, // ---------- 新手主线任务 ---------- async tasks() { const r = await fetch(_worldUrl('/yt_world/api/tasks'), { credentials: _wcred }); diff --git a/assets/story-player.css b/assets/story-player.css new file mode 100644 index 0000000..a627f56 --- /dev/null +++ b/assets/story-player.css @@ -0,0 +1,13 @@ +/* 宇森剧情漫画播放器 —— 全屏遮罩样式 */ +#introOverlay{position:fixed;inset:0;z-index:6000;background:#05070d;display:none;align-items:center;justify-content:center;flex-direction:column;opacity:0;transition:opacity .5s ease;user-select:none;} +#introOverlay.active{display:flex;opacity:1;} +#introOverlay .introImg{max-width:92vw;max-height:78vh;border-radius:12px;box-shadow:0 20px 70px rgba(0,0,0,.7);opacity:0;transform:scale(.98);transition:opacity .45s ease,transform .45s ease;pointer-events:none;} +#introOverlay .introImg.show{opacity:1;transform:scale(1);} +#introOverlay .introDots{display:flex;gap:10px;margin-top:22px;} +#introOverlay .introDot{width:9px;height:9px;border-radius:50%;background:rgba(255,255,255,.25);transition:background .3s,transform .3s;} +#introOverlay .introDot.on{background:#4af;transform:scale(1.25);} +#introOverlay .introHint{position:absolute;bottom:26px;left:0;right:0;text-align:center;color:#9fb0c0;font-size:13px;letter-spacing:.5px;transition:opacity .3s;} +#introOverlay .introSkip{position:absolute;top:18px;right:20px;padding:7px 16px;border-radius:20px;background:rgba(255,255,255,.08);border:1px solid rgba(255,255,255,.18);color:#cdd8e6;font-size:13px;cursor:pointer;transition:background .2s;} +#introOverlay .introSkip:hover{background:rgba(255,255,255,.18);} +#introOverlay .introEnter{margin-top:26px;padding:12px 38px;border:none;border-radius:26px;background:linear-gradient(135deg,#8b5cf6,#4aafff);color:#fff;font-size:15px;font-weight:600;cursor:pointer;box-shadow:0 8px 24px rgba(74,175,255,.4);opacity:0;transform:translateY(10px);pointer-events:none;transition:opacity .4s,transform .4s;} +#introOverlay .introEnter.show{opacity:1;transform:translateY(0);pointer-events:auto;} diff --git a/assets/story-player.js b/assets/story-player.js new file mode 100644 index 0000000..610a888 --- /dev/null +++ b/assets/story-player.js @@ -0,0 +1,106 @@ +/** + * 宇森剧情漫画播放器 + * 用法: + * await playIntroStory(['assets/story/story_01.jpg', 'assets/story/story_02.jpg']); + * 或: + * await playIntroStory(['assets/story/story_03.jpg'], { enterText: '进入世界 ▸', holdMs: 5000 }); + */ + +// 确保遮罩 DOM 存在(页面只需引入本脚本与 CSS) +(function ensureOverlay() { + if (document.getElementById('introOverlay')) return; + const overlay = document.createElement('div'); + overlay.id = 'introOverlay'; + overlay.innerHTML = + '
跳过剧情 ▸
' + + '剧情' + + '' + + '
' + + '
点击 / 空格 / → 继续
'; + document.body.appendChild(overlay); +})(); + +window.playIntroStory = function playIntroStory(panels, options) { + options = options || {}; + const holdMs = options.holdMs || 5000; + const enterText = options.enterText || '踏入宇森世界 ▸'; + const onStart = options.onStart || null; // 开始播放前回调,用于隐藏页面原生元素 + + return new Promise((resolve) => { + const overlay = document.getElementById('introOverlay'); + const img = document.getElementById('introImg'); + const dotsWrap = document.getElementById('introDots'); + const hint = document.getElementById('introHint'); + const skipBtn = document.getElementById('introSkip'); + const enterBtn = document.getElementById('introEnter'); + + if (!Array.isArray(panels) || panels.length === 0) { + resolve(); + return; + } + + if (typeof onStart === 'function') onStart(); + enterBtn.textContent = enterText; + + let idx = 0, autoTimer = null, done = false, dots = []; + + dotsWrap.innerHTML = ''; + panels.forEach((_, i) => { + const d = document.createElement('div'); + d.className = 'introDot' + (i === 0 ? ' on' : ''); + dotsWrap.appendChild(d); + }); + dots = dotsWrap.querySelectorAll('.introDot'); + + function showPanel(i) { + idx = i; + dots.forEach((d, k) => d.classList.toggle('on', k === i)); + img.classList.remove('show'); + img.onload = () => img.classList.add('show'); + img.src = panels[i]; + if (img.complete) img.classList.add('show'); + const last = (i === panels.length - 1); + enterBtn.classList.toggle('show', last); + hint.style.opacity = last ? '0' : '1'; + clearTimeout(autoTimer); + if (!last) autoTimer = setTimeout(nextPanel, holdMs); + } + + function nextPanel() { + if (done || idx >= panels.length - 1) return; + showPanel(idx + 1); + } + + function finish() { + if (done) return; + done = true; + clearTimeout(autoTimer); + document.removeEventListener('keydown', onKey); + overlay.classList.remove('active'); + setTimeout(() => { overlay.style.display = 'none'; }, 500); + resolve(); + } + + function onKey(e) { + if (!overlay.classList.contains('active')) return; + if (e.key === ' ' || e.key === 'Enter' || e.key === 'ArrowRight') { + e.preventDefault(); + if (idx === panels.length - 1) finish(); else nextPanel(); + } else if (e.key === 'Escape') { + finish(); + } + } + + overlay.addEventListener('click', (e) => { + if (e.target === skipBtn || e.target === enterBtn) return; + nextPanel(); + }); + skipBtn.addEventListener('click', finish); + enterBtn.addEventListener('click', finish); + document.addEventListener('keydown', onKey); + + overlay.style.display = 'flex'; + requestAnimationFrame(() => overlay.classList.add('active')); + showPanel(0); + }); +}; diff --git a/assets/story/story_01_transmigration.jpg b/assets/story/story_01_transmigration.jpg new file mode 100644 index 0000000..adedfd6 Binary files /dev/null and b/assets/story/story_01_transmigration.jpg differ diff --git a/assets/story/story_02_awakening.jpg b/assets/story/story_02_awakening.jpg new file mode 100644 index 0000000..de3e8ad Binary files /dev/null and b/assets/story/story_02_awakening.jpg differ diff --git a/assets/story/story_03_registration.jpg b/assets/story/story_03_registration.jpg new file mode 100644 index 0000000..1801294 Binary files /dev/null and b/assets/story/story_03_registration.jpg differ diff --git a/codex.html b/codex.html index 246c174..40ca4b0 100644 --- a/codex.html +++ b/codex.html @@ -234,6 +234,15 @@ body::before{ .md-chips{margin-top:14px;display:flex;flex-wrap:wrap;gap:8px;} .md-chip{padding:6px 12px;border-radius:8px;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.07);font-size:13px;color:#cdd8e6;} +.md-action-btn{ + width:100%;padding:12px 0;border:none;border-radius:10px; + background:linear-gradient(135deg,#8b5cf6,#4aafff);color:#fff;font-size:15px;font-weight:600;cursor:pointer; + box-shadow:0 8px 24px rgba(74,175,255,.25);transition:.2s; +} +.md-action-btn:hover{opacity:.92;transform:translateY(-1px);} +.md-action-btn:disabled{opacity:.55;cursor:not-allowed;transform:none;} +.md-action-error{margin-top:8px;font-size:13px;color:#ff8a8a;text-align:center;} + @@ -324,33 +333,8 @@ const QUALITY_NAMES = { common:'普通', uncommon:'优秀', rare:'精良', epic: const TIER_NAMES = { normal:'普通野怪', elite:'精英野怪', boss:'Boss' }; 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:'受到大地之灵的祝福,经手种植的作物总是长势喜人,收获时也比旁人更加丰盈。' }, -]; +// 当前玩家已拥有天赋(从后端加载){ code: { level, active } } +let MY_TALENTS = {}; // ===== 技能书预置数据(前端硬编码,后端就绪后迁移) ===== const SKILL_BOOK_DATA = [ @@ -403,10 +387,14 @@ const BOOK_DATA = [ ]; function normTalent(t){ + const mine = MY_TALENTS[t.code]; 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||'' + desc:t.description||t.desc||'', maxLevel:t.max_level||1, + costType:t.cost_type||'point', costValue:t.cost_value||0, + owned:!!mine, level:(mine&&mine.level)||0, active:(mine&&mine.active)||false, + prerequisiteCode:t.prerequisite_code||null }; } function normSkillBook(s){ @@ -497,15 +485,25 @@ async function loadCodexData(){ const grid = document.getElementById('grid'); grid.innerHTML = '
正在从服务器加载图鉴数据…
'; try{ - const [res, cre, mon, rac] = await Promise.all([ - API.codexResources(), API.codexCreatures(), API.codexMonsters(), API.codexRaces() + const [res, cre, mon, rac, talents] = await Promise.all([ + API.codexResources(), API.codexCreatures(), API.codexMonsters(), API.codexRaces(), API.codexTalents() ]); + // 同时尝试拉取当前角色天赋状态;未登录或无角色时忽略。 + MY_TALENTS = {}; + try{ + const ct = await API.charTalents(); + if(ct && ct.ok && Array.isArray(ct.talents)){ + (ct.talents || []).forEach(t => { + if(t.owned) MY_TALENTS[t.code] = { level: t.level || 0, active: t.active || false }; + }); + } + }catch(e){ /* 未登录,忽略 */ } ALL_ENTRIES = [ ...(res||[]).map(normResource), ...(cre||[]).map(normCreature), ...(mon||[]).map(normMonster), ...(rac||[]).map(normRace), - ...TALENT_DATA.map(normTalent), + ...(talents||[]).map(normTalent), ...SKILL_BOOK_DATA.map(normSkillBook), ...BOOK_DATA.map(normBook), ]; @@ -590,12 +588,14 @@ function renderGrid(){ `; } else if(e.type==='talent'){ div.classList.add('q-'+e.qual, 'talent'); + const mine = e.owned ? `Lv ${e.level}` : ''; div.innerHTML = ` ${QUALITY_NAMES[e.qual]}
${e.icon}
${e.name}
${e.trigger||'被动'} + ${mine}
`; } else if(e.type==='skill_book'){ @@ -736,10 +736,23 @@ function showDetail(e){ html += `

📋 属性

`; html += `
触发方式${e.trigger||'被动'}
`; html += `
品质${QUALITY_NAMES[e.qual]||'—'}
`; + html += `
当前等级${e.owned ? 'Lv ' + e.level : '未激活'}
`; + html += `
最大等级Lv ${e.maxLevel||1}
`; + if(e.costValue){ + const costLabel = e.costType === 'gold' ? '金币' : '天赋点'; + const costTitle = e.owned ? '升级消耗' : '激活消耗'; + html += `
${costTitle}${e.costValue} ${costLabel}
`; + } html += `
`; if(e.effect){ html += `

⚡ 效果

${e.effect}
`; } + const canUpgrade = e.owned && e.level < e.maxLevel; + if(!e.owned){ + html += `
`; + } else if(canUpgrade){ + html += `
`; + } } // 技能书详情 @@ -767,6 +780,38 @@ function showDetail(e){ html += ''; box.innerHTML = html; overlay.classList.add('open'); + + // 天赋激活/升级按钮事件绑定 + const actionBtn = document.getElementById('talentAction'); + if(actionBtn){ + actionBtn.addEventListener('click', async function onTalentAction(){ + const code = this.dataset.code; + const action = this.dataset.action; + this.disabled = true; + const oldText = this.textContent; + this.textContent = '处理中…'; + let res; + if(action === 'activate') res = await API.activateTalent(code); + else res = await API.upgradeTalent(code); + if(res && res.ok){ + await loadCodexData(); + const entry = findEntry(code); + if(entry) showDetail(entry); + }else{ + this.disabled = false; + this.textContent = oldText; + const msg = (res && res.error) ? res.error : '失败'; + let errBox = document.getElementById('talentActionError'); + if(!errBox){ + errBox = document.createElement('div'); + errBox.id = 'talentActionError'; + errBox.className = 'md-action-error'; + this.parentNode.appendChild(errBox); + } + errBox.textContent = '操作失败:' + msg; + } + }); + } } // ===== 键盘关闭 ===== diff --git a/index.html b/index.html index 63fce74..f7f00c4 100644 --- a/index.html +++ b/index.html @@ -88,6 +88,7 @@ .hero .enter-btn{font-size:15px;padding:11px 30px;} } +
@@ -247,5 +248,25 @@ document.getElementById('enterGame').addEventListener('click', async ()=>{ } }); + + diff --git a/login.html b/login.html index 90cadef..e3d93f4 100644 --- a/login.html +++ b/login.html @@ -71,6 +71,7 @@ .submit{padding:13px;font-size:15px;} } +
@@ -234,7 +235,17 @@ const btn = regForm.querySelector('.submit'); btn.disabled = true; btn.textContent = '注册中…'; try { const r = await API.register({ name, login, password }); - if (r.ok){ location.href = nextUrl; } + if (r.ok){ + // 注册成功后播放第三幕「登记注册」剧情漫画 + if (!localStorage.getItem('seen_intro_part2')) { + localStorage.setItem('seen_intro_part1', '1'); // 防止后续流程重复播放前两幕 + try { + await playIntroStory(['assets/story/story_03_registration.jpg'], { enterText: '进入世界 ▸' }); + } catch (e) { console.warn('注册剧情播放异常', e); } + localStorage.setItem('seen_intro_part2', '1'); + } + location.href = nextUrl; + } else { showMsg(r.error || '注册失败', 'err'); } } catch (err) { showMsg('网络错误,请稍后重试', 'err'); @@ -243,5 +254,6 @@ } }); + diff --git a/world.html b/world.html index d86dac8..9fd00e5 100644 --- a/world.html +++ b/world.html @@ -377,6 +377,7 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa .mv-cell.active { background: rgba(255,213,79,.14); border-color: #ffd54f; box-shadow: 0 0 10px rgba(255,213,79,.45); } .mv-cell .mv-ico { font-size: 30px; line-height: 1; pointer-events: none; } .mv-cell .mv-ico-img { width: 44px; height: 44px; object-fit: contain; pointer-events: none; border-radius: 6px; } +.mv-cell .mv-ico-char { width: 44px; height: 44px; background-repeat: no-repeat; background-position: center 0; background-size: auto 100%; pointer-events: none; border-radius: 6px; image-rendering: pixelated; } .mv-cell .mv-nm { font-size: 11px; color: #9fb3c8; pointer-events: none; } .mv-cost { font-size: 10px; color: #ffd54f; pointer-events: none; font-weight: 600; } .mv-cost.free { color: #7fdca0; } @@ -744,7 +745,84 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa #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;} } + +/* ===== 我的天赋面板(顶栏 🌟 按钮 → 居中 modal) ===== */ +.topBtn{margin-left:8px;display:inline-flex;align-items:center;gap:5px;background:rgba(139,92,246,.18);border:1px solid rgba(139,92,246,.5);color:#e9edf6;padding:7px 14px;border-radius:999px;font:inherit;font-size:14px;font-weight:500;cursor:pointer;user-select:none;transition:background .18s,border-color .18s;pointer-events:auto;} +.topBtn:hover{background:rgba(139,92,246,.32);border-color:rgba(139,92,246,.7);} +.topRight .topBtn[hidden]{display:none;} +#talentPanel{position:fixed;inset:0;z-index:1200;display:flex;align-items:center;justify-content:center;background:rgba(4,7,15,.65);backdrop-filter:blur(6px);opacity:0;visibility:hidden;transition:opacity .28s,visibility .28s;padding:20px;} +#talentPanel.open{opacity:1;visibility:visible;} +#talentPanel[hidden]{display:none;} +.tp-box{width:520px;max-width:100%;max-height:88vh;overflow-y:auto;position:relative;background:linear-gradient(180deg,rgba(17,23,42,.98),rgba(10,14,26,.98));border:1px solid rgba(139,92,246,.35);border-radius:18px;box-shadow:0 12px 48px rgba(0,0,0,.6);padding:26px 28px 24px;transform:translateY(20px);transition:transform .3s ease;} +#talentPanel.open .tp-box{transform:translateY(0);} +.tp-close{position:absolute;top:12px;right:14px;width:28px;height:28px;border-radius:8px;border:1px solid rgba(255,255,255,.12);background:rgba(255,255,255,.04);color:#9aa6c4;font-size:13px;cursor:pointer;transition:.12s;} +.tp-close:hover{background:rgba(239,68,68,.2);color:#ffb4b4;} +.tp-head{display:flex;align-items:center;gap:16px;margin-bottom:18px;} +.tp-icon{font-size:46px;line-height:1;} +.tp-title{font-size:22px;font-weight:800;color:#eef2fb;} +.tp-meta{display:flex;flex-wrap:wrap;gap:6px;margin-top:6px;} +.tp-badge{font-size:12px;padding:2px 10px;border-radius:6px;background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.1);color:#9fb3cc;} +.tp-list{display:flex;flex-direction:column;gap:12px;} +.tp-item{display:flex;gap:14px;padding:12px 14px;border-radius:12px;background:rgba(255,255,255,.03);border:1px solid rgba(255,255,255,.08);} +.tp-ico{font-size:34px;line-height:1;flex:0 0 auto;} +.tp-body{flex:1;min-width:0;} +.tp-name{font-size:16px;font-weight:700;color:#eef2fb;display:flex;align-items:center;gap:8px;flex-wrap:wrap;} +.tp-state{font-size:11px;padding:1px 8px;border-radius:6px;} +.tp-state.active{background:rgba(70,224,122,.18);color:#46e07a;border:1px solid rgba(70,224,122,.4);} +.tp-state.inactive{background:rgba(255,180,80,.15);color:#ffcf86;border:1px solid rgba(255,180,80,.35);} +.tp-lv{font-size:12px;color:#9fb3cc;margin-top:3px;} +.tp-eff{font-size:13px;color:#cdd8e6;line-height:1.6;margin-top:6px;} +.tp-btn{margin-top:10px;padding:8px 16px;border:none;border-radius:9px;background:linear-gradient(135deg,#8b5cf6,#4aafff);color:#fff;font-size:14px;font-weight:600;cursor:pointer;transition:.18s;} +.tp-btn:hover{filter:brightness(1.08);} +.tp-btn:disabled{opacity:.5;cursor:not-allowed;} +.tp-max{color:#fbbf24;font-size:12px;margin-top:8px;} +.tp-empty{text-align:center;padding:30px 10px;color:#7b8ea8;font-size:14px;line-height:1.7;} +.tp-empty a{color:#8b5cf6;} +/* ===== 新手抽天赋引导 ===== */ +#talentDraw{position:fixed;inset:0;z-index:1300;display:flex;align-items:flex-end;justify-content:center;background:rgba(4,7,15,.72);backdrop-filter:blur(7px);opacity:0;visibility:hidden;transition:opacity .3s,visibility .3s;padding:28px 18px 40px;} +#talentDraw.open{opacity:1;visibility:visible;} +#talentDraw[hidden]{display:none;} +.td-wrap{width:760px;max-width:100%;display:flex;flex-direction:column;align-items:center;gap:18px;} +.td-npc{display:flex;align-items:flex-end;gap:14px;align-self:flex-start;max-width:100%;} +.td-avatar{width:64px;height:64px;flex:0 0 auto;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:34px;background:radial-gradient(circle at 30% 30%,#a78bfa,#6d28d9);box-shadow:0 0 26px rgba(139,92,246,.6);animation:td-float 3s ease-in-out infinite;} +@keyframes td-float{0%,100%{transform:translateY(0);}50%{transform:translateY(-7px);}} +.td-bubble{position:relative;background:linear-gradient(180deg,rgba(30,38,66,.98),rgba(18,24,44,.98));border:1px solid rgba(139,92,246,.45);border-radius:16px;padding:14px 18px;max-width:560px;color:#e8edf8;font-size:15px;line-height:1.7;box-shadow:0 8px 32px rgba(0,0,0,.5);} +.td-bubble:after{content:"";position:absolute;left:-9px;bottom:18px;border:9px solid transparent;border-right-color:rgba(139,92,246,.45);} +.td-npcname{font-size:12px;color:#a78bfa;margin-bottom:4px;font-weight:700;} +.td-bubble b{color:#c4b5fd;} +.td-cards{display:flex;gap:14px;flex-wrap:wrap;justify-content:center;min-height:168px;} +.td-card{width:120px;height:168px;perspective:900px;cursor:pointer;position:relative;} +.td-card.dealt{animation:td-deal .42s ease both;} +@keyframes td-deal{from{opacity:0;transform:translateY(26px) rotate(-6deg);}to{opacity:1;transform:none;}} +.td-inner{position:relative;width:100%;height:100%;transform-style:preserve-3d;transition:transform .55s cubic-bezier(.2,.8,.3,1);} +.td-card.flipped .td-inner{transform:rotateY(180deg);} +.td-face{position:absolute;inset:0;backface-visibility:hidden;-webkit-backface-visibility:hidden;border-radius:14px;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;padding:10px;box-shadow:0 6px 18px rgba(0,0,0,.45);} +.td-back{background:linear-gradient(160deg,#3b2a6b,#1c1340);border:1px solid rgba(167,139,250,.5);color:#c4b5fd;font-size:40px;} +.td-back:after{content:"";position:absolute;inset:10px;border:1px dashed rgba(167,139,250,.35);border-radius:10px;} +.td-front{transform:rotateY(180deg);background:linear-gradient(180deg,rgba(20,26,46,.98),rgba(12,16,30,.98));border:1px solid rgba(255,255,255,.12);} +.td-front .td-ico{font-size:40px;line-height:1;} +.td-front .td-q{font-size:11px;letter-spacing:1px;margin:7px 0 2px;font-weight:700;} +.td-front .td-name{font-size:15px;font-weight:800;color:#fff;} +.td-front .td-eff{font-size:11px;color:#cdd8e6;line-height:1.5;margin-top:6px;padding:0 4px;} +.td-front.got{box-shadow:0 0 0 2px rgba(70,224,122,.7),0 6px 18px rgba(70,224,122,.35);} +.td-front .td-got{position:absolute;top:6px;right:8px;font-size:10px;color:#46e07a;font-weight:700;} +.td-card.done{cursor:default;} +.td-actions{display:flex;gap:12px;align-items:center;flex-wrap:wrap;justify-content:center;} +.td-btn{padding:10px 22px;border:none;border-radius:10px;background:linear-gradient(135deg,#8b5cf6,#4aafff);color:#fff;font-size:15px;font-weight:700;cursor:pointer;transition:.18s;box-shadow:0 6px 18px rgba(74,175,255,.35);} +.td-btn:hover{filter:brightness(1.08);} +.td-btn.ghost{background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.16);box-shadow:none;color:#cdd8e6;} +.td-count{color:#9fb3cc;font-size:13px;} +.td-q-common{color:#9ca3af;} .td-q-uncommon{color:#5fd17a;} .td-q-rare{color:#4aafff;} .td-q-epic{color:#c084fc;} .td-q-legendary{color:#f59e0b;} .td-q-mythic{color:#fb7185;} +/* 抽卡完成动画:未翻牌碎裂 / 已翻牌飞向天赋按钮 */ +.td-card.shatter{pointer-events:none;} +.td-card.shatter .td-inner{opacity:0;transform:scale(.92);transition:opacity .22s,transform .22s;} +.shard{position:absolute;width:16px;height:16px;background:linear-gradient(135deg,#a78bfa,#4c1d95);border:1px solid rgba(196,181,253,.35);border-radius:3px;pointer-events:none;opacity:0;left:50%;top:50%;margin:-8px 0 0 -8px;box-shadow:0 0 8px rgba(139,92,246,.5);} +.td-card.shatter .shard{animation:shard-burst .7s cubic-bezier(.2,.7,.2,1) forwards;} +@keyframes shard-burst{0%{opacity:1;transform:translate3d(0,0,0) rotate(0deg) scale(1);}100%{opacity:0;transform:translate3d(var(--tx),var(--ty),0) rotate(var(--rot)) scale(.15);}} +.td-fly-clone{position:fixed;z-index:9999;pointer-events:none;border-radius:14px;overflow:hidden;box-shadow:0 0 28px rgba(139,92,246,.75);transition:transform .75s cubic-bezier(.2,.8,.2,1),opacity .75s;} +.td-fly-clone .td-inner{transition:none !important;} +
登录校验中…
@@ -771,6 +849,7 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa
清晨 07:30
星元 1年 · 春
+