Posted June 10, 2024 by bwldrbst
In my previous post I described the process for adding new monsters to Ecliptic. This time, let's talk about some weapons that can be used to turn those monsters into green goo or a fine red mist.
Just your standard sci-fi armory - laser carbine, grenade launcher, pulse rifle, flamethrower and shotgun. I really like the look of the grenade launcher but the pulse rifle needs some more work.
Weapons in Ecliptic are Items - objects that the robots can interact with in some way. Pretty much everything in the game that isn't a Player or Monster is an Item. The game engine doesn't have any particular knowledge of Weapons, it just provides operations that can be used to implement them - target selection, animations, area effects and damage.
Most weapons are organized into a type hierarchy with a root type defining some common attributes and base types for melee and ranged weapons. Some items that you would classify as a weapon don't really fit into this hierarchy but that's OK - there's no rule that says only Official Weapons can be used to smash aliens into little bits. However, in this post, I'm mostly going to talk about ranged weapons.
Ranged weapons are weapons that throw some sort of projectile. They vary in power, range and the type of damage they do. A base type provides some common attributes such as charge - the number of shots left before a reload is required, max_charge - the maximum number of shots the weapon can hold, and range - the distance available for target selection.
The base type also defines some events that occur while the weapon is being used: needs_reload - emitted when you're out of ammo and fired - emitted when a target has been successfully selected.
Begin Item
ID = ranged_weapon_base
Base = $game.item.weapon_base
Static = True
Begin Attribute
ID = charge
BitCount = 4
Value = 0
End
Begin Attribute
ID = maxCharge
BitCount = 4
Value = 0
End
Begin Event
ID = needs_reload
End
Begin Event
ID = fired
End
When the player attempts to use the weapon, the game will request they select a target from the map. You can select any square of the map visible to the robot - it doesn't matter if there's an enemy there or not.
;
; Invoked when the player uses a ranged weapon.
;
Begin Handler
EventID = $game.event.use
MatchTarget = true
Begin
*select_target
;
; Out of ammo?
;
Push $game.item.ranged_weapon_base.attribute.charge
Push 0
Cmp
Equal
Jump need_reload
Push[en] "Select a target"
Push 0
Log
;
; Ask the player to choose
; a target location.
;
*arg !Event:Object !selected
*select_map_tile $game.item.ranged_weapon_base.event.target_selected $game.item.ranged_weapon_base.attribute.range 1
Exit
;
; No ammo so as for a reload.
;
Label need_reload
*emit_event $game.item.ranged_weapon_base.event.needs_reload 0
End
The *select_map_tile macro triggers an event that switches the game into selection mode. By default, the player is prompted to choose a location on the map but it has options for specifying selecting monsters, other robots or items from the map or inventory.
Once the player has selected a location, the reply event - $game.item.ranged_weapon_base.event.target_selected - will be triggered and the next event handler will run.
Begin Handler
EventID = $game.item.ranged_weapon_base.event.target_selected
MatchObject = True
Begin
;
; Emit an event that causes the robot's
; action points to be reduced.
;
*emit_object_used
;
; Elide some stuff that generates a
; "Target Selected" log message
;
;
; Reduce the ammo count.
;
*select_object
Push 1
Push $game.item.ranged_weapon_base.attribute.charge
Sub
Store $game.item.ranged_weapon_base.attribute.charge
;
; Trigger the derived type's fired handler
;
*emit_event $game.item.ranged_weapon_base.event.fired 0
End
End
The type that derives from ranged_weapon_base needs to provide a handler for the fired event that will trigger the weapon's effect. This handler is from the Shotgun weapon.
Begin Handler
EventID = $game.item.ranged_weapon_base.event.fired
MatchObject = true
Begin
;
; Trigger the "bullet_large_fire" effect
; starting from the robot's location and
; ending at the selected map location.
;
*select_actor
*arg !Effect:Position !Player:Position
*select_event
*arg !Effect:TargetPosition !Event:Position
*arg $game.effect.bullet_large_fire.attribute.hit_strength 20
*start_effect $game.effect.bullet_large_fire 2
;
; Play the shotgun sound.
;
*start_sound $game.soundset.shotgun.sound.shotgun_fire 0
End
End
Similar to the way monster attacks work, weapons trigger effects when they are used. An effect is a re-usable object that can define its own events, handlers and attributes. In this case, the shotgun triggers the $game.effect.bullet_large_fire effect that plays a muzzle flash animation and then shows a bullet moving to the target. This effect is used by any weapon that throws a large bullet and can be customized with different hit_strength values.
Begin Effect
ID = bullet_large_fire
;
; Attribute use to control how much
; damage the effect will do.
;
Begin Attribute
ID = hit_strength
BitCount = 8
End
;
; Event triggered when the muzzle
; flash animation completes.
;
Begin Event
ID = bullet_large_launch
End
;
; Event triggered when the bullet
; reaches the target.
;
Begin Event
ID = bullet_large_done
End
;
; Even triggered when the "hit"
; animation completes.
;
Begin Event
ID = bullet_large_hit_done
End
;
; Handler for the "effect_started" event
; This is the entry-point of the effect.
;
Begin Handler
EventID = $game.event.effect_started
MatchObject = True
Begin
;
; Work out the direction between
; the robot and the target so we
; can play the correct muzzle flash
; animation.
;
*select_actor
*arg !Anim:Position !Player:Position
Push !Player:Position
*select_object
Push !Effect:TargetPosition
Facing
Push #!Anim:Facing
;
; Trigger the muzzle flash
;
*arg !Anim:CompletionEventID $game.effect.bullet_large_fire.event.bullet_large_launch
*arg !Anim:Target !selected
*arg !Anim:Delay 10
*start_anim $game.animset.muzzle_flash 5
;
; Make the game wait until the effect completes.
;
*arg !Wait:WaitEventID $game.event.effect_completed
Push 1
Wait
End
End
The next step is to play the actual bullet animation from the robot to the target.
Begin Handler
EventID = $game.effect.bullet_large_fire.event.bullet_large_launch
MatchObject = True
Begin
*select_actor
*arg !Anim:Position !Player:Position *select_object
*arg !Anim:TargetPosition !Effect:TargetPosition
*arg !Anim:CompletionEventID $game.effect.bullet_large_fire.event.bullet_large_done
*arg !Anim:Target !selected
*start_anim $game.animset.bullet_large 4
End
End
Then we decide whether the target has taken a hit.
Begin Handler
EventID = $game.effect.bullet_large_fire.event.bullet_large_done
MatchObject = True
Begin
;
; Play a "splat" animation
; at the target position.
;
*select_object
*arg !Anim:Position !Effect:TargetPosition
*arg !Anim:CompletionEventID $game.effect.bullet_large_fire.event.bullet_large_hit_done
*arg !Anim:Target !selected
*start_anim $game.anim.hit_1 3
;
; Generate a "hit" on the target
; position.
;
Push !Effect:TargetPosition
Store !Map
*arg !Hit:Target !Map:Occupant
*arg !Hit:Type #hit_projectile
*select_owner
*arg !Hit:Strength $game.effect.bullet_large_fire.attribute.hit_strength
*hit 3
End
End
Finally, we emit an effect_completed event, signalling to the game that the effect is finished and the game can continue.
Begin Handler
EventID = $game.effect.bullet_large_fire.event.bullet_large_hit_done
MatchObject = True
Begin
*emit_event $game.event.effect_completed 0
End
End
Ranged weapons all follow this pattern - from the piddly little Cutters the robots start with through to the heavy stuff like the Grenade Launcher. One place where the grenade launcher differs is what happens when the grenade clunks off the forehead of some unlucky dweeb. Rather than a simple hit, this one triggers a second effect - a $game.effect.explosion!
Explosions make use of an operation called an Area. This operation takes a radius and a set of octants and executes an event handler for each affected map position. Octants divide an area of the map into 45 degree segments that can be operated on with obstacle detection - this is used by the lighting and vision code. The Area operation makes this available to code running in the virtual machine.
This algorithm came from a really helpful blog post named What The Hero Sees by Bob Nystrom. I've implemented it as a C++ class called OctantMapEnumerator that can be used like this:
OctantMapEnumerator mapEnum;
mapEnum.init(x, y, octants, radius);
while (mapEnum.nextOctant()) {
while (mapEnum.nextRow()) {
while (mapEnum.nextTile()) {
if (mapEnum.isVisible() == FALSE) {
continue;
}
// Do something with current tile
}
}
}
There are a number of other MapEnumerator classes with similar interfaces that are used for efficiently examining and manipulating the game map - RectMapEnumerator for rectangular areas, LineMapEnumerator for tracing a straight line between two tiles, AStarMapEnumerator for path finding an so on.
The result of the grenade launcher's Area Effect looks something like this:
Most of the weapons shown at the top of the post use this same pattern, just with different hit types and strengths, animations and sounds. One exception is the flamethrower, it isn't a ranged weapon. When the player uses the flamethrower, it triggers a "fire" effect that immediately blasts flames out in front of the robot.
The flames also use the Area operation. In this case, instead of being applied to all octants, it is only applied to those in the direction the robot is facing.
Thanks for reading, I hope you found this post interesting! I need to go and draw a better pulse rifle, I think.