packages feed

Allure-0.11.0.0: GameDefinition/Content/FactionKind.hs

-- Copyright (c) 2008--2011 Andres Loeh
-- Copyright (c) 2010--2021 Mikolaj Konarski and others (see git history)
-- This file is a part of the computer game Allure of the Stars
-- and is released under the terms of the GNU Affero General Public License.
-- For license and copyright information, see the file LICENSE.
--
-- | Definitions of kinds of factions present in a game, both human
-- and computer-controlled.
module Content.FactionKind
  ( -- * Group name patterns
    pattern EXPLORER_REPRESENTATIVE, pattern EXPLORER_SHORT, pattern EXPLORER_NO_ESCAPE, pattern EXPLORER_MEDIUM, pattern EXPLORER_TRAPPED, pattern EXPLORER_AUTOMATED, pattern EXPLORER_AUTOMATED_TRAPPED, pattern EXPLORER_CAPTIVE, pattern EXPLORER_PACIFIST, pattern COMPETITOR_REPRESENTATIVE, pattern COMPETITOR_SHORT, pattern COMPETITOR_NO_ESCAPE, pattern CIVILIAN_REPRESENTATIVE, pattern CONVICT_REPRESENTATIVE, pattern MONSTER_REPRESENTATIVE, pattern MONSTER_ANTI, pattern MONSTER_ANTI_CAPTIVE, pattern MONSTER_ANTI_PACIFIST, pattern MONSTER_TOURIST, pattern MONSTER_TOURIST_PASSIVE, pattern MONSTER_CAPTIVE, pattern MONSTER_CAPTIVE_NARRATING, pattern ANIMAL_REPRESENTATIVE, pattern ANIMAL_MAGNIFICENT, pattern ANIMAL_EXQUISITE, pattern ANIMAL_CAPTIVE, pattern ANIMAL_NARRATING, pattern ANIMAL_MAGNIFICENT_NARRATING, pattern ANIMAL_CAPTIVE_NARRATING, pattern HORROR_REPRESENTATIVE, pattern HORROR_CAPTIVE, pattern HORROR_PACIFIST
  , pattern EXPLORER_EXTERMINATOR, pattern ROBOT_REPRESENTATIVE, pattern ROBOT_CAPTIVE, pattern ROBOT_NARRATING, pattern ROBOT_VIRUS, pattern ROBOT_GAUNTLET, pattern OFF_WORLD_REPRESENTATIVE
  , pattern REPRESENTATIVE
  , pattern ANIMAL_OR_ROBOT_NARRATING
  , groupNamesSingleton, groupNames
  , -- * Content
    content
#ifdef EXPOSE_INTERNAL
  , hiHeroExterminator
#endif
  ) where

import Prelude ()

import Game.LambdaHack.Core.Prelude

import           Game.LambdaHack.Content.FactionKind
import qualified Game.LambdaHack.Content.ItemKind as IK
import           Game.LambdaHack.Definition.Ability
import           Game.LambdaHack.Definition.Defs
import           Game.LambdaHack.Definition.DefsInternal

import Content.ItemKindActor
import Content.ItemKindOrgan

-- * Group name patterns

groupNamesSingleton :: [GroupName FactionKind]
groupNamesSingleton =
       [EXPLORER_REPRESENTATIVE, EXPLORER_SHORT, EXPLORER_NO_ESCAPE, EXPLORER_MEDIUM, EXPLORER_TRAPPED, EXPLORER_AUTOMATED, EXPLORER_AUTOMATED_TRAPPED, EXPLORER_CAPTIVE, EXPLORER_PACIFIST, COMPETITOR_REPRESENTATIVE, COMPETITOR_SHORT, COMPETITOR_NO_ESCAPE, CIVILIAN_REPRESENTATIVE, CONVICT_REPRESENTATIVE, MONSTER_REPRESENTATIVE, MONSTER_ANTI, MONSTER_ANTI_CAPTIVE, MONSTER_ANTI_PACIFIST, MONSTER_TOURIST, MONSTER_TOURIST_PASSIVE, MONSTER_CAPTIVE, MONSTER_CAPTIVE_NARRATING, ANIMAL_REPRESENTATIVE, ANIMAL_MAGNIFICENT, ANIMAL_EXQUISITE, ANIMAL_CAPTIVE, ANIMAL_NARRATING, ANIMAL_MAGNIFICENT_NARRATING, ANIMAL_CAPTIVE_NARRATING, HORROR_REPRESENTATIVE, HORROR_CAPTIVE, HORROR_PACIFIST]
       ++ [EXPLORER_EXTERMINATOR, ROBOT_REPRESENTATIVE, ROBOT_CAPTIVE, ROBOT_NARRATING, ROBOT_VIRUS, ROBOT_GAUNTLET, OFF_WORLD_REPRESENTATIVE]

pattern EXPLORER_REPRESENTATIVE, EXPLORER_SHORT, EXPLORER_NO_ESCAPE, EXPLORER_MEDIUM, EXPLORER_TRAPPED, EXPLORER_AUTOMATED, EXPLORER_AUTOMATED_TRAPPED, EXPLORER_CAPTIVE, EXPLORER_PACIFIST, COMPETITOR_REPRESENTATIVE, COMPETITOR_SHORT, COMPETITOR_NO_ESCAPE, CIVILIAN_REPRESENTATIVE, CONVICT_REPRESENTATIVE, MONSTER_REPRESENTATIVE, MONSTER_ANTI, MONSTER_ANTI_CAPTIVE, MONSTER_ANTI_PACIFIST, MONSTER_TOURIST, MONSTER_TOURIST_PASSIVE, MONSTER_CAPTIVE, MONSTER_CAPTIVE_NARRATING, ANIMAL_REPRESENTATIVE, ANIMAL_MAGNIFICENT, ANIMAL_EXQUISITE, ANIMAL_CAPTIVE, ANIMAL_NARRATING, ANIMAL_MAGNIFICENT_NARRATING, ANIMAL_CAPTIVE_NARRATING, HORROR_REPRESENTATIVE, HORROR_CAPTIVE, HORROR_PACIFIST :: GroupName FactionKind

pattern EXPLORER_EXTERMINATOR, ROBOT_REPRESENTATIVE, ROBOT_CAPTIVE, ROBOT_NARRATING, ROBOT_VIRUS, ROBOT_GAUNTLET, OFF_WORLD_REPRESENTATIVE :: GroupName FactionKind

groupNames :: [GroupName FactionKind]
groupNames = [REPRESENTATIVE, ANIMAL_OR_ROBOT_NARRATING]

-- This pattern was supposed to be used in faction lore to distinguish
-- the faction kinds to show. Instead all factions from the current game
-- are shown. Perhaps in the future we'll also show encountered actions
-- from previous games and then this will be useful to limit the number.
pattern REPRESENTATIVE :: GroupName FactionKind

pattern ANIMAL_OR_ROBOT_NARRATING :: GroupName FactionKind

pattern REPRESENTATIVE = GroupName "representative"
pattern EXPLORER_REPRESENTATIVE = GroupName "explorer"
pattern EXPLORER_SHORT = GroupName "explorer short"
pattern EXPLORER_NO_ESCAPE = GroupName "explorer no escape"
pattern EXPLORER_MEDIUM = GroupName "explorer medium"
pattern EXPLORER_TRAPPED = GroupName "explorer trapped"
pattern EXPLORER_AUTOMATED = GroupName "explorer automated"
pattern EXPLORER_AUTOMATED_TRAPPED = GroupName "explorer automated trapped"
pattern EXPLORER_CAPTIVE = GroupName "explorer captive"
pattern EXPLORER_PACIFIST = GroupName "explorer pacifist"
pattern COMPETITOR_REPRESENTATIVE = GroupName "competitor"
pattern COMPETITOR_SHORT = GroupName "competitor short"
pattern COMPETITOR_NO_ESCAPE = GroupName "competitor no escape"
pattern CIVILIAN_REPRESENTATIVE = GroupName "civilian"
pattern CONVICT_REPRESENTATIVE = GroupName "convict"
pattern MONSTER_REPRESENTATIVE = GroupName "monster"
pattern MONSTER_ANTI = GroupName "monster anti"
pattern MONSTER_ANTI_CAPTIVE = GroupName "monster anti captive"
pattern MONSTER_ANTI_PACIFIST = GroupName "monster anti pacifist"
pattern MONSTER_TOURIST = GroupName "monster tourist"
pattern MONSTER_TOURIST_PASSIVE = GroupName "monster tourist passive"
pattern MONSTER_CAPTIVE = GroupName "monster captive"
pattern MONSTER_CAPTIVE_NARRATING = GroupName "monster captive narrating"
pattern ANIMAL_REPRESENTATIVE = GroupName "animal"
pattern ANIMAL_MAGNIFICENT = GroupName "animal magnificent"
pattern ANIMAL_EXQUISITE = GroupName "animal exquisite"
pattern ANIMAL_CAPTIVE = GroupName "animal captive"
pattern ANIMAL_NARRATING = GroupName "animal narrating"
pattern ANIMAL_MAGNIFICENT_NARRATING = GroupName "animal magnificent narrating"
pattern ANIMAL_CAPTIVE_NARRATING = GroupName "animal captive narrating"
pattern HORROR_REPRESENTATIVE = GroupName "horror"
pattern HORROR_CAPTIVE = GroupName "horror captive"
pattern HORROR_PACIFIST = GroupName "horror pacifist"

-- ** Allure-specific

pattern ANIMAL_OR_ROBOT_NARRATING = GroupName "animal or robot narrating"

pattern EXPLORER_EXTERMINATOR = GroupName "explorer exterminator"
pattern ROBOT_REPRESENTATIVE = GroupName "robot"
pattern ROBOT_CAPTIVE = GroupName "robot captive"
pattern ROBOT_NARRATING = GroupName "robot narrating"
pattern ROBOT_VIRUS = GroupName "robot replicant"
pattern ROBOT_GAUNTLET = GroupName "robot virus-infested"
pattern OFF_WORLD_REPRESENTATIVE = GroupName "off-world mercenary"

-- * Teams

teamCompetitor, teamCivilian, teamConvict, teamMonster, teamAnimal, teamHorror, teamRobot, teamOffWorld, teamOther :: TeamContinuity
teamCompetitor = TeamContinuity 2
teamCivilian = TeamContinuity 3
teamConvict = TeamContinuity 4
teamMonster = TeamContinuity 5
teamAnimal = TeamContinuity 6
teamHorror = TeamContinuity 7
teamRobot = TeamContinuity 8
teamOffWorld = TeamContinuity 9
teamOther = TeamContinuity 10

-- * Content

content :: [FactionKind]
content = [factExplorer, factExplorerShort, factExplorerNoEscape, factExplorerMedium, factExplorerTrapped, factExplorerAutomated, factExplorerAutomatedTrapped, factExplorerCaptive, factExplorerPacifist, factCompetitor, factCompetitorShort, factCompetitorNoEscape, factCivilian, factConvict, factMonster, factMonsterAnti, factMonsterAntiCaptive, factMonsterAntiPacifist, factMonsterTourist, factMonsterTouristPassive, factMonsterCaptive, factMonsterCaptiveNarrating, factAnimal, factAnimalMagnificent, factAnimalExquisite, factAnimalCaptive, factAnimalNarrating, factAnimalMagnificentNarrating, factAnimalCaptiveNarrating, factHorror, factHorrorCaptive, factHorrorPacifist]
  ++ [factExplorerExterminator, factRobot, factRobotCaptive, factRobotNarrating, factRobotVirus, factRobotGauntlet, factOffWorld]

factExplorer,            factExplorerShort, factExplorerNoEscape, factExplorerMedium, factExplorerTrapped, factExplorerAutomated, factExplorerAutomatedTrapped, factExplorerCaptive, factExplorerPacifist, factCompetitor, factCompetitorShort, factCompetitorNoEscape, factCivilian, factConvict, factMonster, factMonsterAnti, factMonsterAntiCaptive, factMonsterAntiPacifist, factMonsterTourist, factMonsterTouristPassive, factMonsterCaptive, factMonsterCaptiveNarrating, factAnimal, factAnimalMagnificent, factAnimalExquisite, factAnimalCaptive, factAnimalNarrating, factAnimalMagnificentNarrating, factAnimalCaptiveNarrating, factHorror, factHorrorCaptive, factHorrorPacifist :: FactionKind

factExplorerExterminator, factRobot, factRobotCaptive, factRobotNarrating, factRobotVirus, factRobotGauntlet, factOffWorld :: FactionKind

-- * Content

-- ** teamExplorer

factExplorer = FactionKind
  { fname = "Spacefarer"
  , ffreq = [(EXPLORER_REPRESENTATIVE, 1), (REPRESENTATIVE, 1)]
  , fteam = teamExplorer
  , fgroups = [(HERO, 100)]  -- don't spam the escapists, etc., in description
  , fskillsOther = meleeAdjacent
  , fcanEscape = True
  , fneverEmpty = True
  , fhiCondPoly = hiHeroLong
  , fhasGender = True
  , finitDoctrine = TExplore
  , fspawnsFast = False
  , fhasPointman = True
  , fhasUI = True
  , finitUnderAI = False
  , fenemyTeams =
      [teamCompetitor, teamMonster, teamAnimal, teamRobot, teamHorror]
  , falliedTeams = []
  }
factExplorerShort = factExplorer
  { ffreq = [(EXPLORER_SHORT, 1)]
  , fhiCondPoly = hiHeroShort
  }
factExplorerNoEscape = factExplorer
  { ffreq = [(EXPLORER_NO_ESCAPE, 1)]
  , fcanEscape = False
  , fhiCondPoly = hiHeroMedium
  }
factExplorerMedium = factExplorer
  { ffreq = [(EXPLORER_MEDIUM, 1)]
  , fhiCondPoly = hiHeroMedium
  }
factExplorerTrapped = factExplorer
  { ffreq = [(EXPLORER_TRAPPED, 1)]
  , fcanEscape = False
  , fhiCondPoly = hiHeroLong
  }
factExplorerAutomated = factExplorer
  { ffreq = [(EXPLORER_AUTOMATED, 1)]
  , fhasUI = False
  , finitUnderAI = True
  }
factExplorerAutomatedTrapped = factExplorerAutomated
  { ffreq = [(EXPLORER_AUTOMATED_TRAPPED, 1)]
  , fcanEscape = False
  , fhiCondPoly = hiHeroLong
  }
factExplorerCaptive = factExplorer
  { ffreq = [(EXPLORER_CAPTIVE, 1)]
  , fneverEmpty = True  -- already there
  }
factExplorerPacifist = factExplorerCaptive
  { ffreq = [(EXPLORER_PACIFIST, 1)]
  , fenemyTeams = []
  , falliedTeams = []
  }

-- ** teamCompetitor, symmetric opponents of teamExplorer

factCompetitor = factExplorer
  { fname = "Red Collar Bro"
  , ffreq = [(COMPETITOR_REPRESENTATIVE, 1), (REPRESENTATIVE, 1)]
  , fteam = teamCompetitor
  , fhasUI = False
  , finitUnderAI = True
  , fenemyTeams = [teamExplorer, teamMonster, teamAnimal, teamRobot, teamHorror]
  , falliedTeams = []
  }
factCompetitorShort = factCompetitor
  { ffreq = [(COMPETITOR_SHORT, 1)]
  , fhiCondPoly = hiHeroShort
  }
factCompetitorNoEscape = factCompetitor
  { ffreq = [(COMPETITOR_NO_ESCAPE, 1)]
  , fcanEscape = False
  , fhiCondPoly = hiHeroMedium
  }

-- ** teamCivilian

factCivilian = FactionKind
  { fname = "Civilian"
  , ffreq = [(CIVILIAN_REPRESENTATIVE, 1), (REPRESENTATIVE, 1)]
  , fteam = teamCivilian
  , fgroups = [(HERO, 100), (CIVILIAN, 100)]  -- symmetric vs player
  , fskillsOther = zeroSkills  -- not coordinated by any leadership
  , fcanEscape = False
  , fneverEmpty = True
  , fhiCondPoly = hiHeroMedium
  , fhasGender = True
  , finitDoctrine = TPatrol
  , fspawnsFast = False
  , fhasPointman = False  -- unorganized
  , fhasUI = False
  , finitUnderAI = True
  , fenemyTeams = [teamMonster, teamAnimal, teamRobot, teamHorror]
  , falliedTeams = []
  }

-- ** teamConvict, different demographics

factConvict = factCivilian
  { fname = "Hunam Convict"
  , ffreq = [(CONVICT_REPRESENTATIVE, 1), (REPRESENTATIVE, 1)]
  , fteam = teamConvict
  , fhasPointman = True  -- convicts organize better
  , finitUnderAI = True
  , fenemyTeams = [teamMonster, teamAnimal, teamRobot, teamHorror]
  , falliedTeams = []
  }

-- ** teamMonster

factMonster = FactionKind
  { fname = "Alien Hierarchy"
  , ffreq = [(MONSTER_REPRESENTATIVE, 1), (REPRESENTATIVE, 1)]
  , fteam = teamMonster
  , fgroups = [ (MONSTER, 100)
              , (MOBILE_MONSTER, 1), (AQUATIC_MONSTER, 1) ]
  , fskillsOther = zeroSkills
  , fcanEscape = False
  , fneverEmpty = False
  , fhiCondPoly = hiDweller
  , fhasGender = False
  , finitDoctrine = TExplore
  , fspawnsFast = True
  , fhasPointman = True
  , fhasUI = False
  , finitUnderAI = True
  , fenemyTeams = [teamExplorer, teamCompetitor, teamCivilian, teamConvict]
  , falliedTeams = [teamAnimal, teamRobot]
  }
-- This has continuity @teamMonster@, despite being playable.
factMonsterAnti = factMonster
  { ffreq = [(MONSTER_ANTI, 1)]
  , fhasUI = True
  , finitUnderAI = False
  }
factMonsterAntiCaptive = factMonsterAnti
  { ffreq = [(MONSTER_ANTI_CAPTIVE, 1)]
  , fneverEmpty = True
  }
factMonsterAntiPacifist = factMonsterAntiCaptive
  { ffreq = [(MONSTER_ANTI_PACIFIST, 1)]
  , fenemyTeams = []
  , falliedTeams = []
  }
-- More flavour and special backstory, but the same team.
factMonsterTourist = factMonsterAnti
  { fname = "Alien Tourist Office"
  , ffreq = [(MONSTER_TOURIST, 1)]
  , fcanEscape = True
  , fneverEmpty = True  -- no spawning
  , fhiCondPoly = hiHeroMedium
  , finitDoctrine = TFollow  -- follow-the-guide, as tourists do
  , fspawnsFast = False  -- on a trip, so no spawning
  , finitUnderAI = False
  , fenemyTeams =
      [teamAnimal, teamExplorer, teamCompetitor, teamCivilian, teamConvict]
  , falliedTeams = [teamRobot]
  }
factMonsterTouristPassive = factMonsterTourist
  { ffreq = [(MONSTER_TOURIST_PASSIVE, 1)]
  , fhasUI = False
  , finitUnderAI = True
  }
factMonsterCaptive = factMonster
  { ffreq = [(MONSTER_CAPTIVE, 1)]
  , fneverEmpty = True
  }
factMonsterCaptiveNarrating = factMonsterAntiCaptive
  { ffreq = [(MONSTER_CAPTIVE_NARRATING, 1)]
  , fhasUI = True
  }

-- ** teamAnimal

factAnimal = FactionKind
  { fname = "Animal Kingdom"
  , ffreq = [(ANIMAL_REPRESENTATIVE, 1), (REPRESENTATIVE, 1)]
  , fteam = teamAnimal
  , fgroups = [ (ANIMAL, 100), (INSECT, 100), (AQUATIC_ANIMAL, 100)
                   -- only the distinct enough ones
              , (MOBILE_ANIMAL, 1), (IMMOBILE_ANIMAL, 1), (SCAVENGER, 1) ]
  , fskillsOther = zeroSkills
  , fcanEscape = False
  , fneverEmpty = False
  , fhiCondPoly = hiDweller
  , fhasGender = False
  , finitDoctrine = TRoam  -- can't pick up, so no point exploring
  , fspawnsFast = True
  , fhasPointman = False
  , fhasUI = False
  , finitUnderAI = True
  , fenemyTeams = [teamExplorer, teamCompetitor, teamCivilian, teamConvict]
  , falliedTeams = [teamMonster, teamRobot]
  }
-- These two differ from outside, but share information and boasting
-- about them tends to be general, too.
factAnimalMagnificent = factAnimal
  { fname = "Animal Magnificent Specimen Variety"
  , ffreq = [(ANIMAL_MAGNIFICENT, 1)]
  , fneverEmpty = True
  , fenemyTeams =
      [teamMonster, teamExplorer, teamCompetitor, teamCivilian, teamConvict]
  , falliedTeams = []
  }
factAnimalExquisite = factAnimal
  { fname = "Animal Exquisite Herds and Packs Galore"
  , ffreq = [(ANIMAL_EXQUISITE, 1)]
  , fteam = teamOther
      -- in the same mode as @factAnimalMagnificent@, so borrow
      -- identity from horrors to avoid a clash
  , fneverEmpty = True
  , fenemyTeams =
      [teamMonster, teamExplorer, teamCompetitor, teamCivilian, teamConvict]
  , falliedTeams = []
  }
factAnimalCaptive = factAnimal
  { ffreq = [(ANIMAL_CAPTIVE, 1)]
  , fneverEmpty = True
  }
factAnimalNarrating = factAnimal
  { ffreq = [(ANIMAL_NARRATING, 1), (ANIMAL_OR_ROBOT_NARRATING, 70)]
  , fhasUI = True
  }
factAnimalMagnificentNarrating = factAnimalMagnificent
  { ffreq = [(ANIMAL_MAGNIFICENT_NARRATING, 1)]
  , fhasPointman = True
  , fhasUI = True
  , finitUnderAI = False
  }
factAnimalCaptiveNarrating = factAnimalCaptive
  { ffreq = [(ANIMAL_CAPTIVE_NARRATING, 1)]
  , fhasUI = True
  }

-- ** teamHorror, not much of a continuity intended, but can't be ignored

-- | A special faction, for summoned actors that don't belong to any
-- of the main factions of a given game. E.g., animals summoned during
-- a brawl game between two hero factions land in the horror faction.
-- In every game, either all factions for which summoning items exist
-- should be present or a horror faction should be added to host them.
factHorror = FactionKind
  { fname = "Horror Den"
  , ffreq = [(HORROR_REPRESENTATIVE, 1), (REPRESENTATIVE, 1)]
  , fteam = teamHorror
  , fgroups = [(IK.HORROR, 100)]
  , fskillsOther = zeroSkills
  , fcanEscape = False
  , fneverEmpty = False
  , fhiCondPoly = []
  , fhasGender = False
  , finitDoctrine = TPatrol  -- disoriented
  , fspawnsFast = False
  , fhasPointman = False
  , fhasUI = False
  , finitUnderAI = True
  , fenemyTeams = [teamExplorer, teamCompetitor, teamCivilian, teamConvict]
  , falliedTeams = []
  }
factHorrorCaptive = factHorror
  { ffreq = [(HORROR_CAPTIVE, 1)]
  , fneverEmpty = True
  }
factHorrorPacifist = factHorrorCaptive
  { ffreq = [(HORROR_PACIFIST, 1)]
  , fenemyTeams = []
  , falliedTeams = []
  }

-- * Allure-specific

factExplorerExterminator = factExplorer
  { ffreq = [(EXPLORER_EXTERMINATOR, 1)]
  , fhiCondPoly = hiHeroExterminator
  }

hiHeroExterminator :: HiCondPoly
hiHeroExterminator =
  hiHeroShort
  ++ [([(HiConst, 200)], [Conquer])]  -- bonus for extermination over escape

-- ** teamRobot

factRobot = FactionKind
  { fname = "Robot Anarchy"
  , ffreq = [(ROBOT_REPRESENTATIVE, 1), (REPRESENTATIVE, 1)]
  , fteam = teamRobot
  , fgroups = [ (ROBOT, 100), (MECHANICAL_CONTRAPTION, 100)
              , (MOBILE_ROBOT, 1), (IMMOBILE_ROBOT, 1)
              , (CONSTRUCTION_ROBOT, 1) ]
  , fskillsOther = zeroSkills
  , fcanEscape = False
  , fneverEmpty = False
  , fhiCondPoly = hiDweller
  , fhasGender = False
  , finitDoctrine = TRoam
      -- TODO:TFollow -- coordinated via net, follow alien leader
  , fspawnsFast = True
  , fhasPointman = False
  , fhasUI = False
  , finitUnderAI = True
  , fenemyTeams = [teamExplorer, teamCompetitor, teamCivilian, teamConvict]
  , falliedTeams = [teamMonster, teamAnimal]
  }
factRobotCaptive = factRobot
  { ffreq = [(ROBOT_CAPTIVE, 1)]
  , fneverEmpty = True
  }
factRobotNarrating = factRobot
  { ffreq = [(ROBOT_NARRATING, 1), (ANIMAL_OR_ROBOT_NARRATING, 30)]
  , fhasUI = True
  }
factRobotVirus = factRobot
  { fname = "Replicant Infestation"
  , ffreq = [(ROBOT_VIRUS, 1)]
  , fteam = teamOther  -- not the same as other robots
  , fgroups = [(MOBILE_ROBOT, 100)]  -- help assign proper actors to the faction
  , fneverEmpty = True  -- faction dissolved when all killed, despite spawning
  , falliedTeams = []  -- cut off, weird
  }
factRobotGauntlet = factRobotVirus
  { ffreq = [(ROBOT_GAUNTLET, 1)]
  , fgroups = fgroups factRobotVirus ++ [(GAUNTLET_ROBOT, 1)]
      -- keep the faction description the same, despite different composition
  }

-- ** teamOffWorld

factOffWorld = factCompetitor
  { fname = "Gray Off-World Mercenary"
  , ffreq = [(OFF_WORLD_REPRESENTATIVE, 1), (REPRESENTATIVE, 1)]
  , fteam = teamOffWorld
  , fgroups = [(MERCENARY_HERO, 100)]  -- summoned heroes all go to the player
  , fcanEscape = False
  , fhiCondPoly = hiHeroMedium
  , fenemyTeams = [teamExplorer, teamMonster, teamAnimal, teamRobot, teamHorror]
  , falliedTeams = []
  }