
#####################################################################
#                CLASSES AND FUNCTIONS
#####################################################################


#####################################################################
#                USEFUL FUNCTIONS
#####################################################################

init python:

    def hardRandInt(a,b): # int
        # easy mode?
        if GAME.diff() > 0:
            return(random.randint(a,b))
        # check random stack
        if len(GAME.rand) < 100:
            for i in range(100):
                GAME.rand.append(random.uniform(0.0,1.0))
        # get number
        r_uniform = GAME.rand.pop(0)
        r = float(a)+ r_uniform*(float(b)-float(a))
        r_int = int(round(r))
        # map range
        return(r_int)

    def hardRandFloat(a,b): # float
        # easy mode?
        if GAME.diff() > 0:
            return(random.uniform(a,b))
        # check random stack
        if len(GAME.rand) < 100:
            for i in range(100):
                GAME.rand.append(random.uniform(0.0,1.0))
        # get number
        r_uniform = GAME.rand.pop(0)
        r_float = a + r_uniform * (b-a)
        # map range
        return(r_float)

    def force_number(s):
        try:
            float(s)
            return(float(s))
        except ValueError:
            pass
        return(0)

    def titlePlus(string):
        elems = string.split(" ")
        newElems = []
        for i in elems:
            newElems.append(i[:1].upper()+ i[1:])
        string = " ".join(newElems)
        return(string)

    def simple_prod(a,b):
        return(a*b)

    def vary(string):
        elements = re.split('<(.*?)>',string)
        result = ""
        for i in elements:
            result += random.choice(i.split("|"))
        return(result)

    def occurrence(prob):
        if random.uniform(0.0,1.0) <= prob:
            return(True)
        return(False)

    def capValue(val, minLim,maxLim):
        result = val
        if result < minLim:
            result = minLim
        if result > maxLim:
            result = maxLim
        return(result)

    def listNext(refList, item):
        try:
            # Find the index of the current value
            current_index = refList.index(item)
            # Calculate the index of the next value with wrap-around
            next_index = (current_index + 1) % len(refList)
            # Return the value at the next index
            return(refList[next_index])
        except ValueError:
            # Handle the case where the current_value is not in the list
            return(None)

    def random_choices(population, weights=None, k=1):
        if weights is None:
            # If no weights are provided, select uniformly at random
            total = len(population)
            return [population[random.randint(0, total - 1)] for _ in range(k)]
        total = sum(weights)
        cum_weights = []
        cum_sum = 0
        for weight in weights:
            cum_sum += weight
            cum_weights.append(cum_sum)
        
        results = []
        for _ in range(k):
            rnd = random.random() * total
            for i, cum_weight in enumerate(cum_weights):
                if rnd < cum_weight:
                    results.append(population[i])
                    break

        return(results)
    
    def itemRollProportionalAmount(itemID):
        item = GAME.items[itemID]

        # Constants
        max_quantity = 10.0
        min_quantity = 1.0
        max_price = float(max([x.price for x in GAME.items.values() if x.kind == "ware"]))
        min_price = float(min([x.price for x in GAME.items.values() if x.kind == "ware"]))

        # Calculate the inversely proportional quantity using logarithmic scale
        price = item.price
        log_max_price = math.log(max_price + 1)
        log_min_price = math.log(min_price + 1)
        log_price = math.log(price + 1)

        quantity = min_quantity + (log_max_price - log_price) * (max_quantity - min_quantity) / (log_max_price - log_min_price)

        # Round the quantity to the nearest whole number
        quantity = round(quantity*random.uniform(0.5,1.0))
        return(max(int(quantity),int(min_quantity)))

    def article(noun): # cheap workaround, not gramatically correct
        if noun[0] in "eaiou":
            return("an "+noun)
        return("a "+noun)

    def colorize(msg, col):
        mu = {"gold": C_GO_B, "blue": C_BL_B, "purple": C_SU_B, "acc": C_HI_B}
        return(mu[col]+msg+C_WH_E)

    def percentColorized(val, interval= [0.0,100.0], reverse = False): 
        CB, CE = C_WH_B, C_WH_E
        rating = {  
            100: C_GR_B,
            80: C_YE_B,
            50: C_OR_B, 
            30: C_RE_B
        }
        # normalize value
        nx = int(float(val)/(interval[1]-interval[0]) * 100)
        if reverse:
            nxR = 100 - nx
        else:
            nxR = nx

        for i in sorted(rating.keys()):
            if nxR <= i:
                CB, CE = rating[i], "{/color}"
                return([CB+"{}%".format(nx)+CE, CB, CE,nx] ) 

        return( [CB+"{}%".format(nx)+CE, CB, CE, nx] )      

    def levelUpAt(start, level, fac = 2.0):
        lu = start * (1-fac**level)/(1-fac)
        return(lu)

    def calcReward(amount):
        result = int(round(float(amount)/10.0+pow(float(amount)/50.0,1.1)))
        return(result)

    def dye(msg, colGrade): # grades 0 to 5
        # crimson, bright-red,orange, dark yellow,yellow,light-green, green
        return(C_B+"{}".format(msg)+C_E)

    def colMap(number, cols, interval):
        # ex: colMap("someText", ["red", "yellow", "green"], [f_min, f_max])
        # generate the value step array 
        i0 = interval[0]
        i1 = interval[1]
        capNum = capValue(number, i0,i1)
        step = (float(i1) - float(i0))/ float(len(cols)) 
        # find matching step from left to right 
        for i in range(len(cols)):
            if capNum <=  float(i0)+step*(i+1):
                # colorize and return string  
                return("{}{}{}".format(ACCENTS[cols[i]][0], number, ACCENTS[cols[i]][1])) 
        return(str(number)) 

    def rotateRight(lst, k=1):
        n = len(lst)
        k = k % n
        result = lst[-k:]+lst[:-k]
        return(result)

    def rotateLeft(lst, k=1):
        n = len(lst)
        k = k % n
        result = lst[k:]+lst[:k]
        return(result)

    #####################################################################
    #                CLASSES
    #####################################################################

    #####################################################################
    #                GAME CORE
    #####################################################################
            
    class Game():
        def __init__(self,name):
            self.name = name
            self.act = 1
            self.day = 0
            self.money = 0
            self.hour = 14
            self.ap = 5
            self.version = str(config.version)
            self.IDcounter = 1
            self.tags = []


            # sandbox data
            self.locs = {} # room locations
            self.surfaces = {} # surface locations
            self.starSys = {} # star system systems
            self.locState = "loc_interior"
            self.sandboxLoc = None

            # GUI data
            self.guiInt = {"ship_stats": True, "hotkeys": True, "inventory": True, "ship_decks": True} # subsystem status widget on/off

            # other data elements
            self.items = {}
            self.arcade = [] # bots working in remote sex arcade
            self.drones = [] # transport etc. drones



            self.miniMaps = {}
            self.aElem = {}
            self.missions = []
            self.missionSys = MissionSystem()
            self.racingSys = RacingSystem()
            self.questSys = None
            self.lab = None # bot lab

            self.attr = {"SUB": "Submission", "STS": "Stress", "MOD": "Mood", "SLU": "Sluttiness", "ARO": "Arousal", "INT": "Intelligence", "RSP": "Respect", "APP": "Appreciation", "NOT": "Notoriety", "REP": "Reputation", "AFF": "Affection", "EXH": "Exhibitionism", "ENE": "Energy", "TEC": "Tech Skill", "MLK": "Milk Pet Skill", "SPD": "Speed", "END": "Endurance", "DEX": "Dexterity"}

            self.para = []
            self.mc = False
            self.npc = False
            self.actors = []
            self.crew = []
            self.passengers = []
            self.guests = []
            self.idleTalk = []
            self.locHistory = ["LOCEngine", None]
            self.log = []
            self.logging = False
            self.trophies = {"scholar": 0, "traveller": 1, "casanova": 0, "admiral": 0, "pharaoh": 0, "trainer": 0}
            self.trophyActive = {"scholar": False, "traveller": False, "casanova": False, "admiral": False, "pharaoh": False, "trainer": False}
            self.reports = []
            self.infractions = []
            self.repQueue = []
            self.repMisses = []
            self.books = []
            self.consoleData = []
            self.console = False
            self.debugMode = []
            self.safeZone = False
            self.extensions = []
            self.difficulty = 1 # 1 Normal, 2: Easy
            # self.inQueue = [] # active in-queue for Bots Access via cargo terminal to "unpack" DEPRECATED               
            self.inQueuePending = [] # all kind of incoming stuff  / item=["kind", <object>, <arrival day>]

            # Queue item format
            # item = ["kind", <object>, <label>]
            self.inQueueNew = []
            self.inQueuePendingNew = []

            self.adv = {}
            self.minigameDB = {}
            self.activityDB = {}
            self.jobDB = {}
            self.uniformDB = {}
            self.uniformSnapshot = {} # to restore uniforms post krell system
            self.combatDB = {}
            self.investments = []
            self.timers = {}
            self.occurredToday = []

            self.rand = []
            self.gallery = {}
            self.containers =  {
                "inventory": ["misc", "key", "tool", "map", "cube", "drink", "exotic", "magazine", "poster", "book", "liquor", "accessory", "clothing"], 
                "botlab": ["sexbot parts", "sexbot addons", ], 
                "cargo":["pod", "module", "ware", "subunit", "subsys", "crate" ]
                }
            #self.galleryMax = 20

        #####################################################################
        #					GAME MANAGEMENT AND PATCHES	
        #####################################################################
        def canLoad(self, version_number):
            if version_number[:7] == GAME.version[:7]:
                return(True)
            return(False)

        def canImport(self, version_number): 
            bins = [
                ["200_001", "200_002", "200_003", "200_004", "200_005", "200_006", "200_007", "200_008", "200_009", "200_010"],
                [ ]
            ]
            # unknown save game version
            if version_number == '': # 
                return(True) # continue vetting post data import
            # incompatible bin
            if not any([version_number in bin and GAME.version in bin for bin in bins]):
                return(False)
            if GAME.version < version_number:
                return(False)
            return(True)

        def patchApplied(self, ID):
            if ID not in self.extensions:
                self.extensions.append(ID)
                return(False)
            return(True)

        def scrubber(self):
            #####################################################################
            #              PARANOID / COMPATIBILITY SCRUBBERS 
            #####################################################################
            for i in GAME.lab.parts:
                # data scrubber for legacy data
                i.update()

            # scrub cargo for cost containers and nuke extraneous cargo tokens
            numContainersCargo = len([x for x in GAME.ship.cargo if x.ID == "ITMContainerBot"])
            numInQueue = len(GAME.inQueueNew)
 
            # CR-2008 START
            # fix cargo token inconsistencies
            # extraneous containers
            if numInQueue < numContainersCargo:
                for i in range(numContainersCargo-numInQueue):
                    GAME.ship.unloadWare("ITMContainerBot")
            # too few containers?
            elif numInQueue > numContainersCargo:
                for i in range(numInQueue-numContainersCargo):
                    GAME.ship.cargo.append(GAME.items["ITMContainerBot"])
            # CR-2008 END
            
            return()

        def patch(self):

            ################################################################
            #              PER VERSION INCREMENTAL PATCHES
            ################################################################

            # patch blocks (incremental)

            ##################################################################
            #              UPDATE DATA ACROSS VERSIONS
            ##################################################################
            # ===================================
            if GAME.version < "V200_002" and not REGISTRY.getKey("patch", "V200_002"):
                REGISTRY.setKey("patch", "V200_002", True)
                # character new stat max
                khelara.attrMaxTotal = {"APP": 9, "SLU": 6, "SUB": 3, "EXH": 10}
                tris.attrMaxTotal = {"APP": 5, "SLU": 13, "SUB": 7, "EXH": 14}
                nimhe.attrMaxTotal = {"APP": 5, "SLU": 15, "SUB": 9, "EXH": 11}
                moira.attrMaxTotal = {"APP": 3, "SLU": 8, "SUB": 7, "EXH": 10}
                vee.attrMaxTotal = {"APP": 0, "SLU": 9, "SUB": 0, "EXH": 12}
                seraphine.attrMaxTotal = {"APP": 1, "SLU": 5, "SUB": 4, "EXH": 9}
                # other stuff

                # mission system tracking update 
                patched = False
                for k in GAME.starSys:
                    for j in GAME.starSys[k].locs:
                        if GAME.starSys[k].locs[j].mission:
                            GAME.starSys[k].locs[j].missionsUnlock()
                            patched = True 
                REGISTRY.setKey("patch", "V200_002", True)
                GAME.consoleWrite("Game data updated to {}...".format("V200_002"))
                ##################################################################

            # ===================================
            if GAME.version < "V200_003" and not REGISTRY.getKey("patch", "V200_003"):
                REGISTRY.setKey("patch", "V200_003", True)
                GAME.consoleWrite("Game data updated to {}...".format("V200_003"))
                ##################################################################

            # ===================================
            if GAME.version < "V200_004" and not REGISTRY.getKey("patch", "V200_004"):
                # link states forgotten migration fix
                if GAME.starSys["SSIDNX010"].linkState["SSIDWaystation"]=="open":
                    REGISTRY.setKey("SSIDNX010", "SSIDWaystation", "open")
                    REGISTRY.setKey("SSIDWaystation", "SSIDNX010", "open")
                if GAME.questSys.isDone("QID_MAIN_SWARM"):
                    REGISTRY.setKey("SSIDNX010", "SSIDWaystation", "open")
                    REGISTRY.setKey("SSIDWaystation", "SSIDNX010", "open")
                if GAME.questSys.isDone('QID_MAIN_SWARMGATE_REPAIR'):
                    REGISTRY.setKey("SSIDIltari", "SSIDQuthor", "open")
                    REGISTRY.setKey("SSIDQuthor", "SSIDIltari", "open")

                REGISTRY.setKey("patch", "V200_004", True)
                GAME.consoleWrite("Game data updated to {}...".format("V200_004"))
                ##################################################################

            # ===================================
            if GAME.version < "V200_005" and not REGISTRY.getKey("patch", "V200_005"):

                # old pet data purge
                if "petwalk_unlocked" in nimhe.done:
                    nimhe.done.remove("petwalk_unlocked")
                for actor in [nimhe, seraphine]:
                    if hasattr(actor, "petMod") and getattr(actor, "petMod") != None:
                        actor.petMod.remCommand("present", "LOCArellartiDomePL")

                # pet module REP stat init fix
                for actor in GAME.crew:
                    if hasattr(actor, "petMod") and getattr(actor, "petMod") != None:
                        if "REP" not in actor.petMod.attr:
                            actor.petMod.attr["REP"] = 0
                nimhe.attrMaxTotal = {"APP": 5, "SLU": 15, "SUB": 9, "EXH": 11, "REP": 2, "MLK": 50}
                moira.attrMaxTotal = {"APP": 3, "SLU": 8, "SUB": 7, "EXH": 10, "MLK": 50}
                seraphine.attrMaxTotal = {"APP": 1, "SLU": 5, "SUB": 4, "EXH": 9, "REP": 2}

                REGISTRY.setKey("patch", "V200_005", True)
                GAME.consoleWrite("Game data updated to {}...".format("V200_005"))
                ##################################################################

            # ===================================
            patch_number = "V200_006" 
            if GAME.version < patch_number and not REGISTRY.getKey("patch", patch_number):
                # legacy milk mod attr upgrade if detected
                for actor in GAME.crew:
                    if hasattr(actor, "milkMod") and getattr(actor, "milkMod") != None:
                        if not hasattr(actor.milkMod, "attr"):
                            # migrate
                            setattr(actor.milkMod, "attr", {"MLK": actor.attr.get("MLK", 0)})

                REGISTRY.setKey("patch", patch_number, True)
                GAME.consoleWrite("Game data updated to {}...".format(patch_number))

            # ===================================
            patch_number = "V200_007" 
            if GAME.version < patch_number and not REGISTRY.getKey("patch", patch_number):
                # pet mod attr cleanup
                for actor in [tris, nimhe, seraphine]:
                    if getattr(actor, "petMod", None) != None: # mod object present?
                        # new attribute existence and init check
                        if not hasattr(actor.petMod, "attr"):
                            actor.petMod.attr = {"REP": 0}
                        new_attr = actor.petMod.attr.get("REP",0)

                        # legacy attr
                        old_attr = 0 
                        if "REP" in actor.attr:
                            old_attr = actor.attr.get("REP",0) 
                            del actor.attr["REP"]

                        # set new attribute
                        actor.petMod.attr["REP"] = max(old_attr, new_attr)

                # milk mod attr cleanup
                for actor in [moira, nimhe]:
                    if getattr(actor, "milkMod", None) != None: # mod object present?
                        # new attribute existence and init check
                        if not hasattr(actor.milkMod, "attr"):
                            actor.milkMod.attr = {"MLK": 0}
                        new_attr = actor.milkMod.attr.get("MLK",0)

                        # legacy attr
                        old_attr = 0 
                        if "MLK" in actor.attr:
                            old_attr = actor.attr.get("MLK",0) 
                            del actor.attr["MLK"]

                        # set new attribute
                        actor.milkMod.attr["MLK"] = max(old_attr, new_attr)

                # skill max update
                khelara.attrMaxTotal = {"APP": 9, "SLU": 7, "SUB": 3, "EXH": 10, "TEC": 25} # SLU 6 SUB 2 orig values
                tris.attrMaxTotal = {"APP": 5, "SLU": 13, "SUB": 7, "EXH": 14, "TEC": 25}
                nimhe.attrMaxTotal = {"APP": 5, "SLU": 15, "SUB": 9, "EXH": 11, "REP": 2, "MLK": 50, "TEC": 40}
                moira.attrMaxTotal = {"APP": 3, "SLU": 8, "SUB": 7, "EXH": 10, "MLK": 50}
                vee.attrMaxTotal = {"APP": 0, "SLU": 9, "SUB": 3, "EXH": 12, "TEC": 40} # SUB 0 orig value
                seraphine.attrMaxTotal = {"APP": 1, "SLU": 5, "SUB": 4, "EXH": 9, "REP": 2}

                REGISTRY.setKey("patch", patch_number, True)
                GAME.consoleWrite("Game data updated to {}...".format(patch_number))

            return()
     
        #####################################################################
        #              LOGGING
        #####################################################################

        def consoleWrite(self, msg):

            try:
                day = GAME.day 
            except:
                day = 0
            try:
                hour = GAME.hour
            except:
                hour = 0
            timestamp = "Day {}-{:02}:00 - ".format(day, hour)
            if len(self.consoleData) >= 20:
                self.consoleData = self.consoleData[-19:] + [timestamp + msg]
            else:
                self.consoleData.append(timestamp + msg)
            return()

        def consoleGet(self, lines = 20):
            return(GAME.consoleData[:lines])

        def consoleClear(self):
            self.consoleData = []
            return()

        #####################################################################
        #              SANDBOX MANAGEMENT
        #####################################################################
        def setSandboxLoc(self, loc):
            GAME.sandboxLoc = loc
            # interior 
            if loc.ID[:3] == "LOC":
                self.setLocID(loc.ID)
                
            return()

        def determineStarSys(self, spaceLocID):
            for k in GAME.starSys.keys():
                if spaceLocID in GAME.starSys[k].locs.keys():
                    return (GAME.starSys[k])
            return(False)
        
        def getSandboxLoc(self, locID = False): # current or specified
            if locID:
                # space
                if locID[:3] == "LID":
                    loc = GAME.getStarSys().locs[locID] 
                # interior 
                elif locID[:3] == "LOC":
                    loc = GAME.locs[locID]
                # surface
                elif locID[:3] == "SUI":
                    loc = GAME.surfaces[locID]
                else:
                    loc = False
                return(loc)
            else:
                return(GAME.sandboxLoc)

        #####################################################################
        #                LOCATION INFO
        #####################################################################
        # ===== CR-0466 END

        def timerPurge(self, k):
            if k in self.timers.keys():
                self.timers.pop(k)
            return()

        def timerSet(self, k,days):
            self.timers[k] = days
            return()

        def timerGet(self, k):
            if k not in self.timers.keys():
                return(None) # CR-2007
            return(self.timers[k])

        def hasRepSpot(self, locID):
            repTags = {"LOCCargo": "AERepCargo", "LOCEngine": "AERepEngine", "LOCHallway1": "AERepHallOne", "LOCHallway2": "AERepHallTwo"}
            # Python 3.x Migration
            if locID in repTags:
                if any([x for x in GAME.repQueue if repTags[locID] in x]):
                    return(True)
            else:
                return(False)
            return(False)

        def getMiniMapMatch(self, locID=False):
            if not locID:
                locID = GAME.getLoc().ID 
            for miniMap in GAME.miniMaps.values():
                for cRow in miniMap.cells:
                    for cell in cRow:
                        if cell:
                            if cell.locID == locID:
                                return(miniMap) 
            return(False)

        def getUniqueID(self):
            self.IDcounter += 1
            return(self.IDcounter)

        def getDueMissions(self): # DEPRECATED / MOVED TO MISSIONSYS
            missions = []
            for i in self.missions:
                if i.due == GAME.day:
                    missions.append(i)
            return(missions)

        def getFailedMissions(self):
            missions = []
            for i in self.missions:
                if i.due < GAME.day:
                    missions.append(i)
            return(missions)

        def getExteriorBackdrop(self):
            bg = "05_gui/intermission.png"
            return(bg)

        def getStarSys(self):
            return(self.starSys[self.ship.starSysID])

        def swapStarSys(self, starSysID, locID):
            # local? 
            if starSysID == self.getSpaceLoc().ID:
                return(False)
            # intersystem?
            elif locID in self.starSys[starSysID].locs.keys():
                self.ship.spaceLocID = "LOCDummy" # paranoid default for screens
                self.ship.starSysID = starSysID
                self.setSpaceLocID(locID)
                return(True)
            return(False)
       
        def getSpaceLoc(self, sLocID = False):
            if not sLocID:
                starSys = self.starSys[self.ship.starSysID]
                if self.ship.spaceLocID in starSys.locs.keys():
                    return(starSys.locs[self.ship.spaceLocID])
                return(False)
                
            else: # search through starmaps for location OD
                for x in self.starSys.values():
                    for y in x.locs.values():
                        if sLocID == y.ID:
                            return(y)
            return(False)

        def setSpaceLocID(self, spaceLocID):
            self.ship.spaceLocID = spaceLocID
            return()

        def locHasChanged(self):
            if len(self.locHistory) >= 2:                       # CR-0274
                if self.locHistory[0] != self.locHistory[1]:    # CR-0274
                    return(True)                                # CR-0274
            return(False)                                       # CR-0274

        def getLoc(self):
            return(self.locs[self.locHistory[0]])

        def setLocID(self, locID):  
            if locID == self.locHistory[0]:                                 # CR-0274
                return(False)                                                # CR-0274
            self.locHistory = [locID] + self.locHistory # shift by one      # CR-0274
            if len (self.locHistory) > 20:                                  # CR-0274
                self.locHistory = self.locHistory[:10]                      # CR-0274
            return(True)                                                    # CR-0274

        def getSpriteCoord(self, actKey, activity):
            result = False
            for i in self.activityDB[actKey]:
                if i[1] == activity:
                    return(i[2])
            return(result)

        #####################################################################
        #                MISC GAME SERVICES
        #####################################################################
        def initTodaysRace(self, crew = False):
            if not self.racingSys.getTodaysRace():
                # set up race
                race = RaceEvent(GAME.day, {"crew": crew})
                self.racingSys.registerRace(race) # log race
            else:
                race = self.racingSys.getTodaysRace()

            if not race:
                return(False) # graceful termination

            return(True)

        def nextEpisode(self): 
            for i in range(1,max(GAME.galaxinaData.keys())+1):
                if not REGISTRY.getKey("galaxina",i):
                    return(i)
                # if not GAME.galaxinaData[i][3]:
                #     return(i)
            return(False)

        def knownEpisode(self, k): # CR-2031
            if k not in GAME.galaxinaData:
                return(False)
            elif REGISTRY.getKey("galaxina",k): # GAME.galaxinaData[k][3]:
                return(True)
            return(False)

        def registerEpisode(self, mode = "next"): # CR-2031
            for k in range(1,max(GAME.galaxinaData.keys())+1):
                if not REGISTRY.getKey("galaxina",k): # GAME.galaxinaData[k][3]:
                    REGISTRY.setKey("galaxina",k, True)
                    # GAME.galaxinaData[k][3] = True
                    return(k)
            return(False)

        def hasOccurredToday(self,topic, n=1):
            if self.occurredToday.count(topic) >= n:
                return(True)
            else:
                self.occurredToday.append(topic)
                return(False)

        def galleryAdd(self, slot):
            GAME.gallery[slot].found = True # deprecated
            REGISTRY.setKey("gallery", slot, True)
            return()

        def getPartialGallery(self, galleryFilter):
            partialGallery = []
            for k in range(len(self.gallery.keys())):
                item = self.gallery[k]
                # Python 3.x Migration
                for fKey in galleryFilter:
                    if galleryFilter[fKey][2] and galleryFilter[fKey][0] in item.tags:
                        partialGallery.append(item)
                        break
            return(partialGallery)

        def galleryGetMax(self, galleryFilter = {}):
            if galleryFilter:
                n = 0
                # Python 3.x Migration
                for k in galleryFilter:
                    if galleryFilter[k][2]:
                        n += len([x for x in GAME.gallery.values() if galleryFilter[k][0] in x.tags])
                return(n)
            return(max(self.gallery.keys()))

        def diff(self,mode = "factor"):
            return(self.difficulty)

        def hasExt(self, ID):
            if ID in self.extensions:
                return(True)
            return(False)

        def getItemKind(self, item):
            if not isinstance(item, (SexbotPart, Item)):  
                return("unknown")
            if item.kind in self.containers["cargo"]:
                return("cargo")
            elif item.kind in self.containers["inventory"]:
                return("inventory")
            elif item.kind in self.containers["botlab"]:
                return("botlab")
            return("unknown")

        def addItemSmart(self, item): 

            if isinstance(item, str): # autoconversion from ID to item
                item = self.items.get(item, False)
            if not item:
                return(False)

            if item.kind in self.containers["cargo"]:
                self.ship.cargo.append(item)
                return(True)
            elif item.kind in self.containers["inventory"]:
                self.mc.addItem(item.ID)
                return(True)
            elif item.kind in self.containers["botlab"]:
                self.lab.append(item)

            return(False)

        def rollItem(self, isKind, notKind, level, cond="True"):
            item = None
            # build filtered list
            iList = []
            for i in GAME.items.values():
                passed = True
                if i.kind in notKind:
                    passed = False
                if i.kind not in isKind and "any" not in isKind:
                    passed = False
                for actor in GAME.npc + [GAME.mc]:
                    if i in actor.inventory and "unique" in i.tags:
                        passed = False
                if "no_find" in i.tags:
                    passed = False
                if i.price < level * 20 and random.randint(1,100) > 50:
                    passed = False
                if not eval(cond):
                    passed = False

                if passed:
                    iList.append(i)
            # roll item
            i = 0
            while item == None:
                rollPrice = abs(random.gauss(0,50*GAME.mc.lvl))
                rollItem = random.choice(iList)
                if rollPrice > rollItem.price:
                    item = rollItem
                i += 1
                if i > 100: # DEBUGGING PURPOSE
                    item = GAME.items["ITMLampOil"]
            if item == None: # redundant / paranoid
                item = GAME.items["ITMLampOil"]

            return(item)

        def lootCrate(self, item, tags = [], attr = {}): # xxx
            result = []

            if item.ID == "ITMCrateLiquor":
                roll = random.choice(["ITMLiquorFairy", "ITMLiquorDrengar", "ITMLiquorIltari"]*4+["ITMLiquorVortex"]) 
                amount = random.choice([6,6,6,12,12,18])
                if roll == "ITMLiquorVortex":
                    amount = 1
                result += [self.items[roll]]*amount     


            elif item.ID in ["ITMCrateBots", "ITMCrateBotsMil", "ITMCrateBotsRare", "ITMCrateBotsPre"]:
                amount = random.choice([3,3,3,3,4,4,4,5,5])

                if item.ID == "ITMCrateBotsRare":
                    genList = [2,2,2,3]
                else:
                    genList = [1,1,1,1,1,1,2,2,2,3]

                while amount:
                    gen = random.choice(genList)
                    amount -= 1
                    kind = random.choice(["sexbot addons", "sexbot parts", "sexbot parts", "sexbot parts"])

                    if item.ID in ["ITMCrateBotsPre"]:
                        lootList = [x for x in GAME.sbPartsData.values() if x.slot =="cube"]*3+[GAME.sbPartsData["SBPCortexPre"]]
                    elif kind == "sexbot parts":
                        lootList = [x for x in GAME.sbPartsData.values() if x.kind == kind and x.cat == gen and "no_find" not in x.tags]
                    else:
                        lootList = [x for x in GAME.sbPartsData.values() if x.kind == kind and "no_find" not in x.tags and x.cat == gen]

                    loot = random.choice(lootList)
                    if item.ID == "ITMCrateBotsMil":
                        loot = loot.clone({"prefs": ["military grade"]})
                    elif item.ID == "ITMCrateBotsRare":
                        loot = loot.clone({"prefs": ["basic", "advanced", "advanced", "advanced", "military grade"]})                
                    else:
                        loot = loot.clone()
                    result.append(loot) 

            return(result)

        def uncrateAllLiquorCrates(self, crateID):

            # prepare data
            tallied = False
            result = None

            uncrateTally = {
                "ITMCrateLiquor": 0, 
                "ITMLiquorVortex": 0,
                "ITMLiquorFairy": 0,
                "ITMLiquorIltari": 0,
                "ITMLiquorDrengar": 0,
                "Debug": []
            }
            
            # check liquor
            crates = [x for x in self.ship.cargo if x.ID == crateID]
            items = []
            while crates:
                crate = crates.pop()
                items += GAME.lootCrate(crate) 
                self.ship.unloadWare(crate.ID)
                uncrateTally[crate.ID] = uncrateTally.get(crate.ID,0)+1 
            uncrateTally["Debug"] = list(items)

            # tallyUp & resister
            for i in items:
                uncrateTally[i.ID] = uncrateTally.get(i.ID,0)+1 
                self.mc.addItem(i.ID)
                tallied = True

            # clean up data     
            if tallied == True:
                result = uncrateTally                
            return(result)

        def uncrateAllBotCrates(self, crateID):

            # prepare data
            tallied = False
            result = None

            uncrateTally = {
                "ITMCrateBots": 0, 
                "ITMCrateBotsRare": 0, 
                "ITMCrateBotsMil": 0, 
                "ITMCrateBotsPre": 0, 
                "Parts": 0, # total parts 
                "Special": [], # report-worthy parts
                "Debug": []
            }
            
            # check Bot Crates 
            crates = [x for x in self.ship.cargo if x.ID == crateID]
            parts = []
            while crates:
                crate = crates.pop()
                uncrateTally[crate.ID] = uncrateTally.get(crate.ID, 0) + 1
                parts += self.lootCrate(crate) 
                self.ship.unloadWare(crate.ID)
                tallied = True
            uncrateTally["Parts"] = uncrateTally.get("Parts", 0) + len(parts)

            # tallyUp & register
            for i in parts:
                # report-worthy stuff 
                if i.pref in ["military grade", "elite"] and i.cat in [3,4]:
                    special = i.getName(["col", "noArt", "cap"])
                    uncrateTally["Special"].append(special)
                self.lab.parts.append(i)

            # clean up data     
            if tallied == True:
                result = uncrateTally
                if result["Special"]:
                    result["Special"].sort()  

            return(result)

        def tallyInfiltrators(self): # used for "lost" infiltrators recovery
            iShells = []
            # shell in lab?
            iShells = [x for x in GAME.lab.parts if x.isShell() == "I"]
            # shell in lab bot?
            for bot in [x for x in GAME.lab.cells if x]:
                iShells += [x for x in bot.addOns if x and x.isShell() == "I"] # CR-1068
            # shell in arcade bot?
            for bot in [x for x in GAME.arcade ]:
                iShells += [x for x in bot.addOns if x and x.isShell() == "I"]
            # shell in cargo bot?
            for bot in [x[1] for x in GAME.inQueueNew if x[0] == "bot"]:
                iShells += [x for x in bot.addOns if x and x.isShell() == "I"]
            # shell in warehouse bot?
            for bot in [x[1] for x in GAME.base.storedGoods if x[0] == "bot"]:
                iShells += [x for x in bot.addOns if x and x.isShell() == "I"]
            return(len(iShells))

        def unlockNewGalaxina(self, name, comment, gallery, found):
            if GAME.mc.getTally("Galaxina Episodes") >= max(self.galaxinaData.keys()):
                return(False)
            k = GAME.mc.getTally("Galaxina Episodes")+1
            REGISTRY.setKey("galaxina",k, True)
            # self.galaxinaData[k][3] = True # mark as found
            return(k)

    class GalleryItem():
        def __init__(self,ID,name, hint, found, anim):
            self.ID = ID
            self.name = name
            self.hint = hint
            self.found = found
            self.anim = anim
            self.tags = []

    class Registry():
        def __init__(self):
            self.ENTRIES = {}

        def setKey(self, key, attribute, value):
            if not self.ENTRIES.get(key, False):
                self.ENTRIES[key] = {attribute: value}
            else:
                self.ENTRIES[key][attribute] = value
            return()

        def getKey(self, key, attribute):
            if key in self.ENTRIES:
                return(self.ENTRIES[key].get(attribute, None))
            return(None)
    #####################################################################
    #                ITEMS
    #####################################################################
    class Item():
        def __init__(self, ID, name, art, kind, description, slot, price, tags, req = {}):
            self.art = art
            self.ID = ID
            self.name = name
            self.description =description
            self.slot = slot
            self.kind = kind
            self.price = price
            self.tags = tags
            self.req = req
            self.attr = self.req
            self.accent = self.getAccent()

        def getAttr(self, k):
            if k in self.attr.keys():
                return(self.attr[k])
            return(None)

        def lacksLicense(self):
            for i in ["grade_1", "grade_2", "grade_3"]:
                if i in self.tags:
                    if i not in GAME.mc.done:
                        return(i.replace("_", " ").title())
            return(False)

        # CR-0970 START ============
        def isOnlyOne(self):
            count = 0
            count += GAME.ship.cargo.count(self)
            count += GAME.base.wares.count(self)
            count += GAME.mc.inventory.count(self.ID)
            count += [x[0] for x in GAME.ship.subSys.values()].count(self.ID)
            if count <= 1:
                return(True)
            return(False)
        # CR-0970 END ============

        def scrub(self): # for object pointer duplicates due to patches gone wrong etc.
            item = self
            if self.ID in GAME.items.keys() and self != GAME.items[self.ID]:
                item = GAME.items[self.ID]
            return(item)

        def getBoostOneLiner(self): # tooltip compatibility
            return("")

        def getPath(self):
            return("08_items/")

        def getIco(self):
            return("ico {}.png".format(self.ID))

        def getKey(self):
            return(self.kind+self.ID)

        def getGrade(self): # wrapper for weapons grade etc:
            if "grade_1" in self.tags:
                return(1)
            if "grade_2" in self.tags:
                return(2)
            if "grade_3" in self.tags:
                return(3)      
            return(False)

        def getSound(self):
            sounds = {
                "gem": ["crystal"],
                "reading": ["scroll"],
                "equip key": ["key"],
                "potion": ["fragrance", "glitter fragrance", "milk", "liquor"]
            }
            # Python 3.x Migration
            for k in sounds:
                if self.kind in sounds[k]:
                    return(k+".wav")
            return("equip.wav")

        def getAccent(self):
            # generic colors
            categories = {
                "gold": ["jewelry", "accessory", "ware"],
                "orange": ["build", "liquor"],
                "bright blue": ["tech", "subsys", "subunit", "tool"],
                "pink": ["pod", "ai", "map"],
                "brown": ["misc"],
                "cyan": ["crate"]
            }
            # special overrides
            if self.getAttr("accent"):
                return(self.attr["accent"])

            if "no_sell" in self.tags:
                return("yellow")

            if "krell" in self.ID.lower():
                return("green")
            
            # weapon
            if any(x in self.tags for x in ["grade_1", "grade_2", "grade_3"]):
                return("orange")

            if self.ID in ["ITMBomb"]:
                return("orange")

            # Python 3.x Migration
            for k in categories:
                if self.kind in categories[k]:
                    return(k)

            return("blue")

        def getName(self, tags=[]):
            blank = " "
            C_B, C_E = "", ""
            art = self.art.lower()
            if "noArt" in tags:
                art = ""
                blank = ""

            else:
                if art == "":
                    blank = ""
            if "col" in tags:
                C_B = ACCENTS[self.getAccent()][0]
                C_E = ACCENTS[self.getAccent()][1]
            if "cap" in tags:

                result = art+blank+C_B+titlePlus(self.name)+C_E
            else:
                result = art+blank+C_B+self.name+C_E
                result = result.lower()
            return(result)

        def getOutput(self): # weapon shield etc. output with addons)
            output = self.attr.get("output", 0.0)
            return(output)

    #####################################################################
    #                ACTOR
    #####################################################################
    class Job():
        def __init__(self,ID, name, description, req, effect, skillUp = {}):
            self.ID = ID
            self.name = name 
            self.description = description
            self.req = req
            self.effect = effect
            self.skillUp = skillUp

    class UniformOrder():
        def __init__(self,ID, name, description, req, effect, unlocked, itemID):
            self.ID = ID  
            self.name = name
            self.description = description
            self.req = req
            self.effect = effect
            self.unlocked = unlocked 
            self.itemID = itemID
            

    class Actor():
        def __init__(self, ID, name, race = "Unknown", attr = {}):
            # pc or npc
            self.ID = ID
            self.name = name
            self.status = []
            self.attr = attr
            self.attrFrac = {}  
            self.skillProgDict = {}
            for k in self.attr.keys():
                self.attrFrac[k] = float(attr[k])
            self.attrMax = {} # temporary attr max blocks
            self.attrMaxTotal = {} #  max achivable stats
            self.done = []
            self.doneToday = []
            self.actions = []
            self.inventory = []
            self.invHistory = []
            self.invHistPos = -1
            self.money = 0
            self.tags = []
            self.tally = {}
            self.counters = {}
            self.race = race
            self.missions = []
            self.achievements = []
            self.factions = {} # standing voron, krell, swarm, iltari...
            self.activities = []
            self.backstory = "Not much is known about {}'s background".format(self.name)
            self.jobs = ["JOB_Idle"] # available jobs
            #self.uniform = "free" # uniform order, key for outfitDB

            # npc use only
            self.defaultOutfit = False
            self.outfit = [] # item worn
            self.accessory = [] # accessory worn
            self.wardrobe = [] # everything equippable, not inventory
            self.progOutfits = []
            self.progChoice = None
            self.uniform = "UID1_Free"
            self.fp = 0
            self.favours = []
            self.locOverrideReg = {}

            # expansion modules
            self.milkMod = None
            self.petMod = None

            # pc relevant
            self.lvl = 1
            self.exp = 0

            # register
            GAME.actors.append(self)



        #####################################################################
        #                ACTOR: Task Related
        #####################################################################
        def getJobTable(self, k):
            result = []
            if self.skillProgDict.get(k, False):
                skillLvls = self.skillProgDict[k][0::2]
                jobIDs = self.skillProgDict[k][1::2]
                jobNames = [GAME.jobDB[x].name for x in jobIDs]
                jobDescr = [GAME.jobDB[x].description for x in jobIDs]
                for i in range(len(skillLvls)):
                    result.append([skillLvls[i], jobNames[i], jobDescr[i]])
            if result == []:
                return[["{} x".format(k), "Unknown", "No Jobs recorded"]]
            return(result)

        def torpRestock(self):
            torpIDs = ["ITMTorpedoVoron", "ITMTorpedoBFT", "ITMTorpedoNeutronic"]
            GAME.getSpaceLoc().restock()
            supply = GAME.getSpaceLoc().wares

            # check supply 
            # fill up 
            restockLoop = True
            totalItems = 0
            totalSpent = 0

            while restockLoop:
                funds = GAME.money > 5000000
                stockedTorps = sum([1 for x in GAME.ship.cargo if x.ID in torpIDs])
                torpSupply = sum([1 for x in supply if x.ID in torpIDs])
                if funds and stockedTorps < 20 and torpSupply > 0:
                    # buy a torpedo
                    item = [x for x in supply if x.ID in torpIDs][0]
                    GAME.ship.cargo.append(item)
                    supply.remove(item)
                    GAME.money -= GAME.getSpaceLoc().getPrice(item,"buy")
                    totalSpent += GAME.getSpaceLoc().getPrice(item,"buy")
                    totalItems += 1
                else:
                    restockLoop = False

            if totalItems:
                return(totalItems, totalSpent)
                    
            return(False)

        def torpReload(self, attr={}): # in combat reload

            # CR-0989 START ==========
            # check launcher 
            if not GAME.ship.hasSubSys("launcher"):
                return(False)
            # CR-0989 END ==========

            # check torps in cargo 
            torpsInCargo = [x for x in GAME.ship.cargo if x in GAME.ship.supportedAddOns("launcher") and "torpedo" in x.ID.lower() ]
            torpsInCargo.sort(key=lambda x: x.price, reverse=True)
            maxLoadout = GAME.ship.getLoadoutMax("launcher")
            if torpsInCargo == []:
                return(False)
            # check free slots 
            slotNums = [ x for x in range(6) if x < maxLoadout and GAME.ship.getAddOns("launcher")[x]==False]
            if slotNums == []:
                return(False)
            # reload one torp 
            slot = slotNums[0] 
            torp = torpsInCargo[0]
            GAME.ship.equipAddOn("launcher", torp.ID, slot)
            GAME.ship.unloadWare(torp.ID)

            return("Slot {} reloaded with {}".format(slot, torp.getName(["noArt", "cap", "col"])))

        def workAI(self):

            if GAME.hour <= 8:
                return()

            if "job" in self.doneToday:
                return()

            # stress and mood impact of job 
            job = self.getJob()
            if not job:
                return()

            # STS and MOD effect
            if not self.hasDoneToday("job_impact"): # only once for multiple executions daily
                for k in [x for x in job.effect.keys() if x in self.attr.keys()]:
                    impact = self.getJob().effect[k]
                    self.attr[k] = capValue(self.attr[k]+impact,0,100)

            # Skill Up
            for k in job.skillUp.keys():
                self.incAttrFrac(k, job.skillUp[k][0], job.skillUp[k][1])
            
            # parts procurement (Nimhe)
            if self.getJob().ID == "JOB_Parts":
                if GAME.ship.cargoFree() >= 2 and GAME.money > 80000:
                    crates = 2 
                elif GAME.ship.cargoFree() >= 1 and GAME.money > 75000:
                    crates = 1 
                else:
                    crates = 0 
                if crates:
                    GAME.consoleWrite("I: {} - {} crates procured...".format(self.name, crates))
                while crates and GAME.ship.cargo.count(GAME.items["ITMCrateBots"]) < 5:
                    crates -= 1 
                    GAME.ship.loadWare("ITMCrateBots")
                    GAME.money -= 15000

                self.hasDoneToday("job")

            # Space Survey (Vee)
            elif self.getJob().ID == "JOB_Astro" and GAME.hour >= 9:
                if random.randint(1,100) > 50: 
                    if GAME.getStarSys().ID == "SSIDIltari":
                        sLoc = GAME.getStarSys().locs["LIDOddityIltari"]
                        dynLocPara = random.choice(sLoc.tags)
                        sLoc.coord = dynLocPara[:2]
                        sLoc.zone = dynLocPara[2]
                        REGISTRY.setKey(sLoc.ID, "found", True)
                        GAME.reports.append(["SPACE SURVEY", "{} has identified a Space Oddity and marked the site on your map.\n\nIt might be worthwhile to explore the location".format(self.name), ["Continue"], "05_gui/ico_gui_hostile_swarm.png", "pink"])   
                        GAME.consoleWrite("I: {} - Space Oddity Iltari found...".format(self.name))

                    elif GAME.getStarSys().ID == "SSIDNX010" and self.getAttr("TEC") >= 40:
                        sLoc = GAME.getStarSys().locs["LIDOddityNX010"]
                        dynLocPara = random.choice(sLoc.tags)
                        sLoc.coord = dynLocPara[:2]
                        sLoc.zone = dynLocPara[2]
                        REGISTRY.setKey(sLoc.ID, "found", True)
                        GAME.reports.append(["UNDERVERSE SURVEY", "{} has identified an Underverse Oddity and marked the site on your map.\n\nIt might be worthwhile to explore the location".format(self.name), ["Continue"], "05_gui/ico_gui_vaultship.png", "orange"])
                        GAME.consoleWrite("I: {} - Space Oddity NX010 Underverse found...".format(self.name))

                        #self.incAttrFrac("TEC", 0.8, 30)
                    elif GAME.getStarSys().ID == "SSIDNX010" and self.getAttr("TEC") < 40:
                        GAME.consoleWrite("I: {} - Insufficient TEC (40+) to identify NX010 Underverse Rifts...".format(self.name))
                    else:
                        GAME.consoleWrite("I: {} - Unsuitable Star System to find Space Oddities...".format(self.name))
                else:
                    GAME.consoleWrite("I: {} - Space Oddity occurrence RNG not met for day (50 percent)...".format(self.name))

                self.hasDoneToday("job") 

            # parts upgrading (Nimhe)
            elif self.getJob().ID == "JOB_Upgrade":
                upgradeReport = ""

                if self.getAttr("TEC") < 15:
                    exclude_grade = []                      
                elif self.getAttr("TEC") < 20:
                    exclude_grade = ["basic"]                        
                else:
                    exclude_grade = ["basic", "advanced"]                       

                for i in range(3):
                    part = GAME.lab.upgradeParts("pref", ["any"], exclude_grade)
                    self.doneToday.append("parts attempt")
                    self.doneToday.append(part)
                    if part:
                        upgradeReport += part.getName(["col", "noArt", "cap"])+"\n"
                        #self.incAttrFrac("TEC",0.8,20.0)

                if upgradeReport:
                    GAME.reports.append(["BOTLAB REPORT", "{} has created new bot parts from scrap:\n\n{}".format(self.name,upgradeReport), ["Continue"], "08_items/ico ITMSpanner.png", "blue"])   
                    GAME.consoleWrite("I: {} - Upgraded bot part(s) created...".format(self.name))
                else:
                    GAME.consoleWrite("I: {} - No suitable bot parts for upgrade found...".format(self.name))

                self.hasDoneToday("job")                

            # pod restocking (T'Ris)
            elif self.getJob().ID == "JOB_Pods":
                pods = ["ITMPodBio", "ITMPodEnergy", "ITMPodNano"]
                numPods = 0
                for i in range(5):
                    for k in pods:
                        item = GAME.items[k]
                        if GAME.ship.cargoFree() and GAME.ship.cargo.count(item) < 5 and GAME.money > 50000:
                            GAME.ship.loadWare(item.ID)
                            amount = item.price
                            GAME.books.append([GAME.day, "Wares Bought", -amount])
                            GAME.money -= amount 
                            numPods += 1
                    #self.incAttrFrac("TEC", 0.8, 15)
                if numPods:
                    GAME.consoleWrite("I: {} - {} pods acquired and loaded...".format(self.name, numPods))
                else:
                    GAME.consoleWrite("I: {} - Pod acquisition unsuccessful (conditions not met)".format(self.name))

                self.hasDoneToday("job")

            # torpedo restocking (T'Ris):
            elif self.getJob().ID == "JOB_Torps" and GAME.ship.status in ["docked"]:
                result = self.torpRestock()
                if result:
                    GAME.reports.append(["ARMAMENT STATUS REPORT", "{} has {}Restocked Torpedos ({}x){} while your Ship was docked.\n\nShe has spent a total of {:,} Cr.".format(self.name, C_HI_B, result[0], C_HI_E, result[1]), ["Continue"], "08_items/ico ITMTorpedoBFT.png", "orange"])   
                    #self.incAttrFrac("TEC", 0.8, 25)
                self.hasDoneToday("job")                

            elif self.getJob().ID == "JOB_Diag":
                #self.incAttrFrac("TEC", 0.8, 15)
                GAME.consoleWrite("I: {} - Ship diagnosis done...".format(self.name))

                self.hasDoneToday("job")

            # craft shell (nimhe)
            elif self.getJob().ID in ["JOB_Shell_Simple", "JOB_Shell_Exotic", "JOB_Shell_Luxury", "JOB_Shell_Rarified"]:
                GAME.lab.craftShell(nimhe, self.getJob().ID.lower().split("_")[2])
                # actor.incAttrFrac("TEC", 0.8, 40)
                self.hasDoneToday("job")

            # torpedo loadout (Khelara)
            elif self.getJob().ID == "JOB_Loadout" and GAME.ship.hasSubSys("launcher"): # CR-0600
                # check empty slots 
                maxLoadout =GAME.ship.getLoadoutMax("launcher")

                # check available torpedos 
                torpsInCargo = [x for x in GAME.ship.cargo if x in GAME.ship.supportedAddOns("launcher")]

                # sort torpedoes 
                torpsInCargo = sorted(torpsInCargo, key = lambda x: x.price, reverse = True )

                # loadout torpedos 
                for i in range(6):
                    numTorps = 0
                    if i < maxLoadout:
                        #GAME.log.append(( i , len(torpsInCargo)))
                        if torpsInCargo and GAME.ship.getAddOns("launcher")[i]==False:
                            torp = torpsInCargo.pop(0)
                            GAME.ship.equipAddOn("launcher", torp.ID, i) 
                            GAME.ship.unloadWare(torp.ID) 
                            numTorps += 1  
                            #GAME.log.append((i,torp.ID, len(torpsInCargo)))  # DEBUG 
                    if numTorps:
                        GAME.consoleWrite("I: {} - {} torpedoes reloaded...".format(self.name, numTorps))
                    else:
                        GAME.consoleWrite("I: {} - No torpedoes to reload found...".format(self.name))

                #actor.incAttrFrac("TEC", 0.8, 20)
                self.hasDoneToday("job")

            # repair ship (Khelara)
            elif self.getJob().ID == "JOB_Repair": 
                # fix all issues
                self.tallyUp("Repairs", len(GAME.repQueue))
                if len(GAME.repQueue):
                    GAME.consoleWrite("I: {} - {} repairs carried out...".format(self.name, len(GAME.repQueue)))
                else:
                    GAME.consoleWrite("I: {} - Nothing to repair found...".format(self.name))

                self.hasDoneToday("repair")
                GAME.mc.hasDoneToday("repairs_assigned")
                GAME.repQueue = []
                GAME.repMisses = []
                GAME.mc.hasDoneToday("repair")
                self.hasDoneToday("job")
                #self.incAttrFrac("TEC", 0.8, 15)

            # surprise crate shopping (Seraphine)
            elif self.getJob().ID == "JOB_Shop" and seraphine.getLocOverride() == False and GAME.money > 10000000: # CR-0733
                # intitialize 
                content = []
                name = "Standard Crate"
                # roll surprise crate 
                level = hardRandInt(1,100)
                # Ultimate Bot Box
                if level > 95:
                    name = "Ultimate Bot Crate"
                    content = random.sample(["ITMContainerBot"]+["ITMAICoreElite"]*2+["ITMCrateBotsPre"]+ ["ITMCrateBotsMil"],random.randint(3,5))+["ITMContainerBot"]
                # Elite Gear Box
                if level > 85:
                    name = "Elite Gear Crate"
                    content = random.sample(["ITMTorpedoVoron"]*3+["ITMMineCluster"]*3+["ITMTorpedoBFT"]*4,random.randint(3,5)) # CR-0751
                # Luxury Box
                elif level > 70:
                    name = "Luxury Crate"
                    content = random.sample(["ITMEternium"]+["ITMLiquorVortex"]*3+["ITMCrateBotsRare"]*2+["ITMCrateExclusive"]+["ITMCrateLiquor"]*2,random.randint(3,5))
                # Odd Crate First
                elif level > 50 and "odd_crate" not in self.done:
                    name = "Odd Crate"
                    content =["ITMCandle"]*16
                # Exckusive Goods Box
                elif level > 50:
                    name = "Exclusive Crate"
                    content = random.sample(["ITMClustersHalidium"]+["ITMDiscsTellurium"]+["ITMBioActivators"]+["ITMPlasmoidSpheres"]+["ITMNanotubes"]+["ITMOreInfernium"]+["ITMInferniumPlating"]+["ITMOrePromethium"],random.randint(3,5))
                # Random Box
                elif level > 10:
                    name = "Random Crate"
                    content = random.sample([x.ID for x in GAME.items.values() if x.kind == "ware"]+["ITMCandle"],random.randint(3,5))
                else:
                    pass

                # store surprise crate
                if content:
                    self.surpriseCrate = [name, [GAME.items[x] for x in content]]
                    GAME.consoleWrite("I: {} - {} identified for procurement...".format(self.name, name))
                else:
                    GAME.consoleWrite("I: {} - No suitable crate identified...".format(self.name))

                self.hasDoneToday("job")
                #self.incAttrFrac("TEC", 0.8, 10)

            return()

        def workNightAI(self):
            # replate hull / Khelara
            if self.getJob().ID == "JOB_Repair":
                # check conditions
                if self.hasDoneToday("night_job"):
                    return(False)
                if self.getAttr("TEC") < 15:
                    return(False)
                # replate
                result = GAME.ship.replate()
                if not result:
                    return(False)
                # add TEC
                sInc = GAME.jobDB["JOB_Replate"].skillUp.get("TEC", [0,0])[0]
                sMax = GAME.jobDB["JOB_Replate"].skillUp.get("TEC", [0,0])[1]
                self.incAttrFrac("TEC", sInc, sMax)
                # report
                GAME.reports.append(["HULL REPAIR REPORT", "{} has initiated a {}Ship Replating{} from Ship Cargo during your overnight stay.\n\nPlate slots have been filled and suitable plates have been swapped in from Cargo.".format(self.name, C_HI_B, C_HI_E), ["Continue"], "08_items/ico {}.png".format(result.ID), "orange"])   

            # crew care / Moira
            # CR-2101 START
            if self.getJob().ID == "JOB_Wellness":
                for actor in GAME.crew:
                    actor.setAttr("STS", max(0, actor.getAttr("STS")-20))
                    actor.setAttr("MOD", min(100, actor.getAttr("MOD")+10))
                    GAME.consoleWrite("Wellness - {} STS {} MOD {}".format(actor.name, actor.getAttr("STS"), actor.getAttr("MOD")))

                    if actor == vee: 
                        actor.setAttr("ENE",100) 
                GAME.reports.append(["CREW RELAXATION", "Having received top Medical Care and Wellness Support from Moira, the crew's Stress and Mood have been improved...".format(), ["Continue"], "05_gui/ico_gui_heart.png", "blue"])  
            # CR-2101 END

            return()

        def nextJob(self):
            if self.jobs == []:
                return(False)
            self.jobs = self.jobs[1:] + self.jobs[:1]
            return(True) 

        def getJob(self):
            return(GAME.jobDB[self.jobs[0]])

        def setJob(self, jobID):
            if jobID not in self.jobs:
                return()
            while self.jobs[0] != jobID: 
                self.nextJob()
            return()

        def giveFP(self,amount):
            self.tallyUp("FP Awarded", amount)
            self.fp += amount
            return()

        def getFavours(self, locID, mode="close"):
            fav = dict((k,GAME.favours[k])
                        for k in self.favours
                        if GAME.favours[k][3] == locID and eval (GAME.favours[k][4]))
            return(fav)

        def checkFavour(self, favour):
            if self.fp < GAME.favours[favour][0]:
                result = [False, 'You need more points for that favour...']
            elif favour in self.favours:
                result = [True, GAME.favours[favour][5]]
            else:
                result = [False, "I will not do such stuff."]

            return(result)
        
        def hasAchievement(self, achievement):
            for i in self.achievements:
                if i.lower() == achievement.lower():
                    return(True)
            return(False)

        #####################################################################
        #                ACTOR: Milking Sys
        #####################################################################
        def milkable(self):
            if self.milkMod == None:
                return(False)
            return(True)

        #####################################################################
        #                ACTOR: Items and Outfits
        #####################################################################

        def canWear(self, itemID):
            item = GAME.items[itemID]
            for k in self.attr.keys():
                if k in item.req.keys():
                    if self.attr[k] < item.req[k]:
                        return(False)
            return(True)

        def adjustProgOutfit(self): # make lewder outfit if conditions are met
            if self.hasDoneToday("adjust_outfit"):
                return()
            # clothing kit?
            if not self.hasItem("ITMClothingKit"):
                return(False)

            if self.hasDoneToday("ITMClothingKit"): # delay until night_passes
                return()

            # anything to do / more lewd accepted / paranoid check
            if self.getProgOutfit() == self.getProgOutfit("max"):
                return(False)

            # worn current lewdest at least 3 days
            if self.counters.get(self.getProgOutfit("available").ID, 0) < 3:
                return(False)

            # anything lewder to make left?
            if self.getProgOutfit("max") in self.wardrobe:
                return(False)

            # make clothing and add to wardrobe
            item = self.getProgOutfit("next")
            self.wardrobe.append(item)

            # CR-0986 START ======
            # reset progChoice to new lewdest
            self.progChoice = None
            # CR-0986 END =======
            
            # remove kit
            self.remItem("ITMClothingKit")
            return(True)

        def getProgOutfit(self, mode="available"): # available, next, max (accepted), less, more
            if mode == "max":
                progOutfit = [GAME.items[x] for x in self.progOutfits if self.canWear(x)][-1]
                return(progOutfit)

            if mode == "available":
                progOutfit = [GAME.items[x] for x in self.progOutfits if self.hasItem(x)][-1]
                return(progOutfit)

            if mode == "less":
                # entry checks 
                if self.uniform != "UID1_Free":
                    return(False)
                # current free Outfit 
                if self.progChoice:
                    progCurrent = self.progChoice 
                else:
                    progCurrent = self.getProgOutfit("available")

                # check lower exists
                progIndex = self.progOutfits.index(progCurrent.ID) 
                if progIndex > 0:
                    # return if exists
                    return(GAME.items[self.progOutfits[progIndex-1]] )
                else:
                    return(False)

            if mode == "more":
                # entry checks 
                if self.uniform != "UID1_Free":
                    return(False)
                # current free Outfit 
                progAvailable = self.getProgOutfit("available")
                if self.progChoice:
                    progCurrent = self.progChoice 
                else:
                    progCurrent = progAvailable

                # check higher exists
                progIndex = self.progOutfits.index(progCurrent.ID) 
                progIndexAvailable = self.progOutfits.index(progAvailable.ID)

                if progIndex < progIndexAvailable:
                    # return if exists
                    return(GAME.items[self.progOutfits[progIndex+1]] )
                else:
                    return(False)

            # next in upgrade path
            best = self.getProgOutfit("available")
            if best.ID == self.progOutfits[-1]:  # last one reached?
                return(best)
                
            i = self.progOutfits.index(best.ID)
            if not self.canWear(self.progOutfits[i+1]):
                return(best)
            return(GAME.items[self.progOutfits[i+1]])


        def getSlot(self, slotID):
            slot = [i for i in self.outfit+self.accessory if i.slot == slotID]
            if slot:
                return(slot[0])
            return(False)


        def meetsReq(self, attr, reqAttr):
            # Python 3.x Migration
            if attr in reqAttr:
                if self.attr[attr] < reqAttr[attr]:
                    return(False)
            return(True)

        def refuseItem(item):
            for k in item.req.keys():
                if self.attr[k] < item.req[k]:
                    return(k)
            return(False)

        def getNaked(self):
            self.outfit = []
            self.accessory = []
            return()

        def passItem(self, ID, recipient, mode = "inventory"):
            self.remItem(ID)
            recipient.addItem(ID, mode)
            return()

        def addItem(self, ID, mode = "inventory"):
            if mode == "inventory":
                self.inventory.append(GAME.items[ID])
            elif mode == "wardrobe":
                if GAME.items[ID] not in self.wardrobe:
                    self.wardrobe.append(GAME.items[ID])
            return()

        def remItem(self, ID):
            if GAME.items[ID] in self.inventory:
                self.inventory.remove(GAME.items[ID])
                return(True)
            return(False)

        def hasItem(self,ID, amount = 1):
            if ID not in [x.ID for x in GAME.items.values()]:
                return(False)
            if (self.inventory + self.wardrobe).count(GAME.items[ID]) >= amount:
                return (True)
            return(False)

        def getActivityOutfit(self):
            outfitID = False
            for i in self.activities:
                if eval(i[0]):
                    outfitID = i[3]
            return(outfitID)

        def uniformOK(self, uniformID, keys=False):
            uniformOrder = GAME.uniformDB[uniformID]
            # special cases
            if uniformID == "UID4_Krell" and GAME.getStarSys().race == "Krell":
                return(True)
            # attributes to check 
            if not keys:
                keys = uniformOrder.req.keys()             
            result = all([self.meetsReq(k, uniformOrder.req) for k in keys])      
            return(result)

        def getDressed(self, mode = "auto"): # only works for crew, auto/no_nude
            log = []
            # loc override ?
            if self.getLocOverride() and mode == "auto": # no autonomy 
                return()
            if self in [bb_2]:
                return()

            # activity outfit?
            if self in GAME.crew:
                outfit = self.getActivityOutfit()
                uniformID  = GAME.uniformDB[self.uniform].itemID
                # autonomy? 
                if uniformID == "ITMUniformFree":
                    # get maximum lewd available progressive free uniform if data available
                    if self.progChoice:
                        self.putOn(self.progChoice.ID)
                        log.append(self.progChoice.ID)
                    else:
                        self.putOn(self.getProgOutfit("available").ID)
                        log.append(self.getProgOutfit("available").ID)
                # nude activity?                    
                elif uniformID == "ITMUniformNude" and mode != "no_nude":
                    self.getNaked()
                else:
                    self.putOn(uniformID)
                    GAME.consoleWrite("{} putOn({}) triggered".format(self.name, uniformID))
                #elif outfit == False:
                #    self.getNaked()    

            return(log)

        def putOn(self, itemID, mode = "autonomy"):
            item = GAME.items[itemID]
            if item not in self.wardrobe: 
                return(False) 
            # accessory?
            if item.slot == "plug":
                self.accessory.append(item)
                return(True)

            # outfit?
            # exception processing / progressive outfits
            if mode == "autonomy":
                if self.ID == "ACTTRIS" and itemID == "ITMOutfitValkora":
                    if self.progChoice:
                        itemID = self.progChoice.ID
                    else:
                        itemID = self.getProgOutfit("available").ID
            # apply uniform
            if itemID == False:
                self.getNaked()
                return()
            item = GAME.items[itemID]
            for i in self.outfit:
                if i.slot == item.slot:
                    self.takeOff(i.ID)
            self.outfit.append(item)
            return()

        def takeOff(self, itemID):
            item = GAME.items[itemID]
            if item not in self.accessory+self.outfit:
                return(False)
            for i in self.outfit:
                if i.ID == item.ID:
                    self.outfit.remove(item)
            for i in self.accessory:
                if i.ID == item.ID:
                    self.accessory.remove(item)
            return()

        def wears(self, itemID):
            for i in self.outfit+self.accessory:
                if i.ID == itemID:
                    return(True)
            return(False)

        def isNaked(self):
            if self.getSlot("body"):
                return(False)
            return(True)

        def isShaved(self):
            if "shaved" in self.tags:
                return(True)
            return(False)

        def isPlugged(self):
            for i in self.outfit+self.accessory:
                if "plug" in i.ID.lower():
                    return(i.ID)
            return(False)

        #####################################################################
        #                ACTOR: States etc.
        #####################################################################
        def getCharge(self):
            result = 1
            result  += GAME.day - self.tally.get("Last Done (Day)", 0)
            result = min(result, 3)
            if "ITMDrinkStellarShake" in self.doneToday:
                result = 3 # full / ready
            return(result)

        def setAttr(self, k, amount):
            found_in_mod = False
            # if exists in pet module
            if self.milkMod and k in self.milkMod.attr:
                self.milkMod.attr[k] = amount
                self.attrFrac[k] = float(amount)
            # if exist in milk module 
            elif self.petMod and k in self.petMod.attr:
                self.petMod.attr[k] = amount
                self.attrFrac[k] = float(amount)
            # else default attributes
            else:
                if k not in self.attr.keys():
                    self.attr[k] = 0
                if k not in self.attrFrac.keys():
                    self.attrFrac[k] = 0.0
                self.attr[k] = amount
                self.attrFrac[k] = float(amount)
            return()

        def getAttr(self, k):
            # check Pet Module
            if getattr(self, "petMod", None) != None:
                if self.petMod.getAttr(k):
                    return(self.petMod.getAttr(k))
            # check Milking Module
            if getattr(self, "milkMod", None) != None:
                if self.milkMod.getAttr(k):
                    return(self.milkMod.getAttr(k))
            # paranoid regular attr
            if hasattr(self, "attr"):
                if k in self.attr.keys():
                    return(self.attr[k])
            return(0)

        def getAttrMax(self,k):
            if k in self.attrMax.keys():
                return(self.attrMax[k])
            return(100.0)

        def getAttrFrac(self, k):
            if k in self.attrFrac.keys():
                return(self.attrFrac[k])
            return(0)

        def incAttrFrac(self, attr, inc, limit=False): # fractional attribute increment
            if not limit:
                limit = self.getAttrMax(attr)

            if attr not in self.attrFrac.keys():
                self.attrFrac[attr] = 0.0

            limit = min(limit, self.getAttrMax(attr))
            inc = min(inc, limit-self.attrFrac[attr])
            inc = max(0, inc)
            self.attrFrac[attr] += inc
            round(self.attrFrac[attr],3)
            return(self.attrFrac[attr])

        def getRating(self):
            rating = {1: "Harmless", 2: "Mostly Harmless", 3:"Average" , 4: "Capable" , 5: "Proficient", 6: "Superior", 7:"Unparalleled", 8: "Paragon", 9: "Galactic Paragon"}[capValue(self.lvl,1,9)]
            return(rating)

        def tallyUp(self, k, val):
            if k not in self.tally.keys():
                self.tally[k] = val
            else:
                self.tally[k] += val

        def getTally(self,k):
            if k not in self.tally.keys():
                return(0)
            return(self.tally[k])

        #def meetsReq(self, req): # STUB
        #    return(True)

        def setActivities(self, ID):
            self.activities = GAME.activityDB[ID]
            return()

        def getActivity(self):
            activity = None
            for i in self.activities:
                if eval(i[0]):
                    return(i[2])
            return(activity)

        def getActivitySpritePos(self):
            activity = None
            for i in self.activities:
                if eval(i[0]):
                    if len(i) >= 3:
                        return(i[2]) # position data found in db
                    else:
                        return([0,0]) # default full screen sprite (lazy)
            return([0,0])

        def getActions(self):
            actions = []
            i = 0
            while i < len(self.actions):
                if eval(self.actions[i+1]):
                    actions.append(self.actions[i])
                i += 2
            return(actions)

        def hasDone(self,topic, n=1):
            if self.done.count(topic) >= n:
                return(True)
            else:
                self.done.append(topic)
                return(False)

        def hasDoneToday(self,topic, n=1):
            if self.doneToday.count(topic) >= n:
                return(True)
            else:
                self.doneToday.append(topic)
                return(False)

        def isAsleep(self):
            if self.getActivity() in ["sleep"]:
                return(True)
            return(False)

        #####################################################################
        #					NEW LOC OVERRIDE		
        #####################################################################

        def setLocOverride(self, locID, timeSlot = False):
            # check for pet module initialization
            if locID == "LOCPetwalk" and not self.petMod:
                self.petMod = PetModule(self)

            if timeSlot:
                self.locOverrideReg[timeSlot] = locID # CR-0509
            else:
                self.clearLocOverride() # clear registry
                self.locOverrideReg[99] = locID
            return()

        def getLocOverride(self):
            if 99 in self.locOverrideReg.keys():
                return(self.locOverrideReg[99])
            elif GAME.hour in self.locOverrideReg.keys():
                return(self.locOverrideReg[GAME.hour])
            return(False)

        def clearLocOverride(self, timeSlot=False):
            if timeSlot:
                if timeSlot in self.locOverrideReg.keys():
                    del self.locOverrideReg[timeSlot]
                    return(True)
            else:
                self.locOverrideReg = {}
                return(True)
            return(False)

        #####################################################################
        #					MISC		
        #####################################################################



        def isAt(self):
            if self.getLocOverride():
                return(self.getLocOverride()) 
            for i in self.activities:
                if eval(i[0]):
                    return(i[1])
            return(False)

        def isHere(self):
            # CR-0969 START =====
            locID = GAME.getLoc().ID
            if self.isAt() == locID:
                return(True)
            # if self.getLocOverride():
            #     if self.getLocOverride() == locID:
            #         return(True)
            #     return(False)
            # for i in self.activities:
            #     if i[1] == locID:
            #         if eval(i[0]):
            #             return(True)
            # CR-0969 END =======
            return(False)

        def updateAI(self, mode = "standard"):
            # Overrides 
            if self.getLocOverride():
                self.getDressed()
                return()

            # Sleeping
            if self.getActivity() == "sleep":
                if "sleeping" not in self.status:
                    self.status.append("sleeping")
            elif "sleeping" in self.status:
                self.status.remove("sleeping")

            # Location


            # Attribute management
            if not self.hasDoneToday("impact_uniform"):
                uniform = GAME.uniformDB[self.uniform]
                for k in [x for x in uniform.effect.keys() if x in self.attr.keys()]:
                    impact = uniform.effect[k]

                    # EXH level attenuating effect on MOD drops
                    if "EXH" in uniform.req.keys():
                        if k == "MOD":
                            impact += self.attr["EXH"]*2

                    self.attr[k] = capValue(self.attr[k]+impact,0,100)


            # Job 
            self.workAI()
            # khalera repair adjustments in engine room 
            if khelara in GAME.crew:
                GAME.repQueue = [x for x in GAME.repQueue if "AERepEngine" not in x]
                GAME.repMisses = [x for x in GAME.repMisses if "AERepEngine" not in x]
             
            # Dress
            self.adjustProgOutfit() # CR-0986
            self.getDressed()

            # Other
            if "facial" in self.tags:
                self.tags.remove("facial")

            return()

        def isWalked(self):
            if self.getLocOverride() == "LOCPetwalk":
                return(True)
            return(False)



    #####################################################################
    #              PET AND MILK MODS
    #####################################################################
    class PetModule():
        def __init__(self, actor):
            self.actor = actor 
            self.commands = {} # command: [LOCID, ...]
            self.attr = {"REP": 0}
            self.tags = []

        def getAttr(self, k):
            return(self.attr.get(k, 0))

        def setAttr(self, k, n):
            self.attr[k] = n
            return()

        def canDo(self, command):
            if command in self.commands:
                return(True)
            return(False)

        def getCommands(self, locID=None):
            if not locID:
                locID = GAME.getLoc().ID
            commands = []
            for k in self.commands:
                if locID in self.commands[k]:
                    commands.append(k)
            return(commands)

        def addCommand(self, command, locID):
            if not self.commands.get(command, False):
                self.commands[command] = [locID]
            elif locID not in self.commands[command]:
                self.commands[command].append(locID)
            return()

        def remCommand(self, command, locID):
            if not self.commands.get(command, False):
                return()
            elif locID in self.commands[command]:
                self.commands[command].remove(locID)
            return()

    class MilkingModule(): # xxx
        def __init__(self, actor, breastCapa=500, breastMax=900, skill=0):
            self.actor = actor
            self.actor.attr["MLK"] = 1 # DEPRECATED
            self.actor.attrMax["MLK"] = 50 # DEPRECATED
            self.attr = {}
            # self.skills = {"MLK": skill}
            self.lactin = 0
            self.breastCapa = breastCapa
            self.breastMax = breastMax
            self.milked = 0.0
            self.milk = {}
            self.milk["left"] = 0.0
            self.milk["right"] = 0.0
            self.yieldToday = 0.0
            self.yieldTotal = 0.0

        def getAttr(self, k):
            if not self.attr:
                return(0)
            return(self.attr.get(k, 0))

        def setAttr(self, k, n):
            self.attr[k] = n
            return()

        ###########################################################################
        #   MILKING SYSTEM
        ###########################################################################

        def feedLactin(self):
            if self.lactin < 100:
                self.lactin = min(100, self.lactin+100) # mod full fillup
                return(True)
            return(False)

        def milkRemaining(self):
            amount = int(actor.milkMod.milk["left"]+actor.milkMod.milk["right"])
            return(int(amount))

        def hasMilk(self):
            if sum(self.milk.values()) > 0.0:
                return(True)
            return(False)

        def milking(self, breast):
            if self.milk[breast] <= 0.0:
                    return(0.0)
            quantity = min(random.uniform(3.0, self.breastCapa/100.0+4.0),self.milk[breast])
            self.milk[breast] -= quantity
            self.yieldTotal += quantity
            self.yieldToday += quantity
            return(quantity)

        def produce(self):
            if self.lactin == 0:
                return()
            # produce milk
            la_factor = min(1.0,(self.lactin / 50.0)) # lactin
            sk_factor = float(self.actor.getAttr("MLK"))/100.0 # skill
            for s in ["left", "right"]:
                self.milk[s] = min(self.breastCapa/2.0, self.milk[s]+(self.breastCapa/2.0)*(la_factor*sk_factor+0.1))*random.uniform(0.8,1.2)
            self.lactin = max(0, self.lactin-50.0)

            # yieldCows += self.yieldToday
            # if "milked dry" in self.doneToday and self.yieldToday > 0:
                # self.skills["milk cow"] = min(self.talents["milk cow"],self.skills["milk cow"]+5)
                # if self.yieldToday > topCowYield:
                #     topCowYield = self.yieldToday
                #     topCow = self
            # reset
            self.yieldToday = 0
            return()
