polyflux/game/init.lua

local evloop = require "evloop"
local playfield = require "game.playfield"
local tetrominoes = require "game.tetrominoes"
local heav_optimal_shapes = require "game.heav_optimal_shapes"
local gfx = require "game.gfx"
local sfx = require "game.sfx"
local music = require "game.music"
local bag = require "game.bag"

local M = {}
M.__index = M

local pieces = {
	tetrominoes.i,
	tetrominoes.j,
	tetrominoes.l,
	tetrominoes.o,
	tetrominoes.s,
	tetrominoes.t,
	tetrominoes.z,
}

function M.new(assets, params)
	local new = setmetatable({}, M)
	new.assets = assets
	new.params = params
	new.field = playfield.new(params.lines or 20, params.columns or 10)
	new.gfx = gfx.new(assets, new)
	new.sfx = sfx.new(assets)
	new.music = music.new(assets, new)

	new.hold = false
	new.can_hold = true
	new.bag = bag.new(pieces, {seed = os.time(), randomly_add = {
		[heav_optimal_shapes.heav] = {inverse_chance = 5000},
		[heav_optimal_shapes.spite_shape] = {inverse_chance = 10000},
		[heav_optimal_shapes.amongus] = {inverse_chance = 13500},
	}})

	new.t_spun = false
	new.combo = -1
	new.stats = {pieces = 0, lines = 0, time = 0, start_time = love.timer.getTime()}

	new.gravity_delay = 0.5
	new.lock_delay = params.lock_delay or 0.8
	new.infinity = params.infinity or false
	new.das_delay = params.das_delay or 0.16
	new.das_repeat_delay = params.das_repeat_delay or 0.03

	new.loop = evloop.new()
	return new
end

local directions = {
	left = {0, -1},
	right = {0, 1},
}

function M:input_loop()
	local delay = self.das_delay

	for e, key in evloop.events("keypressed", "keyreleased") do
		-- TODO: interface with a remappable input system (it should generate
		-- its own events)
		if not self.piece then
			goto continue
		end

		delay = self.das_delay

		if e == "keypressed" then
			local moved
			if directions[key] then
				self:move(unpack(directions[key]))
				self.loop:queue("game.das", key)
			elseif key == "down" then
				moved = self:move(-1, 0)
				while self.piece:move(-1, 0) do end
			elseif key == "up" then
				moved = self:rotate()
			elseif key == "lctrl" then
				moved = self:rotate(true)
			elseif key == "space" then
				local dropped = false
				while self.piece:move(-1, 0) do
					dropped = true
				end
				self:place_piece()
				if dropped then self.sfx:play("harddrop") end
			elseif key == "c" then
				if not self.can_hold then goto bypass end
				if not self.hold then
					self.hold = self.piece.poly
					self:next_piece()
				else
					local tmp = self.hold
					self.hold = self.piece.poly
					self.piece = tmp:drop(self.field)
					self.loop:queue "game.lock_cancel"
				end
				self.can_hold = false
				::bypass::
			end
		elseif e == "keyreleased" then
			if key == "left" or key == "right" then
				self.loop:queue("game.das_cancel", key)
			end
		end

		::continue::
	end
end

function M:das_loop()
	for _, dir in evloop.events "game.das" do
		::das::
		local e, new_dir = evloop.poll(
			self.das_delay, "game.das", "game.das_cancel")
		while true do
			if e == "game.das" then
				dir = new_dir
				goto das
			elseif e == "game.das_cancel" then
				if dir == new_dir then break end
			end
			self:move(unpack(directions[dir]))
			e, new_dir = evloop.poll(
				self.das_repeat_delay, "game.das", "game.das_cancel")
		end
	end
end

function M:on_moved()
	if self.infinity then
		self.loop:queue "game.lock_cancel"
	elseif not self.piece:can_move(-1, 0) then
		self.loop:queue "game.lock"
	end
end

function M:move(lines, columns)
	if self.piece:move(lines, columns) then
		self:on_moved()
	end
end

function M:rotate(ccw)
	if self.piece:rotate(ccw) then
		self:on_moved()
		if self.piece.t_spun then
			if self.piece.last_kick_id == 5 then
				self.sfx:play("tspinkick5")
			else
				self.sfx: play("tspin")
			end
		end
		return true
	else
		return false
	end
end

function M:place_piece()
	if not self.piece:can_move(-1, 0) then
		self.piece:place()
		self.stats.pieces = self.stats.pieces + 1
		self.loop:queue "game.lock_cancel"
		local cleared = self.field:remove_cleared()
		if cleared > 0 then
			self.combo = self.combo + 1
			self.stats.lines = self.stats.lines + cleared
			if self.piece.t_spun then
				local sound = ({"tspinsingle","tspindouble","tspintriple"})[cleared]
				self.sfx:play(sound)
			end
			self.loop:queue("game.line_clear")
		else
			self.combo = -1
		end
		self.loop:queue("game.piece_placed")
		self:next_piece()
		return true
	else
		return false
	end
end

function M:next_piece()
	self.can_hold = true
	self.piece = self.bag:next_piece():drop(self.field)
	return self.piece and true or false
end

function M:lock_loop()
	for _ in evloop.events "game.lock" do
		local e = evloop.poll(self.lock_delay, "game.lock_cancel")
		if not e then
			self:place_piece()
		end
	end
end

function M:gravity_loop()
	for _ in evloop.events(self.gravity_delay) do
		if self.piece and not self.piece:move(-1, 0) then
			self.loop:queue "game.lock"
		end
		if not self.piece then
			self.loop:quit()
		end
	end
end

function M:time_loop()
	while true do
		self.loop.poll("update")
		self.stats.time = love.timer.getTime() - self.stats.start_time
	end
end

function M:win()
	self.loop:kill("input_loop")
	self.loop:kill("gravity_loop")
	self.loop:kill("lock_loop")
	self.loop:kill("das_loop")
	self.loop:kill("time_loop")
	self.piece = nil
	self.gfx.text_sidebar[1].text = "you win.\n\n"
	self.gfx.text_sidebar[1].color = {1, 1, 0}
	self.music:fade(self.loop, 4)
end

function M:run()
	self:next_piece()
	self.loop:wrap{function() self:input_loop() end, name="input_loop"}
	self.loop:wrap{function() self:gravity_loop() end, name="gravity_loop"}
	self.loop:wrap{function() self:lock_loop() end, name="lock_loop"}
	self.loop:wrap{function() self:das_loop() end, name="das_loop"}
	self.loop:wrap{function() self:time_loop() end, name="time_loop"}
	self.loop:wrap{function() self.gfx:run() end, name="gfx"}
	return self.loop:run()
end

return M