V0 version

Add ``.gitignore``

Update ``.vscode\settings.json``

Main file changed a bit

Replace every single ``io.open`` into ``fs.read()``

Add ``input.waiting2trigger`` as buffer for too quick inputs

Replace ``binser`` with ``bitser``

Add the missing buffer logical code in training mode

Add a debug connector

Not a big update

Update VirtualControl.lua

Update in vctrl system

Trimming some unnecessary empty lines in classic library

Update virtual control stuff

Replace ``table.getn`` with ``#`` and ``scene`` with ``SCENE``

Renaming and moving some modules

Removing unnecessary ``local mino = {...}``

Add loading screen

Update loading screen

Apply replay patch

Not showing virtual control on computer

Adding touch screen configuration scene (placeholder)

Fix loading screen

update virtual control texture

Do some preparation for touch config screen

Quick patch

Compress background

Not important uodates

Small changes on how virtual key call action

Add ``SCENE:onInputMove``

Apply V2.2 patch

Clean up unnecessary imports

Test

.

Remove a redudant global variable

Small change

Split up alpha number

Sorting code

Update storeInput function

Optimize replay storing, saving and reading

Add VCTRL.export (for saving feature)

Remove unnecessary imports

Redesign loading screen

Replace loading screen

Make a simple BUTTON module

Update BUTTON module

Update button module

Add new callback

Add new callback

TEST

Update simple-button module

Update simple button module

Set default draw function for button

Add scene type notation

TEST

Not important updates

Design a error screen

Small update

Remove error key

Update

TEST

TEST

Test

TEST

TEST

Update button module

TEST

TEST

TEST

TEST

TEST

TEST

TEST

TEST

test

TEST

TEST

TEST

test

TEST

test

Fix a bug in VCTRL module that affect to SCENE:onInputRelease

Moving VCTRL related calls and adding buttons for name entry screen

Add type notation

Update modules

Final update for touch configuration scene

Fix 2 buttons can be highlighted at the same time in simple-button module

Narrow the safe border

Remove id = b (it was there for test)

Update of touch configuration scene

Add touch gesture for replay and input configuration scene

Add buttons for Replay, add MENU to go out after finishing game or in 20G Training mode

TEST

Fix some bugs (TEST)

Fix lỗi giữa đêm

Fix bug again

It should work imo

TEST

Fix SCENE:onInputMove{type="touch"} is not working

Fix bug once again (DONE!)

Temproraily allowing save

Fix settings module

Fix VCTRL.exportAll()

Fix VCTRL.exportAll returns userdata

Reverse a change

Fix forgetting to import virtual control settings

Fix grid drawing

Fix bugs related to the first time launching game

Add README file

Add README file

Update README and add LICENSE files

Update README

Add TV remote code

Disable debug code

Fix Android code

Small fix

Rename LICENSE to COPYING

Moving scene.lua to modules folder

Add new libraries

Make a new FILE API and add a simple error screen in case most thing went down

Change special code, add a way to skip keys

Update icon + README file

Rename screenshot file

Update README

Updating README file

Replace loading screen

Update README

Update virtual control texture

Fix virtual button method

Update README

Add icon font

Add importing and exporting replays

Update touch control

Update conf.lua

Replacing font, to avoid license issue

convert indents to spaces

Update font related stuff

Replace font

Updating README file

Update virtual control texture
This commit is contained in:
Squishy (C6H12O6+NaCl+H2O)
2024-04-11 08:33:58 +07:00
commit 3343d8711b
91 changed files with 12085 additions and 0 deletions

1120
game/gamemode.lua Normal file

File diff suppressed because it is too large Load Diff

211
game/grid.lua Normal file
View File

@@ -0,0 +1,211 @@
local Object = require 'libs.classic'
local Grid = Object:extend()
local empty = { skin = "", colour = "", oob=false }
local oob = { skin = "", colour = "", oob=true }
local block = { skin = "2tie", colour = "A", oob=false }
function Grid:new(width, height)
self.grid = {}
self.grid_age = {}
self.width = width
self.height = height
for y = 1, self.height do
self.grid[y] = {}
self.grid_age[y] = {}
for x = 1, self.width do
self.grid[y][x] = empty
self.grid_age[y][x] = 0
end
end
end
function Grid:clear()
for y = 1, self.height do
for x = 1, self.width do
self.grid[y][x] = empty
self.grid_age[y][x] = 0
end
end
end
function Grid:getCell(x, y)
if x < 1 or x > self.width or y > self.height then return oob
elseif y < 1 then return oob
else return self.grid[y][x]
end
end
function Grid:isOccupied(x, y)
return self:getCell(x+1, y+1) ~= empty
end
function Grid:isRowFull(row)
for index, square in pairs(self.grid[row]) do
if square == empty then return false end
end
return true
end
function Grid:canPlacePiece(piece)
local offsets = piece:getBlockOffsets()
for index, offset in pairs(offsets) do
local x = piece.position.x + offset.x
local y = piece.position.y + offset.y
if self:isOccupied(x, y) then
return false
end
end
return true
end
function Grid:canPlacePieceInVisibleGrid(piece)
local offsets = piece:getBlockOffsets()
for index, offset in pairs(offsets) do
local x = piece.position.x + offset.x
local y = piece.position.y + offset.y
if y < 1 or self:isOccupied(x, y) ~= empty then
return false
end
end
return true
end
function Grid:getClearedRowCount()
local count = 0
local cleared_row_table = {}
for row = 1, self.height do
if self:isRowFull(row) then
count = count + 1
table.insert(cleared_row_table, row)
end
end
return count, cleared_row_table
end
function Grid:markClearedRows()
local block_table = {}
for row = 1, self.height do
if self:isRowFull(row) then
block_table[row] = {}
for x = 1, self.width do
block_table[row][x] = {
skin = self.grid[row][x].skin,
colour = self.grid[row][x].colour,
}
self.grid[row][x] = {
skin = self.grid[row][x].skin,
colour = "X"
}
end
end
end
return block_table
end
function Grid:clearClearedRows()
for row = 1, self.height do
if self:isRowFull(row) then
for above_row = row, 2, -1 do
self.grid[above_row] = self.grid[above_row - 1]
self.grid_age[above_row] = self.grid_age[above_row - 1]
end
self.grid[1] = {}
self.grid_age[1] = {}
for i = 1, self.width do
self.grid[1][i] = empty
self.grid_age[1][i] = 0
end
end
end
return true
end
function Grid:clearSpecificRow(row)
for col = 1, self.width do
self.grid[row][col] = empty
end
end
function Grid:applyPiece(piece)
if piece.big then
self:applyBigPiece(piece)
return
end
for index, offset in pairs(piece:getBlockOffsets()) do
local x = piece.position.x + offset.x
local y = piece.position.y + offset.y
if y + 1 > 0 and y < self.height then
self.grid[y+1][x+1] = {
skin = piece.skin,
colour = piece.colour,
flash = 5
}
end
end
end
function Grid:checkStackHeight()
for i = 0, self.height - 1 do
for j = 0, self.width - 1 do
if self:isOccupied(j, i) then return self.height - i end
end
end
return 0
end
function Grid:applyMap(map)
for y, row in pairs(map) do
for x, block in pairs(row) do
self.grid_age[y][x] = 0
self.grid[y][x] = block
end
end
end
function Grid:update()
for y = 1, self.height do
for x = 1, self.width do
if self.grid[y][x] ~= empty then
self.grid_age[y][x] = self.grid_age[y][x] + 1
end
end
end
end
function Grid:draw(greyscale, timer)
love.graphics.setColor(0,0,0,1)
love.graphics.rectangle("fill", 256, 31, 80, 45, 0, 0)
love.graphics.setColor(0.3, 0.3, 0.3, 1)
love.graphics.line(256,31,256,31+45)
love.graphics.line(256,31,256+80,31)
love.graphics.line(256+80,31,256+80,31+45)
love.graphics.line(256,31+45,256+80,31+45)
for y = 1, self.height do
for x = 1, self.width do
if BLOCKS[self.grid[y][x].skin] and
BLOCKS[self.grid[y][x].skin][self.grid[y][x].colour] then
if self.grid[y][x].flash ~= nil then
if self.grid[y][x].flash > 0 then
love.graphics.setColor(0.4+(self.grid[y][x].flash*0.1), 0.4+(self.grid[y][x].flash*0.1), 0.4+(self.grid[y][x].flash*0.1), 1)
love.graphics.draw(BLOCKS[self.grid[y][x].skin]['W'], 200+x*16, 64+y*16)
self.grid[y][x].flash = self.grid[y][x].flash - 1
else
love.graphics.setColor(1, 1, 1, 1)
love.graphics.draw(BLOCKS[self.grid[y][x].skin][self.grid[y][x].colour..'_d'], 200+x*16, 64+y*16)
end
end
if greyscale then
if timer > 1 then timer = 1 end
love.graphics.setColor(0.7, 0.7, 0.7, 0+timer)
if self.grid[y][x] ~= empty and self.grid[y][x].colour ~= 'X' then
love.graphics.draw(BLOCKS[self.grid[y][x].skin]["A"], 200+x*16, 64+y*16)
end
end
end
end
end
end
return Grid

174
game/piece.lua Normal file
View File

@@ -0,0 +1,174 @@
local Object = require 'libs.classic'
local Piece = Object:extend()
function Piece:new(shape, rotation, position, block_offsets, gravity, lock_delay, skin, colour, big)
self.shape = shape
self.rotation = rotation
self.position = position
self.block_offsets = block_offsets
self.gravity = gravity
self.lock_delay = lock_delay
self.skin = skin
self.colour = colour
self.lowest_point = -1
-- self.ghost = false
self.locked = false
-- self.big = big
end
-- Functions that return a new piece to test in rotation systems.
function Piece:withOffset(offset)
return Piece(
self.shape, self.rotation,
{x = self.position.x + offset.x, y = self.position.y + offset.y},
self.block_offsets, self.gravity, self.lock_delay, self.skin, self.colour
)
end
function Piece:withRelativeRotation(rot)
local new_rot = self.rotation + rot
while new_rot < 0 do new_rot = new_rot + 4 end
while new_rot >= 4 do new_rot = new_rot - 4 end
return Piece(
self.shape, new_rot, self.position,
self.block_offsets, self.gravity, self.lock_delay, self.skin, self.colour
)
end
-- Functions that return predicates relative to a grid.
function Piece:getBlockOffsets()
return self.block_offsets[self.shape][self.rotation + 1]
end
function Piece:occupiesSquare(x, y)
local offsets = self:getBlockOffsets()
for index, offset in pairs(offsets) do
local new_offset = {x = self.position.x + offset.x, y = self.position.y + offset.y}
if new_offset.x == x and new_offset.y == y then
return true
end
end
return false
end
function Piece:isMoveBlocked(grid, offset)
local moved_piece = self:withOffset(offset)
return not grid:canPlacePiece(moved_piece)
end
function Piece:isDropBlocked(grid)
return self:isMoveBlocked(grid, { x=0, y=1 })
end
-- Procedures to actually do stuff to pieces.
function Piece:setOffset(offset)
self.position.x = self.position.x + offset.x
self.position.y = self.position.y + offset.y
return self
end
function Piece:setRelativeRotation(rot)
new_rot = self.rotation + rot
while new_rot < 0 do new_rot = new_rot + 4 end
while new_rot >= 4 do new_rot = new_rot - 4 end
self.rotation = new_rot
return self
end
function Piece:moveInGrid(step, squares, grid, instant)
local moved = false
for x = 1, squares do
if grid:canPlacePiece(self:withOffset(step)) then
moved = true
self:setOffset(step)
if instant then
self:dropToBottom(grid)
end
else
break
end
end
return self
end
function Piece:dropSquares(dropped_squares, grid)
self:moveInGrid({ x = 0, y = 1 }, dropped_squares, grid)
end
function Piece:dropToBottom(grid)
local piece_y = self.position.y
self:dropSquares(math.huge, grid)
self.gravity = 0
if self.position.y > piece_y then
--self.lock_delay = 0
end
return self
end
function Piece:lockIfBottomed(grid)
if self:isDropBlocked(grid) then
self.locked = true
end
return self
end
function Piece:addGravity(gravity, grid, classic_lock)
gravity = gravity / (self.big and 2 or 1)
local new_gravity = self.gravity + gravity
if self:isDropBlocked(grid) then
if classic_lock then
self.gravity = new_gravity
else
self.gravity = 0
self.lock_delay = self.lock_delay + 1
end
elseif not (
self:isMoveBlocked(grid, { x=0, y=-1 }) and gravity < 0
) then
local dropped_squares = math.floor(math.abs(new_gravity))
if gravity >= 0 then
local new_frac_gravity = new_gravity - dropped_squares
self.gravity = new_frac_gravity
self:dropSquares(dropped_squares, grid)
if self:isDropBlocked(grid) then
PlaySE("bottom")
end
else
local new_frac_gravity = new_gravity + dropped_squares
self.gravity = new_frac_gravity
self:moveInGrid({ x=0, y=-1 }, dropped_squares, grid)
if self:isMoveBlocked(grid, { x=0, y=-1 }) then
PlaySE("bottom")
end
end
else
self.gravity = 0
end
return self
end
-- Procedures for drawing.
function Piece:draw(opacity, brightness, grid, partial_das)
if opacity == nil then opacity = 1 end
if brightness == nil then brightness = 1 end
love.graphics.setColor(brightness, brightness, brightness, opacity)
local offsets = self:getBlockOffsets()
local gravity_offset = 0
if partial_das == nil then partial_das = 0 end
for index, offset in pairs(offsets) do
local x = self.position.x + offset.x
local y = self.position.y + offset.y
love.graphics.draw(
BLOCKS[self.skin][self.colour],
216+x*16+partial_das, 80+y*16+gravity_offset
)
end
return false
end
return Piece

148
game/randomizer.lua Normal file
View File

@@ -0,0 +1,148 @@
local Object = require 'libs.classic'
local Randomizer = Object:extend()
local highest_count = 0
function Randomizer:new(always, seed)
self.always = always
if seed ~= nil then
self.seed = seed
else
self.seed = love.math.random(1, 9007199254740991)
end
self.drought = {
I = 0,
J = 0,
L = 0,
T = 0,
S = 0,
Z = 0,
O = 0
}
self.droughted_deals = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}
self.piece_counts = {
I = 0,
J = 0,
L = 0,
T = 0,
S = 0,
Z = 0,
O = 0
}
self.piece_round = 0
self:initialize()
end
function Randomizer:nextPiece()
return self:generatePiece()
end
function Randomizer:runTest()
local i_distance = 0
local highest_distance = 0
local distance_total = 0
local i_appearances = 0
local dupe = 0
local trip = 0
local quad = 0
local quin = 0
local lastpiece = 0
local lastpiece2 = 0
local lastpiece3 = 0
local lastpiece4 = 0
local pieceseq_rep = 0
local pieceseq = {}
local last_pieceseq = {}
local highest_discrepancy = 0
local discrepancy = self.piece_counts[table.highest(self.piece_counts)] - self.piece_counts[table.lowest(self.piece_counts)]
local total_chance = 0
for i=0, 750000 do
piece = self:generatePiece()
if #pieceseq < 7 then
table.insert(pieceseq, piece)
else
--print(table.concat(pieceseq))
if table.concat(pieceseq) == table.concat(last_pieceseq) then
pieceseq_rep = pieceseq_rep + 1
end
last_pieceseq = copy(pieceseq)
pieceseq = {}
end
if piece == lastpiece then dupe = dupe + 1 end
if piece == lastpiece and piece == lastpiece2 then trip = trip + 1 end
if piece == lastpiece and piece == lastpiece2 and piece == lastpiece3 then quad = quad + 1 end
if piece == lastpiece and piece == lastpiece2 and piece == lastpiece3 and piece == lastpiece4 then quin = quin + 1 end
if piece == 'I' then
distance_total = distance_total + i_distance
i_appearances = i_appearances + 1
i_distance = 0
else i_distance = i_distance + 1 end
if highest_distance < i_distance then highest_distance = i_distance end
lastpiece4 = lastpiece3
lastpiece3 = lastpiece2
lastpiece2 = lastpiece
lastpiece = piece
--if self.piece_round <= 750 then print(discrepancy) end
local new_discrepancy = self.piece_counts[table.highest(self.piece_counts)] - self.piece_counts[table.lowest(self.piece_counts)]
if new_discrepancy > discrepancy then highest_discrepancy = new_discrepancy end
discrepancy = new_discrepancy
local chance = 0
for piece,drought in pairs(self.drought) do
local piece_chance = 0.14/( ((self.drought[table.highest(self.drought)]+1)-drought) / (self.drought[table.highest(self.drought)]) )
chance = chance + piece_chance
end
chance = chance / 7
total_chance = total_chance + chance
end
--something = something / 750000
print(string.format('dupes: %d, trips: %s, quads: %s, quins: %s, highest i distance: %d, average i distance: %f, pieceseq reps: %d, highest discrepancy:%d, average chance:%f\ndrought lengths dealt: %s', dupe, trip, quad, quin, highest_distance, distance_total/i_appearances, pieceseq_rep, highest_discrepancy, total_chance/750000, table.concat(self.droughted_deals, '-')))
for piece,count in pairs(self.piece_counts) do
print(piece..' '..count)
end
end
function Randomizer:initialize()
local start_pieces = {"I", "J", "L", "T"}
local shapes = {"I", "J", "L", "T", "S", "Z", "O"}
love.math.setRandomSeed(self.seed)
self.drought[table.remove(shapes, love.math.random(1, 4))] = 10
for i=4,9 do
self.drought[table.remove(shapes, love.math.random(1, #shapes))] = i
end
--self:runTest()
end
function Randomizer:generatePiece(always)
if self.always then
return "I"
end
local shapes = {"I", "J", "L", "T", "S", "Z", "O"}
local new_piece = shapes[love.math.random(1,7)]
local chance = love.math.random(0,self.drought[table.highest(self.drought)])
while self.drought[new_piece] < chance do
new_piece = shapes[love.math.random(1,7)]
end
for piece,drought in pairs(self.drought) do
if drought >= 10 then
new_piece = piece
end
end
generated_piece = new_piece
for piece,drought in pairs(self.drought) do
if new_piece ~= piece then self.drought[piece] = self.drought[piece] + 1
else
if drought < #self.droughted_deals then self.droughted_deals[drought+1] = self.droughted_deals[drought+1] + 1 end
self.piece_counts[piece] = self.piece_counts[piece] + 1
self.drought[piece] = 0
end
end
self.piece_round = self.piece_round + 1
return generated_piece
end
return Randomizer

299
game/rotation.lua Normal file
View File

@@ -0,0 +1,299 @@
local Object = require 'libs.classic'
local Piece = require 'game.piece'
local Rotation = Object:extend()
Rotation.spawn_positions = {
I = { x=3, y= -1 },
J = { x=3, y= -1 },
L = { x=3, y= -1 },
O = { x=3, y= -1 },
S = { x=3, y= -1 },
T = { x=3, y= -1 },
Z = { x=3, y= -1 },
}
Rotation.colourscheme = {
I = "R",
L = "O",
J = "B",
S = "M",
Z = "G",
O = "Y",
T = "C",
}
Rotation.block_offsets = {
I={
{ {x=0, y=1}, {x=1, y=1}, {x=2, y=1}, {x=3, y=1} },
{ {x=2, y=0}, {x=2, y=1}, {x=2, y=2}, {x=2, y=3} },
{ {x=0, y=1}, {x=1, y=1}, {x=2, y=1}, {x=3, y=1} },
{ {x=2, y=0}, {x=2, y=1}, {x=2, y=2}, {x=2, y=3} },
},
J={
{ {x=0, y=1}, {x=1, y=1}, {x=2, y=1}, {x=2, y=2} },
{ {x=1, y=0}, {x=1, y=1}, {x=1, y=2}, {x=0, y=2} },
{ {x=0, y=1}, {x=0, y=2}, {x=1, y=2}, {x=2, y=2} },
{ {x=1, y=0}, {x=2, y=0}, {x=1, y=1}, {x=1, y=2} },
},
L={
{ {x=0, y=1}, {x=0, y=2}, {x=1, y=1}, {x=2, y=1} },
{ {x=0, y=0}, {x=1, y=0}, {x=1, y=1}, {x=1, y=2} },
{ {x=0, y=2}, {x=1, y=2}, {x=2, y=1}, {x=2, y=2} },
{ {x=1, y=0}, {x=1, y=1}, {x=1, y=2}, {x=2, y=2} },
},
O={
{ {x=1, y=1}, {x=2, y=1}, {x=1, y=2}, {x=2, y=2} },
{ {x=1, y=1}, {x=2, y=1}, {x=1, y=2}, {x=2, y=2} },
{ {x=1, y=1}, {x=2, y=1}, {x=1, y=2}, {x=2, y=2} },
{ {x=1, y=1}, {x=2, y=1}, {x=1, y=2}, {x=2, y=2} },
},
S={
{ {x=1, y=1}, {x=2, y=1}, {x=0, y=2}, {x=1, y=2} },
{ {x=0, y=0}, {x=0, y=1}, {x=1, y=1}, {x=1, y=2} },
{ {x=1, y=1}, {x=2, y=1}, {x=0, y=2}, {x=1, y=2} },
{ {x=0, y=0}, {x=0, y=1}, {x=1, y=1}, {x=1, y=2} },
},
T={
{ {x=0, y=1}, {x=1, y=1}, {x=1, y=2}, {x=2, y=1} },
{ {x=0, y=1}, {x=1, y=0}, {x=1, y=1}, {x=1, y=2} },
{ {x=0, y=2}, {x=1, y=1}, {x=1, y=2}, {x=2, y=2} },
{ {x=1, y=0}, {x=1, y=1}, {x=2, y=1}, {x=1, y=2} },
},
Z={
{ {x=0, y=1}, {x=1, y=1}, {x=1, y=2}, {x=2, y=2} },
{ {x=2, y=0}, {x=1, y=1}, {x=2, y=1}, {x=1, y=2} },
{ {x=0, y=1}, {x=1, y=1}, {x=1, y=2}, {x=2, y=2} },
{ {x=2, y=0}, {x=1, y=1}, {x=2, y=1}, {x=1, y=2} },
}
}
Rotation.pieces = 7
-- Component functions.
function Rotation:new(game_mode)
self.game = require 'game.gamemode'
end
function Rotation:rotatePiece(inputs, piece, grid, prev_inputs, initial, lastdir)
local new_inputs = {}
for input, value in pairs(inputs) do
if value and not prev_inputs[input] then
new_inputs[input] = true
end
end
local was_drop_blocked = piece:isDropBlocked(grid)
if self:canPieceRotate(piece, grid) then
-- if not self.held_rotate then
-- self:attemptRotate(inputs, piece, grid, initial)
-- self.held_rotate = true
-- else
self:attemptRotate(new_inputs, piece, grid, initial, lastdir)
-- end
end
if not initial and not was_drop_blocked and piece:isDropBlocked(grid) then
PlaySE("bottom")
end
-- prev_inputs becomes the previous inputs
for input, value in pairs(inputs) do
prev_inputs[input] = inputs[input]
end
end
function Rotation:attemptRotate(new_inputs, piece, grid, initial, lastdir)
local rot_dir = 0
if (new_inputs["rotate_left"] or new_inputs["rotate_left2"]) then
rot_dir = 3
if lastdir == 0 then lastdir = -1 end
elseif (new_inputs["rotate_right"] or new_inputs["rotate_right2"]) then
rot_dir = 1
if lastdir == 0 then lastdir = 1 end
end
if rot_dir == 0 then return end
local new_piece = piece:withRelativeRotation(rot_dir)
self:attemptWallkicks(piece, new_piece, rot_dir, grid, lastdir)
end
function Rotation:attemptWallkicks(piece, new_piece, rot_dir, grid, lastdir)
-- wallkick routine designed to maximize flexibility while minimizing teleports
-- O doesn't kick
if (piece.shape == "O") then return end
-- assess precisely what rows/columns would be blocked given the desired rotation
local sides = {top=false,uright=false,lright=false,uleft=false,lleft=false,center=false,bottom=false}
local left_exists = false
local right_exists = false
local kick = {x=0,y=0}
for _,offset in pairs(new_piece:getBlockOffsets()) do
local x = piece.position.x + offset.x
local y = piece.position.y + offset.y
if offset.x == 0 then left_exists = true end
if offset.x == 2 or offset.x == 3 then right_exists = true end
if grid:isOccupied(x,y) then
if offset.y == 0 then sides.top = true end
if offset.y == 3 then sides.bottom = true end
if offset.y == 1 and offset.x == 0 then sides.uleft = true end
if offset.y == 2 and offset.x == 0 then sides.lleft = true end
if offset.y == 1 and (offset.x == 2 or offset.x == 3) then sides.uright = true end
if offset.y == 2 and (offset.x == 2 or offset.x == 3) then sides.lright = true end
if offset.x == 1 then sides.center = true end
end
end
if sides.top then kick = {x=0,y=1}
elseif (sides.uleft and sides.lright) or (sides.uright and sides.lleft) or (sides.uright and sides.uleft) then kick = {x=0,y=1}
elseif (sides.lleft and sides.lright) then kick = {x=0,y=-1}
elseif (sides.lleft or sides.uleft) then kick = {x=1,y=0}
elseif (sides.lright or sides.uright) then kick = {x=-1,y=0}
elseif sides.center and left_exists then kick = {x=1,y=0}
elseif sides.center and right_exists then kick = {x=-1,y=0}
elseif sides.bottom then kick = {x=0,y=-1}
end
if grid:canPlacePiece(new_piece:withOffset({x=0,y=0})) then
self:onPieceRotate(piece, grid)
piece:setRelativeRotation(rot_dir):setOffset({x=0,y=0})
elseif grid:canPlacePiece(new_piece:withOffset(kick)) then
self:onPieceRotate(piece, grid)
piece:setRelativeRotation(rot_dir):setOffset(kick)
end
end
function Rotation:movePiece(piece, grid, move, instant)
local was_drop_blocked = piece:isDropBlocked(grid)
local offset = ({x=0, y=0})
local moves = 0
local y = piece.position.y
if move == "left" then
offset.x = -1
moves = 1
elseif move == "right" then
offset.x = 1
moves = 1
elseif move == "speedleft" then
offset.x = -1
moves = grid.width
elseif move == "speedright" then
offset.x = 1
moves = grid.width
end
if not self:canPieceMove(piece, grid) then return end
for i = 1, moves do
local x = piece.position.x
if moves ~= 1 then
piece:moveInGrid(offset, 1, grid, instant)
else
piece:moveInGrid(offset, 1, grid, false)
end
if piece.position.x ~= x then
self:onPieceMove(piece, grid)
if piece.locked then break end
end
end
if not was_drop_blocked and piece:isDropBlocked(grid) then
PlaySE("bottom")
end
end
function Rotation:dropPiece(
inputs, piece, grid, gravity, drop_speed, drop_locked, hard_drop_locked,
hard_drop_enabled, additive_gravity, classic_lock
)
local y = piece.position.y
if inputs["down"] == true and drop_locked == false then
piece:addGravity(math.max(gravity, drop_speed), grid, classic_lock)
else
piece:addGravity(gravity, grid, classic_lock)
end
if piece.position.y ~= y then
self:onPieceDrop(piece, grid)
end
end
function Rotation:lockPiece(piece, grid, lock_delay, classic_lock)
if piece:isDropBlocked(grid) and piece.lock_delay >= lock_delay then
piece.locked = true
end
end
function Rotation:get180RotationValue() return 3 end
function Rotation:getDefaultOrientation() return 1 end
function Rotation:getDrawOffset(shape, orientation) return { x=0, y=0 } end
function Rotation:getAboveFieldOffset(shape, orientation)
if shape == "I" then
return 1
else
return 2
end
end
function Rotation:initializePiece(
inputs, data, grid, gravity, prev_inputs,
move, lock_delay, drop_speed,
drop_locked, hard_drop_locked, big, irs, lastdir
)
local spawn_positions
spawn_positions = self.spawn_positions
local colours
colours = self.colourscheme
self.last_dir = 0
self.held_rotate = false
if inputs['left'] then self.last_dir = -1
elseif inputs['right'] then self.last_dir = 1 end
local spawn_x = math.floor(spawn_positions[data.shape].x / 10 * grid.width)
local spawn_dy
spawn_dy = 0
local piece = Piece(data.shape, data.orientation - 1, {
x = spawn_x or spawn_positions[data.shape].x,
y = spawn_positions[data.shape].y - spawn_dy
}, self.block_offsets, 0, 0, data.skin, colours[data.shape], big)
self:onPieceCreate(piece)
if irs then
self:rotatePiece(inputs, piece, grid, {}, true, lastdir)
end
return piece
end
function Rotation:onPieceCreate(piece) end
function Rotation:processPiece(
inputs, piece, grid, gravity, prev_inputs,
move, lock_delay, drop_speed,
drop_locked, hard_drop_locked,
hard_drop_enabled, additive_gravity, classic_lock, lastdir
)
self:rotatePiece(inputs, piece, grid, prev_inputs, false, lastdir)
self:movePiece(piece, grid, move, gravity >= grid.height)
self:dropPiece(
inputs, piece, grid, gravity, drop_speed, drop_locked, hard_drop_locked,
hard_drop_enabled, additive_gravity, classic_lock
)
self:lockPiece(piece, grid, lock_delay, classic_lock)
end
function Rotation:canPieceMove(piece, grid) return true end
function Rotation:canPieceRotate(piece, grid) return true end
function Rotation:onPieceMove(piece) end
function Rotation:onPieceRotate(piece) end
function Rotation:onPieceDrop(piece)
if piece.position.y > piece.lowest_point then
piece.lock_delay = 0
piece.lowest_point = piece.position.y
end
end
return Rotation

287
game/rotation_pent.lua Normal file
View File

@@ -0,0 +1,287 @@
local Object = require 'libs.classic'
local Piece = require 'game.piece'
local Rotation = Object:extend()
Rotation.spawn_positions = {
I = { x=3, y= -2 },
J = { x=3, y= -2 },
L = { x=3, y= -2 },
O = { x=3, y= -2 },
S = { x=3, y= -2 },
T = { x=3, y= -2 },
Z = { x=3, y= -2 },
}
Rotation.colourscheme = {
I = "R",
L = "O",
J = "B",
S = "M",
Z = "G",
O = "Y",
T = "C",
}
Rotation.block_offsets = {
I={
{ {x=0, y=2}, {x=1, y=2}, {x=2, y=2}, {x=3, y=2}, {x=4, y=2} },
{ {x=2, y=0}, {x=2, y=1}, {x=2, y=2}, {x=2, y=3}, {x=2, y=4} },
{ {x=0, y=2}, {x=1, y=2}, {x=2, y=2}, {x=3, y=2}, {x=4, y=2} },
{ {x=2, y=0}, {x=2, y=1}, {x=2, y=2}, {x=2, y=3}, {x=2, y=4} },
},
J={
{ {x=0, y=2}, {x=1, y=2}, {x=2, y=2}, {x=2, y=3}, {x=3, y=2} },
{ {x=1, y=0}, {x=1, y=1}, {x=1, y=2}, {x=0, y=2}, {x=1, y=3} },
{ {x=1, y=2}, {x=1, y=3}, {x=2, y=3}, {x=3, y=3}, {x=0, y=3} },
{ {x=1, y=1}, {x=2, y=1}, {x=1, y=2}, {x=1, y=3}, {x=1, y=0} },
},
L={
{ {x=0, y=2}, {x=0, y=3}, {x=1, y=2}, {x=2, y=2}, {x=3, y=2} },
{ {x=0, y=0}, {x=1, y=0}, {x=1, y=1}, {x=1, y=2}, {x=1, y=3} },
{ {x=1, y=3}, {x=2, y=3}, {x=3, y=2}, {x=3, y=3}, {x=0, y=3} },
{ {x=1, y=1}, {x=1, y=2}, {x=1, y=3}, {x=2, y=3}, {x=1, y=0} },
},
O={
{ {x=0, y=2}, {x=0, y=3}, {x=1, y=2}, {x=2, y=2}, {x=1, y=3} },
{ {x=0, y=1}, {x=1, y=1}, {x=1, y=2}, {x=1, y=3}, {x=0, y=2} },
{ {x=0, y=3}, {x=1, y=3}, {x=2, y=2}, {x=2, y=3}, {x=1, y=2} },
{ {x=1, y=1}, {x=1, y=2}, {x=1, y=3}, {x=2, y=3}, {x=2, y=2} },
},
S={
{ {x=1, y=2}, {x=2, y=2}, {x=0, y=3}, {x=1, y=3}, {x=3, y=2} },
{ {x=0, y=0}, {x=0, y=1}, {x=1, y=1}, {x=1, y=2}, {x=1, y=3} },
{ {x=2, y=2}, {x=3, y=2}, {x=1, y=3}, {x=2, y=3}, {x=0, y=3} },
{ {x=1, y=1}, {x=1, y=2}, {x=2, y=2}, {x=2, y=3}, {x=1, y=0} },
},
T={
{ {x=0, y=2}, {x=1, y=2}, {x=1, y=3}, {x=2, y=2}, {x=3, y=2} },
{ {x=0, y=1}, {x=1, y=0}, {x=1, y=1}, {x=1, y=2}, {x=1, y=3} },
{ {x=1, y=3}, {x=2, y=2}, {x=2, y=3}, {x=3, y=3}, {x=0, y=3} },
{ {x=2, y=1}, {x=2, y=2}, {x=3, y=2}, {x=2, y=3}, {x=2, y=0} },
},
Z={
{ {x=0, y=2}, {x=1, y=2}, {x=1, y=3}, {x=2, y=3}, {x=2, y=2} },
{ {x=1, y=1}, {x=0, y=2}, {x=1, y=2}, {x=0, y=3}, {x=1, y=3} },
{ {x=0, y=2}, {x=1, y=2}, {x=1, y=3}, {x=2, y=3}, {x=0, y=3} },
{ {x=2, y=1}, {x=1, y=2}, {x=2, y=2}, {x=1, y=3}, {x=1, y=1} },
}
}
Rotation.pieces = 7
-- Component functions.
function Rotation:new(game_mode)
self.game = require 'game.gamemode'
end
function Rotation:rotatePiece(inputs, piece, grid, prev_inputs, initial, lastdir)
local new_inputs = {}
for input, value in pairs(inputs) do
if value and not prev_inputs[input] then
new_inputs[input] = true
end
end
local was_drop_blocked = piece:isDropBlocked(grid)
if self:canPieceRotate(piece, grid) then
-- if not self.held_rotate then
-- self:attemptRotate(inputs, piece, grid, initial)
-- self.held_rotate = true
-- else
self:attemptRotate(new_inputs, piece, grid, initial, lastdir)
-- end
end
if not initial and not was_drop_blocked and piece:isDropBlocked(grid) then
PlaySE("bottom")
end
-- prev_inputs becomes the previous inputs
for input, value in pairs(inputs) do
prev_inputs[input] = inputs[input]
end
end
function Rotation:attemptRotate(new_inputs, piece, grid, initial, lastdir)
local rot_dir = 0
if (new_inputs["rotate_left"] or new_inputs["rotate_left2"]) then
rot_dir = 3
if lastdir == 0 then lastdir = -1 end
elseif (new_inputs["rotate_right"] or new_inputs["rotate_right2"]) then
rot_dir = 1
if lastdir == 0 then lastdir = 1 end
end
if rot_dir == 0 then return end
local new_piece = piece:withRelativeRotation(rot_dir)
self:attemptWallkicks(piece, new_piece, rot_dir, grid, lastdir)
end
function Rotation:attemptWallkicks(piece, new_piece, rot_dir, grid, lastdir)
-- assess precisely what rows/columns would be blocked given the desired rotation
local sides = {top=false,uright=false,lright=false,uleft=false,lleft=false,center=false,bottom=false}
local left_exists = false
local right_exists = false
local kick = {x=0,y=0}
for _,offset in pairs(new_piece:getBlockOffsets()) do
local x = piece.position.x + offset.x
local y = piece.position.y + offset.y
if grid:isOccupied(x,y) then
if offset.y <= 1 then sides.top = true end
if offset.y == 4 then sides.bottom = true end
if offset.x <= 1 then sides.lleft = true end
if offset.x >= 2 then sides.lright = true end
end
end
if sides.top then kick = {x=0,y=1}
elseif sides.bottom then kick = {x=0,y=-1}
elseif (sides.lleft or sides.uleft) then kick = {x=1,y=0}
elseif (sides.lright or sides.uright) then kick = {x=-1,y=0}
end
if grid:canPlacePiece(new_piece:withOffset({x=0,y=0})) then
self:onPieceRotate(piece, grid)
piece:setRelativeRotation(rot_dir):setOffset({x=0,y=0})
elseif grid:canPlacePiece(new_piece:withOffset(kick)) then
self:onPieceRotate(piece, grid)
piece:setRelativeRotation(rot_dir):setOffset(kick)
elseif grid:canPlacePiece(new_piece:withOffset({x=kick.x*2,y=kick.y*2})) then
self:onPieceRotate(piece, grid)
piece:setRelativeRotation(rot_dir):setOffset({x=kick.x*2,y=kick.y*2})
end
end
function Rotation:movePiece(piece, grid, move, instant)
local was_drop_blocked = piece:isDropBlocked(grid)
local offset = ({x=0, y=0})
local moves = 0
local y = piece.position.y
if move == "left" then
offset.x = -1
moves = 1
elseif move == "right" then
offset.x = 1
moves = 1
elseif move == "speedleft" then
offset.x = -1
moves = grid.width
elseif move == "speedright" then
offset.x = 1
moves = grid.width
end
if not self:canPieceMove(piece, grid) then return end
for i = 1, moves do
local x = piece.position.x
if moves ~= 1 then
piece:moveInGrid(offset, 1, grid, instant)
else
piece:moveInGrid(offset, 1, grid, false)
end
if piece.position.x ~= x then
self:onPieceMove(piece, grid)
if piece.locked then break end
end
end
if not was_drop_blocked and piece:isDropBlocked(grid) then
PlaySE("bottom")
end
end
function Rotation:dropPiece(
inputs, piece, grid, gravity, drop_speed, drop_locked, hard_drop_locked,
hard_drop_enabled, additive_gravity, classic_lock
)
local y = piece.position.y
if inputs["down"] == true and drop_locked == false then
piece:addGravity(math.max(gravity, drop_speed), grid, classic_lock)
else
piece:addGravity(gravity, grid, classic_lock)
end
if piece.position.y ~= y then
self:onPieceDrop(piece, grid)
end
end
function Rotation:lockPiece(piece, grid, lock_delay, classic_lock)
if piece:isDropBlocked(grid) and piece.lock_delay >= lock_delay then
piece.locked = true
end
end
function Rotation:get180RotationValue() return 3 end
function Rotation:getDefaultOrientation() return 1 end
function Rotation:getDrawOffset(shape, orientation) return { x=0, y=0 } end
function Rotation:getAboveFieldOffset(shape, orientation)
if shape == "I" then
return 1
else
return 2
end
end
function Rotation:initializePiece(
inputs, data, grid, gravity, prev_inputs,
move, lock_delay, drop_speed,
drop_locked, hard_drop_locked, big, irs, lastdir
)
local spawn_positions
spawn_positions = self.spawn_positions
local colours
colours = self.colourscheme
self.last_dir = 0
self.held_rotate = false
if inputs['left'] then self.last_dir = -1
elseif inputs['right'] then self.last_dir = 1 end
local spawn_x = math.floor(spawn_positions[data.shape].x / 10 * grid.width)
local spawn_dy
spawn_dy = 0
local piece = Piece(data.shape, data.orientation - 1, {
x = spawn_x or spawn_positions[data.shape].x,
y = spawn_positions[data.shape].y - spawn_dy
}, self.block_offsets, 0, 0, data.skin, colours[data.shape], big)
self:onPieceCreate(piece)
if irs then
self:rotatePiece(inputs, piece, grid, {}, true, lastdir)
end
return piece
end
function Rotation:onPieceCreate(piece) end
function Rotation:processPiece(
inputs, piece, grid, gravity, prev_inputs,
move, lock_delay, drop_speed,
drop_locked, hard_drop_locked,
hard_drop_enabled, additive_gravity, classic_lock, lastdir
)
self:rotatePiece(inputs, piece, grid, prev_inputs, false, lastdir)
self:movePiece(piece, grid, move, gravity >= grid.height)
self:dropPiece(
inputs, piece, grid, gravity, drop_speed, drop_locked, hard_drop_locked,
hard_drop_enabled, additive_gravity, classic_lock
)
self:lockPiece(piece, grid, lock_delay, classic_lock)
end
function Rotation:canPieceMove(piece, grid) return true end
function Rotation:canPieceRotate(piece, grid) return true end
function Rotation:onPieceMove(piece) end
function Rotation:onPieceRotate(piece) end
function Rotation:onPieceDrop(piece)
if piece.position.y > piece.lowest_point then
piece.lock_delay = 0
piece.lowest_point = piece.position.y
end
end
return Rotation

238
game/vctrl.lua Normal file
View File

@@ -0,0 +1,238 @@
local gc_newQuad=love.graphics.newQuad
---Get distance between two points
---@param x1 number
---@param y1 number
---@param x2 number
---@param y2 number
---@return number
local function math_distance(x1,y1,x2,y2)
return ((x1-x2)^2+(y1-y2)^2)^.5
end
local function mDrawQ(obj,quad,x,y,a,k)
local _,_,w,h=quad:getViewport()
love.graphics.draw(obj,quad,x,y,a,k,nil,w*.5,h*.5)
end
ShowLoadingText('virtual key skin')
local empty_quad
-- A table containing quads used to draw icons for virtual control system.
-- local virtual_quad=setmetatable((function()
-- local t={}
-- local w=180
-- empty_quad=gc_newQuad(0,0,1,1,5*w,7*w)
-- for i,name in next,{
-- 'left','right','up','down','',
-- 'rotate_right','rotate_left','','','',
-- '','','','','',
-- '','','','','',
-- '','','menu_back','','',
-- '','','','','',
-- '','','','','menu_decide',
-- } do if #name>0 then t[name]=gc_newQuad((i-1)%5*w,math.floor((i-1)/5)*w,w,w,5*w,7*w) end end
-- t.rotate_right2, t.rotate_left2 = t.rotate_right, t.rotate_left
-- return t
-- end)(),{
-- __index=function() return empty_quad end
-- })
local virtual_quad=setmetatable((function()
local t={}
local w=180
empty_quad=gc_newQuad(0,0,1,1,5*w,2*w)
for i,name in next,{
'left','right','up','down','restart',
'rotate_right','rotate_left','rotate_right2','rotate_left2','',
} do if #name>0 then t[name]=gc_newQuad((i-1)%5*w,math.floor((i-1)/5)*w,w,w,5*w,2*w) end end
return t
end)(),{
__index=function() return empty_quad end
})
local virtual_texture=love.graphics.newImage('game/vctrlTexture.png')
local control_type={}
control_type.button={}
control_type.button.__index=control_type.button
function control_type.button:new(data)
local data=data or {}
return setmetatable({
show=data.show==nil and true or data.show,
x=data.x or 320,
y=data.y or 240,
r=data.r or 80, -- size
shape=data.shape or 'circle',
key=data.key or 'X',
iconSize=data.iconSize or 80,
alpha=data.alpha or 0.75,
quad=virtual_quad[data.key]
},self)
end
function control_type.button:export()
return {
type = 'button',
show = self.show,
x = self.x,
y = self.y,
r = self.r,
shape = self.shape,
key = self.key,
iconSize = self.iconSize,
alpha = self.alpha
}
end
function control_type.button:reset()
self.pressed=false
self.lastPressTime=-1e99
self.pressingID=false
end
function control_type.button:press(_,_,id)
self.pressed=true
self.lastPressTime=love.timer.getTime()
self.pressing=id
-- love.keypressed(self.key, love.keyboard.getScancodeFromKey(self.key))
SCENE:onInputPress{input=self.key,type="virtual"}
end
function control_type.button:release()
self.pressed=false
self.pressingID=false
-- love.keyreleased(self.key,love.keyboard.getScancodeFromKey(self.key))
SCENE:onInputRelease{input=self.key,type="virtual"}
end
function control_type.button:drag(dx,dy)
self.x,self.y=self.x+dx,self.y+dy
end
function control_type.button:draw(forceAlpha)
local alpha = forceAlpha or self.alpha
love.graphics.setLineWidth(4)
if self.shape=='circle' then
love.graphics.setColor(0,0,0,alpha)
love.graphics.circle('fill',self.x,self.y,self.r-4)
love.graphics.setColor(1,1,1,self.pressed and .5 or 0)
love.graphics.circle('fill',self.x,self.y,self.r-4)
love.graphics.setColor(1,1,1,alpha)
love.graphics.circle('line',self.x,self.y,self.r-2)
elseif self.shape=='square' then
love.graphics.setColor(0,0,0,alpha)
love.graphics.rectangle('fill',self.x-self.r-4,self.y-self.r-4,self.r*2+8,self.r*2+8)
love.graphics.setColor(1,1,1,self.pressed and .5 or 0)
love.graphics.rectangle('fill',self.x-self.r-4,self.y-self.r-4,self.r*2+8,self.r*2+8)
love.graphics.setColor(1,1,1,alpha)
love.graphics.rectangle('line',self.x-self.r-2,self.y-self.r-2,self.r*2+4,self.r*2+4)
end
if self.iconSize>0 and self.quad then
love.graphics.setColor(1,1,1,alpha)
local _,_,w,h=self.quad:getViewport()
mDrawQ(
virtual_texture,
self.quad,
self.x,self.y,0,
self.iconSize/100*math.min(self.r*2/w,self.r*2/h)
)
end
end
function control_type.button:getDistance(x,y)
if self.shape=='circle' then
return math_distance(x,y,self.x,self.y)/self.r
elseif self.shape=='square' then
return math.max(math.abs(x-self.x),math.abs(y-self.y))/self.r
end
end
local touches={}
local global_toggle=false
VCTRL={}
VCTRL.focus=nil -- Focusing buttons
---@class VCTRL.data
---@field type 'button'
---@field x number
---@field y number
---@field shape? string
---@field key? string
---@field iconSize? number
---@field alpha? number
---@field show? boolean
---@param ... VCTRL.data[]
---Adding (multiple) virtual button(s)
function VCTRL.new(...)
for _,d in pairs(...) do table.insert(VCTRL,control_type[d.type]:new(d)) end
end
---@param toggle boolean|false
---Enabling virtual control or not
function VCTRL.toggle(toggle)
if not toggle then
-- Release all buttons to prevent button ghost situation
for id, b in pairs(touches) do
b:release(id)
touches[id]=nil
end
end
global_toggle=toggle
end
function VCTRL.clearAll()
local toggle = global_toggle
VCTRL.toggle(false)
global_toggle = toggle
for i=#VCTRL,1,-1 do VCTRL[i] = nil end
collectgarbage()
end
---@param force? boolean Forcing click on hidden widgets?
function VCTRL.press(x,y,id,force)
if not (global_toggle and id) then return end
local obj,closestDist=false,1e99
for _, w in ipairs(VCTRL) do
if w.show or force then
local d=w:getDistance(x,y)
if d<=1 and d<closestDist then
obj,closestDist=w,d
end
end
end
if obj then
touches[id]=obj
obj:press(x,y,id)
VCTRL.focus=obj
return true
end
end
function VCTRL.release(id)
if not (global_toggle and id) then return end
if touches[id] then
touches[id]:release()
touches[id]=nil
return true
end
end
function VCTRL.drag(dx,dy,id)
if not global_toggle then return end
if touches[id] then
touches[id]:drag(dx,dy)
return true
end
end
function VCTRL.draw(forceAlpha)
if not global_toggle then return end
for _, w in ipairs(VCTRL) do
if w.show then w:draw(forceAlpha) end
end
end
function VCTRL.exportAll()
local t = {}
for o, k in ipairs(VCTRL) do t[o] = k:export() end
return t
end

BIN
game/vctrlTexture.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB