├── .gitignore ├── pacpac.love ├── pacpac ├── audio │ ├── bwop.ogg │ ├── death.ogg │ ├── open.ogg │ ├── runny.ogg │ ├── weeoo.ogg │ ├── nomnom.ogg │ ├── thunder.ogg │ ├── weeoo1.ogg │ ├── weeoo2.ogg │ ├── weeoo3.ogg │ ├── weeoo4.ogg │ ├── weeoo5.ogg │ ├── weeoo6.ogg │ ├── notes │ │ ├── c1.ogg │ │ ├── c2.ogg │ │ ├── c3.ogg │ │ ├── c4.ogg │ │ ├── d3.ogg │ │ ├── e2-.ogg │ │ ├── e3.ogg │ │ ├── g1.ogg │ │ └── g2.ogg │ └── watawata.ogg ├── img │ ├── gamepad.png │ ├── arrow_keys.png │ ├── pacpac_logo.png │ └── gamepad_overlay.png ├── 8bitoperator_jve.ttf ├── graphics.lua ├── conf.lua ├── draw.lua ├── level3.txt ├── level2.txt ├── level1.txt ├── util.lua ├── levelreader.lua ├── notes.lua ├── events.lua ├── Character.lua └── main.lua ├── screenshots ├── title.png ├── level1_2.png └── old_screenshot.png ├── make_love_file.sh └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /pacpac.love: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tylerneylon/pacpac/HEAD/pacpac.love -------------------------------------------------------------------------------- /pacpac/audio/bwop.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tylerneylon/pacpac/HEAD/pacpac/audio/bwop.ogg -------------------------------------------------------------------------------- /pacpac/audio/death.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tylerneylon/pacpac/HEAD/pacpac/audio/death.ogg -------------------------------------------------------------------------------- /pacpac/audio/open.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tylerneylon/pacpac/HEAD/pacpac/audio/open.ogg -------------------------------------------------------------------------------- /pacpac/audio/runny.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tylerneylon/pacpac/HEAD/pacpac/audio/runny.ogg -------------------------------------------------------------------------------- /pacpac/audio/weeoo.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tylerneylon/pacpac/HEAD/pacpac/audio/weeoo.ogg -------------------------------------------------------------------------------- /pacpac/img/gamepad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tylerneylon/pacpac/HEAD/pacpac/img/gamepad.png -------------------------------------------------------------------------------- /screenshots/title.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tylerneylon/pacpac/HEAD/screenshots/title.png -------------------------------------------------------------------------------- /pacpac/audio/nomnom.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tylerneylon/pacpac/HEAD/pacpac/audio/nomnom.ogg -------------------------------------------------------------------------------- /pacpac/audio/thunder.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tylerneylon/pacpac/HEAD/pacpac/audio/thunder.ogg -------------------------------------------------------------------------------- /pacpac/audio/weeoo1.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tylerneylon/pacpac/HEAD/pacpac/audio/weeoo1.ogg -------------------------------------------------------------------------------- /pacpac/audio/weeoo2.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tylerneylon/pacpac/HEAD/pacpac/audio/weeoo2.ogg -------------------------------------------------------------------------------- /pacpac/audio/weeoo3.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tylerneylon/pacpac/HEAD/pacpac/audio/weeoo3.ogg -------------------------------------------------------------------------------- /pacpac/audio/weeoo4.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tylerneylon/pacpac/HEAD/pacpac/audio/weeoo4.ogg -------------------------------------------------------------------------------- /pacpac/audio/weeoo5.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tylerneylon/pacpac/HEAD/pacpac/audio/weeoo5.ogg -------------------------------------------------------------------------------- /pacpac/audio/weeoo6.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tylerneylon/pacpac/HEAD/pacpac/audio/weeoo6.ogg -------------------------------------------------------------------------------- /screenshots/level1_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tylerneylon/pacpac/HEAD/screenshots/level1_2.png -------------------------------------------------------------------------------- /pacpac/8bitoperator_jve.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tylerneylon/pacpac/HEAD/pacpac/8bitoperator_jve.ttf -------------------------------------------------------------------------------- /pacpac/audio/notes/c1.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tylerneylon/pacpac/HEAD/pacpac/audio/notes/c1.ogg -------------------------------------------------------------------------------- /pacpac/audio/notes/c2.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tylerneylon/pacpac/HEAD/pacpac/audio/notes/c2.ogg -------------------------------------------------------------------------------- /pacpac/audio/notes/c3.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tylerneylon/pacpac/HEAD/pacpac/audio/notes/c3.ogg -------------------------------------------------------------------------------- /pacpac/audio/notes/c4.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tylerneylon/pacpac/HEAD/pacpac/audio/notes/c4.ogg -------------------------------------------------------------------------------- /pacpac/audio/notes/d3.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tylerneylon/pacpac/HEAD/pacpac/audio/notes/d3.ogg -------------------------------------------------------------------------------- /pacpac/audio/notes/e2-.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tylerneylon/pacpac/HEAD/pacpac/audio/notes/e2-.ogg -------------------------------------------------------------------------------- /pacpac/audio/notes/e3.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tylerneylon/pacpac/HEAD/pacpac/audio/notes/e3.ogg -------------------------------------------------------------------------------- /pacpac/audio/notes/g1.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tylerneylon/pacpac/HEAD/pacpac/audio/notes/g1.ogg -------------------------------------------------------------------------------- /pacpac/audio/notes/g2.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tylerneylon/pacpac/HEAD/pacpac/audio/notes/g2.ogg -------------------------------------------------------------------------------- /pacpac/audio/watawata.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tylerneylon/pacpac/HEAD/pacpac/audio/watawata.ogg -------------------------------------------------------------------------------- /pacpac/img/arrow_keys.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tylerneylon/pacpac/HEAD/pacpac/img/arrow_keys.png -------------------------------------------------------------------------------- /pacpac/img/pacpac_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tylerneylon/pacpac/HEAD/pacpac/img/pacpac_logo.png -------------------------------------------------------------------------------- /pacpac/img/gamepad_overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tylerneylon/pacpac/HEAD/pacpac/img/gamepad_overlay.png -------------------------------------------------------------------------------- /screenshots/old_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tylerneylon/pacpac/HEAD/screenshots/old_screenshot.png -------------------------------------------------------------------------------- /make_love_file.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Hehe. 4 | # 5 | 6 | cd "$( dirname "${BASH_SOURCE[0]}" )" 7 | cd pacpac 8 | zip -r ../pacpac.love * 9 | -------------------------------------------------------------------------------- /pacpac/graphics.lua: -------------------------------------------------------------------------------- 1 | -- graphics.lua 2 | -- 3 | -- Encapsulate love.graphics drawing functions so we can apply 4 | -- universal transformations easily. 5 | -------------------------------------------------------------------------------- /pacpac/conf.lua: -------------------------------------------------------------------------------- 1 | -- conf.lua 2 | -- 3 | -- Basic game configuration and info. 4 | -- 5 | 6 | function love.conf(t) 7 | t.title = 'PacPac' 8 | t.author = 'Tyler Neylon' 9 | t.url = 'https://github.com/tylerneylon/pacpac' 10 | t.identity = 'pacpac' 11 | 12 | t.window.width = 1280 13 | t.window.height = 720 14 | end 15 | -------------------------------------------------------------------------------- /pacpac/draw.lua: -------------------------------------------------------------------------------- 1 | -- draw.lua 2 | -- 3 | -- PacPac functions to wrap low-level love.graphics functions. 4 | -- 5 | 6 | local M = {} 7 | 8 | -- Maps nonnegative reals to [0, 1]. 9 | local function lightning_taper(x) 10 | return math.exp(-5 * x) 11 | end 12 | 13 | function M.setColor(r, g, b, a, opts) 14 | if level_num == 3 then 15 | local m = 1.0 16 | if last_lightning ~= nil then 17 | m = 1.0 - lightning_taper(clock - last_lightning) 18 | end 19 | if opts and opts.is_wall then m = 1.0 - m end 20 | if opts and opts.is_live then m = 1.0 end 21 | r = math.floor(r * m) 22 | g = math.floor(g * m) 23 | b = math.floor(b * m) 24 | end 25 | love.graphics.setColor(r, g, b, a) 26 | end 27 | 28 | return M 29 | -------------------------------------------------------------------------------- /pacpac/level3.txt: -------------------------------------------------------------------------------- 1 | # level3.txt 2 | # 3 | # File format: 4 | # Comments have # as the first character in the line, and end at a newline. 5 | # Each section starts with a word followed by a : and a newline, and ends 6 | # with a blank line. 7 | # Extra blank lines are ignored. 8 | # Feel free to edit these for fun! 9 | # 10 | 11 | map: 12 | 1111111111111111111 13 | 1000000000000000001 14 | 1101111110111111011 15 | 1000000000000000001 16 | 1011111110111111101 17 | 1010000000000000101 18 | 1010111110111110101 19 | 1010122210122210101 20 | 1010111210121110101 21 | 1000001110111000001 22 | 1011100000000011101 23 | 1000111010101110001 24 | 1110001010101000111 25 | 0000100000000010000 26 | 1110101010101010111 27 | 1100001010101000011 28 | 1001101010101011001 29 | 1011100010100011101 30 | 1011111000001111101 31 | 1000111113111110001 32 | 1110001122211000111 33 | 1111111111111111111 34 | 35 | superdots: 36 | (10.5, 3.5) (6.5, 21.5) (14.5, 21.5) 37 | 38 | start_pos: 39 | yellow = (10, 11) 40 | red = (10, 19) 41 | blue = (9, 21) 42 | pink = (10, 21) 43 | orange = (11, 21) 44 | 45 | ghost_hotel: 46 | inside = (10, 21) 47 | outside = (10, 19) 48 | 49 | wall_color: 50 | 255 255 255 51 | -------------------------------------------------------------------------------- /pacpac/level2.txt: -------------------------------------------------------------------------------- 1 | # level2.txt 2 | # 3 | # File format: 4 | # Comments have # as the first character in the line, and end at a newline. 5 | # Each section starts with a word followed by a : and a newline, and ends 6 | # with a blank line. 7 | # Extra blank lines are ignored. 8 | # Feel free to edit these for fun! 9 | # 10 | 11 | map: 12 | 1111111111111111111 13 | 0000000000000000000 14 | 1101011110111101011 15 | 2101000000000001012 16 | 1101011110111101011 17 | 1001000010100001001 18 | 1011111010101111101 19 | 1000100000000010001 20 | 1010001010101000101 21 | 1011101010101011101 22 | 1000001010101000001 23 | 1101111000001111011 24 | 1100011010101100011 25 | 1001000010100001001 26 | 1011111010101111101 27 | 1000010010100100001 28 | 1101000110110001011 29 | 2101110110110111012 30 | 2100010000000100012 31 | 1101010113110101011 32 | 0000000122210000000 33 | 1111111111111111111 34 | 35 | superdots: 36 | (4.5, 8.5) (16.5, 8.5) (4.5, 19.5) (16.5, 19.5) 37 | 38 | start_pos: 39 | yellow = (10, 4) 40 | red = (10, 19) 41 | blue = (9, 21) 42 | pink = (10, 21) 43 | orange = (11, 21) 44 | 45 | ghost_hotel: 46 | inside = (10, 21) 47 | outside = (10, 19) 48 | 49 | wall_color: 50 | 0 255 0 51 | -------------------------------------------------------------------------------- /pacpac/level1.txt: -------------------------------------------------------------------------------- 1 | # level1.txt 2 | # 3 | # File format: 4 | # Comments have # as the first character in the line, and end at a newline. 5 | # Each section starts with a word followed by a : and a newline, and ends 6 | # with a blank line. 7 | # Extra blank lines are ignored. 8 | # Colors are in the form R G B where each compononent is an integer in the 9 | # range 0-255. 10 | # 11 | # Feel free to edit these for fun! 12 | # 13 | 14 | map: 15 | 1111111111111111111 16 | 1000000001000000001 17 | 1011011101011101101 18 | 1011011101011101101 19 | 1000000000000000001 20 | 1011010111110101101 21 | 1000010001000100001 22 | 1111011101011101111 23 | 2221010000000101222 24 | 1111010113110101111 25 | 0000000122210000000 26 | 1111010111110101111 27 | 2221010000000101222 28 | 1111010111110101111 29 | 1000000001000000001 30 | 1011011101011101101 31 | 1001000000000001001 32 | 1101010111110101011 33 | 1000010001000100001 34 | 1011111101011111101 35 | 1000000000000000001 36 | 1111111111111111111 37 | 38 | superdots: 39 | (2.5, 4) (18.5, 4) (2.5, 17.5) (18.5, 17.5) 40 | 41 | start_pos: 42 | yellow = (10, 17) 43 | red = (10, 9) 44 | pink = (10, 11) 45 | blue = (9, 11) 46 | orange = (11, 11) 47 | 48 | ghost_hotel: 49 | inside = (10, 11) 50 | outside = (10, 9) 51 | 52 | wall_color: 53 | 0 0 255 54 | -------------------------------------------------------------------------------- /pacpac/util.lua: -------------------------------------------------------------------------------- 1 | -- util.lua 2 | -- 3 | -- A collection of generally useful functions. 4 | -- 5 | 6 | 7 | local M = {} 8 | 9 | local function is_pos_int(n) 10 | return type(n) == "number" and n > 0 and math.floor(n) == n 11 | end 12 | 13 | local function is_array(array) 14 | local max, n = 0, 0 15 | for k, _ in pairs(array) do 16 | if not is_pos_int(k) then return false end 17 | max = math.max(max, k) 18 | n = n + 1 19 | end 20 | return n == max 21 | end 22 | 23 | function M.str(t) 24 | if type(t) == 'table' then 25 | local s = '{' 26 | if is_array(t) then 27 | for i, v in ipairs(t) do 28 | if #s > 1 then s = s .. ', ' end 29 | s = s .. M.str(v) 30 | end 31 | else 32 | -- It's a non-array table. 33 | for k, v in pairs(t) do 34 | if #s > 1 then s = s .. ', ' end 35 | s = s .. M.str(k) 36 | s = s .. ' = ' 37 | s = s .. M.str(v) 38 | end 39 | end 40 | s = s .. '}' 41 | return s 42 | elseif type(t) == 'number' then 43 | return tostring(t) 44 | elseif type(t) == 'boolean' then 45 | return tostring(t) 46 | elseif type(t) == 'string' then 47 | return t 48 | end 49 | return 'unknown type' 50 | end 51 | 52 | -- Turns {a, b} into {[str(a)] = a, [str(b)] = b}. 53 | -- This is useful for testing if hash[key] for inclusion. 54 | function M.hash_from_list(list) 55 | local hash = {} 56 | for k, v in pairs(list) do hash[M.str(v)] = v end 57 | return hash 58 | end 59 | 60 | 61 | 62 | return M 63 | -------------------------------------------------------------------------------- /pacpac/levelreader.lua: -------------------------------------------------------------------------------- 1 | -- levelreader.lua 2 | -- 3 | -- Interface for loading level data (aka maps) from files. 4 | -- This doesn't really have to be a separate module, but I felt like pulling 5 | -- out this functionality. 6 | -- 7 | -- Meant to be used as: 8 | -- local levelreader = require('levelreader') 9 | -- local level = levelreader.read('level1.txt') 10 | -- 11 | -- The output has the following form: 12 | -- level = { m = << 2d array of sprite layout by (x,y) coords >>, 13 | -- start_pos = << start positions of characters >>, 14 | -- superdots = << positions of superdots >>, 15 | -- ghost_hotel = << hotel position info >>, 16 | -- wall_color = {r = red, g = green, b = blue} 17 | -- } 18 | -- 19 | 20 | 21 | local M = {} 22 | 23 | 24 | ------------------------------------------------------------------------------- 25 | -- Public interface. 26 | ------------------------------------------------------------------------------- 27 | 28 | -- Read level info from a file. 29 | function M.read(filename) 30 | local section = nil 31 | local level = { map = {}, superdots = {}, start_pos = {}, ghost_hotel = {} } 32 | for line in love.filesystem.lines(filename) do 33 | -- Skip over lines starting with #, which has ascii code 35. 34 | if line:byte() ~= 35 then 35 | if #line == 0 then 36 | section = nil 37 | elseif section == 'map' then 38 | for i = 1, #line do 39 | if level.map[i] == nil then level.map[i] = {} end 40 | table.insert(level.map[i], tonumber(line:sub(i, i))) 41 | end 42 | elseif section == 'superdots' then 43 | for x, y in string.gmatch(line, '([%d%.]+),%s*([%d%.]+)') do 44 | table.insert(level.superdots, {x, y}) 45 | end 46 | elseif section == 'start_pos' then 47 | for color, x, y in string.gmatch(line, '(%w+).-(%d+).-(%d+)') do 48 | level.start_pos[color] = {x + 0.5, y + 0.5} 49 | end 50 | elseif section == 'ghost_hotel' then 51 | for pos, x, y in string.gmatch(line, '(%w+).-(%d+).-(%d+)') do 52 | level.ghost_hotel[pos] = {x + 0.5, y + 0.5} 53 | end 54 | elseif section == 'wall_color' then 55 | local r, g, b = string.match(line, '(%d+)%s+(%d+)%s+(%d+)') 56 | level.wall_color = {r = r, g = g, b = b} 57 | elseif section == nil then 58 | section = line:sub(1, #line - 1) 59 | end 60 | end 61 | end 62 | return level 63 | end 64 | 65 | 66 | return M 67 | 68 | -------------------------------------------------------------------------------- /pacpac/notes.lua: -------------------------------------------------------------------------------- 1 | -- notes.lua 2 | -- 3 | -- Used to play songs composed programmatically of notes. 4 | -- The notes themselves are prerecorded as small audio files. 5 | -- 6 | 7 | local M = {} 8 | 9 | 10 | ------------------------------------------------------------------------------- 11 | -- Public interface. Definitions are below. 12 | ------------------------------------------------------------------------------- 13 | 14 | -- Play a song, which is an array of notes. A note is either a string like 15 | -- "c3" or an array of strings which will be played simulateneously. 16 | -- between_notes is an optional parameter specifying time between note starts. 17 | -- The default 0.3 seconds. 18 | -- done_cb is an optional callback called at the end of a song. 19 | -- note_db is an optional callback called as each note is played; it receives 20 | -- the note begin played as a parameter. 21 | -- The return value is a song_id that can be used to stop the song early. 22 | function M.play_song(song, between_notes, done_cb, note_cb) end 23 | 24 | function M.stop_song(song_id) end 25 | 26 | 27 | ------------------------------------------------------------------------------- 28 | -- Private parts. 29 | ------------------------------------------------------------------------------- 30 | 31 | local events = require('events') 32 | local next_song_id = 1 33 | local event_ids_per_song_id = {} 34 | 35 | -- Load in the notes audio. 36 | local note_names = {'c1', 'g1', 'c2', 'e2-', 'g2', 'c3', 'd3', 'e3', 'c4'} 37 | local notes = {} 38 | for k, v in pairs(note_names) do 39 | notes[v] = love.audio.newSource('audio/notes/' .. v .. '.ogg', 'static') 40 | notes[v]:setVolume(0.7) 41 | end 42 | 43 | local function play_note(note) 44 | if type(note) == 'string' then 45 | if notes[note] then 46 | notes[note]:play() 47 | else 48 | print('Error: No audio data for note named "' .. note .. '"') 49 | end 50 | elseif type(note) == 'number' then 51 | return -- It's a rest. 52 | else 53 | -- It's an array of notes. 54 | for k, v in pairs(note) do play_note(v) end 55 | end 56 | end 57 | 58 | local function _play_song(song_id, song, between_notes, done_cb, note_cb) 59 | if #song == 0 then 60 | if done_cb then done_cb() end 61 | return 62 | end 63 | if note_cb then note_cb(song[1]) end 64 | if not between_notes then between_notes = 0.3 end 65 | 66 | play_note(song[1], note_cb) 67 | table.remove(song, 1) 68 | 69 | function play_rest() 70 | _play_song(song_id, song, between_notes, done_cb, note_cb) 71 | end 72 | 73 | local e_id = events.add(between_notes, play_rest) 74 | event_ids_per_song_id[song_id] = e_id 75 | return song_id 76 | end 77 | 78 | -- Useful for debugging. 79 | local function traceback() 80 | local level = 1 81 | while true do 82 | local info = debug.getinfo(level, "Sl") 83 | if not info then break end 84 | if info.what == "C" then -- is a C function? 85 | print(level, "C function") 86 | else -- a Lua function 87 | print(string.format("[%s]:%d", 88 | info.short_src, info.currentline)) 89 | end 90 | level = level + 1 91 | end 92 | end 93 | 94 | ------------------------------------------------------------------------------- 95 | -- Public function definitions. 96 | ------------------------------------------------------------------------------- 97 | 98 | 99 | function M.play_song(song, between_notes, done_cb, note_cb) 100 | local song_id = next_song_id 101 | next_song_id = next_song_id + 1 102 | _play_song(song_id, song, between_notes, done_cb, note_cb) 103 | return song_id 104 | end 105 | 106 | function M.stop_song(song_id) 107 | events.cancel(event_ids_per_song_id[song_id]) 108 | end 109 | 110 | 111 | return M 112 | 113 | -------------------------------------------------------------------------------- /pacpac/events.lua: -------------------------------------------------------------------------------- 1 | -- events.lua 2 | -- 3 | -- Meant to be used as: 4 | -- local events = require('events') 5 | -- events.add(1.0, my_fn) 6 | -- 7 | -- It's important to call events.update from love.update: 8 | -- function love.update(dt) 9 | -- events.update(dt) 10 | -- -- other update code 11 | -- end 12 | -- 13 | 14 | local M = {} 15 | 16 | 17 | ------------------------------------------------------------------------------- 18 | -- Public interface. Definitions are below. 19 | ------------------------------------------------------------------------------- 20 | 21 | -- Register a callback to be called after a delay. 22 | -- The name is an optional string, and allows for easier cancellation. 23 | -- Returns a unique event_id. 24 | function M.add(delay, callback, name) end 25 | 26 | -- The event_id can either be the value returned from add or the name given to 27 | -- add. If it is a non-unique name, then only the last-added event with that 28 | -- name will be cancelled. 29 | function M.cancel(event_id) end 30 | 31 | -- This must be called often for everything to work; it's designed to be called 32 | -- from the end of love.update. (From the end because otherwise it's likely to 33 | -- have unexpected changes due to a callback before your update function is 34 | -- done executing.) 35 | function M.update(dt) end 36 | 37 | 38 | ------------------------------------------------------------------------------- 39 | -- Singleton wrapper. 40 | ------------------------------------------------------------------------------- 41 | 42 | local selfname = debug.getinfo(1).source 43 | if not global_singleton then global_singleton = {} end 44 | if global_singleton[selfname] then return global_singleton[selfname] end 45 | global_singleton[selfname] = M 46 | 47 | 48 | ------------------------------------------------------------------------------- 49 | -- Private parts. 50 | ------------------------------------------------------------------------------- 51 | 52 | local clock = 0 53 | local next_number_id = 1 54 | local event_ids_by_time = {} -- An array with values = an event_id. 55 | local events_by_id = {} -- A dict with key = event_id, value = event table. 56 | local event_ids_by_name = {} 57 | 58 | local function insert(event_id) 59 | local event = events_by_id[event_id] 60 | local e = event_ids_by_time 61 | local i = 1 62 | while i <= #e and events_by_id[e[i]].time < event.time do 63 | i = i + 1 64 | end 65 | table.insert(event_ids_by_time, i, event_id) 66 | end 67 | 68 | local function remove(event_id) 69 | local e = event_ids_by_time 70 | for i = 1, #e do 71 | if e[i] == event_id then 72 | table.remove(event_ids_by_time, i) 73 | return 74 | end 75 | end 76 | end 77 | 78 | ------------------------------------------------------------------------------- 79 | -- Public function definitions. 80 | ------------------------------------------------------------------------------- 81 | 82 | function M.add(delay, callback, name) 83 | event_id = next_number_id 84 | next_number_id = next_number_id + 1 85 | if name then event_ids_by_name[name] = event_id end 86 | local event = {time = clock + delay, callback = callback, name = name} 87 | events_by_id[event_id] = event 88 | insert(event_id) -- Inserts into event_ids_by_time. 89 | return event_id 90 | end 91 | 92 | function M.cancel(event_id) 93 | if type(event_id) == 'string' then 94 | event_id = event_ids_by_name[event_id] 95 | end 96 | local e = events_by_id[event_id] 97 | if e == nil then return end 98 | if e.name and event_ids_by_name[e.name] == event_id then 99 | event_ids_by_name[e.name] = nil 100 | end 101 | remove(event_id) -- Removes from event_ids_by_time. 102 | events_by_id[event_id] = nil 103 | end 104 | 105 | function M.update(dt) 106 | clock = clock + dt 107 | local e = event_ids_by_time 108 | while #e > 0 and events_by_id[e[1]].time < clock do 109 | events_by_id[e[1]].callback() 110 | M.cancel(e[1]) 111 | end 112 | end 113 | 114 | return M 115 | 116 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # PacPac 2 | 3 | This is Pac-Man from a parallel universe. 4 | 5 | ![PacPac Title](https://raw.github.com/tylerneylon/pacpac/master/screenshots/title.png) 6 | 7 | ![PacPac Level Samples](https://raw.github.com/tylerneylon/pacpac/master/screenshots/level1_2.png) 8 | 9 | There are 3 mazes to play through. This is thrice as 10 | many as the original pac-man :) 11 | 12 | You need the [löve](http://love2d.org) game engine to play. The current code is written for löve 13 | v0.9.2. 14 | 15 | The original code was written in under 24 hours as a challenge. 16 | My wife didn't believe I could make a pac-man-like game in a day. 17 | Here's a fun [first 24-hour evolution of the game in screenshots](http://tylerneylon.com/pacpac/). 18 | 19 | ## How to install and run 20 | 21 | 1. Download and install [löve](http://love2d.org). (Current code has been tested with löve v0.9.2.) 22 | 2. Download and unzip the [zipfile of this repo](https://github.com/tylerneylon/pacpac/archive/master.zip). 23 | 3. Double-click the file `pacpac.love`. 24 | Alternatively, in OS X and Ubuntu, type `love pacpac.love` from the command line - which assumes 25 | the `love` executable is in your path. 26 | 27 | ## Level Editing 28 | 29 | I've set up the game so that you can make your own levels without having to know how to program. 30 | Just edit `level1.txt` or any other `levelN.txt` file to change that level. The file format is 31 | explained within those files, and this format is designed to be human-friendly and flexible. 32 | 33 | If you're running PacPac using `pacpac.love`, then you need to run 34 | `make_love_file.sh` before the level changes will show up in the game. This 35 | shell script is meant to be run from the command line by cd'ing into your pacpac 36 | directory (the one containing `make_love_file.sh`) and typing the command 37 | `./make_love_file.sh`. 38 | 39 | ## Contributions 40 | 41 | It would be awesome if other coders contributed more levels. I'd like each level to add something 42 | new to the game. For now, level 2 adds a new layout and color, which in most games would 43 | not count as "new" but since Pac-Man has such a strong 1-layout tradition, I'm counting it as new. 44 | 45 | My code philosophy for PacPac is to keep the code a little dirty, as in using global variables 46 | freely. Seriously. It's not that dirty is good, but rather that getting things done is good. 47 | So I'm asking for contributions that fix bugs or improve gameplay, but are not focused on 48 | refactoring. Refactoring is fine as a by-product of other changes, though. 49 | 50 | If you'd like to add a level, please read the next section to understand what kind of 51 | level designs would fit in with the game. Thanks! 52 | 53 | ## Things That Could be Added 54 | 55 | ### Levels 56 | 57 | Below are a few ideas for later levels. 58 | It would be cool to arrange them in the game from easiest to hardest. 59 | 60 | * New ghost AI's in different colors. 61 | * A gun that can shoot ghosts. 62 | * A level with keys that can open doors. Doors are basically walls that you can erase 63 | with a key. 64 | * A level with portal-like mechanics. Maybe a warp door that changes connected doors, 65 | or a warp gun. (This sounds a little scary to have to debug.) 66 | * A level where the hero and ghosts switch roles. By default, the ghosts are weak - i.e., flashing 67 | white/blue and can be eaten. The ghosts eat dots, and if they eat a superdot, then the hero 68 | becomes vulnerable - i.e., the ghosts appear non-flashing temporarily. However, the ghosts no 69 | longer reincarnate, and it is the hero's goal to eat all of them. 70 | 71 | Once we have 10 good and mostly bug-free levels, I'll consider the game to be v1-ready. 72 | 73 | ### Other features 74 | 75 | Summary: 76 | 77 | * Tasty foods for bonus points 78 | * One or maybe two extra lives for certain score points 79 | * Replay a previously-played game 80 | * Analytics 81 | * Server-based high-score-of-the-day 82 | 83 | #### Tasty foods 84 | 85 | In the original game, you can eat fruit like apples and oranges. 86 | It would be cool to add more fun foods like pizza, burgers, fries, 87 | and waffles. Maybe cinnamon rolls. Foods that are tasty and would 88 | make for fun pixel art. 89 | 90 | #### Extra lives 91 | 92 | In the original game, you also get an extra life once you reach a 93 | certain score. This is a nice feature that we could include 94 | in PacPac. 95 | 96 | #### Game replay 97 | 98 | Automatically save all the effective commands the user provides so that 99 | we can exactly replay that game as a watch-only experience. Maybe this 100 | could happen automatically for the highest-scoring game, which would 101 | be displayed from the title screen if the user is idle. 102 | 103 | There are a couple points to be careful about. The game currently uses 104 | a random number generator, so we'd have to save the seed used. It also 105 | depends on the dt values sent in to update, so we'd have to be careful 106 | about how the replay worked with the dt values. That might be tricky. 107 | Finally, there is technically analog input available through gamepads, 108 | but this can be discretized so that we only need to remember the 109 | successful calls to `dir_request`. 110 | 111 | #### Analytics 112 | 113 | By this, I mean heat maps of death locations on each level, and average 114 | time-of-life per level. This could help us figure out which levels are 115 | most challenging. From there we could do things like modify too-hard or 116 | too-easy sections, and make sure the levels are in the right order. 117 | 118 | #### Server-based scores 119 | 120 | This is self-explanatory. Even better is being able to download and watch 121 | a replay of good high scores. 122 | 123 | ## Credits 124 | 125 | This game uses the font 8bitoperator created by 126 | [GrandChaos9000](http://grandchaos9000.deviantart.com/) 127 | (aka Jayvee D. Enaguas) and is distributed under the 128 | [CC-BY-SA](http://creativecommons.org/licenses/by-sa/2.0/) license. 129 | 130 | Thanks to [jonfk](https://github.com/jonfk) for upgrading the code 131 | for löve v0.9.2. 132 | -------------------------------------------------------------------------------- /pacpac/Character.lua: -------------------------------------------------------------------------------- 1 | -- Character.lua 2 | -- 3 | -- A class to work with the hero and ghosts. 4 | -- 5 | -- Use like this: 6 | -- local Character = require('Character') 7 | -- 8 | 9 | local draw = require('draw') 10 | local util = require('util') 11 | 12 | -- TODO Remove this once we have more confidence the bug is fixed. 13 | function ask_to_report_error() 14 | s = 'Please report this error and version number: ' .. version 15 | s = s .. ' on this thread: ' 16 | s = s .. 'https://github.com/tylerneylon/pacpac/issues/7' 17 | print(s) 18 | end 19 | 20 | local Character = {} ; Character.__index = Character 21 | 22 | -- shape is 'hero' or 'ghost'; color is in {'red', 'pink', 'blue', 'orange'}. 23 | function Character.new(shape, color) 24 | local c = setmetatable({shape = shape, color = color}, Character) 25 | c.is_fake = false 26 | c:reset() 27 | return c 28 | end 29 | 30 | function Character:is_dead() 31 | return self.dead_till > clock 32 | end 33 | 34 | function Character:is_weak() 35 | return super_mode_till > clock and not self.eaten 36 | end 37 | 38 | function Character:reset() 39 | self.dead_till = -1 40 | self.mode = 'normal' -- Can be 'freemove' for ghosts through the hotel door. 41 | self.eaten = false -- To avoid ghosts being double eaten. 42 | self.past_turns = {} 43 | local start_pos = level.start_pos[self.color] 44 | self.x = start_pos[1] 45 | self.y = start_pos[2] 46 | if self.shape == 'hero' then 47 | self.dir = {-1, 0} 48 | self.next_dir = nil 49 | else 50 | -- It's a ghost. 51 | if self.color == 'red' then 52 | self.dir = {1, 0} 53 | self.exit_time = math.huge 54 | elseif self.color == 'pink' then 55 | self.dir = {0, 0} 56 | self.exit_time = clock + 6 57 | elseif self.color == 'blue' then 58 | self.dir = {0, 0} 59 | self.exit_time = clock + 12 60 | elseif self.color == 'orange' then 61 | self.dir = {0, 0} 62 | self.exit_time = clock + 18 63 | end 64 | end 65 | end 66 | 67 | function Character:speed() 68 | local hotel_pos = level.ghost_hotel.outside 69 | if self.shape == 'hero' then return 4 end 70 | if self:is_dead() then 71 | -- The dead move fast, except near the hotel so they don't miss the door. 72 | if self:dist_to_pt(hotel_pos) < 1 then return 4 end 73 | return 8 74 | end 75 | if self:is_weak() then 76 | return 2.5 77 | else 78 | return 4 79 | end 80 | end 81 | 82 | function Character:target() 83 | local hotel = level.ghost_hotel 84 | if self.shape == 'hero' then return {} end 85 | if self:is_dead() then return hotel.inside end 86 | if self.mode == 'freemove' then return hotel.outside end 87 | if self:is_weak() then 88 | return {math.random() * 19, math.random() * 22} 89 | end 90 | if self.color == 'red' then 91 | if ghost_mode == 'scatter' then return {18.5, 2.5} end 92 | if ghost_mode == 'pursue' then 93 | return {man.x, man.y} 94 | end 95 | elseif self.color == 'pink' then 96 | if ghost_mode == 'scatter' then return {2.5, 2.5} end 97 | if ghost_mode == 'pursue' then 98 | return {man.x + man.dir[1] * 2, man.y + man.dir[2] * 2} 99 | end 100 | elseif self.color == 'blue' then 101 | if ghost_mode == 'scatter' then return {18.5, 21.5} end 102 | if ghost_mode == 'pursue' then 103 | local v1 = {man.x + man.dir[1], man.y + man.dir[2]} 104 | local v2 = {v1[1] - red.x, v1[2] - red.y} 105 | return {v1[1] + v2[1], v1[2] + v2[2]} 106 | end 107 | elseif self.color == 'orange' then 108 | local default = {2.5, 21.5} 109 | if ghost_mode == 'scatter' then return default end 110 | if ghost_mode == 'pursue' then 111 | local dist_v = {self.x - man.x, self.y - man.y} 112 | local dist_sq = dist_v[1] * dist_v[1] + dist_v[2] * dist_v[2] 113 | if dist_sq > 16 then 114 | return {man.x, man.y} 115 | else 116 | return default 117 | end 118 | end 119 | end 120 | end 121 | 122 | -- TODO Is this needed anymore? Check. 123 | function Character:snap_into_place() 124 | if self.dir[1] == 0 then 125 | self.x = math.floor(2 * self.x + 0.5) / 2 126 | end 127 | if self.dir[2] == 0 then 128 | self.y = math.floor(2 * self.y + 0.5) / 2 129 | end 130 | end 131 | 132 | function Character:can_go_in_dir(dir) 133 | if dir == nil then return false end 134 | local new_x, new_y = self.x + dir[1], self.y + dir[2] 135 | local can_pass_hotel_door = (self.mode == 'freemove' or self:is_dead()) 136 | return not xy_hits_a_wall(new_x, new_y, can_pass_hotel_door) 137 | end 138 | 139 | -- Only returns true for u-turns or when pac-pac is at a grid point, which is 140 | -- common when he is stopped. 141 | function Character:can_turn_right_now(dir) 142 | if not self:can_go_in_dir(dir) then return false end 143 | 144 | -- Allow legit turns at grid points. 145 | if self.x - math.floor(self.x) == 0.5 and 146 | self.y - math.floor(self.y) == 0.5 then 147 | return true 148 | end 149 | 150 | -- Allow u-turns anywhere. 151 | for i = 1, 2 do 152 | if dir[i] ~= -self.dir[i] then return false end 153 | end 154 | return true 155 | end 156 | 157 | function Character:turn_score(dir) 158 | local target = self:target() 159 | local target_dir = {target[1] - self.x, target[2] - self.y} 160 | local score = target_dir[1] * dir[1] + target_dir[2] * dir[2] 161 | local past = self.past_turns[util.str({self.x, self.y})] 162 | if past and util.str(past.dir) == util.str(dir) then 163 | score = score - 5 * past.times 164 | end 165 | return score 166 | end 167 | 168 | -- Returns available directions, skipping u-turns unless it's the only choice. 169 | -- This is for use by ghosts. 170 | function Character:available_dirs() 171 | local turn = {self.dir[2], self.dir[1]} 172 | local turns = {} 173 | if self:can_go_in_dir(self.dir) then turns = {self.dir} end 174 | for sign = -1, 1, 2 do 175 | local t = {turn[1] * sign, turn[2] * sign} 176 | if self:can_go_in_dir(t) then table.insert(turns, t) end 177 | end 178 | if #turns == 0 then table.insert(turns, {-self.dir[1], -self.dir[2]}) end 179 | return turns 180 | end 181 | 182 | -- Switch self.dir to dir if it is more aligned with getting to the target. 183 | function Character:turn_if_better(turn) 184 | if self:turn_score(turn) > self:turn_score(self.dir) then 185 | self.dir = turn 186 | self.last_turn = {self.x, self.y} 187 | end 188 | end 189 | 190 | function Character:next_grid_point() 191 | local pt = {self.x, self.y} 192 | 193 | -- TODO Remove this once we have more confidence it doesn't happen. 194 | local num_bad = 0 195 | for i = 1, 2 do 196 | if pt[i] - math.floor(pt[i]) ~= 0.5 then num_bad = num_bad + 1 end 197 | end 198 | if num_bad == 2 then 199 | print('Error: bad coordinate <' .. self.shape .. '> (' .. self.x .. ', ' .. self.y .. ')') 200 | ask_to_report_error() 201 | end 202 | 203 | for i = 1, 2 do 204 | -- This if block is more readable than a short but opaque mathy summary. 205 | if self.dir[i] == 1 then 206 | pt[i] = math.floor(pt[i] + 0.5) + 0.5 207 | elseif self.dir[i] == -1 then 208 | pt[i] = math.ceil(pt[i] - 0.5) - 0.5 209 | elseif self.dir[i] ~= 0 then 210 | print('Error: Unexpected dir value (' .. self.dir[i] .. ') found.') 211 | ask_to_report_error() 212 | end 213 | end 214 | return pt 215 | end 216 | 217 | function Character:update(dt) 218 | if pause_till > clock then return end 219 | 220 | -- Check if it's time for a ghost to exit the ghost hotel. 221 | if self.shape == 'ghost' and self.exit_time < clock then 222 | 223 | -- TODO Remove this once we have more confidence it doesn't happen. 224 | local pt = {self.x, self.y} 225 | local num_bad = 0 226 | for i = 1, 2 do 227 | if pt[i] - math.floor(pt[i]) ~= 0.5 then num_bad = num_bad + 1 end 228 | end 229 | if num_bad > 0 then 230 | print('Error: ghost turn triggered when not on a grid point') 231 | print('Current position=(' .. self.x .. ', ' .. self.y .. ')') 232 | print('Current dir=(' .. self.dir[1] .. ', ' .. self.dir[2] .. ')') 233 | ask_to_report_error() 234 | end 235 | 236 | self.mode = 'freemove' 237 | self.exit_time = math.huge 238 | self.dir = {0, -1} 239 | end 240 | 241 | local movement = dt * self:speed() 242 | while movement > 0 do 243 | -- This is inside the loop because the hero can hit a wall and stop. 244 | if self.dir[1] == 0 and self.dir[2] == 0 then break end 245 | 246 | local pt = self:next_grid_point() 247 | local dist = self:dist_to_pt(pt) 248 | if dist <= movement then 249 | self.x, self.y = pt[1], pt[2] 250 | self:reached_grid_point() 251 | else 252 | self.x = self.x + self.dir[1] * movement 253 | self.y = self.y + self.dir[2] * movement 254 | end 255 | movement = movement - dist -- May end up below 0; that's ok. 256 | end 257 | 258 | self:check_for_side_warps() 259 | self:register_dots_eaten() 260 | end 261 | 262 | function Character:reached_grid_point() 263 | if self.x < 1 or self.x > (#map + 1) then return end 264 | 265 | if self.shape == 'hero' then 266 | if self:can_go_in_dir(self.next_dir) then 267 | self.dir = self.next_dir 268 | self.next_dir = nil 269 | elseif not self:can_go_in_dir(self.dir) then 270 | self.dir = {0, 0} 271 | end 272 | end 273 | 274 | if self.shape == 'ghost' then 275 | 276 | -- Handle movements around the ghost hotel door. 277 | local can_pass_hotel_door = (self.mode == 'freemove' or self:is_dead()) 278 | local t = self:target() 279 | if can_pass_hotel_door and self.x == t[1] and self.y == t[2] then 280 | if self:is_dead() then 281 | self.dir = {0, -1} 282 | self.dead_till = clock 283 | self.mode = 'freemove' 284 | self.eaten = true 285 | else 286 | self.mode = 'normal' 287 | end 288 | end 289 | 290 | -- Handle standard movements (not into/out of the ghost hotel). 291 | local dirs = self:available_dirs() 292 | self.dir = dirs[1] 293 | for k, t in pairs(dirs) do self:turn_if_better(t) end 294 | self:register_turn() 295 | end 296 | end 297 | 298 | -- Records that we turned to self.dir at self.xy. 299 | function Character:register_turn() 300 | local key = util.str({self.x, self.y}) 301 | local value = self.past_turns[key] 302 | if value and util.str(value.dir) == util.str(self.dir) then 303 | value.times = value.times + 1 304 | else 305 | self.past_turns[key] = {dir = self.dir, times = 1} 306 | end 307 | end 308 | 309 | -- If a character is far to the left right, they jump across the map. 310 | function Character:check_for_side_warps() 311 | if self.x <= 0.5 then 312 | self.x = #map + 1.5 313 | self.dir = {-1, 0} 314 | elseif self.x >= #map + 1.5 then 315 | self.x = 0.5 316 | self.dir = {1, 0} 317 | end 318 | end 319 | 320 | function Character:register_dots_eaten() 321 | if self.shape ~= 'hero' then return end 322 | local dots_hit = dots_hit_by_man_at_xy(self.x, self.y) 323 | for k, v in pairs(dots_hit) do 324 | if dots[k] then 325 | if superdots[k] then superdot_eaten() end 326 | dots[k] = nil 327 | num_dots = num_dots - 1 328 | add_to_score(10) 329 | play_wata_till = clock + 0.2 330 | wata:play() 331 | if num_dots <= dots_at_end then 332 | pause_till = math.huge 333 | level_won() 334 | end 335 | end 336 | end 337 | end 338 | 339 | function Character:draw_death_anim() 340 | local t = death_anim_time - (death_anim_till - clock) 341 | local start, stop = 1.0, 2 * math.pi 342 | local erase_time = 0.5 343 | -- From time_left, map 3 -> start, 1 -> stop. 344 | local angle = (t / erase_time) * (stop - start) + start 345 | if angle > stop then 346 | if (death_anim_time - t) < 1.1 then 347 | love.graphics.setLineWidth(1) 348 | draw.setColor(255, 255, 255) 349 | local n = 7 350 | local r1, r2 = 0.15 * tile_size, 0.3 * tile_size 351 | for a = 0, 2 * math.pi, 2 * math.pi / n do 352 | local c, s = math.cos(a), math.sin(a) 353 | local x, y = self.x * tile_size, self.y * tile_size 354 | love.graphics.line(c * r1 + x, s * r1 + y, 355 | c * r2 + x, s * r2 + y) 356 | end 357 | end 358 | return 359 | end 360 | draw.setColor(255, 255, 0) 361 | local offset = math.atan2(-1, 0) 362 | local r = 0.45 363 | love.graphics.arc('fill', self.x * tile_size, self.y * tile_size, 364 | tile_size * r, 365 | offset + angle / 2, 366 | offset + 2 * math.pi - angle / 2, 16) 367 | end 368 | 369 | function Character:draw() 370 | local draw_opts = {is_live = not self.is_fake} 371 | if not self.always_draw then 372 | if self.shape == 'hero' and death_anim_till > clock then 373 | self:draw_death_anim() 374 | return 375 | end 376 | if pause_till > clock then return end 377 | end 378 | local colors = {red = {255, 0, 0}, pink = {255, 128, 128}, 379 | blue = {0, 224, 255}, orange = {255, 128, 0}, 380 | yellow = {255, 255, 0}} 381 | local color = colors[self.color] 382 | draw.setColor(color[1], color[2], color[3], 255, draw_opts) 383 | if self.shape == 'hero' then 384 | local p = 0.15 -- Period, in seconds, of chomps. 385 | local max = 1.0 -- Max mouth angle, in radians. 386 | local r = 0.45 -- Fraction of tile_size to use as a radius. 387 | if self.always_draw then 388 | local mouth_angle = max 389 | local start = math.atan2(0, -1) 390 | love.graphics.arc('fill', self.x * tile_size, self.y * tile_size, 391 | tile_size * r, 392 | start + mouth_angle / 2, 393 | start + 2 * math.pi - mouth_angle / 2, 16) 394 | else 395 | local mouth_angle = max * (math.sin((clock % p) / p * 2 * math.pi) + 1.0) 396 | local start = math.atan2(self.dir[2], self.dir[1]) 397 | love.graphics.arc('fill', self.x * tile_size, self.y * tile_size, 398 | tile_size * r, 399 | start + mouth_angle / 2, 400 | start + 2 * math.pi - mouth_angle / 2, 16) 401 | end 402 | else -- It's a ghost. 403 | local is_inverted_weak = false 404 | if self:is_weak() then 405 | draw.setColor(0, 0, 255, 255, draw_opts) 406 | local time_left = super_mode_till - clock 407 | is_inverted_weak = time_left < 2 and 408 | (math.floor(time_left * 3) % 2 == 0) 409 | if is_inverted_weak then 410 | draw.setColor(255, 255, 255, 255, draw_opts) 411 | end 412 | end 413 | if not self:is_dead() then 414 | -- Draw the ghost body. 415 | local r = 0.45 416 | love.graphics.circle('fill', self.x * tile_size, 417 | self.y * tile_size, tile_size * r, 14) 418 | 419 | local vertices = {self.x * tile_size, self.y * tile_size, 420 | (self.x - r) * tile_size, self.y * tile_size} 421 | local n = 7 422 | local left = (self.x - r) * tile_size 423 | local bottom = (self.y + 0.45) * tile_size 424 | for i = 0, n - 1 do 425 | local dy = 2 * (1 - (i % 2) * 2) 426 | table.insert(vertices, left + (i / (n - 1)) * tile_size * (2 * r)) 427 | table.insert(vertices, bottom + dy) 428 | end 429 | table.insert(vertices, (self.x + r) * tile_size) 430 | table.insert(vertices, self.y * tile_size) 431 | love.graphics.polygon('fill', vertices) 432 | end 433 | -- Draw the eyes. 434 | draw.setColor(255, 255, 255, 255, draw_opts) 435 | if is_inverted_weak then draw.setColor(0, 0, 255, 255, draw_opts) end 436 | for i = -1, 1, 2 do 437 | local dx = i * 5 438 | local radius = 4 439 | if self:is_weak() then radius = 2 end 440 | love.graphics.circle('fill', self.x * tile_size + dx, 441 | (self.y - 0.1) * tile_size, radius, 10) 442 | end 443 | if self:is_dead() or not self:is_weak() then 444 | -- Draw the pupils. 445 | draw.setColor(0, 0, 192, 255, draw_opts) 446 | for i = -1, 1, 2 do 447 | local dx = i * 5 448 | love.graphics.circle('fill', self.x * tile_size + dx + 1.5 * self.dir[1], 449 | (self.y - 0.1) * tile_size + self.dir[2], 2.5, 10) 450 | end 451 | elseif self:is_weak() then 452 | -- We're in super mode; draw a wavy mouth. 453 | love.graphics.setLineWidth(1) 454 | local base = {self.x * tile_size - 7.5, self.y * tile_size + 5} 455 | local last_pt = nil 456 | for i = 0, 6 do 457 | local dy = (i % 2) * 2 - 1 458 | local pt = {base[1] + i * 2.5, base[2] - dy} 459 | if last_pt then 460 | love.graphics.line(last_pt[1], last_pt[2], pt[1], pt[2]) 461 | end 462 | last_pt = pt 463 | end 464 | end 465 | end 466 | end 467 | 468 | function Character:dist_to_pt(pt) 469 | local dist_v = {self.x - pt[1], self.y - pt[2]} 470 | -- Using L1 makes it easier to survive close-pursuit turns. 471 | return math.abs(dist_v[1]) + math.abs(dist_v[2]) 472 | end 473 | 474 | function Character:dist(other) 475 | if other:is_dead() then return math.huge end 476 | return self:dist_to_pt({other.x, other.y}) 477 | end 478 | 479 | return Character 480 | -------------------------------------------------------------------------------- /pacpac/main.lua: -------------------------------------------------------------------------------- 1 | --[[ main.lua 2 | 3 | Main code for PacPac, a lua-based pac-man clone. 4 | There are many pac-man clones. This one is mine. 5 | ]] 6 | 7 | local Character = require('Character') 8 | local draw = require('draw') 9 | local events = require('events') 10 | local levelreader = require('levelreader') 11 | local notes = require('notes') 12 | local util = require('util') 13 | 14 | ------------------------------------------------------------------------------- 15 | -- Declare all globals here. 16 | ------------------------------------------------------------------------------- 17 | 18 | version = "0.301" 19 | 20 | map = nil 21 | num_levels = 3 22 | 23 | -- This can be 'start screen' or 'playing'. 24 | game_mode = nil 25 | 26 | superdots = {} -- Value given below. 27 | num_dots = 0 28 | 29 | tile_size = 28 30 | 31 | man_x = 10.5 32 | man_y = 17.5 33 | man_dir = {-1, 0} 34 | pending_dir = nil 35 | clock = 0 36 | 37 | message = '' 38 | show_message_till = -1 39 | pause_till = -1 40 | game_over = false 41 | super_mode_till = -1 42 | death_anim_till = -1 43 | 44 | man = nil -- A Character object for the hero. 45 | red = nil 46 | characters = {} -- All moving Character objects = man + ghosts. 47 | 48 | ghost_mode = 'scatter' 49 | 50 | lives_left = 0 51 | life_start_time = 0 52 | next_music_speedup = -1 53 | score = 0 54 | hi_score = 0 55 | ghost_eaten_scores = {} 56 | next_ghost_score = 200 57 | death_anim_time = 2 58 | 59 | jstick = nil 60 | jstick_img = nil 61 | jstick_overlay = nil 62 | keybd_img = nil 63 | 64 | logo = nil 65 | large_font = nil 66 | small_font = nil 67 | 68 | say_ready_till = 0 69 | 70 | -- Sound-related variables. 71 | 72 | wata = nil 73 | play_wata_till = -1 74 | weeoo = nil 75 | bwop = nil 76 | death_noise = nil 77 | nomnomz = {} 78 | nomnomz_index = 1 79 | runny = nil 80 | open_noise = nil 81 | thunder = {} 82 | thunder_index = 1 83 | 84 | start_song_id = nil 85 | 86 | start_lives = 3 87 | dots_at_end = 0 88 | easy_mode = false 89 | is_invincible = false 90 | 91 | if easy_mode then 92 | start_lives = 10 93 | dots_at_end = 250 94 | end 95 | 96 | -- Potential debug settings. 97 | --[[ 98 | dots_at_end = -1 99 | is_invincible = true 100 | ]] 101 | 102 | ------------------------------------------------------------------------------- 103 | -- Define the PacSource class. 104 | -- This is a weird class because we have to wrap instead of subclass, as the 105 | -- object that acts like our base class is userdata instead of a table. 106 | ------------------------------------------------------------------------------- 107 | 108 | PacSource = {} ; PacSource.__index = PacSource 109 | 110 | function PacSource.new(filename) 111 | local pac_src = {} 112 | pac_src.src = love.audio.newSource(filename, 'static') 113 | pac_src.filename = filename 114 | return setmetatable(pac_src, PacSource) 115 | end 116 | 117 | -- This is a workaround for an infrequent but annoying audio bug where clips 118 | -- simply stop playing and need to be recreated as new objects. 119 | function PacSource:play() 120 | if self.src:isPaused() or self.src:isStopped() then 121 | self.src:play() 122 | end 123 | if self.src:isPaused() then 124 | -- Here is the workaround. Theoretically, this block should never happen. 125 | -- But it does. 126 | local is_looping = self.src:isLooping() 127 | self.src = love.audio.newSource(self.filename, 'static') 128 | self.src:setLooping(is_looping) 129 | self.src:play() 130 | end 131 | end 132 | 133 | function PacSource:pause() 134 | if not self.src:isPaused() and not self.src:isStopped() then 135 | self.src:pause() 136 | end 137 | end 138 | 139 | function PacSource:setLooping(should_loop) self.src:setLooping(should_loop) end 140 | function PacSource:isPaused() return self.src:isPaused() end 141 | function PacSource:setVolume(volume) self.src:setVolume(volume) end 142 | function PacSource:stop() self.src:stop() end 143 | function PacSource:rewind() self.src:rewind() end 144 | 145 | 146 | ------------------------------------------------------------------------------- 147 | -- Non-love functions. 148 | ------------------------------------------------------------------------------- 149 | 150 | function superdot_eaten() 151 | for k, c in pairs(characters) do 152 | if c.shape == 'ghost' then c.eaten = false end 153 | end 154 | super_mode_till = clock + 6.0 155 | add_to_score(40) -- An additional +10 is given for every dot. 156 | next_ghost_score = 200 157 | end 158 | 159 | -- Sets ghost_mode to either 'scatter' or 'pursue', based on a 26-second cycle, 160 | -- where the first 6 seconds are scatter, and the next 20 are pursue. 161 | function update_ghost_mode() 162 | local cycle_point = clock % 26 163 | if cycle_point < 6 then 164 | ghost_mode = 'scatter' 165 | else 166 | ghost_mode = 'pursue' 167 | end 168 | end 169 | 170 | -- The input x, y is the center of the dot in tile-based coordinates. 171 | function draw_one_dot(x, y, is_superdot) 172 | local dot_size = 2 173 | is_superdot = is_superdot or superdots[util.str({x, y})] 174 | if is_superdot then dot_size = 6 end 175 | local flash_rate = 0.2 -- In seconds. 176 | -- Don't draw superdots every other cycle. 177 | if is_superdot and math.floor(clock / flash_rate) % 2 == 1 and 178 | pause_till <= clock then 179 | return 180 | end 181 | draw.setColor(255, 220, 128) 182 | love.graphics.circle('fill', 183 | x * tile_size, 184 | y * tile_size, 185 | dot_size, 10) 186 | end 187 | 188 | function draw_dots() 189 | for k, v in pairs(dots) do draw_one_dot(v[1], v[2]) end 190 | end 191 | 192 | function draw_wall(x, y) 193 | love.graphics.setLineWidth(3) 194 | 195 | -- A shortcut for map[pt[1]][pt[2]]. 196 | function m(pt) 197 | if math.min(pt[1], pt[2]) < 1 then return 0 end 198 | if pt[1] > #map or pt[2] > #(map[1]) then return 0 end 199 | return map[pt[1]][pt[2]] 200 | end 201 | 202 | -- Avoid accidental line transparency. 203 | function line(x1, y1, x2, y2) 204 | love.graphics.line(math.floor(x1 + 0.5) + 0.5, math.floor(y1 + 0.5) + 0.5, 205 | math.floor(x2 + 0.5) + 0.5, math.floor(y2 + 0.5) + 0.5) 206 | end 207 | 208 | local w = 0.7 -- A parameter for how to draw the walls. 0.7 looks good. 209 | local ww = 1.0 - w 210 | local map_pt = {x, y} 211 | 212 | if m(map_pt) == 3 then 213 | -- Draw the ghost hotel door. 214 | draw.setColor(255, 200, 200, 255, {is_wall = true}) 215 | local z = w - 0.5 216 | local h = w * 0.2 217 | love.graphics.rectangle('fill', 218 | (x - z) * tile_size + 1, 219 | (y + (1 - h) / 2) * tile_size, 220 | (1 + 2 * z) * tile_size, 221 | tile_size * h) 222 | return 223 | end 224 | 225 | local wc = level.wall_color 226 | draw.setColor(wc.r, wc.g, wc.b, 255, {is_wall = true}) 227 | for coord = 1, 2 do for delta = -1, 1, 2 do 228 | local other_pt = {map_pt[1], map_pt[2]} 229 | other_pt[coord] = other_pt[coord] + delta 230 | local other = m(other_pt) 231 | if other ~= 1 then 232 | -- We choose w + ww = 1.0 for a weighted average. 233 | local c = {(w * map_pt[1] + ww * other_pt[1] + 0.5) * tile_size, 234 | (w * map_pt[2] + ww * other_pt[2] + 0.5) * tile_size} 235 | 236 | -- Find the differences from c to draw; longer if wall continues. 237 | local d = {{0, 0}, {0, 0}} 238 | for dd = -1, 1, 2 do 239 | local i = (dd + 3) / 2 -- This is 1, 2 as dd is -1, 1. 240 | local side = {map_pt[1], map_pt[2]} 241 | local normal = 3 - coord 242 | side[normal] = side[normal] + dd 243 | d[i][normal] = dd * ww * tile_size 244 | if m(side) == 1 then d[i][normal] = dd * w * tile_size end 245 | end 246 | 247 | line(c[1] + d[1][1], c[2] + d[1][2], 248 | c[1] + d[2][1], c[2] + d[2][2]) 249 | end 250 | end end -- Loop over coord, delta. 251 | end 252 | 253 | function lightning_strike() 254 | play_thunder() 255 | last_lightning = clock 256 | end 257 | 258 | function lightning_sequence() 259 | lightning_strike() 260 | events.add(0.2, lightning_strike) 261 | events.add(1.5, lightning_strike) 262 | events.add(math.random() * 10 + 6, lightning_sequence, 'lightning') 263 | end 264 | 265 | function stop_lightning() 266 | events.cancel('lightning') 267 | end 268 | 269 | function pts_hit_by_man_at_xy(x, y) 270 | local h = 0.45 -- Less than 0.5 to allow turns near intersections. 271 | local pts = {} 272 | for dx = -1, 1, 2 do for dy = -1, 1, 2 do 273 | table.insert(pts, {math.floor(x + dx * h), math.floor(y + dy * h)}) 274 | end end 275 | return pts 276 | end 277 | 278 | -- Returns a hash set of the dot pts nearby, whether or not a dot is there. 279 | function dots_hit_by_man_at_xy(x, y) 280 | local pts = pts_hit_by_man_at_xy(2 * x + 0.5, 2 * y + 0.5) 281 | local dots = {} 282 | for k, v in pairs(pts) do 283 | local pt = {v[1] / 2, v[2] / 2} 284 | dots[util.str(pt)] = pt 285 | end 286 | return dots 287 | end 288 | 289 | function xy_hits_a_wall(x, y, can_pass_hotel_door) 290 | local pts = pts_hit_by_man_at_xy(x, y) 291 | for k, v in pairs(pts) do 292 | if v[1] >= 1 and v[1] <= #map then 293 | local m = map[v[1]][v[2]] 294 | if m == 1 then return true end 295 | if m == 3 and not can_pass_hotel_door then return true end 296 | end 297 | end 298 | return false 299 | end 300 | 301 | function draw_lives_left() 302 | local char = Character.new('hero', 'yellow') 303 | char.y = 24 304 | char.always_draw = true 305 | char.is_fake = true 306 | for i = 1, lives_left - 1 do 307 | char.x = 0.5 + 1.2 * i 308 | char:draw() 309 | end 310 | end 311 | 312 | function play_start_screen_music() 313 | local ending = 1 314 | local betw_time = 0.12 315 | local offset = 0 -- Alternates between 0 and 1. 316 | 317 | function note_played(note) 318 | if note == 0 then return end 319 | 320 | -- Set up dir to either left or right based on offset. 321 | local dir = {offset * 2 - 1, 0} 322 | if note == 'c1' then 323 | dir = {0, 1} 324 | elseif note == 'c3' then 325 | dir = {0, -1} 326 | end 327 | character_dance(dir) 328 | 329 | offset = 1 - offset 330 | end 331 | 332 | function play() 333 | local last_note = 'c1' 334 | if ending == 2 then last_note = 'c3' end 335 | local s = {'c2', 0, 'c2', 0, 'g2', 0, 'g2', 0, 'c2', 0, 'c2', 'c2', 336 | 'g2', 0, 'g2', 0, 'c2', 0, 'g1', 0, 'c2', 0, 0, 'g1', 337 | 'c2', 0, 0, 0, last_note, 0, 0, 0} 338 | if game_mode == 'start screen' then 339 | start_song_id = notes.play_song(s, betw_time, play, note_played) 340 | end 341 | ending = 3 - ending 342 | end 343 | 344 | play() 345 | end 346 | 347 | function stop_start_screen_music() 348 | if start_song_id then 349 | notes.stop_song(start_song_id) 350 | end 351 | end 352 | 353 | function play_game_over_music() 354 | notes.play_song({'g2', 'g2', 'e2-', 'e2-', 'c2', 'c2', 'c2'}, 0.1) 355 | end 356 | 357 | function play_level_won_music() 358 | local song = {'c1', 'g1', 'c2', 'g2', 'c3', 0, 'g2', 0, {'c3', 'c4'}} 359 | notes.play_song(song, 0.15) 360 | end 361 | 362 | function level_won() 363 | message = 'Level Complete!' 364 | show_message_till = math.huge 365 | set_music('none') 366 | play_level_won_music() 367 | if level_num == num_levels then 368 | show_victory() 369 | else 370 | events.add(3, next_level) 371 | end 372 | characters = {} 373 | end 374 | 375 | function show_victory() 376 | message = 'You Win! w00t' 377 | game_ended() 378 | end 379 | 380 | -- There's a function for this since we might want to play up to 4 overlapping 381 | -- instances at once. 382 | function play_nomnom() 383 | local n = nomnomz[nomnomz_index] 384 | n:play() 385 | nomnomz_index = (nomnomz_index % 4) + 1 386 | end 387 | 388 | function play_thunder() 389 | local t = thunder[thunder_index] 390 | t:play() 391 | thunder_index = (thunder_index % 9) + 1 392 | end 393 | 394 | function check_for_hit() 395 | for k, character in pairs(characters) do 396 | if character ~= man and man:dist(character) < 0.5 then 397 | if character:is_weak() then 398 | play_nomnom() 399 | character.dead_till = math.huge 400 | character.eaten = true 401 | add_ghost_eaten_score(next_ghost_score, character.x, character.y) 402 | next_ghost_score = next_ghost_score * 2 403 | elseif not is_invincible then 404 | death_noise:play() 405 | lives_left = lives_left - 1 406 | message = 'oops' 407 | show_message_till = math.huge 408 | pause_till = math.huge 409 | 410 | characters = {man} 411 | 412 | death_anim_till = clock + death_anim_time 413 | life_start_time = pause_till 414 | set_weeoo(1) 415 | 416 | if lives_left == 0 then 417 | message = 'Game Over' 418 | events.add(1, play_game_over_music) 419 | game_ended() 420 | else 421 | events.add(death_anim_time, begin_play) 422 | end 423 | end 424 | end 425 | end 426 | end 427 | 428 | function game_ended() 429 | game_over = true 430 | save_hi_score() 431 | stop_lightning() 432 | 433 | function show_start_screen() 434 | pause_till = 0 435 | events.add(0.5, play_start_screen_music) 436 | set_game_mode('start screen') 437 | end 438 | 439 | events.add(3, show_start_screen) 440 | end 441 | 442 | function draw_message() 443 | if show_message_till < clock then return end 444 | draw.setColor(255, 255, 255) 445 | love.graphics.setFont(large_font) 446 | local t = 14 -- Tweak the positioning. 447 | love.graphics.printf(message, t, 23.25 * tile_size, 448 | 21 * tile_size - t, 'center') 449 | end 450 | 451 | function draw_score() 452 | draw.setColor(255, 255, 255) 453 | love.graphics.setFont(large_font) 454 | love.graphics.printf(score, 0, 23.25 * tile_size, 20 * tile_size, 'right') 455 | 456 | if hi_score == score then 457 | draw.setColor(255, 255, 0) 458 | end 459 | local x, y = 9, -5 460 | love.graphics.print('High Score', tile_size + x, y) 461 | love.graphics.printf(hi_score, tile_size, y, 19 * tile_size, 'right') 462 | end 463 | 464 | function add_ghost_eaten_score(points, x, y) 465 | add_to_score(points) 466 | local s = {score = points, 467 | x = (x - 0.4) * tile_size, 468 | y = (y - 0.3) * tile_size} 469 | local event_id = nil 470 | 471 | -- Register the score to disappear in 2 seconds. 472 | function remove_ghost_eaten_score() 473 | ghost_eaten_scores[event_id] = nil 474 | end 475 | event_id = events.add(2, remove_ghost_eaten_score) 476 | 477 | ghost_eaten_scores[event_id] = s 478 | end 479 | 480 | function draw_ghost_eaten_scores() 481 | love.graphics.setFont(small_font) 482 | draw.setColor(0, 255, 255) 483 | for k, v in pairs(ghost_eaten_scores) do 484 | love.graphics.print(v.score, v.x, v.y) 485 | end 486 | end 487 | 488 | function add_to_score(points) 489 | score = score + points 490 | if score > hi_score then hi_score = score end 491 | end 492 | 493 | -- Input is similar to {0, 1}, which would be a request to go right. 494 | function dir_request(dir) 495 | if dir == nil then return end 496 | if man == nil then return end 497 | if man:can_turn_right_now(dir) then 498 | man.dir = dir 499 | else 500 | man.next_dir = dir 501 | end 502 | end 503 | 504 | function sign(x) 505 | if x == 0 then return 0 end 506 | if x < 0 then return -1 end 507 | return 1 508 | end 509 | 510 | function check_jstick_if_present() 511 | if not jstick then return end 512 | joystick = love.joystick.getJoysticks()[1] 513 | x, y = joystick:getAxes() 514 | -- Discard low-volume movements. 515 | if math.max(math.abs(x), math.abs(y)) < 0.5 then return end 516 | -- Discretize the direction. 517 | if math.abs(x) < math.abs(y) then 518 | x = 0 519 | else 520 | y = 0 521 | end 522 | x, y = sign(x), sign(y) 523 | dir_request({x, y}) 524 | end 525 | 526 | -- Expects music to be one of 'none', 'weeoo', 'bwop', or 'runny'. 527 | function set_music(music) 528 | local clips = {weeoo = weeoo, bwop = bwop, runny = runny} 529 | for clip_name, clip in pairs(clips) do 530 | if clip then 531 | if music == clip_name then clip:play() else clip:pause() end 532 | end 533 | end 534 | end 535 | 536 | function update_audio() 537 | if play_wata_till <= clock then 538 | if not wata:isPaused() then 539 | wata:pause() 540 | end 541 | end 542 | 543 | if game_over or pause_till > clock then 544 | set_music('none') 545 | return 546 | end 547 | 548 | if super_mode_till > clock then 549 | for k, character in pairs(characters) do 550 | if character:is_dead() then 551 | set_music('runny') 552 | return 553 | end 554 | end 555 | set_music('bwop') 556 | return 557 | end 558 | 559 | set_music('weeoo') 560 | -- Speed up the weeoo over time. 561 | local music_speedup_cycle = 15 -- In seconds. 562 | local first_speedup = life_start_time + music_speedup_cycle 563 | next_music_speedup = math.max(next_music_speedup, first_speedup) 564 | if clock > next_music_speedup then 565 | local i = math.floor((clock - life_start_time) / music_speedup_cycle) + 1 566 | set_weeoo(i) 567 | next_music_speedup = next_music_speedup + music_speedup_cycle 568 | end 569 | end 570 | 571 | -- Input speed is an integer >= 1. If speed > 6, we still play speed 6. 572 | function set_weeoo(speed) 573 | if weeoo then weeoo:stop() end 574 | speed = math.min(speed, 6) 575 | local filename = 'audio/weeoo' .. speed .. '.ogg' 576 | weeoo = PacSource.new(filename) 577 | weeoo:setLooping(true) 578 | weeoo:setVolume(0.6) 579 | weeoo:play() 580 | end 581 | 582 | function next_level() 583 | level_num = level_num + 1 584 | setup_level() 585 | end 586 | 587 | -- Loads the level corresponding to level_num. 588 | function setup_level() 589 | local filename = 'level' .. level_num .. '.txt' 590 | level = levelreader.read(filename) 591 | map = level.map 592 | 593 | show_message_till = 0 594 | num_dots = 0 595 | 596 | superdots = util.hash_from_list(level.superdots) 597 | 598 | -- This will be a hash set of all dot locations. 599 | dots = {} 600 | 601 | -- Inner functions to help find the dot locations. 602 | -- The input x, y is the integer square location in tile coordinates. 603 | function add_dots(x, y) 604 | if map[x][y] ~= 0 then return end 605 | add_one_dot(x + 0.5, y + 0.5) 606 | if x + 1 <= #map and map[x + 1][y] == 0 then 607 | add_one_dot(x + 1, y + 0.5) 608 | end 609 | if y + 1 <= #(map[1]) and map[x][y + 1] == 0 then 610 | add_one_dot(x + 0.5, y + 1) 611 | end 612 | end 613 | function add_one_dot(x, y) 614 | dots[util.str({x, y})] = {x, y} 615 | num_dots = num_dots + 1 616 | end 617 | 618 | for x = 1, #map do for y = 1, #(map[1]) do add_dots(x, y) end end 619 | 620 | characters = {} 621 | local startup_time = 2.55 622 | events.add(1, show_character_preview) 623 | events.add(startup_time, begin_play) 624 | pause_till = math.huge 625 | local song = {{'c2', 'c3'}, 'c3', 'c3', 'c3', {'c2', 'e3'}, 0, 'c3', 626 | {'g1', 'd3'}, 0, 'c3', {'g1', 'd3'}, 'c3', {'c2', 'e3'}, 627 | 0, 'g2', 0, {'c1', 'c4'}} 628 | say_ready_till = clock + startup_time 629 | notes.play_song(song, 0.15) 630 | 631 | if level_num == 3 then 632 | events.add(5, lightning_sequence) 633 | end 634 | end 635 | 636 | function start_new_game() 637 | lives_left = start_lives 638 | game_over = false 639 | score = 0 640 | 641 | level_num = 1 642 | setup_level() 643 | end 644 | 645 | function setup_characters() 646 | characters = {} 647 | man = Character.new('hero', 'yellow') 648 | table.insert(characters, man) 649 | 650 | red = Character.new('ghost', 'red') 651 | table.insert(characters, red) 652 | 653 | table.insert(characters, Character.new('ghost', 'pink')) 654 | table.insert(characters, Character.new('ghost', 'blue')) 655 | table.insert(characters, Character.new('ghost', 'orange')) 656 | end 657 | 658 | -- This displays stationary characters in position before play starts. 659 | function show_character_preview() 660 | setup_characters() 661 | for k, c in pairs(characters) do 662 | c.dir = {0, 0} 663 | c.always_draw = true 664 | end 665 | end 666 | 667 | function begin_play() 668 | setup_characters() 669 | set_weeoo(1) 670 | pause_till = 0 671 | show_message_till = 0 672 | end 673 | 674 | -- This is similar to love.graphics.rectangle, except that the rectangle has 675 | -- rounded corners. r = radius of the corners, n ~ #points used in the polygon. 676 | function rounded_rectangle(mode, x, y, w, h, r, n) 677 | n = n or 20 -- Number of points in the polygon. 678 | if n % 4 > 0 then n = n + 4 - (n % 4) end -- Include multiples of 90 degrees. 679 | local pts, c, d, i = {}, {x + w / 2, y + h / 2}, {w / 2 - r, r - h / 2}, 0 680 | while i < n do 681 | local a = i * 2 * math.pi / n 682 | local p = {r * math.cos(a), r * math.sin(a)} 683 | for j = 1, 2 do 684 | table.insert(pts, c[j] + d[j] + p[j]) 685 | if p[j] * d[j] <= 0 and (p[1] * d[2] < p[2] * d[1]) then 686 | d[j] = d[j] * -1 687 | i = i - 1 688 | end 689 | end 690 | i = i + 1 691 | end 692 | love.graphics.polygon(mode, pts) 693 | end 694 | 695 | function draw_ready_text() 696 | if say_ready_till <= clock then return end 697 | 698 | -- Draw the rounded rect background. 699 | local x, y, w, h = 206, 361, 176, 75 700 | draw.setColor(0, 0, 0, 200) 701 | rounded_rectangle('fill', x, y, w, h, 10, 30) 702 | draw.setColor(50, 50, 50) 703 | rounded_rectangle('line', x, y, w, h, 10, 30) 704 | 705 | -- Draw the text. 706 | love.graphics.setFont(large_font) 707 | draw.setColor(200, 200, 200) 708 | love.graphics.printf('Level ' .. level_num, x + 6, y + 2, w, 'center') 709 | draw.setColor(255, 200, 0) 710 | love.graphics.printf('Ready!', x + 6, y + 35, w, 'center') 711 | end 712 | 713 | function character_dance(dir) 714 | local y = 16 715 | local tw = math.floor(love.graphics.getWidth() / tile_size) 716 | for k, c in pairs(characters) do 717 | local j = k 718 | if j == 1 or j == 3 then 719 | j = 4 - j -- Map 1 -> 3, 3 -> 1 to put the hero in the middle. 720 | end 721 | local x = j * 2 + tw / 2 - 6 722 | c.x = x + 0.4 * dir[1] 723 | c.y = y + 0.4 * dir[2] 724 | c.dir = dir 725 | end 726 | end 727 | 728 | function setup_start_screen_characters() 729 | characters = {} 730 | local y = 25 731 | local colors = {'yellow', 'red', 'pink', 'blue', 'orange'} 732 | local tw = math.floor(love.graphics.getWidth() / tile_size) 733 | 734 | for k, color in pairs(colors) do 735 | local shape = 'ghost' 736 | local j = k 737 | if j == 1 or j == 3 then 738 | j = 4 - j -- Map 1 -> 3, 3 -> 1 to put the hero in the middle. 739 | end 740 | local x = j * 2 + tw / 2 - 6 741 | if color == 'yellow' then shape = 'hero' end 742 | local c = Character.new(shape, color) 743 | c.x = x 744 | c.y = y 745 | table.insert(characters, c) 746 | end 747 | character_dance({0, 0}) 748 | end 749 | 750 | function draw_start_text() 751 | local w = love.graphics.getWidth() 752 | local dy = -50 753 | love.graphics.setFont(large_font) 754 | draw.setColor(255, 255, 255) 755 | love.graphics.printf('Start', 0, 400 + dy, w, 'center') 756 | 757 | if math.floor(clock / 0.3) % 2 == 0 then 758 | draw.setColor(100, 100, 100) 759 | else 760 | draw.setColor(0, 0, 0) 761 | end 762 | local vertices = {568, 409 + dy, 583, 417 + dy, 568, 425 + dy} 763 | love.graphics.polygon('fill', vertices) 764 | end 765 | 766 | function draw_controls() 767 | local w = love.graphics.getWidth() 768 | local x, y = 528, 500 769 | draw.setColor(255, 255, 255) 770 | if jstick then 771 | love.graphics.draw(jstick_img, x, y) 772 | local alpha = 255 * (math.sin(clock * 5) + 1) / 2 773 | draw.setColor(alpha, alpha, alpha) 774 | love.graphics.draw(jstick_overlay, x, y) 775 | else 776 | love.graphics.draw(keybd_img, (w - 200) / 2, y) 777 | end 778 | 779 | love.graphics.setFont(small_font) 780 | draw.setColor(255, 255, 255) 781 | if jstick then 782 | love.graphics.print('Controls', 578, 631) 783 | else 784 | love.graphics.printf('Controls', 0, 631, w, 'center') 785 | draw.setColor(80, 80, 80) 786 | love.graphics.printf('no gamepad detected', 0, 651, w, 'center') 787 | end 788 | end 789 | 790 | function load_hi_score() 791 | if not love.filesystem.exists('hi_score') then 792 | hi_score = 1000 793 | return 794 | end 795 | local file = love.filesystem.newFile('hi_score') 796 | file:open('r') 797 | local score_str = file:read() 798 | hi_score = tonumber(score_str) 799 | file:close() 800 | end 801 | 802 | function save_hi_score() 803 | local file = love.filesystem.newFile('hi_score') 804 | file:open('w') 805 | file:write(tostring(hi_score)) 806 | file:close() 807 | end 808 | 809 | function set_game_mode(new_mode) 810 | game_mode = new_mode 811 | if game_mode == 'start screen' then 812 | setup_start_screen_characters() 813 | love.draw = draw_start_screen 814 | love.update = update_start_screen 815 | love.keypressed = keypressed_start_screen 816 | love.joystickpressed = joystickpressed_start_screen 817 | elseif game_mode == 'playing' then 818 | love.graphics.setFont(small_font) 819 | love.draw = draw_playing 820 | love.update = update_playing 821 | love.keypressed = keypressed_playing 822 | love.joystickpressed = joystickpressed_playing 823 | end 824 | end 825 | 826 | 827 | ------------------------------------------------------------------------------- 828 | -- Start screen key functions. 829 | ------------------------------------------------------------------------------- 830 | 831 | function draw_start_screen() 832 | 833 | -- Draw the logo. 834 | local w, h = love.graphics.getWidth(), love.graphics.getHeight() 835 | local logo_w = logo:getWidth() 836 | draw.setColor(255, 255, 255) 837 | love.graphics.draw(logo, math.floor((w - logo_w) / 2), 100) 838 | 839 | -- Draw the dot border. 840 | local tw, th = math.floor(w / tile_size) - 1, math.floor(h / tile_size) - 1 841 | superdots = {{.5, .5}, {.5, th + .5}, {tw + .5, .5}, {tw + .5, th + .5}} 842 | superdots = util.hash_from_list(superdots) 843 | for x = 0, tw do for y = 0, th, th do draw_one_dot(x + 0.5, y + 0.5) end end 844 | for x = 0, tw, tw do for y = 0, th do draw_one_dot(x + 0.5, y + 0.5) end end 845 | 846 | -- Draw the characters. 847 | for k, c in pairs(characters) do c:draw() end 848 | 849 | draw_start_text() 850 | draw_controls() 851 | end 852 | 853 | function update_start_screen(dt) 854 | clock = clock + dt 855 | events.update(dt) 856 | end 857 | 858 | function keypressed_start_screen(key) 859 | stop_start_screen_music() 860 | start_new_game() 861 | set_game_mode('playing') 862 | end 863 | 864 | function joystickpressed_start_screen(joystick, button) 865 | keypressed_start_screen() -- Start a game. 866 | end 867 | 868 | ------------------------------------------------------------------------------- 869 | -- Playing key functions. 870 | ------------------------------------------------------------------------------- 871 | 872 | function draw_playing() 873 | love.graphics.translate(345, 15) 874 | 875 | -- Draw walls. 876 | for x = 1, #map do for y = 1, #(map[1]) do 877 | if map[x][y] == 1 or map[x][y] == 3 then 878 | draw_wall(x, y) 879 | end 880 | end end -- Loop over x, y. 881 | 882 | -- Draw dots. 883 | for k, v in pairs(dots) do draw_one_dot(v[1], v[2]) end 884 | 885 | for k, character in pairs(characters) do 886 | character:draw() 887 | end 888 | 889 | draw_lives_left() 890 | draw_message() 891 | draw_ready_text() 892 | draw_score() 893 | draw_ghost_eaten_scores() 894 | end 895 | 896 | function update_playing(dt) 897 | -- Uncomment this bit to see what kind of dt's we're getting. 898 | --[[ 899 | local s = '' 900 | for i = 1, dt * 1000 do s = s .. '-' end 901 | print('dt=' .. dt .. s) 902 | ]] 903 | 904 | local max_dt = 0.06 -- Useful to avoid bugs that happen for large dt's. 905 | dt = math.min(dt, max_dt) 906 | clock = clock + dt 907 | 908 | check_jstick_if_present() 909 | update_ghost_mode() 910 | update_audio() 911 | for k, character in pairs(characters) do 912 | character:update(dt) 913 | end 914 | check_for_hit() 915 | events.update(dt) 916 | end 917 | 918 | function keypressed_playing(key) 919 | local dirs = {up = {0, -1}, down = {0, 1}, left = {-1, 0}, right = {1, 0}} 920 | dir_request(dirs[key]) 921 | end 922 | 923 | function joystickpressed_playing(joystick, button) 924 | -- These button numbers work for the PS3 controller. 925 | local dirs = {[5] = {0, -1}, [6] = {1, 0}, [7] = {0, 1}, [8] = {-1, 0}} 926 | dir_request(dirs[button]) 927 | end 928 | 929 | 930 | ------------------------------------------------------------------------------- 931 | -- Love functions. 932 | ------------------------------------------------------------------------------- 933 | 934 | function love.load() 935 | level = levelreader.read('level1.txt') 936 | load_hi_score() 937 | 938 | small_font = love.graphics.newFont('8bitoperator_jve.ttf', 16) 939 | large_font = love.graphics.newFont('8bitoperator_jve.ttf', 32) 940 | 941 | logo = love.graphics.newImage('img/pacpac_logo.png') 942 | jstick_img = love.graphics.newImage('img/gamepad.png') 943 | jstick_overlay = love.graphics.newImage('img/gamepad_overlay.png') 944 | keybd_img = love.graphics.newImage('img/arrow_keys.png') 945 | 946 | open_noise = PacSource.new('audio/open.ogg') 947 | open_noise:setVolume(0.5) 948 | open_noise:play() 949 | 950 | wata = PacSource.new('audio/watawata.ogg') 951 | wata:setLooping(true) 952 | bwop = PacSource.new('audio/bwop.ogg') 953 | bwop:setLooping(true) 954 | death_noise = PacSource.new('audio/death.ogg') 955 | death_noise:setVolume(0.3) 956 | for i = 1, 4 do 957 | local n = PacSource.new('audio/nomnom.ogg') 958 | n:setVolume(0.4) 959 | table.insert(nomnomz, n) 960 | end 961 | runny = PacSource.new('audio/runny.ogg') 962 | runny:setLooping(true) 963 | runny:setVolume(0.08) 964 | for i = 1, 9 do 965 | local t = PacSource.new('audio/thunder.ogg') 966 | table.insert(thunder, t) 967 | end 968 | 969 | events.add(0.5, play_start_screen_music) 970 | 971 | jstick = (love.joystick.getJoystickCount() > 0) 972 | if jstick then 973 | local joysticks = love.joystick.getJoysticks() 974 | for i, joystick in ipairs(joysticks) do 975 | print('Detected ' .. joystick:getName()) 976 | end 977 | else 978 | print('No gamepad detected.') 979 | end 980 | 981 | set_game_mode('start screen') 982 | end 983 | --------------------------------------------------------------------------------