Repository navigation
NPC Implementation
Ticket: #244 (NPC implementation), #317 (unit tests) Feature: #186
I replaced the old Traveler NPC with 4 new NPCs. They all walk around, talk to you, and stop and face you when you get close. Two of them also fight back if you hit them.
| NPC | Where | Map symbol | What they do |
|---|---|---|---|
| Hermes | Level 1 dungeon | * |
Shopkeeper, press F near him to open the shop. Can't be killed |
| Wizard | Level 1 Nether | 2 |
Throws a poison potion at you if you hit him |
| Philosopher | Level 2 base | 1 |
Quest giver, can be killed |
| Satyr | Level 2 skies | 3 |
Headbutts you if you hit him |
All 4 NPCs go through the same methods in NPCFactory so they act the same way:
-
createAnimatedNPC- adds the walk/idle animations. I reusedSkeletonAnimationControllersince it already flips between left and right -
addFriendlyNpcParts- physics, dialogue, wandering, and standing still when you're near -
makeKillable- gives health (50) so they can be killed. Hermes doesn't use this one
Each NPC picks what to do based on task priority (highest wins):
| Task | Priority | When |
|---|---|---|
| PlatformWanderTask | 1 | always |
| StandStillTask | 5 | player is close enough to talk |
| RetaliateTask | 10 | NPC got hit and player is in range |
So if you hit an NPC while talking to it, it switches to attacking straight away since 10 > 5.
Walk up to Hermes and press F, the shop opens. Walk away and it closes by itself. This is done in ShopkeeperComponent, it triggers an openShop event on the player that ShopDisplay listens for. I only had to add one line to the shop team's ShopDisplay for this.
If you hit the wizard he throws a potion at you. When it smashes and you're close to it (within 1.5 tiles), you get poisoned and lose half a heart every 2 seconds, 5 times. When you kill him he drops a coin and a regen potion.
Some rules:
- he only throws once per fight, he calms down 8 seconds after your last hit
- shields block the poison
- getting hit again resets the poison instead of stacking it
flowchart LR
A([Normal<br/>wander / talk])
B([Throw potion<br/>arcs from above his head])
C([Potion smashes<br/>ground or player])
D{Player within<br/>1.5 tiles<br/>and no shield?}
E([Poisoned<br/><i>5 damage every 2s × 5</i>])
F([No effect])
A -->|hit by player| B
B --> C
C --> D
D -->|yes| E
D -->|no| F
B -->|back to normal<br/><i>no 2nd throw until he calms down, 8s</i>| A
If you hit the satyr he lowers his head for half a second (that's the warning), then charges at you. If he hits you, you lose 1 heart and get knocked back. Unlike the wizard he keeps doing it every 1.5 seconds until he calms down. He won't run off his platform and you can dodge him by jumping over.
flowchart LR
A([Normal<br/><i>wander / talk</i>])
B([Wind-up<br/><i>0.5 s warning</i>])
C([Charge<br/><i>runs at player</i>])
D([Headbutt<br/><i>1 heart + knockback</i>])
E([Recover<br/><i>0.4 s</i>])
A -->|hit by player| B
B -->|after 0.5 s| C
C -->|touches player| D
C -->|misses / edge| E
D --> E
E -->|still angry,<br/>1.5 s break| B
E -->|calms down<br/><i>8 s after last hit</i>| A
| Class | What it does |
|---|---|
| ShopkeeperComponent | F opens shop, walking away closes it |
| StandStillTask | NPC stops and faces you while talking |
| ProvokedComponent | notices when the NPC loses health. Can do one attack per fight or keep attacking |
| RetaliateTask | starts the attack when the NPC is provoked and you're in range |
| PotionThrowComponent | works out the arc and throws the potion |
| SplashPotionComponent | the flying potion |
| SplashEffectComponent | plays the splash/impact animation once then removes it |
| PoisonEffectComponent | the poison damage over time + icon above your head |
| HeadbuttAttackComponent | wind up, charge, hit, recover |
| SpinningTextureRenderComponent | makes the potion spin |
classDiagram
StandStillTask <|-- RetaliateTask
RetaliateTask ..> ProvokedComponent : checks
RetaliateTask ..> PotionThrowComponent : triggers
RetaliateTask ..> HeadbuttAttackComponent : triggers
PotionThrowComponent ..> SplashPotionComponent : creates
PotionThrowComponent ..> PoisonEffectComponent : creates
PotionThrowComponent ..> SplashEffectComponent : creates
class StandStillTask {
stops and faces player
}
class RetaliateTask {
starts attack when provoked
}
class ProvokedComponent {
tracks NPC provocation
}
class PotionThrowComponent {
throws poison potion
}
class SplashPotionComponent {
flying potion
}
class PoisonEffectComponent {
applies damage over time
}
class SplashEffectComponent {
shows potion impact
}
class HeadbuttAttackComponent {
wind-up, charge and hit
}
If you want it to fight back, add a ProvokedComponent, your own attack component, and a RetaliateTask with your attack's event name:
npc.addComponent(new ProvokedComponent(8f))
.addComponent(new MyAttackComponent(player));
npc.getComponent(AITaskComponent.class)
.addTask(new RetaliateTask(player, 10, range, "myAttack"));Unit tests (14 tests):
-
ProvokedComponentTest- gets provoked when hit, not when healed, only one attack per fight, calms down, repeat attacks work -
PoisonEffectComponentTest- right damage per tick, nothing before first tick, restart works, no damage when paused -
RetaliateTaskTest- only attacks when provoked and in range -
NPCFactoryTest- all 4 NPC names are registered
I also play tested everything in game:
- all NPCs walk around and stop to talk
- F opens and closes Hermes' shop
- wizard potion splashes and poisons for 2.5 hearts, shield blocks it, drops loot after dying
- satyr keeps headbutting and calms down after 8s
- Traveler to 4 NPCs - the Traveler was hardcoded to one spot so it spawned in every level (even in mid air in level 2).
- Straight spell to a thrown potion - my first version was a straight spell but it never hit anything similar to the arrow by the enemy. Turned out its hitbox was on a layer with no collisions. Changed it to a potion thrown from above his head in an arc, which looks better anyway.
- Poison made slower - one tick per second felt too fast so I changed it to every 2 seconds, kind of like minecraft.
- Satyr attacks more than once - only attacking once didn't really make sense for him, so he keeps going now but I lowered the damage from 15 to 10 so it's not too hard.
- F is now also the special attack key so it clashes with the shop.
- NPCs respawn when you reload, so you could farm the wizard's loot.