0,}q8d%qy)6o;]AsVl3 ```` ` ` ```` ` ` ```` ` ` ```` ` ` ```` ` ` ```` ` ` ```` ` ` ```` ` `  `  @  `  @          `@``@``@``@``@`0 00 ? "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" """"" """"" """"" """"" """"" """""!""!""!""!""!""!""!""!""!""!""!""!""%-- title: Story of the Hero -- author: Alex Mayer -- desc: Make the challenging choices every hero must overcome in this story based game. -- site: https://gitlab.com/amayer5125/story-of-the-hero -- license: MIT License -- version: 0.2.0 -- script: lua -- input: gamepad -- saveid: StoryOfTheHero mode = "title" init = true current_tick = 0 music_track = nil Character = {} function Character:new(o) o = o or {} setmetatable(o, self) self.__index = self return o end function Character:setHealth(health) self.health = math.max(math.min(health, self.max_health), 0) end Player = Character:new{health = 50, max_health = 50} Attack = {damage = 0} function Attack:new(o) o = o or {} setmetatable(o, self) self.__index = self return o end function Attack:getDamage() return self.damage end function Attack:getAttackResult() return AttackResult:new{damage = self:getDamage()} end AttackResult = {damage = 0, health_restored = 0} function AttackResult:new(o) o = o or {} setmetatable(o, self) self.__index = self return o end -- armor DirtyCloths = {name = "Dirty clothing", damage_negation_percent = 0} LightArmor = {name = "Light armor", damage_negation_percent = 0.2} HeavyArmor = {name = "Heavy armor", damage_negation_percent = 0.4} -- physical attacks PhysicalAttack = Attack:new{type = "physical"} UseWeapon = PhysicalAttack:new{weapon = nil} function UseWeapon:new(weapon) o = setmetatable({}, self) self.__index = self self.weapon = weapon self.name = "Weapon (" .. self.weapon.name .. ")" return o end function UseWeapon:getDamage() return self.weapon:getDamage() end ShadowGore = PhysicalAttack:new{name = "Shadow Gore", damage = 12} ShadowStomp = PhysicalAttack:new{name = "Shadow Stomp", damage = 8} Bunnigore = PhysicalAttack:new{name = "Bunnigore", damage = 6} SoulDrainingPunch = PhysicalAttack:new{name = "Soul Draining Punch", damage = 8} function SoulDrainingPunch:getAttackResult() return AttackResult:new{damage = self:getDamage(), health_restored = 2} end Bite = PhysicalAttack:new{name = "Bite", damage = 5} Bash = PhysicalAttack:new{name = "Bash", damage = 12} Scratch = PhysicalAttack:new{name = "Scratch", damage = 2} -- weapons Weapon = {damage = 0} function Weapon:new(o) o = o or {} setmetatable(o, self) self.__index = self return o end function Weapon:getDamage() -- deal variable damage to add some realism local damage = math.floor(self.damage * math.random(75, 100) / 100 + 0.5) -- critical hit if math.random(10) == 10 then damage = damage * 2 end return damage end Dagger = Weapon:new{name = "Dagger", damage = 5} ShortSword = Weapon:new{name = "Short sword", damage = 7} SacredSwordOfShadow = Weapon:new{name = "Sacred Sword of Shadow", damage = 11} Naginata = Weapon:new{name = "Naginata", damage = 9} HolySwordOfLight = Weapon:new{name = "Holy Sword of Light", damage = 11} -- spells Spell = Attack:new{type = "spell"} ShadowBeam = Spell:new{name = "Shadow Beam", damage = 15} VioletFire = Spell:new{name = "Violet Fire", damage = 8} HealingLight = Spell:new{name = "Healing Light"} function HealingLight:getAttackResult() return AttackResult:new{health_restored = 15} end Fireball = Spell:new{name = "Fireball", damage = 5} AbsorbLight = Spell:new{name = "Absorb Light"} function AbsorbLight:getAttackResult() return AttackResult:new{health_restored = 15} end CallOfTheFae = Spell:new{name = "Call of the Fae", damage = 8} DragDown = Spell:new{name = "Drag Down", damage = 10} LeechLife = Spell:new{name = "Leech Life", damage = 10} function LeechLife:getAttackResult() return AttackResult:new{damage = self:getDamage(), health_restored = math.random(6, 10)} end RootOfEvil = Spell:new{name = "Root of Evil", damage = 10} DarkBackstory = Spell:new{name = "Dark Backstory", damage = 7} HealingFlame = Spell:new{name = "Healing Flame"} function HealingFlame:getAttackResult() return AttackResult:new{health_restored = math.random(15, 30)} end -- non-player characters NonPlayerCharacter = Character:new{sprite_width = 2, sprite_height = 2, sprite_scale = 4} Bunnicorn = NonPlayerCharacter:new{name = "Bunnicorn", max_health = 50, health = 50, sprite = 136} function Bunnicorn:getNextAttack() local attack = math.random(2) if attack == 2 then return Bite:new() end return Bunnigore:new() end Goblin = NonPlayerCharacter:new{name = "Goblin", max_health = 38, health = 38, sprite = 8} function Goblin:getNextAttack() local attack = math.random(5) if attack == 5 then return Scratch:new() end return UseWeapon:new(ShortSword) end BabyPhoenix = NonPlayerCharacter:new{name = "Baby Phoenix", max_health = 50, health = 50, sprite = 2, can_use_healing_flame = true} function BabyPhoenix:getNextAttack() if self.can_use_healing_flame and self.health <= self.max_health / 2 then self.can_use_healing_flame = false return HealingFlame:new() end if math.random(2) == 1 then return VioletFire:new() end return Fireball:new() end LichWarrior = NonPlayerCharacter:new{name = "Lich Warrior", max_health = 60, health = 60, sprite = 388, sprite_width = 4, sprite_height = 3, sprite_scale = 2, can_use_leech_life = true} function LichWarrior:getNextAttack() if self.can_use_leech_life and self.health <= self.max_health / 2 then self.can_use_leech_life = false return LeechLife:new() end if math.random(2) == 1 then return DarkBackstory:new() end return SoulDrainingPunch:new() end Oberon = NonPlayerCharacter:new{name = "Oberon", max_health = 100, health = 100, force_root_of_evil = true, use_healing_light_next_turn = false, sprite = 900, sprite_width = 4, sprite_height = 4, sprite_scale = 3} function Oberon:getNextAttack() if self.use_healing_light_next_turn and self.health < self.max_health then self.use_healing_light_next_turn = false return HealingLight:new() end if self.force_root_of_evil and self.health <= self.max_health / 2 then self.force_root_of_evil = false return RootOfEvil:new() end local attack = math.random(10) if attack == 1 then if self.health <= self.max_health - 8 then return HealingLight:new() end self.use_healing_light_next_turn = true end if attack <= 3 then self.force_root_of_evil = false return RootOfEvil:new() elseif attack <= 5 then return Bash:new() end return CallOfTheFae:new() end ShadowStag = NonPlayerCharacter:new{name = "Shadow Stag", max_health = 150, health = 150, sprite = 896, sprite_width = 4, sprite_height = 4, sprite_scale = 3} function ShadowStag:getNextAttack() local attack = math.random(10) if attack >= 8 then return DragDown:new() elseif attack >= 5 then return ShadowStomp:new() elseif attack >= 3 then return AbsorbLight:new() elseif attack >= 2 then return ShadowBeam:new() end return ShadowGore:new() end -- potions Potion = {health_restored = 15} function Potion:new(o) o = o or {} setmetatable(o, self) self.__index = self return o end function Potion:getHealthRestored() return self.health_restored end MinorPotion = Potion:new{name = "Minor potion", description = "A potion that heals the player for 15 health."} MajorPotion = Potion:new{name = "Major potion", health_restored = 30, description = "A potion that heals the player for 30 health."} FullPotion = Potion:new{name = "Full potion", description = "A potion that fully heals the player."} function FullPotion:getHealthRestored() return Player.max_health - Player.health end DialogTimeline = {index = 1} function DialogTimeline:new(o) o = o or {} setmetatable(o, self) self.__index = self return o end function DialogTimeline:next() local current = self.events[self.index] if current:hasEvents() then for i, event in ipairs(current:getEvents()) do table.insert(self.events, self.index + i, event) end end self.index = self.index + 1 if self.index >= #self.events then return nil end if self.events[self.index].type == "conditional" then return self:next() end return self.events[self.index] end function DialogTimeline:getCurrentDialog() return self.events[self.index] end StoryNode = {} function StoryNode:new(o) o = o or {} setmetatable(o, self) self.__index = self return o end function StoryNode:hasEvents() return self.events ~= nil end function StoryNode:getEvents() return {} end ConditionalNode = StoryNode:new{type = "conditional", condition = function() return true end, events = {}} function ConditionalNode:getEvents() if self.condition() then return self.events end return {} end SceneNode = StoryNode:new{type = "scene", sprites = {}} function SceneNode:new(sprites) o = setmetatable({sprites = sprites}, self) self.__index = self return o end DialogNode = StoryNode:new{type = "dialog", callback = function() end} PROMPT_YES = 1 PROMPT_NO = 2 PromptNode = StoryNode:new{type = "prompt", options = {[PROMPT_YES] = "Yes", [PROMPT_NO] = "No"}} function PromptNode:setChoice(choice) self.choice = choice end function PromptNode:getEvents() return self.events(self.choice) or {} end CombatNode = StoryNode:new{type = "combat", player_goes_first = true} function CombatNode:setOutcome(victory) self.victory = victory end function CombatNode:getEvents() return self.events(self.victory) end EndingNode = StoryNode:new{type = "ending"} function EndingNode:new(ending) o = setmetatable({ending = ending}, self) self.__index = self return o end ETERNAL_SLEEP_OF_THE_HERO = 1 QUENCHED_THIRST_OF_THE_HERO = 2 SOFT_SPRING_LIGHT_SHINES_ON_THE_HERO = 3 UNENDING_THIRST_OF_THE_HERO = 4 HUMERUS_COMPANY_OF_THE_HERO = 5 FRIENDS_OF_THE_HERO = 6 WARM_EMBRACE_OF_THE_HERO = 7 FIERY_RETRIBUTION_OF_THE_HERO = 8 ETERNAL_UNREST_OF_THE_HERO = 9 IMMORTAL_JOY_OF_THE_HERO = 10 FINAL_RESTING_PLACE_OF_THE_HERO = 11 UNDECIDED_FATE_OF_THE_HERO = 12 endings = { [ETERNAL_SLEEP_OF_THE_HERO] = {name = "Eternal Sleep of the Hero", icon = 330}, [QUENCHED_THIRST_OF_THE_HERO] = {name = "Quenched Thirst of the Hero", icon = 134}, [SOFT_SPRING_LIGHT_SHINES_ON_THE_HERO] = {name = "Soft Spring Light Shines on the Hero", icon = 334}, [UNENDING_THIRST_OF_THE_HERO] = {name = "Unending Thirst of the Hero", icon = 140}, [HUMERUS_COMPANY_OF_THE_HERO] = {name = "Humerus Company of the Hero", icon = 66}, [FRIENDS_OF_THE_HERO] = {name = "Friends of the Hero", icon = 12}, [WARM_EMBRACE_OF_THE_HERO] = {name = "Warm Embrace of the Hero", icon = 4}, [FIERY_RETRIBUTION_OF_THE_HERO] = {name = "Fiery Retribution of the Hero", icon = 2}, [ETERNAL_UNREST_OF_THE_HERO] = {name = "Eternal Unrest of the Hero", icon = 768}, [IMMORTAL_JOY_OF_THE_HERO] = {name = "Immortal Joy of the Hero", icon = 200}, [FINAL_RESTING_PLACE_OF_THE_HERO] = {name = "Final Resting Place of the Hero", icon = 770}, [UNDECIDED_FATE_OF_THE_HERO] = {name = "Undecided Fate of the Hero", icon = 640}, } function BOOT() -- set bits per pixel to 2 poke4(2 * 0x3ffc, 4) math.randomseed(tstamp()) end function TIC() if init then initGame() init = false end if music_track ~= desired_music_track then music_track = desired_music_track music(music_track) end if mode == "game" then game() elseif mode == "title" then title() elseif mode == "endings" then endings_screen() elseif mode == "controls" then controls() elseif mode == "credits" then credits() end current_tick = current_tick + 1 end function game() cls() local current_dialog = timeline:getCurrentDialog() if current_dialog.type == "scene" then current_scene = current_dialog.sprites timeline:next() current_dialog = timeline:getCurrentDialog() end local show_scene = true if current_dialog.type == "dialog" then desired_music_track = 0 if dialog(callable(current_dialog.text)) then current_dialog.callback() timeline:next() end elseif current_dialog.type == "prompt" then desired_music_track = 0 local selected = prompt(callable(current_dialog.text), callable(current_dialog.options)) if selected ~= nil then current_dialog:setChoice(selected) timeline:next() end elseif current_dialog.type == "combat" then show_scene = false desired_music_track = 1 -- create opponent if they don't exist if game_combat_opponent == nil then game_combat_opponent = current_dialog.opponent:new() end local player_wins = combat(game_combat_opponent, current_dialog.player_goes_first) if player_wins ~= nil then game_combat_opponent = nil current_dialog:setOutcome(player_wins) timeline:next() end elseif current_dialog.type == "ending" then show_scene = false desired_music_track = 0 if current_dialog.ending == UNDECIDED_FATE_OF_THE_HERO and show_ending_scene then spr(512, 8, 0, 0, 4, 0, 0, 7, 4) local width = print("The End", 0, 136, 1, false, 2) print("The End", (240-width)//2, 94, 1, false, 2) if btnp(4) then show_ending_scene = false end else pmem(0, pmem(0) | 1 << (current_dialog.ending - 1)) ending_selected = current_dialog.ending mode = "endings" end end if show_scene then for _, sprite in ipairs(current_scene or {}) do local sprite_id = sprite.sprite -- animate the trees if sprite_id == 72 or sprite_id == 76 then sprite_id = sprite_id+current_tick%240//120*2 end spr(sprite_id, sprite.x, sprite.y, 0, sprite.scale or 1, sprite.flip or 0, 0, sprite.width or 1, sprite.height or 1) end end end title_current_option = 1 function title() cls() print("Story of the", 18, 10, 1, false, 3) print("Hero", 60, 28, 1, false, 5) local options_y = 78 local options = { {text = "Start Adventure", mode = "game"}, {text = "Endings", mode = "endings"}, {text = "Controls", mode = "controls"}, {text = "Credits", mode = "credits"}, } for i, option in ipairs(options) do print(option.text, 76, options_y+(9*(i-1)), 1) end -- arrow spr(495, 69, options_y+(9*(title_current_option-1)), 0, 1, 1, 1) spr(Bunnicorn.sprite, 194, 112, 0, 2, 0, 0, 2, 2) rect(200, 134, 4, 2, 1) rect(220, 134, 4, 2, 1) if btnp(0) then title_current_option = title_current_option - 1 if title_current_option < 1 then title_current_option = #options end elseif btnp(1) then title_current_option = title_current_option + 1 if title_current_option > #options then title_current_option = 1 end end if btnp(4) then init = true mode = options[title_current_option].mode end end function endings_screen() cls() if ending_selected == nil then ending_selected = 1 end local y = 0 local step = 26 local first = math.max(1, ending_selected - 2) local last = math.min(first + 5, #endings) if last < first + 5 then first = last - 5 y = -18 end local highlighted = ending_selected - first + 1 local color for i, ending in ipairs(slice(endings, first, first + 5)) do if i == highlighted then color = 2 else color = 1 end rectb(2, y+(step*(i-1)), 24, 24, 1) rectb(4, y+2+(step*(i-1)), 20, 20, 2) rectb(28, y+(step*(i-1)), 212, 24, color) if pmem(0) & 1 << (first + i - 2) ~= 0 then spr(ending.icon, 6, y+4+(step*(i-1)), 0, 1, 0, 0, 2, 2) print(ending.name, 31, y+9+(step*(i-1)), 1) else print("?", 8, y+7+(step*(i-1)), 1, 0, 2) print("???", 31, y+9+(step*(i-1)), color) end end if btnp(0) then ending_selected = ending_selected - 1 if ending_selected < 1 then ending_selected = 1 end elseif btnp(1) then ending_selected = ending_selected + 1 if ending_selected > #endings then ending_selected = #endings end end if btnp(5) then init = true mode = "title" end end function controls() cls() local width = print("Controls", 0, 136, 1, false, 2) print("Controls", (240-width)//2, 2, 1, false, 2) local controls = { {controller = "Controller", keyboard = "Keyboard", description = "Description"}, {controller = "D-Pad", keyboard = "Arrows", description = "Change selection"}, {controller = "A", keyboard = "Z", description = "Confirm selection"}, {controller = "B", keyboard = "X", description = "Advance text"}, } local y = 25 local table_hight = 11 local table_padding = 3 for i, control in ipairs(controls) do local text_color = 1 if i == 1 then text_color = 2 end rectb(0, y, 63, table_hight, 1) print(control.controller, table_padding, y + table_padding, text_color) rectb(62, y, 53, table_hight, 1) print(control.keyboard, 62 + table_padding, y + table_padding, text_color) rectb(114, y, 125, table_hight, 1) print(control.description, 114 + table_padding, y + table_padding, text_color) y = y + table_hight - 1 end print("Press B/X to return to title screen", 0, y + 11, 1) if btnp(5) then mode = "title" end end function credits() cls() if credits_y == nil then credits_y = 30 end local width = print("Story, artwork, and music:", 0, 136, 1) local x = (240-width)//2 print("Story, artwork, and music:", x, credits_y, 1) spr(640, x, credits_y + 8, 0, 1, 0, 0, 2, 2) print("Lazulia", x + 20, credits_y + 10, 2, false, 2) print("Development:", x, credits_y + 30, 1) spr(Goblin.sprite, x, credits_y + 30 + 8, 0, 1, 0, 0, 2, 2) print("Alex Mayer", x + 20, credits_y + 30 + 10, 2, false, 2) print("The player:", x, credits_y + 136 + 54, 1) spr(14, x + 3, credits_y + 136 + 54 + 8, 0, 1, 0, 0, 2, 2) print("You", x + 20, credits_y + 136 + 54 + 10, 2, false, 2) print("Thank you for playing!!!", x, credits_y + 136 + 54 + 8 + 16 + 3, 1) if (btn(0) or btn(1)) and current_tick % 2 == 0 then credits_y = credits_y - 1 end if btnp(5) or credits_y <= -225 then mode = "title" credits_y = nil end end function dialog(text, x, y, width, height) x = x or 0 y = y or 96 width = width or 240 height = height or 40 if dialog_start_tick == nil then dialog_start_tick = current_tick dialog_lines = wordwrap(text, width - 6) dialog_current_letter = 0 dialog_current_line = 1 end -- boarder rectb(x, y, width, height, 1) if current_tick ~= dialog_start_tick and (current_tick - dialog_start_tick) % 2 == 0 then dialog_current_letter = dialog_current_letter + 1 end local lines_to_print = slice(dialog_lines, dialog_current_line, dialog_current_line + 2) local letters_printed = 0 local printing = false for i, line in ipairs(lines_to_print) do if letters_printed >= dialog_current_letter then printing = true break end if #line + letters_printed > dialog_current_letter then line = string.sub(line, 0, dialog_current_letter - letters_printed) printing = true end print(line, x+3, y+3+(9*(i-1)), 1) letters_printed = letters_printed + #line end if not printing then local continue_x = 227 local continue_y = 128 if current_tick % 40 // 20 == 0 then continue_y = continue_y - 1 end spr(495, continue_x, continue_y, 0) if btnp(4) or btnp(5) then if dialog_current_line + 3 > #dialog_lines then dialog_start_tick = nil return true end dialog_current_line = dialog_current_line + 3 dialog_current_letter = 0 end -- pressing A or B while printing will output the full dialog elseif btnp(4) or btnp(5) then local total_letters = 0 for _, line in ipairs(lines_to_print) do total_letters = total_letters + #line end dialog_current_letter = total_letters end end function prompt(text, options, x, y, width, height) x = x or 0 y = y or 96 width = width or 240 height = height or 40 if prompt_start_tick == nil then prompt_start_tick = current_tick prompt_lines = wordwrap(text, width - 6) prompt_current_option = 1 end -- boarder rectb(x, y, width, height, 1) for i, line in ipairs(prompt_lines) do print(line, x+3, y+3+(9*(i-1)), 1) end local prompt_offset = 9 * #prompt_lines for i, option in ipairs(options) do print(option, x+10, y+prompt_offset+3+(9*(i-1)), 1) end -- arrow spr(495, x+3, y+prompt_offset+3+(9*(prompt_current_option-1)), 0, 1, 1, 1) if btnp(0) then prompt_current_option = prompt_current_option - 1 if prompt_current_option < 1 then prompt_current_option = #options end elseif btnp(1) then prompt_current_option = prompt_current_option + 1 if prompt_current_option > #options then prompt_current_option = 1 end end if btnp(4) then prompt_start_tick = nil return prompt_current_option end end function combat(opponent, player_goes_first) local opponent_x = 120-(8*opponent.sprite_width*opponent.sprite_scale)/2 + (opponent_x_offset or 0) local opponent_y = 48-(8*opponent.sprite_height*opponent.sprite_scale)/2 spr(opponent.sprite, opponent_x, opponent_y, 0, opponent.sprite_scale, 0, 0, opponent.sprite_width, opponent.sprite_height) local hero_health_percent = Player.health / Player.max_health * 100 print("Hero", 0, 78, 1) rect(0, 85, hero_health_percent, 9, 3) rectb(0, 85, 100, 9, 1) local opponent_health_percent = opponent.health / opponent.max_health * 100 rect(140+(100-opponent_health_percent), 0, opponent_health_percent, 9, 3) rectb(140, 0, 100, 9, 1) local width = print(opponent.name, 0, 136) print(opponent.name, 240-width, 11, 1) if Player.health == 0 or opponent.health == 0 then combat_phase = "end_combat" end if combat_start_tick == nil then combat_start_tick = current_tick combat_animations = {} if player_goes_first then combat_phase = "player_action" else combat_phase = "opponent_action" end end if #combat_animations > 0 then for i, effect in ipairs(combat_animations) do if effect.type == "player_heal" then if effect.x == nil then effect.x = 40 end if effect.y == nil then effect.y = 85 end if current_tick % 2 == 0 then effect.y = effect.y - 1 end print("+" .. effect.change, effect.x, effect.y, 2, false, 1, true) if effect.y <= 65 then table.remove(combat_animations, i) end elseif effect.type == "player_damage" then if effect.x == nil then effect.x = 40 end if effect.y == nil then effect.y = 85 end if current_tick % 2 == 0 then effect.y = effect.y - 1 end print("-" .. effect.change, effect.x, effect.y, 3, false, 1, true) if effect.y <= 65 then table.remove(combat_animations, i) end elseif effect.type == "opponent_heal" then if effect.x == nil then effect.x = 180 end if effect.y == nil then effect.y = 6 end if current_tick % 2 == 0 then effect.y = effect.y + 1 end print("+" .. effect.change, effect.x, effect.y, 2, false, 1, true) if effect.y >= 26 then table.remove(combat_animations, i) end elseif effect.type == "opponent_damage" then if effect.x == nil then effect.x = 180 end if effect.y == nil then effect.y = 6 end if current_tick % 2 == 0 then effect.y = effect.y + 1 end print("-" .. effect.change, effect.x, effect.y, 3, false, 1, true) if effect.y >= 26 then table.remove(combat_animations, i) end elseif effect.type == "opponent_shake" then if effect.end_tick == nil then effect.end_tick = current_tick + 24 end if current_tick % 12 == 0 then opponent_x_offset = -3 elseif current_tick % 6 == 0 then opponent_x_offset = 3 end if current_tick >= effect.end_tick then opponent_x_offset = nil table.remove(combat_animations, i) end end end end if combat_phase == "player_action" then local player_options = {"Attack with " .. Player.weapon.name} if #Player.spells > 0 then table.insert(player_options, "Cast Spell") end if #Player.inventory > 0 and Player.health < Player.max_health then table.insert(player_options, "Use Potion") end local choice = prompt("What do you do?", player_options) if choice == 1 then combat_phase = "player_attack" elseif player_options[choice] == "Cast Spell" then combat_phase = "player_spell" elseif player_options[choice] == "Use Potion" then combat_phase = "player_item" end elseif combat_phase == "player_attack" then combat_attack = UseWeapon:new(Player.weapon) combat_attack_result = combat_attack:getAttackResult() combat_phase = "player_deal_damage" elseif combat_phase == "player_spell" then local player_options = {} for _, spell in ipairs(Player.spells) do table.insert(player_options, spell.name) end local choice = prompt("Which spell would you like to cast?", player_options) if choice ~= nil then combat_attack = Player.spells[choice]:new() combat_attack_result = combat_attack:getAttackResult() combat_phase = "player_deal_damage" end if btnp(5) then combat_phase = "player_action" end elseif combat_phase == "player_item" then local player_options = {} for _, item in ipairs(Player.inventory) do table.insert(player_options, item.name) end local choice = prompt("Which item would you like to use?", player_options) if choice ~= nil then combat_player_potion = Player.inventory[choice]:new() table.remove(Player.inventory, choice) combat_phase = "player_heal" end if btnp(5) then combat_phase = "player_action" end elseif combat_phase == "player_deal_damage" then local damage = combat_attack_result.damage if combat_attack.type == "physical" and opponent.armor ~= nil then damage = damage - math.floor(damage * opponent.armor.damage_negation_percent + 0.5) end local text = "" if damage > 0 then text = text .. " You deal " .. damage .. " damage." end if combat_attack_result.health_restored > 0 then text = text .. " You heal " .. combat_attack_result.health_restored .. " health." end if dialog(text) then opponent:setHealth(opponent.health - damage) Player:setHealth(Player.health + combat_attack_result.health_restored) if damage > 0 then table.insert(combat_animations, { type = "opponent_damage", change = damage, }) table.insert(combat_animations, { type = "opponent_shake", }) end if combat_attack_result.health_restored > 0 then table.insert(combat_animations, { type = "player_heal", change = combat_attack_result.health_restored, }) end combat_attack = nil combat_attack_result = nil combat_phase = "opponent_action" end elseif combat_phase == "player_heal" then local health_restored = combat_player_potion:getHealthRestored() if dialog(combat_player_potion.name .. " healed " .. health_restored .. " health.") then Player:setHealth(Player.health + health_restored) combat_phase = "opponent_action" table.insert(combat_animations, { type = "player_heal", change = health_restored, }) end elseif combat_phase == "opponent_action" then combat_attack = opponent:getNextAttack() combat_attack_result = combat_attack:getAttackResult() combat_phase = "opponent_deal_damage" elseif combat_phase == "opponent_deal_damage" then local damage = combat_attack_result.damage if combat_attack.type == "physical" and Player.armor ~= nil then damage = damage - math.floor(damage * Player.armor.damage_negation_percent + 0.5) end local text = "" if combat_attack.type == "spell" then text = opponent.name .. " uses " .. combat_attack.name .. "." else text = opponent.name .. " attacks you." end if damage > 0 then text = text .. " You take " .. damage .. " damage." end if dialog(text) then Player:setHealth(Player.health - damage) opponent:setHealth(opponent.health + combat_attack_result.health_restored) if damage > 0 then table.insert(combat_animations, { type = "player_damage", change = damage, }) end if combat_attack_result.health_restored > 0 then table.insert(combat_animations, { type = "opponent_heal", change = combat_attack_result.health_restored, }) end combat_attack = nil combat_attack_result = nil combat_phase = "player_action" end elseif combat_phase == "end_combat" then local message = "You died!" if Player.health ~= 0 then message = opponent.name .. " defeated!" end -- clean up from combat and return the result if dialog(message) and #combat_animations == 0 then combat_start_tick = nil return Player.health ~= 0 end end end function trim(str) return str:gsub("^%s*(.-)%s*$", "%1") end function wordwrap(text, width) width = width or 240 local words = {} for word in string.gmatch(text, "%S+") do table.insert(words, word) end local lines = {""} for _, word in ipairs(words) do if print(trim(lines[#lines] .. " " .. word), 0, 136) > width then table.insert(lines, "") end lines[#lines] = trim(lines[#lines] .. " " .. word) end return lines end function slice(array, first, last) local result = {} for i = first, last do table.insert(result, array[i]) end return result end function callable(entity) if type(entity) == "function" then return entity() end return entity end function initGame() current_tick = 0 show_ending_scene = true desired_music_track = 0 Player.health = 50 Player.weapon = Dagger:new() Player.armor = DirtyCloths Player.inventory = { MinorPotion, MajorPotion, } Player.spells = {} initStory() end function initStory() local inventory_prompt = PromptNode:new{ text = "Which would you like to drink?", options = function() local options = {} for _, potion in ipairs(Player.inventory) do table.insert(options, potion.name) end table.insert(options, "Nevermind") return options end, events = function(choice) if choice <= #Player.inventory then local health_restored = Player.inventory[choice]:getHealthRestored() Player:setHealth(Player.health + health_restored) table.remove(Player.inventory, choice) return { DialogNode:new{ text = "You heal " .. health_restored .. " health.", }, } end end, } local final_resting_place_of_the_hero_ending = { SceneNode:new{ {sprite = ShadowStag.sprite, x = 72, y = 0, width = 4, height = 4, scale = 3}, }, DialogNode:new{ text = function() return "The " .. ShadowStag.name .. " stomps out the life from your body. You sense it removes the " .. HolySwordOfLight.name .. ". Whether it returns it to the oak tree or hides it away, you will never know. You decide to embrace the world of shadow that surrounds you. Maybe you didn't find salvation, but at least now you can rest peacefully without fear of the morning sun waking you." end, }, EndingNode:new(FINAL_RESTING_PLACE_OF_THE_HERO), } local blank_tree_scene = SceneNode:new{ {sprite = 72, x = 0, y = 0, width = 2, height = 2, scale = 6}, {sprite = 76, x = 144, y = 0, width = 2, height = 2, scale = 6}, } local blank_path_scene = SceneNode:new{ {sprite = 260, x = 0, y = 0, width = 1, height = 2, scale = 6}, {sprite = 260, x = 192, y = 0, width = 1, height = 2, scale = 6, flip = 1}, } timeline = DialogTimeline:new{ events = { SceneNode:new{ {sprite = 70, x = 0, y = 10, width = 2, height = 2, scale = 4}, {sprite = 68, x = 60, y = 60, width = 2, height = 1, scale = 2}, {sprite = 100, x = 120, y = 12, width = 2, height = 2, scale = 5}, }, DialogNode:new{ text = "Your village has been undergoing famine for some time now. In spite of the history that exists between your village and the magical forest nearby, blame has been assigned to the magical creatures within. You've not eaten for three days and can no longer remember your name.", }, PromptNode:new{ text = "Assign yourself a new name?", }, DialogNode:new{ text = "It hardly matters what you're called. Nobody in town is left to call your name anyway. The trade routes to your village have been blocked off by boulders.", }, PromptNode:new{ text = "Will you stay in the village?", events = function(choice) if choice == PROMPT_YES then return { DialogNode:new{ text = "You decide to stay with your family's bodies. You believe they are comforted by your warmth. Lying next to your loved ones, you drift off into an eternal sleep. You dream you are a hero that defeats the 'Dark Lord'. You are truly a hero adrift in a sea of death.", }, EndingNode:new(ETERNAL_SLEEP_OF_THE_HERO) } end end, }, blank_tree_scene, DialogNode:new{ text = "You journey into the magical forest. At first, it is uncomfortably still and dark, but then light begins to filter through the trees and the babbling of a brook reaches your ears. For the first time in weeks, you feel a sense of peace.", }, DialogNode:new{ text = "You come to a place where the road splits into three separate paths.", }, DialogNode:new{ text = "To the left is the sound of running water. Having nothing to drink in days, you have a thirst that cannot be quenched.", }, DialogNode:new{ text = "To the right is an idyllic woodland path. You see berries and hear the sound of small animals. Down the center is a dark and winding path. Rocks, thorns, and dust promise only hardship.", }, PromptNode:new{ text = "Which path will you take?", options = {"Water", "Woodland", "Winding"}, events = function(choice) local WATER_PATH, WOODLAND_PATH, WINDING_PATH = 1, 2, 3 if choice == WATER_PATH then return { DialogNode:new{ text = "You run as fast as your weak body can manage to the source of the sound. A spring with water as pure and beautiful as morning dew spreads out before you, but you hear the rustling of leaves to your right.", }, PromptNode:new{ text = "Which will you approach?", options = {"Water", "Leaves"}, events = function(choice) local WATER, LEAVES = 1, 2 if choice == WATER then return { SceneNode:new{ {sprite = 128, x = 40, y = 0, width = 2, height = 2, scale = 6}, }, DialogNode:new{ text = "You approach the water and look in to see your reflection before you drink. Your face is gaunt and your eyes sunken. You are covered in dirt, and you struggle to recall what you looked like before.", }, DialogNode:new{ text = "After a moment, a face replaces your own. It is beautiful and full with eyes an iridescent purple and lips as pink as lillies. Thin arms stretch from the water and wrap around your neck. The beautiful creature kisses you as it drags you into the depths of the water. You are never thirsty again.", }, EndingNode:new(QUENCHED_THIRST_OF_THE_HERO), } elseif choice == LEAVES then return { SceneNode:new{ {sprite = 130, x = 72, y = 0, width = 2, height = 2, scale = 6}, }, DialogNode:new{ text = "You approach the rustling leaves and see a small rabbit with a magic horn. It stares up at you with warm brown eyes.", }, SceneNode:new{ {sprite = Bunnicorn.sprite, x = 88, y = 16, width = 2, height = 2, scale = 4}, }, DialogNode:new{ text = function() return "Eyes that remind you of the cow you had before the disaster. Your stomach rumbles as you remember how good your cow tasted after being hungry for so long. You remove your " .. string.lower(Player.weapon.name) .. "." end, }, PromptNode:new{ text = "Will you kill the bunnicorn?", events = function(choice) if choice == PROMPT_YES then return { CombatNode:new{ opponent = Bunnicorn, events = function(victory) if victory then return { SceneNode:new{ {sprite = 140, x = 88, y = 16, width = 2, height = 2, scale = 4}, }, DialogNode:new{ text = function() return "Your " .. string.lower(Player.weapon.name) .. " sinks into the soft flesh of the bunnicorn. Its screams are not unlike that of a newborn baby. You feel a pang in your chest, but only for a moment. You regain focus as the bunnicorn stops its movements. You look down at the creature and are overcome by your thirst. You dive toward the golden liquid pouring from the creature. You know as you descend that you are losing something important--something human--by doing this, but the dry feeling in your throat is all you can recall as you drink." end, }, DialogNode:new{ text = "You vaguely know you are living after that, but your only impulse is to keep drinking from anything that crosses your path.", }, EndingNode:new(UNENDING_THIRST_OF_THE_HERO), } else return { SceneNode:new{ {sprite = 138, x = 72, y = 0, width = 2, height = 2, scale = 6}, }, DialogNode:new{ text = "You die to the bunnicorn. It scampers away into the trees and lives a long and healthy life. As you die, you remember long hours spent in the fields reading beside your cow. You think that this could be for the best. The grass is soft, and the sun warms your tired body. For a final moment, you feel at peace.", }, EndingNode:new(SOFT_SPRING_LIGHT_SHINES_ON_THE_HERO), } end end, }, } elseif choice == PROMPT_NO then return { SceneNode:new{ {sprite = 136, x = 88, y = 16, width = 2, height = 2, scale = 4}, }, DialogNode:new{ text = function() return "You try to put your " .. string.lower(Player.weapon.name) .. " back into its sheath, but hunger overwhelms you. You fall to the ground and the light that filters through the trees guides you to sleep. In your dreams, you are a hero to a whole village of bunnicorns. They praise you for saving them from evil, and their warmth almost makes you forget the gnawing pain in your stomach." end, }, EndingNode:new(ETERNAL_SLEEP_OF_THE_HERO), } end end, }, } end end, }, } elseif choice == WOODLAND_PATH then return { DialogNode:new{ text = "You decide to venture to the right down a beautiful woodland path. Birds sing in the trees, berries grow, and flowers bloom all across the path. The path is narrow, clearly used more by animals than any travelers, and you do your best not to step on any small magical creatures as you pass. You approach the berries but get distracted by glowing green moss. Your eyes follow the moss to a skeleton with light armour on it. The armour is in excellent condition and clearly was not the undoing of the skeleton.", }, SceneNode:new{ {sprite = 72, x = 0, y = 0, width = 2, height = 2, scale = 6}, {sprite = 76, x = 144, y = 0, width = 2, height = 2, scale = 6}, {sprite = 14, x = 97, y = 30, width = 2, height = 2, scale = 4}, }, PromptNode:new{ text = function() return "Will you exchange your '" .. string.lower(Player.armor.name) .. "' for 'light armour'?" end, events = function(choice) if choice == PROMPT_YES then return { SceneNode:new{ {sprite = 72, x = 0, y = 0, width = 2, height = 2, scale = 6}, {sprite = 76, x = 144, y = 0, width = 2, height = 2, scale = 6}, {sprite = 64, x = 100, y = 30, width = 2, height = 2, scale = 4}, }, DialogNode:new{ text = "You get +5 to your defense. You feel safer.", callback = function() Player.armor = LightArmor end, }, } elseif choice == PROMPT_NO then return { DialogNode:new{ text = "You neither gain nor lose any defense. Your future is in your own hands.", }, } end end, }, blank_tree_scene, DialogNode:new{ text = "You look back at the path and before your eyes rest again on the berries, you see a large red egg in the distance. Either would be a great source of nutrients.", }, PromptNode:new{ text = "Which will you eat?", options = {"Berries", "Egg"}, events = function(choice) local BERRIES = 1 local EGG = 2 if choice == BERRIES then return { SceneNode:new{ {sprite = 72, x = 0, y = 0, width = 2, height = 2, scale = 6}, {sprite = 76, x = 144, y = 0, width = 2, height = 2, scale = 6}, {sprite = 66, x = 88, y = 30, width = 2, height = 2, scale = 4}, }, DialogNode:new{ text = "You crouch down and begin to scarf down as many berries as you can. No birds are eating the berries, but you think nothing of it. The berries are sweet and juicy. The juice runs down your throat and your pain begins to fade. Eventually every sensation joins it. You can no longer move your hands.", }, ConditionalNode:new{ condition = function() return Player.armor == LightArmor end, events = { SceneNode:new{ {sprite = 72, x = 0, y = 0, width = 2, height = 2, scale = 6}, {sprite = 76, x = 144, y = 0, width = 2, height = 2, scale = 6}, {sprite = 64, x = 97, y = 30, width = 2, height = 2, scale = 4}, }, }, }, ConditionalNode:new{ condition = function() return Player.armor ~= LightArmor end, events = { SceneNode:new{ {sprite = 72, x = 0, y = 0, width = 2, height = 2, scale = 6}, {sprite = 76, x = 144, y = 0, width = 2, height = 2, scale = 6}, {sprite = 14, x = 97, y = 30, width = 2, height = 2, scale = 4}, }, }, }, DialogNode:new{ text = "Your eyes dart around desperately as you fall into the bush, but all you can see is the skeleton. Its mouth is agape as if laughing at you. It tried to warn you.", }, EndingNode:new(HUMERUS_COMPANY_OF_THE_HERO), } elseif choice == EGG then return { SceneNode:new{ {sprite = 72, x = 0, y = 0, width = 2, height = 2, scale = 6}, {sprite = 76, x = 144, y = 0, width = 2, height = 2, scale = 6}, {sprite = 8, x = 88, y = 30, width = 2, height = 2, scale = 4}, }, DialogNode:new{ text = function() return "You attempt to approach the egg, but a small goblin scurries out with arms outstretched. It waves frantically while looking up at you with large round eyes. You draw your " .. string.lower(Player.weapon.name) .. " and its huge bat-like ears twitch as it reluctantly removes a " .. string.lower(ShortSword.name) .. " from its belt and stands in your way." end, }, DialogNode:new{ text = "You must fight.", }, CombatNode:new{ opponent = Goblin, events = function(victory) if victory then return { SceneNode:new{ {sprite = 72, x = 0, y = 0, width = 2, height = 2, scale = 6}, {sprite = 76, x = 144, y = 0, width = 2, height = 2, scale = 6}, {sprite = 10, x = 88, y = 52, width = 2, height = 2, scale = 4}, }, DialogNode:new{ text = function() return "You cut through the goblin, and turn your attention to the egg, but not before you consider the " .. string.lower(ShortSword.name) .. " it no longer needs." end, }, PromptNode:new{ text = function() return "Will you take the " .. string.lower(ShortSword.name) .. " with you?" end, events = function(choice) if choice == PROMPT_YES then return { DialogNode:new{ text = "You receive +5 to your attack. You also take a piece of taffy from its pocket. You feel invincible.", callback = function() Player.weapon = ShortSword:new() end, }, } elseif choice == PROMPT_NO then return { DialogNode:new{ text = "You neither gain nor lose any attack. Your future is in your own hands.", }, } end end, }, } else return { SceneNode:new{ {sprite = 72, x = 0, y = 0, width = 2, height = 2, scale = 6}, {sprite = 76, x = 144, y = 0, width = 2, height = 2, scale = 6}, {sprite = 12, x = 88, y = 30, width = 2, height = 2, scale = 4}, }, DialogNode:new{ text = "The goblin cries at your side. It did not want this, but could not risk the consequences of letting you move forward. It stays by your side until you drift off into your final sleep. You dream of tea parties and games with many bat-eared goblins.", }, EndingNode:new(FRIENDS_OF_THE_HERO), } end end, }, blank_tree_scene, DialogNode:new{ text = "You sense an ominous force ahead.", }, ConditionalNode:new{ condition = function() return #Player.inventory > 0 end, events = { PromptNode:new{ text = "Do you want to drink a potion?", events = function(choice) if choice == PROMPT_YES then return { inventory_prompt, } end end, }, }, }, SceneNode:new{ {sprite = 72, x = 0, y = 0, width = 2, height = 2, scale = 6}, {sprite = 76, x = 144, y = 0, width = 2, height = 2, scale = 6}, {sprite = 0, x = 88, y = 30, width = 2, height = 2, scale = 4}, }, DialogNode:new{ text = function() return "The egg flickers in many shades of red as if flames are licking its shell. The temperature increases as you get closer. You sink your " .. string.lower(Player.weapon.name) .. " into the shell. It cracks and bursts, throwing you onto the ground." end, }, SceneNode:new{ {sprite = 72, x = 0, y = 0, width = 2, height = 2, scale = 6}, {sprite = 76, x = 144, y = 0, width = 2, height = 2, scale = 6}, {sprite = 2, x = 88, y = 38, width = 2, height = 2, scale = 4}, }, DialogNode:new{ text = "A pudgy round bird flaps its wings as it drips fire down around it. It is enraged.", }, PromptNode:new{ text = "What will you do?", options = {"Flee", "Fight"}, events = function(choice) local FLEE = 1 local FIGHT = 2 if choice == FLEE then return { SceneNode:new{ {sprite = 72, x = 0, y = 0, width = 2, height = 2, scale = 6}, {sprite = 76, x = 144, y = 0, width = 2, height = 2, scale = 6}, {sprite = 4, x = 88, y = 30, width = 2, height = 2, scale = 4}, {sprite = 4, x = 20, y = 30, width = 2, height = 2, scale = 4}, {sprite = 4, x = 168, y = 30, width = 2, height = 2, scale = 4}, }, DialogNode:new{ text = "You run into the trees, but flames spread around you. The further you run the more your lungs burn. The fire is eating up the oxygen, but you can only continue to run. You slip on a rock and as you look over your shoulder one final time, you see the pudgy bird, vengeance in its eyes, plunge toward you. You are engulfed in flame.", }, EndingNode:new(WARM_EMBRACE_OF_THE_HERO), } elseif choice == FIGHT then return { DialogNode:new{ text = "You get to your feet and brace yourself for a fight with the phoenix.", }, CombatNode:new{ opponent = BabyPhoenix, events = function(victory) if victory then return { SceneNode:new{ {sprite = 72, x = 0, y = 0, width = 2, height = 2, scale = 6}, {sprite = 76, x = 144, y = 0, width = 2, height = 2, scale = 6}, {sprite = 6, x = 88, y = 38, width = 2, height = 2, scale = 4}, }, DialogNode:new{ text = "You are covered in sweat, your skin is burnt and blistered, but you are victorious. You approach the dead phoenix and before it can reignite you begin feasting. The entire thing is gone in minutes. You feel a warmth from your stomach. It covers your entire body. Before you can respond, you are entirely aflame, but you feel no pain.", }, DialogNode:new{ text = "Your wounds are somewhat healed and you are now a creature of fire. You set the area ablaze with every footstep. In the remnants of the nest, you find a full potion as warm and molten as your own blood.", callback = function() table.insert(Player.inventory, FullPotion) end, }, DialogNode:new{ text = function () return "You heal " .. MinorPotion:getHealthRestored() .. " health." end, callback = function() Player:setHealth(Player.health + MinorPotion:getHealthRestored()) end, }, DialogNode:new{ text = "You learn the spell " .. HealingFlame.name .. ".", callback = function() table.insert(Player.spells, HealingFlame:new()) end, }, } else return { SceneNode:new{ {sprite = 72, x = 0, y = 0, width = 2, height = 2, scale = 6}, {sprite = 76, x = 144, y = 0, width = 2, height = 2, scale = 6}, {sprite = 2, x = 88, y = 38, width = 2, height = 2, scale = 4}, {sprite = 4, x = 25, y = 30, width = 2, height = 2, scale = 4}, {sprite = 4, x = 155, y = 30, width = 2, height = 2, scale = 4}, }, DialogNode:new{ text = "You try to escape, but are engulfed in flame. You think of the goblin and its attempt to stop you. You wonder if this is what you deserve.", }, EndingNode:new(FIERY_RETRIBUTION_OF_THE_HERO), } end end, }, } end end, }, } end end, }, blank_tree_scene, DialogNode:new{ text = "You continue on until you reach the end of the path. With a full stomach and a new spark for life, you decide not to venture back down the winding path you missed. Instead you trek forward to a land of silvery trees. Strange creatures with a mixture of human and animal bodies and insect wings flutter about no bigger than a bird. You swat them away.", }, } elseif choice == WINDING_PATH then return { blank_path_scene, DialogNode:new{ text = "Despite your misgivings you venture down the winding center path. Thorns scratch at your ankles, rocks attempt to trip you, and dust fills your lungs. Many times you consider turning back, but you move forward anyway.", }, SceneNode:new{ {sprite = 260, x = 0, y = 0, width = 1, height = 2, scale = 6}, {sprite = 260, x = 192, y = 0, width = 1, height = 2, scale = 6, flip = 1}, {sprite = 262, x = 62, y = 0, width = 4, height = 3, scale = 4}, }, DialogNode:new{ text = "After what feels like hours, you come across a man shrouded in shadow. His eyes flicker like blue flames and he appears skeletal. He stands tall and broad with heavy armour and a naginata. Before you can attempt to attack him, he speaks. \"Halt. I tire of endless fighting. Please, take my naginata and free me from a lifetime of violence\".", }, PromptNode:new{ text = "Will you take the naginata?", events = function(choice) if choice == PROMPT_YES then return { SceneNode:new{ {sprite = 260, x = 0, y = 0, width = 1, height = 2, scale = 6}, {sprite = 260, x = 192, y = 0, width = 1, height = 2, scale = 6, flip = 1}, {sprite = 388, x = 62, y = 0, width = 4, height = 3, scale = 4}, }, DialogNode:new{ text = "You get +10 to your attack. You feel the temptation of power.", callback = function() Player.weapon = Naginata:new() end, }, DialogNode:new{ text = "You consider the man's heavy armour. One power deserves another.", }, PromptNode:new{ text = "Will you take the man's heavy armour?", events = function(choice) if choice == PROMPT_YES then return{ DialogNode:new{ text = "You reach for the heavy armour, but the man sighs. \"If you insist on taking more than has been offered, I will defend myself.\"", }, CombatNode:new{ opponent = LichWarrior, events = function(victory) if victory then return{ SceneNode:new{ {sprite = 260, x = 0, y = 0, width = 1, height = 2, scale = 6}, {sprite = 260, x = 192, y = 0, width = 1, height = 2, scale = 6, flip = 1}, {sprite = 384, x = 62, y = 0, width = 4, height = 3, scale = 4}, }, DialogNode:new{ text = "You slice through the man's skull and the light leaves his eyes. You are exhausted, but you take your prize all the same.", }, DialogNode:new{ text = "You get +10 to your defense. You feel better defended, but less safe.", callback = function() Player.armor = HeavyArmor end, }, DialogNode:new{ text = "You are no longer the person you used to be. Your skin hangs looser from your frame, your eyes burn relentlessly, and the weight of ten thousand souls bears down on you.", }, DialogNode:new{ text = function () return "You heal " .. MinorPotion:getHealthRestored() .. " health." end, callback = function() Player:setHealth(Player.health + MinorPotion:getHealthRestored()) end, }, DialogNode:new{ text = "You learn the spell " .. LeechLife.name .. ".", callback = function() table.insert(Player.spells, LeechLife:new()) end, }, blank_path_scene, DialogNode:new{ text = "You travel to the end of the winding path and consider the choices that brought you here.", }, ConditionalNode:new{ condition = function() return #Player.inventory > 0 end, events = { PromptNode:new{ text = "Do you want to drink a potion?", events = function(choice) if choice == PROMPT_YES then return { inventory_prompt, } end end, }, }, }, } else return{ DialogNode:new{ text = "\"You have cut through this land violently and tried to claim what was not yours. I hope you are prepared for what awaits you.\" You drift into an eternal sleep, but you are not alone. The spirits of those cut down by the naginata pull at your soul. You know no peace.", }, EndingNode:new(ETERNAL_UNREST_OF_THE_HERO), } end end, }, } elseif choice == PROMPT_NO then return{ DialogNode:new{ text = "You gratefully take the naginata and journey to the end of the path. You shoulder the burdens of his past, ten thousand souls slain, but feel lighter for having salvaged what you could of your soul.", }, } end end, }, } elseif choice == PROMPT_NO then return { DialogNode:new{ text = "You neither gain nor lose attack. You look regretfully at the man, but walk past him all the same. You journey to the end of the winding path without the burden of a lifetime of violence. The path has worn you down, but your spirit prevails.", }, } end end, }, blank_tree_scene, DialogNode:new{ text = "You reach where the winding path and the woodland path meet, but after experiencing the hardship and dangers of the winding path, you feel you could not disturb the peace of the woodland path and be the same person anymore. You decide to move forward toward whatever destiny awaits you in the land of silver trees and faeries.", }, } end end, }, -- silver trees blank_tree_scene, DialogNode:new{ text = "You journey along the path lined with silver leaves into the overgrown home of the fae. Some cower at the sight of an intruder, while others approach you, buzzing around with open curiosity. You cannot decide if you are charmed or repulsed.", }, DialogNode:new{ text = "The trees are so dense that you cannot see the sky. You lose all sense of time and are guided only by the light of the vegetation.", }, SceneNode:new{ {sprite = 72, x = 0, y = 0, width = 2, height = 2, scale = 6}, {sprite = 76, x = 144, y = 0, width = 2, height = 2, scale = 6}, {sprite = 256, x = 72, y = 0, width = 4, height = 4, scale = 3}, }, DialogNode:new{ text = "After some time you reach an ancient oak tree. Every part of the forest feels as if it has grown solely to surround this single point. At the base of the tree, the roots gnarl and twist into an archway. You venture inside.", }, SceneNode:new{ {sprite = 72, x = 0, y = 0, width = 2, height = 2, scale = 6}, {sprite = 76, x = 144, y = 0, width = 2, height = 2, scale = 6}, {sprite = Oberon.sprite, x = 72, y = 0, width = 4, height = 4, scale = 3}, }, DialogNode:new{ text = "Ancient as the sky, a creature that is both faerie and tree twists and shambles toward you as if moved by an unseen puppeteer. A crown of gold and thorns twists through the hardened leather of its head. The wind whispers the name 'Oberon'. The faerie king.", }, DialogNode:new{ text = function() return "Before you can try to escape, " .. Oberon.name .. " strikes you." end, }, CombatNode:new{ opponent = Oberon, player_goes_first = false, events = function(victory) if victory then return { SceneNode:new{ {sprite = 72, x = 0, y = 0, width = 2, height = 2, scale = 6}, {sprite = 76, x = 144, y = 0, width = 2, height = 2, scale = 6}, {sprite = 324, x = 88, y = 16, width = 2, height = 2, scale = 4}, }, DialogNode:new{ text = function() return "You have struggled against fate and finally your " .. string.lower(Player.weapon.name) .. " cuts open a hole in " .. Oberon.name .. "'s chest. Instead of organs, a light radiates from the cavity. You reach inside and pull from his chest the " .. HolySwordOfLight.name .. "." end, callback = function() Player.weapon = HolySwordOfLight end, }, DialogNode:new{ text = "As you extract it, the glowing light of the silver trees is snuffed out and a thrumming of wing beats--as if every flying creature in the forest took off at once--echoes through the oak tree.", }, blank_tree_scene, DialogNode:new{ text = "You learn the spell " .. RootOfEvil.name .. ".", callback = function() table.insert(Player.spells, RootOfEvil:new()) end, }, } else return { DialogNode:new{ text = function() return "You gave it your all, but, despite your efforts, you were no match for the king of faeries. As your breath slows, you feel " .. Oberon.name .. " collect your body. You are melded into the bark of his body and become one of many protective layers. Suddenly, you can hear every footstep in the forest, and every wing beat of the fae. Their joy is your joy and you begin to forget why you ever wanted anything else." end, }, EndingNode:new(IMMORTAL_JOY_OF_THE_HERO), } end end, }, SceneNode:new{ {sprite = 202, x = 0, y = 0, width = 2, height = 2, scale = 9}, }, DialogNode:new{ text = function() return "A rumbling beneath your feet demands your attention. The floor opens up to stairs that wind further into darkness and the " .. HolySwordOfLight.name .. " guides you inside and illuminates the path ahead." end, }, PromptNode:new{ text = "Will you press your hand along the wall as you go down?", events = function(choice) if choice == PROMPT_YES then return { SceneNode:new{ {sprite = 198, x = 88, y = 16, width = 2, height = 2, scale = 4}, }, DialogNode:new{ text = "You press against the wall and accidentally engage a switch that opens up a secret passage.", }, SceneNode:new{ {sprite = 204, x = 80, y = 0, width = 4, height = 4, scale = 3}, }, DialogNode:new{ text = "You tumble inside and discover an abandoned hideout. After looking around briefly, you find a full potion.", callback = function() table.insert(Player.inventory, FullPotion) end, }, SceneNode:new{ {sprite = 196, x = 98, y = 20, width = 2, height = 2, scale = 4}, }, DialogNode:new{ text = "It's a few hundred years past expiration, but you hope it's only a suggestion.", }, SceneNode:new{ {sprite = 202, x = 0, y = 0, width = 2, height = 2, scale = 9}, }, DialogNode:new{ text = "You exit and continue down the stairs to the end of the path.", }, } end end, }, DialogNode:new{ text = "You reach the bottom of the stairs and stagger through to the very end of the path.", }, SceneNode:new{ {sprite = 392, x = 48, y = 0, width = 6, height = 4, scale = 3}, }, DialogNode:new{ text = function() return "The end of the path opens up to a cavern with stalactites that glow and fill the room with a soft blue light. There is an island at the center of the cavern that consumes the glowing light and leaves only shadows. You hop along the stone path to reach it and hold the " .. HolySwordOfLight.name .. " in the air." end, }, SceneNode:new{ {sprite = ShadowStag.sprite, x = 72, y = 0, width = 4, height = 4, scale = 3}, }, DialogNode:new{ text = function() return "The light of the sword is consumed by the shadow and from it steps the " .. ShadowStag.name .. ". Beautiful and terrifying, this creature towers over you, but you can tell this is the final fight of your journey." end, }, DialogNode:new{ text = "After this, you tell yourself, you can finally rest.", }, PromptNode:new{ text = "Will you fight?", events = function(choice) if choice == PROMPT_YES then return { CombatNode:new{ opponent = ShadowStag, events = function(victory) if victory then return { SceneNode:new{ {sprite = 266, x = 88, y = 16, width = 2, height = 2, scale = 4}, }, DialogNode:new{ text = function() return "You plunge the " .. HolySwordOfLight.name .. " into the " .. ShadowStag.name .. "'s chest. The " .. HolySwordOfLight.name .. " begins to split in two and is joined by the " .. SacredSwordOfShadow.name .. ". You grab the " .. SacredSwordOfShadow.name .. " and hear the stones shifting. You look around and see a new opening on the far side of the cavern. The ground is now shaking violently and the stalactites begin to fall. You sprint toward the opening and make your way through just as the cavern collapses. Outside you see the sky again for the first time in what feels like days, but was longer than you will ever know." end, }, } else return final_resting_place_of_the_hero_ending end end, }, } elseif choice == PROMPT_NO then return final_resting_place_of_the_hero_ending end end, }, SceneNode:new{ {sprite = 519, x = 8, y = 0, width = 7, height = 2, scale = 4}, }, DialogNode:new{ text = "Stars dance in the sky, the wind blows past you, and what remains of the magical forest is behind you.", }, DialogNode:new{ text = "You can see the nations of humans in the distance. You start making your way toward them with both swords. You are no longer sure if you have a place there, but there is no turning back.", }, DialogNode:new{ text = "The forest behind you is dead, and the future is unclear, but you are no longer trapped by circumstance. You are free to carve your own future.", }, DialogNode:new{ text = "Will you protect humanity or remake the world to fit what you've become?", }, EndingNode:new(UNDECIDED_FATE_OF_THE_HERO), }, } end