diff --git a/data/lifepath_earth.json b/data/lifepath_earth.json new file mode 100644 index 00000000..07dfbb31 --- /dev/null +++ b/data/lifepath_earth.json @@ -0,0 +1,18 @@ +[ +{ + "name": "Earth", + "desc": "You are from Earth.", + "stage": "ORIGIN", + "choices": [ + {"prompt": "What part of Earth are you from?", "options": [ + { + "name": "The Green Zone" + }, + { + "name": "The Dead Zone" + } + ], "allow_random": true + } + ] +} +] \ No newline at end of file diff --git a/game/chargen/lifepath.py b/game/chargen/lifepath.py index 7f75315b..0b97a586 100644 --- a/game/chargen/lifepath.py +++ b/game/chargen/lifepath.py @@ -1030,6 +1030,14 @@ def __init__(self,name,desc,choices=(),next=(),next_prompt='',auto_fx=None): STARTING_CHOICES = (A_EARTH,) +STAGE_ORIGIN = "ORIGIN" +STAGE_CHILDHOOD = "CHILDHOOD" +STAGE_VOCATION = "VOCATION" +STAGE_CRISIS = "CRISIS" +STAGE_RESOLUTION = "RESOLUTION" + +STAGES = (STAGE_ORIGIN, STAGE_CHILDHOOD, STAGE_VOCATION, STAGE_CRISIS, STAGE_RESOLUTION) + class BioBlock( object ): def __init__(self,model,width=220,bio_font=None,**kwargs): self.model = model diff --git a/game/content/__init__.py b/game/content/__init__.py index eaa74aea..c9199195 100644 --- a/game/content/__init__.py +++ b/game/content/__init__.py @@ -29,6 +29,7 @@ def __init__(self, camp: gears.GearHeadCampaign, *args, **kwargs): from . import dungeonmaker from . import adventureseed +from . import missiontext diff --git a/game/content/ghplots/lancedev.py b/game/content/ghplots/lancedev.py index 6d6a6cc2..c2203113 100644 --- a/game/content/ghplots/lancedev.py +++ b/game/content/ghplots/lancedev.py @@ -85,6 +85,107 @@ def lose_mission(self, camp): # The actual plots... +class HowDoYouLikeThat(LMMissionPlot): + LABEL = "LANCEDEV" + active = True + scope = True + UNIQUE = True + + def custom_init(self, nart): + npc = self.seek_element(nart, "NPC", self._is_good_npc, scope=nart.camp.scene, lock=True) + enemy_npc = self.seek_element(nart, "ENEMY_NPC", self._is_good_enemy, scope=nart.camp, lock=True) + other_scene = self.seek_element(nart, "OTHER_SCENE", self._is_good_scene, scope=nart.camp) + self.elements["ENEMY_FACTION"] = enemy_npc.faction + self.prep_mission(nart.camp) + self.started_convo = False + return not nart.camp.are_faction_allies(enemy_npc, npc) + + def _is_good_npc(self, nart, candidate): + if self.npc_is_ready_for_lancedev(nart.camp, candidate): + return ( + candidate.relationship.attitude == gears.relationships.A_JUNIOR + and candidate.relationship.role == gears.relationships.R_COLLEAGUE + ) + + def _is_good_enemy(self, nart, candidate): + return ( + isinstance(candidate, gears.base.Character) and candidate.combatant and + nart.camp.is_not_lancemate(candidate) and + candidate.relationship and candidate.relationship.is_unfavorable() + ) + + def _is_good_scene(self, nart, candidate): + return ( + isinstance(candidate, gears.GearHeadScene) and gears.tags.SCENE_PUBLIC in candidate.attributes and + candidate.scale is gears.scale.HumanScale + ) + + def METROSCENE_ENTER(self, camp): + if not self.started_convo: + self.started_convo = True + npc = self.elements["NPC"] + pbge.alert( + "As you enter {METROSCENE}, you notice {NPC} looking at {NPC.gender.possessive_determiner} phone. After a moment {NPC.gender.subject_pronoun} turns to you.".format( + **self.elements)) + + if npc.get_reaction_score(camp.pc, camp) <= random.randint(20,35): + # NPC has decided to take a better offer. Tough luck. + npc.relationship.attitude = gears.relationships.A_RESENT + npc.relationship.history.append(gears.relationships.Memory( + "I quit your lance after getting a better offer", + "you quit my lance with absolutely no warning", + -10, memtags=(gears.relationships.MEM_Clash, gears.relationships.MEM_Ideological) + )) + ghcutscene.SimpleMonologueDisplay( + "[GOODBYE] I just got a better offer to join a different lance. I guess I'll see you around.", + npc)(camp, False) + pbge.alert("And with that, {NPC} quits the lance.".format(**self.elements)) + plotutility.AutoLeaver(npc)(camp) + npc.place(self.elements["OTHER_SCENE"], team=self.elements["OTHER_SCENE"].civilian_team) + self.proper_end_plot(camp, False) + + else: + npc.relationship.attitude = gears.relationships.A_FRIENDLY + mymenu = ghcutscene.SimpleMonologueMenu( + "I can't believe it! [I_GOT_A_MISSION_OFFER] {ENEMY_NPC} is in town, and I thought you might want to fight {ENEMY_NPC.gender.object_pronoun}.".format( + **self.elements), + npc, camp + ) + mymenu.no_escape = True + mymenu.add_item("Of course I do. We've got some scores to settle.", self._accept_offer) + mymenu.add_item("Maybe some other time... I'm not in the mood to deal with {ENEMY_NPC} today.".format(**self.elements), self._reject_offer) + choice = mymenu.query() + if choice: + choice(camp) + + def _accept_offer(self, camp): + self.mission_active = True + npc: gears.base.Character = self.elements["NPC"] + ghcutscene.SimpleMonologueDisplay( + "I'm really excited about finding this lead. I think I might be getting the hang of this cavalier thing? [LETSGO]", + npc)(camp, False) + missionbuilder.NewMissionNotification(self.mission_seed.name, self.elements["MISSION_GATE"]) + + def _reject_offer(self, camp): + npc: gears.base.Character = self.elements["NPC"] + ghcutscene.SimpleMonologueDisplay( + "[UNDERSTOOD] {ENEMY_NPC} can be a [insult]; still, I'm excited to have gotten the call! Maybe I'm getting better at this cavalier stuff.".format(**self.elements), + npc)(camp, False) + self.proper_end_plot(camp) + + def prep_mission(self, camp: gears.GearHeadCampaign): + self.mission_seed = missionbuilder.BuildAMissionSeed( + camp, "Find and defeat {ENEMY_NPC}".format(**self.elements), + self.elements["METROSCENE"], self.elements["MISSION_GATE"], + enemy_faction=self.elements.get("ENEMY_FACTION"), + allied_faction=self.elements["METROSCENE"].faction, + rank=camp.renown + 10, objectives=(missionbuilder.BAMO_DEFEAT_NPC,), + cash_reward=self.CASH_REWARD, experience_reward=self.EXPERIENCE_REWARD, + on_win=self.win_mission, on_loss=self.lose_mission, + custom_elements={missionbuilder.BAME_NPC: self.elements["ENEMY_NPC"]} + ) + + class BetterCallAPlumber(LMMissionPlot): LABEL = "LANCEDEV" active = True diff --git a/game/content/ghplots/lancemates.py b/game/content/ghplots/lancemates.py index e6b6e9da..e224b0c4 100644 --- a/game/content/ghplots/lancemates.py +++ b/game/content/ghplots/lancemates.py @@ -911,6 +911,11 @@ def _pay_to_join(self, camp): def _join_lance(self, camp): npc = self.elements["NPC"] npc.relationship.tags.add(gears.relationships.RT_LANCEMATE) + npc.relationship.history.append(gears.relationships.Memory( + "I was nearly killed by Typhon", + "you were almost killed by Typhon", + memtags=(gears.relationships.MEM_Trauma,) + )) effect = game.content.plotutility.AutoJoiner(npc) effect(camp) self.end_plot(camp) diff --git a/game/content/ghplots/mission_conversations.py b/game/content/ghplots/mission_conversations.py index c6f5b49e..5b30fb2c 100644 --- a/game/content/ghplots/mission_conversations.py +++ b/game/content/ghplots/mission_conversations.py @@ -135,6 +135,100 @@ def t_UPDATE(self, camp): self._lose_adventure(camp) +class RalphAndSam(BasicBattleConversation): + UNIQUE = True + + @classmethod + def matches(cls, pstate): + return ( + pstate.elements["NPC"].relationship and + gears.personality.Cheerful in pstate.elements["NPC"].personality and + pstate.elements["NPC"].relationship.expectation == gears.relationships.E_MERCENARY and + pstate.elements["NPC"].relationship.get_recent_memory([relationships.MEM_Clash]) and + pstate.elements["NPC"].relationship.attitude in ( + None, gears.relationships.A_JUNIOR, gears.relationships.A_OPENUP + ) and len(pstate.elements["NPC"].relationship.history) >= 3 + ) or cls.LABEL == "TEST_ENEMY_CONVO" + + def NPC_offers(self, camp): + mylist = list() + mylist.append(Offer("[HELLO] We keep running into each other at work... I guess you're playing for the other team again?", + context=ContextTag([context.ATTACK, ]), effect=self._start_conversation)) + + mylist.append(Offer( + "Remember when [MEM_Clash]? This time I'm going to [defeat_you].", + context=ContextTag([context.COMBAT_CUSTOM, ]), + data={"reply": "Afraid so. [LETSFIGHT]"}, effect=self.friendly_battle + )) + + mylist.append(Offer( + "Well that's just rude. [CHALLENGE]", + context=ContextTag([context.COMBAT_CUSTOM, ]), + data={"reply": "Do you think this is a game?! [THREATEN]"}, effect=self.unfriendly_battle, + dead_end=True + )) + + if not self.elements.get(CONVO_CANT_RETREAT, False): + game.ghdialogue.SkillBasedPartyReply( + Offer( + "[REALLY?] Honestly, I don't even care if you're telling the truth or not. I could use a little break. See you around, [audience].", + context=ContextTag([context.COMBAT_CUSTOM]), effect=self.friendly_retreat, + data={"reply": "Actually I'm here to let you know you have the day off."} + ), camp, mylist, gears.stats.Charm, gears.stats.Negotiation, self._effective_rank(), + difficulty=gears.stats.DIFFICULTY_LEGENDARY, no_random=False + ) + + if self._effective_rank() > camp.pc.renown and not self.elements.get(CONVO_CANT_WITHDRAW, False): + mylist.append(Offer( + "Do whatever makes you happy. [GOODBYE]", + context=ContextTag([context.COMBAT_CUSTOM, ]), + data={"reply": "I really don't want to deal with you today. Do you mind if I just leave?"}, + effect=self.friendly_withdraw + )) + + return mylist + + def unfriendly_battle(self, camp): + npc: gears.base.Character = self.elements["NPC"] + npc.relationship = camp.get_relationship(npc) + npc.relationship.attitude = relationships.A_RESENT + + def friendly_battle(self, camp): + npc: gears.base.Character = self.elements["NPC"] + npc.relationship = camp.get_relationship(npc) + npc.relationship.attitude = relationships.A_FRIENDLY + npc.relationship.history.append(Memory( + "we fought on opposite sides a bunch of times", + "you kept showing up during my missions", + 10, memtags=(relationships.MEM_Clash, relationships.MEM_Ideological) + )) + self.end_plot(camp, True) + + def friendly_retreat(self, camp): + npc: gears.base.Character = self.elements["NPC"] + npc.relationship = camp.get_relationship(npc) + npc.relationship.attitude = relationships.A_FRIENDLY + npc.relationship.history.append(Memory( + "you convinced me to take some time off work", + "you abandoned your mission when I joked about it being a holiday", + 10, memtags=(relationships.MEM_Clash, relationships.MEM_CallItADraw) + )) + self._enemies_retreat(camp) + self.end_plot(camp, True) + + def friendly_withdraw(self, camp): + npc: gears.base.Character = self.elements["NPC"] + npc.relationship = camp.get_relationship(npc) + npc.relationship.attitude = relationships.A_FRIENDLY + npc.relationship.history.append(Memory( + "you let me get off work early when you decided to retreat", + "I wasn't ready to fight you", + 20, memtags=(relationships.MEM_Clash, relationships.MEM_DefeatPC) + )) + self._player_retreat(camp) + self.end_plot(camp, True) + + class AegisInferiorityIntroduction(BasicBattleConversation): UNIQUE = True diff --git a/game/content/ghplots/missionbuilder.py b/game/content/ghplots/missionbuilder.py index 7e12d480..10a398e1 100644 --- a/game/content/ghplots/missionbuilder.py +++ b/game/content/ghplots/missionbuilder.py @@ -5,7 +5,7 @@ import pygame import random from game import teams, ghdialogue -from game.content import gharchitecture, ghterrain, ghwaypoints, plotutility +from game.content import gharchitecture, ghterrain, ghwaypoints, plotutility, missiontext from pbge.dialogue import Offer, ContextTag, Reply from game.ghdialogue import context from game.content.ghcutscene import SimpleMonologueDisplay @@ -14,6 +14,7 @@ from game.content.dungeonmaker import DG_NAME, DG_ARCHITECTURE, DG_SCENE_TAGS, DG_MONSTER_TAGS, DG_TEMPORARY, \ DG_PARENT_SCENE, DG_EXPLO_MUSIC, DG_COMBAT_MUSIC, DG_DECOR import copy +from game.content import missiontext # Mecha Objectives BAMO_AID_ALLIED_FORCES = "BAMO_AidAlliedForces" diff --git a/game/content/missiontext.py b/game/content/missiontext.py new file mode 100644 index 00000000..a070540c --- /dev/null +++ b/game/content/missiontext.py @@ -0,0 +1,91 @@ +import random + +import gears +from game.content.ghplots import missionbuilder + +MOTIVE_ATTACK = "ATTACK" +MOTIVE_DEFEND = "DEFEND" +MOTIVE_GREED = "GREED" +MOTIVE_REVENGE = "REVENGE" +MOTIVE_RECON = "RECON" + +MEANS_OFFENSE = "OFFENSE" +MEANS_STEAL = "STEAL" +MEANS_INFILTRATE = "INFILTRATE" +MEANS_FORTIFY = "FORTIFY" + + +class MissionText: + + def __init__(self, camp: gears.GearHeadCampaign, objectives, metroscene, allied_faction=None, enemy_faction=None): + # Give us some text that can be used to generate a MissionGrammar and also some text that can be used by the + # mission-giver to describe the mission. + motive_candidates = list() + means_candidates = list() + if { + missionbuilder.BAMO_AID_ALLIED_FORCES, missionbuilder.BAMO_DEFEAT_ARMY, + missionbuilder.BAMO_DEFEAT_COMMANDER, missionbuilder.BAMO_DESTROY_ARTILLERY, + missionbuilder.BAMO_LOCATE_ENEMY_FORCES, missionbuilder.BAMO_RESPOND_TO_DISTRESS_CALL, + }.intersection(objectives): + means_candidates.append(MEANS_OFFENSE) + if enemy_faction and camp.are_faction_allies(metroscene, enemy_faction): + motive_candidates.append(MOTIVE_DEFEND) + elif enemy_faction and gears.tags.Criminal in enemy_faction.factags: + motive_candidates.append(MOTIVE_GREED) + + if { + missionbuilder.BAMO_DEFEAT_THE_BANDITS, missionbuilder.BAMO_RECOVER_CARGO, + missionbuilder.BAMO_CAPTURE_THE_MINE, missionbuilder.BAMO_PROTECT_BUILDINGS, + missionbuilder.BAMO_RESPOND_TO_DISTRESS_CALL, missionbuilder.BAMO_SURVIVE_THE_AMBUSH, + }.intersection(objectives): + means_candidates.append(MEANS_STEAL) + motive_candidates.append(MOTIVE_GREED) + if enemy_faction and {gears.tags.Military, gears.tags.CorporateWorker}.intersection(enemy_faction.factags): + motive_candidates.append(MOTIVE_RECON) + if enemy_faction and {gears.tags.Criminal, gears.tags.CorporateWorker}.intersection(enemy_faction.factags): + means_candidates.append(MEANS_STEAL) + + if { + missionbuilder.BAMO_EXTRACT_ALLIED_FORCES, missionbuilder.BAMO_AID_ALLIED_FORCES, + missionbuilder.BAMO_SURVIVE_THE_AMBUSH, missionbuilder.BAMO_LOCATE_ENEMY_FORCES, + missionbuilder.BAMO_DEFEAT_NPC, missionbuilder.BAMO_RESCUE_NPC, missionbuilder.BAMO_SURVIVE_THE_AMBUSH, + }.intersection(objectives): + means_candidates.append(MEANS_INFILTRATE) + motive_candidates.append(MOTIVE_RECON) + if enemy_faction and gears.tags.Criminal in enemy_faction.factags: + motive_candidates.append(MOTIVE_GREED) + + if { + missionbuilder.BAMO_CAPTURE_BUILDINGS, missionbuilder.BAMO_CAPTURE_THE_MINE, + missionbuilder.BAMO_DESTROY_ARTILLERY, missionbuilder.BAMO_STORM_THE_CASTLE, + missionbuilder.BAMO_DEFEAT_ARMY, missionbuilder.BAMO_NEUTRALIZE_ALL_DRONES, + }.intersection(objectives): + means_candidates.append(MEANS_FORTIFY) + if enemy_faction and camp.are_faction_allies(metroscene, enemy_faction): + motive_candidates.append(MOTIVE_DEFEND) + else: + motive_candidates.append(MOTIVE_ATTACK) + if enemy_faction and {gears.tags.Politician, gears.tags.CorporateWorker}.intersection(enemy_faction.factags): + motive_candidates.append(MOTIVE_DEFEND) + + if camp.are_faction_enemies(metroscene, enemy_faction): + motive_candidates.append(MOTIVE_ATTACK) + + if enemy_faction and gears.tags.Military in enemy_faction.factags and not camp.are_faction_allies(enemy_faction, metroscene): + motive_candidates.append(MOTIVE_ATTACK) + + if camp.are_faction_enemies(allied_faction, enemy_faction): + motive_candidates.append(MOTIVE_REVENGE) + + if enemy_faction and gears.tags.Criminal in enemy_faction.factags: + motive_candidates.append(MOTIVE_GREED) + + if not motive_candidates: + motive_candidates = [MOTIVE_ATTACK, MOTIVE_GREED, MOTIVE_RECON] + + if not means_candidates: + means_candidates = [MEANS_OFFENSE, MEANS_INFILTRATE] + + motive = random.choice(motive_candidates) + means = random.choice(means_candidates) + diff --git a/game/exploration.py b/game/exploration.py index 214d222e..26b1a6a5 100644 --- a/game/exploration.py +++ b/game/exploration.py @@ -704,6 +704,17 @@ def go(self): if hasattr(pc, "relationship") and pc.relationship and hasattr(pc, "renown"): print("{} {} {} OK:{}".format(pc, pc.renown, pc.relationship.hilights(), pc.relationship.can_do_development())) + elif gdi.unicode == "N" and pbge.util.config.getboolean("GENERAL", "dev_mode_on"): + print("Checking Enemies") + enemies = [candidate for candidate in self.camp.all_contents(self.camp) if ( + isinstance(candidate, gears.base.Character) and candidate.combatant and + candidate.relationship and candidate.relationship.is_unfavorable() + )] + for pc in enemies: + if hasattr(pc, "relationship") and pc.relationship and hasattr(pc, "renown"): + print("{} ({}): {}\n --{}\n --Renown {}, {}\n --Memories: {}".format(pc, pc.faction, pc.get_text_desc(self.camp), pc.get_tags(False), pc.renown, pc.relationship.hilights(), len(pc.relationship.history))) + for mem in pc.relationship.history: + print(mem) elif gdi.unicode == "V" and pbge.util.config.getboolean("GENERAL", "dev_mode_on"): for pc in list(self.camp.party): if pc in self.scene.contents and isinstance(pc, diff --git a/game/ghdialogue/ghgrammar.py b/game/ghdialogue/ghgrammar.py index e19a52a3..72683706 100644 --- a/game/ghdialogue/ghgrammar.py +++ b/game/ghdialogue/ghgrammar.py @@ -3257,6 +3257,46 @@ ] }, + "[insult]": { + # Noun phrase describing a character unfavorably. + Default: [ + "jerk", "arsehole", + ], + personality.Cheerful: [ + "killjoy", "downer", "asshat" + ], + personality.Grim: [ + "pain in the arse", "waste of carbon", "ash-gibbon" + ], + personality.Easygoing: [ + "bad person", "doody head", "numbskull" + ], + personality.Passionate: [ + "sniveling worm", "abomination", "coward", "jackass" + ], + personality.Sociable: [ + "nobody", "brat", "piece of trash", "undesirable" + ], + personality.Shy: [ + "loudmouth", "git" + ], + personality.Justice: [ + "scoundrel", "scumbag" + ], + personality.Peace: [ + "meanie", "brute", + ], + personality.Glory: [ + "lowlife", "gorf herder", "wannabe" + ], + personality.Fellowship: [ + "creep", "slimeball" + ], + personality.Duty: [ + "weasel", "delinquent" + ] + }, + "[INTERESTING_NEWS]": { # Character has something interesting to reveal. Default: ["Very interesting.", diff --git a/gears/base.py b/gears/base.py index 18ca5240..b1a7772c 100644 --- a/gears/base.py +++ b/gears/base.py @@ -4509,11 +4509,18 @@ def get_reaction_score(self, pc, camp): rs -= 15 fac = self.get_tacit_faction(camp) if fac: + fac_score = 0 if pc is camp.pc: - rs += camp.get_faction_reaction_modifier(fac) + fac_score += camp.get_faction_reaction_modifier(fac) pc_fac = pc.get_tacit_faction(camp) if pc_fac and camp.are_faction_enemies(fac, pc_fac): - rs -= 20 + fac_score -= 20 + if self.relationship: + if self.relationship.is_favorable(): + fac_score = max(fac_score, 0) + elif self.relationship.is_unfavorable(): + fac_score = min(fac_score, 0) + rs += fac_score # Add bonuses from PC's merit badges for badge in pc.badges: diff --git a/gears/relationships.py b/gears/relationships.py index 6ce6aa4c..1542b232 100644 --- a/gears/relationships.py +++ b/gears/relationships.py @@ -80,9 +80,10 @@ MEM_Romantic = "MEM_Romantic" MEM_Ideological = "MEM_Ideological" # An ideological event MEM_Debt = "MEM_Debt" # The NPC owes a debt to the PC, formally or informally +MEM_Trauma = "MEM_Trauma" # The NPC has a trauma, which may or may not be related to the PC. -MEMORY_TYPES = (MEM_DefeatPC,MEM_LoseToPC,MEM_CallItADraw,MEM_Clash,MEM_AidedByPC, MEM_Romantic, MEM_Ideological) +MEMORY_TYPES = (MEM_DefeatPC,MEM_LoseToPC,MEM_CallItADraw,MEM_Clash,MEM_AidedByPC, MEM_Romantic, MEM_Ideological, MEM_Trauma) class Memory(object): def __init__(self, npc_perspective, pc_perspective, reaction_mod=0, memtags=(), ): @@ -93,6 +94,9 @@ def __init__(self, npc_perspective, pc_perspective, reaction_mod=0, memtags=(), self.reaction_mod = reaction_mod self.memtags = set(memtags) + def __str__(self): + return self.npc_perspective + class Relationship(object): # Contains info about the relationship between this NPC and the player character. diff --git a/history.md b/history.md index 3787232a..7c80c40e 100644 --- a/history.md +++ b/history.md @@ -1,3 +1,5 @@ +* Dialogue replies should more closely match context + v0.970 September 3, 2024 * Experience display in FieldHQ updated after training * Increased Overwhelmed modifier from 3 to 4 diff --git a/image/eyecatch_corsairdielancer.png b/image/eyecatch_corsairdielancer.png new file mode 100644 index 00000000..e667eea5 Binary files /dev/null and b/image/eyecatch_corsairdielancer.png differ diff --git a/pbge/__init__.py b/pbge/__init__.py index ac9deb03..24543d5b 100644 --- a/pbge/__init__.py +++ b/pbge/__init__.py @@ -399,6 +399,11 @@ def update_mouse_pos(self): self.mouse_pos = pygame.mouse.get_pos() +class SHLayer(): + def __init__(self): + w, h = my_state.physical_screen.get_size() + self.surf = pygame.Surface((max(800, 600 * w // h), 600)) + INPUT_CURSOR = None SMALLFONT = None TINYFONT = None diff --git a/pbge/dialogue/__init__.py b/pbge/dialogue/__init__.py index a7f53a4a..97b71f43 100644 --- a/pbge/dialogue/__init__.py +++ b/pbge/dialogue/__init__.py @@ -255,12 +255,27 @@ def _get_offer_for_cue(self, cue, candidates, allow_standards=True): return self._find_std_offer_to_match_cue(cue) def _get_reply_for_offers(self, off1: Offer, off2: Offer): - if not ((off1.no_repeats and off1.subject == off2.subject and off1.context == off2.context) or ( - off2.is_generic and not off1.allow_generics)): - candidates = [r for r in STANDARD_REPLIES if - r.context.matches(off1.context) and r.destination.context.matches(off2.context) - and (off1.subject == off2.subject or off2.subject is None or str( - off2.subject) in off1.msg or off2.subject_start)] + # First, make sure this offer even can be connected to the other offer. + if off1.no_repeats and off1.subject == off2.subject and off1.context == off2.context: + # Off2 repeats the subject and context of Off1 when we've been told to avoid repeats. + return None + + elif off2.is_generic and not off1.allow_generics: + # Off2 is a generic reply and we've been asked to avoid generics. + return None + + elif not (off1.subject == off2.subject or off2.subject is None or str(off2.subject) in off1.msg or off2.subject_start): + # There is a subject mismatch between the two offers. Don't connect them. + return None + + else: + # First, check for replies that match off1's context perfectly. + candidates = [ + r for r in STANDARD_REPLIES if off1.context.matches(r.context) and r.destination.context.matches(off2.context) + ] + if not candidates: + candidates = [r for r in STANDARD_REPLIES if + r.context.matches(off1.context) and r.destination.context.matches(off2.context)] if candidates: return copy.deepcopy(random.choice(candidates))