Files
game/addons/yt_world/models/yt_world_game.py
T
2026-08-27 20:15:18 +08:00

222 lines
9.9 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 -*-
"""宇森世界 · 游戏化系统模型(角色 / 生物)。
这些模型让前端「建筑交互 / 生物装备 / 经济」在 Odoo 后端拥有可记忆的真实记录,
与已存在的微观建造布置(yt.world.tile.micro.build_items JSON)互补:
- yt.world.character : 玩家英雄(等级 / 经验 / 金币 / 背包 / 装备),每世界一条
- yt.world.creature : 世界生物(居民 / NPC),含等级 / 经验 / 生命 / 装备 / 职业
"""
from odoo import models, fields, api
def _fmt_kv(d):
"""把 dict 格式化成可读多行文本;空则返回占位。"""
d = d or {}
if not d:
return '(空)'
if not isinstance(d, dict):
return str(d)
return '\n'.join('%s: %s' % (k, v) for k, v in d.items())
class YtWorldCharacter(models.Model):
_name = 'yt.world.character'
_description = '玩家角色(英雄)'
_sql_constraints = [
('uniq_world', 'unique(world_id)', '一个世界只能有一个角色'),
]
world_id = fields.Many2one('yt.world', string='世界', required=True,
ondelete='cascade', index=True)
employee_id = fields.Many2one('hr.employee', string='所属玩家',
ondelete='cascade', index=True)
name = fields.Char(string='角色名', default='旅者')
level = fields.Integer(string='等级', default=1)
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)
tutorial_chapter = fields.Integer(string='当前章节', default=1)
tutorial_step = fields.Integer(string='章节内步骤', default=0)
@api.depends('inventory', 'equip')
def _compute_displays(self):
for r in self:
r.inventory_display = _fmt_kv(r.inventory)
r.equip_display = _fmt_kv(r.equip)
# ========== 任务系统 ==========
# 前端可发射的游戏事件类型(服务端事件总线):
# panel_opened / world_named / build_placed{code} /
# building_action{key:'code_action'} / resource_collected{res} /
# char_recruited{race} / creature_recruited{species} / level_up{level}
@staticmethod
def _obj_id(o):
return '%s:%s' % (o.get('type'), o.get('key'))
@staticmethod
def _match_event(o, event, payload):
"""单条 objective 是否被该事件命中。"""
if o.get('type') != event:
return False
key = o.get('key')
if key is None:
return True
if event == 'level_up':
try:
return int(payload.get('level', 0)) >= int(key)
except Exception:
return False
field = {
'build_placed': 'code',
'building_action': 'key',
'resource_collected': 'res',
'char_recruited': 'race',
'creature_recruited': 'species',
}.get(event)
if not field:
return True # 不带 key 的事件(panel_opened/world_named)命中即计数
val = payload.get(field)
if val is None:
return True
return str(val) == str(key)
def on_game_event(self, event, payload=None):
"""服务端事件总线:遍历未完成任务的 objectives,命中则推进进度,满额置 claimable。
返回 {ok, changed, claimable:[codes]} 供接口提示前端。"""
self.ensure_one()
payload = payload or {}
Task = self.env['yt.world.task'].sudo()
tasks = list(self.tasks or [])
done_codes = [t['code'] for t in tasks if t.get('state') == 'done']
newly_claimable = []
changed = False
for task in Task.search([('active', '=', True)], order='chapter, sequence, id'):
code = task.code
idx = next((i for i, t in enumerate(tasks) if t.get('code') == code), None)
entry = tasks[idx] if idx is not None else None
if entry and entry.get('state') in ('done', 'claimable'):
continue # 已结算/待领取,不再推进
prereq = task.prereq or []
if any(p not in done_codes for p in prereq):
continue # 前置未解锁
objectives = task.objectives or []
if not objectives:
continue
prog = dict((entry or {}).get('progress') or {})
was_claimable = bool(entry and entry.get('state') == 'claimable')
for o in objectives:
oid = o.get('id') or self._obj_id(o)
need = o.get('count', 1) or 1
have = prog.get(oid, 0)
if have >= need:
continue
if self._match_event(o, event, payload):
prog[oid] = have + 1
changed = True
all_done = all(
(prog.get(o.get('id') or self._obj_id(o), 0)) >= (o.get('count', 1) or 1)
for o in objectives)
new_state = 'claimable' if all_done else 'in_progress'
rec = {'code': code, 'progress': prog, 'state': new_state}
if idx is not None:
tasks[idx] = rec
else:
tasks.append(rec)
changed = True
if new_state == 'claimable' and not was_claimable:
newly_claimable.append(code)
if changed:
self.sudo().write({'tasks': tasks})
return {'ok': True, 'changed': changed, 'claimable': newly_claimable}
def complete_task(self, code, task_model=None):
"""领取任务奖励(仅 state==claimable 可领)。校验前置→发奖→置 done。"""
self.ensure_one()
Task = task_model or self.env['yt.world.task'].sudo()
task = Task.search([('code', '=', code), ('active', '=', True)], limit=1)
if not task:
return {'ok': False, 'error': 'task_not_found', 'code': code}
# 前置校验
prereq = task.prereq or []
done_codes = [t['code'] for t in (self.tasks or []) if t.get('state') == 'done']
for p in prereq:
if p not in done_codes:
return {'ok': False, 'error': 'prereq_unmet', 'code': p}
# 找到进度条目
tasks = list(self.tasks or [])
entry = next((t for t in tasks if t.get('code') == code), None)
if entry and entry.get('state') == 'done':
return {'ok': True, 'already': True, 'level': self.level,
'exp': self.exp, 'gold': self.gold}
if not entry or entry.get('state') != 'claimable':
return {'ok': False, 'error': 'not_claimable', 'code': code}
# 加经验(带越级)
exp = self.exp + (task.reward_exp or 0)
level = self.level
exp_max = self.exp_max
while exp >= exp_max:
exp -= exp_max
level += 1
exp_max = int(exp_max * 1.3) # 每级所需经验递增
# 加货币
gold = self.gold + int((task.reward_res or {}).get('gold', 0))
# 置已完成
for i, t in enumerate(tasks):
if t.get('code') == code:
tasks[i] = {'code': code, 'progress': t.get('progress') or {},
'state': 'done', 'ts': fields.Datetime.to_string(fields.Datetime.now())}
break
self.sudo().write({
'level': level, 'exp': exp, 'exp_max': exp_max,
'gold': gold, 'tasks': tasks,
})
return {'ok': True, 'level': level, 'exp': exp, 'exp_max': exp_max,
'gold': gold, 'reward_exp': task.reward_exp or 0,
'reward_res': task.reward_res or {}}
class YtWorldCreature(models.Model):
_name = 'yt.world.creature'
_description = '世界生物(居民 / NPC)'
_order = 'id'
world_id = fields.Many2one('yt.world', string='世界', required=True,
ondelete='cascade', index=True)
name = fields.Char(string='名称', required=True)
species = fields.Char(string='物种编码', index=True)
level = fields.Integer(string='等级', default=1)
exp = fields.Integer(string='经验', default=0)
hp = fields.Integer(string='当前生命', default=100)
hp_max = fields.Integer(string='生命上限', default=100)
energy = fields.Integer(string='能量', default=100)
mood = fields.Char(string='心情', default='😊')
job = fields.Char(string='职业', default='none')
equip = fields.Json(string='装备', default=dict)
equip_display = fields.Text(string='装备(可读)', compute='_compute_displays')
tile_ref = fields.Json(string='所在地块', default=dict)
tile_ref_display = fields.Text(string='所在地块(可读)', compute='_compute_displays')
state = fields.Char(string='状态', default='resting')
partner = fields.Json(string='相关对象', default=dict)
partner_display = fields.Text(string='相关对象(可读)', compute='_compute_displays')
@api.depends('equip', 'tile_ref', 'partner')
def _compute_displays(self):
for r in self:
r.equip_display = _fmt_kv(r.equip)
r.tile_ref_display = _fmt_kv(r.tile_ref)
r.partner_display = _fmt_kv(r.partner)