--!name Pocket Pet
--!icon game
--!category Games
--
-- A virtual pet for eOctavo. Time only passes while the app is open: close
-- it and your pet waits for you. Hatch the egg, then keep it fed, happy,
-- clean and rested and it grows from baby to child to adult (how it turns
-- out depends on the care it got). Neglect it too long and it's gone.
--
-- L/R pick a care action, OK does it, UP shows its stats.
-- Art: pet.art (tools/make_pet_art.py). The board tiles are an eOctavo
-- extension, so this is an eOctavo app.

local TICK = 15000     -- one pet moment
local FAST = 500       -- animation frames
local DAY = 40         -- moments per pet day (10 minutes open)
local HATCH = 6
local CHILD_AT, ADULT_AT = DAY, 3 * DAY
local NEGLECT_MAX = 48 -- moments of neglect before it's gone

local NAMES = {"Mochi", "Pip", "Bean", "Tofu", "Nori", "Pebble", "Momo", "Kiwi", "Biscuit", "Dumpling", "Sprout",
  "Waffle", "Noodle", "Pickle", "Button", "Clover"}
local ACTIONS = {
  {id = "meal", tile = "m", sel = "M", label = "Meal", about = "fills a hunger heart"},
  {id = "snack", tile = "s", sel = "S", label = "Snack", about = "a joy heart, but fattening"},
  {id = "play", tile = "g", sel = "G", label = "Play", about = "guess which way it looks"},
  {id = "clean", tile = "c", sel = "C", label = "Clean", about = "tidy up the mess"},
  {id = "meds", tile = "d", sel = "D", label = "Medicine", about = "cures sickness"},
  {id = "light", tile = "l", sel = "L", label = "Light", about = "bedtime, or wake up"},
}
local STAGE_NAME = {"Baby", "Child", "Adult"}
local FIELDS = {"stage", "age", "hunger", "joy", "energy", "poop", "sick", "asleep", "light", "weight", "care",
  "neglect", "snacks", "poopIn", "hAcc", "jAcc", "eAcc", "sAcc", "dead", "gen", "hatch"}
local BOOL = {sick = true, asleep = true, light = true, dead = true}

local p          -- the pet
local sel = 1
local msg = ""
local msgAge = 0
local mood = nil -- "happy": shown until the next moment
local frame = 1
local pose = "idle1" -- what it's doing this moment: idle1 idle2 walkl walkr
local anim = nil     -- {frames, i, after}: frames shown FAST apart
local interval = TICK
local clock = 0      -- ms toward the next moment
local POSES = {"idle1", "idle2", "idle1", "walkl", "walkr"}
local CHEER = {"happy1", "happy2", "happy1", "happy2"}
local info = false
local game = nil -- {round, wins, res, look, done}
local seed = 1

local function newPet(gen)
  return {stage = 0, form = "a", age = 0, hunger = 3, joy = 3, energy = 4, poop = 0, sick = false, asleep = false,
    light = true, weight = 5, care = 0, neglect = 0, snacks = 0, poopIn = 0, hAcc = 0, jAcc = 0, eAcc = 0,
    sAcc = 0, dead = false, gen = gen or 1, hatch = 0, name = "Egg"}
end

local function say(s)
  msg = s
  msgAge = 0
end

local function setTick(ms)
  interval = snail.tick(ms) or ms
end

-- Plays frames (sprite suffixes) quickly, then shows `after` until the next moment.
local function play(frames, after)
  anim = {frames = frames, i = 1, after = after}
  setTick(FAST)
end

local function cheer() play(CHEER, "happy") end

local function up(v) return v < 4 and v + 1 or 4 end
local function down(v) return v > 0 and v - 1 or 0 end

local function save()
  local t = {}
  for i, f in ipairs(FIELDS) do
    local v = p[f]
    if v == true then v = 1 elseif v == false then v = 0 end
    t[i] = v
  end
  t[#t + 1] = p.form
  t[#t + 1] = p.name
  t[#t + 1] = sel
  t[#t + 1] = seed
  snail.save(table.concat(t, " "))
end

local function load()
  local s = snail.load()
  if not s then return false end
  local parts = {}
  for w in s:gmatch("%S+") do parts[#parts + 1] = w end
  local n = #FIELDS
  if #parts < n + 4 then return false end
  p = newPet(1)
  for i, f in ipairs(FIELDS) do
    local v = math.tointeger(tonumber(parts[i])) or 0
    if BOOL[f] then p[f] = v ~= 0 else p[f] = v end
  end
  p.form = parts[n + 1] == "z" and "z" or "a"
  p.name = parts[n + 2]
  sel = math.tointeger(tonumber(parts[n + 3])) or 1
  if sel < 1 or sel > #ACTIONS then sel = 1 end
  seed = math.tointeger(tonumber(parts[n + 4])) or 1
  return true
end

-- One moment passes.
local function step()
  if p.dead then return end
  if p.stage == 0 then
    p.hatch = p.hatch + 1
    if p.hatch >= HATCH then
      math.randomseed(seed + p.hatch)
      p.stage = 1
      p.name = NAMES[math.random(#NAMES)]
      say(p.name .. " hatched! Say hello.")
      cheer()
    elseif p.hatch == HATCH - 2 then
      say("The egg is wobbling...")
    end
    return
  end
  p.age = p.age + 1
  if p.age % DAY == 0 and p.weight > 4 then p.weight = p.weight - 1 end

  if p.asleep then
    p.hAcc = p.hAcc + 1
    if p.light then p.jAcc = p.jAcc + 3 end
    p.eAcc = p.eAcc + 1
    if p.eAcc >= 4 then
      p.eAcc = 0
      p.energy = up(p.energy)
    end
    if p.energy >= 4 then
      p.asleep, p.light, p.eAcc = false, true, 0
      say("Good morning, " .. p.name .. "!")
    end
  else
    p.hAcc = p.hAcc + 3
    p.jAcc = p.jAcc + (p.sick and 4 or 2)
    p.eAcc = p.eAcc + 1
    if p.eAcc >= 24 then
      p.eAcc = 0
      p.energy = down(p.energy)
      if p.energy == 0 then
        p.asleep = true
        say(p.name .. " fell asleep. Turn off the light!")
      elseif p.energy == 1 then
        say(p.name .. " is getting sleepy.")
      end
    end
  end

  if p.hAcc >= 48 then
    p.hAcc = 0
    p.hunger = down(p.hunger)
    if p.hunger == 0 then
      p.care = p.care + 1
      say(p.name .. " is starving!")
    elseif p.hunger == 1 then
      say(p.name .. " is hungry.")
    end
  end
  if p.jAcc >= 24 then
    p.jAcc = 0
    p.joy = down(p.joy)
    if p.joy == 0 then
      p.care = p.care + 1
      say(p.name .. " is miserable.")
    elseif p.joy == 1 then
      say(p.name .. " is bored.")
    end
  end

  if p.poopIn > 0 then
    p.poopIn = p.poopIn - 1
    if p.poopIn == 0 then
      p.poop = up(p.poop)
      say(p.name .. " made a mess.")
    end
  end
  if p.snacks > 0 then
    p.sAcc = p.sAcc + 1
    if p.sAcc >= 20 then
      p.sAcc = 0
      p.snacks = p.snacks - 1
    end
  end
  if not p.sick and p.poop >= 3 and math.random() < 0.15 then
    p.sick = true
    say(p.name .. " got sick. Clean up and give medicine.")
  end

  if p.hunger == 0 or p.joy == 0 or p.sick or p.poop >= 4 then
    p.neglect = p.neglect + 1
  else
    p.neglect = p.neglect > 2 and p.neglect - 2 or 0
  end
  if p.neglect == NEGLECT_MAX // 2 then say(p.name .. " needs you!") end
  if p.neglect >= NEGLECT_MAX then
    p.dead, p.asleep = true, false
    say(p.name .. " has gone to the stars.")
    return
  end

  if p.stage == 1 and p.age >= CHILD_AT then
    p.stage = 2
    cheer()
    say(p.name .. " grew into a child!")
  elseif p.stage == 2 and p.age >= ADULT_AT then
    p.stage = 3
    p.form = (p.care <= 3 and p.weight <= 20) and "a" or "z"
    cheer()
    say(p.form == "a" and p.name .. " grew up healthy and proud!" or p.name .. " grew up... a bit rough.")
  end
end

local function act(a)
  if p.stage == 0 then
    say("The egg is warm. It hatches while the app is open.")
    return
  end
  if p.asleep and a ~= "light" then
    say("Shh... " .. p.name .. " is asleep.")
    return
  end
  if a == "meal" then
    if p.hunger >= 4 then
      say(p.name .. " is full.")
      return
    end
    p.hunger = up(p.hunger)
    p.weight = p.weight + 1
    play({"eat1", "eat2", "eat3"}, "happy")
    say("Yum!")
    if p.poopIn == 0 then p.poopIn = math.random(6, 12) end
  elseif a == "snack" then
    p.joy = up(p.joy)
    p.weight = p.weight + 2
    p.snacks = p.snacks + 1
    play({"snack1", "snack2", "snack3"}, "happy")
    if p.snacks >= 4 and not p.sick and math.random() < 0.5 then
      p.sick = true
      say("Too many sweets! " .. p.name .. " feels sick.")
    else
      say("A treat! " .. p.name .. " loves it.")
    end
  elseif a == "play" then
    if p.sick then
      say(p.name .. " is too sick to play.")
      return
    end
    game = {round = 1, wins = 0, res = {}}
    say("")
  elseif a == "clean" then
    if p.poop == 0 then
      say("All clean already.")
      return
    end
    p.poop = 0
    cheer()
    say("Sparkling clean!")
  elseif a == "meds" then
    if not p.sick then
      p.joy = down(p.joy)
      say(p.name .. " isn't sick. Yuck!")
      return
    end
    p.sick = false
    say(p.name .. " feels better.")
  elseif a == "light" then
    p.eAcc = 0
    if p.asleep and p.light then
      p.light = false
      say("Lights out. Sweet dreams.")
    elseif p.asleep then
      p.asleep, p.light = false, true
      if p.energy < 3 then
        p.joy = down(p.joy)
        say(p.name .. " woke up grumpy.")
      else
        say("Rise and shine!")
      end
    else
      p.asleep, p.light = true, false
      say("Good night, " .. p.name .. ".")
    end
  end
end

local function guess(dir)
  local look = math.random(2) == 1 and "l" or "r"
  local win = (dir == "left") == (look == "l")
  game.look = look
  if win then game.wins = game.wins + 1 end
  game.res[#game.res + 1] = win and "v" or "x"
  if game.round < 5 then
    game.round = game.round + 1
    say(win and "Right!" or "Nope!")
    return
  end
  game.done = true
  p.energy = down(p.energy)
  local w = game.wins
  if w >= 3 then
    p.joy = w == 5 and up(up(p.joy)) or up(p.joy)
    if p.weight > 4 then p.weight = p.weight - 1 end
    cheer()
    say(string.format("%d of 5! %s had fun.", w, p.name))
  else
    say(string.format("Only %d of 5. Maybe next time.", w))
  end
end

function start()
  local ok = load()
  math.randomseed(seed)
  if not ok then
    p = newPet(1)
    say("A new egg! It only grows while this app is open.")
  end
  snail.ink("fast")
  setTick(TICK)
end

-- A pet moment: time passes and it does something new.
local function moment()
  frame = 3 - frame
  pose = POSES[math.random(#POSES)]
  mood = nil
  msgAge = msgAge + 1
  if msgAge > 2 then msg = "" end
  step()
  save()
end

function tick()
  clock = clock + interval
  if anim then
    anim.i = anim.i + 1
    if anim.i > #anim.frames then
      mood = anim.after
      anim = nil
      setTick(TICK)
    end
  end
  if clock >= TICK then
    clock = 0
    moment()
  end
end

function key(k)
  seed = (seed * 31 + #k + p.age) % 2147483647
  if info then
    info = false
  elseif game then
    if game.done then
      game = nil
      say("")
    elseif k == "left" or k == "right" then
      guess(k)
    elseif k == "ok" or k == "top" then
      game = nil
      say("Maybe later.")
    end
  elseif p.dead then
    if k == "ok" then
      p = newPet(p.gen + 1)
      say("A new egg. Keep it company.")
    end
  elseif k == "left" then
    sel = sel == 1 and #ACTIONS or sel - 1
  elseif k == "right" then
    sel = sel % #ACTIONS + 1
  elseif k == "ok" then
    act(ACTIONS[sel].id)
  elseif k == "up" or k == "down" then
    info = true
  end
  save()
end

local function hearts(n) return string.rep("h", n) .. string.rep("e", 4 - n) end

local function sprite()
  if p.dead then return "ghost" .. frame end
  if p.stage == 0 then return p.hatch >= HATCH - 1 and "egg3" or "egg" .. frame end
  local s = p.stage == 1 and "b" or p.stage == 2 and "c" or p.form
  local m
  if anim then m = anim.frames[anim.i]
  elseif p.asleep then m = "sleep" .. frame
  elseif game and game.look then m = "look" .. game.look
  elseif mood == "happy" then m = "happy2"
  elseif p.sick then m = "sick"
  elseif p.hunger <= 1 or p.joy <= 1 then m = "sad"
  else m = pose end
  return s .. "_" .. m
end

local function board()
  local mess = string.rep("p", p.poop < 2 and p.poop or 2)
  local last = ".."
  if game then
    for i = 1, 5 do last = last .. (game.res[i] or "q") end
    last = last .. "..."
  else
    for i, a in ipairs(ACTIONS) do last = last .. (i == sel and a.sel or a.tile) end
    last = last .. ".."
  end
  return "..F" .. hearts(p.hunger) .. mess .. string.rep(".", 3 - #mess) .. "/" ..
    "..J" .. hearts(p.joy) .. (p.sick and "k" or ".") .. "../" ..
    "..N" .. hearts(p.energy) .. (p.asleep and "z" or ".") .. "../" .. last
end

local function drawInfo()
  snail.title(p.stage == 0 and "An egg" or p.name)
  if p.stage == 0 then
    snail.text("Not hatched yet.")
  else
    snail.text(string.format("%s, day %d", p.dead and "Gone" or STAGE_NAME[p.stage], p.age // DAY + 1))
    snail.text(string.format("Weight: %d lb", p.weight))
    snail.text(string.format("Hunger %d/4   Joy %d/4   Energy %d/4", p.hunger, p.joy, p.energy))
    snail.text(string.format("Care mistakes: %d", p.care))
  end
  snail.text(string.format("Generation: %d", p.gen))
  snail.rule()
  snail.small("Time only passes while Pocket Pet is open. A pet day is 10 minutes.")
  snail.small("Keep the hearts up and clean up messes.")
  snail.small("Give medicine when it's sick. Turn the light off when it sleeps.")
  snail.small("Hungry, miserable, sick or messy for too long and it leaves for the stars.")
  snail.hint("BACK close")
end

function draw()
  if info then
    drawInfo()
    return
  end
  if p.stage == 0 then
    snail.title("An egg")
  else
    snail.title(p.name)
    if not p.dead then
      snail.small(string.format("%s  -  day %d  -  %d lb", STAGE_NAME[p.stage], p.age // DAY + 1, p.weight))
    end
  end
  snail.center(true)
  snail.image(sprite())
  snail.center(false)
  if p.stage > 0 and not p.dead then snail.board(board()) end
  snail.center(true)
  local line = msg
  if line == "" then
    if p.dead then line = "Thank you for looking after " .. p.name .. "."
    elseif p.stage == 0 then line = "Keep it company. It hatches while the app is open."
    elseif game then line = string.format("Round %d of 5: which way will %s look?", game.round, p.name)
    else line = ACTIONS[sel].label .. ": " .. ACTIONS[sel].about end
  end
  snail.text(line)
  snail.center(false)
  if p.dead then
    snail.hint("OK new egg")
  elseif game then
    snail.hint(game.done and "OK done" or "L/R guess  OK stop")
  elseif p.stage == 0 then
    snail.hint("UP stats")
  else
    snail.hint("OK " .. ACTIONS[sel].label .. "  L/R pick  UP stats")
  end
end
