mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-18 11:44:42 +02:00
initial commit
This commit is contained in:
@@ -0,0 +1,357 @@
|
||||
-- Link play UI: one player hosts (the screen shows their LAN address),
|
||||
-- the other joins by typing that address in. Direct peer-to-peer over
|
||||
-- lua-enet (bundled with LÖVE), no relay server.
|
||||
|
||||
local Font = require("src.render.Font")
|
||||
local Net = require("src.link.Net")
|
||||
local Protocol = require("src.link.Protocol")
|
||||
local TextBox = require("src.render.TextBox")
|
||||
|
||||
local LinkState = {}
|
||||
LinkState.__index = LinkState
|
||||
LinkState.isOpaque = true
|
||||
|
||||
local CURSOR = 0xED
|
||||
|
||||
-- the joiner edits an IPv4 address as 12 digits (three per octet),
|
||||
-- prefilled with our own LAN IP so usually only the tail needs changing
|
||||
local function ipDigits(ip)
|
||||
local digits = {}
|
||||
local a, b, c, d = (ip or ""):match("^(%d+)%.(%d+)%.(%d+)%.(%d+)$")
|
||||
local octets = { tonumber(a) or 192, tonumber(b) or 168,
|
||||
tonumber(c) or 0, tonumber(d) or 1 }
|
||||
for _, o in ipairs(octets) do
|
||||
o = math.min(255, o)
|
||||
table.insert(digits, math.floor(o / 100))
|
||||
table.insert(digits, math.floor(o / 10) % 10)
|
||||
table.insert(digits, o % 10)
|
||||
end
|
||||
return digits
|
||||
end
|
||||
|
||||
function LinkState.new(game)
|
||||
local self = setmetatable({}, LinkState)
|
||||
self.game = game
|
||||
self.stage = "menu"
|
||||
self.index = 1
|
||||
self.addr = ipDigits(Net.lanIP())
|
||||
self.addrPos = 12 -- the last octet is what usually differs
|
||||
self.status = ""
|
||||
return self
|
||||
end
|
||||
|
||||
function LinkState:exitWith(message)
|
||||
if self.net then self.net:close() end
|
||||
self.game.stack:pop()
|
||||
if message then
|
||||
self.game.stack:push(TextBox.new(self.game, message))
|
||||
end
|
||||
end
|
||||
|
||||
-- -------------------------------------------------------------------
|
||||
-- update
|
||||
-- -------------------------------------------------------------------
|
||||
|
||||
function LinkState:update(dt)
|
||||
local input = self.game.input
|
||||
if self.net then
|
||||
self.net:update()
|
||||
if self.net.error and self.stage ~= "menu" then
|
||||
self:exitWith("Link error:\n" .. self.net.error:sub(1, 60))
|
||||
return
|
||||
end
|
||||
-- the peer vanished without a bye (only once the inbox is drained,
|
||||
-- so a final message travelling with the disconnect still counts)
|
||||
if self.net.closed and #self.net.inbox == 0
|
||||
and self.stage ~= "menu" and self.stage ~= "addrEntry"
|
||||
and self.stage ~= "battleRunning" then
|
||||
self:exitWith("The link was\nbroken.")
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
if self.stage == "menu" then
|
||||
if input:wasPressed("up") or input:wasPressed("down") then
|
||||
self.index = self.index == 1 and 2 or 1
|
||||
elseif input:wasPressed("b") then
|
||||
self:exitWith(nil)
|
||||
elseif input:wasPressed("a") then
|
||||
self.net = Net.new()
|
||||
if self.index == 1 then
|
||||
if self.net:host() then
|
||||
self.stage = "hosting"
|
||||
else
|
||||
self:exitWith("Link error:\n" .. (self.net.error or "?"))
|
||||
end
|
||||
else
|
||||
self.stage = "addrEntry"
|
||||
end
|
||||
end
|
||||
|
||||
elseif self.stage == "hosting" then
|
||||
if input:wasPressed("b") then self:exitWith(nil) return end
|
||||
if self.net.paired then
|
||||
self.stage = "modeSelect"
|
||||
self.index = 1
|
||||
end
|
||||
|
||||
elseif self.stage == "addrEntry" then
|
||||
if input:wasPressed("b") then self:exitWith(nil) return end
|
||||
if input:wasPressed("up") then
|
||||
self.addr[self.addrPos] = (self.addr[self.addrPos] + 1) % 10
|
||||
elseif input:wasPressed("down") then
|
||||
self.addr[self.addrPos] = (self.addr[self.addrPos] - 1) % 10
|
||||
elseif input:wasPressed("left") then
|
||||
self.addrPos = math.max(1, self.addrPos - 1)
|
||||
elseif input:wasPressed("right") then
|
||||
self.addrPos = math.min(12, self.addrPos + 1)
|
||||
elseif input:wasPressed("a") then
|
||||
local octets = {}
|
||||
for i = 1, 4 do
|
||||
local base = (i - 1) * 3
|
||||
octets[i] = math.min(255, self.addr[base + 1] * 100
|
||||
+ self.addr[base + 2] * 10
|
||||
+ self.addr[base + 3])
|
||||
end
|
||||
if self.net:join(table.concat(octets, ".")) then
|
||||
self.stage = "joining"
|
||||
else
|
||||
self:exitWith("Link error:\n" .. (self.net.error or "?"))
|
||||
end
|
||||
end
|
||||
|
||||
elseif self.stage == "joining" then
|
||||
if input:wasPressed("b") then self:exitWith(nil) return end
|
||||
if self.net.paired then
|
||||
self.stage = "waitMode"
|
||||
end
|
||||
|
||||
elseif self.stage == "modeSelect" then -- host picks
|
||||
if input:wasPressed("up") or input:wasPressed("down") then
|
||||
self.index = self.index == 1 and 2 or 1
|
||||
elseif input:wasPressed("a") then
|
||||
local mode = self.index == 1 and "trade" or "battle"
|
||||
self.net:send({ type = "hello", name = self.game.save.player.name, mode = mode })
|
||||
self:startMode(mode, true)
|
||||
elseif input:wasPressed("b") then
|
||||
self:exitWith(nil)
|
||||
end
|
||||
|
||||
elseif self.stage == "waitMode" then -- guest waits for host's pick
|
||||
if input:wasPressed("b") then self:exitWith(nil) return end
|
||||
local msgs = self.net:poll()
|
||||
for i, msg in ipairs(msgs) do
|
||||
if msg.type == "hello" then
|
||||
self.peerName = msg.name
|
||||
self:startMode(msg.mode, false)
|
||||
-- the host's next messages (party, ...) can share this batch;
|
||||
-- put them back so the new stage's poll sees them
|
||||
for j = #msgs, i + 1, -1 do
|
||||
table.insert(self.net.inbox, 1, msgs[j])
|
||||
end
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
elseif self.stage == "trade" then
|
||||
self:updateTrade(input)
|
||||
|
||||
elseif self.stage == "battleWait" then
|
||||
if input:wasPressed("b") then self:exitWith(nil) return end
|
||||
local msgs = self.net:poll()
|
||||
for i, msg in ipairs(msgs) do
|
||||
if msg.type == "party" then
|
||||
local LinkBattle = require("src.link.LinkBattle")
|
||||
local opts = {
|
||||
myParty = Protocol.packParty(self.game.save.party),
|
||||
theirParty = msg.mons,
|
||||
theirName = self.peerName or "FOE",
|
||||
seed = self.isHost and self.linkSeed or msg.seed,
|
||||
}
|
||||
if self.isHost then
|
||||
self.game.stack:push(LinkBattle.newHost(self.game, self.net, opts))
|
||||
else
|
||||
self.game.stack:push(LinkBattle.newGuest(self.game, self.net, opts))
|
||||
end
|
||||
self.stage = "battleRunning"
|
||||
for j = #msgs, i + 1, -1 do
|
||||
table.insert(self.net.inbox, 1, msgs[j])
|
||||
end
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
elseif self.stage == "battleRunning" then
|
||||
if self.game.stack:top() == self then
|
||||
self:exitWith(nil) -- battle finished
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function LinkState:startMode(mode, isHost)
|
||||
self.isHost = isHost
|
||||
if mode == "trade" then
|
||||
self.stage = "trade"
|
||||
self.trade = Protocol.TradeSession.new(self.game.data, self.game.save.party)
|
||||
self.net:send({ type = "party", mons = Protocol.packParty(self.game.save.party) })
|
||||
self.index = 1
|
||||
else
|
||||
self.stage = "battleWait"
|
||||
-- the host deals the shared RNG seed for the lockstep simulation
|
||||
if isHost then
|
||||
self.linkSeed = love.math.random(1, 2 ^ 30)
|
||||
end
|
||||
self.net:send({ type = "party",
|
||||
mons = Protocol.packParty(self.game.save.party),
|
||||
seed = self.linkSeed })
|
||||
end
|
||||
end
|
||||
|
||||
-- -------------------------------------------------------------------
|
||||
-- trade flow
|
||||
-- -------------------------------------------------------------------
|
||||
|
||||
function LinkState:updateTrade(input)
|
||||
for _, msg in ipairs(self.net:poll()) do
|
||||
self.trade:handle(msg)
|
||||
end
|
||||
local t = self.trade
|
||||
|
||||
if t.stage == "cancelled" then
|
||||
self:exitWith("The trade was\ncancelled.")
|
||||
return
|
||||
end
|
||||
if t.stage == "done" then
|
||||
local sent = t.party[t.myPick]
|
||||
local received, evoTo = t:apply(self.game)
|
||||
local name = received.nickname or self.game.data.pokemon[received.species].name
|
||||
self.net:close()
|
||||
self.game.stack:pop()
|
||||
local game = self.game
|
||||
require("src.core.Sound").play(game.data, "Trade_Machine")
|
||||
local TradeAnim = require("src.ui.TradeAnim")
|
||||
game.stack:push(TradeAnim.new(game, {
|
||||
sent = sent, received = received,
|
||||
onDone = function()
|
||||
game.stack:push(TextBox.new(game,
|
||||
("Trade completed!\f%s received\n%s!"):format(game.save.player.name, name),
|
||||
function()
|
||||
if evoTo then
|
||||
require("src.pokemon.Evolution").evolve(game, received, evoTo)
|
||||
end
|
||||
end))
|
||||
end,
|
||||
}))
|
||||
return
|
||||
end
|
||||
|
||||
if t.stage == "picking" and input:wasPressed("up") then
|
||||
self.index = math.max(1, self.index - 1)
|
||||
elseif t.stage == "picking" and input:wasPressed("down") then
|
||||
self.index = math.min(#self.game.save.party, self.index + 1)
|
||||
elseif self.confirmed == nil and input:wasPressed("b") then
|
||||
-- once confirm=true has been sent to the peer, backing out here
|
||||
-- would desync the two sides (the peer may already be committing
|
||||
-- the trade) -- B is dead after that, matching the A branch's own
|
||||
-- self.confirmed == nil guard
|
||||
self.net:send({ type = "bye" })
|
||||
self:exitWith("The trade was\ncancelled.")
|
||||
elseif t.stage == "picking" and input:wasPressed("a") then
|
||||
self.net:send(t:pick(self.index))
|
||||
elseif t.stage == "confirming" and self.confirmed == nil then
|
||||
if input:wasPressed("a") then
|
||||
self.confirmed = true
|
||||
self.net:send(t:confirm(true))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- -------------------------------------------------------------------
|
||||
-- draw
|
||||
-- -------------------------------------------------------------------
|
||||
|
||||
local function drawTitle(text)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.rectangle("fill", 0, 0, 160, 144)
|
||||
love.graphics.setColor(0, 0, 0, 1)
|
||||
Font.draw(text, 8, 6)
|
||||
end
|
||||
|
||||
function LinkState:draw()
|
||||
if self.stage == "menu" then
|
||||
drawTitle("LINK CABLE CLUB")
|
||||
Font.draw("HOST A GAME", 32, 48)
|
||||
Font.draw("JOIN A GAME", 32, 68)
|
||||
Font.drawCode(CURSOR, 24, self.index == 1 and 48 or 68)
|
||||
Font.draw("UDP port " .. Net.defaultPort(), 8, 128)
|
||||
|
||||
elseif self.stage == "hosting" then
|
||||
drawTitle("HOSTING")
|
||||
Font.draw("Friend joins at:", 16, 48)
|
||||
Font.draw(self.net.address or "?", 16, 64)
|
||||
Font.draw("Waiting for join...", 8, 96)
|
||||
|
||||
elseif self.stage == "addrEntry" then
|
||||
drawTitle("ENTER HOST ADDRESS")
|
||||
for i = 1, 12 do
|
||||
local octet = math.floor((i - 1) / 3) -- 0..3
|
||||
local x = 16 + (i - 1) * 8 + octet * 8 -- gap for the dots
|
||||
Font.draw(tostring(self.addr[i]), x, 64)
|
||||
if i == self.addrPos then
|
||||
Font.drawCode(0xEE, x, 76) -- ▼ under the active digit
|
||||
end
|
||||
end
|
||||
for octet = 1, 3 do
|
||||
Font.draw(".", 16 + octet * 32 - 8, 64)
|
||||
end
|
||||
Font.draw("Port: " .. Net.defaultPort(), 16, 96)
|
||||
Font.draw("A: connect B: back", 8, 128)
|
||||
|
||||
elseif self.stage == "joining" then
|
||||
drawTitle("JOINING...")
|
||||
Font.draw("Calling...", 8, 56)
|
||||
Font.draw(self.net.target or "", 8, 72)
|
||||
|
||||
elseif self.stage == "modeSelect" then
|
||||
drawTitle("CONNECTED!")
|
||||
Font.draw("TRADE", 32, 48)
|
||||
Font.draw("BATTLE", 32, 68)
|
||||
Font.drawCode(CURSOR, 24, self.index == 1 and 48 or 68)
|
||||
|
||||
elseif self.stage == "waitMode" then
|
||||
drawTitle("CONNECTED!")
|
||||
Font.draw("Waiting for the", 16, 56)
|
||||
Font.draw("host to choose...", 16, 72)
|
||||
|
||||
elseif self.stage == "trade" then
|
||||
drawTitle("TRADE")
|
||||
local t = self.trade
|
||||
Font.draw("YOURS", 8, 20)
|
||||
for i, mon in ipairs(self.game.save.party) do
|
||||
local def = self.game.data.pokemon[mon.species]
|
||||
Font.draw((mon.nickname or def.name):sub(1, 8), 16, 20 + i * 12)
|
||||
if i == self.index then Font.drawCode(CURSOR, 8, 20 + i * 12) end
|
||||
end
|
||||
Font.draw("THEIRS", 84, 20)
|
||||
for i, mon in ipairs(t.theirParty or {}) do
|
||||
local def = self.game.data.pokemon[mon.species]
|
||||
Font.draw((mon.nickname or def.name):sub(1, 8), 92, 20 + i * 12)
|
||||
if t.theirPick == i then Font.drawCode(CURSOR, 84, 20 + i * 12) end
|
||||
end
|
||||
local hint
|
||||
if t.stage == "waitParty" then hint = "Exchanging data..."
|
||||
elseif t.stage == "picking" then hint = "Pick one to trade"
|
||||
elseif t.stage == "waitPick" then hint = "Waiting for them..."
|
||||
elseif t.stage == "confirming" then
|
||||
hint = self.confirmed and "Waiting..." or "A: trade B: cancel"
|
||||
end
|
||||
Font.draw(hint or "", 8, 132)
|
||||
|
||||
elseif self.stage == "battleWait" or self.stage == "battleRunning" then
|
||||
drawTitle("LINK BATTLE")
|
||||
Font.draw("Exchanging data...", 16, 64)
|
||||
end
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
end
|
||||
|
||||
return LinkState
|
||||
Reference in New Issue
Block a user