Code: Select all
<?xml version="1.0" ?>
<!DOCTYPE TranscendenceExtension
[
; Type number for Human Space. DO NOT CHANGE!!!
<!ENTITY unidHumanSpaceLibrary "0x00100000">
; Types used by this extension.
<!ENTITY unid912ChainLightning "0xd912ffec">
<!ENTITY it912ChainLightningGun "0xd912ffed">
<!ENTITY ef912ChainLightningBeam "0xd912ffee">
<!ENTITY type912Cheat "0xd912ffef">
]>
; Made for Transcendence v1.8 beta 2 or later.
<TranscendenceExtension UNID="&unid912ChainLightning;"
name= "Chain Lightning Tutorial"
apiVersion= "41"
credits= "PM"
extends= "&unidHumanSpaceLibrary;"
>
; Since we are using resources from the Human Space library, we need to include it!
; Including libraries is the first thing we need to do before anything else in this file.
<Library unid="&unidHumanSpaceLibrary;"/>
; This weapon is a hitscan chain lightning gun. It was balanced for normal play.
; However, this example mod will provide the weapon for free when starting a new game.
<!-- LEVEL 9: Chain Lightning Gun -->
<ItemType UNID="&it912ChainLightningGun;"
name= "chain lightning gun"
attributes= "energyWeapon, majorItem, military"
level= "9"
frequency= "rare"
value= "235000"
mass= "3000"
description= "This weapon discharges lightning that can arc from target to target, up to seven hits maximum. Damage each target takes is divided by number of targets struck."
>
<Image imageID="&rsItemsEI3;" imageX="96" imageY="96" imageWidth="96" imageHeight="96"/>
<Weapon
powerUse= "2500"
>
; All variants should have the same stats except for damage.
<Variants type="charges">
; 0 - Average damage is 21. Most damage, assumes only a single hit and no secondary beams.
<Variant repeating="4" type="continuousBeam" damage="ion:6d6" fireRate="30" lifetime="1" missileSpeed="1000" sound="&snLaserCannon;" />
; 1 - Average damage is 10.5. Half damage, two hits - the main beam and one secondary hit.
<Variant repeating="4" type="continuousBeam" damage="ion:3d6" fireRate="30" lifetime="1" missileSpeed="1000" sound="&snLaserCannon;" />
; 2 - Average damage is 7. Third damage, three hits - main beam and two more hits.
<Variant repeating="4" type="continuousBeam" damage="ion:2d6" fireRate="30" lifetime="1" missileSpeed="1000" sound="&snLaserCannon;" />
; 3 - Average damage is 5. Quarter damage... well, not quite but close enough, four hits.
<Variant repeating="4" type="continuousBeam" damage="ion:2d4" fireRate="30" lifetime="1" missileSpeed="1000" sound="&snLaserCannon;" />
; 4 - Average damage is 4. Fifth damage, roughly, and five hits.
<Variant repeating="4" type="continuousBeam" damage="ion:1d7" fireRate="30" lifetime="1" missileSpeed="1000" sound="&snLaserCannon;" />
; 5 - Average damage is 3.5. Sixth damage, six hits
<Variant repeating="4" type="continuousBeam" damage="ion:1d6" fireRate="30" lifetime="1" missileSpeed="1000" sound="&snLaserCannon;" />
; 6 - Average damage is 3. Seventh damage, seven hits, main beam plus six secondaries. As the last variant, this is the most hits and least damage possible.
<Variant repeating="4" type="continuousBeam" damage="ion:1d5" fireRate="30" lifetime="1" missileSpeed="1000" sound="&snLaserCannon;" />
</Variants>
; NOTE 1: This chain lightning gun distributes damage among all targets equally. If we only hit one target, it takes full damage of 21.
; If we hit seven targets, they take 3 damage each. It is not always possible to have the total damage equal to full if number
; of targets do not divide evenly with full damage. That is okay as long as it is close, no more than a point of damage either way.
; However, the last variant for max hits and least damage should have combined damage equal to full damage of one hit.
;
; NOTE 2: I designed this chain lightning gun to distribute damage because its total damage is roughly the same and stays balanced.
; To implement damage that varies by number of targets hit, the weapon needs several variants that have identical stats except for damage.
;
; I could have used a capacitor and have single target damage applied to all hits and have capacitor drain that varies by number of hits,
; but the problem with that is it does not take long before the capacitor drains and the weapon is stuck hitting only a single target
; and not chaining. Not very chain-lightning like if it only chains for one second then does not for the remainder of the fight.
;
; Also, I had considered additional hits at full damage and no capacitor, which would make it akin to passthrough and balanced likewise.
; That was dismissed for being too weak against only one or two targets and too powerful against more than four or five targets.
;
; In the end, I settled with distributed damage. Maybe a bit weak against many targets, but practical and looks good.
;
; NOTE 3: I used missileSpeed instead of relativisticSpeed because I want full control of beam range, in case I want change it
; while preserving its hitscan nature.
</Weapon>
<Events>
<OnFireWeapon>
(block (
; Maximum targets beyond the first we can hit for this attack.
; We do not want this too high mainly for performance reasons, although if we divide damage too,
; hits cannot be too high anyway if maximum damage is low enough.
(maxVariant 6)
(extraHits maxVariant)
; Because we cannot use GetParameters event in the effectType, we need to do that work here.
; Damage property is DPS over six seconds or 180 ticks.
; After dividing by 180 and returning a float, add 0.5 then chop off the decimal as a way to round the result.
; NOTE: Since our weapon has no charges, it should use the first and most powerful variant.
(damageHP (int (+ (/ (itmGetProperty gItem 'damage) 180) 0.5)))
; Typical width for ion effects.
(theWidth (mathScale damageHP 10 140 20 40 50))
; Scan for our first target.
(theRange (itmGetProperty gItem 'range))
(pixelsPerRange 24)
(startPos aFirePos)
(endPos (sysVectorPolarOffset startPos aFireAngle theRange))
(firstScan (sysHitScan gSource startPos endPos))
; Whatever we hit will become the source of the next beam. If our hitscan does not hit anyone,
; then there will be no source and no more beams beyond the first one our attacker will fire.
(nextSource (@ firstScan 0))
(nextPos (if firstScan (@ firstScan 1) endPos))
; Distance to target and beam length will vary.
; Effect parameters do not accept floats, so we need to convert it to an integer.
(theLength (int (* (sysVectorDistanceExact startPos nextPos) pixelsPerRange)))
(theStruct { length: theLength
width: theWidth
}
)
; For this exercise, secondary beams will have the same range as the primary beam.
(scanRange theRange)
; Don't check friend/enemy/angry because it varies by source, and we have many sources but only one attacker.
(criteria (cat "s t A N:" scanRange " S:d"))
; Objects in this list are exempt from targeting.
(ignoreList (list gSource))
; Each beam is represented as a list of effect parameters.
(beamList
(list { obj:gSource
pos:aFirePos
angle:aFireAngle
vfx:theStruct
}
)
)
; Whoever is in this list will take damage!
(killList
(if firstScan
; YES: Our first target.
(list { obj:nextSource
pos:nextPos
}
)
; NO: Missed.
Nil
)
)
)
; If our first target was an obstable the player can hide behind or inside, do not allow the beam to chain
; and hit more targets because we do not want to give the player a perfect safe spot to attack from.
(if (and nextSource
(or (objGetProperty nextSource 'abandoned)
(objGetProperty nextSource 'immutable)
)
)
(setq nextSource Nil)
)
; Acquire more targets.
(loop (and nextSource (gr extraHits 0))
; Search for objects hostile to attacker. Note that the source is likely not the attacker, which makes
; enemy relative to source featured by sysFindObject inadequate. We need to use filter to keep only the
; objects hostile to the attacker.
(block ( (moreTargets (filter
(sysFindObject nextSource criteria)
who
(or (objIsEnemy gSource who)
(and (objIsAngryAt who gSource) (neq (shpGetOrder who) 'mine))
)
))
(searching True)
)
; Ugh! Dirty O^2 operation, better hope max hits were low.
(enum moreTargets thisOne
(if searching
; YES: No new target to chain yet.
(block ( (virgin True) )
; Cycle through the list of objects we are not allowed to target and see if any match.
(enum ignoreList whoNot
(if (eq thisOne whoNot) (setq virgin Nil))
)
; See if we are allowed another hitscan.
(if virgin
; YES: Possible target, check if we can hit it without hitting something else first.
(block ( (nextScan (sysHitScan nextSource nextPos (objGetPos thisOne))) )
(if (eq (@ nextScan 0) thisOne)
; YES: Target after next can be hit, complete the chain!
(block ( (thisHitPos (@ nextScan 1))
(nextAngle (sysVectorAngle thisHitPos nextPos))
(nextLength (int (* (sysVectorDistanceExact nextPos thisHitPos) pixelsPerRange)))
(nextStruct
{ length:nextLength
width:theWidth
}
)
)
; Update our lists.
(setq ignoreList (append ignoreList nextSource))
(setq beamList (append
beamList
{ obj:nextSource
pos:nextPos
angle:nextAngle
vfx:nextStruct
}
))
(setq killList (append
killList
{ obj:thisOne
pos:thisHitPos
}
))
; Set loop up for this target as the next source...
(setq nextSource thisOne)
(setq nextPos thisHitPos)
; This will continue the loop from the next source if we have extra hits left.
(setq extraHits (subtract extraHits 1))
; No more hitscans from the current source object because we just found our next hit!
(setq searching Nil)
)
; NO: Something is in the way.
)
)
; NO: Already known, do not retarget!
)
)
; NO: Already found the next target in the chain.
)
)
; By now, we have searched everything in range.
; If we were still searching, then we failed to find another target and have reached the end of the line.
(if searching (setq nextSource Nil))
)
)
; Draw all beams.
(enum beamList thisStruct
(sysCreateEffect &ef912ChainLightningBeam; (@ thisStruct 'obj) (@ thisStruct 'pos) (@ thisStruct 'angle) (@ thisStruct 'vfx))
)
; Damage all targets. We do this last in case damaging a target causes special events such as backlash.
; I have no idea what happens if the attacker dies before all targets in the list take damage.
(if killList
; YES: Kill 'em all! Note that damage for this weapon is divided by number of hits. That's why we have so many variants.
(block ( (targetsHit (count killList))
(variantToUse (min (max (subtract targetsHit 1) 0) maxVariant))
; We copy gItem to a dummy weapon that only exists here, assign it the variant we want to use, then use the dummy in objDamage.
; Remember the only difference this weapon's variants should have is the average damage.
; We use an item instead of a type for the weapon because the item may have non-standard stats due to enhancements or level scaling.
(theWeapon (itmSetProperty gItem 'charges variantToUse))
)
(enum killList thisStruct
(objDamage (@ thisStruct 'obj) theWeapon gSource (@ thisStruct 'pos) 'fullResult)
)
)
; NO: No targets!
)
; We did all of the work above, so return True to block default weapon fire.
True
)
</OnFireWeapon>
</Events>
</ItemType>
<EffectType UNID="&ef912ChainLightningBeam;" instance="creator" >
<Effect>
; Lifetime must be at least two for it to be drawn for one tick if drawn via sysCreateEffect.
; Must rotate the beam 180 for it to point at a target.
; Values here can be overriden by the struct passed by sysCreateEffect.
<Ray style="jagged" shape="straight" primaryColor="#00d5ff" secondaryColor="#006b80" length="24" width="20" intensity="40" xformRotation="180" lifetime="2" />
; We cannot have GetParmeters Event because that interferes with the passed struct from sysCreateEffect.
</Effect>
</EffectType>
; The following in this next type is not part of the chain lightning gun itself, but it gives everything the player needs to test the weapon quickly after starting a new game.
<Type UNID="&type912Cheat;">
<Events>
; Called when we start a new game, after ship select.
<OnGlobalUniverseCreated>
(block Nil
; We need a powerful reactor for our new weapon. Add Hyperion to cargo hold then install.
(shpInstallDevice gPlayerShip (objAddItem gPlayerShip (itmCreate &it1GWReactor; 1)))
; After reactor upgrade, fuel up our ship.
(objSetProperty gPlayerShip 'fuelLeft (objGetProperty gPlayerShip 'maxFuel))
; If player will test this on friendlies, he will get wrecked fast, so upgrade those defenses...
; First, the shields.
(shpInstallDevice gPlayerShip (objAddItem gPlayerShip (itmCreate &itPlasmaShieldGenerator; 1)))
(shpRechargeShield gPlayerShip 10000)
; Then armor.
(for armorSeg 0 (subtract (shpGetArmorCount gPlayerShip) 1)
(shpInstallArmor gPlayerShip (objAddItem gPlayerShip (itmCreate &itDiamondLatticeArmor; 1)) armorSeg)
)
; Create the weapon, add it to the ship's cargo hold, then install it!
(shpInstallDevice gPlayerShip (objAddItem gPlayerShip (itmCreate &it912ChainLightningGun; 1)))
; Remove fuel and the stuff we replaced.
(objEnumItems gPlayerShip "a r s f U" thisItem (objRemoveItem gPlayerShip thisItem))
; Add some more junk to make things easy, at least for George.
(objAddItem gPlayerShip (itmCreate &itHadronVacuumFuelCell; 24))
(objAddItem gPlayerShip (itmCreate &itSystemMapROM; 9))
(objAddItem gPlayerShip (itmCreate &itGemOfDespair; 3))
(objAddItem gPlayerShip (itmCreate &itHeavyArmorRepair; (shpGetArmorCount gPlayerShip)))
; Identify all unknown items on our ship!
(objEnumItems gPlayerShip "*" thisItem (itmSetKnown thisItem True))
; Add targeting because we always want that!
(objChangeEquipmentStatus gPlayerShip 'TargetingComputer 'install)
; Finally, give player a million in all currencies to buy more fuel or whatever.
(enum (typFind "$") ecoType
(block Nil
; Remove all money.
(objCharge gPlayerShip ecoType (objGetBalance gPlayerShip ecoType))
; Give one million per currency type.
(objCredit gPlayerShip ecoType 1000000)
)
)
)
</OnGlobalUniverseCreated>
</Events>
</Type>
</TranscendenceExtension>