diff --git a/data/images/duckie.png b/data/images/duckie.png index 92ad840..91c826b 100644 Binary files a/data/images/duckie.png and b/data/images/duckie.png differ diff --git a/data/scripts/classes/duck.lua b/data/scripts/classes/duck.lua new file mode 100644 index 0000000..0b2cce9 --- /dev/null +++ b/data/scripts/classes/duck.lua @@ -0,0 +1,104 @@ +local Vector3 = require "types.vector3" +local Signal = require "types.signal" + +local TEXTURE = "images/duckie.png" + +local States = { + IDLE = 1, + WANDER = 2, + CHASE = 3, +} + +local WANDER_TIME = 1.0 +local IDLE_TIME = 1.0 + +local Duck = { + position = {}, + velocity = {}, + direction = {}, + + target = nil, + state = States.IDLE, + + wander_timer = 0, + + speed = 0.02, + chase_speed = 0.04, + accel = 0.5, + + STATES = States, + + AteFeed = Signal.new() +} + +Duck.__index = Duck + +function Duck.new(position) + local d = util.shallow_copy(Duck) + d.position = position + d.velocity = Vector3() + d.direction = Vector3(0, 0, -1):rotated(Vector3.UP, util.random_float(-math.pi, math.pi)) + + return setmetatable(d, Duck) +end + +function Duck:tick(ctx) + if self.state == States.IDLE then + self:idle(ctx.frame_duration) + elseif self.state == States.WANDER then + self:wander(ctx.frame_duration) + elseif self.state == States.CHASE then + self:chase(ctx.frame_duration) + end + draw_billboard{position = self.position, size = {x = 0.4, y = 0.4}, texture = TEXTURE} +end + +function Duck:idle(delta) + self.wander_timer = self.wander_timer + delta + if self.wander_timer >= IDLE_TIME then + self.state = States.WANDER + self.wander_timer = 0 + self.direction = Vector3(0, 0, -1):rotated(Vector3.UP, math.random() * (math.pi * 2.0)) + return + end +end + +function Duck:wander(delta) + self.wander_timer = self.wander_timer + delta + if self.wander_timer >= WANDER_TIME then + self.state = States.IDLE + self.wander_timer = 0 + return + end + + local target_vel = self.direction * self.speed + self.velocity = self.velocity:lerp(target_vel, self.accel) + self.position = self.position + self.velocity +end + +function Duck:chase(delta) + local hpos = self.position:horizontal() + local fhpos = self.target.position:horizontal() + + local dist = hpos:distance_to(fhpos) + if dist <= 0.1 then + self.AteFeed:emit(self.target) + self.wander_timer = 0 + self.state = States.IDLE + self.target = nil + else + local dir = self.position:horizontal():direction_to(self.target.position:horizontal()) + local target_vel = dir * self.chase_speed + self.velocity = self.velocity:lerp(target_vel, self.accel) + self.position = self.position + self.velocity + end +end + +function Duck:start_chase(feed) + if self.state == States.CHASE then return end + self.state = States.CHASE + self.target = feed + feed.occupied = true +end + +return Duck \ No newline at end of file diff --git a/data/scripts/classes/feed.lua b/data/scripts/classes/feed.lua index b4cd07f..0a13428 100644 --- a/data/scripts/classes/feed.lua +++ b/data/scripts/classes/feed.lua @@ -7,6 +7,7 @@ local Feed = { direction = {}, velocity = {}, landed = false, + occupied = false, speed = 0.15, } diff --git a/data/scripts/game.lua b/data/scripts/game.lua index ca4b477..2b38371 100644 --- a/data/scripts/game.lua +++ b/data/scripts/game.lua @@ -1,12 +1,29 @@ util = require "util" local player = require "classes.player" local Vector3 = require "types.vector3" +local List = require "types.list" local Feed = require "classes.feed" -local feed = {} +local feed = List() + +local Duck = require "classes.duck" +local ducks = List{Duck.new(Vector3(0, 0.4, 3))} local function create_feed(position, direction) - table.insert(feed, Feed.new(position, direction)) + local f = Feed.new(position, direction) + feed:push(f) + local eligible_ducks = ducks:filter( + function (duck) + return duck.state ~= Duck.STATES.CHASE + end + ) + if #eligible_ducks == 0 then return end + eligible_ducks[1]:start_chase(f) + f.occupied = true +end + +local function delete_feed(f) + util.list_remove_value(feed, f) end -- called every frame, with constant delta time @@ -18,16 +35,23 @@ function game_tick() capture = false, } player.ThrowPressed:connect(create_feed) + for _, v in ipairs(ducks) do + v.AteFeed:connect(delete_feed) + end end ctx.mouse_capture = ctx.udata.capture input_action{name = "toggle_mouse", control = "ESCAPE"} - + if input_action_just_pressed{name = "toggle_mouse"} then ctx.udata.capture = not ctx.udata.capture end - for _, v in pairs(feed) do + for _, v in ipairs(feed) do + v:tick(ctx) + end + + for _, v in ipairs(ducks) do v:tick(ctx) end diff --git a/data/scripts/types/list.lua b/data/scripts/types/list.lua new file mode 100644 index 0000000..219bb69 --- /dev/null +++ b/data/scripts/types/list.lua @@ -0,0 +1,98 @@ +---@class List +local List = {} + +local function reduce(list, f, init) + local acc = init + for i, v in ipairs(list) do + acc = f(v, acc, i) + end + return acc +end + +local function filter(list, predicate) + return reduce(list, function(el, acc, i) + if predicate(el) then + table.insert(acc, el) + end + return acc + end, List.create{}) +end + +local function shallow_copy(t) + local t2 = {} + for k, v in ipairs(t) do + t2[k] = v + end + return t2 +end + +List.__index = List +setmetatable(List, { + __call = function(self, ...) + local args = {...} + if #args == 0 then + return List.create() + end + + if type(args[1]) == "table" then + return List.create(args[1]) + end + return List.create(args) + end +}) + +---Constructs a new list from the given table. +---@param from table? +---@return List +function List.create(from) + from = from or {} + local l = shallow_copy(from) + return setmetatable(l, List) +end + +function List:__tostring() + local s = "List(" .. table.concat(self, ", ", 1, #self) .. ")" + return s +end + +---Appends v to the end of the list. +---@param v any +function List:push(v) + table.insert(self, v) +end + +---Removes the last element in the list and returns it. +---@return any +function List:pop() + return table.remove(self, #self) +end + +---Reduce. +---@param f function called with element, accumulator, index +---@param init any initial value of accumulator +---@return any +function List:reduce(f, init) + return reduce(self, f, init) +end + +---Returns a new List of all elements of this list that match the predicate function. +---@param predicate function called with element +---@return List +function List:filter(predicate) + return filter(self, predicate) +end + +---Returns the index of value, if it exists in the list, -1 otherwise. +---@param value any +---@return integer +function List:find(value) + local idx = -1 + for i, v in ipairs(self) do + if v == value then + idx = i + break + end + end + return idx +end +return List \ No newline at end of file diff --git a/data/scripts/util.lua b/data/scripts/util.lua index 1cad958..a4497fc 100644 --- a/data/scripts/util.lua +++ b/data/scripts/util.lua @@ -116,5 +116,13 @@ function util.clamp(v, min, max) return math.max(math.min(v, max), min) end +---returns a random float in range (min, max) +---@param min number +---@param max number +---@return number +function util.random_float(min, max) + return min + math.random() * (max - min) +end + return util \ No newline at end of file