@@ -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',
|
||||
|
||||
@@ -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 '🌟'})
|
||||
|
||||
# ============================================================
|
||||
# 游戏化系统:角色 / 生物 / 订单 / 建筑交互(前端-后端打通)
|
||||
# ============================================================
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="talent_ancient_blood" model="yt.codex.talent">
|
||||
<field name="code">talent_ancient_blood</field>
|
||||
<field name="name">先民血脉</field>
|
||||
<field name="icon">🩸</field>
|
||||
<field name="quality">mythic</field>
|
||||
<field name="trigger">被动</field>
|
||||
<field name="effect">生命回复+20%;濒死时触发先民护盾,5秒内免伤</field>
|
||||
<field name="description">流淌着远古先民的血脉,身体拥有超乎寻常的自愈与守护之力。传说中先民以此血脉抵御了灭世之灾。</field>
|
||||
<field name="max_level">5</field>
|
||||
<field name="cost_type">point</field>
|
||||
<field name="cost_value">1</field>
|
||||
<field name="sequence">10</field>
|
||||
</record>
|
||||
|
||||
<record id="talent_dragon_soul" model="yt.codex.talent">
|
||||
<field name="code">talent_dragon_soul</field>
|
||||
<field name="name">龙裔之魂</field>
|
||||
<field name="icon">🐉</field>
|
||||
<field name="quality">mythic</field>
|
||||
<field name="trigger">被动</field>
|
||||
<field name="effect">火焰抗性+50%;攻击附带灼烧效果</field>
|
||||
<field name="description">体内沉睡着巨龙的灵魂碎片,对火焰有着天然的亲和与免疫,攻击时能唤出龙息灼烧敌人。</field>
|
||||
<field name="max_level">5</field>
|
||||
<field name="cost_type">point</field>
|
||||
<field name="cost_value">1</field>
|
||||
<field name="sequence">20</field>
|
||||
</record>
|
||||
|
||||
<record id="talent_star_resonance" model="yt.codex.talent">
|
||||
<field name="code">talent_star_resonance</field>
|
||||
<field name="name">星辰共鸣</field>
|
||||
<field name="icon">✨</field>
|
||||
<field name="quality">legendary</field>
|
||||
<field name="trigger">被动</field>
|
||||
<field name="effect">法力回复+15%;夜间额外+10%</field>
|
||||
<field name="description">与星辰之力产生共鸣,能够汲取星光转化为法力。夜幕降临时,这份共鸣尤为强烈。</field>
|
||||
<field name="max_level">5</field>
|
||||
<field name="cost_type">point</field>
|
||||
<field name="cost_value">1</field>
|
||||
<field name="sequence">30</field>
|
||||
</record>
|
||||
|
||||
<record id="talent_beast_king" model="yt.codex.talent">
|
||||
<field name="code">talent_beast_king</field>
|
||||
<field name="name">兽王本能</field>
|
||||
<field name="icon">🐾</field>
|
||||
<field name="quality">epic</field>
|
||||
<field name="trigger">被动</field>
|
||||
<field name="effect">驯养生物好感度+30%;生物产出+20%</field>
|
||||
<field name="description">天生与野兽亲和,能感知它们的情绪与需求。驯养的生物更加忠诚,产出也更加丰厚。</field>
|
||||
<field name="max_level">5</field>
|
||||
<field name="cost_type">point</field>
|
||||
<field name="cost_value">1</field>
|
||||
<field name="sequence">40</field>
|
||||
</record>
|
||||
|
||||
<record id="talent_smith_hand" model="yt.codex.talent">
|
||||
<field name="code">talent_smith_hand</field>
|
||||
<field name="name">铁匠之手</field>
|
||||
<field name="icon">🔨</field>
|
||||
<field name="quality">rare</field>
|
||||
<field name="trigger">被动</field>
|
||||
<field name="effect">加工品品质+1阶;加工成功率+10%</field>
|
||||
<field name="description">双手有着与生俱来的锻造天赋,加工材料时更加精准,成品的品质也往往超出预期。</field>
|
||||
<field name="max_level">5</field>
|
||||
<field name="cost_type">point</field>
|
||||
<field name="cost_value">1</field>
|
||||
<field name="sequence">50</field>
|
||||
</record>
|
||||
|
||||
<record id="talent_forest_walker" model="yt.codex.talent">
|
||||
<field name="code">talent_forest_walker</field>
|
||||
<field name="name">林间行者</field>
|
||||
<field name="icon">🌿</field>
|
||||
<field name="quality">rare</field>
|
||||
<field name="trigger">被动</field>
|
||||
<field name="effect">森林地形移速+25%;隐蔽+15%</field>
|
||||
<field name="description">在密林中如鱼得水,脚步轻盈无声,仿佛与森林融为一体。敌人很难在林间发现你的踪迹。</field>
|
||||
<field name="max_level">5</field>
|
||||
<field name="cost_type">point</field>
|
||||
<field name="cost_value">1</field>
|
||||
<field name="sequence">60</field>
|
||||
</record>
|
||||
|
||||
<record id="talent_ore_sense" model="yt.codex.talent">
|
||||
<field name="code">talent_ore_sense</field>
|
||||
<field name="name">矿脉感应</field>
|
||||
<field name="icon">⛏️</field>
|
||||
<field name="quality">uncommon</field>
|
||||
<field name="trigger">被动</field>
|
||||
<field name="effect">发现稀有矿脉概率+20%</field>
|
||||
<field name="description">对地底的矿脉有着敏锐的直觉,总能在别人忽略的地方发现珍贵的矿石。</field>
|
||||
<field name="max_level">5</field>
|
||||
<field name="cost_type">point</field>
|
||||
<field name="cost_value">1</field>
|
||||
<field name="sequence">70</field>
|
||||
</record>
|
||||
|
||||
<record id="talent_harvest_blessing" model="yt.codex.talent">
|
||||
<field name="code">talent_harvest_blessing</field>
|
||||
<field name="name">丰收祝福</field>
|
||||
<field name="icon">🌾</field>
|
||||
<field name="quality">uncommon</field>
|
||||
<field name="trigger">被动</field>
|
||||
<field name="effect">农作物产量+15%;生长速度+10%</field>
|
||||
<field name="description">受到大地之灵的祝福,经手种植的作物总是长势喜人,收获时也比旁人更加丰盈。</field>
|
||||
<field name="max_level">5</field>
|
||||
<field name="cost_type">point</field>
|
||||
<field name="cost_value">1</field>
|
||||
<field name="sequence">80</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
@@ -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 });
|
||||
|
||||
@@ -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;}
|
||||
@@ -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 =
|
||||
'<div class="introSkip" id="introSkip">跳过剧情 ▸</div>' +
|
||||
'<img class="introImg" id="introImg" alt="剧情" />' +
|
||||
'<button class="introEnter" id="introEnter">踏入宇森世界 ▸</button>' +
|
||||
'<div class="introDots" id="introDots"></div>' +
|
||||
'<div class="introHint" id="introHint">点击 / 空格 / → 继续</div>';
|
||||
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);
|
||||
});
|
||||
};
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 531 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 437 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 397 KiB |
+76
-31
@@ -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;}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -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 = '<div class="empty show">正在从服务器加载图鉴数据…</div>';
|
||||
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 ? `<span class="tag" style="background:rgba(139,92,246,.25);color:#d0bcff;">Lv ${e.level}</span>` : '';
|
||||
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>
|
||||
${mine}
|
||||
</div>
|
||||
`;
|
||||
} else if(e.type==='skill_book'){
|
||||
@@ -736,10 +736,23 @@ function showDetail(e){
|
||||
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 class="gi"><span class="gl">当前等级</span><span class="gv">${e.owned ? 'Lv ' + e.level : '未激活'}</span></div>`;
|
||||
html += `<div class="gi"><span class="gl">最大等级</span><span class="gv">Lv ${e.maxLevel||1}</span></div>`;
|
||||
if(e.costValue){
|
||||
const costLabel = e.costType === 'gold' ? '金币' : '天赋点';
|
||||
const costTitle = e.owned ? '升级消耗' : '激活消耗';
|
||||
html += `<div class="gi"><span class="gl">${costTitle}</span><span class="gv">${e.costValue} ${costLabel}</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>`;
|
||||
}
|
||||
const canUpgrade = e.owned && e.level < e.maxLevel;
|
||||
if(!e.owned){
|
||||
html += `<div class="md-section"><button class="md-action-btn" id="talentAction" data-code="${e.id}" data-action="activate">激活天赋</button></div>`;
|
||||
} else if(canUpgrade){
|
||||
html += `<div class="md-section"><button class="md-action-btn" id="talentAction" data-code="${e.id}" data-action="upgrade">升级天赋</button></div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// 技能书详情
|
||||
@@ -767,6 +780,38 @@ function showDetail(e){
|
||||
html += '</div>';
|
||||
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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 键盘关闭 =====
|
||||
|
||||
+21
@@ -88,6 +88,7 @@
|
||||
.hero .enter-btn{font-size:15px;padding:11px 30px;}
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" href="assets/story-player.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="app">
|
||||
@@ -247,5 +248,25 @@ document.getElementById('enterGame').addEventListener('click', async ()=>{
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<script src="assets/story-player.js"></script>
|
||||
<script>
|
||||
// 首页首次访问自动播放前两幕剧情漫画
|
||||
(function() {
|
||||
if (localStorage.getItem('seen_intro_part1')) return;
|
||||
const btn = document.getElementById('enterGame');
|
||||
if (btn) btn.style.display = 'none';
|
||||
playIntroStory([
|
||||
'assets/story/story_01_transmigration.jpg',
|
||||
'assets/story/story_02_awakening.jpg'
|
||||
]).then(() => {
|
||||
localStorage.setItem('seen_intro_part1', '1');
|
||||
if (btn) btn.style.display = '';
|
||||
}).catch(e => {
|
||||
console.warn('首页剧情播放异常', e);
|
||||
localStorage.setItem('seen_intro_part1', '1');
|
||||
if (btn) btn.style.display = '';
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+13
-1
@@ -71,6 +71,7 @@
|
||||
.submit{padding:13px;font-size:15px;}
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" href="assets/story-player.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="bg-slides"></div>
|
||||
@@ -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 @@
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<script src="assets/story-player.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+8297
-7882
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user