RPG demo in narrat with dungeon crawler and turn based combat elements
Liana P
Published on June 16, 2022I built an RPG dungeon crawler demo for narrat as a way to test the engine and push the new recently added syntax improvements. It’s a pretty simple demo (about 200 lines of script) but involves picking a class, crawling through a dungeon and turn-based battles with random monsters.
What the demo looks like
While making this demo, I found a few bugs to fix and added a lot of new commands to the scripting language (especially commands related to maths, see 2.1.0 news). Overall, making this demo has confirmed to me that it’s possible to write programs that use more complex logic than your average simple branching narrative.
New examples system in the narrat repo
While making that demo, I also refactored the repo to be able to easily contain multiple example games and run each of them via environment variables. There is now an examples folder in the narrat repo with al the example/testing games.
Each example game can be run (or built) directly from the narrat repo, which makes it convenient to keep demos updated
The demo
You can play this demo and others at the narrat demos page.
The script
Here’s the full script for the demo:
main:
set_screen rpg
run setup_variables
setup_variables:
set spells.fireball.cost 10
set spells.whirlwind.cost 3
set spells.heal.cost 12
restart_character:
set_stat hp $player.maxHp
set_stat mp $player.maxMp
set victories 0
setup_character:
set_stat hp 0
set_stat mp 0
set player.name ""
set player.def 0
set player.name (text_field "Choose your character's name")
set best_victories 0
setup_rpg:
set_button startGame hidden
run setup_character
run choose_class
run start_rpg
start_rpg:
run restart_character
run enter_new_room
enter_new_room:
play music game
set pattern (run random_pattern)
run show_room
set_button go_left true
set_button go_right true
set_button go_front true
process_room:
if (== $randomChance 1):
"The room is empty."
else:
run random_battle
enter_and_process_room:
run enter_new_room
run process_room
show_room:
set_screen (concat "dungeon_" $pattern)
choose_left:
set player.direction left
jump go_direction
choose_right:
set player.direction right
jump go_direction
choose_front:
set player.direction front
jump go_direction
go_direction:
set_button go_left false
set_button go_front false
set_button go_right false
"%{player.name} turns %{player.direction}."
var randomChance (random 1 5)
run enter_and_process_room
random_pattern:
return (random_from_args "F" "FL" "FR" "LR" "FRL")
random_battle:
play music battle
set battle.attackedLast undefined
run start_level
set_button $player.class true
run random_enemy
"Suddenly, a %{enemy.name} appears!"
jump battle
battle_end:
stop music
log $battle.result
set_button $enemy.name false
set_button $player.class false
if (== $battle.result "won"):
add victories 1
"You defeated %{enemy.name}!"
if (>= $victories $best_victories)
set best_victories $victories
run show_room
else:
set_screen rpg
"You were defeated by %{enemy.name}! after %{victories} victories. Your best record is %{best_victories} victories."
choice:
"Restart?"
"Yes":
jump start_rpg
"No":
"Game over."
start_level:
var level (random_from_args "grass" "cave" "dungeon")
set_screen (concat "battle_" $level)
random_enemy:
set enemy.name (random_from_args "goblin" "slime" "skeleton")
set_button $enemy.name true
set enemy.hp (random 2 10)
set enemy.str (random 2 5)
set enemy.def (random 1 3)
set enemy.spd (random 1 3)
choose_class:
choice:
"Pick a class"
"Wizard":
set player.class wizard
set player.maxHp 30
set player.maxMp 60
set player.def 1
set player.spd 1
set_level strength 1
set_level agility 2
set_level intelligence 3
"Warrior":
set player.class warrior
set player.def 3
set player.maxHp 100
set player.maxMp 15
set player.spd 2
set_level strength 3
set_level agility 2
set_level intelligence 1
"Rogue":
set player.class rogue
set player.maxHp 50
set player.maxMp 30
set player.def 2
set player.spd 3
set_level strength 2
set_level agility 3
set_level intelligence 1
set_button $player.class true
"Your stats: %{stats.hp.value} HP, %{stats.mp.value} MP <br /> %{skills.strength.level} strength, %{skills.agility.level} agility, %{skills.intelligence.level} intelligence <br /> Defence: %{player.def}, Speed: %{player.spd}"
battle:
run get_attacker
log $battle.attacker
if (== $battle.attacker "player")
run process_combat_options
else:
run enemy_attack
if (<= (get_stat_value hp) 0):
set battle.result "lost"
else:
if (<= $enemy.hp 0):
set battle.result "won"
else:
run tick
jump battle
jump battle_end
get_attacker:
log $battle.attackedLast
if (== $battle.attackedLast undefined):
if (> $player.spd $enemy.spd):
"%{player.name} attacks first!"
set battle.attackedLast "player"
set battle.attacker "player"
else:
"%{enemy.name} attacks first!"
set battle.attackedLast "enemy"
set battle.attacker "enemy"
else:
if (== $battle.attackedLast "player"):
set battle.attackedLast "enemy"
set battle.attacker "enemy"
else:
set battle.attackedLast "player"
set battle.attacker "player"
process_combat_options:
if (== $player.class warrior):
return (run warrior_combat)
if (== $player.class wizard):
return (run wizard_combat)
if (== $player.class rogue):
return (run rogue_combat)
warrior_combat:
choice:
"Choose an action"
"Attack":
return (run attack)
"Whirlwind (%{spells.whirlwind.cost} MP)" if (>= (get_stat_value mp) $spells.whirlwind.cost):
return (run whirlwind)
rogue_combat:
choice:
"Choose an action"
"Attack":
return (run attack)
wizard_combat:
choice:
"Choose an action"
"Attack":
return (run attack)
"Fireball (%{spells.fireball.cost} MP)" if (>= (get_stat_value mp) $spells.fireball.cost):
return (run fireball)
"Heal (%{spells.heal.cost} MP)" if (>= (get_stat_value mp) $spells.heal.cost):
return (run heal)
attack:
var damage (+ 1 $skills.strength.level)
run process_hit_on_monster $damage $enemy
add_xp strength 1
fireball:
var damage (* 3 (get_level intelligence))
run process_hit_on_monster $damage $enemy
add_xp intelligence 3
add_stat mp (neg $spells.fireball.cost)
heal:
var heal (* 2 (get_level intelligence))
add_stat hp $heal
add_xp intelligence 3
add_stat mp (neg $spells.heal.cost)
set_stat hp (min (get_stat_value hp) $player.maxHp)
"%{player.name} healed for %{heal} HP!"
whirlwind:
var damage (* 3 (get_level strength))
run process_hit_on_monster $damage $enemy
add_stat mp (neg $spells.whirlwind.cost)
add_xp strength 3
enemy_attack:
var damage (+ 1 $enemy.str)
run process_hit_on_player $damage
process_hit_on_monster damage enemy:
var final_dmg (run calculate_defenses $damage $enemy.def)
add enemy.hp (neg $final_dmg)
"The %{enemy.name} takes %{final_dmg} damage!"
process_hit_on_player damage:
var final_dmg (run calculate_defenses $damage $player.def)
add_stat hp (neg $final_dmg)
"%{player.name} takes %{final_dmg} damage!"
calculate_defenses damage def:
return (max 0 (- $damage $def))
tick:
if (== $player.class "wizard"):
add_stat mp 5
if (== $player.class "warrior"):
add_stat mp 1
if (== $player.class "rogue"):
add_stat mp 2