# -*- coding: utf-8 -*- import logging import random from datetime import datetime, timedelta from odoo import api, fields, models, _ from odoo.exceptions import ValidationError _logger = logging.getLogger(__name__) # ── 角色类型/日程/效果常量 ── ARCHETYPE_SCHEDULES = { 'villager': { 0:'sleep', 1:'sleep', 2:'sleep', 3:'sleep', 4:'sleep', 5:'wake', 6:'rest', 7:'eat', 8:'work', 9:'work', 10:'work', 11:'work', 12:'eat', 13:'rest', 14:'work', 15:'work', 16:'work', 17:'work', 18:'rest', 19:'eat', 20:'social', 21:'social', 22:'social', 23:'sleep', }, 'merchant': { 0:'sleep', 1:'sleep', 2:'sleep', 3:'sleep', 4:'sleep', 5:'wake', 6:'rest', 7:'eat', 8:'trade', 9:'trade', 10:'trade', 11:'trade', 12:'eat', 13:'rest', 14:'trade', 15:'trade', 16:'trade', 17:'trade', 18:'rest', 19:'eat', 20:'social', 21:'social', 22:'social', 23:'sleep', }, 'warrior': { 0:'sleep', 1:'sleep', 2:'sleep', 3:'sleep', 4:'sleep', 5:'wake', 6:'train', 7:'eat', 8:'train', 9:'work', 10:'work', 11:'work', 12:'eat', 13:'rest', 14:'work', 15:'work', 16:'train', 17:'rest', 18:'eat', 19:'social', 20:'social', 21:'wander', 22:'wander', 23:'sleep', }, 'scholar': { 0:'sleep', 1:'sleep', 2:'sleep', 3:'sleep', 4:'sleep', 5:'wake', 6:'rest', 7:'eat', 8:'study', 9:'study', 10:'study', 11:'study', 12:'eat', 13:'rest', 14:'study', 15:'study', 16:'social', 17:'rest', 18:'eat', 19:'social', 20:'social', 21:'study', 22:'wander', 23:'sleep', }, 'artisan': { 0:'sleep', 1:'sleep', 2:'sleep', 3:'sleep', 4:'sleep', 5:'wake', 6:'rest', 7:'eat', 8:'craft', 9:'craft', 10:'craft', 11:'craft', 12:'eat', 13:'rest', 14:'craft', 15:'craft', 16:'craft', 17:'craft', 18:'rest', 19:'eat', 20:'social', 21:'social', 22:'social', 23:'sleep', }, 'leader': { 0:'sleep', 1:'sleep', 2:'sleep', 3:'sleep', 4:'sleep', 5:'wake', 6:'rest', 7:'eat', 8:'work', 9:'social', 10:'work', 11:'work', 12:'eat', 13:'rest', 14:'work', 15:'social', 16:'wander', 17:'rest', 18:'eat', 19:'social', 20:'social', 21:'social', 22:'work', 23:'sleep', }, 'wanderer': { 0:'sleep', 1:'sleep', 2:'sleep', 3:'sleep', 4:'wander', 5:'wake', 6:'wander', 7:'eat', 8:'wander', 9:'wander', 10:'explore', 11:'explore', 12:'eat', 13:'rest', 14:'wander', 15:'wander', 16:'explore', 17:'rest', 18:'eat', 19:'social', 20:'social', 21:'wander', 22:'wander', 23:'sleep', }, } ACTIVITY_EFFECTS = { 'sleep': {'energy': 40, 'hunger': -2, 'safety': 5, 'goal': 0}, 'wake': {'energy': 10, 'hunger': -3, 'safety': 2, 'goal': 0}, 'eat': {'energy': 5, 'hunger': 35, 'safety': 0, 'goal': 0}, 'work': {'energy': -15,'hunger': -8, 'safety': -2, 'goal': (1,5)}, 'rest': {'energy': 15, 'hunger': -2, 'safety': 0, 'goal': 0}, 'social': {'energy': -5, 'hunger': -4, 'safety': 5, 'goal': 0}, 'wander': {'energy': -8, 'hunger': -5, 'safety': -3, 'goal': (0,2)}, 'explore': {'energy': -12,'hunger': -7, 'safety': -5, 'goal': (1,4)}, 'study': {'energy': -10,'hunger': -6, 'safety': 0, 'goal': (1,3)}, 'craft': {'energy': -12,'hunger': -7, 'safety': 0, 'goal': (2,5)}, 'trade': {'energy': -5, 'hunger': -4, 'safety': 2, 'goal': (1,4)}, 'train': {'energy': -20,'hunger': -9, 'safety': 0, 'goal': (2,6)}, } ACTIVITY_LABELS = { 'sleep': '💤 睡眠', 'wake': '☀ 苏醒', 'eat': '🍽 用餐', 'work': '⚒ 劳作', 'rest': '🛋 休息', 'social': '💬 社交', 'wander': '🚶 游荡', 'explore': '🔍 探索', 'study': '📖 研习', 'craft': '🔨 制作', 'trade': '💰 交易', 'train': '⚔ 训练', } ARCHETYPE_LABELS = { 'villager': '居民', 'merchant': '商人', 'warrior': '战士', 'scholar': '学者', 'artisan': '工匠', 'leader': '领袖', 'wanderer': '游荡者', } MOOD_ORDER = ['rage', 'sad', 'low', 'fair', 'good', 'cheerful', 'ecstatic'] MOOD_LABELS = { 'rage': '🤬 暴怒', 'sad': '😢 悲伤', 'low': '😟 低落', 'fair': '😐 一般', 'good': '🙂 不错', 'cheerful': '😊 愉快', 'ecstatic': '🤩 极佳', } RELATION_ORDER = ['enemy', 'hostile', 'stranger', 'acquaintance', 'friend', 'close_friend', 'family'] RELATION_LABELS = { 'enemy': '死敌', 'hostile': '敌对', 'stranger': '陌生人', 'acquaintance': '熟人', 'friend': '朋友', 'close_friend': '挚友', 'family': '至亲', } # ════════════════════════════════════════════ # NPC 主模型 # ════════════════════════════════════════════ class GameNpc(models.Model): _name = 'game.npc' _description = 'NPC 角色' _rec_name = 'name' _order = 'sequence, id' # ── 基本身份 ── name = fields.Char(string='姓名', required=True, index=True) title = fields.Char(string='称号', help='如:铁匠、老村长') code = fields.Char(string='代号', help='NPC 唯一标识,用于前端引用') active = fields.Boolean(string='激活', default=True) sequence = fields.Integer(string='排序', default=10) archetype = fields.Selection([ ('villager', '居民'), ('merchant', '商人'), ('warrior', '战士'), ('scholar', '学者'), ('artisan', '工匠'), ('leader', '领袖'), ('wanderer', '游荡者'), ], string='角色类型', required=True, default='villager', help='决定日程模板和行为倾向') personality = fields.Selection([ ('gentle', '温和'), ('cheerful', '开朗'), ('serious', '严肃'), ('cautious', '谨慎'), ('bold', '大胆'), ('mysterious', '神秘'), ('grumpy', '暴躁'), ('kind', '善良'), ('crafty', '狡黠'), ('stoic', '刚毅'), ('curious', '好奇'), ('lazy', '慵懒'), ], string='性格', required=True, default='gentle') origin = fields.Char(string='出身') bio = fields.Html(string='背景故事', help='NPC 的完整背景描述') # ── 目标 ── current_goal = fields.Char(string='当前目标', help='如:收集 100 木材 / 修建防御工事 / 研究古代符文') goal_progress = fields.Integer(string='目标进度', default=0) goal_detail = fields.Text(string='目标说明') # ── 状态数值(0-100) ── energy = fields.Integer(string='精力', default=80, help='影响可执行的工作效率') hunger = fields.Integer(string='饱腹', default=70) safety = fields.Integer(string='安全感', default=60, help='受所在区域和平度、关系网保护力影响') mood = fields.Selection([ ('rage', '暴怒'), ('sad', '悲伤'), ('low', '低落'), ('fair', '一般'), ('good', '不错'), ('cheerful', '愉快'), ('ecstatic', '极佳'), ], string='心情', default='fair', compute='_compute_mood', store=True) # ── 当前活动 ── current_activity = fields.Selection([ ('sleep','睡眠'),('wake','苏醒'),('eat','用餐'),('work','劳作'), ('rest','休息'),('social','社交'),('wander','游荡'),('explore','探索'), ('study','研习'),('craft','制作'),('trade','交易'),('train','训练'), ], string='当前活动', default='wake') current_area = fields.Char(string='当前区域', help='前端位置标识:如在哪个地块/村落') # ── Tick 状态 ── last_tick = fields.Datetime(string='上次行为刻', readonly=True) tick_count = fields.Integer(string='累计刻数', default=0, readonly=True) tick_enabled = fields.Boolean(string='自动 Tick', default=True, help='开启后由 Cron 定时推进行为') # ── 关联 ── affiliation = fields.Many2one('game.rule', string='所属势力', domain=[('type', '=', 'faction')], help='关联到 game.rule 中 type=faction 的势力等级记录') relationship_ids = fields.One2many( 'game.npc.relationship', 'npc_id', string='关系网') behavior_log_ids = fields.One2many( 'game.npc.behavior.log', 'npc_id', string='行为日志') knowledge_ids = fields.One2many( 'game.npc.knowledge', 'npc_id', string='所知信息') # ── 计算/统计 ── relationship_count = fields.Integer( string='关系数', compute='_count_relations', store=True) recent_log = fields.Text( string='最近行为', compute='_compute_recent_log') # ── 虚拟字段 ── mood_label = fields.Char(string='心情标签', compute='_compute_mood_label') activity_label = fields.Char(string='活动标签', compute='_compute_activity_label') # ── 约束 ── _sql_constraints = [ ('unique_code', 'UNIQUE(code)', 'NPC 代号已存在!'), ] @api.depends('relationship_ids') def _count_relations(self): for r in self: r.relationship_count = len(r.relationship_ids) @api.depends('behavior_log_ids') def _compute_recent_log(self): for r in self: logs = r.behavior_log_ids.sorted('tick_time', reverse=True)[:3] r.recent_log = '\n'.join( f'[{l.tick_time.strftime("%m-%d %H:%M")}] {l.activity_label}' for l in logs ) if logs else '尚无行为' @api.depends('mood') def _compute_mood_label(self): for r in self: r.mood_label = MOOD_LABELS.get(r.mood, '😐 一般') @api.depends('current_activity') def _compute_activity_label(self): for r in self: r.activity_label = ACTIVITY_LABELS.get(r.current_activity, '') # ── 心情计算(基于状态合计) ── @api.depends('energy', 'hunger', 'safety', 'goal_progress') def _compute_mood(self): for r in self: score = (r.energy + r.hunger + r.safety + r.goal_progress) / 4 if score >= 85: r.mood = 'ecstatic' elif score >= 70: r.mood = 'cheerful' elif score >= 55: r.mood = 'good' elif score >= 40: r.mood = 'fair' elif score >= 25: r.mood = 'low' elif score >= 15: r.mood = 'sad' else: r.mood = 'rage' # ════════════════════════════════════════ # Tick 行为模拟 # ════════════════════════════════════════ def _current_hour(self): """基于上次 Tick 时间推断当前『游戏小时』。""" if not self.last_tick: return 6 # 默认从早晨醒来到开始 elapsed_hours = int((fields.Datetime.now() - self.last_tick).total_seconds() // 3600) # 每个刻推进 1 小时(如果长时间离线,只推进有限次,避免一次性耗尽资源) return max(0, min(23, elapsed_hours)) def _get_activity(self, hour): """根据角色类型日程获取当前活动。""" schedule = ARCHETYPE_SCHEDULES.get(self.archetype, ARCHETYPE_SCHEDULES['villager']) return schedule.get(hour, 'wake') @api.model def _clamp(self, val, lo=0, hi=100): return max(lo, min(hi, val)) def action_tick(self): """执行一个行为刻(可由按钮或 Cron 调用)。""" for npc in self: if not npc.tick_enabled or not npc.active: continue hour = npc._current_hour() activity = npc._get_activity(hour) effects = ACTIVITY_EFFECTS.get(activity, ACTIVITY_EFFECTS['rest']) rng = random.Random() # ├ 消耗基础资源(每个刻都会消耗) npc.energy = npc._clamp(npc.energy - rng.randint(2, 5)) npc.hunger = npc._clamp(npc.hunger - rng.randint(3, 6)) # ├ 应用活动效果 npc.energy = npc._clamp(npc.energy + effects.get('energy', 0)) npc.hunger = npc._clamp(npc.hunger + effects.get('hunger', 0)) npc.safety = npc._clamp(npc.safety + effects.get('safety', 0)) goal_range = effects.get('goal', 0) if isinstance(goal_range, (list, tuple)): npc.goal_progress = min(100, npc.goal_progress + rng.randint(goal_range[0], goal_range[1])) elif goal_range: npc.goal_progress = min(100, npc.goal_progress + goal_range) # ├ 如果精力 ≤ 0,转为休息 if npc.energy <= 5 and activity not in ('sleep', 'wake', 'rest', 'eat'): activity = 'rest' # 更新当前活动 npc.current_activity = activity # ├ 30% 概率触发社交 if effects.get('safety', 0) > 0 and rng.randint(1, 100) <= 30: self._socialize(npc) # └ 记录行为日志 self._log_behavior(npc, activity) npc.last_tick = fields.Datetime.now() npc.tick_count += 1 return True # ── 社交逻辑 ── def _socialize(self, npc): """与其他 NPC 随机社交互动。""" others = self.search([ ('id', '!=', npc.id), ('active', '=', True), ('tick_enabled', '=', True), ]) if not others: return target = others[random.randint(0, len(others) - 1)] # 性格兼容性计算 compat_map = { 'gentle': {'gentle':15,'cheerful':10,'cautious':10,'kind':15}, 'cheerful':{'cheerful':15,'kind':10,'bold':10,'curious':10}, 'serious': {'serious':10,'cautious':10,'stoic':10,'gentle':5}, 'cautious':{'cautious':5,'serious':5,'gentle':5,'grumpy':-10}, 'bold': {'bold':10,'cheerful':10,'curious':10,'grumpy':-5}, 'mysterious':{'mysterious':10,'curious':10,'serious':5,'lazy':5}, 'grumpy': {'grumpy':-10,'serious':-5,'stoic':-5}, 'kind': {'kind':15,'gentle':15,'cheerful':10,'cautious':10}, 'crafty': {'crafty':10,'mysterious':5,'curious':5}, 'stoic': {'stoic':10,'serious':10,'cautious':5,'mysterious':5}, 'curious': {'curious':15,'cheerful':10,'mysterious':10,'crafty':5}, 'lazy': {'lazy':5,'gentle':5}, } affinity_delta = (compat_map.get(npc.personality, {}).get(target.personality, 0) + random.randint(-5, 5)) # 查找(或创建)关系记录 rel = self.env['game.npc.relationship'].search([ ('npc_id', '=', npc.id), ('target_npc_id', '=', target.id), ], limit=1) if not rel: rel = self.env['game.npc.relationship'].create({ 'npc_id': npc.id, 'target_npc_id': target.id, 'affinity': affinity_delta, }) else: rel.affinity = max(-100, min(100, rel.affinity + affinity_delta)) rel.last_interaction = fields.Datetime.now() # 关系层级自动演进 rel._update_status() def _log_behavior(self, npc, activity): """生成行为日志。""" self.env['game.npc.behavior.log'].create({ 'npc_id': npc.id, 'activity': activity, 'location': npc.current_area, 'summary': f'{npc.name} {ACTIVITY_LABELS.get(activity, activity)}', }) # ── 批量 Tick(Cron 调用) ── @api.model def cron_batch_tick(self, batch_size=50): """定时 Cron:对所有活跃 NPC 执行一次 Tick。""" npcs = self.search([ ('active', '=', True), ('tick_enabled', '=', True), ], limit=batch_size) count = len(npcs) if count: npcs.action_tick() _logger.info(f'[NPC Tick] {count} 个角色完成一个行为刻') return count # ── Name Get ── def name_get(self): res = [] for r in self: display = r.name if r.title: display = f'{r.name} · {r.title}' res.append((r.id, display)) return res