Strict

Import "globals.bmx"				'not ALL globals, but all the ones that matter (or are commonly used)
Import "maths.bmx"					'a bunch of small math functions that make some things easier
Import "soundControl.bmx"			'play sounds and update music
Import "entity_shot.bmx"			'defines shot entities that guns can fire
Import "emitters.bmx"				'defines sprite emitters to be created by the host then remade by the client
Import "gertrude.bmx"				'entity placeholder that contains its physics information
Import "timers.bmx"					'defines egg timers
Import "luaLoad.bmx"				'allows the loading of lua scripts
Import "vararray.bmx"				'an array of variables with names; useful when we want to track only a couple unique variables

Import "entity_sprite.bmx"			'defines sprites to be drawn (so we can make text)

Rem
Global firemodeMap:TMap = CreateMap()
Function setupFireModes()
	MapInsert(firemodeMap, "0", "Automatic")
	MapInsert(firemodeMap, "1", "Semi-automatic")
	MapInsert(firemodeMap, "2", "Hold-and-release")
EndFunction
EndRem

'a weapon archtype, held by components
'Type Weapon
'	Field name$					'the name of the gun
'	Field class$				'"auto" | "semi" | "release"
'	Field script:TLuaObject	'points to the script that controls this gun (also sets up the spawning component)
	'Field icon					'the index pointer to weapon_gfx[] to use for an icon
	
'	Field stats:GunStats		'an object that stores stats	
'EndType


Const GUN_TURRET = 3
Const GUN_FORWARD = 4
Const GUN_ANGLE = 5
Const GUN_REVERSE = 6
Const GUN_ALT = 2

Const GUN_ANGLE_THETA# = 13	'the degrees of offset for angled guns

'make & return a new weapon for a ship : requires a gun archtype and x,y pixel offsets to fire shots from
'_fixed is the freedom of movement of the gun, whether it fires at the target or straight forward
Function newGun:Gun(_filename$, _class$, ox, oy)', _fixed = GUN_TURRET)
	Local g:Gun	
	Select _class
	Case "auto"
		g = New GunAuto
	Case "semi"
		g = New GunSemi
	Case "release"
		g = New GunRelease
	Case "beam"
		g = New GunBeam
	EndSelect
	g.name = _filename'(defaults the name to the filename)
	g.class = _class
	
	'set the firing angle
	'g.fixed = _fixed
	
	'set the offset from the center of the ship
	g.offsetx = ox * SHIPSCALE
	g.offsety = oy * SHIPSCALE
	
	'grab the source for this gun
	g.scriptslot = gunScript.produce(g.name, GUN_PATH + _filename)
			
	'initialize the gun's stats
	LuaRegisterObject g, "gun"
	gunScript.call(g.scriptslot, "init")
	
	g.reset()
	
	'adjust the cost of beams to per loop (assuming 40 fps)
	'If _class = "beam" Then g.firecost = (g.firecost / 40)

	Return g
EndFunction


'an ingame, owned gun. Gun is to weapon what ship is to chassis.
Type Gun Abstract
	Field name$							'the name of the gun
	Field class$						'"auto" | "semi" | "release" | "beam"
	Field scriptslot
	'Field script:TLuaObject				'points to the script that initializes and controls this gun
	Field skipUpdate = True				'skips calling the lua gun update function (in entity_ship.bmx)
	Field gunTimeMod#						'keeps up-to-date what the latest timemod is
	
	Field owner:Gertrude				'the ship that owns this gun
	Field target:Gertrude				'optional target of this gun
	Field oldtarget:Gertrude			'at the start of update function, store the target
	
	Field offsetx,offsety				'x,y offset at rot=0 that shots come from (scaled by shipscale)
	
	Field shotlayer = 5					'layer to make shots
	
	Field firing						'whether this gun is attemping to fire at this moment
	Field firing_start					'whether this gun JUST started firing (this is valid only really during the fire lua script)
	Field skipAIFire					'should we skip the next fire command? (used by AI to fire semiautos, releases)
	
	Field firex#, firey#				'parameters to pass the .lua fire script for the position and angle of shooting
	Field firetheta#, firedist#			'--^
	Field firetarx#, firetary#			'--^
	
	Field fixed = GUN_TURRET			'whether this gun is fixed in place [3:turret | 4:forward | 5:angle | 6:reverse]
	Field shotdamage#					'the damage of shots
	Field shotknockback#				'the knockback of shots
	Field shotspeed#					'the speed of shots
	Field shotrange#					'the range of the shots
	Field shotcritbonus#				'the increase in damage during critical hits (adds to the multiplier)
	Field shotdurable = False			'should produced shots be durable 
	Field burstNum = 1					'the number of shots each burst	
	Field clipNum						'the number of shots in each clip (0 = no clip)
	Field firecost#						'the juice cost of firing
	Field overheat						'number of milliseconds the gun causes overheating
	Field piercing = False				'does this shot penentrate shields and damage buffers?
	
	Field clip = clipNum				'the current number of shots in the clip
	Field burst							'the counter for the number of shots left in the curret burst
	Field burstX,burstY					'stores where it targeted
	
	Field fireDelay = 0					'ms in between shots
	Field burstDelay = 100				'ms between shots inside a burst
	Field reloadDelay = 0				'ms it takes to reload
	
	Field enabled = True				'can't fire unless the gun's enabled
	Field enabledoppler = False			'should we turn off adding owner's speed for the player?
	Field readynotify = False			'should it make a message when readied
	
	Field vars:VarArray = New VarArray	'an array of variable spaces
	Field timerList:TList = New TList	'a list of timers that guns can use to track things
	
	Field shotChannel:TChannel' = AllocChannel()'alloc'd when used
	
	Method update() Abstract
	Method remove() Abstract
	
	'called every cycle-> shots cool down, missiles try to lock on...
	Method baseUpdate()			
		gunTimeMod = timeMod
		If firing Then firing_start = 0 Else firing_start = 1
		firing = False
		skipAIFire = False
		oldtarget = target
		Local readied = False'did we just ready?
	
		'are we bursting?
		If burst > 0 
			timerTick(timerList, "burstDelay")
			If timerDinged(timerList, "burstDelay")
				fire(burstX, burstY, True)					'force-fire the gun
				If clipNum > 0 And clip <= 0 Then burst = 0	'if we run out of ammo, end the burst

				'count down the number of bursts, reset the timer
				burst:- 1
				If burst > 0 Then timerSet(timerList, "burstDelay")
			EndIf
			
		Else'if we're done bursting, tick down the firedelay timer
			If timerTick(timerList, "fireDelay") Then readied = True
		EndIf

		'dec. the timer for reloading
		If clipNum > 0 And clip = 0
			If timerTick(timerList, "reloadDelay", reloadDelay)
				'If _s = p1 Then playSFX(getSound("gun_reload.wav"),_s.x,_s.y)
				new_text(name + " reloaded!", owner.x, owner.y+20)
				'timerSet(timerList, "reloadDelay")'reset the reload timer
				clip = clipNum
				readied = True
			EndIf
		Else
			timerDing(timerList, "reloadDelay")
		EndIf
		
		'if just readied the gun, say so (maybe)
		If readynotify And readied
			new_text(name + " ready!", owner.x, owner.y+20)
			sfx("ability_ready.wav", owner.x, owner.y, 1, 0)
		EndIf	
	EndMethod

	'try to fire the guns (can force them to be available) (returns TRUE if we fired)
	Method fire(tarx, tary, forceFlag = False)
		'Print "attempting To fire " + name + " at " + tarx + "   " + tary
		 		
		'can we fire according to this gun's rules?
		If canFire(tarx, tary) Or forceFlag
			'reset firedelay
			timerSet(timerList, "fireDelay", fireDelay)
			
			'get the gun x,y offset, firing angle
			Local shotpacket#[]
			Local tar_theta# = ATan2((tary - owner.y),(tarx - owner.x))
			
			'players get their speed added to shots (unless we've enabled doppler), so they need to compensate for that
			If owner.behavior = "player" And Not enabledoppler
				shotpacket = getFiringPacket(Self, tarx, tary)
			Else
				shotpacket = getFiringCoord(Self)
				shotpacket[2] = 0'no modification due to own velocity
			EndIf
			'Print "firing gun " + name + " with freedom " + fixed
			
			'set the firing angle according to whether this gun is a turret or is fixed
			Select fixed
			Case GUN_TURRET
				firetheta = tar_theta
			Case GUN_FORWARD, GUN_ALT
				firetheta = owner.rot
			Case GUN_ANGLE
				firetheta = owner.rot + GUN_ANGLE_THETA*Sgn(offsetx)
			Case GUN_REVERSE
				firetheta = owner.rot + 180
			EndSelect
			
			'modify firing angle due to ship's velocity (if shotpacket[2] has been set)
			If class <> "beam" Then firetheta:+ shotpacket[2]
			
			'calculate the absolute x,y starting coords
			firex = shotpacket[0] + owner.x
			firey = shotpacket[1] + owner.y
			
			'store the absolute target coords
			firedist = Sqr((firex-tarx)^2 + (firey-tary)^2)
			If fixed <> GUN_TURRET
				firetarx = firex + Cos(firetheta)*Min(shotrange, firedist)
				firetary = firey + Sin(firetheta)*Min(shotrange, firedist)
			Else
				firetarx = tarx
				firetary = tary
			EndIf
			
			'Print "    firing shot at " + firetarx + "    " + firetary
			
			'-DEBUG TRACER SHOT
			'Local s:Shot = addShot("shot7.png", firetarx, firetary, 0)
			's.speed = 0
			's.damage = 0
			's.setRange(1000)
			's.ignoreCollisions = True
			
			'Print "->calling fire script"
			firing = True
						
			'ask the script to fire from absx,absy at angle theta
			LuaRegisterObject Self, "gun"
			gunScript.call(scriptslot, "fire")
			
			Return True
		Else
			'even if we can't fire now, if it's bursting we might be firing
			If firing Then Return True Else Return False
		EndIf
	EndMethod
	
	Method canFire(tarx, tary) Abstract
	
	'returns whether this gun can currently fire according to base gun rules
	Method baseCanFire(tarx,tary)
		'Print "trying to fire weapon :    reload:" + timerDinged(timerList, "reloadDelay") + "  firedelay:"+timerDinged(timerList, "fireDelay")
	
		If enabled													'can't do anything unless the gun is enabled (99.9% of them are)
			If timerDinged(timerList, "reloadDelay")				'can't do anything unless reloaded
				If timerDinged(timerList, "fireDelay")				'can't do anything unless fire delay has elapsed
					'If owner = Null Or (owner.juice >= firecost)	'can't do anything unless we have enough juice
					'If owner = Null Or (owner.barrier >= firecost)	'can't do anything unless we have enough juice
					
						'does this gun come in bursts? (only trigger on the first shot of a burst)
						If (burstNum > 1 And burst <= 0)
							timerSet(timerList, "fireDelay")'reset the firedelay timer
							burst = burstNum - 1			'set number of remaining shots in burst
							
							'burstX = tarx					'store position of target for this burst
							'burstY = tary
						EndIf
				
						'does this gun have a clip?
						If clipNum > 0
							clip:- 1		'count down the number of shots left in the clip
							'if the clip is out of ammo, time to reload
							If clip <= 0
								timerSet(timerList, "reloadDelay")	'reset the reload delay timer
								'If _s = p1 Then playSFX(getSound("gun_depleted.wav"),_s.x,_s.y)
								new_text(name + " reloading...", owner.x, owner.y+20)
							EndIf
						EndIf
						
						'use up any juice (THIS ONLY CHANGES THE PLACEHOLDER; ship must update its own juice total following)
						If class <> "release" Then useCost()'(release weapons have to manually choose when to use costs)
						
						'yes; we can fire
						Return True
						
					'ElseIf owner <> Null'if we don't have enough juice
					'	new_text("Not enough juice!", owner.x+20, owner.y) 
					'EndIf
				EndIf
			EndIf
		EndIf
		
		'something is keeping us from firing
		Return False
	EndMethod
	
	'make a shot
	Method addShot:Shot(gfxName$, absx#, absy#, theta#, static = False)
		Local s:Shot = New Shot
		s.name = name
		s.owner = owner
		s.scriptslot = scriptslot
		
		'If owner.behavior <> "player" Then theta:+ Rand(-5,5)
		
		s.x = absx
		s.y = absy
		s.rot = theta
		s.movrot = theta
		s.speed = shotspeed
		s.setRange(shotrange)
		s.damage = shotdamage
		s.knockback = shotknockback
		s.crit = shotcritbonus
		If shotdurable Then s.durable = True
		If piercing
			s.skipFortify = True
			s.skipShields = True
		EndIf
		
		If enabledoppler Then s.doppler = True
		
		Local animated = Not static'cause lua scripts are so pessimistic 								(they always assume false)
		s.init(gfxName, animated, shotlayer)
		
		If piercing
			'make it crackle
			Local e:Emitter = new_emitter("shotcrackle", False)
			e.num = -1
			e.scale = .5
			e.attach(s)
			e.upload()
		EndIf
		
		'give any lua scripts access to this shot
		LuaRegisterObject s, "shot"
		Return s
	EndMethod

	'resets the weapon back to its base values
	Method reset()
		firing = False
		
		clip = clipNum
		burst = 0
		
		timerSet(timerList, "fireDelay", fireDelay)
		timerSet(timerList, "reloadDelay", reloadDelay)
		timerSet(timerList, "burstDelay", burstDelay)
		
		timerLoop(timerList, "fireDelay", False)
		timerLoop(timerList, "reloadDelay", False)
		timerLoop(timerList, "burstDelay", True)

	EndMethod
	
	'uses up [firecost] amount of juice
	Method useCost()
		If (owner <> Null)
			If firecost > 0
				owner.juiceUse:+ firecost
				owner.barrier:- firecost'in case anything else is curious before the ship updates it
			EndIf
			If overheat > 0 Then owner.overheat:+ overheat
		EndIf
	EndMethod
	
	'ticks a specified timer (or creates it if it doesn't yet exist)
	Method hasTimerTicked(_timerName$, _interval = -1)
		'Print "has timer ticked? " + _timerName + " inte " + _interval
		If timerTick(timerList, _timerName, _interval)
			'Print "yes"
			Return True
		EndIf
	EndMethod
	
	'returns whether or not we have a target
	Method hasTarget()
		If target = Null Then Return 0 Else Return 1
	EndMethod
	
	'returns whether or not we have a new target
	Method hasNewTarget()
		If target = oldtarget Then Return 0 Else Return 1
	EndMethod

	'sets target to null
	Method clearTarget()
		target = Null
	EndMethod
	
	Method sfx(path$, _x, _y, _vol# = 1, skipRateModulation = 0)
		If _vol = 0 Then _vol = 1
		If shotChannel = Null Then shotChannel = AllocChannel()
		If Not ChannelPlaying(shotChannel) Then playLocationalSFX(getSound(path), shotChannel, _x, _y, _vol, skipRateModulation)
	EndMethod
	
EndType


Const BEAMSTEP = 16
'Global beamList:TList

Function newBeamNetObject:TGNetObject(damage#, knockback#, range#, style# = 0, absolute = 0)
	Local beam:TGNetObject
	If netHost <> Null
		'create the beam net object
		beam = CreateGNetObject(netHost)	
		SetGNetInt(beam, NET_ID, ID_BEAM)
		SetGNetInt(beam, NET_BEAM_ACTIVE, 0)
		SetGNetInt(beam, NET_BEAM_X1, 0)
		SetGNetInt(beam, NET_BEAM_Y1, 0)
		SetGNetInt(beam, NET_BEAM_X2, 0)
		SetGNetInt(beam, NET_BEAM_Y2, 0)
		SetGNetInt(beam, NET_BEAM_PIERCE, 0)
		updateBeamNetObject(beam, damage, knockback, range, style, absolute)
	EndIf
	Return beam
EndFunction

'updates the non-continuously-changing values
Function updateBeamNetObject(beam:TGNetObject, damage#, knockback#, range#, style#, absolute)
	SetGNetFloat(beam, NET_BEAM_STYLE, style)
	SetGNetInt(beam, NET_BEAM_ABSOLUTE, absolute)'if absolute, then it'll go less than the maximum range
	SetGNetFloat(beam, NET_BEAM_DAMAGE, damage)
	SetGNetFloat(beam, NET_BEAM_KNOCKBACK, knockback)
	SetGNetFloat(beam, NET_BEAM_RANGE, range)
	Return 1
EndFunction

'sets a beam to piercing/not piercing
Function setBeamNetObjectPiercing(beam:TGNetObject, pierce = 1)
	If beam <> Null Then SetGNetInt(beam, NET_BEAM_PIERCE, pierce) Else Print "tried to make null beam piercing!!!"
	Return 1
EndFunction

Type GunBeam Extends Gun
	Field beam:TGNetObject
	Field beamList:TList = New TList	'some guns can fire multiple beams
	Field cycle = 0						'how many beams we've cycled through this loop
	
	Field heat#							'builds up from 0 to 1 while firing
	Field coolTime = 500				'ms it takes for heat to change from 0 to 1 or vice-versa

	Method initBeam(style# = 0, absolute = 0)
		If beam = Null
			beam = newBeamNetObject(shotdamage, shotknockback, shotrange, style, absolute)
			setBeamNetObjectPiercing(beam, piercing)
		EndIf
	EndMethod
	
	'creates a beam if none is available
	Method cycleBeam(style# = 0, absolute = 0)
		beam = Null
		cycle:+ 1
		'Print "CYCLING BEAM :" + cycle
		
		'get a pre-existing beam
		Local i = cycle
		For Local b:TGNetObject = EachIn beamList
			i:- 1
			If i <= 0
				beam = b
				updateBeamNetObject(beam, shotdamage, shotknockback, shotrange, style, absolute)
				Exit
			EndIf
		Next
		
		'make a new beam
		If beam = Null
			initBeam(style, absolute)
			beamList.addLast(beam)
		EndIf
		
		SetGNetInt(beam, NET_BEAM_ACTIVE, 1)
	EndMethod

	'stuff to do every loop
	Method update()
		'once we stop firing, update the beam net object to not be firing
		If Not firing
			If beam <> Null Then SetGNetInt(beam, NET_BEAM_ACTIVE, 0)
		EndIf
		
		'non-cycled beams get inactivated
		Local i
		For Local b:TGNetObject = EachIn beamList
			i:+ 1
			If i > cycle Then SetGNetInt(b, NET_BEAM_ACTIVE, 0)
		Next
	
		'when we cycle, we have to do so from scratch
		cycle = 0
		
		'cool down while not firing
		If Not firing
			'Print "cooling beam : " + heat
			heat = constrain(heat - frameTime/coolTime, 0, 1)
		EndIf
		
		'update the gun
		baseUpdate()
	EndMethod
	
	'returns whether we can currently fire
	Method canFire(tarx,tary)
		If baseCanFire(tarx,tary)								'asks the base weapon class if we can fire
			'Print "heating beam : " + heat + "    ( + " + (frameTime/coolTime) + ")"
			heat = constrain(heat + frameTime/coolTime, 0, 1)	'heat up while firing
			Return True
		EndIf
	EndMethod		
			
	Method remove()
		For Local b:TGNetObject = EachIn beamList
			If beam = b Then beam = Null
			CloseGNetObject(b)
		Next
		If beam <> Null
			CloseGNetObject(beam)
			beam = Null
		EndIf
		
		beamList = Null
	EndMethod
EndType

Type GunAuto Extends Gun
	Field heat#					'builds up from 0 to 1 while firing

	Field coolTime = 500		'ms it takes for heat to return to 0 from 1 - AND - build from 0 to 1

	'stuff to do every loop
	Method update()
		baseUpdate()
	
		'cool down while not firing
		If Not firing Then heat = constrain(heat - frameTime/coolTime, 0, 1)
	EndMethod
	
	'returns whether we can currently fire
	Method canFire(tarx,tary)
		If baseCanFire(tarx,tary)								'asks the base weapon class if we can fire
			heat = constrain(heat + frameTime/coolTime, 0, 1)	'heat up while firing
			Return True
		EndIf
	EndMethod
	
	Method remove()
	EndMethod
EndType


Type GunRelease Extends Gun

	Field charge#				'rises from 0 to 1 as we hold down the button
	Field releaseX, releaseY	'x & y coords of the target release location
	
	Field chargeTime# = 750		'time it takes to fully charge

	'stuff to do every loop
	Method update()
		baseUpdate()
		
		'so the ship knows to flash
		vars.set("charge", charge)
		
		'was the charge not re-held? (not tried to fire)
		If charge < 0
			'fire the released shot!
			fire(releaseX, releaseY, True)
			useCost()
			
			'if there's no burst, we're done, reset the charge
			If burst <= 0 Then charge = 0
		EndIf
		
		'if we're not bursting
		If burst <= 0		
			'try and release the charge
			If charge > 0 Then charge = -Abs(charge)
		EndIf
	EndMethod
	
	'stuff to do while attempting to fire
	Method canFire(tarx,tary)
		'if reach full charge, AI releases
		If charge >= 1 Then skipAIFire = True
		
		'track where we're aiming
		releaseX = tarx
		releaseY = tary
		
		If baseCanFire(tarx, tary)
			'press-and-hold weapons are always trying to release their charge by setting negative; while holding, keep positive
			If charge < 0 Then charge = Abs(charge)
			'build up a % charge (and cancel out the cooling)
			Local oldcharge = charge
			charge = constrain(charge + timePass/chargeTime, 0, 1)
			
			'play a blip when we fully charge
			'If owner.behavior = "player" And oldstate < 1 And state >= 1 Then playSFX("gun_pew2.wav", blipChannel)
		EndIf
		
		'can never fire release weapons by trying to fire
		Return False
	EndMethod
	
	Method remove()
	EndMethod

EndType


Type GunSemi Extends Gun

	Field state				'tracks whether the gun is ready to fire : 2=ready | 1=attempting to be readied | 0=not ready

	'stuff to do every loop
	Method update()
		baseUpdate()
		
		'if we can't fire, AI skips trying to fire (so they'll be able to next time)
		If state < 2 And owner.behavior <> "player"
			skipAIFire = True
		EndIf
		
		'try to move up a ready step
		If state < 2 Then state:+ 1
	EndMethod
	
	'stuff to do while attempting to fire
	Method canFire(tarx,tary)
		'if we just tried to fire last time around, prevent firing this round
		If state = 1 Then state = 0
		
		'unless it's bursting -- then we're always ready
		If burst > 1 Then state = 2

		'if it's ready, then fire!
		If state = 2
			If baseCanFire(tarx,tary)
				'we won't be able to fire next time around
				state = 0
				'return whether or not this gun can currently fire
				Return True
			EndIf
		EndIf
	EndMethod
	
	Method remove()
	EndMethod

EndType


'calculates and returns the current RELATIVE (to the ship's position) coord of the gun, given the owner ship's present rotation
Function getFiringCoord#[](g:Gun)
	Local coord#[3]
	
	'find the current relative position of the gun	
	coord[0] = (-g.offsetx*Sin(g.owner.rot) + g.offsety*Cos(-g.owner.rot))*SHIPSCALE
	coord[1] = (-g.offsety*Sin(-g.owner.rot) + g.offsetx*Cos(g.owner.rot))*SHIPSCALE
	
	Return coord
EndFunction

'calculates and returns the current starting location angle to aim shots given a ship's current trajectory and a target coord
Function getFiringPacket#[](g:Gun, _tarx, _tary)
	
	'find the current relative position of the gun
	Local coord#[3]
	coord = getFiringCoord(g)
	Local gx# = coord[0]
	Local gy# = coord[1]
	
	'find relative angle, dist to target
	Local dx = _tarx-(g.owner.x+gx)
	Local dy = _tary-(g.owner.y+gy)
	Local tar_theta# = ATan2(dy,dx)
	Local tar_dist# = approxDist(dx,dy)
	
	'find the difference in shooting angle versus movement angle
	Local v_theta#= tar_theta - g.owner.movrot
	
	'now figure out how much we need to alter the angle of the shot to hit those coords
	'the vector for how much the velocity is gonna alter the angle of the shot
	Local v_perp# = g.owner.speed * Sin(v_theta)'for every timestep, the shot is going to go this far perpendicularly as a result of ship speed
	
	'find the angle to modify the shot
	Local theta_mod# = ATan2(v_perp, g.shotspeed)
	
	'now modify the angle of shot to correct for the movement on the ship's effect on the shot
	'tar_theta:+ theta_mod

	'keep within bounds
	'tar_theta = constrain(convert_to_relative(tar_theta, g.owner.rot), -g.freedom/2, g.freedom/2)
	'tar_theta:+ g.owner.rot'(convert back to absolute)
	
	Local packet#[3]	'creates a packet for the location and angle to return
	packet[0] = gx
	packet[1] = gy
	packet[2] = theta_mod'tar_theta
	Return packet
EndFunction
