diff --git a/addons/yt_world/__manifest__.py b/addons/yt_world/__manifest__.py index 398c279..f172669 100644 --- a/addons/yt_world/__manifest__.py +++ b/addons/yt_world/__manifest__.py @@ -27,6 +27,7 @@ 'views/npc_knowledge_views.xml', 'views/npc_views.xml', 'views/npc_menu.xml', + 'views/yt_world_chest_views.xml', ], 'assets': { 'web.assets_backend': [ diff --git a/addons/yt_world/controllers/__init__.py b/addons/yt_world/controllers/__init__.py index 6cdb872..d47ca62 100644 --- a/addons/yt_world/controllers/__init__.py +++ b/addons/yt_world/controllers/__init__.py @@ -1,2 +1,3 @@ from . import world_api from . import npc_api +from . import chest_api diff --git a/addons/yt_world/controllers/chest_api.py b/addons/yt_world/controllers/chest_api.py new file mode 100644 index 0000000..1dde514 --- /dev/null +++ b/addons/yt_world/controllers/chest_api.py @@ -0,0 +1,129 @@ +# -*- coding: utf-8 -*- +import logging +from odoo import http +from odoo.http import request + +_logger = logging.getLogger(__name__) + + +def _json(data): + headers = { + 'Content-Type': 'application/json; charset=utf-8', + 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type', + } + try: + origin = request.httprequest.headers.get('Origin') + if origin: + headers['Access-Control-Allow-Origin'] = origin + headers['Access-Control-Allow-Credentials'] = 'true' + else: + headers['Access-Control-Allow-Origin'] = '*' + except Exception: + headers['Access-Control-Allow-Origin'] = '*' + return request.make_response( + __import__('json').dumps(data, ensure_ascii=False), headers=headers) + + +def _post_body(): + try: + raw = request.httprequest.get_data(as_text=True) or '{}' + return __import__('json').loads(raw) + except Exception: + return {} + + +def _require_user(): + uid = request.session.uid + if not uid: + return {'ok': False, 'error': 'auth'} + user = request.env['res.users'].sudo().browse(uid) + if not user.exists() or not user.active: + return {'ok': False, 'error': 'auth'} + return None + + +def _get_world(): + user = request.env['res.users'].sudo().browse(request.session.uid) \ + if request.session.uid else request.env['res.users'] + if not user.exists(): + return None + return request.env['yt.world'].sudo().get_or_create_for_user(user) + + +class YtWorldChestApi(http.Controller): + + # ── 列表:当前世界某地块的活跃宝箱 ── + @http.route('/yt_world/api/chests', type='http', auth='public', + methods=['GET', 'OPTIONS'], csrf=False) + def list_chests(self, **kw): + auth_err = _require_user() + if auth_err: + return _json(auth_err) + world = _get_world() + if not world: + return _json({'ok': False, 'error': 'no_world'}) + domain = [('world_id', '=', world.id), ('state', '=', 'active')] + q = kw.get('q'); r = kw.get('r') + if q is not None and r is not None: + # tile_ref 是 Json,按字典值过滤:保留该地块的宝箱 + recs = request.env['yt.world.chest'].sudo().search(domain) + recs = recs.filtered(lambda c: (c.tile_ref or {}).get('q') == int(q) + and (c.tile_ref or {}).get('r') == int(r)) + else: + recs = request.env['yt.world.chest'].sudo().search(domain) + out = [{ + 'id': c.id, + 'cell_i': c.cell_i, 'cell_j': c.cell_j, + 'tier': c.tier, 'reward_type': c.reward_type, + 'reward_amount': c.reward_amount, 'state': c.state, + 'tile_ref': c.tile_ref or {}, + } for c in recs] + return _json({'ok': True, 'chests': out}) + + # ── 生成:测试期前端每 10s 调一次;生产可挂 ir.cron ── + @http.route('/yt_world/api/chest/generate', type='http', auth='public', + methods=['POST', 'OPTIONS'], csrf=False) + def generate_chest(self, **kw): + auth_err = _require_user() + if auth_err: + return _json(auth_err) + world = _get_world() + if not world: + return _json({'ok': False, 'error': 'no_world'}) + body = _post_body() + q = body.get('q'); r = body.get('r') + grid_n = int(body.get('grid_n') or 20) + q = int(q) if q is not None else None + r = int(r) if r is not None else None + chest = request.env['yt.world.chest'].sudo().generate_for_world( + world, q=q, r=r, grid_n=grid_n) + if not chest: + return _json({'ok': False, 'error': 'no_slot'}) + return _json({'ok': True, 'chest': { + 'id': chest.id, 'cell_i': chest.cell_i, 'cell_j': chest.cell_j, + 'tier': chest.tier, 'reward_type': chest.reward_type, + 'reward_amount': chest.reward_amount, 'state': chest.state, + 'tile_ref': chest.tile_ref or {}, + }}) + + # ── 开启:前端生物到达宝箱后调用 ── + @http.route('/yt_world/api/chest/claim', type='http', auth='public', + methods=['POST', 'OPTIONS'], csrf=False) + def claim_chest(self, **kw): + auth_err = _require_user() + if auth_err: + return _json(auth_err) + world = _get_world() + if not world: + return _json({'ok': False, 'error': 'no_world'}) + body = _post_body() + cid = body.get('chest_id') + creature_cid = body.get('cid') or body.get('creature_cid') + chest = request.env['yt.world.chest'].sudo().browse(int(cid) if cid else 0) + if not chest.exists(): + return _json({'ok': False, 'error': 'no_chest'}) + if chest.world_id.id != world.id: + return _json({'ok': False, 'error': 'forbidden'}) + res = chest.claim(creature_cid or '') + return _json(res) diff --git a/addons/yt_world/data/codex_resource.xml b/addons/yt_world/data/codex_resource.xml index c52ee13..a5c5f16 100644 --- a/addons/yt_world/data/codex_resource.xml +++ b/addons/yt_world/data/codex_resource.xml @@ -128,6 +128,36 @@ 210 + + + chest_wood木宝箱/assets/icons/scatter/chest_wood_front.png + scattercommon + 微观世界随机生成开启后获得少量铜币 + 最常见的粗糙木箱,由几块旧木板和锈铁箍钉成,里面通常装着零散的铜币。 + 10 + + + chest_silver银宝箱/assets/icons/scatter/chest_silver_front.png + scatterrare + 微观世界随机生成开启后获得少量银币 + 银质镶边的精致宝箱,表面刻有朴素符文,开启时会掉落一些银币。 + 20 + + + chest_gold金宝箱/assets/icons/scatter/chest_gold_front.png + scatterepic + 微观世界随机生成开启后获得少量金币 + 沉甸甸的鎏金宝箱,边角镶嵌红宝石,里面藏着诱人的金币。 + 30 + + + chest_diamond钻石宝箱/assets/icons/scatter/chest_diamond_front.png + scatterlegendary + 微观世界随机生成开启后获得少量钻石 + 传说中由星陨水晶打造的宝箱,周身泛着冷冽光辉,开启后可得珍贵钻石。 + 40 + + plank木板📦 diff --git a/addons/yt_world/models/__init__.py b/addons/yt_world/models/__init__.py index 7f3a03b..4ed06aa 100644 --- a/addons/yt_world/models/__init__.py +++ b/addons/yt_world/models/__init__.py @@ -16,3 +16,4 @@ from . import npc from . import npc_relationship from . import npc_behavior from . import npc_knowledge +from . import yt_world_chest diff --git a/addons/yt_world/models/yt_codex.py b/addons/yt_world/models/yt_codex.py index debdc1e..cc20993 100644 --- a/addons/yt_world/models/yt_codex.py +++ b/addons/yt_world/models/yt_codex.py @@ -14,6 +14,7 @@ class YtCodexResource(models.Model): category = fields.Selection([ ('material', '材料'), ('processed', '加工品'), + ('scatter', '散落物'), ('special', '特殊'), ], string='分类', required=True, default='material') quality = fields.Selection([ diff --git a/addons/yt_world/models/yt_world_chest.py b/addons/yt_world/models/yt_world_chest.py new file mode 100644 index 0000000..a329acd --- /dev/null +++ b/addons/yt_world/models/yt_world_chest.py @@ -0,0 +1,213 @@ +# -*- coding: utf-8 -*- +import random +from datetime import datetime +from odoo import models, fields, api +from odoo.tools import float_compare + +# 默认微观沙盘网格尺寸(与前端 microBuildGeom 的 N 保持一致; +# generate 时可由前端传 grid_n 覆盖,确保宝箱落在真实网格内)。 +DEFAULT_GRID_N = 20 + +# 品质 → 随机权重(普通多、传说少) +TIER_WEIGHTS = [ + ('common', 50), + ('rare', 30), + ('epic', 15), + ('legendary', 5), +] + +# 品质 → 奖励货币类型与数量区间 [min, max] +TIER_REWARD = { + 'common': ('copper', 1, 10), + 'rare': ('silver', 1, 6), + 'epic': ('gold', 1, 5), + 'legendary': ('diamond', 1, 3), +} + + +class YtWorldChest(models.Model): + _name = 'yt.world.chest' + _description = '世界宝箱(微观沙盘随机生成,生物自动开启)' + _order = 'create_time desc' + + world_id = fields.Many2one('yt.world', string='世界', required=True, + ondelete='cascade', index=True) + tile_ref = fields.Json(string='所在地块', default=dict, + help='{q, r} 归属哪个微观世界') + cell_i = fields.Float(string='网格X', default=0, + help='等距网格列(与建筑同级坐标系)') + cell_j = fields.Float(string='网格Y', default=0, + help='等距网格行(与建筑同级坐标系)') + tier = fields.Selection([ + ('common', '普通'), + ('rare', '稀有'), + ('epic', '史诗'), + ('legendary', '传说'), + ], string='品质', default='common', index=True) + reward_type = fields.Selection([ + ('copper', '铜币'), + ('silver', '银币'), + ('gold', '金币'), + ('diamond', '钻石'), + ], string='奖励类型', default='copper') + reward_amount = fields.Integer(string='奖励数量', default=1) + state = fields.Selection([ + ('active', '可开启'), + ('claimed', '已开启'), + ('expired', '已过期'), + ], string='状态', default='active', index=True) + claimed_by = fields.Char(string='开启者cid') + create_time = fields.Datetime(string='生成时间', default=fields.Datetime.now) + claimed_time = fields.Datetime(string='开启时间') + + # ── 生成 ── + @api.model + def generate_for_world(self, world, q=None, r=None, grid_n=DEFAULT_GRID_N): + """在当前世界随机生成一个宝箱。 + + - 定位目标微观世界:优先用传入的 (q,r);否则从世界生物已有的 tile_ref 里随机挑一个; + 再不行就挑一个有微观世界的地块。 + - 排除建筑占用格,随机落在一个空格。 + - 按权重选品质,再派生奖励货币与数量。 + 返回新建的 chest 记录;若无可落点返回 None。 + """ + self = self.sudo() + if not world: + return None + # 1) 选地块 + if q is None or r is None: + q, r = self._pick_tile(world) + if q is None or r is None: + return None + # 1.5) 限制同地块 active 宝箱数量(测试期 10s 一刷,避免满屏堆积) + MAX_CHESTS_PER_TILE = 8 + existing = self.search([ + ('world_id', '=', world.id), + ('state', '=', 'active'), + ('tile_ref', '=', {'q': q, 'r': r}), + ], order='create_time asc') + if len(existing) >= MAX_CHESTS_PER_TILE: + # 删掉最旧的,腾出一个空位给新宝箱 + to_remove = existing[:len(existing) - MAX_CHESTS_PER_TILE + 1] + to_remove.unlink() + # 2) 占用格集合(建筑 footprint) + occ = self._occupied_cells(world, q, r, grid_n) + # 3) 随机空格(留 1 格边距,防止贴边/溢出岛外) + margin = 1 + free = [] + for i in range(margin, grid_n - margin): + for j in range(margin, grid_n - margin): + if (i, j) not in occ: + free.append((i, j)) + if not free: + return None + ci, cj = random.choice(free) + # 4) 品质 + 奖励 + tier = self._weighted_tier() + rtype, rmin, rmax = TIER_REWARD[tier] + amount = random.randint(rmin, rmax) + chest = self.create({ + 'world_id': world.id, + 'tile_ref': {'q': q, 'r': r}, + 'cell_i': ci, + 'cell_j': cj, + 'tier': tier, + 'reward_type': rtype, + 'reward_amount': amount, + }) + return chest + + @api.model + def cron_generate(self, world=None, per_run=1): + """供 ir.cron 批量生成。测试期可由前端每 10s 调 generate API 替代。""" + if world is None: + # 对所有有世界记录的用户世界各生成若干(避免跨世界串扰) + worlds = self.env['yt.world'].sudo().search([]) + else: + worlds = world + for w in worlds: + for _ in range(per_run): + self.generate_for_world(w) + + def _pick_tile(self, world): + """返回 (q, r)。优先挑有生物的微观世界,其次任意该世界地块,兜底 (0,0)。""" + cr = self.env['yt.world.creature'].sudo().search( + [('world_id', '=', world.id)], limit=200) + tiles = set() + for c in cr: + tr = c.tile_ref or {} + if tr.get('q') is not None and tr.get('r') is not None: + tiles.add((int(tr['q']), int(tr['r']))) + if tiles: + return random.choice(list(tiles)) + tile = self.env['yt.world.tile'].sudo().search( + [('world_id', '=', world.id)], limit=1) + if tile: + return (tile.q, tile.r) + return (0, 0) + + def _occupied_cells(self, world, q, r, grid_n): + """读取该地块微观世界建筑占用格集合(考虑旋转交换长宽)。""" + occ = set() + tile = self.env['yt.world.tile'].sudo().search([ + ('world_id', '=', world.id), ('q', '=', q), ('r', '=', r), + ], limit=1) + mw = tile and tile.micro_world_id + if not mw: + return occ + for b in (mw.build_items or []): + t = b.get('type') + bi = b.get('i', 0) + bj = b.get('j', 0) + ew = b.get('w') or 1 + eh = b.get('h') or 1 + if ((b.get('rot') or 0) % 180) != 0: + ew, eh = eh, ew + for di in range(int(ew)): + for dj in range(int(eh)): + occ.add((bi + di, bj + dj)) + return occ + + def _weighted_tier(self): + total = sum(w for _, w in TIER_WEIGHTS) + x = random.uniform(0, total) + for tier, w in TIER_WEIGHTS: + x -= w + if x <= 0: + return tier + return 'common' + + # ── 开启 ── + def claim(self, cid): + """由某生物(cid)开启当前宝箱,发放奖励给该世界角色。 + + 返回 dict:{ok, reward_type, amount, balance} 或 {ok:False, error}。 + 幂等:已 claimed 的宝箱再次调用返回原有结果(不重复发奖)。 + """ + self.ensure_one() + self = self.sudo() + if self.state == 'claimed': + return {'ok': True, 'already': True, 'reward_type': self.reward_type, + 'amount': self.reward_amount, + 'balance': self._char_balance(self.reward_type)} + if self.state != 'active': + return {'ok': False, 'error': 'not_active'} + char = self.env['yt.world.character'].sudo().search( + [('world_id', '=', self.world_id.id)], limit=1) + if not char: + return {'ok': False, 'error': 'no_char'} + rtype = self.reward_type + cur = getattr(char, rtype) or 0 + char.write({rtype: cur + self.reward_amount}) + self.write({ + 'state': 'claimed', + 'claimed_by': cid, + 'claimed_time': fields.Datetime.now(), + }) + return {'ok': True, 'reward_type': rtype, 'amount': self.reward_amount, + 'balance': getattr(char, rtype) or 0} + + def _char_balance(self, rtype): + char = self.env['yt.world.character'].sudo().search( + [('world_id', '=', self.world_id.id)], limit=1) + return getattr(char, rtype) or 0 if char else 0 diff --git a/addons/yt_world/security/ir.model.access.csv b/addons/yt_world/security/ir.model.access.csv index 52d1dee..6dde80b 100644 --- a/addons/yt_world/security/ir.model.access.csv +++ b/addons/yt_world/security/ir.model.access.csv @@ -59,3 +59,6 @@ access_yt_world_stock_portal,yt.world.stock portal,model_yt_world_stock,base.gro access_yt_world_task_user,yt.world.task user,model_yt_world_task,base.group_user,1,1,1,1 access_yt_world_task_public,yt.world.task public,model_yt_world_task,base.group_public,1,0,0,0 access_yt_world_task_portal,yt.world.task portal,model_yt_world_task,base.group_portal,1,0,0,0 +access_yt_world_chest_user,yt.world.chest user,model_yt_world_chest,base.group_user,1,1,1,1 +access_yt_world_chest_public,yt.world.chest public,model_yt_world_chest,base.group_public,1,0,0,0 +access_yt_world_chest_portal,yt.world.chest portal,model_yt_world_chest,base.group_portal,1,0,0,0 diff --git a/addons/yt_world/views/yt_world_chest_views.xml b/addons/yt_world/views/yt_world_chest_views.xml new file mode 100644 index 0000000..206922e --- /dev/null +++ b/addons/yt_world/views/yt_world_chest_views.xml @@ -0,0 +1,52 @@ + + + + yt.world.chest.tree + yt.world.chest + + + + + + + + + + + + + + + + + + yt.world.chest.form + yt.world.chest + +
+ + + + + + + + + + + + + +
+
+
+ + + 世界宝箱 + yt.world.chest + tree,form + + + +
diff --git a/assets/characters.js b/assets/characters.js index b61ccfb..0ad7b06 100644 --- a/assets/characters.js +++ b/assets/characters.js @@ -493,8 +493,17 @@ function updateChars(dt) { return; } - // 键盘 WASD 控制(仅选中角色):8 方向实时移动,优先级高于点击移动/漫游 - const kbdDir = ch.selected ? wasdDirection() : null; + // 宝箱目标失效(被其他角色开启或切地块清空):重置为 idle + if (ch._chestTarget && (!window.microChests || !window.microChests.includes(ch._chestTarget))) { + ch._chestTarget = null; + if (ch.state === 'walk') { + ch.state = 'idle'; ch.frame = 0; ch.frameT = 0; + ch.wanderT = 0; ch._userMove = false; + } + } + + // 玩家移动控制已移除(含 WASD 键盘):kbdDir 恒为 null,生物只由 AI/漫游/宝箱逻辑驱动 + const kbdDir = null; if (kbdDir) { // 键盘移动恒用种族基础移速(不受漫游/点击移动改过的 ch.speed 残留影响) const kbdSpeed = raceStats(ch.race).speed; @@ -522,32 +531,61 @@ function updateChars(dt) { // 原有漫游 / 行走逻辑 // 自由移动开关:ch.freeMove !== false 才允许自主漫游(默认开,可被面板按钮关闭) if (ch.freeMove !== false && !ch.selected && ch.state === 'idle') { - ch.wanderT = (ch.wanderT || 0) + dt; - const interval = ch.wanderInterval || (2 + Math.random() * 4); - if (ch.wanderT >= interval) { -ch.wanderT = 0; - ch.wanderInterval = 2 + Math.random() * 4; - // 多意图漫游目标:闲逛 / 朝中心 / 远行(带岛内+建筑校验) - const target = generateWanderTarget(ch); - if (!target) { - ch.wanderInterval = 1 + Math.random() * 2; // 找不到就短等重试 - return; - } - ch.targetX = target.tx; - ch.targetY = target.ty; - ch.state = 'walk'; - ch.frame = 0; - ch.frameT = 0; - ch.speed = 35 + Math.random() * 20; // 慢速:约全速的一半 - ch._userMove = false; - ch.trail = []; - } + // 宝箱吸引:附近存在活跃宝箱时优先去开启(测试阶段吸引半径全屏) + const chests = window.microChests; + const c2s = window.microCellToScreen; + let best = null, bestD = Infinity; + if (chests && c2s && chests.length) { + for (const chest of chests) { + if (chest._claiming) continue; + const sp = c2s(chest.cell_i + 0.5, chest.cell_j + 0.5); + const d = Math.hypot(sp.x - ch.x, sp.y - ch.y); + if (d < bestD) { bestD = d; best = { chest, sp }; } + } + } + if (best && bestD < 2000) { // 吸引半径:2000px 已覆盖整个浮岛视图 + ch._chestTarget = best.chest; + ch.targetX = best.sp.x; + ch.targetY = best.sp.y; + ch.state = 'walk'; + ch.frame = 0; ch.frameT = 0; + ch.speed = 90 + Math.random() * 30; + ch._userMove = false; + ch.wanderT = 0; + ch.trail = []; + } else { + ch.wanderT = (ch.wanderT || 0) + dt; + const interval = ch.wanderInterval || (2 + Math.random() * 4); + if (ch.wanderT >= interval) { + ch.wanderT = 0; + ch.wanderInterval = 2 + Math.random() * 4; + // 多意图漫游目标:闲逛 / 朝中心 / 远行(带岛内+建筑校验) + const target = generateWanderTarget(ch); + if (!target) { + ch.wanderInterval = 1 + Math.random() * 2; // 找不到就短等重试 + return; + } + ch.targetX = target.tx; + ch.targetY = target.ty; + ch.state = 'walk'; + ch.frame = 0; + ch.frameT = 0; + ch.speed = 35 + Math.random() * 20; // 慢速:约全速的一半 + ch._userMove = false; + ch.trail = []; + } + } } // 行走逻辑(else 块的直接子块:键盘移动激活时不会执行) if (ch.state === 'walk') { const dx = ch.targetX - ch.x, dy = ch.targetY - ch.y; const dist = Math.hypot(dx, dy); - if (dist < 3) { + if (dist < 3 || (ch._chestTarget && dist < 35)) { + // 宝箱目标到达:调用 world.html 的开启逻辑(飘字+发奖+移除宝箱) + if (ch._chestTarget && window.microClaimChest) { + window.microClaimChest(ch._chestTarget, ch); + } + ch._chestTarget = null; ch.state = 'idle'; ch.frame = 0; ch.frameT = 0; @@ -877,19 +915,9 @@ function hitChar(wx, wy) { return null; } -// ---- 限制在浮岛椭圆内 ---- +// ---- 限制在浮岛椭圆内(已移除:用户要求生物可走到视图任意位置,不再卡边界) ---- function clampCharToIsland(ch) { - if (typeof microCanvas === 'undefined' || !microCanvas) return; - const mW = microCanvas.width, mH = microCanvas.height; - const cx = mW / 2, cy = mH / 2; - const Rx = mW * 0.50, Ry = mH * 0.43; - const dx = ch.x - cx, dy = ch.y - cy; - const d = Math.hypot(dx / Rx, dy / Ry); - if (d > 0.92) { - const k = 0.92 / d; - ch.x = cx + dx * k; - ch.y = cy + dy * k; - } + // no-op: boundary restriction removed; characters can roam freely within the micro canvas } // ---- UI 面板 ---- diff --git a/assets/game-api.js b/assets/game-api.js index fb83046..f78b9a3 100644 --- a/assets/game-api.js +++ b/assets/game-api.js @@ -446,4 +446,48 @@ const API = { return { ok: false, error: 'network', detail: String(e && e.message || e) }; } }, + + // ---------- 宝箱系统 ---------- + async chests(q, r) { + try { + const qs = (q !== undefined && r !== undefined) ? ('&q=' + q + '&r=' + r) : ''; + const r0 = await fetch(_worldUrl('/yt_world/api/chests') + qs, { credentials: _wcred }); + if (!r0.ok) return { ok: false, error: 'http_' + r0.status }; + return await r0.json(); + } catch (e) { + return { ok: false, error: 'network', detail: String(e && e.message || e) }; + } + }, + async chestGenerate(q, r, grid_n = 20) { + try { + const r0 = await fetch(_worldUrl('/yt_world/api/chest/generate'), { + method: 'POST', credentials: _wcred, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ q, r, grid_n: grid_n }), + }); + if (r0.redirected || (r0.url && r0.url.indexOf('/web/login') >= 0)) { + return { ok: false, error: 'auth' }; + } + const j = await r0.json(); + return j && j.ok === false ? j : (j || { ok: true }); + } catch (e) { + return { ok: false, error: 'network', detail: String(e && e.message || e) }; + } + }, + async chestClaim(chest_id, cid) { + try { + const r0 = await fetch(_worldUrl('/yt_world/api/chest/claim'), { + method: 'POST', credentials: _wcred, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ chest_id, cid }), + }); + if (r0.redirected || (r0.url && r0.url.indexOf('/web/login') >= 0)) { + return { ok: false, error: 'auth' }; + } + const j = await r0.json(); + return j && j.ok === false ? j : (j || { ok: true }); + } catch (e) { + return { ok: false, error: 'network', detail: String(e && e.message || e) }; + } + }, }; diff --git a/assets/icons/scatter/chest_diamond_front.png b/assets/icons/scatter/chest_diamond_front.png new file mode 100644 index 0000000..2416176 Binary files /dev/null and b/assets/icons/scatter/chest_diamond_front.png differ diff --git a/assets/icons/scatter/chest_diamond_top.png b/assets/icons/scatter/chest_diamond_top.png new file mode 100644 index 0000000..50ec87e Binary files /dev/null and b/assets/icons/scatter/chest_diamond_top.png differ diff --git a/assets/icons/scatter/chest_gold_front.png b/assets/icons/scatter/chest_gold_front.png new file mode 100644 index 0000000..15a9404 Binary files /dev/null and b/assets/icons/scatter/chest_gold_front.png differ diff --git a/assets/icons/scatter/chest_gold_top.png b/assets/icons/scatter/chest_gold_top.png new file mode 100644 index 0000000..f1bf5ae Binary files /dev/null and b/assets/icons/scatter/chest_gold_top.png differ diff --git a/assets/icons/scatter/chest_silver_front.png b/assets/icons/scatter/chest_silver_front.png new file mode 100644 index 0000000..48209fa Binary files /dev/null and b/assets/icons/scatter/chest_silver_front.png differ diff --git a/assets/icons/scatter/chest_silver_top.png b/assets/icons/scatter/chest_silver_top.png new file mode 100644 index 0000000..33c28f0 Binary files /dev/null and b/assets/icons/scatter/chest_silver_top.png differ diff --git a/assets/icons/scatter/chest_wood_front.png b/assets/icons/scatter/chest_wood_front.png new file mode 100644 index 0000000..fb858ae Binary files /dev/null and b/assets/icons/scatter/chest_wood_front.png differ diff --git a/assets/icons/scatter/chest_wood_top.png b/assets/icons/scatter/chest_wood_top.png new file mode 100644 index 0000000..128513b Binary files /dev/null and b/assets/icons/scatter/chest_wood_top.png differ diff --git a/assets/icons/scatter/gen/Hearthstone_game_icon__single__2026-09-10T10-30-38.png b/assets/icons/scatter/gen/Hearthstone_game_icon__single__2026-09-10T10-30-38.png new file mode 100644 index 0000000..e9ade44 Binary files /dev/null and b/assets/icons/scatter/gen/Hearthstone_game_icon__single__2026-09-10T10-30-38.png differ diff --git a/assets/icons/scatter/gen/chest_wood_icon_review.png b/assets/icons/scatter/gen/chest_wood_icon_review.png new file mode 100644 index 0000000..3fd48f2 Binary files /dev/null and b/assets/icons/scatter/gen/chest_wood_icon_review.png differ diff --git a/assets/icons/scatter/gen/chest_wood_icon_review_v2.png b/assets/icons/scatter/gen/chest_wood_icon_review_v2.png new file mode 100644 index 0000000..fae5751 Binary files /dev/null and b/assets/icons/scatter/gen/chest_wood_icon_review_v2.png differ diff --git a/assets/icons/scatter/gen/diamond/Hearthstone_game_icon__single__2026-09-10T10-50-52.png b/assets/icons/scatter/gen/diamond/Hearthstone_game_icon__single__2026-09-10T10-50-52.png new file mode 100644 index 0000000..5f84cc4 Binary files /dev/null and b/assets/icons/scatter/gen/diamond/Hearthstone_game_icon__single__2026-09-10T10-50-52.png differ diff --git a/assets/icons/scatter/gen/diamond/Hearthstone_game_icon__single__2026-09-10T10-52-30.png b/assets/icons/scatter/gen/diamond/Hearthstone_game_icon__single__2026-09-10T10-52-30.png new file mode 100644 index 0000000..bc1ccb5 Binary files /dev/null and b/assets/icons/scatter/gen/diamond/Hearthstone_game_icon__single__2026-09-10T10-52-30.png differ diff --git a/assets/icons/scatter/gen/diamond/Hearthstone_game_isometric_ico_2026-09-10T11-07-43.png b/assets/icons/scatter/gen/diamond/Hearthstone_game_isometric_ico_2026-09-10T11-07-43.png new file mode 100644 index 0000000..96c427e Binary files /dev/null and b/assets/icons/scatter/gen/diamond/Hearthstone_game_isometric_ico_2026-09-10T11-07-43.png differ diff --git a/assets/icons/scatter/gen/gold/Hearthstone_game_icon__single__2026-09-10T10-39-06.png b/assets/icons/scatter/gen/gold/Hearthstone_game_icon__single__2026-09-10T10-39-06.png new file mode 100644 index 0000000..7ce6b73 Binary files /dev/null and b/assets/icons/scatter/gen/gold/Hearthstone_game_icon__single__2026-09-10T10-39-06.png differ diff --git a/assets/icons/scatter/gen/gold/Hearthstone_game_icon__single__2026-09-10T10-48-11.png b/assets/icons/scatter/gen/gold/Hearthstone_game_icon__single__2026-09-10T10-48-11.png new file mode 100644 index 0000000..c101a3d Binary files /dev/null and b/assets/icons/scatter/gen/gold/Hearthstone_game_icon__single__2026-09-10T10-48-11.png differ diff --git a/assets/icons/scatter/gen/gold/Hearthstone_game_icon__single__2026-09-10T10-49-44.png b/assets/icons/scatter/gen/gold/Hearthstone_game_icon__single__2026-09-10T10-49-44.png new file mode 100644 index 0000000..20a5bc0 Binary files /dev/null and b/assets/icons/scatter/gen/gold/Hearthstone_game_icon__single__2026-09-10T10-49-44.png differ diff --git a/assets/icons/scatter/gen/gold/Hearthstone_game_isometric_ico_2026-09-10T11-06-18.png b/assets/icons/scatter/gen/gold/Hearthstone_game_isometric_ico_2026-09-10T11-06-18.png new file mode 100644 index 0000000..fa56d6f Binary files /dev/null and b/assets/icons/scatter/gen/gold/Hearthstone_game_isometric_ico_2026-09-10T11-06-18.png differ diff --git a/assets/icons/scatter/gen/silver/Hearthstone_game_icon__single__2026-09-10T10-38-44.png b/assets/icons/scatter/gen/silver/Hearthstone_game_icon__single__2026-09-10T10-38-44.png new file mode 100644 index 0000000..149a815 Binary files /dev/null and b/assets/icons/scatter/gen/silver/Hearthstone_game_icon__single__2026-09-10T10-38-44.png differ diff --git a/assets/icons/scatter/gen/silver/Hearthstone_game_icon__single__2026-09-10T10-42-37.png b/assets/icons/scatter/gen/silver/Hearthstone_game_icon__single__2026-09-10T10-42-37.png new file mode 100644 index 0000000..ae9afe2 Binary files /dev/null and b/assets/icons/scatter/gen/silver/Hearthstone_game_icon__single__2026-09-10T10-42-37.png differ diff --git a/assets/icons/scatter/gen/silver/Hearthstone_game_icon__single__2026-09-10T10-43-03.png b/assets/icons/scatter/gen/silver/Hearthstone_game_icon__single__2026-09-10T10-43-03.png new file mode 100644 index 0000000..ba859fd Binary files /dev/null and b/assets/icons/scatter/gen/silver/Hearthstone_game_icon__single__2026-09-10T10-43-03.png differ diff --git a/assets/icons/scatter/gen/silver/Hearthstone_game_isometric_ico_2026-09-10T11-05-15.png b/assets/icons/scatter/gen/silver/Hearthstone_game_isometric_ico_2026-09-10T11-05-15.png new file mode 100644 index 0000000..ae3eb89 Binary files /dev/null and b/assets/icons/scatter/gen/silver/Hearthstone_game_isometric_ico_2026-09-10T11-05-15.png differ diff --git a/assets/icons/scatter/gen/wood/Hearthstone_game_icon__single__2026-09-10T10-38-24.png b/assets/icons/scatter/gen/wood/Hearthstone_game_icon__single__2026-09-10T10-38-24.png new file mode 100644 index 0000000..81a561d Binary files /dev/null and b/assets/icons/scatter/gen/wood/Hearthstone_game_icon__single__2026-09-10T10-38-24.png differ diff --git a/assets/icons/scatter/gen/wood/Hearthstone_game_icon__single__2026-09-10T10-41-05.png b/assets/icons/scatter/gen/wood/Hearthstone_game_icon__single__2026-09-10T10-41-05.png new file mode 100644 index 0000000..45052b8 Binary files /dev/null and b/assets/icons/scatter/gen/wood/Hearthstone_game_icon__single__2026-09-10T10-41-05.png differ diff --git a/assets/icons/scatter/gen/wood/Hearthstone_game_isometric_ico_2026-09-10T10-53-46.png b/assets/icons/scatter/gen/wood/Hearthstone_game_isometric_ico_2026-09-10T10-53-46.png new file mode 100644 index 0000000..4e93362 Binary files /dev/null and b/assets/icons/scatter/gen/wood/Hearthstone_game_isometric_ico_2026-09-10T10-53-46.png differ diff --git a/assets/icons/scatter/gen/wood/Hearthstone_game_isometric_ico_2026-09-10T10-56-28.png b/assets/icons/scatter/gen/wood/Hearthstone_game_isometric_ico_2026-09-10T10-56-28.png new file mode 100644 index 0000000..8fc6262 Binary files /dev/null and b/assets/icons/scatter/gen/wood/Hearthstone_game_isometric_ico_2026-09-10T10-56-28.png differ diff --git a/assets/icons/scatter/gen/wood/Hearthstone_game_isometric_ico_2026-09-10T10-57-19.png b/assets/icons/scatter/gen/wood/Hearthstone_game_isometric_ico_2026-09-10T10-57-19.png new file mode 100644 index 0000000..e3278b1 Binary files /dev/null and b/assets/icons/scatter/gen/wood/Hearthstone_game_isometric_ico_2026-09-10T10-57-19.png differ diff --git a/assets/icons/scatter/gen/wood/Hearthstone_game_isometric_ico_2026-09-10T10-58-46.png b/assets/icons/scatter/gen/wood/Hearthstone_game_isometric_ico_2026-09-10T10-58-46.png new file mode 100644 index 0000000..8f04a78 Binary files /dev/null and b/assets/icons/scatter/gen/wood/Hearthstone_game_isometric_ico_2026-09-10T10-58-46.png differ diff --git a/assets/icons/scatter/gen/wood/Hearthstone_game_isometric_ico_2026-09-10T11-00-03.png b/assets/icons/scatter/gen/wood/Hearthstone_game_isometric_ico_2026-09-10T11-00-03.png new file mode 100644 index 0000000..65dbcda Binary files /dev/null and b/assets/icons/scatter/gen/wood/Hearthstone_game_isometric_ico_2026-09-10T11-00-03.png differ diff --git a/assets/icons/scatter/gen/wood/Hearthstone_game_isometric_ico_2026-09-10T11-01-31.png b/assets/icons/scatter/gen/wood/Hearthstone_game_isometric_ico_2026-09-10T11-01-31.png new file mode 100644 index 0000000..20df117 Binary files /dev/null and b/assets/icons/scatter/gen/wood/Hearthstone_game_isometric_ico_2026-09-10T11-01-31.png differ diff --git a/assets/icons/scatter/gen/wood/Hearthstone_game_isometric_ico_2026-09-10T11-03-56.png b/assets/icons/scatter/gen/wood/Hearthstone_game_isometric_ico_2026-09-10T11-03-56.png new file mode 100644 index 0000000..6f35f77 Binary files /dev/null and b/assets/icons/scatter/gen/wood/Hearthstone_game_isometric_ico_2026-09-10T11-03-56.png differ diff --git a/assets/world-app.js b/assets/world-app.js index 753454c..8d6d613 100644 --- a/assets/world-app.js +++ b/assets/world-app.js @@ -55,7 +55,6 @@ document.querySelectorAll('.world-nav button[data-panel]').forEach(b => { // ---------- 程序化矢量缩略图(零 AI 生图,不消耗积分) ---------- function hashStr(s){let h=2166136261;for(let i=0;i>>0;} function rng(seed){let s=seed>>>0;return function(){s=(Math.imul(s,1664525)+1013904223)>>>0;return s/4294967296;};} -const TIERCOL={'普通':'#9aa6c4','精良':'#5fd38a','稀有':'#4cc9f0','史诗':'#c77dff','传说':'#e7b85c'}; function starPts(cx,cy,spikes,outer,inner){let pts=[],rot=-Math.PI/2;for(let i=0;i`;if(v===0){x+=``;}else if(v===1){x+=``;}else{x+=``;}x+=``;return x;} diff --git a/codex.html b/codex.html index 584ba49..3110037 100644 --- a/codex.html +++ b/codex.html @@ -153,10 +153,11 @@ body::before{ .card.race .tags{display:flex;gap:4px;flex-wrap:wrap;justify-content:center;margin-top:2px;} .card.race .tag{font-size:9px;padding:1px 6px;border-radius:4px;background:rgba(255,255,255,.06);color:#8899aa;} -/* 装备/天赋/技能书等通用标签 */ -.card.equip .tags,.card.talent .tags,.card.skill_book .tags{display:flex;gap:4px;flex-wrap:wrap;justify-content:center;margin-top:2px;} -.card.equip .tag,.card.talent .tag,.card.skill_book .tag{font-size:9px;padding:1px 6px;border-radius:4px;background:rgba(255,255,255,.06);color:#8899aa;} +/* 装备/天赋/技能书/散落物等通用标签 */ +.card.equip .tags,.card.talent .tags,.card.skill_book .tags,.card.scatter .tags{display:flex;gap:4px;flex-wrap:wrap;justify-content:center;margin-top:2px;} +.card.equip .tag,.card.talent .tag,.card.skill_book .tag,.card.scatter .tag{font-size:9px;padding:1px 6px;border-radius:4px;background:rgba(255,255,255,.06);color:#8899aa;} .card.equip{border-color:rgba(120,200,255,.12);} +.card.scatter{border-color:rgba(245,158,11,.12);} /* 野怪等级角标 */ .card .tierbadge{position:absolute;top:8px;right:8px;font-size:9px;font-weight:700;padding:1px 6px;border-radius:6px;letter-spacing:.5px;} @@ -284,6 +285,7 @@ body::before{ 📖 技能书 📚 书籍 🛡️ 装备 + 🎁 散落物 ✨ 特殊
@@ -339,7 +341,7 @@ body::before{ 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','talent','skill_book','book','equip','special']; +const CAT_ORDER = ['material','processed','creature','monster','race','talent','skill_book','book','equip','scatter','special']; const SLOT_NAMES = { weapon:'武器', armor:'护甲', accessory:'饰品', boots:'鞋子' }; // 当前玩家已拥有天赋(从后端加载){ code: { level, active } } @@ -693,7 +695,7 @@ function showDetail(e){ 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 += `${QUALITY_NAMES[e.qual]}`; - const catLabel = e.type==='talent'?'🌟 天赋':e.type==='skill_book'?'📖 技能书':e.type==='equip'?'🛡️ 装备':e.cat==='material'?'材料':e.cat==='processed'?'加工品':'特殊'; + const catLabel = e.type==='talent'?'🌟 天赋':e.type==='skill_book'?'📖 技能书':e.type==='equip'?'🛡️ 装备':e.cat==='material'?'材料':e.cat==='processed'?'加工品':e.cat==='scatter'?'🎁 散落物':'特殊'; html += `${catLabel}`; } else if(e.type==='creature'){ html += `🐾 生物${e.hab||''}${e.diet||''}${e.tame?'✅ 可驯养':'❌ 野性'}`; diff --git a/world.html b/world.html index 5d2875f..045ff9e 100644 --- a/world.html +++ b/world.html @@ -1519,6 +1519,10 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa 成就 +
@@ -1540,10 +1544,6 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa 高度 高度 -