Page 1 of 1

Example mod: Chain Lightning Gun

Posted: Tue Jun 12, 2018 4:36 pm
by PM
This example mod adds a chain lightning gun that acquires a list of up to seven targets then fires a hitscan ion beam that arcs from target to target. The weapon and other useful items are installed on the playership at the start of a new game. The mod is fully functional and should work after a simple copy-and-paste to a blank xml file. However, due to the items granted to facilitate testing, your ship will start at nearly endgame strength, making this (almost) a godmod.

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"/>

			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;" />

			; 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.

				(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.
							(list	{	obj:gSource
						; Whoever is in this list will take damage!
							(if firstScan
								; YES:  Our first target.
								(list	{	obj:nextSource
								; NO:  Missed.
				; 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)
									(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)))
																{	length:nextLength
														; Update our lists.
														(setq ignoreList (append ignoreList nextSource))
														(setq beamList (append
															{	obj:nextSource
														(setq killList (append
															{	obj:thisOne

														; 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.

	<EffectType UNID="&ef912ChainLightningBeam;"	instance="creator" >
			; 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.

; 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;">
		; Called when we start a new game, after ship select.
				(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)