Files
game/addons/yt_world/controllers/world_api.py
T
2026-09-20 20:34:42 +08:00

1264 lines
58 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 json
import logging
import re
import random
from odoo import http, fields
from odoo.http import request, Response
_logger = logging.getLogger(__name__)
def _json(data):
"""返回 UTF-8 JSON 响应(与 game_base 一致),并附带 CORS 头以支持跨域(8080→8069)。"""
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 Response(json.dumps(data, ensure_ascii=False), headers=headers)
def _post_body():
try:
raw = request.httprequest.get_data(as_text=True) or '{}'
return json.loads(raw)
except Exception:
return {}
class YtWorldApi(http.Controller):
"""前端沙盘记忆接口(JSON over HTTP)。auth=user:需已登录 Odoo。
前端统一带 ?db=game 且 credentials:'same-origin',故路由须为 type='http'
并以 _json 返回原始 JSON(与 game-api.js 的 fetch().json() 对齐)。"""
@http.route('/yt_world/api/world', type='http', auth='public',
methods=['GET', 'OPTIONS'], csrf=False)
def world(self, **kw):
"""取或建当前用户世界,导出 seed + 地块覆盖。"""
auth_err = self._require_user()
if auth_err:
return _json(auth_err)
user = self._get_user()
rec = request.env['yt.world'].sudo().get_or_create_for_user(user)
return _json(rec.export_state())
@http.route('/yt_world/api/tiles/save', type='http', auth='public',
methods=['POST', 'OPTIONS'], csrf=False)
def save_tiles(self, **kw):
"""增量覆盖保存地块。body: {"tiles": [...]}"""
auth_err = self._require_user()
if auth_err:
return _json(auth_err)
user = self._get_user()
world = request.env['yt.world'].sudo().search(
[('user_id', '=', user.id)], limit=1)
if not world:
return _json({'ok': False, 'error': 'no_world'})
body = _post_body()
tiles = body.get('tiles') or []
world.import_tiles(tiles)
return _json({'ok': True, 'saved': len(tiles)})
@http.route('/yt_world/api/settings', type='http', auth='public',
methods=['POST', 'OPTIONS'], csrf=False)
def save_settings(self, **kw):
"""保存相机/UI 偏好。body: {"settings": {...}}"""
auth_err = self._require_user()
if auth_err:
return _json(auth_err)
user = self._get_user()
world = request.env['yt.world'].sudo().search(
[('user_id', '=', user.id)], limit=1)
body = _post_body()
settings = body.get('settings')
if world and settings is not None:
world.settings = settings
return _json({'ok': True})
@http.route('/yt_world/api/world/rename', type='http', auth='public',
methods=['POST', 'OPTIONS'], csrf=False)
def rename_world(self, **kw):
"""修改当前用户世界名称。body: {"name": "..."}
校验:trim 后 1-24 字符,禁控制字符,空白正常允许。"""
auth_err = self._require_user()
if auth_err:
return _json(auth_err)
user = self._get_user()
world = request.env['yt.world'].sudo().search(
[('user_id', '=', user.id)], limit=1)
if not world:
return _json({'ok': False, 'error': 'no_world'})
body = _post_body()
name = (body.get('name') or '').strip()
if not name:
return _json({'ok': False, 'error': 'empty'})
if len(name) > 24:
return _json({'ok': False, 'error': 'too_long'})
if re.search(r'[\x00-\x1f\x7f]', name):
return _json({'ok': False, 'error': 'invalid_char'})
old = world.name
world.name = name
_logger.info('yt.world rename uid=%s %r -> %r', user.id, old, name)
return _json({'ok': True, 'name': name})
@http.route('/yt_world/api/micro/build/save', type='http', auth='public',
methods=['POST', 'OPTIONS'], csrf=False)
def save_micro_build(self, **kw):
"""保存某地块微观世界的建造布置。body: {"q":int,"r":int,"items":[{i,j,type}]}
找不到地块时自动建(保证种子地块也能落库记忆)。"""
auth_err = self._require_user()
if auth_err:
return _json(auth_err)
user = self._get_user()
world = request.env['yt.world'].sudo().search(
[('user_id', '=', user.id)], limit=1)
if not world:
return _json({'ok': False, 'error': 'no_world'})
body = _post_body()
q, r, items = body.get('q'), body.get('r'), body.get('items')
if q is None or r is None or items is None:
return _json({'ok': False, 'error': 'bad_params'})
if not isinstance(items, list):
items = []
tile = request.env['yt.world.tile'].sudo().search(
[('world_id', '=', world.id), ('q', '=', q), ('r', '=', r)], limit=1)
if not tile:
tile = request.env['yt.world.tile'].sudo().create(
{'world_id': world.id, 'q': q, 'r': r})
if not tile.micro_world_id:
tile._ensure_micro()
tile.micro_world_id.build_items = items
return _json({'ok': True, 'count': len(items)})
@http.route('/yt_world/api/micro/name', type='http', auth='public',
methods=['POST', 'OPTIONS'], csrf=False)
def rename_micro(self, **kw):
"""修改某地块微观世界的名称。body: {"q":int,"r":int,"name":"..."}
找不到地块时自动建(保证种子地块也能落库记忆);与 rename_world 同样的校验。"""
auth_err = self._require_user()
if auth_err:
return _json(auth_err)
user = self._get_user()
world = request.env['yt.world'].sudo().search(
[('user_id', '=', user.id)], limit=1)
if not world:
return _json({'ok': False, 'error': 'no_world'})
body = _post_body()
q, r, name = body.get('q'), body.get('r'), (body.get('name') or '').strip()
if q is None or r is None:
return _json({'ok': False, 'error': 'bad_params'})
if not name:
return _json({'ok': False, 'error': 'empty'})
if len(name) > 24:
return _json({'ok': False, 'error': 'too_long'})
if re.search(r'[\x00-\x1f\x7f]', name):
return _json({'ok': False, 'error': 'invalid_char'})
tile = request.env['yt.world.tile'].sudo().search(
[('world_id', '=', world.id), ('q', '=', q), ('r', '=', r)], limit=1)
if not tile:
tile = request.env['yt.world.tile'].sudo().create(
{'world_id': world.id, 'q': q, 'r': r})
if not tile.micro_world_id:
tile._ensure_micro()
old = tile.micro_world_id.name
tile.micro_world_id.name = name
_logger.info('yt.world.tile.micro rename uid=%s (%s,%s) %r -> %r',
user.id, q, r, old, name)
return _json({'ok': True, 'name': name, 'q': q, 'r': r})
@http.route('/yt_world/api/micro/catalog', type='http', auth='public',
methods=['GET', 'OPTIONS'], csrf=False)
def micro_catalog(self, **kw):
"""微观世界建造素材目录(前端 MICRO_BUILD_ITEMS 的单一来源)。
关联当前用户世界:返回「全局模板(world_id空) + 本世界专属」的素材。"""
auth_err = self._require_user()
if auth_err:
return _json(auth_err)
user = self._get_user()
world = request.env['yt.world'].sudo().search(
[('user_id', '=', user.id)], limit=1)
if world:
dom = ['|', ('world_id', '=', False), ('world_id', '=', world.id)]
else:
dom = [('world_id', '=', False)]
dom = ['&', ('active', '=', True)] + dom
items = request.env['yt.world.build.item'].sudo().search(dom, order='sequence, id')
CAT_LABELS = {
'build': '建筑', 'facility': '设施', 'scene': '场景',
'bio': '生物', 'other': '其他', 'road': '道路', 'ground': '地面',
}
seen = []
out = []
for r in items.read(['code', 'name', 'category', 'icon', 'footprint_w',
'footprint_h', 'cost_gold', 'terrains', 'desc', 'flat',
'stat_level', 'stat_hp', 'stat_yield', 'stat_yield_instant',
'stat_yield_tick', 'actions']):
if r['category'] not in seen:
seen.append(r['category'])
# 产出以代码规格 BUILD_SPECS 为单一真源;代码里已定义的建筑不再回退到 item 字段
from odoo.addons.yt_world.build_spec import get_spec, spec_defined
spec = get_spec(r['code'])
if spec_defined(r['code']):
instant = spec.get('stat_yield_instant') or {}
tick = spec.get('stat_yield_tick') or {}
else:
# 未在代码规格中定义的建筑,才回退到 item 字段/旧 stat_yield 做兼容
instant = spec.get('stat_yield_instant') or r['stat_yield_instant'] or {}
tick = spec.get('stat_yield_tick') or r['stat_yield_tick'] or {}
legacy = r['stat_yield'] or {}
if not instant and not tick:
# 旧数据尚未拆分,全部视为一次性产出(保守处理)
instant = legacy
out.append({
'code': r['code'], 'name': r['name'], 'category': r['category'],
'icon': r['icon'], 'w': r['footprint_w'], 'h': r['footprint_h'],
'costGold': r['cost_gold'] or 0,
'terrains': r['terrains'] or [],
'desc': r['desc'] or '',
'flat': bool(r['flat']),
'statLevel': r['stat_level'] or 1,
'statHp': r['stat_hp'] or 100,
'statYield': r['stat_yield'] or {},
'statYieldInstant': instant,
'statYieldTick': tick,
'actions': r['actions'] or [],
})
cats = [{'id': c, 'name': CAT_LABELS.get(c, c)} for c in seen]
return _json({'cats': cats, 'items': out})
@http.route('/yt_world/api/micro/build/charge', type='http', auth='public',
methods=['POST', 'OPTIONS'], csrf=False)
def charge_build(self, **kw):
"""建造扣费(落库前调用)。body: {"code": "cottage", "count": 1}
校验建筑存在并按 cost_gold * count 扣减玩家金币;余额不足明确报错。"""
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()
count = body.get('count') or 1
try:
count = int(count)
except (TypeError, ValueError):
count = 1
if count < 1:
count = 1
if not code:
return _json({'ok': False, 'error': 'bad_params'})
item = request.env['yt.world.build.item'].sudo().search(
[('code', '=', code), ('active', '=', True)], limit=1)
if not item:
return _json({'ok': False, 'error': 'unknown_build', 'code': code})
cost = (item.cost_gold or 0) * count
char = self._get_char(world)
gold = char.gold or 0
if gold < cost:
return _json({'ok': False, 'error': 'no_gold', 'need': cost, 'have': gold, 'code': code})
char.sudo().write({'gold': gold - cost})
_logger.info('yt.world build charge uid=%s code=%s count=%s cost=%s remain=%s',
request.session.uid, code, count, cost, gold - cost)
return _json({'ok': True, 'code': code, 'count': count, 'cost': cost, 'gold': gold - cost})
# ========== 图鉴 API ==========
def _records_json(self, model, fields):
"""将 model 的全部记录转为 [{field: value}, ...] 格式(Json 字段解析为对象)。"""
recs = request.env[model].sudo().search([], order='sequence, id')
out = []
for r in recs:
item = {}
for f in fields:
val = getattr(r, f)
if isinstance(val, str):
try:
val = json.loads(val)
except (ValueError, TypeError):
pass
item[f] = val
out.append(item)
return _json(out)
@http.route('/yt_world/api/codex/resources', type='http', auth='public',
methods=['GET', 'OPTIONS'], csrf=False)
def codex_resources(self, **kw):
return self._records_json('yt.codex.resource',
['code','name','icon','category','quality','source','uses','recipe','description'])
@http.route('/yt_world/api/codex/creatures', type='http', auth='public',
methods=['GET', 'OPTIONS'], csrf=False)
def codex_creatures(self, **kw):
return self._records_json('yt.codex.creature',
['code','name','icon','habitat','tameable','diet','products','description'])
@http.route('/yt_world/api/codex/monsters', type='http', auth='public',
methods=['GET', 'OPTIONS'], csrf=False)
def codex_monsters(self, **kw):
return self._records_json('yt.codex.monster',
['code','name','icon','tier','level_min','level_max','habitat','drops','description'])
@http.route('/yt_world/api/codex/races', type='http', auth='public',
methods=['GET', 'OPTIONS'], csrf=False)
def codex_races(self, **kw):
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,
**self._char_currency(char),
'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,
**self._char_currency(char),
})
@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,
**self._char_currency(char),
})
@http.route('/yt_world/api/char/talent/draw', type='http', auth='public',
methods=['POST', 'OPTIONS'], csrf=False)
def char_talent_draw(self, **kw):
"""新手抽天赋:返回 15 张随机未拥有的天赋牌(前端先展示牌背,翻牌时再免费激活)。"""
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[:15]})
@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 '🌟'})
# ============================================================
# 游戏化系统:角色 / 生物 / 订单 / 建筑交互(前端-后端打通)
# ============================================================
# 建筑动作效果表(数据驱动,与前端 BUILD_INTERACT 的 action id 对应)。
# gain/cost 中 'gold' 表示金币,其余键为资源编码(food/wood/metal/...)。
# heal: True 时需要将 creature_id 传回,并把该生物生命回满。
BUILD_EFFECTS = {
# —— 建筑(3/4 立绘)——
('cottage', 'employ'): {'msg': '已发布村长任职委托'},
('cottage', 'recruit'): {'gain': {'gold': 5}, 'msg': '招募村民,人口+1'},
('cottage', 'upgrade'): {'cost': {'gold': 50}, 'msg': '农舍升级申请已提交'},
('windmill', 'collect'): {'gain': {'flour': 3}, 'msg': '领取面粉 +3'},
('windmill', 'produce'): {'cost': {'flour': 2}, 'gain': {'food': 4}, 'msg': '加工粮食 +4'},
('windmill', 'upgrade'): {'cost': {'gold': 40}, 'msg': '风车升级申请已提交'},
('blacksmith', 'forge'): {'gain': {'metal': 3}, 'msg': '锻造金属 +3'},
('blacksmith', 'commission'): {'msg': '已发布装备委托'},
('blacksmith', 'upgrade'): {'cost': {'gold': 60}, 'msg': '铁匠铺升级申请已提交'},
('grocery', 'sell'): {'gain': {'gold': 15}, 'msg': '售货收入 +15 金币'},
('grocery', 'restock'): {'cost': {'gold': 10}, 'gain': {'food': 2, 'wood': 1}, 'msg': '进货:粮+2 木+1'},
('grocery', 'upgrade'): {'cost': {'gold': 30}, 'msg': '杂货铺升级申请已提交'},
('temple', 'pray'): {'gain': {'prestige': 2}, 'msg': '祈祷获得声望 +2'},
('temple', 'donate'): {'cost': {'gold': 20}, 'gain': {'prestige': 5}, 'msg': '捐赠获得声望 +5'},
('temple', 'upgrade'): {'cost': {'gold': 50}, 'msg': '神庙升级申请已提交'},
('clinic', 'heal'): {'heal': True, 'msg': '治疗完成,生物生命已回满'},
('clinic', 'upgrade'): {'cost': {'gold': 40}, 'msg': '医务室升级申请已提交'},
('restaurant', 'cook'): {'cost': {'food': 2}, 'gain': {'satiety': 3}, 'msg': '烹制料理,饱食 +3'},
('restaurant', 'upgrade'): {'cost': {'gold': 30}, 'msg': '饭馆升级申请已提交'},
('pasture', 'breed'): {'gain': {'meat': 2, 'milk': 1}, 'msg': '畜牧产出:肉+2 奶+1'},
('pasture', 'upgrade'): {'cost': {'gold': 40}, 'msg': '牧场升级申请已提交'},
('blackmarket', 'trade'): {'cost': {'gold': 25}, 'gain': {'rare': 1}, 'msg': '黑市交易(风险):稀有物资 +1'},
('blackmarket', 'upgrade'):{'cost': {'gold': 80}, 'msg': '黑市升级申请已提交'},
('school', 'study'): {'gain': {'research': 3}, 'msg': '研读获得科研 +3'},
('school', 'lecture'): {'cost': {'gold': 10}, 'gain': {'prestige': 2}, 'msg': '讲学获得声望 +2'},
('school', 'upgrade'): {'cost': {'gold': 50}, 'msg': '学堂升级申请已提交'},
# —— 设施(平面)——
('farmland', 'harvest'): {'gain': {'food': 5}, 'msg': '收获粮食 +5'},
('farmland', 'sow'): {'cost': {'gold': 5}, 'gain': {'food': 2}, 'msg': '播种:预期粮+2'},
('farmland', 'upgrade'): {'cost': {'gold': 20}, 'msg': '农田升级申请已提交'},
('fence', 'repair'): {'cost': {'wood': 2}, 'msg': '栅栏已修缮'},
('fence', 'upgrade'): {'cost': {'gold': 15}, 'msg': '栅栏升级申请已提交'},
('wall', 'reinforce'): {'cost': {'stone': 3}, 'gain': {'prestige': 1}, 'msg': '围墙加固,防御+'},
('wall', 'upgrade'): {'cost': {'gold': 25}, 'msg': '围墙升级申请已提交'},
('storage', 'deposit'): {'msg': '已打开仓储(在前端存取背包)'},
('storage', 'upgrade'): {'cost': {'gold': 20}, 'msg': '存储箱升级申请已提交'},
('well', 'draw'): {'gain': {'water': 3}, 'msg': '取水 +3'},
('well', 'upgrade'): {'cost': {'gold': 15}, 'msg': '水井升级申请已提交'},
('market', 'stall'): {'gain': {'gold': 12}, 'msg': '摆摊收入 +12 金币'},
('market', 'upgrade'): {'cost': {'gold': 25}, 'msg': '集市升级申请已提交'},
}
def _require_user(self):
"""auth='public' 时手动校验登录态;未登录返回统一错误字典。
Odoo auth='public' 不会把 session uid 同步到 request.env.user,
必须显式从 request.session.uid 读取真实用户。
仅校验「已登录 + 账号有效」:门户/游客账号(share=True)也允许访问
自己的世界——数据安全靠 world_id 按用户隔离,不依赖内部用户组。"""
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_user(self):
uid = request.session.uid
return request.env['res.users'].sudo().browse(uid) if uid else request.env['res.users']
def _get_world(self):
"""取当前用户世界,不存在则自动创建(保证任何入口/新账号都能保存)。"""
user = self._get_user()
if not user.exists():
return None
return request.env['yt.world'].sudo().get_or_create_for_user(user)
def _get_char(self, world):
char = request.env['yt.world.character'].sudo().search(
[('world_id', '=', world.id)], limit=1)
if not char:
# 认领当前登录人的玩家档案(hr.employee),把注册昵称同步为角色名
vals = {'world_id': world.id}
uid = request.session.uid
if uid:
emp = request.env['hr.employee'].sudo().search(
[('user_id', '=', uid)], limit=1)
if emp:
vals['employee_id'] = emp.id
vals['name'] = emp.name or char.name
char = request.env['yt.world.character'].sudo().create(vals)
else:
# 兼容老数据:已存在角色但没关联 employee 时补上
if not char.employee_id:
uid = request.session.uid
if uid:
emp = request.env['hr.employee'].sudo().search(
[('user_id', '=', uid)], limit=1)
if emp:
char.sudo().write({'employee_id': emp.id,
'name': emp.name or char.name})
return char
def _char_currency(self, char):
"""角色货币字段统一序列化(金币/铜币/银币/钻石)。"""
return {
'gold': char.gold or 0,
'copper': char.copper or 0,
'silver': char.silver or 0,
'diamond': char.diamond or 0,
}
# ========== 任务系统序列化(get_tasks / task_progress 共用) ==========
def _serialize_tasks(self, char):
"""把任务定义 + 角色进度汇总成前端面板可直接渲染的结构。"""
done_codes = [t['code'] for t in (char.tasks or []) if t.get('state') == 'done']
tasks = request.env['yt.world.task'].sudo().search(
[('active', '=', True)], order='chapter, sequence, id')
out = []
for t in tasks:
prereq = t.prereq or []
entry = None
for x in (char.tasks or []):
if x.get('code') == t.code:
entry = x
break
if entry and entry.get('state') == 'done':
state = 'done'
elif any(p not in done_codes for p in prereq):
state = 'locked'
elif entry and entry.get('state') == 'claimable':
state = 'claimable'
else:
state = 'in_progress'
progress = (entry or {}).get('progress') or {}
objs = []
for o in (t.objectives or []):
oid = o.get('id') or ('%s:%s' % (o.get('type'), o.get('key')))
need = o.get('count', 1) or 1
have = min(progress.get(oid, 0), need)
objs.append({
'type': o.get('type'), 'key': o.get('key'),
'label': o.get('label'), 'hint': o.get('hint'),
'have': have, 'need': need,
})
out.append({
'code': t.code, 'chapter': t.chapter, 'title': t.title,
'desc': t.desc or '', 'objectives': objs,
'reward_exp': t.reward_exp or 0,
'reward_res': t.reward_res or {},
'prereq': prereq, 'state': state,
})
chapters = sorted({t['chapter'] for t in out})
return chapters, out
@http.route('/yt_world/api/tasks', type='http', auth='public',
methods=['GET', 'OPTIONS'], csrf=False)
def get_tasks(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)
# 回溯校验:角色等级可能已满足 level_up 类型任务,补发事件
char.on_game_event('level_up', {'level': char.level})
chapters, out = self._serialize_tasks(char)
return _json({
'ok': True,
'chapters': chapters,
'tasks': out,
'char': {
'name': char.name, 'level': char.level, 'exp': char.exp,
'exp_max': char.exp_max, **self._char_currency(char),
},
})
@http.route('/yt_world/api/task/progress', type='http', auth='public',
methods=['POST', 'OPTIONS'], csrf=False)
def task_progress(self, **kw):
"""接收前端游戏事件并推进任务进度。body: {"event":"build_placed","payload":{"code":"farmland"}}
返回更新后的任务列表(与 /tasks 同结构),claimable 列出本次新可领取的任务。"""
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()
event = (body.get('event') or '').strip()
payload = body.get('payload') or {}
if not event:
return _json({'ok': False, 'error': 'bad_params'})
char = self._get_char(world)
res = char.on_game_event(event, payload)
# 回溯校验:角色等级可能已满足 level_up 类型任务,补发事件
char.on_game_event('level_up', {'level': char.level})
chapters, out = self._serialize_tasks(char)
return _json({
'ok': True,
'chapters': chapters,
'tasks': out,
'char': {
'name': char.name, 'level': char.level, 'exp': char.exp,
'exp_max': char.exp_max, **self._char_currency(char),
},
'claimable': res.get('claimable', []),
})
@http.route('/yt_world/api/task/complete', type='http', auth='public',
methods=['POST', 'OPTIONS'], csrf=False)
def complete_task(self, **kw):
"""领取/完成任务。body: {"code": "ch1_welcome"}
校验前置→发奖→写进度,返回最新角色状态。"""
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)
res = char.complete_task(code)
if not res.get('ok'):
return _json(res)
return _json({
'ok': True,
'already': res.get('already', False),
'char': {
'name': char.name, 'level': char.level, 'exp': char.exp,
'exp_max': char.exp_max, **self._char_currency(char),
},
'reward_exp': res.get('reward_exp', 0),
'reward_res': res.get('reward_res', {}),
})
@http.route('/yt_world/api/char', type='http', auth='public',
methods=['GET', 'OPTIONS'], csrf=False)
def get_char(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,
'name': char.name, 'level': char.level, 'exp': char.exp,
'exp_max': char.exp_max or 100, **self._char_currency(char),
'inventory': char.inventory or {},
'equip': char.equip or {'weapon': None, 'armor': None, 'accessory': None},
})
@http.route('/yt_world/api/char/save', type='http', auth='public',
methods=['POST', 'OPTIONS'], csrf=False)
def save_char(self, **kw):
"""保存角色(等级/经验/金币/背包/装备)。body: 部分字段皆可。"""
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)
body = _post_body()
vals = {}
for f in ('name', 'level', 'exp', 'gold', 'copper', 'silver', 'diamond'):
if f in body and body[f] is not None:
vals[f] = body[f]
if 'inventory' in body and body['inventory'] is not None:
vals['inventory'] = body['inventory']
if 'equip' in body and body['equip'] is not None:
vals['equip'] = self._normalize_equip(body['equip'])
char.sudo().write(vals)
return _json({'ok': True, 'char': {
'name': char.name, 'level': char.level, 'exp': char.exp,
**self._char_currency(char),
'inventory': char.inventory or {},
'equip': char.equip or {},
}})
@staticmethod
def _normalize_equip(equip):
"""equip 的值统一规范为装备 code 字符串。
读取接口返回的是解析后的装备对象(含 code/name/desc…),前端原样回存
会把它写成字典,导致后续序列化异常。这里统一压回 code。"""
if not isinstance(equip, dict):
return {}
out = {}
for slot, raw in equip.items():
out[slot] = (raw.get('code') if isinstance(raw, dict) else raw)
return out
@http.route('/yt_world/api/creatures', type='http', auth='public',
methods=['GET', 'OPTIONS'], csrf=False)
def get_creatures(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'})
recs = request.env['yt.world.creature'].sudo().search(
[('world_id', '=', world.id)], order='id')
out = []
for r in recs:
try:
out.append(r.to_panel_dict())
except Exception:
# 单条脏数据不得拖垮整个接口,否则玩家所有生物都读不出来
_logger.exception('[yt_world] 生物 %s 序列化失败,已降级返回', r.cid)
out.append({
'oid': r.id, 'cid': r.cid, 'race': r.race, 'name': r.name,
'level': r.level, 'exp': r.exp, 'exp_max': r.exp_max,
'hp': r.hp, 'hp_max': r.hp_max,
'energy': r.energy, 'energy_max': r.energy_max,
'attack': r.attack, 'defense': r.defense, 'speed': r.speed,
'mood': r.mood, 'job': r.job, 'status': r.status, 'state': r.state,
'x': r.x, 'y': r.y, 'facing': r.facing,
'tile_ref': r.tile_ref or {}, 'equip': {},
})
return _json({'ok': True, 'creatures': out})
@http.route('/yt_world/api/creatures/save', type='http', auth='public',
methods=['POST', 'OPTIONS'], csrf=False)
def save_creatures(self, **kw):
"""upsert 生物列表。body: {"creatures":[{cid,race,name,level,exp,exp_max,hp,hp_max,energy,energy_max,attack,defense,speed,x,y,facing,status,state,mood,job,equip,tile_ref}]}
按 cid 匹配:存在则更新,否则新建。返回带 oid 的列表。"""
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()
items = body.get('creatures') or []
model = request.env['yt.world.creature'].sudo()
out = []
for c in items:
cid = c.get('cid')
vals = {
'name': c.get('name') or '居民',
'race': c.get('race') or 'human',
'level': c.get('level', 1), 'exp': c.get('exp', 0),
'exp_max': c.get('exp_max', 100),
'hp': c.get('hp', 50), 'hp_max': c.get('hp_max', 50),
'energy': c.get('energy', 100), 'energy_max': c.get('energy_max', 100),
'attack': c.get('attack', 5), 'defense': c.get('defense', 3),
'speed': c.get('speed', 5),
'x': float(c.get('x') or 0), 'y': float(c.get('y') or 0),
'facing': int(c.get('facing') or 1),
'status': c.get('status') or 'idle',
'state': c.get('state') or 'idle',
'mood': c.get('mood', '😊'), 'job': c.get('job', 'none'),
'tile_ref': c.get('tile_ref') or {},
}
rec = model.search([('world_id', '=', world.id), ('cid', '=', cid)], limit=1) if cid else None
if rec:
# 装备加成是"临时"的(来自实例,不存基础字段);前端回传的是有效属性,
# 落库时回扣装备加成,避免把临时加成固化进基础值导致下次加载重复叠加。
bonus = rec._equip_bonus()
eff_hp_max = vals.get('hp_max', rec.hp_max)
for f in ('attack', 'defense', 'hp_max'):
if f in vals and isinstance(vals[f], (int, float)):
vals[f] = max(0, int(vals[f]) - bonus.get(f, 0))
if 'hp' in vals:
vals['hp'] = min(vals['hp'], eff_hp_max)
rec.write(vals)
else:
vals['world_id'] = world.id
vals['cid'] = cid
rec = model.create(vals)
out.append({'oid': rec.id, 'cid': cid, 'level': rec.level,
'exp': rec.exp, 'hp': rec.hp})
# 删除指定 cid
deleted = body.get('deleted') or []
if deleted:
model.search([('world_id', '=', world.id), ('cid', 'in', list(deleted))]).unlink()
return _json({'ok': True, 'creatures': out, 'deleted': len(deleted)})
def _find_build_action(self, code, action):
"""优先从 yt.world.build.item 的 actions 字段查动作配置;没有则回退 BUILD_EFFECTS。"""
item = request.env['yt.world.build.item'].sudo().search(
[('code', '=', code), ('active', '=', True)], limit=1)
if item and item.actions:
for a in item.actions:
if a.get('action') == action:
return a
return self.BUILD_EFFECTS.get((code, action))
def _upgrade_building_instance(self, world, code, q, r, i, j):
"""在地块 micro_world 的 build_items 中定位指定建筑并升级一级。"""
tile = request.env['yt.world.tile'].sudo().search(
[('world_id', '=', world.id), ('q', '=', q), ('r', '=', r)], limit=1)
if not tile or not tile.micro_world_id:
return None
items = list(tile.micro_world_id.build_items or [])
for b in items:
if b.get('type') == code and b.get('i') == i and b.get('j') == j:
b['level'] = (b.get('level') or 1) + 1
b['exp'] = (b.get('exp') or 0) + 50
tile.micro_world_id.sudo().write({'build_items': items})
return {'level': b['level'], 'exp': b['exp']}
return None
@http.route('/yt_world/api/building/action', type='http', auth='public',
methods=['POST', 'OPTIONS'], csrf=False)
def building_action(self, **kw):
"""执行建筑交互动作。body: {"code","action","q","r","i","j","creature_id"}
数据驱动:优先查数据库 actions,回退 BUILD_EFFECTS;升级会真正写入 build_items。"""
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 body.get('type')
action = body.get('action')
creature_id = body.get('creature_id')
q, r, i, j = body.get('q'), body.get('r'), body.get('i'), body.get('j')
eff = self._find_build_action(code, action)
if not eff:
return _json({'ok': False, 'error': 'unknown_action', 'key': [code, action]})
char = self._get_char(world)
inv = dict(char.inventory or {})
gold = char.gold or 0
# 扣除成本
for k, v in (eff.get('cost') or {}).items():
if k == 'gold':
if gold < v:
return _json({'ok': False, 'error': 'no_gold'})
gold -= v
else:
if inv.get(k, 0) < v:
return _json({'ok': False, 'error': 'no_res', 'res': k})
inv[k] = inv.get(k, 0) - v
# 发放收益
for k, v in (eff.get('gain') or {}).items():
if k == 'gold':
gold += v
else:
inv[k] = inv.get(k, 0) + v
# 治疗生物
if eff.get('heal') and creature_id:
cr = request.env['yt.world.creature'].sudo().search(
[('id', '=', int(creature_id)), ('world_id', '=', world.id)], limit=1)
if cr:
cr.hp = cr.hp_max
# 升级:持久化建筑实例状态
upgrade_info = None
if action == 'upgrade':
if q is None or r is None or i is None or j is None:
return _json({'ok': False, 'error': 'bad_pos'})
upgrade_info = self._upgrade_building_instance(world, code, q, r, i, j)
if not upgrade_info:
return _json({'ok': False, 'error': 'building_not_found'})
# 持久化角色
char.sudo().write({'gold': gold, 'inventory': inv})
res = {
'ok': True, 'gold': gold, 'inventory': inv,
'message': eff.get('msg', '操作成功'),
}
if upgrade_info:
res.update(upgrade_info)
return _json(res)
# ============================================================
# 微观世界角色(玩家放置的种族角色,绑定用户持久化)
# ============================================================
@http.route('/yt_world/api/races', type='http', auth='public',
methods=['GET', 'OPTIONS'], csrf=False)
def get_races(self, **kw):
"""种族模板(单一事实来源)。返回前端 characters.js 初始化所需全部字段。"""
races = request.env['yt.world.race'].sudo().search([('active', '=', True)], order='sequence')
return _json({'ok': True, 'races': [r.to_dict() for r in races]})
@http.route('/yt_world/api/equip/catalog', type='http', auth='public',
methods=['GET', 'OPTIONS'], csrf=False)
def equip_catalog(self, **kw):
"""装备目录(单一事实来源)。同时返回 slots 槽位真源列表,
前端面板据此动态渲染(含尚无装备的空槽位)。"""
Item = request.env['yt.world.equip.item'].sudo()
items = Item.search([('active', '=', True)])
slots = [{'value': v, 'label': l} for v, l in Item._fields['slot'].selection]
return _json({'ok': True, 'items': [it.to_dict() for it in items], 'slots': slots})
def _creature_by_cid(self, world, cid):
if not cid:
return None
return request.env['yt.world.creature'].sudo().search(
[('world_id', '=', world.id), ('cid', '=', cid)], limit=1)
@http.route('/yt_world/api/creature/<string:cid>', type='http', auth='public',
methods=['GET', 'OPTIONS'], csrf=False)
def creature_detail(self, cid, **kw):
"""取单个生物详情(按 cid)。"""
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'})
rec = self._creature_by_cid(world, cid)
if not rec:
return _json({'ok': False, 'error': 'char_not_found'})
return _json({'ok': True, 'char': rec.to_panel_dict()})
@http.route('/yt_world/api/creature/action', type='http', auth='public',
methods=['POST', 'OPTIONS'], csrf=False)
def creature_action(self, cid=None, **kw):
"""生物操作:equip / unequip / train / rest。body: {cid, action, slot?, code?}"""
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()
cid = body.get('cid') or cid
action = body.get('action')
rec = self._creature_by_cid(world, cid)
if not rec:
return _json({'ok': False, 'error': 'char_not_found'})
if action == 'equip':
slot = body.get('slot')
code = body.get('code')
valid_slots = [s[0] for s in request.env['yt.world.equip.item']._fields['slot'].selection]
if slot not in valid_slots:
return _json({'ok': False, 'error': 'bad_slot'})
item = request.env['yt.world.equip.item'].sudo().search([('code', '=', code)], limit=1)
if not item:
return _json({'ok': False, 'error': 'item_not_found'})
if item.slot != slot:
return _json({'ok': False, 'error': 'slot_mismatch'})
# 装备改为管理"实例":每只装备是独立实物,支撑未来耐久/强化/交易
Inst = request.env['yt.world.equip.instance'].sudo()
# 1) 先卸下该槽现有装备(equipped_slot 置空 = 回背包)
old = Inst.search([('owner_creature_id', '=', rec.id),
('equipped_slot', '=', slot)], limit=1)
if old:
old.write({'equipped_slot': False})
# 2) 优先复用背包中同款未装备实例(避免每次点击都新建,造成实例堆积)
reuse = Inst.search([('owner_creature_id', '=', rec.id),
('item_id', '=', item.id),
('equipped_slot', '=', False)], limit=1)
if reuse:
reuse.write({'equipped_slot': slot})
else:
Inst.create({'item_id': item.id, 'owner_creature_id': rec.id,
'equipped_slot': slot, 'durability': 100,
'max_durability': 100, 'enhance_level': 0})
elif action == 'unequip':
slot = body.get('slot')
# 仅把该槽实例的 equipped_slot 置空(回背包),不删除实例
inst = request.env['yt.world.equip.instance'].sudo().search(
[('owner_creature_id', '=', rec.id), ('equipped_slot', '=', slot)], limit=1)
if inst:
inst.write({'equipped_slot': False})
elif action == 'train':
rec.write({
'level': rec.level + 1,
'exp': 0,
'exp_max': rec.exp_max + 50,
'hp_max': rec.hp_max + 10,
'energy_max': rec.energy_max + 5,
'attack': rec.attack + 2,
'defense': rec.defense + 1,
'speed': rec.speed + 1,
'hp': rec.hp_max + 10,
'energy': rec.energy_max + 5,
})
elif action == 'rest':
rec.write({'status': 'resting', 'hp': rec.hp_max, 'energy': rec.energy_max})
else:
return _json({'ok': False, 'error': 'unknown_action'})
return _json({'ok': True, 'char': rec.to_panel_dict()})
# ============================================================
# 资源系统:库存 / 结算 / 离线囤积回收
# ============================================================
def _serialize_build_instance(self, inst):
"""把 yt.world.build.instance 序列化为前端可用结构。"""
return {
'id': inst.id,
'code': inst.code,
'i': inst.i, 'j': inst.j,
'rot': inst.rot or 0,
'level': inst.level or 1,
'stored': inst.stored or {},
'last_tick': fields.Datetime.to_string(inst.last_tick) if inst.last_tick else None,
}
def _serialize_stock(self, world, resource_code=None):
"""序列化世界库存。resource_code 为 None 返回全部。"""
Stock = request.env['yt.world.stock'].sudo()
dom = [('world_id', '=', world.id)]
if resource_code:
dom.append(('resource_code', '=', resource_code))
return {s.resource_code: {
'amount': s.amount or 0.0,
'rate_per_min': s.rate_per_min or 0.0,
'capacity': s.capacity or -1.0,
'last_settle': fields.Datetime.to_string(s.last_settle) if s.last_settle else None,
} for s in Stock.search(dom)}
@http.route('/yt_world/api/world/resources', type='http', auth='public',
methods=['GET', 'OPTIONS'], csrf=False)
def world_resources(self, **kw):
"""取当前世界资源状态。
可选 query: q, r(当前微观地块),返回该地块内建筑实例的囤积状态。
"""
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'})
world._recalc_rates()
q, r = kw.get('q'), kw.get('r')
instances = []
if q is not None and r is not None:
tile = request.env['yt.world.tile'].sudo().search([
('world_id', '=', world.id), ('q', '=', int(q)), ('r', '=', int(r))], limit=1)
if tile and tile.micro_world_id:
instances = [
self._serialize_build_instance(i)
for i in request.env['yt.world.build.instance'].sudo().search(
[('micro_id', '=', tile.micro_world_id.id)])
]
return _json({
'ok': True,
'stocks': self._serialize_stock(world),
'instances': instances,
})
@http.route('/yt_world/api/world/settle', type='http', auth='public',
methods=['POST', 'OPTIONS'], csrf=False)
def world_settle(self, **kw):
"""手动/定时触发世界资源结算。body: {"auto_collect": true/false, "force_collect": true/false}
在线自动进账时 auto_collect=true;离线登录后 force_collect=true 可把已有囤积也兜底回库。
"""
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()
auto_collect = bool(body.get('auto_collect', False))
force_collect = bool(body.get('force_collect', False))
res = world._settle_all(auto_collect=auto_collect, force_collect=force_collect)
world._recalc_rates()
return _json({
'ok': True,
'auto_collect': auto_collect,
'force_collect': force_collect,
'produced': res.get('produced', {}),
'collected': res.get('collected', {}),
'stocks': self._serialize_stock(world),
})
@http.route('/yt_world/api/building/collect', type='http', auth='public',
methods=['POST', 'OPTIONS'], csrf=False)
def building_collect(self, **kw):
"""回收指定建筑实例的囤积资源。body: {"q": int, "r": int, "i": int, "j": int}
返回回收到的资源数量与最新库存。
"""
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()
q, r, i, j = body.get('q'), body.get('r'), body.get('i'), body.get('j')
if q is None or r is None or i is None or j is None:
return _json({'ok': False, 'error': 'bad_params'})
tile = request.env['yt.world.tile'].sudo().search([
('world_id', '=', world.id), ('q', '=', int(q)), ('r', '=', int(r))], limit=1)
if not tile or not tile.micro_world_id:
return _json({'ok': False, 'error': 'no_micro'})
res = world._collect_building(tile.micro_world_id.id, int(i), int(j))
if not res.get('ok'):
return _json(res)
world._recalc_rates()
return _json({
'ok': True,
'collected': res.get('collected', {}),
'stocks': self._serialize_stock(world),
})