Files
game/addons/yt_world/models/yt_world_chest.py
T
2026-09-22 18:01:02 +08:00

219 lines
8.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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')
to_remove = self.browse() # 空记录集:保证下方推导式在 existing<MAX 时也有定义
if len(existing) >= MAX_CHESTS_PER_TILE:
# 删掉最旧的,腾出一个空位给新宝箱
to_remove = existing[:len(existing) - MAX_CHESTS_PER_TILE + 1]
to_remove.unlink()
# 2) 占用格集合(建筑 footprint + 已有 active 宝箱格子)
# 宝箱作为 1×1 建筑:不能和建筑重叠,也不能多个宝箱叠在同一格
occ = self._occupied_cells(world, q, r, grid_n)
keep_chests = [c for c in existing if c not in to_remove]
chest_cells = {(int(c.cell_i), int(c.cell_j)) for c in keep_chests}
occ |= chest_cells
# 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