def _compatibility(self, unitobj, tested=None): # print(f"{self.name} testing against {unit.id} {unit.name}") if tested is None: tested = [] if unitobj.uniqueid in tested: return True for r in self.reqs: if not r.test(unitobj): return False # These need only be true for the first shape in the collection # ... except I'm getting bugs from this if self.landok: if unitobj.aquatic != -1: return False if self.uwok: if unitobj.aquatic != -1 and unitobj.amphibian != -1 and unitobj.pooramphibian != -1: return False if self.reqmage and len(tested) == 0: haspaths = False for path in ["F", "A", "W", "E", "S", "D", "N", "B"]: if hasattr(unitobj, path) and getattr(unitobj, path, 0) > 0: haspaths = True break if not haspaths: return False # I cannot support shrink or growhp if unitobj.shrinkhp != -1: return False if unitobj.growhp != -1: return False wpnmod = utils.weaponmods.get(self.weaponmod, utils.weaponmods.get("Do Nothing")) if not wpnmod.compatibilityunit(unitobj): return False tested.append(unitobj.uniqueid) # Must recursively check for eligibility on all subshapes too for x in [ "shapechange", "firstshape", "secondshape", "secondtmpshape", "forestshape", "plainshape", "foreignshape", "homeshape", "domshape", "notdomshape", "springshape", "summershape", "autumnshape", "wintershape", "landshape", "watershape" ]: if hasattr(unitobj, x): uid = getattr(unitobj, x) if uid not in tested and uid > 0: unittottest = unitinbasedatafinder.get(uid) if not self._compatibility(unittottest, tested=tested): return False return True
def toUnitBaseData(self) -> unitinbasedatafinder.UnitInBaseDataFinder: #self._init() if self.baseunit is not None: retval = unitinbasedatafinder.get(self.baseunit) if self.clearweapons != 0: retval.weapons = [] else: retval = unitinbasedatafinder.UnitInBaseDataFinder() for wpnid in self.addweapons: retval.weapons.append(weapon.get(wpnid)) for attrib, value in self.setcmds: setattr(retval, attrib, value) retval.additionalmodcmds += f"\n-- Generated from NewUnit {self.name}\n" retval.additionalmodcmds += "\n".join(self.rawcmds) retval.additionalmodcmds += "\n" retval.uniqueid = f"newunit-{self.name}" return retval
def generateUnit(parentobj, numtogenerate, spell, secondaryeffect, actualpowerlvl): # When dealing with nextspell chains, we need to update #details on the first one (the one the players see) firstspell = spell while True: if firstspell.prevspell is None: break firstspell = firstspell.prevspell output = "" montag = MontagBuilder() montag.makedummymonster = parentobj.makedummymonster montag.makebattledummymonster = parentobj.makebattledummymonster dummynames = parentobj.dummymonsternames.get(spell.path1, []) if len(dummynames) > 0: montag.dummymonstername = naming.parsestring(random.choice(dummynames)) for n in range(0, numtogenerate): # Find a unit to use unittouse = None effectpool = [] if parentobj.usefixedunitid > 0: unittouse = unitinbasedatafinder.get(parentobj.usefixedunitid) minnreff = 1 costper = 1.0 chosensummoneffect = None # Unitmod elif parentobj.selectunitmods is not None and len( parentobj.selectunitmods) > 0: for selectunitmodname in parentobj.selectunitmods: realunitmod = utils.unitmods[selectunitmodname] # this unit mod should instead be a picker for a unit to grab # start by building a pool of spelleffects that we could use for effname, effect in utils.spelleffects.items(): if effect.effect in parentobj.effectnumberforunits: unitid = effect.damage # no montags if unitid < 0: continue #unitobj = unitinbasedatafinder.get(unitid) if parentobj.restrictunitstospellpaths > 0: if not (effect.paths & spell.path1): continue if effect.secondarypathchance >= 85 and not ( effect.secondarypaths & spell.path2): continue if realunitmod.compatibilityWithSpellEffect(effect): effectpool.append(effect) random.shuffle(effectpool) generateokay = False # Loop to deal with deadend failures # this is typically a bad unit choice for which there is no way to adjust # the power up/down with legal secondaries secondary = None print( f"Spell paths for this generation are {spell.path1} and {spell.path2}" ) print( f"Effect pool contains {len(effectpool)} effects from {parentobj.selectunitmods}" ) while 1: if len(effectpool) > 0: chosensummoneffect = effectpool.pop(0) print( f"Consider effect {chosensummoneffect}, {len(effectpool)} remain after this " ) unittouse = chosensummoneffect.getUnit() if chosensummoneffect.damage < 0: print( f"Discard effect {chosensummoneffect}: it makes montag {chosensummoneffect.damage}" ) continue elif unittouse is None: print( f"ERROR: {parentobj.name} with paths {spell.path1} and {spell.path2} found no valid unit summon" ) return None if (parentobj.unitmodlist is not None or len( parentobj.allowedunitmods) > 0) and unittouse is not None: # If rollSpell enforces a secondary effect (unlikely), use that if secondaryeffect is not None and secondaryeffect.name != "Do Nothing" and len( secondaryeffect.unitmod) > 0: realunitmod = utils.unitmods[secondaryeffect.unitmod] unitobj = unittouse secondary = utils.unitmodToSecondary(realunitmod, fallback=True) if not _CanUseUnitAndSecondaryCombo( parentobj, unitobj, secondary, realunitmod, spell, chosensummoneffect, actualpowerlvl): print( f"Forced unitmod {realunitmod.name} not allowed with unit {unittouse}" ) unittouse = None continue output = f"-- {parentobj.name} applied secondary effect unitmod {realunitmod.name} " \ f"to {unittouse.uniqueid}\n\n" + output else: # Find a secondary effect to use for this creature if parentobj.secondaryeffectchance is not None and random.random( ) * 100 > parentobj.secondaryeffectchance: realunitmod = utils.unitmods["Do Nothing"] secondary = utils.unitmodToSecondary(realunitmod, fallback=True) else: # shallow copy unitmodlist = parentobj.allowedunitmods[:] if parentobj.unitmodlist is not None: unitmodlist += utils.unitmodlists[ parentobj.unitmodlist] random.shuffle(unitmodlist) bad = False while 1: if len(unitmodlist) == 0: print(f"No valid unitmod for unit {unittouse}") bad = True break unitmodtouse = unitmodlist.pop(0) realunitmod = utils.unitmods[unitmodtouse] # Find the parent secondary effect to test it for paths secondary = utils.unitmodToSecondary(realunitmod, fallback=True) isvalid = _CanUseUnitAndSecondaryCombo( parentobj, unittouse, secondary, realunitmod, spell, chosensummoneffect, actualpowerlvl) if not isvalid: print( f"... {len(unitmodlist)} unitmods remain..." ) continue print( f"Successfully picked secondary {secondary.name} for {unittouse}" ) output = f"-- {parentobj.name} applied non-secondary effect unitmod " \ f"{realunitmod.name} to {unittouse.uniqueid}\n\n" + output break if bad: # There was no unitmod, start by finding another chassis unittouse = None continue if numtogenerate == 1: unitcode = realunitmod.applytounit(None, unittouse) if parentobj.modulegroup is None: output = unitcode + "\n\n" + output else: parentobj.moduletailingcode += unitcode + "\n" parentobj.lastunitid = realunitmod.lastparentid parentobj.lastunitname = realunitmod.lastunitname if parentobj.usefixedunitid is None or parentobj.usefixedunitid < 0: if "SINGLERANDOMCREATURENAME" in firstspell.details: firstspell.details = firstspell.details.replace( "SINGLERANDOMCREATURENAME", realunitmod.lastunitname, 1) else: firstspell.details += f"The creature for this spell is always a {realunitmod.lastunitname}." generateokay = True else: # work out the effective fatigue cost scaleportion = chosensummoneffect.nreff // 1000 minnreff = (scaleportion * chosensummoneffect.pathlevel ) + chosensummoneffect.nreff % 1000 basecostper = chosensummoneffect.fatiguecost / minnreff # Secondary modification fatiguemult = 1.0 if secondary is not None: for attrib, val in secondary.multcommands: if attrib == "fatiguecost": fatiguemult *= val # The chassis/paths split for commanders if chosensummoneffect.chassisvalue is not None and secondary is not None: chosensummoneffect.calcchassisvalues() magiccostmod = chosensummoneffect.magicvaluepercent * basecostper * secondary.magicpathvaluescaling * fatiguemult chassiscostmod = chosensummoneffect.chassisvaluepercent * basecostper * ( fatiguemult - 1.0) costper = int( basecostper * (chosensummoneffect.magicvaluepercent + chosensummoneffect.chassisvaluepercent) + magiccostmod + chassiscostmod) costper += secondary.fatiguecostpereffect elif secondary is None: costper = basecostper else: costper = basecostper * fatiguemult costper += secondary.fatiguecostpereffect basepower = chosensummoneffect.power montag.add(unittouse, secondary, costper) generateokay = True if realunitmod.lastparentid is not None and numtogenerate == 1: output = output.replace("UNITID", str(realunitmod.lastparentid)) output = f"-- {parentobj.name} generated with unitid " \ f"{realunitmod.lastparentid}\n\n" + output if generateokay: break return (output, montag)
def applytounit(self, spell, u, extrashapechain=None, additionals_firstshape=None, spelleffect=None, actualpowerlvl=None, secondaryeffect=None): """Apply this effect to the given unit object, returning mod code for the new unit. spell can be None. If given, spell.damage is set to the correct unit ID for the new unit. extrashapechain should not be set manually as it is used to track wounded shapes and make them all join together neatly additionals_firstshape should be a dict of {modcommand:value} that is appended to the first shape ONLY. So far this is used for things like adding units to montags and giving them a montagweight. The modcommand given should INCLUDE A HASH. """ if extrashapechain is None: extrashapechain = {} self.lastunitAIScore = 0 u.descr += " " + self.descr u.descr = u.descr.replace("CREATURE", u.name) u.descr = naming.parsestring(u.descr) u.name = self.nameprefix + " " + u.name u.name = u.name.strip() self.lastunitname = u.name out = "" # Weapon mod wpnmod = utils.weaponmods[self.weaponmod] wpnreplacements, wpnoutput = wpnmod.apply(u) for wpn in u.weapons: if wpn.uniqueid in wpnreplacements: wpn.origid = wpnreplacements[wpn.uniqueid] out += wpnoutput # Event set derived attributes, for things like summoning magicgen creatures or actual event set things if self.eventset != "" and self.attributeforrandomunit != "": realeventset = utils.eventsets[self.eventset] # actualpowerlvl is 0 in all current cases - because commander spells do not have any research level # scaling that is not done through secondary effects eventsetdata = realeventset.formatdata(spelleffect, spell, 0, None, actualpowerlvl) if eventsetdata is None: return None out = eventsetdata + "\n" + out setattr(u, self.attributeforrandomunit, realeventset.lastunitid) u.additionalmodcmds += f"#{self.attributeforrandomunit} {realeventset.lastunitid}\n" if utils.MONSTER_ID >= 9000: raise ValueError( "New Monster ID is too high for Dominions! Consider changing starting ID or lowering montag sizes." ) out += f"-- Modified unit {u.uniqueid} with unitmod {self.name}\n" out += f"#newmonster {utils.MONSTER_ID}\n" # Update the spell to summon the new creature # needless to say, don't do this if the current monster is a secondshape something caused by running in recursive mode # or the spell will be left summoning the last thing in the secondshape chain if len(extrashapechain) == 0: if spell is not None and spell.effect % 1000 in [ 1, 21, 37, 38, 43, 50, 89, 93, 119, 137, 54, 130, 62 ]: print( f"unitmod {self.name}: is first pass, fixed spell.damage -> {utils.MONSTER_ID}" ) spell.damage = utils.MONSTER_ID else: self.lastparentid = utils.MONSTER_ID utils.MONSTER_ID += 1 if u.origid is not None: out += f"#copystats {u.origid}\n" if u.newunit is None or u.newunit.spr1 is None: out += f"#copyspr {u.origid}\n" out += "#descr {}{}{}\n".format('"', u.descr, '"') out += "#name {}{}{}\n".format('"', u.name, '"') out += f"#clearweapons\n" # New unit stuff should go here out += u.additionalmodcmds + "\n" if len(self.addweapons) > 0: for wpn in self.addweapons: #out += f"#weapon {wpn}\n" u.weapons.append(weapon.get(wpn)) for wpn in u.weapons: out += f"#weapon {wpn.origid}\n" # Disable transformation if hasattr(u, "transformation"): if u.transformation != 0: out += "#transformation 0\n" for param in self.params: if hasattr(self, param): paramv = getattr(self, param) if paramv != 0: if not hasattr(u, param): print( f"Unit {u.name} didn't have param {param} when affected by unitmod {self.name}, assumed 0" ) setattr(u, param, 0) newparamval = getattr(u, param) + paramv # do not mess with certain values of morale, they should be done with set commands only if param == "mor": oldval = getattr(u, param) if oldval == 30: newparamval = 30 if oldval == 50: newparamval = 50 setattr(u, param, newparamval) modcmd = flags_to_mod_cmds.get(param, param) if modcmd not in argless_cmds: out += f"#{modcmd} {newparamval}\n" else: out += f"#{modcmd}\n" for param, val in self.setcommands: modcmd = flags_to_mod_cmds.get(param, param) if modcmd not in argless_cmds: if isinstance(val, str): out += "#{} {}{}{}\n".format(modcmd, '"', val, '"') else: out += f"#{modcmd} {val}\n" else: out += f"#{modcmd}\n" for param, val in self.multcommands: paramv = getattr(u, param) newparamval = int(val * paramv) setattr(u, param, newparamval) modcmd = flags_to_mod_cmds.get(param, param) out += f"#{modcmd} {newparamval}\n" aiscore = u.calcSummonAIScore() self.lastunitAIScore += aiscore print( f"AIscore for this unit is {aiscore}, total now {self.lastunitAIScore}" ) secondshapeextra = "" # Second shape needs to propagate for x in [ "shapechange", "firstshape", "secondshape", "secondtmpshape", "forestshape", "plainshape", "foreignshape", "homeshape", "domshape", "notdomshape", "springshape", "summershape", "autumnshape", "wintershape", "landshape", "watershape" ]: if hasattr(u, x): uid = getattr(u, x) oldscore = copy(self.lastunitAIScore) if uid not in extrashapechain and uid > 0: extrashapechain[uid] = copy(utils.MONSTER_ID) print( f"UID {uid} is a {x} of {u.name}, applying to that too..." ) secondshapeextra += self.applytounit( spell, unitinbasedatafinder.get(uid), extrashapechain, spelleffect=spelleffect, actualpowerlvl=actualpowerlvl, secondaryeffect=secondaryeffect) if uid in extrashapechain and uid > 0: # update this creature's secondshape or whatever with the new creature out += f"#{x} {extrashapechain[uid]}\n" # only secondshape counts towards AI scores if x != "secondshape": self.lastunitAIScore = oldscore if additionals_firstshape is not None: for modcmd, val in additionals_firstshape.items(): out += f"{modcmd} {val}\n" # Special case: Do nothing should firstshape to the original unit # This makes montags built with Do Nothing-modified units quickly transform to their normal version # which means double click correctly selects all of them amongst other things # ...but not if this is a newunit, as u.id will instead be undefined or whatever we copystats-ed from originally if self.name == "Do Nothing" and u.newunit is None: out += f"#firstshape {u.id}\n" if u.newunit is not None: if u.newunit.spr1 is not None: utils.spritedependencies.add(u.newunit.spr1) if u.newunit.spr2 is not None: utils.spritedependencies.add(u.newunit.spr2) out += "#end\n\n" out += secondshapeextra return out
def generateNewIndepspellsModFile(f, baseucrc): indepspellscontent = f"--{baseucrc}\n-- This file (indepspells.dm) is transcluded into generated " \ f"mod files when MagicGen clears vanilla generic spells.\n" \ f"-- The above is the CRC of the BaseU.csv file used to generate this file " \ f"- this is saved as generating this from scratch takes several minutes\n" for unitid in range(0, 3500): if unitid % 100 == 0: _writetoconsole(f"Beginning indepspells for unit {unitid}...\n") try: unitobj = unitinbasedatafinder.get(unitid) except Exception: print(f"Error trying to get uid {unitid}") print(traceback.format_exc()); continue # leave illwinter-set values intact if hasattr(unitobj, "indepspells"): if int(getattr(unitobj, "indepspells")) > 0: continue indeplevel = None if hasattr(unitobj, "startdom"): if int(getattr(unitobj, "startdom")) > 0: indeplevel = 7 if indeplevel is None: totalmagiclevel = 0 for path in ["F", "A", "W", "E", "S", "D", "N", "B"]: if getattr(unitobj, path, "") <= 0: continue pathval = int(getattr(unitobj, path, 0)) if pathval > 0: totalmagiclevel += pathval print(f"Unit {unitid} has {path} level {pathval}") randomaverage = 0 for n in range(1, 5): mask = f"mask{n}" mask = int(getattr(unitobj, mask, 0)) randomchance = int(getattr(unitobj, f"rand{n}", 0)) nbr = max(0, int(getattr(unitobj, f"nbr{n}"))) if randomchance > 0: randomaverage += nbr * (randomchance / 100) print( f"Unit {unitid} has random {n} giving {nbr} paths with {randomchance}% of success") print(f"\ttotal random path value now {randomaverage}") totalmagiclevel += math.floor(randomaverage) if totalmagiclevel >= 3: indeplevel = 5 elif totalmagiclevel == 2: indeplevel = 4 elif totalmagiclevel == 1: indeplevel = 3 if indeplevel is not None: indepspellscontent += f"#selectmonster {unitid}\n" indepspellscontent += f"#indepspells {indeplevel}\n" indepspellscontent += f"#end\n" with open("indepspells.dm", "w") as indepspells: indepspells.write(indepspellscontent) f.write(indepspellscontent)