Merge branch 'main' of https://github.com/26F-Studio/Techmino into mod-patch

This commit is contained in:
SweetSea
2024-11-10 14:48:54 +07:00
165 changed files with 2168 additions and 1265 deletions

View File

@@ -177,12 +177,12 @@ local function _drawField(P,showInvis)
if P.falling==0 then-- Blocks only
if ENV.upEdge then
gc_setShader(shader_lighter)
gc_translate(0,-4)
gc_translate(0,-6)
-- <drawRow>
for j=start,min(start+21,#F) do _drawRow(texture,j,V[j],F[j]) end
-- </drawRow>
gc_setShader(shader_fieldSatur)
gc_translate(0,4)
gc_translate(0,6)
else
gc_setShader(shader_fieldSatur)
end
@@ -197,7 +197,7 @@ local function _drawField(P,showInvis)
if ENV.upEdge then
gc_push('transform')
gc_setShader(shader_lighter)
gc_translate(0,-4)
gc_translate(0,-6)
-- <drawRow>
for j=start,min(start+21,#F) do
while j==P.clearingRow[h] do

View File

@@ -1,8 +1,9 @@
return {
das=10,arr=2,
dascut=0,dropcut=0,
dascut=0,irscut=6,dropcut=0,
sddas=2,sdarr=2,
ihs=true,irs=true,ims=true,
logicalIHS=true,logicalIRS=true,logicalIMS=true,
ghostType='gray',
block=true,ghost=.3,center=1,
@@ -40,7 +41,7 @@ return {
RS='TRS',
sequence='bag',
seqData={1,2,3,4,5,6,7},
skinSet='crystal_scf',
skinSet='Crystal (Scf)',
face=false,skin=false,
mission=false,
@@ -62,10 +63,16 @@ return {
mindas=0,minarr=0,minsdarr=0,
noInitSZO=false,
mesDisp={},
hook_drop={},
hook_die={},
task={},
-- Some Events are registered in player/init.lua, see "tableNeedMerge"
extraEvent={
{'attack',4},
},
extraEventHandler={
attack=function(P,P2,...)
P:beAttacked(P2,...)
end,
},
eventSet="X",
bg='none',bgm='race',

View File

@@ -273,17 +273,8 @@ local function _loadRemoteEnv(P,confStr)-- Load gameEnv
end
end
end
local function _mergeFuncTable(f,L)
if type(f)=='function' then
ins(L,f)
elseif type(f)=='table' then
for i=1,#f do
ins(L,f[i])
end
end
return L
end
local hooks = {
local tableNeedMerge={
'task',
'mesDisp',
'hook_left',
'hook_left_manual',
@@ -296,36 +287,63 @@ local hooks = {
'hook_spawn',
'hook_hold',
'hook_die',
'task'
'extraEvent',
}
for _,k in next,tableNeedMerge do gameEnv0[k]={} end
local function _mergeFuncTable(f,L)
if type(f)=='function' then
ins(L,f)
elseif type(f)=='table' then
for i=1,#f do
ins(L,f[i])
end
end
return L
end
local function _applyGameEnv(P)-- Finish gameEnv processing
local ENV=P.gameEnv
-- Apply events
for i=1,#hooks do
ENV[hooks[i]]=_mergeFuncTable(ENV[hooks[i]],{})
-- Create event tables
for i=1,#tableNeedMerge do
ENV[tableNeedMerge[i]]=_mergeFuncTable(ENV[tableNeedMerge[i]],{})
end
-- Apply eventSet
if ENV.eventSet and ENV.eventSet~="X" then
if type(ENV.eventSet)=='string' then
local eventSet=require('parts.eventsets.'..ENV.eventSet)
if eventSet then
for k,v in next,eventSet do
if TABLE.find(hooks,k) then
_mergeFuncTable(v,ENV[k])
elseif type(v)=='table' then
ENV[k]=TABLE.copy(v)
while true do
if not (ENV.eventSet and ENV.eventSet~="X") then
break
end
if type(ENV.eventSet)~='string' then
MES.new('warn',"Wrong event set type: "..type(ENV.eventSet))
break
end
local eventSet=require('parts.eventsets.'..ENV.eventSet)
if not eventSet then
MES.new('warn',"No event set called: "..ENV.eventSet)
break
end
for k,v in next,eventSet do
if k=='extraEventHandler' then
for ev,handler in next,v do
if ENV.extraEventHandler[ev] then
local prevHandler=ENV.extraEventHandler[ev]
ENV.extraEventHandler[ev]=function(...)
prevHandler(...)
handler(...)
end
else
ENV[k]=v
ENV.extraEventHandler[ev]=handler
end
end
elseif TABLE.find(tableNeedMerge,k) then
_mergeFuncTable(v,ENV[k])
elseif type(v)=='table' then
ENV[k]=TABLE.copy(v)
else
MES.new('warn',"No event set called: "..ENV.eventSet)
ENV[k]=v
end
else
MES.new('warn',"Wrong event set type: "..type(ENV.eventSet))
end
break
end
if ENV.allowMod and GAME.modApplyAt~='preInit' then

View File

@@ -270,27 +270,61 @@ function Player:act_rotRight()
if not self.control then return end
if self.cur then
self.ctrlCount=self.ctrlCount+1
if self.bufferedIRS then
-- Ensure IRS is spent before the rotation is processed so it doesn't throw things off.
-- This is so that if you for instance, are holding left IRS and then rotate right, it doesn't process
-- the left and right rotates in the reverse order.
self.keyPressing[3]=false
self:resolveIRS()
self.keyPressing[3]=true
end
self:spin(1)
self:_triggerEvent('hook_rotate',1)
self.keyPressing[3]=false
-- Disable held inputs if IRS is off
if not self.gameEnv.irs then
self.keyPressing[3]=false
end
end
end
function Player:act_rotLeft()
if not self.control then return end
if self.cur then
self.ctrlCount=self.ctrlCount+1
if self.bufferedIRS then
-- Ensure IRS is spent before the rotation is processed so it doesn't throw things off.
-- This is so that if you for instance, are holding left IRS and then rotate right, it doesn't process
-- the left and right rotates in the reverse order.
self.keyPressing[4]=false
self:resolveIRS()
self.keyPressing[4]=true
end
self:spin(3)
self:_triggerEvent('hook_rotate',3)
self.keyPressing[4]=false
-- Disable held inputs if IRS is off
if not self.gameEnv.irs then
self.keyPressing[4]=false
end
end
end
function Player:act_rot180()
if not self.control then return end
if self.cur then
self.ctrlCount=self.ctrlCount+2
if self.bufferedIRS then
-- Ensure IRS is spent before the rotation is processed so it doesn't throw things off.
-- This is so that if you for instance, are holding left IRS and then rotate right, it doesn't process
-- the left and right rotates in the reverse order.
self.keyPressing[5]=false
self:resolveIRS()
self.keyPressing[5]=true
end
self:spin(2)
self:_triggerEvent('hook_rotate',2)
self.keyPressing[5]=false
-- Disable held inputs if IRS is off
if not self.gameEnv.irs then
self.keyPressing[5]=false
end
end
end
function Player:act_hardDrop()
@@ -300,6 +334,10 @@ function Player:act_hardDrop()
if self.lastPiece.autoLock and self.frameRun-self.lastPiece.frame<ENV.dropcut then
SFX.play('drop_cancel',.3)
else
if self.bufferedIRS then
-- If the player drops quicker than their IRS cut delay, make sure IRS still resolves.
self:resolveIRS()
end
if self.curY>self.ghoY then
self:createDropFX()
self.curY=self.ghoY
@@ -344,7 +382,10 @@ function Player:act_hold()
if not self.control then return end
if self.cur then
if self:hold() then
self.keyPressing[8]=false
-- Disable held inputs if IHS is off
if not self.gameEnv.ihs then
self.keyPressing[8]=false
end
self:_triggerEvent('hook_hold')
end
end
@@ -701,6 +742,40 @@ function Player:_triggerEvent(eventName)
return true
end
end
function Player:extraEvent(eventName,...)
if not (self.gameEnv.extraEvent and self.gameEnv.extraEventHandler) then return end
local list=self.gameEnv.extraEvent
local eventID
for i=1,#list do
if list[i][1]==eventName then
eventID=i
break
end
end
if not eventID then
MES.new('warn',"Extra event '"..eventName.."' doesn't exist in this mode")
return
end
local SELF
-- Trigger for all non-remote players
for _,p in next,PLAYERS do
if p.type~='remote' then
if p.type=='human' then
SELF=p
end
self.gameEnv.extraEventHandler[eventName](p,self,...)
end
end
ins(GAME.rep,SELF.frameRun)
ins(GAME.rep,64+eventID)
ins(GAME.rep,self.sid)
local data={...}
for i=1,#data do
ins(GAME.rep,data[i])
end
end
function Player:getHolePos()-- Get a good garbage-line hole position
if self.garbageBeneath==0 then
@@ -833,34 +908,13 @@ function Player:ifoverlap(bk,x,y)
end
end
end
function Player:attack(R,send,time,line,fromStream)
if GAME.net then
if self.type=='human' then-- Local player attack others
ins(GAME.rep,self.frameRun)
ins(GAME.rep,
R.sid+
send*0x100+
time*0x10000+
line*0x100000000+
0x2000000000000
)
self:createBeam(R,send)
end
if fromStream and R.type=='human' then-- Local player receiving lines
ins(GAME.rep,R.frameRun)
ins(GAME.rep,
self.sid+
send*0x100+
time*0x10000+
line*0x100000000+
0x1000000000000
)
R:receive(self,send,time,line)
end
else
R:receive(self,send,time,line)
self:createBeam(R,send)
end
function Player:attack(R,send,time,line)
self:extraEvent('attack',R.sid,send,time,line)
end
function Player:beAttacked(P2,sid,send,time,line)
if self==P2 or self.sid~=sid then return end
self:receive(P2,send,time,line)
P2:createBeam(self,send)
end
function Player:receive(A,send,time,line)
self.lastRecv=A
@@ -969,7 +1023,7 @@ function Player:freshBlockGhost()
if self.curY>self.ghoY then
self:createDropFX()
if ENV.shakeFX then
self.swingOffset.vy=.5
self.swingOffset.vy=MATH.clamp((self.curY-self.ghoY-2.6)/10,0,0.626)
end
self.curY=self.ghoY
end
@@ -1151,34 +1205,67 @@ function Player:resetBlock()-- Reset Block's position and execute I*S
self.curY=y
self.minY=y+sc[1]
local ENV=self.gameEnv
-- In the game settings, there are user-set control flags for irs,irs,ims
-- These control in what way the user can buffer their rotate/hold/move inputs.
-- (If enabled, they may hold these inputs from the previous piece instead of just Entry Delay)
-- And mode-set flags for logicalIRS,logicalIHS,logicalIMS
-- These control whether IRS/IHS/IMS are effective in modifying what you can do.
-- (For instance, changing your piece's spawn position in 20g, or saving you from a death).
-- If logical IRS is disabled, the player may still IRS, but it will just buffer their input,
-- not actually allowing them to survive in a way they could not without.
local pressing=self.keyPressing
-- IMS
if self.gameEnv.ims and (pressing[1] and self.movDir==-1 or pressing[2] and self.movDir==1) and self.moving>=self.gameEnv.das then
local x=self.curX+self.movDir
if not self:ifoverlap(C.bk,x,y) then
self.curX=x
-- IMS is enabled only when logicalIMS is enabled, because otherwise, it's just faster DAS.
if ENV.logicalIMS and (pressing[1] and self.movDir==-1 or pressing[2] and self.movDir==1) and self.moving>=self.gameEnv.das then
-- To avoid a top-out
if self:ifoverlap(C.bk,self.curX,self.curY) then
-- Always perform the shift, since you're topped out anyway
self.curX=self.curX+self.movDir
elseif ENV.wait>0 and ENV.ims then
-- Otherwise, only check IMS if it's enabled and you're in a mode with entry delay (20g)
local x=self.curX+self.movDir
if not self:ifoverlap(C.bk,x,y) then
self.curX=x
end
end
end
-- IRS
if self.gameEnv.irs then
if not ENV.logicalIRS then
-- If logical IRS is disabled, all IRS inputs will be buffered to prevent survival.
self.bufferedIRS=true
self.bufferedDelay=0
if ENV.wait==0 then
self.bufferedDelay=ENV.irscut
end
elseif ENV.wait==0 and ENV.irscut>0 and not self:ifoverlap(C.bk,self.curX,self.curY) then
-- If IRS cut delay is enabled and we aren't currently dying, buffer the input instead.
self.bufferedIRS=true
self.bufferedDelay=ENV.irscut
else
-- If we're currently dying or in an entry-delay mode (20g), perform the rotation right away.
if pressing[5] then
self:spin(2,true)
self:act_rot180()
elseif pressing[3] then
if pressing[4] then
self:spin(2,true)
self:act_rot180()
else
self:spin(1,true)
self:act_rotRight()
end
elseif pressing[4] then
self:spin(3,true)
self:act_rotLeft()
end
end
-- Disable held inputs if IRS is off
if not ENV.irs then
pressing[3],pressing[4],pressing[5]=false,false,false
end
-- DAS cut
if self.gameEnv.dascut>0 then
self.moving=self.moving-(self.moving>0 and 1 or -1)*self.gameEnv.dascut
if ENV.dascut>0 then
self.moving=self.moving-(self.moving>0 and 1 or -1)*ENV.dascut
end
-- Spawn SFX
@@ -1498,9 +1585,29 @@ function Player:_popNext(ifhold)-- Pop nextQueue to hand
local pressing=self.keyPressing
-- IHS
if not ifhold and pressing[8] and ENV.ihs and self.holdTime>0 then
self:hold(true)
pressing[8]=false
if not ifhold and pressing[8] and self.holdTime>0 then
if not ENV.logicalIHS then
-- If logical IHS is disabled, all IHS inputs will be buffered to prevent survival.
self.bufferedIRS=true
self.bufferedIHS=true
self.bufferedDelay=0
if ENV.wait==0 then
self.bufferedDelay=ENV.irscut
end
elseif ENV.wait==0 and ENV.irscut>0 and not self:willDieWith(self.cur) then
-- If IRS cut delay is enabled and we're not currently dying, buffer the input instead.
self.bufferedIRS=true
self.bufferedIHS=true
self.bufferedDelay=ENV.irscut
self:resetBlock()
else
-- If we're currently dying or in an entry-delay mode (20g), perform the hold immediately.
self:hold(true)
end
-- Disable held inputs if IHS is off
if not ENV.ihs then
pressing[8]=false
end
else
self:resetBlock()
end
@@ -1814,7 +1921,7 @@ do
end
end
local yomi = ""
local yomi=''
piece.spin,piece.mini=dospin,false
piece.pc,piece.hpc=false,false
@@ -1825,7 +1932,7 @@ do
cscore=(spinSCR[C.name] or spinSCR[8])[cc]
if self.b2b>800 then
self:showText(text.b3b..text.block[C.name]..text.spin..text.clear[cc],0,-30,35,'stretch')
yomi = yomi..text.b3b..text.block[C.name]..text.spin..text.clear[cc]
yomi=yomi..text.b3b..text.block[C.name]..text.spin..text.clear[cc]
atk=b2bATK[cc]+cc*.5
exblock=exblock+1
cscore=cscore*2
@@ -1835,7 +1942,7 @@ do
end
elseif self.b2b>=50 then
self:showText(text.b2b..text.block[C.name]..text.spin..text.clear[cc],0,-30,35,'spin')
yomi = yomi..text.b2b..text.block[C.name]..text.spin..text.clear[cc]
yomi=yomi..text.b2b..text.block[C.name]..text.spin..text.clear[cc]
atk=b2bATK[cc]
cscore=cscore*1.2
Stat.b2b=Stat.b2b+1
@@ -1844,13 +1951,13 @@ do
end
else
self:showText(text.block[C.name]..text.spin..text.clear[cc],0,-30,45,'spin')
yomi = yomi..text.block[C.name]..text.spin..text.clear[cc]
yomi=yomi..text.block[C.name]..text.spin..text.clear[cc]
atk=2*cc
end
sendTime=20+atk*20
if mini then
self:showText(text.mini,0,-80,35,'appear')
yomi = text.mini..' '..yomi
yomi=text.mini..' '..yomi
atk=atk*.25
sendTime=sendTime+60
cscore=cscore*.5
@@ -1871,7 +1978,7 @@ do
cscore=clearSCR[cc]
if self.b2b>800 then
self:showText(text.b3b..text.clear[cc],0,-30,50,'fly')
yomi = text.b3b..text.clear[cc]..yomi
yomi=text.b3b..text.clear[cc]..yomi
atk=4*cc-10
sendTime=100
exblock=exblock+1
@@ -1882,7 +1989,7 @@ do
end
elseif self.b2b>=50 then
self:showText(text.b2b..text.clear[cc],0,-30,50,'drive')
yomi = text.b2b..text.clear[cc]..yomi
yomi=text.b2b..text.clear[cc]..yomi
sendTime=80
atk=3*cc-7
cscore=cscore*1.3
@@ -1892,7 +1999,7 @@ do
end
else
self:showText(text.clear[cc],0,-30,70,'stretch')
yomi = text.clear[cc]..yomi
yomi=text.clear[cc]..yomi
sendTime=60
atk=2*cc-4
end
@@ -1900,7 +2007,7 @@ do
piece.special=true
else
self:showText(text.clear[cc],0,-30,35,'appear',(8-cc)*.3)
yomi = text.clear[cc]..yomi
yomi=text.clear[cc]..yomi
atk=cc-.5
sendTime=20+floor(atk*20)
cscore=cscore+clearSCR[cc]
@@ -1919,7 +2026,7 @@ do
atk=atk+1
end
self:showText(text.cmb[min(cmb,21)],0,25,15+min(cmb,15)*5,cmb<10 and 'appear' or 'flicker')
yomi = yomi..' '..text.cmb[min(cmb,21)]
yomi=yomi..' '..text.cmb[min(cmb,21)]
cscore=cscore+min(50*cmb,500)*(2*cc-1)
end
@@ -2447,6 +2554,28 @@ local function _updateFX(P,dt)
end
end
end
function Player:resolveIRS()
if self.bufferedIHS then
self:hold(true)
self.bufferedIHS=false
end
self.bufferedIRS=false
local pressing=self.keyPressing
if pressing[5] then
self:act_rot180()
elseif pressing[3] then
if pressing[4] then
self:act_rot180()
else
self:act_rotRight()
end
elseif pressing[4] then
self:act_rotLeft()
end
end
local function update_alive(P,dt)
local ENV=P.gameEnv
@@ -2505,6 +2634,18 @@ local function update_alive(P,dt)
end
end
-- Buffer IRS after IRS cut delay has elapsed.
-- The purpose of this is to allow the player to release their rotate key during the IRS cut delay,
-- which will allow them to avoid accidentally using IRS.
if P.bufferedDelay then
P.bufferedDelay=P.bufferedDelay-1
if P.bufferedDelay<=0 then
if P.bufferedIRS then
P:resolveIRS()
end
end
end
-- Moving pressed
if P.movDir~=0 then
local das,arr=ENV.das,ENV.arr
@@ -2701,44 +2842,32 @@ local function update_alive(P,dt)
end
local function update_streaming(P)
local eventTime=P.stream[P.streamProgress]
while eventTime and P.frameRun==eventTime do
while eventTime and P.frameRun==eventTime or eventTime==0 do
local event=P.stream[P.streamProgress+1]
if event==0 then-- Just wait
elseif event<=32 then-- Press key
P:pressKey(event)
elseif event<=64 then-- Release key
P:releaseKey(event-32)
elseif event>0x2000000000000 then-- Sending lines
local sid=event%0x100
local amount=floor(event/0x100)%0x100
local time=floor(event/0x10000)%0x10000
local line=floor(event/0x100000000)%0x10000
for _,p in next,PLY_ALIVE do
if p.sid==sid then
P.netAtk=P.netAtk+amount
if P.netAtk~=P.stat.send then-- He cheated or just desynchronized to death
MES.new('warn',"#"..P.uid.." desynchronized")
NET.player_finish({reason='desync'})
P:lose(true)
return
end
P:attack(p,amount,time,line,true)
P:createBeam(p,amount)
elseif event<=128 then-- Extra Event
local eventName=P.gameEnv.extraEvent[event-64][1]
local eventParamCount=P.gameEnv.extraEvent[event-64][2]
local sourceSid=P.stream[P.streamProgress+2]
local paramList={}
for i=1,eventParamCount do
ins(paramList,P.stream[P.streamProgress+2+i])
end
P.streamProgress=P.streamProgress+eventParamCount+1
local SRC
for _,p in next,PLAYERS do
if p.sid==sourceSid then
SRC=p
break
end
end
elseif event>0x1000000000000 then-- Receiving lines
local sid=event%0x100
for _,p in next,PLY_ALIVE do
if p.sid==sid then
P:receive(
p,
floor(event/0x100)%0x100,-- amount
floor(event/0x10000)%0x10000,-- time
floor(event/0x100000000)%0x10000-- line
)
break
end
if SRC then
P.gameEnv.extraEventHandler[eventName](P,SRC,unpack(paramList))
end
end
P.streamProgress=P.streamProgress+2
@@ -2804,7 +2933,7 @@ function Player:update(dt)
end
while self.trigFrame>=1 do
if self.streamProgress then
local frameDelta-- Time between now and end of stream
local dataDelta -- How much data wating to be process
if self.type=='remote' then
if self.loseTimer then
self.loseTimer=self.loseTimer-1
@@ -2813,25 +2942,26 @@ function Player:update(dt)
self:lose(true)
end
end
frameDelta=(self.stream[#self.stream-1] or 0)-self.frameRun
if frameDelta==0 then frameDelta=nil end
dataDelta=#self.stream-self.streamProgress
else
frameDelta=0
dataDelta=1
end
if frameDelta then
if dataDelta>0 then
for _=1,
self.loseTimer and min(frameDelta,
-- Speed up to finish
self.loseTimer and min(dataDelta,
self.loseTimer>16 and 2 or
self.loseTimer>6.2 and 12 or
self.loseTimer>2.6 and 260 or
2600
) or
frameDelta<26 and 1 or
frameDelta<50 and 2 or
frameDelta<80 and 3 or
frameDelta<120 and 5 or
frameDelta<160 and 7 or
frameDelta<200 and 10 or
-- Chasing faster when slower
dataDelta<26 and 1 or
dataDelta<42 and 2 or
dataDelta<62 and 3 or
dataDelta<70.23 and 5 or
dataDelta<94.2 and 7 or
dataDelta<126 and 10 or
20
do
update_streaming(self)
@@ -2880,7 +3010,7 @@ function Player:revive()
SFX.play('emit')
end
function Player:torikanEnd(requiredTime)
if self.stat.time < requiredTime then
if self.stat.time<requiredTime then
return false
end
self:_die()

View File

@@ -120,20 +120,17 @@ local seqGenerators={
-- Pick a mino from pool
local tryTime=0
local r
repeat-- ::REPEAT_pickAgain::
local pickAgain
repeat
r=_poolPick()-- Random mino-index in pool
local duplicated
for i=1,len do
if r==history[i] then
tryTime=tryTime+1
if tryTime<hisLen then
pickAgain=true
break-- goto REPEAT_pickAgain
end
duplicated=true
break
end
end
if not pickAgain then break end
until true
tryTime=tryTime+1
until not duplicated or tryTime>hisLen
-- Give mino to player & update history
if history[1]~=0 then