Skip to content

Instantly share code, notes, and snippets.

@IndigoFenix
Last active September 22, 2015 14:43
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save IndigoFenix/c487191fa3a857820b0d to your computer and use it in GitHub Desktop.
Save IndigoFenix/c487191fa3a857820b0d to your computer and use it in GitHub Desktop.
Enables soul manipulation and constructing creatures in reactions.
creature_sscc_examples
[OBJECT:CREATURE]
Special classes for use with the SoulShuffle and ConstructCreature scripts:
DFHACK_CONSTRUCTCREATURE_CASTECOLORS - Uses the caste color system to make creatures show up with the color of their construction material. See below.
DFHACK_CONSTRUCTCREATURE_ITEMCORPSE - Causes the creature's itemcorpse to take on the same material as its first defined material.
DFHACK_SOULSHUFFLE_SCUTTLE_ON_REMOVAL - The creature will die when its soul is removed.
DFHACK_SOULSHUFFLE_VANISH_ON_REMOVAL - The creature will vanish into smoke when its soul is removed.
DFHACK_SOULSHUFFLE_CANNOT_TARGET - The creature cannot be targeted by SoulShuffle reactions.
DFHACK_SOULSHUFFLE_NO_SOUL_NEEDED - The creature will not go catatonic when its soul is removed. (It will still not be able to learn.)
DFHACK_SOULSHUFFLE_RETAIN_LOYALTY - The creature will retain its original loyalty even if its soul is replaced or removed. Use this for 'soul container' creatures.
DFHACK_SOULSHUFFLE_ONE_SOUL_LIMIT - The creature cannot store more than one soul at a time.
[CREATURE:IMP_SOUL]
[CREATURE_CLASS:DFHACK_SOULSHUFFLE_VANISH_ON_REMOVAL] -- Causes the creature to vanish like a bogeyman when the last soul is removed
[CREATURE_CLASS:DFHACK_SOULSHUFFLE_ONE_SOUL_LIMIT] -- Prevents you from adding extra souls to the creature.
[DESCRIPTION:A soul manifested as a small, flying creature.]
[NAME:soul imp:soul imps:soul imp]
[CASTE_NAME:soul imp:soul imps:soul imp]
[CREATURE_TILE:'i'][COLOR:6:0:1]
[CANOPENDOORS]
[PREFSTRING:strange features]
[BODY:HUMANOID_NECK:TAIL:2EYES:2EARS:NOSE:HEART:GUTS:ORGANS:HUMANOID_JOINTS:THROAT:NECK:SPINE:BRAIN:SKULL:MOUTH:TONGUE:4FINGERS:4TOES:FACIAL_FEATURES:GENERIC_TEETH_WITH_LARGE_EYE_TEETH:RIBCAGE]
[BODY_DETAIL_PLAN:STANDARD_MATERIALS]
[BODY_DETAIL_PLAN:STANDARD_TISSUES]
[BODY_DETAIL_PLAN:VERTEBRATE_TISSUE_LAYERS:SKIN:FAT:MUSCLE:BONE:CARTILAGE]
[USE_MATERIAL_TEMPLATE:NAIL:NAIL_TEMPLATE]
[USE_TISSUE_TEMPLATE:NAIL:NAIL_TEMPLATE]
[TISSUE_LAYER:BY_CATEGORY:FINGER:NAIL:FRONT]
[TISSUE_LAYER:BY_CATEGORY:TOE:NAIL:FRONT]
[SELECT_TISSUE_LAYER:HEART:BY_CATEGORY:HEART]
[PLUS_TISSUE_LAYER:SKIN:BY_CATEGORY:THROAT]
[TL_MAJOR_ARTERIES]
[BODY_DETAIL_PLAN:STANDARD_HEAD_POSITIONS]
[BODY_DETAIL_PLAN:HUMANOID_HEAD_POSITIONS]
[BODY_DETAIL_PLAN:HUMANOID_RIBCAGE_POSITIONS]
[BODY_DETAIL_PLAN:HUMANOID_RELSIZES]
[USE_MATERIAL_TEMPLATE:SINEW:SINEW_TEMPLATE]
[TENDONS:LOCAL_CREATURE_MAT:SINEW:200]
[LIGAMENTS:LOCAL_CREATURE_MAT:SINEW:200]
[HAS_NERVES]
[NO_DRINK][NO_EAT][NO_SLEEP]
[USE_MATERIAL_TEMPLATE:GOO:GOO_TEMPLATE]
[BLOOD:LOCAL_CREATURE_MAT:GOO:LIQUID]
[BODY_SIZE:0:0:6000]
[BODY_APPEARANCE_MODIFIER:HEIGHT:90:95:98:100:102:105:110]
[BODY_APPEARANCE_MODIFIER:BROADNESS:90:95:98:100:102:105:110]
[ATTACK:BITE:CHILD_BODYPART_GROUP:BY_CATEGORY:HEAD:BY_CATEGORY:TOOTH]
[ATTACK_SKILL:BITE]
[ATTACK_VERB:bite:bites]
[ATTACK_CONTACT_PERC:100]
[ATTACK_PENETRATION_PERC:100]
[ATTACK_FLAG_EDGE]
[ATTACK_PREPARE_AND_RECOVER:3:3]
[ATTACK_PRIORITY:MAIN]
[ATTACK_FLAG_CANLATCH]
[ATTACK:PUNCH:BODYPART:BY_TYPE:GRASP]
[ATTACK_SKILL:GRASP_STRIKE]
[ATTACK_VERB:punch:punches]
[ATTACK_CONTACT_PERC:100]
[ATTACK_PREPARE_AND_RECOVER:3:3]
[ATTACK_FLAG_WITH]
[ATTACK_PRIORITY:MAIN]
[ATTACK:KICK:BODYPART:BY_TYPE:STANCE]
[ATTACK_SKILL:STANCE_STRIKE]
[ATTACK_VERB:kick:kicks]
[ATTACK_CONTACT_PERC:100]
[ATTACK_PREPARE_AND_RECOVER:4:4]
[ATTACK_FLAG_WITH]
[ATTACK_PRIORITY:SECOND]
[ATTACK_FLAG_BAD_MULTIATTACK]
[ATTACK:SCRATCH:CHILD_TISSUE_LAYER_GROUP:BY_TYPE:GRASP:BY_CATEGORY:FINGER:NAIL]
[ATTACK_SKILL:GRASP_STRIKE]
[ATTACK_VERB:scratch:scratches]
[ATTACK_CONTACT_PERC:100]
[ATTACK_PENETRATION_PERC:100]
[ATTACK_FLAG_EDGE]
[ATTACK_PREPARE_AND_RECOVER:3:3]
[ATTACK_PRIORITY:SECOND]
[EQUIPS]
[ALL_ACTIVE]
[NO_FEVERS]
[HOMEOTHERM:10095]
[APPLY_CREATURE_VARIATION:STANDARD_BIPED_GAITS:900:730:561:351:1900:2900] 25 kph
[APPLY_CREATURE_VARIATION:STANDARD_CLIMBING_GAITS:2990:2257:1525:731:4300:6100] 12 kph
[APPLY_CREATURE_VARIATION:STANDARD_SWIMMING_GAITS:2990:2257:1525:731:4300:6100] 12 kph
[APPLY_CREATURE_VARIATION:STANDARD_CRAWLING_GAITS:2990:2257:1525:731:4300:6100] 12 kph
[NATURAL_SKILL:CLIMBING:15]
[SWIMS_INNATE]
[SET_TL_GROUP:BY_CATEGORY:ALL:SKIN]
[TL_COLOR_MODIFIER:RED:1]
[TLCM_NOUN:skin:SINGULAR]
[SET_TL_GROUP:BY_CATEGORY:EYE:EYE]
[TL_COLOR_MODIFIER:BLACK:1]
[TLCM_NOUN:eyes:PLURAL]
[CASTE:DEFAULT_FEMALE]
[FEMALE]
[CASTE:DEFAULT_MALE]
[MALE]
[CASTE:DEFAULT]
[SELECT_CASTE:ALL]
[SELECT_MATERIAL:GOO]
[MELTING_POINT:10000]
[CREATURE:GEM_SOUL]
[CREATURE_CLASS:DFHACK_CONSTRUCTCREATURE_ITEMCORPSE] -- Replaces the itemcorpse's material with the FIRST DEFINED MATERIAL in the creature. To simplify this, define both the main material and the tissue first.
[CREATURE_CLASS:DFHACK_CONSTRUCTCREATURE_CASTECOLORS] -- Uses the color of the FIRST DEFINED MATERIAL to determine the creature's caste. The castes must be defined properly, as they are here.
[CREATURE_CLASS:DFHACK_SOULSHUFFLE_SCUTTLE_ON_REMOVAL] -- Scuttles the creature when its last soul is removed.
[CREATURE_CLASS:DFHACK_SOULSHUFFLE_ONE_SOUL_LIMIT] -- Prevents you from adding extra souls to the creature.
[CREATURE_CLASS:DFHACK_SOULSHUFFLE_RETAIN_LOYALTY] -- Normally, a creature will take on the loyalty of its current soul. This tag prevents this, making it stay loyal to your civ.
[NAME:soul gem:soul gems:soul gem]
[CASTE_NAME:soul gem:soul gems:soul gem]
[DESCRIPTION:A tiny gem that contains a soul.]
[CREATURE_TILE:4][COLOR:0:0:1]
[PET]
[USE_MATERIAL_TEMPLATE:GEM_MATERIAL:STONE_TEMPLATE] -- This is the material that will be replaced by the GEM_MATERIAL in the reaction.
[TISSUE:GEM_TISSUE]
[TISSUE_NAME:gemstone:NP]
[TISSUE_MATERIAL:LOCAL_CREATURE_MAT:GEM_MATERIAL]
[RELATIVE_THICKNESS:1]
[STRUCTURAL][FUNCTIONAL][CONNECTS]
[TISSUE_SHAPE:LAYER]
[BODY:BASIC_1PARTBODY_THOUGHT]
[TISSUE_LAYER:BY_TYPE:UPPERBODY:GEM_TISSUE]
[IMMOBILE][NOPAIN][NONAUSEA][NO_EAT][NO_DRINK][NOBREATHE][NO_FEVERS][PARALYZEIMMUNE][FIREIMMUNE][NOEXERT][NOT_LIVING]
[BODY_SIZE:0:0:5]
[CASTE:00][CASTE_COLOR:0:0:0]
[CASTE:10][CASTE_COLOR:1:0:0]
[CASTE:20][CASTE_COLOR:2:0:0]
[CASTE:30][CASTE_COLOR:3:0:0]
[CASTE:40][CASTE_COLOR:4:0:0]
[CASTE:50][CASTE_COLOR:5:0:0]
[CASTE:60][CASTE_COLOR:6:0:0]
[CASTE:70][CASTE_COLOR:7:0:0]
[CASTE:01][CASTE_COLOR:0:0:1]
[CASTE:11][CASTE_COLOR:1:0:1]
[CASTE:21][CASTE_COLOR:2:0:1]
[CASTE:31][CASTE_COLOR:3:0:1]
[CASTE:41][CASTE_COLOR:4:0:1]
[CASTE:51][CASTE_COLOR:5:0:1]
[CASTE:61][CASTE_COLOR:6:0:1]
[CASTE:71][CASTE_COLOR:7:0:1]
[SELECT_CASTE:ALL]
[ITEMCORPSE:SMALLGEM:NONE:LOCAL_CREATURE_MAT:GEM_MATERIAL] -- This material will be replaced by the material used to construct the creature. NOTE: The material must still be the first one defined.
--A construct to demonstrate more complex creature building. This creature has three materials (SHELL, CLOCKWORK, and BRAIN), all of which can be defined in the reaction.
[CREATURE:BOT_SOUL]
[CREATURE_CLASS:DFHACK_CONSTRUCTCREATURE_CASTECOLORS] -- Same as above. Here, the material used for the caste will be the SHELL.
[CREATURE_CLASS:DFHACK_SOULSHUFFLE_NO_SOUL_NEEDED] -- Prevents the creature from going catatonic when its last soul is removed. Defining this for civ members will cause the unit viewing screen to not display properly. It's okay for pets though.
[NAME:homunculus:homunculi:homunculus]
[CASTE_NAME:homunculus:homunculi:homunculus]
[DESCRIPTION:An tiny artificial humanoid. Although it can function autonomously, it requires a soul in order to learn.]
[COLOR:0:0:1]
[PET]
[USE_MATERIAL_TEMPLATE:SHELL:METAL_TEMPLATE]
[USE_MATERIAL_TEMPLATE:CLOCKWORK:STONE_TEMPLATE]
[USE_MATERIAL_TEMPLATE:BRAIN:STONE_TEMPLATE]
[USE_MATERIAL_TEMPLATE:BLOOD:BLOOD_TEMPLATE]
[TISSUE:SHELL]
[TISSUE_NAME:plating:NP]
[TISSUE_MATERIAL:LOCAL_CREATURE_MAT:SHELL]
[RELATIVE_THICKNESS:2]
[CONNECTS][FUNCTIONAL]
[TISSUE_SHAPE:LAYER]
[TISSUE:CLOCKWORK]
[TISSUE_NAME:clockwork:NP]
[TISSUE_MATERIAL:LOCAL_CREATURE_MAT:CLOCKWORK]
[RELATIVE_THICKNESS:1]
[FUNCTIONAL]
[VASCULAR:5]
[CONNECTS]
[TISSUE_SHAPE:LAYER]
[TISSUE:HYDRAULICS]
[TISSUE_NAME:hydraulics:NP]
[TISSUE_MATERIAL:LOCAL_CREATURE_MAT:CLOCKWORK] --Materials are replaced, not tissues. This will have the same new material as the above tissue.
[RELATIVE_THICKNESS:1]
[MUSCULAR][STRUCTURAL]
[FUNCTIONAL]
[ARTERIES]
[VASCULAR:5]
[CONNECTS]
[TISSUE_SHAPE:LAYER]
[USE_TISSUE_TEMPLATE:BRAIN:BRAIN_TEMPLATE]
[HAS_NERVES]
[BLOOD:LOCAL_CREATURE_MAT:BLOOD:LIQUID]
[CAN_LEARN][CAN_SPEAK]
[EXTRAVISION][NOPAIN][NONAUSEA][NO_EAT][NO_DRINK][NOBREATHE][NO_FEVERS][PARALYZEIMMUNE][FIREIMMUNE][NOEXERT][NOT_LIVING]
[BODY:HUMANOID_SIMPLE:BRAIN]
[BODY_DETAIL_PLAN:EXOSKELETON_TISSUE_LAYERS:SHELL:CLOCKWORK:HYDRAULICS]
[APPLY_CREATURE_VARIATION:PUNCH_ATTACK]
[APPLY_CREATURE_VARIATION:KICK_ATTACK]
[BODY_SIZE:0:0:4000]
[CASTE:00][CASTE_COLOR:0:0:0]
[CASTE:10][CASTE_COLOR:1:0:0]
[CASTE:20][CASTE_COLOR:2:0:0]
[CASTE:30][CASTE_COLOR:3:0:0]
[CASTE:40][CASTE_COLOR:4:0:0]
[CASTE:50][CASTE_COLOR:5:0:0]
[CASTE:60][CASTE_COLOR:6:0:0]
[CASTE:70][CASTE_COLOR:7:0:0]
[CASTE:01][CASTE_COLOR:0:0:1]
[CASTE:11][CASTE_COLOR:1:0:1]
[CASTE:21][CASTE_COLOR:2:0:1]
[CASTE:31][CASTE_COLOR:3:0:1]
[CASTE:41][CASTE_COLOR:4:0:1]
[CASTE:51][CASTE_COLOR:5:0:1]
[CASTE:61][CASTE_COLOR:6:0:1]
[CASTE:71][CASTE_COLOR:7:0:1]
reaction_sscc_examples
[OBJECT:REACTION]
The Soulshuffle script uses reaction codes as commands.
Explanation:
Every Soulshuffle reaction must begin with LUA_HOOK_SOULSHUFFLE, which is followed by a set of commands separated by spaces.
The first word is the operation name. Operations are MOVE, ASSIMILATE, SWAP, SPLIT, MERGE, COPY, FUSE, and DESTROY.
Each operation has two subjects: the source and the target. The source is specified with FROM, the target with TO.
Many source and target specifiers can be specified, but they are exclusive. In order to be considered a valid target, a unit must satisfy ALL of the specifier commands.
MOVE - moves a selected soul from the source unit to the target unit.)
REPLACE - moves the selected soul and destroys the target.
DESTROY - destroys the source soul. Does not require a target to be specified.
ASSIMILATE - causes the target soul's civ to match the source soul's civ.
SWAP - switches the selected source and target souls.
SPLIT - splits the source soul in half and moves it to the target unit. Each fragment will acquire a percentage (default 50) of the original's skills and mental attributes.
COPY - similar to SPLIT, but the source soul does not lose any skills or attributes. The percentage determines the amount of the original's skills and attributes copied.
MERGE - merges the source soul into the target soul. The skills and attributes will be averaged out based on a weighted average.
FUSE - similar to MERGE, but a percentage of the skills and attributes will be added from the source soul to the target soul (default 100), making the new soul stronger.
Source/target specifier strings:
Almost all basic creature tags can be targeted, as well as most behavioral flags. I won't list them all here, but if you can specify it in a creature, it can probably be used as a parameter, and many, many other possible things can also be targeted.
SELF - the unit running the reaction.
ALIVE - conventionally alive, i.e. not dead, ghostly, or undead.
CIV - is a member of the player's civilization. This does include tame animals.
CLASS - Allows targeting based on creature class. Syntax is (for example) CLASS GENERAL_POISON.
CREATURE - Allows targeting based on creature. Syntax is CREATURE RACE CASTE.
CAGED - is in a cage.
TAME - is a tame pet.
GHOSTLY - is a ghost. (I'm not sure what happens if you take the soul out of a ghost. Best not chance it for now.)
DEMON - is a demon.
UNDERWORLD - is a demon currently in 'attack mode'.
RESIDENT - most hostile local creatures have this flag: demons, underground civs, etc.
DEAD - is completely dead. Ghosts and undead are NOT considered DEAD. Note that this does not target dead bodies, it targets dead UNITS, which generally remain in the place they were when they died (or where their ghost was put to rest), even if the body moves around.
All of these specifiers can be inverted by preceding them with NOT.
Two special specifiers: SPAWN and CONSTRUCT:
SPAWN creates a new creature with the specified race and caste, and implants the soul in it. SPAWN RACE CASTE. If CASTE is NONE, the script will try and find the most logical target to turn the soul into, based on the caste's name. It does this by searching the caste name and looking for matches. For instance, if you make a race that (hypothetically speaking) turns human souls into devils and elvish souls into cambions, male devils can be given the caste name HUMAN MALE and female cambions can be given the caste name ELF FEMALE. And so on. If a match is not found, it will look for castes called DEFAULT_MALE and DEFAULT_FEMALE, and finally DEFAULT if neither of those match. If no matches are found, a random caste will be selected.
CONSTRUCT is similar, but utilizes the CONSTRUCTCREATURE system that will use the reagents to construct the creature.
SPAWN and CONSTRUCT only make sense for MOVE, SPLIT, and COPY operations.
SPLIT, COPY, MERGE, and FUSE can also be given a PERC value (on the source soul). This basically determines the percentage of the soul that will be affected by the reaction.
ATTACK is a modifier that can be added to any reaction. Syntax is ATTACK [combat intensity] [success rate]. Default for both is 100.
When a reaction uses the ATTACK modifier, the reaction can fail when attempting to use it on souls that are not part of your civ.
The difficulty in overcoming resistance is based on the combined soul attributes of the souls being affected, as well as their respective stress levels.
When engaging in 'soul combat', both souls will take 'damage', represented by their stress level.
CAN_BREAK is a modifier that can cause a soul to break when it is being manipulated. Weaker souls (with smaller attribute values) have a higher chance of breaking, as do highly stressed souls. Use this to limit how small you can divide up a soul.
MIN_ATTR and MAX_ATTR place limits on soul manipulation based on average soul attributes. Use this to limit splitting and fusing souls.
There are also some special specifier strings:
TOP: Only allows selection of the unit's current soul. When in the target unit in a MOVE or SPLIT operation, this will cause the implanted soul to be set as the target's current soul.
SUB: Opposite of TOP. Forbids selection of the current soul.
LIMIT: Prevents you from selecting a source unit if it has only one soul left.
EMPTY: Only allows selection of targets that have no soul.
SAME: Can be specified for the target to force the use of the source unit.
RANGE: Allows you to change the range that targets can be acquired at. Syntax is (for example) RANGE 50. Range is a square around the target unit.
Z: Like RANGE, but determines Z-levels instead.
A creature can contain multiple souls, however only one will be manifest at a time. Souls contain a unit's skills, personality, stress level, and thoughts.
Souls have a civ identity. Beware of manifesting the souls of hostile units without assimilating them first!
A creature can live without a soul. Extracting the last soul from a creature using Soulshuffle will cause it to go catatonic. Replacing it with a new soul will return the creature to normal. If you remove the current soul of a creature but it still has extra souls remaining, one will be selected at random.
Examples:
[REACTION:LUA_HOOK_SOULSHUFFLE MOVE FROM ALIVE TO ALIVE]
[NAME:Move soul of one creature to another]
[PRODUCT:0:0:BOULDER:NONE:NONE:NONE]
[BUILDING:SOULPOWER:NONE]
[REACTION:LUA_HOOK_SOULSHUFFLE SPLIT FROM ALIVE MIN_ATTR 250 TO ALIVE]
[NAME:Split a soul]
[PRODUCT:0:0:BOULDER:NONE:NONE:NONE]
[BUILDING:SOULPOWER:NONE]
[REACTION:LUA_HOOK_SOULSHUFFLE COPY FROM ALIVE TO ALIVE]
[NAME:Copy a soul]
[PRODUCT:0:0:BOULDER:NONE:NONE:NONE]
[BUILDING:SOULPOWER:NONE]
[REACTION:LUA_HOOK_SOULSHUFFLE MERGE FROM ALIVE TO ALIVE]
[NAME:Merge two souls]
[PRODUCT:0:0:BOULDER:NONE:NONE:NONE]
[BUILDING:SOULPOWER:NONE]
[REACTION:LUA_HOOK_SOULSHUFFLE FUSE FROM ALIVE MAX_ATTR 2500 TO ALIVE MAX_ATTR 2500]
[NAME:Fuse two souls]
[PRODUCT:0:0:BOULDER:NONE:NONE:NONE]
[BUILDING:SOULPOWER:NONE]
[REACTION:LUA_HOOK_SOULSHUFFLE MOVE ATTACK FROM CREATURE GEM_SOUL NONE CIV_SOUL TO ALIVE SUB RANGE 200 Z 10]
[NAME:Cast a sleeper soul from gem]
[PRODUCT:0:0:BOULDER:NONE:NONE:NONE]
[BUILDING:SOULPOWER:NONE]
[REACTION:LUA_HOOK_SOULSHUFFLE MOVE ATTACK FROM ALIVE SUB CIV_SOUL RANGE 1000 Z 1000 TO CONSTRUCT GEM_SOUL NONE PET]
[NAME:Recall sleeper soul to gem]
[REAGENT:GEM_MATERIAL:1:SMALLGEM:NONE:NONE:NONE] -- The name of the reagent must match the name of the material specified in the creature.
[PRODUCT:0:0:BOULDER:NONE:NONE:NONE]
[BUILDING:SOULPOWER:NONE]
[REACTION:LUA_HOOK_SOULSHUFFLE MOVE ATTACK FROM ALIVE NOT SELF SUB CIV_SOUL RANGE 1000 Z 1000 TO SAME TOP]
[NAME:Sleeper soul possession]
[PRODUCT:0:0:BOULDER:NONE:NONE:NONE]
[BUILDING:SOULPOWER:NONE]
[REACTION:LUA_HOOK_SOULSHUFFLE MOVE FROM SELF LIMIT TO SAME TOP]
[NAME:Manifest a different soul]
[PRODUCT:0:0:BOULDER:NONE:NONE:NONE]
[BUILDING:SOULPOWER:NONE]
[REACTION:LUA_HOOK_SOULSHUFFLE MOVE ATTACK FROM SELF TOP TO ALIVE NOT SELF TOP RANGE 1000 Z 1000]
[NAME:Attempt to possess a unit]
[PRODUCT:0:0:BOULDER:NONE:NONE:NONE]
[BUILDING:SOULPOWER:NONE]
[REACTION:LUA_HOOK_SOULSHUFFLE MOVE FROM CREATURE GEM_SOUL NONE TO ALIVE NOT SELF EMPTY]
[NAME:Transfer soul from gem into empty shell]
[PRODUCT:0:0:BOULDER:NONE:NONE:NONE]
[BUILDING:SOULPOWER:NONE]
[REACTION:LUA_HOOK_SOULSHUFFLE ASSIMILATE ATTACK FROM SELF TOP TO CREATURE GEM_SOUL NONE NOT CIV_SOUL]
[NAME:Assimilate stored soul into culture]
[PRODUCT:0:0:BOULDER:NONE:NONE:NONE]
[BUILDING:SOULPOWER:NONE]
[REACTION:LUA_HOOK_SOULSHUFFLE SWAP ATTACK FROM ALIVE CAGED TOP TO ALIVE CAGED TOP]
[NAME:Swap souls of caged creatures]
[PRODUCT:0:0:BOULDER:NONE:NONE:NONE]
[BUILDING:SOULPOWER:NONE]
[REACTION:LUA_HOOK_SOULSHUFFLE SWAP ATTACK FROM SELF TOP TO ALIVE NOT SELF TOP]
[NAME:Swap souls with nearby unit]
[PRODUCT:0:0:BOULDER:NONE:NONE:NONE]
[BUILDING:SOULPOWER:NONE]
[REACTION:LUA_HOOK_SOULSHUFFLE MERGE ATTACK FROM SELF LIMIT TO SELF]
[NAME:Mix souls inside body]
[PRODUCT:0:0:BOULDER:NONE:NONE:NONE]
[BUILDING:SOULPOWER:NONE]
[REACTION:LUA_HOOK_SOULSHUFFLE ATTACK 100 200 MOVE FROM DEAD TO CONSTRUCT GEM_SOUL NONE PET RANGE 1000 Z 1000]
[NAME:Capture lost spirits in gem]
[REAGENT:GEM_MATERIAL:1:SMALLGEM:NONE:NONE:NONE]
[PRODUCT:0:0:BOULDER:NONE:NONE:NONE]
[BUILDING:SOULPOWER:NONE]
[REACTION:LUA_HOOK_SOULSHUFFLE DESTROY FROM CREATURE GEM_SOUL NONE]
[NAME:Burn soul from gem]
[BUILDING:SOULPOWER:NONE]
[PRODUCT:100:1:BAR:NO_SUBTYPE:COAL:COKE][PRODUCT_DIMENSION:150]
[REACTION:LUA_HOOK_SOULSHUFFLE FUSE FROM CREATURE GEM_SOUL NONE PERC 10 TO SELF TOP]
[NAME:Consume soul of stored creature]
[PRODUCT:0:0:BOULDER:NONE:NONE:NONE]
[BUILDING:SOULPOWER:NONE]
[REACTION:LUA_HOOK_SOULSHUFFLE MOVE ATTACK FROM DEMON RANGE 1000 Z 1000 TO CONSTRUCT GEM_SOUL NONE PET]
[NAME:Capture demon soul and store it in a gem]
[REAGENT:GEM_MATERIAL:1:SMALLGEM:NONE:NONE:NONE]
[PRODUCT:0:0:BOULDER:NONE:NONE:NONE]
[BUILDING:SOULPOWER:NONE]
[REACTION:LUA_HOOK_SOULSHUFFLE SPLIT FROM SELF TOP PERC 10 ADD_STRESS 1000 MIN_ATTR 250 TO SPAWN IMP_SOUL NONE PET] -- SPAWN creates a new creature to contain the soul. This creature will have the same loyalty as the original soul.
[NAME:Splinter soul to produce an imp]
[PRODUCT:0:0:BOULDER:NONE:NONE:NONE]
[BUILDING:SOULPOWER:NONE]
[REACTION:LUA_HOOK_SOULSHUFFLE FUSE FROM CREATURE IMP_SOUL NONE MAX_ATTR 1000 TO SELF TOP MAX_ATTR 5000]
[NAME:Fuse with soul imp]
[PRODUCT:0:0:BOULDER:NONE:NONE:NONE]
[BUILDING:SOULPOWER:NONE]
[REACTION:LUA_HOOK_SOULSHUFFLE MOVE ATTACK FROM ALIVE TO CONSTRUCT GEM_SOUL NONE PET] -- CONSTRUCT works the same as SPAWN, except that it uses the reagent items to define the creature's materials.
[NAME:Store a soul in a gem]
[REAGENT:GEM_MATERIAL:1:SMALLGEM:NONE:NONE:NONE] -- The name of the reagent must match the name of the material specified in the creature.
[PRODUCT:0:0:BOULDER:NONE:NONE:NONE]
[BUILDING:SOULPOWER:NONE]
[REACTION:LUA_HOOK_SOULSHUFFLE MERGE FROM CREATURE GEM_SOUL NONE TO CREATURE GEM_SOUL NONE NOT SAME]
[NAME:Combine two souls stored in gems]
[PRODUCT:0:0:BOULDER:NONE:NONE:NONE]
[BUILDING:SOULPOWER:NONE]
[REACTION:LUA_HOOK_CONSTRUCTCREATURE BOT_SOUL NONE NOSOUL PET] -- Adding NOSOUL will cause the creature to be generated without a soul.
[NAME:Create a homunculus]
[REAGENT:SHELL:1:BLOCKS:NONE:NONE:NONE]
[REAGENT:CLOCKWORK:1:TRAPPARTS:NONE:NONE:NONE]
[REAGENT:BRAIN:1:SMALLGEM:NONE:NONE:NONE]
[REAGENT:BLOOD:150:DRINK:NONE:NONE:NONE] -- It's a dwarfy homunculus :)
[REAGENT:drink container:1:NONE:NONE:NONE:NONE]
[CONTAINS:BLOOD]
[PRODUCT:0:0:BOULDER:NONE:NONE:NONE]
[BUILDING:SOULPOWER:NONE]
--Please note: Constructing complex creatures can be finicky.
The script isn't that great at figuring out which job item is matched up to which reagent, especially where the reagent is unclear about which item it should be using.
NONE:NONE:NONE:NONE as a construction material, for example, may confuse the script, as will using two items of the same type.
Also, requiring more than one item in a reagent will usually not work!
(Note: I found a new method, the above may no longer apply. Further testing is needed to be sure.)
The best way of constructing complex creatures (that require more than one material) is to use tools as intermediary items. Create a 'robot body' tool using the Metalsmith's Forge or a custom reaction, then use the tool as a reagent to construct the creature.
You do not need to define every material the creature uses in the reaction. The script creates the creature using its regular materials first, and only afterwards replaces the relevant materials.
So if you leave out a material in the reaction, the creature will simply use its normal body material.
-- Allows many functions relating to the manipulation of bodies and souls.
--[[
By IndigoFenix
]]--
--[[
DFHACK_CONSTRUCTCREATURE_CASTECOLORS
DFHACK_CONSTRUCTCREATURE_ITEMCORPSE
DFHACK_SOULSHUFFLE_SCUTTLE_ON_REMOVAL
DFHACK_SOULSHUFFLE_VANISH_ON_REMOVAL
DFHACK_SOULSHUFFLE_CANNOT_TARGET
DFHACK_SOULSHUFFLE_NO_SOUL_NEEDED
DFHACK_SOULSHUFFLE_RETAIN_LOYALTY
DFHACK_SOULSHUFFLE_ONE_SOUL_LIMIT
]]--
local eventful = require 'plugins.eventful'
local mo = require 'makeown'
local utils = require 'utils'
local script = require 'gui.script'
local soulStorageUnitId = nil
local soulStorageUnit = nil
local prefix = 'SOULSHUFFLE'
local rando=dfhack.random.new()
function closeBy(unit,u,radius,z)
if z == nil then z = 0 end
if unit.id ~= u.id
and unit.pos.x+radius >= u.pos.x
and unit.pos.x-radius <= u.pos.x
and unit.pos.y+radius >= u.pos.y
and unit.pos.y-radius <= u.pos.y
and unit.pos.z+z >= u.pos.z
and unit.pos.z-z <= u.pos.z
then return true
else
return false
end
end
function hasClass(unit,class)
local creature_classes = df.global.world.raws.creatures.all[unit.race].caste[unit.caste].creature_class
for i=0,#creature_classes-1,1 do
if creature_classes[i].value == class then return true end
end
return false
end
function getSoulStorageUnitId()
if soulStorageUnitId ~= nil then
return soulStorageUnitId
else
local data = dfhack.persistent.get(prefix..'_STORAGE')
if data == nil then
local unit=df.unit:new()
unit.id=df.global.unit_next_id
soulStorageUnitId = unit.id
df.global.unit_next_id=df.global.unit_next_id+1
df.global.world.units.all:insert("#",unit)
--df.global.world.units.active:insert("#",unit)
dfhack.persistent.save({key=prefix..'_STORAGE'})
data = dfhack.persistent.get(prefix..'_STORAGE')
data.ints[1] = soulStorageUnitId
data:save()
print ('Saved abstract soul storage unit with id: '..soulStorageUnitId)
return soulStorageUnitId
else
soulStorageUnitId = data.ints[1]
return soulStorageUnitId
end
end
end
function getSoulStorageUnit()
return df.unit.find(getSoulStorageUnitId())
end
function InsertSoul(unit,soul,top)
isEmptyShell = false
if #unit.status.souls == 0 then
isEmptyShell = true
end
unit.status.souls:insert("#",soul)
if isEmptyShell or top == true then
unit.status.current_soul = soul
if unit.mood == 9 then
unit.mood = -1
unit.animal.vanish_countdown = 0
end
end
setCurrentSoulValues(unit)
return soul
end
function RemoveSoul(unit,soul)
foundSoul = false
for i = #unit.status.souls - 1,0, -1 do
s = unit.status.souls[i]
if s.unit_id == soul.unit_id then
unit.status.souls:erase(i)
foundSoul = true
end
end
if unit.status.current_soul ~= nil and unit.status.current_soul.unit_id == soul.unit_id then
unit.status.current_soul = nil
foundSoul = true
end
--Organize souls after removal. If there is no current soul, a random soul is picked from the unit's existing souls. If there are none left, unit goes catatonic.
if #unit.status.souls == 0 then
unit.status.current_soul = nil
end
if unit.status.current_soul == nil then
if #unit.status.souls == 0 then
if not hasClass(unit,'DFHACK_SOULSHUFFLE_NO_SOUL_NEEDED') then unit.mood = 9 end
if hasClass(unit,'DFHACK_SOULSHUFFLE_VANISH_ON_REMOVAL') then
unit.animal.vanish_countdown = 2
elseif hasClass(unit,'DFHACK_SOULSHUFFLE_SCUTTLE_ON_REMOVAL') then
unit.flags3.scuttle = true
end
else
unit.status.current_soul = unit.status.souls[math.random(#unit.status.souls)-1]
end
end
setCurrentSoulValues(unit)
if foundSoul then
return soul
else
return nil
end
end
function FindSoul(unit,soul_id)
soul = nil
for i = 0, #unit.status.souls - 1, 1 do
s = unit.status.souls[i]
if s.unit_id == soul_id then
soul = unit.status.souls[i]
foundSoul = true
end
end
if unit.status.current_soul.unit_id == soul_id then
foundSoul = unit.status.current_soul
end
return soul
end
function InsertSoulAbstract(soul)
return InsertSoul(getSoulStorageUnit(),soul)
end
function RemoveSoulAbstract(soul)
return RemoveSoul(getSoulStorageUnit(),soul)
end
function FindSoulAbstract(soul_id)
return FindSoul(getSoulStorageUnit(),soul_id)
end
function exile(u)
if u then
u.civ_id=-1
u.flags1.tame=false
u.flags1.forest=true
u.relations.following = nil
u.flags1.active_invader = false
u.flags1.hidden_in_ambush = false
u.flags1.coward = false
u.flags1.hidden_ambusher = false
u.flags1.invades = false
end
end
function setCurrentSoulValues(unit,override)
if hasClass(unit,'DFHACK_SOULSHUFFLE_RETAIN_LOYALTY') then return end
if unit.status.current_soul ~= nil then
local soul = unit.status.current_soul
unit.civ_id = soul.personality.civ_id
unit.cultural_identity = soul.personality.cultural_identity
if unit.civ_id == -1 then
soulrace = df.global.world.raws.creatures.all[soul.race]
casteflags = df.global.world.raws.creatures.all[soul.race].caste[soul.caste].flags
if casteflags.DEMON == true or casteflags.TITAN == true or casteflags.FEATURE_BEAST == true then
unit.flags1.tame = false
else
exile(unit)
end
end
else
unit.civ_id = -1
unit.cultural_identity = -1
end
end
function saveJobItemMats(reaction,jobitems)
items = {}
for i,v in ipairs(jobitems or {}) do
items[reaction.reagents[v.job_item_idx].code] = dfhack.matinfo.decode(v.item)
end
return items
end
--Does this actually work? Not sure...
function getReagentItemByCode(reaction,jobmats,code)
if jobmats[code] then return jobmats[code] else return nil end
end
--A messy function to get the item that should be used in the reaction. It's messy.
function getReagentItem(reaction,job,reagent_id)
local reagent_type = -1
local reagent_subtype = -1
local reagent = reaction.reagents[reagent_id]
--[[
for _,v in ipairs(job.items or {}) do
vi = v.item
if (vi:getType() == reagent.item_type or reagent.item_type == -1)
and (vi:getSubtype() == reagent.item_subtype or reagent.item_subtype == -1) then
return vi
end
end
]]--
for _,v in ipairs(job.items or {}) do
vi = v.item
vidx = v.job_item_idx
if (vi:getType() == reagent.item_type or reagent.item_type == -1)
and (vi:getSubtype() == reagent.item_subtype or reagent.item_subtype == -1) then
return vi
end
end
return nil
end
function getReagentByProduct(reaction,product_id)
local reagent_code = reaction.products[product_id].get_material.reagent_code
for i = 0, #reaction.reagents - 1, 1 do
v = reaction.reagents[i]
if v.code == reagent_code then
return i
end
end
return nil
end
function getReagentItemByProduct(reaction,job,product_id)
return getReagentItem(reaction,job,getReagentByProduct(product_id))
end
function CreateSouljar(reaction, job, soul)
typ = reaction.products[0].item_type
subtyp = reaction.products[0].item_subtype
local emptySoulJar = getReagentItemByProduct(reaction, job, 0)
if emptySoulJar ~= nil then
mat_type = emptySoulJar.mat_type
mat_index = emptySoulJar.mat_index
end
local item = df['item_'..string.lower(df.item_type[typ])..'st']:new()
item:setSubtype(subtyp)
item.id = df.global.item_next_id
df.global.world.items.all:insert('#',item)
df.global.item_next_id = df.global.item_next_id+1
item:setMaterial(mat_type)
item:setMaterialIndex(mat_index)
item:setMakerRace(df.global.ui.race_id)
if soul ~= nil then
--item:assignQuality(soul, skill)
item:setMaker(soul.unit_id)
InsertSoulAbstract(soul)
end
item:categorize(true)
item.flags.removed=true
item:moveToGround(worker.pos.x,worker.pos.y,worker.pos.z)
for _,v in ipairs(worker.job.current_job.general_refs or {}) do
if getmetatable(v) == 'general_ref_building_holderst' then
building = df.building.find(v.building_id)
--dfhack.items.moveToBuilding(item,building,0)
end
end
return item
end
function split(str, pat)
local t = {} -- NOTE: use {n = 0} in Lua-5.0
local fpat = "(.-)" .. pat
local last_end = 1
local s, e, cap = str:find(fpat, 1)
while s do
if s ~= 1 or cap ~= "" then
table.insert(t,cap)
end
last_end = e+1
s, e, cap = str:find(fpat, last_end)
end
if last_end <= #str then
cap = str:sub(last_end)
table.insert(t, cap)
end
return t
end
function isAlliedSoul(soul)
local r = false
if soul.personality.civ_id == df.global.ui.civ_id then
r = true
end
return r
end
function setValid(subject,key,value)
if subject ~= nil then
subject[key] = value
end
end
function checkIfValidUnit(u,rules)
local ok = false
local bad = false
if hasClass(u,'DFHACK_SOULSHUFFLE_CANNOT_TARGET') then return false end
if rules['CREATURE'] ~= nil then
for i,v in ipairs(rules['CREATURE'] or {}) do
local racestring = v.race
local castestring = v.caste
local valid = v.valid
if df.global.world.raws.creatures.all[u.race].creature_id == racestring and
(df.global.world.raws.creatures.all[u.race].caste[u.caste].caste_id == castestring or castestring == 'NONE') then
if valid == true then ok = true
else return false end
end
end
end
if rules['CLASS'] ~= nil then
for i,v in ipairs(rules['CLASS'] or {}) do
local classstring = v.class
local valid = v.valid
if hasClass(u,classstring) then
if valid == true then ok = true
else return false end
end
end
end
if rules['SELF'] == true then ok = true end
for i,v in pairs(rules or {}) do
if type(i) == 'string' and type(v) == 'boolean' then
local strlower = i:lower()
local strupper = i:upper()
local flaglists = {u.flags1,u.flags2,u.flags3,df.global.world.raws.creatures.all[u.race].caste[u.caste].flags}
for listid,flaglist in ipairs((flaglists) or {}) do
for flagid,flagvalue in pairs(flaglist or {}) do
if flagid == strlower or flagid == strupper then
if rules[i] ~= nil and rules[i] == flagvalue then ok = true
elseif rules[i] ~= nil then return false end
end
end
end
end
end
if rules['ALIVE'] ~= nil and rules['ALIVE'] ~= dfhack.units.isDead(u) then ok = true
elseif rules['ALIVE'] ~= nil then return false end
if rules['CIV_UNIT'] ~= nil and rules['CIV_UNIT'] == (u.civ_id == df.global.ui.civ_id) then ok = true
elseif rules['CIV_UNIT'] ~= nil then return false end
if ok == true and bad == false then return true
else return false end
end
function getObjectSouls(object,rules,invalidSouls)
local souls = {}
if invalidSouls == nil then invalidSouls = {} end
if getmetatable(object) == 'unit' then
if rules['LIMIT'] ~= true or #object.status.souls > 1 then
if rules['SUB'] ~= true then
table.insert(souls,object.status.current_soul)
end
if rules['TOP'] ~= true then
for _,soul in ipairs(object.status.souls or {}) do
if object.status.current_soul.unit_id ~= soul.unit_id then
table.insert(souls,soul)
end
end
end
end
for i = #souls - 1, 0, -1 do
soul = souls[i]
if soul ~= nil then
if (rules['CIV_SOUL'] == true and isAlliedSoul(soul) == false)
or (rules['CIV_SOUL'] == false and isAlliedSoul(soul) == true) then
table.insert(invalidSouls,soul)
end
end
end
elseif getmetatable(object) == 'item_corpsest' then
else
local soul_id = object.maker
if soul_id ~= nil then
local soul = FindSoulAbstract(soul_id)
if soul ~= nil then
table.insert(souls,soul)
end
end
end
if invalidSouls ~= nil then
for i = #souls - 1, 0, -1 do
soul = souls[i]
for _,invalidsoul in ipairs(invalidSouls or {}) do
if soul ~= nil then
if soul.unit_id == invalidsoul.unit_id then
souls:erase(i)
end
end
end
end
end
return souls
end
function getObjectName(object)
if getmetatable(object) == 'unit' then
name = dfhack.TranslateName(object.name)
sexsym = sexSymbol(object.sex)
if name == '' then name = ((df.global.world.raws.creatures.all[object.race]).caste[object.caste].caste_name[0])..sexsym else
name = name .. ', ' .. ((df.global.world.raws.creatures.all[object.race]).caste[object.caste].caste_name[0])..sexsym
end
return name
elseif getmetatable(object) == 'item_corpsest' then
return 'Corpse'
else
return 'Soul Container'
end
end
function addExp(soul,skillId,amount,max_skill)
if max_skill == nil then max_skill = 20 end
--local skillId = df.job_skill[skillname]
--if skillId == nil or skillId == -1 then return nil end
local skill = df.unit_skill:new()
local foundSkill = false
doneLeveling = false
amountadded = 0
while doneLeveling == false do
for k, soulSkill in ipairs(soul.skills) do
if soulSkill.id == skillId then
skill = soulSkill
foundSkill = true
break
end
end
if foundSkill then
-- Let's not train beyond the max skill
if skill.rating >= max_skill then
return false
end
skill.experience = skill.experience + amount
amount = 0
if skill.experience > 100 * skill.rating + 500 then
skill.experience = skill.experience - (100 * skill.rating + 500)
skill.rating = skill.rating + 1
elseif skill.experience < 0 then
if skill.rating == 0 then
amount = skill.experience * -1
skill.experience = 0
return amount
else
skill.experience = skill.experience + (100 * skill.rating + 500)
skill.rating = skill.rating - 1
end
else
doneLeveling = true
end
else
skill.id = skillId
skill.experience = 0
skill.rating = 0
soul.skills:insert('#',skill)
end
end
return 0
end
local function hasDuplicate(object,array)
for k, v in pairs(array) do
isDuplicate = true
for i, j in pairs(v) do
if object[i] ~= j then
isDuplicate = false
end
end
if isDuplicate == true then
return true
end
end
return false
end
local function insertIfNoDuplicate(object,array)
if hasDuplicate(object,array) == true then
return array
else
array:insert("#",object)
return array
end
end
--Not complete yet.
function CombineSouls(sourceSoul,targetSoul,opextra,efficiency)
if opextra == 'FUSE' then add = true end
if add == nil then add = false end
if efficiency == nil then efficiency = 100 end
local perc = efficiency/100
for k,v in ipairs(sourceSoul.mental_attrs or {}) do
if add then
targetSoul.mental_attrs[k].value = targetSoul.mental_attrs[k].value + (v.value*perc)
else
targetSoul.mental_attrs[k].value = (targetSoul.mental_attrs[k].value * (1 - (perc / 2))) + (v.value * (perc / 2))
end
end
for k,v in ipairs(sourceSoul.skills or {}) do
local experience = skillRatingToExperience(v.rating) + v.experience
if add == true then
addExp(targetSoul,v.id,experience*perc,v.rating)
else
addExp(targetSoul,v.id,experience*perc,v.rating,v.rating)
end
end
local sourceValues = getSoulValues(sourceSoul)
local targetValues = getSoulValues(targetSoul)
soulPowerRatio = sourceValues.attrs / targetValues.attrs
efficiency = efficiency * soulPowerRatio
perc = efficiency/100
for i,v in ipairs(sourceSoul.preferences or {}) do
if math.random(100) < efficiency then targetSoul.preferences = insertIfNoDuplicate(v,targetSoul.preferences) end
end
for k,v in pairs(sourceSoul.personality.traits or {}) do
targetSoul.personality.traits[k] = (targetSoul.personality.traits[k] * (1 - (perc / 2))) + (v * (perc / 2))
end
for i,v in ipairs(sourceSoul.personality.values or {}) do
local isDuplicate = false
for ti, tv in pairs(targetSoul.personality.values) do
if tv.type == v.type then
isDuplicate = true
tv.strength = (tv.strength * (1 - (perc / 2))) + (v.strength * (perc / 2))
end
end
if isDuplicate == false then
v.strength = v.strength * (perc / 2)
targetSoul.personality.values:insert("#",v)
end
end
for i,v in ipairs(sourceSoul.personality.emotions or {}) do
if math.random(100) < efficiency then targetSoul.personality.emotions = insertIfNoDuplicate(v,targetSoul.personality.emotions) end
end
for i,v in ipairs(sourceSoul.personality.dreams or {}) do
if math.random(100) < efficiency then
local isDuplicate = false
for ti, tv in pairs(targetSoul.personality.dreams) do
if tv.type == v.type then
isDuplicate = true
end
end
if isDuplicate == false then
targetSoul.personality.dreams:insert("#",v)
end
end
end
if add then
targetSoul.personality.stress_level = targetSoul.personality.stress_level + (sourceSoul.personality.stress_level * perc)
else
targetSoul.personality.stress_level = (targetSoul.personality.stress_level * (1 - (perc / 2))) + (sourceSoul.personality.stress_level * (perc / 2))
end
return targetSoul
end
local function deepCopySoul(original,copy)
for k, v in pairs(original) do
if getmetatable(v) ~= 'unit_skill'
and getmetatable(v) ~= 'unit_preference'
and getmetatable(v) ~= 'unit_personality' then
-- as before, but if we find a table, make sure we copy that too
if type(v) == 'table' or type(v) == 'userdata' then
if copy[k] == nil then copy:insert("#",v)
else v = deepCopySoul(v,copy[k]) end
else
copy[k] = v
end
end
end
return copy
end
--Not complete yet.
function SplitSoul(sourceSoul,opextra,efficiency)
targetSoul = df.unit_soul:new()
newSoul = deepCopySoul(sourceSoul,targetSoul)
targetSoul.unit_id=sourceSoul.unit_id
targetSoul.name:assign(sourceSoul.name)
targetSoul.race=sourceSoul.race
targetSoul.sex=sourceSoul.sex
targetSoul.caste=sourceSoul.caste
for k,v in pairs(sourceSoul.orientation_flags or {}) do
sourceSoul.orientation_flags[k] = sourceSoul.orientation_flags[k]
end
if opextra == 'COPY' then copy = true end
if copy == nil then copy = false end
if efficiency == nil then
if copy == true then efficiency = 100 else efficiency = 50 end
end
local perc = efficiency/100
local target_perc = perc
local source_perc = 1 - perc
for k,v in ipairs(sourceSoul.mental_attrs or {}) do
if copy == true then
targetSoul.mental_attrs[k].value = sourceSoul.mental_attrs[k].value * perc
else
targetSoul.mental_attrs[k].value = sourceSoul.mental_attrs[k].value * target_perc
sourceSoul.mental_attrs[k].value = sourceSoul.mental_attrs[k].value * source_perc
end
end
for k,v in ipairs(sourceSoul.skills or {}) do
local experience = skillRatingToExperience(v.rating) + v.experience
if copy == true then
addExp(targetSoul,v.id,experience * perc,v.rating)
else
addExp(targetSoul,v.id,experience * target_perc,v.rating)
addExp(sourceSoul,v.id,-1*(experience * source_perc),v.rating)
end
end
for i=#sourceSoul.preferences-1,0,-1 do
v = sourceSoul.preferences[i]
targetSoul.preferences:insert("#",v)
end
for k,v in pairs(sourceSoul.personality.traits or {}) do
targetSoul.personality.traits[k] = v
end
for i=#sourceSoul.personality.values-1,0,-1 do
v = sourceSoul.personality.values[i]
targetSoul.personality.values:insert("#",v)
end
for i=#sourceSoul.personality.emotions-1,0,-1 do
v = sourceSoul.personality.emotions[i]
targetSoul.personality.emotions:insert("#",v)
end
for i=#sourceSoul.personality.dreams-1,0,-1 do
v = sourceSoul.personality.dreams[i]
targetSoul.personality.dreams:insert("#",v)
end
targetSoul.personality.civ_id = sourceSoul.personality.civ_id
targetSoul.personality.cultural_identity = sourceSoul.personality.cultural_identity
targetSoul.personality.stress_level = sourceSoul.personality.stress_level
targetSoul.unit_id = df.global.unit_next_id
df.global.unit_next_id=df.global.unit_next_id+1
return {sourceSoul = sourceSoul, targetSoul = targetSoul}
end
function skillRatingToExperience(rating)
local total = 0
for i=0, rating-1, 1 do
total = total + (100 * i) + 500
end
return total
end
function getSoulValues (soul)
if soul == nil then return nil end
total_attribute_value = 0
total_skill_value = 0
for k,v in ipairs(soul.mental_attrs or {}) do
--local effectivevalue = (v.value + v.max_value)/2
local effectivevalue = v.value
total_attribute_value = total_attribute_value + effectivevalue
end
local avg_attribute_value = total_attribute_value/#soul.mental_attrs
for k,v in ipairs(soul.skills or {}) do
local effectivevalue = skillRatingToExperience(v.rating) + v.experience
total_skill_value = total_skill_value + effectivevalue
end
return {attrs = avg_attribute_value, skills = total_skill_value, stress = soul.personality.stress_level}
end
function qualitySymbol(value)
if value >= 5 then return string.char(15)
elseif value == 4 then return string.char(240)
elseif value == 3 then return '*'
elseif value == 2 then return '+'
elseif value == 1 then return '-'
elseif value == 0 then return ' '
else return '' end
end
function wearSymbol(value)
if value >= 3 then return 'XX'
elseif value == 2 then return 'X'
elseif value == 1 then return 'x'
else return '' end
end
function sexSymbol(value)
if value == 0 then return ', '..string.char(12)
elseif value == 1 then return ', '..string.char(11)
else return '' end
end
function getSoulItemProps (soul)
if soul == nil then return nil end
local values = getSoulValues(soul)
local attrvalue = values.attrs
local attrquality = 0
if attrvalue > 2000 then attrquality = 5
elseif attrvalue > 1300 then attrquality = 4
elseif attrvalue > 1100 then attrquality = 2
elseif attrvalue > 1000 then attrquality = 1
else attrquality = 0 end
local expvalue = values.skills
local expquality = 0
if expvalue > 30000 then expquality = 5
elseif expvalue > 10000 then expquality = 4
elseif expvalue > 7500 then expquality = 3
elseif expvalue > 5000 then expquality = 2
elseif expvalue > 2500 then expquality = 1
else expquality = 0 end
local wear = 0
if values.stress >= 100000 then wear = 1
elseif values.stress >= 250000 then wear = 2
elseif values.stress >= 500000 then wear = 3 end
return {decquality = expquality, quality = attrquality, wear = wear}
end
function getSoulName(soul)
itemprops = getSoulItemProps(soul)
qsym1 = qualitySymbol(itemprops.quality)
if qsym1 == ' ' then qsym1 = '' end
qsym2 = qualitySymbol(itemprops.decquality)
decsym1 = qsym2..string.char(174)
decsym2 = string.char(175)..qsym2
sexsym = sexSymbol(soul.sex)
if qsym2 == '' then
decsym1 = ''
decsym2 = ''
end
wsym = wearSymbol(itemprops.wear)
name = dfhack.TranslateName(soul.name)
if name == '' then name = (df.global.world.raws.creatures.all[soul.race]).caste[soul.caste].caste_name[0]..' soul'..sexsym
else name = 'soul of ' .. name .. ', ' .. (df.global.world.raws.creatures.all[soul.race]).caste[soul.caste].caste_name[0] .. sexsym end
return(wsym..decsym1..qsym1..name..qsym1..decsym2..wsym)
end
function SoulToConstruct(reaction,unit,jobitems,constructrace,constructcaste,position,usemats,sourceSoul,validtargets)
if validtargets['CONSTRUCT'] == true then usemats = true else usemats = false end
if constructrace == nil then
constructrace = sourceSoul.race
constructcaste = sourceSoul.caste
elseif constructcaste == nil then
constructcaste = SoulForm(sourceSoul,constructrace).caste
end
local position = {x = unit.pos.x,y = unit.pos.y,z = unit.pos.z}
local construct = MakeConstruct(reaction,unit,jobitems,constructrace,constructcaste,position,usemats,sourceSoul)
return construct
end
-- ConstructCreature
--This is for the itemcorpses
eventful.onUnitDeath.bla=function(u_id)
local unit = df.unit.find(u_id)
if hasClass(unit,"DFHACK_CONSTRUCTCREATURE_ITEMCORPSE") then
local itemcorpse = nil
for i=0,#unit.corpse_parts-1,1 do
part=df.item.find(unit.corpse_parts[i])
if getmetatable(part) ~= "item_corpsepiecest" then
itemcorpse = part
end
end
if itemcorpse ~= nil then
mat_type = unit.body.body_plan.materials.mat_type[0]
mat_index = unit.body.body_plan.materials.mat_index[0]
itemcorpse.mat_type = mat_type
itemcorpse.mat_index = mat_index
end
end
end
eventful.enableEvent(eventful.eventType.UNIT_DEATH,5)
function ConstructCreature(reaction, unit, in_items, in_reag, out_items, call_native)
--local skill = reaction.products[0].probability + dfhack.units.getEffectiveSkill(unit,reaction.skill)
--local race = reaction.products[0].mat_index --material index of output. If creature_mat:name:NONE, will be index of creature species
--local caste = reaction.products[0].product_dimension
call_native = false
local jobitems = unit.job.current_job.items
local jobmats = saveJobItemMats(reaction,jobitems)
codearray = split(reaction.code,' ')
if #codearray >= 2 then race = codearray[2] end
if #codearray >= 3 then caste = codearray[3] end
race = findRace(race)
print(race)
caste = findCaste(race,caste)
local pos={}
pos.x=unit.pos.x
pos.y=unit.pos.y
pos.z=unit.pos.z
local construct = MakeConstruct(reaction,unit,jobmats,race,caste,pos,true,soul)
if construct ~= nil then
call_native = true
end
end
function SoulShuffle(reaction, unit, in_items, in_reag, out_items, call_native)
call_native = false
local jobmats = saveJobItemMats(reaction,unit.job.current_job.items)
local operation = nil
local opextra = nil
local efficiency = nil
local attack = nil
local attack_success = nil
local validsources = {}
local validtargets = {}
local invert = false
local subject = nil
codearray = split(reaction.code,' ')
for i,code in ipairs(codearray or {}) do
if code == 'MOVE' or
code == 'SPLIT' or
code == 'COMBINE' or
code == 'REPLACE' or
code == 'SWAP' or
code == 'DESTROY' or
code == 'ASSIMILATE' then
operation = code
elseif code == 'ATTACK' then
if codearray[i+1] == tonumber(codearray[i+1]) then
attack = tonumber(codearray[i+1])
if codearray[i+2] == tonumber(codearray[i+2]) then
attack_success = tonumber(codearray[i+2])
i = i+2
else
attack_success = 100
i = i+1
end
else
attack = 100
attack_success = 50
end
elseif code == 'MERGE' then
operation = 'COMBINE'
elseif code == 'FUSE' then
operation = 'COMBINE'
opextra = 'FUSE'
elseif code == 'COPY' then
operation = 'SPLIT'
opextra = 'COPY'
elseif code == 'NOT' then invert = true
elseif code == 'FROM' then
subject = validsources
elseif code == 'TO' then
subject = validtargets
elseif code == 'CONSTRUCT' or code == 'SPAWN' then
setValid(subject,code,not invert)
subject['RACE'] = codearray[i+1]
subject['CASTE'] = codearray[i+2]
i = i+2
elseif code == 'CREATURE' then
if subject['CREATURE'] == nil then
subject['CREATURE'] = {}
end
table.insert(subject['CREATURE'],{race = codearray[i+1], caste = codearray[i+2], valid = not invert})
i = i+2
elseif code == 'CLASS' then
if subject['CLASS'] == nil then
subject['CLASS'] = {}
end
table.insert(subject['CLASS'],{class = codearray[i+1], valid = not invert})
i = i+1
elseif code == 'RANGE' then
subject['RANGE'] = tonumber(codearray[i+1])
i = i+1
elseif code == 'Z' then
subject['Z'] = tonumber(codearray[i+1])
i = i+1
elseif code == 'PERC' then
subject['PERC'] = tonumber(codearray[i+1])
i = i+1
elseif code == 'STRESS_ADD' then
subject['STRESS_ADD'] = tonumber(codearray[i+1])
i = i+1
elseif code == 'STRESS_PERC' then
subject['STRESS_PERC'] = tonumber(codearray[i+1])
i = i+1
elseif code == 'MIN_ATTR' then
subject['MIN_ATTR'] = tonumber(codearray[i+1])
i = i+1
elseif code == 'MAX_ATTR' then
subject['MAX_ATTR'] = tonumber(codearray[i+1])
i = i+1
elseif code == 'CAN_BREAK' then
if codearray[i+1] == tonumber(codearray[i+1]) then
subject['CAN_BREAK'] = tonumber(codearray[i+1])
i = i+1
else
if subject['MIN_ATTR'] ~= nil then
subject['CAN_BREAK'] = subject['MIN_ATTR'] / 2
else
subject['CAN_BREAK'] = 50
end
end
elseif code == 'CIV' then
setValid(subject,'CIV_UNIT',not invert)
setValid(subject,'CIV_SOUL',not invert)
lastkey = code
else --anything else
setValid(subject,code,not invert)
lastkey = code
end
if code ~= 'NOT' then invert = false end
end
--The operation should now be analyzed. Proceed for soul manipulation.
local sourceObject = nil
local sourceSoul = nil
local targetObject = nil
local targetSoul = nil
allUnits = df.global.world.units.active
radius = 10
z = 0
s_radius = nil
t_radius = nil
s_z = nil
t_z = nil
if validsources['RANGE'] ~= nil then s_radius = validsources['RANGE'] end
if validtargets['RANGE'] ~= nil then t_radius = validtargets['RANGE'] end
if validsources['Z'] ~= nil then s_z = validsources['Z'] end
if validtargets['Z'] ~= nil then t_z = validtargets['Z'] end
if s_radius == nil and t_radius ~= nil then s_radius = t_radius
elseif t_radius == nil and s_radius ~= nil then t_radius = s_radius
else
if t_radius == nil then t_radius = radius end
if s_radius == nil then s_radius = radius end
end
if s_z == nil and t_z ~= nil then s_z = t_z
elseif t_z == nil and s_z ~= nil then t_z = s_z
else
if t_z == nil then t_z = z end
if s_z == nil then s_z = z end
end
local sourceObjects = {}
local targetObjects = {}
--[[
if validsources['ITEM'] == true then
item = getReagentItemByProduct(reaction,unit,0)
if item ~= nil then table.insert(sourceObjects, item) end
end
]]--
for i=#allUnits-1,0,-1 do
u = allUnits[i]
if (closeBy(u,unit,s_radius,s_z) and validsources['SELF'] ~= true) or (u == unit and validsources['SELF'] ~= false) then
if checkIfValidUnit(u,validsources) then table.insert(sourceObjects, u) end
end
end
for i=#allUnits-1,0,-1 do
u = allUnits[i]
if (closeBy(u,unit,t_radius,t_z) and validtargets['SELF'] ~= true) or (u == unit and validtargets['SELF'] ~= false) then
if checkIfValidUnit(u,validtargets) and
not (hasClass(u,'DFHACK_SOULSHUFFLE_ONE_SOUL_LIMIT') and #u.status.souls ~= 0 and (operation == 'MOVE' or operation == 'SPLIT')) then --Hack to enforce one soul limit
table.insert(targetObjects, u)
end
end
end
needTargetObject = true
needTargetSoul = true
if operation == 'MOVE' or operation == 'SPLIT' then
needTargetSoul = false
end
if operation == 'DESTROY' or validtargets['CONSTRUCT'] == true or validtargets['SPAWN'] == true then
needTargetSoul = false
needTargetObject = false
end
--Get the souls to be manipulated
script.start(function()
finished1 = false
finished2 = false
finished3 = false
finished = false
while finished ~= true do
local opt_names = {}
local options = {}
for _,object in ipairs(sourceObjects or {}) do
if #getObjectSouls(object,validsources) > 0 then
table.insert(opt_names,getObjectName(object))
table.insert(options,object)
end
end
local choiceok,choice=script.showListPrompt('Find souls', 'Select a source.', COLOR_LIGHTGREEN, opt_names)
if choice ~= nil then
sourceObject = options[choice]
while finished1 ~= true and finished ~= true do
local opt_names = {}
local options = {}
for _,soul in ipairs(getObjectSouls(sourceObject,validsources) or {}) do
table.insert(opt_names,getSoulName(soul))
table.insert(options,soul)
end
local choiceok,choice=script.showListPrompt('Souls of '..dfhack.TranslateName(sourceObject.name), 'Select source soul.', COLOR_LIGHTGREEN, opt_names)
if choice ~= nil then
sourceSoul = options[choice]
if needTargetObject then
invalidSouls = {}
table.insert(invalidSouls,sourceSoul)
while finished2 ~= true and finished ~= true do
local opt_names = {}
local options = {}
for _,object in ipairs(targetObjects or {}) do
if validtargets['EMPTY'] ~= true or #getObjectSouls(object,validtargets,invalidSouls) == 0 then
table.insert(opt_names,getObjectName(object))
table.insert(options,object)
end
end
if validtargets['SAME'] == true then
if validtargets['EMPTY'] ~= true or #getObjectSouls(sourceObject,validtargets,invalidSouls) == 0 then
table.insert(opt_names,getObjectName(sourceObject))
table.insert(options,sourceObject)
end
end
--[[
if validtargets['ITEM'] == true then
table.insert(opt_names,'Container')
table.insert(options,'product')
end
]]--
local choiceok,choice=script.showListPrompt('Prepare for soul infusion', 'Select a target.', COLOR_LIGHTGREEN, opt_names)
if choice ~= nil then
targetObject = options[choice]
if needTargetSoul then
while finished3 ~= true and finished ~= true do
local opt_names = {}
local options = {}
for _,soul in ipairs(getObjectSouls(targetObject,validtargets) or {}) do
table.insert(opt_names,getSoulName(soul))
table.insert(options,soul)
end
local choiceok,choice=script.showListPrompt('Souls of '..dfhack.TranslateName(targetObject.name), 'Select target soul.', COLOR_LIGHTGREEN, opt_names)
if choice ~= nil then
targetSoul = options[choice]
finished = true
else --Target soul was not selected
targetSoul = nil
finished3 = true
end
end
else
finished = true -- Don't need target soul
end
else --Target unit was not selected
targetObject = nil
finished2 = true
end
end
else
finished = true --Don't need target unit
end
else --Source soul was not selected
sourceSoul = nil
finished1 = true
end
end
else --Source unit was not selected
sourceObject = nil
finished = true
end
end
--Perform the operation
local success = false
if sourceObject ~= nil and sourceSoul ~= nil and (targetObject ~= nil or needTargetObject == false) and (targetSoul ~= nil or needTargetSoul == false) then
if validtargets['STRESS_PERC'] ~= nil then
SetStress(targetSoul,true,validtargets['STRESS_PERC'])
end
if validtargets['STRESS_ADD'] ~= nil then
SetStress(targetSoul,false,validtargets['STRESS_ADD'])
end
if validsources['STRESS_PERC'] ~= nil then
SetStress(sourceSoul,true,validsources['STRESS_PERC'])
end
if validsources['STRESS_ADD'] ~= nil then
SetStress(sourceSoul,false,validsources['STRESS_ADD'])
end
local canProceed = true
if attack ~= nil then
local allySoul = nil
local enemySoul = nil
if sourceSoul ~= nil then
if isAlliedSoul(sourceSoul) == true then allySoul = sourceSoul else enemySoul = sourceSoul end
elseif sourceObject ~= nil then
if isAlliedSoul(sourceObject.status.current_soul) == true then allySoul = sourceObject.status.current_soul else enemySoul = sourceObject.status.current_soul end
end
if targetSoul ~= nil then
if isAlliedSoul(targetSoul) == true then allySoul = targetSoul else enemySoul = targetSoul end
elseif targetObject ~= nil and targetObject.status.current_soul ~= nil then
if isAlliedSoul(targetObject.status.current_soul) == true then allySoul = targetObject.status.current_soul else enemySoul = targetObject.status.current_soul end
end
if enemySoul ~= nil then
if allySoul == nil then allySoul = unit.status.current_soul end
local r = SoulAttack(allySoul,enemySoul,attack,attack_success)
allySoul.personality.stress_level = r.sourceSoul.personality.stress_level
enemySoul.personality.stress_level = r.targetSoul.personality.stress_level
canProceed = false
if r.success == true then canProceed = true end
end
end
local message_displayed = false
if sourceSoul ~= nil then
local values = getSoulValues(sourceSoul)
if validsources['MIN_ATTR'] ~= nil then
if values.attrs < validsources['MIN_ATTR'] then
dfhack.gui.showAnnouncement( dfhack.TranslateName(unit.name).." cancels "..reaction.name..": Soul is too weak." , COLOR_RED, true)
message_displayed = true
canProceed = false
end
end
if validsources['MAX_ATTR'] ~= nil then
if values.attrs > validsources['MAX_ATTR'] then
dfhack.gui.showAnnouncement( dfhack.TranslateName(unit.name).." cancels "..reaction.name..": Soul is too strong." , COLOR_RED, true)
message_displayed = true
canProceed = false
end
end
if canProceed == true then
if validsources['CAN_BREAK'] ~= nil then
if SoulBreak(sourceSoul,validsources['CAN_BREAK']) then
if getmetatable(sourceObject) == 'unit' then RemoveSoul(sourceObject,sourceSoul) else RemoveSoulAbstract(sourceSoul) end
dfhack.gui.showAnnouncement( dfhack.TranslateName(unit.name).." cancels "..reaction.name..": Soul was destroyed." , COLOR_FUCHSIA, true)
message_displayed = true
canProceed = false
end
end
end
end
if targetSoul ~= nil then
local values = getSoulValues(targetSoul)
if validtargets['MIN_ATTR'] ~= nil then
if values.attrs < validtargets['MIN_ATTR'] then
dfhack.gui.showAnnouncement( dfhack.TranslateName(unit.name).." cancels "..reaction.name..": Soul is too weak." , COLOR_RED, true)
message_displayed = true
canProceed = false
end
end
if validtargets['MAX_ATTR'] ~= nil then
if values.attrs > validtargets['MAX_ATTR'] then
dfhack.gui.showAnnouncement( dfhack.TranslateName(unit.name).." cancels "..reaction.name..": Soul is too strong." , COLOR_RED, true)
message_displayed = true
canProceed = false
end
end
if canProceed == true then
if validtargets['CAN_BREAK'] ~= nil then
if SoulBreak(targetSoul,validtargets['CAN_BREAK']) then
if getmetatable(targetObject) == 'unit' then RemoveSoul(targetObject,targetSoul) else RemoveSoulAbstract(targetSoul) end
dfhack.gui.showAnnouncement( dfhack.TranslateName(unit.name).." cancels "..reaction.name..": Soul was destroyed." , COLOR_FUCHSIA, true)
message_displayed = true
canProceed = false
end
end
end
end
if canProceed == true then
if operation == 'MOVE' then
if getmetatable(sourceObject) == 'unit' then RemoveSoul(sourceObject,sourceSoul) else RemoveSoulAbstract(sourceSoul) end
if validtargets['CONSTRUCT'] == true or validtargets['SPAWN'] == true then
local construct = SoulToConstruct(reaction,unit,jobmats,validtargets['RACE'],validtargets['CASTE'],position,usemats,sourceSoul,validtargets)
success = true
else
if targetObject == 'container' then CreateSouljar(reaction,unit,sourceSoul)
elseif getmetatable(targetObject) == 'unit' then InsertSoul(targetObject,sourceSoul,validtargets['TOP']) end
success = true
end
elseif operation == 'SWAP' then
if getmetatable(sourceObject) == 'unit' then RemoveSoul(sourceObject,sourceSoul) else RemoveSoulAbstract(sourceSoul) end
if getmetatable(targetObject) == 'unit' then RemoveSoul(targetObject,targetSoul) else RemoveSoulAbstract(targetSoul) end
if getmetatable(sourceObject) == 'unit' then InsertSoul(sourceObject,targetSoul) else CreateSouljar(reaction,unit,targetSoul) end
if getmetatable(targetObject) == 'unit' then InsertSoul(targetObject,sourceSoul,validtargets['TOP']) else CreateSouljar(reaction,unit,sourceSoul) end
success = true
elseif operation == 'COMBINE' then
targetSoul = CombineSouls(sourceSoul,targetSoul,opextra,validsources['PERC'])
if targetSoul ~= nil then
if getmetatable(sourceObject) == 'unit' then RemoveSoul(sourceObject,sourceSoul) else RemoveSoulAbstract(sourceSoul) end
if getmetatable(targetObject) == 'unit' then setCurrentSoulValues(targetObject) end
success = true
end
elseif operation == 'SPLIT' then
newSouls = SplitSoul(sourceSoul,opextra,validsources['PERC'])
if newSouls['sourceSoul'] ~= nil and newSouls['targetSoul'] ~= nil then
if validtargets['CONSTRUCT'] == true or validtargets['SPAWN'] == true then
local construct = SoulToConstruct(reaction,unit,jobmats,validtargets['RACE'],validtargets['CASTE'],position,usemats,newSouls['targetSoul'],validtargets)
success = true
else
if getmetatable(targetObject) == 'unit' then InsertSoul(targetObject,newSouls['targetSoul'],validtargets['TOP']) end
success = true
end
end
elseif operation == 'REPLACE' then
if getmetatable(sourceObject) == 'unit' then RemoveSoul(sourceObject,sourceSoul) else RemoveSoulAbstract(sourceSoul) end
if getmetatable(targetObject) == 'unit' then RemoveSoul(targetObject,targetSoul) else RemoveSoulAbstract(targetSoul) end
if getmetatable(sourceObject) == 'unit' then InsertSoul(targetObject,sourceSoul) else CreateSouljar(reaction,unit,sourceSoul) end
elseif operation == 'DESTROY' then
if getmetatable(sourceObject) == 'unit' then RemoveSoul(sourceObject,sourceSoul) else RemoveSoulAbstract(sourceSoul) end
success = true
elseif operation == 'ASSIMILATE' then
targetSoul.civ_id = sourceSoul.civ_id
if getmetatable(targetObject) == 'unit' then setCurrentSoulValues(targetObject) end
success = true
end
if success == false then
dfhack.gui.showAnnouncement( dfhack.TranslateName(unit.name).." cancels "..reaction.name..": Soul manipulation failed." , COLOR_RED, true)
end
else
if message_displayed == false then
dfhack.gui.showAnnouncement( dfhack.TranslateName(unit.name).." cancels "..reaction.name..": Could not overcome resistance." , COLOR_RED, true)
end
end
if success then
call_native = true
else
for _,v in ipairs(in_reag or {}) do
v.flags.PRESERVE_REAGENT = true
end
call_native = false
end
else
for _,v in ipairs(in_reag or {}) do
v.flags.PRESERVE_REAGENT = true
end
call_native = false
dfhack.gui.showAnnouncement( dfhack.TranslateName(unit.name).." cancels "..reaction.name..": No valid targets." , COLOR_RED, true)
end
end)
end
function SoulBreak(soul,prob)
local values = getSoulValues(soul)
if math.random(500000) < values.stress - 500000 - (values.attrs / prob) or math.random(prob*10) > values.attrs then
return true
else
return false
end
end
function SetStress(soul,perc,amount)
local add_amount = 0
if perc == true then
add_amount = soul.personality.stress_level * (amount/100)
else
add_amount = amount
end
soul.personality.stress_level = soul.personality.stress_level + add_amount
return soul
end
function SoulAttack(sourceSoul,targetSoul,level,success_chance)
if level == nil then level = 100 end
if success_chance == nil then success_chance = 100 end
local sourceValues = getSoulValues(sourceSoul)
local targetValues = getSoulValues(targetSoul)
if sourceValues.stress < 1 then sourceValues.stress = 1 end
if targetValues.stress < 1 then targetValues.stress = 1 end
local sourcePower = sourceValues.attrs / sourceValues.stress
local targetPower = targetValues.attrs / targetValues.stress
local effectiveSourcePower = (sourcePower / targetPower) * level
local effectiveTargetPower = (targetPower / sourcePower) * level
print("sourcePower = " .. effectiveSourcePower)
print("targetPower = " .. effectiveTargetPower)
if math.random(success_chance) < effectiveSourcePower / effectiveTargetPower then
success = true
else
success = false
end
sourceSoul.personality.stress_level = sourceSoul.personality.stress_level + effectiveTargetPower * level
targetSoul.personality.stress_level = targetSoul.personality.stress_level + effectiveSourcePower * level
print("sourceStress = " .. sourceSoul.personality.stress_level)
print("targetStress = " .. targetSoul.personality.stress_level)
return {sourceSoul = sourceSoul, targetSoul = targetSoul, success = success}
end
function MakeConstruct(reaction,worker,jobmats,race,caste,pos,usemats,soul)
race = findRace(race)
caste = findCaste(race,caste)
if caste == nil or caste < 0 then
caste = 0
end
if race == -1 then
print("Error: Creature not found...")
end
if usemats == true then
--Handle colors
for i,v in ipairs(df.global.world.raws.creatures.all[race].caste[0].creature_class) do
if string.starts(v.value, 'DFHACK_CONSTRUCTCREATURE_CASTECOLORS') then
firstmat = df.global.world.raws.creatures.all[race].material[0]
code = firstmat.id
local r_mat = nil
if jobmats[code] then r_mat = jobmats[code] end
if r_mat ~= nil then
local r_mat_color_0=r_mat.material.basic_color[0]
local r_mat_color_1=r_mat.material.basic_color[1]
local c = r_mat_color_0 .. r_mat_color_1
if c == '00' then caste = 0
elseif c == '10' then caste = 1
elseif c == '20' then caste = 2
elseif c == '30' then caste = 3
elseif c == '40' then caste = 4
elseif c == '50' then caste = 5
elseif c == '60' then caste = 6
elseif c == '70' then caste = 7
elseif c == '01' then caste = 8
elseif c == '11' then caste = 9
elseif c == '21' then caste = 10
elseif c == '31' then caste = 11
elseif c == '41' then caste = 12
elseif c == '51' then caste = 13
elseif c == '61' then caste = 14
elseif c == '71' then caste = 15 end
break
else
print('Could not find valid reagent item for color caste system.')
end
end
end
end
race_name = df.creature_raw.find(race).name[0]
caste_name = df.creature_raw.find(race).caste[caste].caste_name[0]
--Create a new unit
local u=PlaceUnit(race,caste,nil,pos)
u.civ_id = worker.civ_id
if soul ~= nil then
InsertSoul(u,soul)
u.name:assign(soul.name)
elseif not string.find(reaction.code,'NOSOUL') then
local castedata=getCaste(race,caste)
soul = CreateSoul(u,castedata)
InsertSoul(u,soul)
end
if u then
if usemats == true then
for i=0,#u.body.body_plan.materials.mat_type-1,1 do
local matbase = dfhack.matinfo.decode(u.body.body_plan.materials.mat_type[i],u.body.body_plan.materials.mat_index[i])
local code = ''
if matbase.mode == 'creature' or matbase.mode == 'plant' then
code = matbase.material.id
elseif matbase.mode == 'inorganic' then
code = matbase.inorganic.id
end
local r_item = getReagentItemByCode(reaction,jobmats,code)
r_mat = nil
if jobmats[code] then r_mat = jobmats[code] end
if r_mat ~= nil then
local r_mat_index=r_mat.index
local r_mat_type=r_mat.type
u.body.body_plan.materials.mat_index[i] = r_mat_index
u.body.body_plan.materials.mat_type[i] = r_mat_type
end
end
baseMaterial = dfhack.matinfo.decode(u.body.body_plan.materials.mat_type[0],u.body.body_plan.materials.mat_index[0])
if baseMaterial ~= nil then
r_mat_name = baseMaterial.material.state_name.Solid
r_mat_adj=baseMaterial.material.state_adj.Solid
u.custom_profession=r_mat_adj .. ' ' .. caste_name
end
end
return u
else
return nil
end
end
--This part is mostly copied from warmist's spawnunit script
function getCaste(race_id,caste_id)
local cr=df.creature_raw.find(race_id)
return cr.caste[caste_id]
end
function genBodyModifier(body_app_mod)
local a=rando:random(#body_app_mod.ranges-2)-1
if a<0 then a=a+(-2*a) end
return rando:random(body_app_mod.ranges[a+1]-body_app_mod.ranges[a])+body_app_mod.ranges[a]
end
function getBodySize(caste,time)
--todo real body size...
return caste.body_size_1[#caste.body_size_1-1] --returns last body size
end
function genAttribute(array)
local a=rando:random(#array-2)-1
if a<0 then a=a+(-2*a) end
return rando:random(array[a+1]-array[a])+array[a]
end
function norm()
return math.sqrt((-2)*math.log(rando:drandom()))*math.cos(2*math.pi*rando:drandom())
end
function normalDistributed(mean,sigma)
return mean+sigma*norm()
end
function clampedNormal(min,median,max)
local val=normalDistributed(median,math.sqrt(max-min))
if val<min then return min end
if val>max then return max end
return val
end
function CreateSoul(unit,caste)
local tmp_soul=df.unit_soul:new()
tmp_soul.unit_id=unit.id
tmp_soul.name:assign(unit.name)
tmp_soul.race=unit.race
tmp_soul.sex=unit.sex
tmp_soul.caste=unit.caste
--todo skills,preferences,traits.
local attrs=caste.attributes
for k,v in pairs(attrs.ment_att_range) do
local max_percent=attrs.ment_att_cap_perc[k]/100
local cvalue=genAttribute(v)
tmp_soul.mental_attrs[k]={value=cvalue,max_value=cvalue*max_percent}
end
for k,v in pairs(tmp_soul.traits) do
local min,mean,max
min=caste.personality.a[k]
mean=caste.personality.b[k]
max=caste.personality.c[k]
tmp_soul.traits[k]=clampedNormal(min,mean,max)
end
return tmp_soul
end
function CreateUnit(race_id,caste_id)
local race=df.creature_raw.find(race_id)
if race==nil then error("Invalid race_id") end
local caste=getCaste(race_id,caste_id)
local unit=df.unit:new()
unit.race=race_id
unit.caste=caste_id
unit.id=df.global.unit_next_id
df.global.unit_next_id=df.global.unit_next_id+1
if caste.misc.maxage_max==-1 then
unit.relations.old_year=-1
else
unit.relations.old_year=df.global.cur_year-math.ceil(caste.misc.maxage_min/2)+rando:random(caste.misc.maxage_max-caste.misc.maxage_min)+caste.misc.maxage_min
end
unit.sex=caste.gender
local body=unit.body
body.body_plan=caste.body_info
local body_part_count=#body.body_plan.body_parts
local layer_count=#body.body_plan.layer_part
--components
unit.relations.birth_year=df.global.cur_year-math.ceil(caste.misc.maxage_min/2)
--unit.relations.birth_time=??
--unit.relations.old_time=?? --TODO add normal age
local cp=body.components
cp.body_part_status:resize(body_part_count)
cp.numbered_masks:resize(#body.body_plan.numbered_masks)
for num,v in ipairs(body.body_plan.numbered_masks) do
cp.numbered_masks[num]=v
end
cp.layer_status:resize(layer_count)
cp.layer_wound_area:resize(layer_count)
cp.layer_cut_fraction:resize(layer_count)
cp.layer_dent_fraction:resize(layer_count)
cp.layer_effect_fraction:resize(layer_count)
local attrs=caste.attributes
for k,v in pairs(attrs.phys_att_range) do
local max_percent=attrs.phys_att_cap_perc[k]/100
local cvalue=genAttribute(v)
unit.body.physical_attrs[k]={value=cvalue,max_value=cvalue*max_percent}
--unit.body.physical_attrs:insert(k,{new=true,max_value=genMaxAttribute(v),value=genAttribute(v)})
end
body.blood_max=getBodySize(caste,0) --TODO normal values
body.blood_count=body.blood_max
body.infection_level=0
unit.status2.body_part_temperature:resize(body_part_count)
for k,v in pairs(unit.status2.body_part_temperature) do
unit.status2.body_part_temperature[k]={new=true,whole=10067,fraction=0}
end
--------------------
local stuff=unit.enemy
stuff.body_part_878:resize(body_part_count) -- all = 3
stuff.body_part_888:resize(body_part_count) -- all = 3
stuff.body_part_relsize:resize(body_part_count) -- all =0
stuff.anon_4 = -1
stuff.anon_5 = -1
stuff.anon_6 = -1
--TODO add correct sizes. (calculate from age)
local size=caste.body_size_2[#caste.body_size_2-1]
body.size_info.size_cur=size
body.size_info.size_base=size
body.size_info.area_cur=math.pow(size,0.666)
body.size_info.area_base=math.pow(size,0.666)
body.size_info.area_cur=math.pow(size*10000,0.333)
body.size_info.area_base=math.pow(size*10000,0.333)
stuff.were_race=race_id
stuff.were_caste=caste_id
stuff.normal_race=race_id
stuff.normal_caste=caste_id
stuff.body_part_8a8:resize(body_part_count) -- all = 1
stuff.body_part_base_ins:resize(body_part_count)
stuff.body_part_clothing_ins:resize(body_part_count)
stuff.body_part_8d8:resize(body_part_count)
unit.recuperation.healing_rate:resize(layer_count)
--appearance
local app=unit.appearance
app.body_modifiers:resize(#caste.body_appearance_modifiers) --3
for k,v in pairs(app.body_modifiers) do
app.body_modifiers[k]=genBodyModifier(caste.body_appearance_modifiers[k])
end
app.bp_modifiers:resize(#caste.bp_appearance.modifier_idx) --0
for k,v in pairs(app.bp_modifiers) do
app.bp_modifiers[k]=genBodyModifier(caste.bp_appearance.modifiers[caste.bp_appearance.modifier_idx[k]])
end
--app.unk_4c8:resize(33)--33
app.tissue_style:resize(#caste.bp_appearance.style_part_idx)
app.tissue_style_civ_id:resize(#caste.bp_appearance.style_part_idx)
app.tissue_style_id:resize(#caste.bp_appearance.style_part_idx)
app.tissue_style_type:resize(#caste.bp_appearance.style_part_idx)
app.tissue_length:resize(#caste.bp_appearance.style_part_idx)
app.genes.appearance:resize(#caste.body_appearance_modifiers+#caste.bp_appearance.modifiers) --3
app.genes.colors:resize(#caste.color_modifiers*2) --???
app.colors:resize(#caste.color_modifiers)--3
df.global.world.units.all:insert("#",unit)
df.global.world.units.active:insert("#",unit)
--todo set weapon bodypart
local num_inter=#caste.body_info.interactions
--unit.curse.anon_5:resize(num_inter)
return unit
end
function findRace(name)
if tonumber(name) ~= nil then return name end
for k,v in pairs(df.global.world.raws.creatures.all) do
if v.creature_id==name then
return k
end
end
return nil
end
function findCaste(race,name)
if tonumber(name) ~= nil then return name end
for k,v in pairs(df.global.world.raws.creatures.all[race].caste) do
if v.caste_id==name then
return k
end
end
return nil
end
function PlaceUnit(race,caste,name,position)
local pos=position or copyall(df.global.cursor)
if pos.x==-30000 then
qerror("Point your pointy thing somewhere")
end
race=findRace(race)
local u=CreateUnit(race,tonumber(caste) or 0)
u.pos:assign(pos)
if name then
u.name.first_name=name
u.name.has_name=true
end
u.civ_id=df.global.ui.civ_id
local desig,ocupan=dfhack.maps.getTileFlags(pos)
if ocupan.unit then
ocupan.unit_grounded=true
u.flags1.on_ground=true
else
ocupan.unit=true
end
createNemesis(u)
return u
end
function createFigure(trgunit)
local hf=df.historical_figure:new()
hf.id=df.global.hist_figure_next_id
hf.race=trgunit.race
hf.caste=trgunit.caste
df.global.hist_figure_next_id=df.global.hist_figure_next_id+1
hf.name.first_name=trgunit.name.first_name
hf.name.has_name=trgunit.name.has_name
df.global.world.history.figures:insert("#",hf)
return hf
end
function createNemesis(trgunit)
local id=df.global.nemesis_next_id
local nem=df.nemesis_record:new()
nem.id=id
nem.unit_id=trgunit.id
nem.unit=trgunit
nem.flags:resize(1)
nem.flags[4]=true
nem.flags[5]=true
nem.flags[6]=true
nem.flags[7]=true
nem.flags[8]=true
nem.flags[9]=true
--[[for k=4,8 do
nem.flags[k]=true
end]]
df.global.world.nemesis.all:insert("#",nem)
df.global.nemesis_next_id=id+1
trgunit.general_refs:insert("#",{new=df.general_ref_is_nemesisst,nemesis_id=id})
trgunit.flags1.important_historical_figure=true
local gen=df.global.world.worldgen
nem.save_file_id=gen.next_unit_chunk_id;
gen.next_unit_chunk_id=gen.next_unit_chunk_id+1
gen.next_unit_chunk_offset=gen.next_unit_chunk_offset+1
--[[ local gen=df.global.world.worldgen
gen.next_unit_chunk_id
gen.next_unit_chunk_offset
]]
nem.figure=createFigure(trgunit)
end
function SoulForm(soul,race)
local race = findRace(race)
local caste = nil
if race == nil or race < 0 then
race = soul.race
caste = soul.caste
else
castes = df.global.world.raws.creatures.all[race].caste
soul_race = df.global.world.raws.creatures.all[soul.race].race_id
soul_caste = df.global.world.raws.creatures.all[soul.race].caste[soul.caste].caste_id
soul_classes = df.global.world.raws.creatures.all[soul.race].caste[soul.caste].creature_class
match_level = 0
caste = math.random(#castes)-1
for i = 0, #castes - 1, 1 do
v = castes[i]
this_match_level = 0
if string.find(v.caste_id,soul_race) then
if string.find(v.caste_id,soul_caste) then
if v.caste_id == soul_race..' '..soul_caste or v.caste_id == soul_race..'_'..soul_caste then
this_match_level = 7
break
else
this_match_level = 6
end
else
if v.caste_id == soul_caste then
this_match_level = 5
else
this_match_level = 4
end
end
else
for _,class in ipairs(soul_classes or {}) do
if class.value == v.caste_id then
this_match_level = 3
end
end
end
if this_match_level == 0 then
if ((v.caste_id == 'DEFAULT_FEMALE' and soul.sex == 0) or (v.caste_id == 'DEFAULT_MALE' and soul.sex == 1)) then
this_match_level = 2
elseif v.caste_id == 'DEFAULT' then
this_match_level = 1
end
end
if this_match_level > match_level then
match_level = this_match_level
caste = i
end
end
end
return {race = race, caste = caste}
end
function FindDead(reaction, unit, in_items, in_reag, out_items, call_native)
end
function TestFunction(reaction, unit, in_items, in_reag, out_items, call_native)
print('test')
end
function AddUsesString (viewscreen,inp_string,indent)
local string = df.new("string")
string.value = tostring(inp_string)
if not indent then indent = 0 end
viewscreen.entry_ref:insert('#', nil)
viewscreen.entry_indent:insert('#', indent)
viewscreen.unk_34:insert('#', nil)
viewscreen.entry_string:insert('#', string)
end
function string.starts(String,Start)
return string.sub(String,1,string.len(Start))==Start
end
dfhack.onStateChange.loadSoulShuffle = function(code)
if code==SC_MAP_LOADED then
--Load all reactions
for i,reaction in ipairs(df.global.world.raws.reactions) do
if string.starts(reaction.code,'LUA_HOOK_'..prefix..'_FIND_DEAD') then
eventful.registerReaction(reaction.code,FindDead)
registered_reactions = true
elseif string.starts(reaction.code,'LUA_HOOK_'..prefix..'_OTHER') then
eventful.registerReaction(reaction.code,TestFunction)
registered_reactions = true
elseif string.starts(reaction.code,'LUA_HOOK_'..prefix) then
eventful.registerReaction(reaction.code,SoulShuffle)
registered_reactions = true
elseif string.starts(reaction.code,'LUA_HOOK_CONSTRUCTCREATURE') then
eventful.registerReaction(reaction.code,ConstructCreature)
registered_reactions = true
end
end
print('SoulShuffle: Loaded.')
print('ConstructCreature: Loaded.')
elseif code == SC_VIEWSCREEN_CHANGED and dfhack.isWorldLoaded() then
if dfhack.gui.getCurViewscreen()._type == df.viewscreen_itemst then
local scr = dfhack.gui.getCurViewscreen()
--AddUsesString(scr,'babababababab adfasdf')
end
end
end
--Initial call
if dfhack.isMapLoaded() then
dfhack.onStateChange.loadSoulShuffle(SC_MAP_LOADED)
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment