Created
May 10, 2014 19:45
-
-
Save 8bitgentleman/12797ff5b8d76121a47c to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
local anim8 = require 'vendor/anim8' | |
local app = require 'app' | |
local Dialog = require 'dialog' | |
local window = require "window" | |
local sound = require 'vendor/TEsound' | |
local fonts = require 'fonts' | |
local utils = require 'utils' | |
local Timer = require 'vendor/timer' | |
local Menu = {} | |
Menu.__index = Menu | |
function Menu.new(items, responses, commands, background, tick, npc, menuColor) | |
local menu = {} | |
setmetatable(menu, Menu) | |
menu.responses = responses | |
menu.commands = commands | |
menu.rootItems = items | |
menu.items = items | |
menu.itemWidth = 150 | |
menu.choice = 1 | |
menu.offset = 0 | |
menu.background = background | |
menu.tick = tick | |
menu.host = npc | |
menu.color = menuColor | |
local h = anim8.newGrid(69, 43, background:getWidth(), background:getHeight()) | |
menu.animation = anim8.newAnimation('once', h('1-6,1'), .08) | |
menu.state = 'closed' | |
return menu | |
end | |
function Menu:keypressed( button, player ) | |
if self.state == 'closing' then return end | |
if self.dialog and (self.state == 'closed' or self.state == 'hidden') | |
and button == 'JUMP' then | |
return self.dialog:keypressed( button, player ) | |
end | |
if self.state == 'closed' or self.state == 'hidden' then | |
return false | |
end | |
if button == 'UP' then | |
sound.playSfx( 'click' ) | |
if self.choice == 4 then | |
self.offset = math.min(self.offset + 1, #self.items - 4) | |
end | |
self.choice = math.min(4, self.choice + 1) | |
elseif button == 'DOWN' then | |
sound.playSfx( 'click' ) | |
if self.choice == 1 then | |
self.offset = math.max(self.offset - 1, 0) | |
end | |
self.choice = math.max(1, self.choice - 1) | |
elseif button == 'JUMP' then | |
sound.playSfx( 'click' ) | |
local item = self.items[self.choice + self.offset] | |
if self.commands then | |
if self.commands[item.text] then | |
self.commands[item.text](self.host, player) | |
end | |
if self.commands[item.text] and not self.responses[item.text] then | |
self:close(player) | |
end | |
end | |
if item == nil or item.text == 'exit' then | |
self:close(player) | |
elseif item.text == 'i am done with you' or item.text == 'back' then | |
self.items = self.rootItems | |
self:resetSelection() | |
elseif item.text == 'inventory' then | |
if self.host.props.inventory then | |
self:instahide() | |
self.host.props.inventory(self.host, player) | |
self.dialog = Dialog.new(self.responses[item.text], function() self:show() end) | |
else | |
self:hide() | |
self.dialog = Dialog.new(self.host.noinventory, function() self:show() end) | |
end | |
elseif item.text == 'command' then | |
if self.host.props.command_items then | |
-- for commands in list | |
-- if affection levels are greater than required | |
-- add command to options | |
self.items = item.option | |
else | |
self:hide() | |
self.dialog = Dialog.new(self.host.nocommands, function() self:show() end) | |
end | |
elseif self.responses[item.text] then | |
self:hide() | |
if item.option then | |
self.items = item.option | |
self:resetSelection() | |
end | |
self.dialog = Dialog.new(self.responses[item.text], function() self:show() end) | |
elseif type(item.option) == 'table' then | |
self.items = item.option | |
end | |
elseif button == 'INTERACT' then | |
self:close(player) | |
elseif button == 'ATTACK' then | |
if self.items == self.rootItems then | |
self:close(player) | |
else | |
self.items = self.rootItems | |
self:resetSelection() | |
end | |
elseif button == 'START' then | |
self:close(player) | |
end | |
return true | |
end | |
function Menu:resetSelection() | |
self.choice = 4 | |
self.offset = 0 | |
end | |
function Menu:update(dt) | |
if self.state == 'closed' or self.state == 'hidden' then | |
if self.dialog then self.dialog:update(dt) end | |
return | |
end | |
if self.state == 'hiding' and self.animation.position == 1 then | |
self.state = 'hidden' | |
end | |
if self.state == 'closing' and self.animation.position == 1 then | |
self.state = 'closed' | |
end | |
self.animation:update(dt) | |
end | |
function Menu:draw(x, y) | |
fonts.set('arial') | |
if self.state == 'closed' or self.state == 'hidden' then | |
if self.dialog then self.dialog:draw() end | |
return | |
end | |
self.animation:draw(self.background, x + 3, y + 4) | |
if self.state == 'opening' and self.animation.position >= 5 then | |
self.state = 'opened' | |
end | |
if self.state ~= 'opened' then | |
return | |
end | |
love.graphics.setColor( self.color.r, self.color.g, self.color.b, self.color.a ) | |
Font = love.graphics.getFont() | |
y = y + 36 | |
for i, value in ipairs(self.items) do | |
i = i - self.offset | |
if i > 0 then | |
love.graphics.printf(value.text, x - self.itemWidth, y - (i - 1) * 12, | |
self.itemWidth, 'right') | |
if self.choice == i then | |
-- pointer | |
love.graphics.setColor( 255, 255, 255, 255 ) | |
love.graphics.draw(self.tick, x - (Font:getWidth(value.text)+10), y - (i - 1) * 12 + 2) | |
love.graphics.setColor( self.color.r, self.color.g, self.color.b, self.color.a ) | |
end | |
end | |
end | |
love.graphics.setColor( 255, 255, 255, 255 ) | |
fonts.revert() | |
end | |
function Menu:open() | |
self.items = self.rootItems | |
self.choice = 4 | |
self.offset = 0 | |
self:show() | |
end | |
function Menu:show() | |
self.state = 'opening' | |
self.animation.direction = 1 | |
self.animation:gotoFrame(1) | |
end | |
function Menu:hide() | |
self.animation:resume() | |
self.animation.direction = -1 | |
self.state = 'hiding' | |
end | |
function Menu:instahide() | |
self.animation:resume() | |
self.animation.direction = -1 | |
self.animation.position = 1 | |
self.state = 'hidden' | |
end | |
function Menu:close(player) | |
player.freeze = false | |
if self.host.finish then self.host.finish(self.host, player) end | |
self.animation:resume() | |
self.animation.direction = -1 | |
self.state = 'closing' | |
end | |
----------------------------------------------- | |
-- NPC.lua | |
----------------------------------------------- | |
local NPC = {} | |
NPC.__index = NPC | |
-- Nodes with 'isInteractive' are nodes which the player can interact with, but not pick up in any way | |
NPC.isInteractive = true | |
--- | |
-- Creates a new NPC object | |
-- @param node the table used to create this | |
-- @param a collider of objects | |
-- @return the NPC object created | |
function NPC.new(node, collider) | |
local p = node.properties | |
--creates a new object | |
local npc = {} | |
--sets it to use the functions and variables defined in NPC | |
-- if it doesn;t have one by that name | |
setmetatable(npc, NPC) | |
--stores all the parameters from the tmx file | |
npc.node = node | |
npc.foreground = p.foreground == 'true' | |
--stores parameters from a lua file | |
npc.props = require('npcs/' .. node.name) | |
npc.name = node.name | |
npc.busy = false | |
--sets the position from the tmx file | |
npc.position = {x = node.x, y = node.y} | |
npc.width = npc.props.width | |
npc.height = npc.props.height | |
--initialize the node's bounding box | |
npc.collider = collider | |
npc.bb = collider:addRectangle(0,0,(npc.props.bb_width or npc.props.width),(npc.props.bb_height or npc.props.height)) | |
npc.bb.node = npc | |
npc.collider:setPassive(npc.bb) | |
--define some offsets for the bounding box that can be used each update cycle | |
npc.bb_offset = {x = npc.props.bb_offset_x or 0, | |
y = npc.props.bb_offset_y or 0} | |
-- Ensures bb is in correct position | |
npc:update_bb() | |
-- deals with npc walking | |
npc.walking = npc.props.walking or false | |
npc.minx = node.x - (npc.props.max_walk or 48) | |
npc.maxx = node.x + (npc.props.max_walk or 48) | |
npc.walk_speed = npc.props.walk_speed or 18 | |
npc.wasWalking = false | |
npc.run_speed = npc.props.run_speed or 100 | |
-- deals with staring | |
npc.stare = npc.props.stare or false | |
npc.donotfacewhentalking = npc.props.donotfacewhentalking or false | |
--add more initialization code here if you want | |
npc.controls = nil | |
-- code to run before and after converstation, can be used for talking sprites | |
npc.begin = npc.props.begin | |
npc.finish = npc.props.finish | |
npc.state = 'default' | |
npc.direction = npc.props.direction or 'right' | |
-- optional replies to no iventory or commands | |
npc.noinventory = npc.props.noinventory or 'I do not have anything to sell you.' | |
npc.nocommands = npc.props.nocommands or 'I do not take commands from the likes of you.' | |
-- deals with the image of the npc | |
local npcImage = love.graphics.newImage('images/npc/'..node.name..'.png') | |
local g = anim8.newGrid(npc.props.width, npc.props.height, npcImage:getWidth(), npcImage:getHeight()) | |
npc.image = npcImage | |
npc.animations = {} | |
for state, data in pairs( npc.props.animations ) do | |
npc.animations[ state ] = anim8.newAnimation( data[1], g( unpack(data[2]) ), data[3]) | |
end | |
npc.lastSoundUpdate = math.huge | |
-- makes the menu | |
menuColor = npc.props.menuColor or {r=0, g=0, b=0, a=255} | |
newMenuItems = { | |
{ ['text']='exit' }, | |
{ ['text']='inventory' }, | |
{ ['text']='command', ['option']=(npc.props.command_items or {})}, | |
{ ['text']='talk', ['option']=npc.props.talk_items} | |
} | |
npc.affectionText = {x=0, y=0} | |
npc.affectionVel = {x=0, y=0} | |
npc.giveAffection = 0 | |
npc.affectionAmount = 1 | |
npc.levels = require 'npclevels' | |
if npc.levels[npc.name] then | |
npc.affection = npc.levels[npc.name][1] or 0 | |
npc.respect = npc.levels[npc.name][2] or 0 | |
npc.trust = npc.levels[npc.name][3] or 0 | |
npc.married = npc.levels[npc.name][4] or false | |
end | |
npc.db = app.gamesaves:active() | |
npc.dead = false | |
-- a special item is an item in the level that the player can steal or the npc reacts to the player having | |
npc.special_items = npc.props.special_items or {} | |
-- store the original position, used in running | |
npc.original_pos = {x=npc.position.x, y=npc.position.y} | |
-- the offset points for an npc to run towards | |
npc.run_offsets = npc.props.run_offsets or {} | |
npc.run_offsets_index = 1 | |
-- Used when the npc has been insulted (i.e something was stolen) | |
npc.angry = false | |
newCommands = npc.props.talk_commands or {} | |
command_commands = npc.props.command_commands or {} | |
for k,v in pairs(command_commands) do newCommands[k] = v end | |
npc.menu = Menu.new(newMenuItems, | |
npc.props.talk_responses, | |
newCommands, | |
npc.props.menuImage or love.graphics.newImage('images/npc/'..node.name..'_menu.png'), | |
npc.props.tickImage or love.graphics.newImage('images/menu/selector.png'), | |
npc, | |
menuColor) | |
return npc | |
end | |
function NPC:enter( previous ) | |
if self.props.enter then self.props.enter(self, previous) end | |
end | |
--- | |
-- Draws the NPC to the screen | |
-- @return nil | |
function NPC:draw() | |
local anim = self:animation() | |
anim:draw(self.image, self.position.x + (self.direction=="left" and self.width or 0), self.position.y, 0, (self.direction=="left") and -1 or 1, 1) | |
self.menu:draw(self.position.x, self.position.y - 50) | |
-- get rid of these | |
--love.graphics.print(self.affection, self.position.x + 80, 50) | |
--love.graphics.print(self.respect, self.position.x + 80, 70) | |
--love.graphics.print(self.trust, self.position.x + 80, 90) | |
if self.giveAffection > 0 then | |
love.graphics.setColor( 0, 0, 255, 255 ) | |
love.graphics.print("+ " .. affection, self.affectionText.x, self.affectionText.y, 0, 0.7, 0.7) | |
love.graphics.setColor(255,255,255,255) | |
Timer.add(.45, function() | |
self.giveAffection = 0 | |
end) | |
end | |
end | |
function NPC:keypressed( button, player ) | |
if self.dead or self.angry then return end | |
if button == 'INTERACT' and self.menu.state == 'closed' and not player.jumping and not player.isClimbing and not self.busy then | |
player.freeze = true | |
player.character.state = 'idle' | |
self.state = 'default' | |
self.orig_direction = self.direction | |
if self.donotfacewhentalking then | |
elseif player.position.x < self.position.x then | |
self.direction = "left" | |
else | |
self.direction = "right" | |
end | |
self.menu:open(player) | |
if self.begin then self.begin(self, player) end | |
else | |
return self.menu:keypressed(button, player) | |
end | |
end | |
--- | |
-- Called when the NPC begins colliding with another node | |
-- @param node the node you're colliding with | |
-- @param dt deltatime | |
-- @param mtv_x amount the node must be moved in the x direction to stop colliding | |
-- @param mtv_y amount the node must be moved in the y direction to stop colliding | |
-- @return nil | |
function NPC:collide(node, dt, mtv_x, mtv_y) | |
if node.isPlayer and self.stare and self.walking then | |
self.wasWalking = true | |
self.walking = false | |
end | |
if self.props.collide then self.props.collide(self, node, dt, mtv_x, mtv_y) end | |
end | |
function NPC:hurt(damage, special_damage, knockback) | |
if self.props.hurt then | |
self.props.hurt(self, special_damage, knockback) | |
end | |
end | |
function NPC:animation() | |
return self.animations[self.state] | |
end | |
--- | |
-- Called when the NPC finishes colliding with another node | |
-- @return nil | |
function NPC:collide_end(node, dt) | |
if node.isPlayer and self.stare and self.wasWalking then | |
self.wasWalking = false | |
self.walking = true | |
end | |
end | |
--- | |
-- Updates the NPC | |
-- dt is the amount of time in seconds since the last update | |
function NPC:update(dt, player) | |
if self.menu.state ~= "closed" then self.menu:update(dt)end | |
self:animation():update(dt) | |
self:handleSounds(dt) | |
if self.menu.state == "closing" then | |
self.direction = self.orig_direction | |
end | |
-- The npc is dead and can no longer interact | |
if self.dead then return end | |
if self.walking and self.menu.state == "closed" then self.state = 'walking' end | |
if self.state == 'walking' and not self.walking then self.state = 'default' end | |
if self.state == 'walking' then self:walk(dt) end | |
if self.stare then | |
if player.position.x < self.position.x then | |
self.direction = "left" | |
else | |
self.direction = "right" | |
end | |
end | |
self:checkInventory(player) | |
if self.props.update then | |
self.props.update(dt, self, player) | |
end | |
if self.giveAffection > 0 then | |
self.affectionText.x = self.position.x + self.width / 2 | |
self.affectionText.y = self.affectionText.y + self.affectionVel.y * dt | |
self.affectionVel.y = -35 | |
else | |
self.affectionText.y = self.position.y | |
end | |
-- Moves the bb with the npc | |
self:update_bb() | |
end | |
function NPC:affection() | |
--local level = self.levels | |
--npc.levels = require 'npclevels' | |
if npc.levels[npc.name] then | |
local affection = npc.levels[npc.name][1] or 0 | |
end | |
for i = 1,self.affectionAmount do | |
if npc.affectionAmount > 1 then | |
self.giveAffection = 1 | |
self.affection = self.affection + i | |
self.affectionAmount = 1 | |
end | |
end | |
end | |
function NPC:update_bb() | |
local x1,y1,x2,y2 = self.bb:bbox() | |
self.bb:moveTo( self.position.x + (x2-x1)/2 + self.bb_offset.x, | |
self.position.y + (y2-y1)/2 + self.bb_offset.y ) | |
end | |
function NPC:walk(dt) | |
if self.minx == self.maxx then | |
elseif self.position.x > self.maxx then | |
self.direction = 'left' | |
elseif self.position.x < self.minx then | |
self.direction = 'right' | |
end | |
local direction = self.direction == 'right' and 1 or -1 | |
self.position.x = self.position.x + self.walk_speed * dt * direction | |
end | |
-- Follows the path defined in self.props.run_offsets | |
function NPC:run(dt, player) | |
-- If npc is within 5px of target it's time to move to the next one | |
if math.abs(self.position.x - self.run_offsets[self.run_offsets_index].x - self.original_pos.x) < self.run_speed * dt and | |
math.abs(self.position.y - self.run_offsets[self.run_offsets_index].y - self.original_pos.y) < self.run_speed * dt / 2 then | |
self.run_offsets_index = self.run_offsets_index + 1 | |
end | |
-- If the end of the target points is reached loop between the last two points | |
if self.run_offsets_index > #self.run_offsets then | |
self.run_offsets_index = self.run_offsets_index - 2 | |
end | |
local target_pos = self.run_offsets[self.run_offsets_index] | |
-- Direction of x movement | |
local direction_x = 0 | |
-- Determine which x direction to move in | |
-- Checks position within one frame of movement | |
if self.position.x < target_pos.x + self.original_pos.x - self.run_speed * dt then | |
direction_x = 1 | |
-- Only switch direction if necessary | |
if self.direction == 'left' then | |
self.direction = 'right' | |
end | |
elseif self.position.x > target_pos.x + self.original_pos.x + self.run_speed * dt then | |
direction_x = -1 | |
-- Only switch direction if necessary | |
if self.direction == 'right' then | |
self.direction = 'left' | |
end | |
end | |
-- Direction of y movement | |
local direction_y = 0 | |
-- Determine which y direction to move in | |
-- Checks position within one frame of movement | |
if self.position.y > target_pos.y + self.original_pos.y + self.run_speed * dt / 2 then | |
direction_y = -1 | |
elseif self.position.y < target_pos.y + self.original_pos.y - self.run_speed * dt / 2 then | |
direction_y = 1 | |
end | |
self.position.x = self.position.x + self.run_speed * dt * direction_x | |
self.position.y = self.position.y + self.run_speed * dt * direction_y / 2 | |
end | |
-- Checks for certain items in the players inventory | |
function NPC:checkInventory(player) | |
for _, special_item in ipairs(self.special_items) do | |
local Item = require('items/item') | |
local itemNode = utils.require ('items/weapons/'..special_item) | |
local item = Item.new(itemNode, 1) | |
if player.inventory:search(item) then | |
-- npc reaction to finding a special item | |
if self.props.item_found then | |
self.props.item_found(self, player) | |
end | |
end | |
end | |
end | |
function NPC:handleSounds(dt) | |
self.lastSoundUpdate = self.lastSoundUpdate + dt | |
for _,v in pairs((self.props.sounds or {})) do | |
if self.state==v.state and self:animation().position==v.position and self.lastSoundUpdate > 0.5 then | |
sound.playSfx(v.file) | |
self.lastSoundUpdate = 0 | |
end | |
end | |
end | |
return NPC |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment