后端(Odoo addons): - game_base: 迁入 关于 功能(game.page/game.page.item/yusen.asset 模型、page_views/asset_views、6条权限、menu_website_about 左侧应用入口),新增 chat 模块(AI世界聊天: chat.py/chat_api.py/chat_data.xml/chat_views.xml) - yt_world: 合并 game_ai_npc(npc 模型/数据/controller),修复菜单 NotNullViolation - yusen_website: 移除已迁出的 page/asset,背景菜单改挂 game_base.action_game_page - 新增 game_asset_backup 纯数据模块: 备份前端素材到 addons(game_assets/),预置 图鉴(game.codex.entry)9条(过场CG/动物) + 素材管理(yusen.asset)120条(地形/建筑/瓦片/幻灯片/背景/路/UI),按类分 tags 前端(assets + HTML): - 新增 chat.js / game-api.js、chat.html / buildings_default_demo.html - 清理旧 slides/website 冗余文件,更新 GENERATION_RULES.md 与动物九宫格素材
125 lines
5.1 KiB
Python
125 lines
5.1 KiB
Python
# -*- coding: utf-8 -*-
|
|
import json
|
|
from odoo import http
|
|
from odoo.http import request
|
|
from datetime import datetime
|
|
|
|
def _json(data):
|
|
return request.make_response(
|
|
json.dumps(data, ensure_ascii=False, default=str),
|
|
headers=[('Content-Type', 'application/json; charset=utf-8')]
|
|
)
|
|
|
|
|
|
class GameNpcApi(http.Controller):
|
|
|
|
# ── NPC 列表 ──
|
|
@http.route('/game/api/npcs', type='http', auth='public', methods=['GET'], csrf=False)
|
|
def npc_list(self, **kw):
|
|
"""返回所有活跃 NPC(含当前活动和关键状态)。"""
|
|
npcs = request.env['game.npc'].sudo().search_read(
|
|
[('active', '=', True)],
|
|
['id', 'name', 'title', 'code', 'archetype', 'personality',
|
|
'current_activity', 'energy', 'hunger', 'safety', 'mood',
|
|
'mood_label', 'activity_label', 'goal_progress', 'current_goal',
|
|
'current_area', 'relationship_count', 'recent_log', 'tick_count',
|
|
'affiliation'],
|
|
order='sequence, id'
|
|
)
|
|
for n in npcs:
|
|
n['archetype_label'] = dict(
|
|
request.env['game.npc']._fields['archetype'].selection
|
|
).get(n['archetype'], n['archetype'])
|
|
|
|
return _json({'npcs': npcs})
|
|
|
|
# ── NPC 详情 ──
|
|
@http.route('/game/api/npc/<int:npc_id>', type='http', auth='public', methods=['GET'], csrf=False)
|
|
def npc_detail(self, npc_id, **kw):
|
|
"""NPC 详情(含关系网 + 最近行为 + 知识)。"""
|
|
Npc = request.env['game.npc'].sudo()
|
|
npc = Npc.browse(npc_id)
|
|
if not npc.exists():
|
|
return _json({'error': 'NPC 不存在'})
|
|
|
|
data = npc.read([
|
|
'id', 'name', 'title', 'code', 'archetype', 'personality',
|
|
'origin', 'bio', 'current_activity', 'energy', 'hunger', 'safety',
|
|
'mood', 'mood_label', 'activity_label', 'goal_progress', 'current_goal',
|
|
'goal_detail', 'current_area', 'tick_count', 'last_tick',
|
|
'affiliation',
|
|
])
|
|
if not data:
|
|
return _json({'error': 'NPC 不存在'})
|
|
data = data[0]
|
|
|
|
# 关系网
|
|
rels = request.env['game.npc.relationship'].sudo().search_read(
|
|
[('npc_id', '=', npc_id)],
|
|
['target_npc_id', 'affinity', 'status', 'last_interaction'],
|
|
order='affinity desc'
|
|
)
|
|
data['relationships'] = [{
|
|
'target_id': r['target_npc_id'][0] if r['target_npc_id'] else None,
|
|
'target_name': r['target_npc_id'][1] if r['target_npc_id'] else '',
|
|
'affinity': r['affinity'],
|
|
'status': r['status'],
|
|
} for r in rels]
|
|
|
|
# 最近行为(20 条)
|
|
logs = request.env['game.npc.behavior.log'].sudo().search_read(
|
|
[('npc_id', '=', npc_id)],
|
|
['tick_time', 'activity', 'activity_label', 'location', 'summary', 'target_npc_id'],
|
|
limit=20, order='tick_time desc'
|
|
)
|
|
data['recent_behaviors'] = [{
|
|
'time': str(l['tick_time']),
|
|
'activity': l['activity'],
|
|
'label': l['activity_label'],
|
|
'location': l['location'] or '',
|
|
'summary': l['summary'],
|
|
} for l in logs]
|
|
|
|
# 知识
|
|
knowledge = request.env['game.npc.knowledge'].sudo().search_read(
|
|
[('npc_id', '=', npc_id)],
|
|
['topic', 'content', 'accuracy', 'source'],
|
|
order='sequence, id'
|
|
)
|
|
data['knowledge'] = knowledge
|
|
|
|
return _json(data)
|
|
|
|
# ── NPC 关系网 ──
|
|
@http.route('/game/api/npc/<int:npc_id>/relationships', type='http', auth='public', methods=['GET'], csrf=False)
|
|
def npc_relationships(self, npc_id, **kw):
|
|
rels = request.env['game.npc.relationship'].sudo().search_read(
|
|
[('npc_id', '=', npc_id)],
|
|
['target_npc_id', 'affinity', 'status', 'last_interaction', 'interaction_count'],
|
|
order='affinity desc'
|
|
)
|
|
return _json({'relationships': [{
|
|
'target_id': r['target_npc_id'][0] if r['target_npc_id'] else None,
|
|
'target_name': r['target_npc_id'][1] if r['target_npc_id'] else '',
|
|
'affinity': r['affinity'],
|
|
'status': r['status'],
|
|
'last_interaction': str(r['last_interaction']),
|
|
'interaction_count': r['interaction_count'],
|
|
} for r in rels]})
|
|
|
|
# ── 主动触发单个 NPC Tick ──
|
|
@http.route('/game/api/npc/<int:npc_id>/tick', type='http', auth='user', methods=['POST'], csrf=False)
|
|
def npc_tick(self, npc_id, **kw):
|
|
npc = request.env['game.npc'].sudo().browse(npc_id)
|
|
if not npc.exists():
|
|
return _json({'error': 'NPC 不存在'})
|
|
npc.action_tick()
|
|
return _json({'ok': True, 'name': npc.name, 'tick_count': npc.tick_count,
|
|
'activity': npc.activity_label, 'mood': npc.mood_label})
|
|
|
|
# ── 批量 Tick ──
|
|
@http.route('/game/api/npc/tick-all', type='http', auth='user', methods=['POST'], csrf=False)
|
|
def npc_tick_all(self, **kw):
|
|
count = request.env['game.npc'].sudo().cron_batch_tick(batch_size=100)
|
|
return _json({'ok': True, 'ticked': count})
|