219 lines
8.5 KiB
Python
219 lines
8.5 KiB
Python
# -*- 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
|