Last active
November 22, 2023 02:30
-
-
Save rdococ/433a3c6461b187403a2f4f1f3f5ec0cd to your computer and use it in GitHub Desktop.
Tachyons in TPT
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--[[Tachyons v0.27 | |
= How it works = | |
1. Track all the tachyons and negatachyons in the current frame. | |
2. If any tachyons are created, rewind the game, reconstructing the tachyon on every frame to determine its time-reversed trajectory. | |
3. Once all tachyons are destroyed, let the simulation run forwards again, constructing negatachyons when and where the tachyons were destroyed. | |
4. Once the game catches back up, if any negatachyons can't trace their trajectory to a tachyon, rewind to erase them from the timeline. | |
= Notes = | |
- Tachyon/negatachyon pairs are detected leniently in an attempt to prevent false positives. When a negatachyon dies, it must be within line of sight of a tachyon created at the exact same position as itself. | |
- If you set up a machine that always creates a tachyon only if it doesn't detect a tachyon, the game will rewind endlessly. This is a time paradox. | |
= Fields = | |
LIFE: Tracks a tachyon's age. | |
TMP2: A (hopefully) unique ID for every tachyon. Used to destroy time paradoxed negatachyons. | |
TMP3: Flag. Indicates that a newly created tachyon should be removed silently (like if rewinding) or that a tachyon/negatachyon pair has successfully cancelled out. | |
TMP4: A non-unique ID based on the tachyon's starting position. | |
]] | |
local elem, sim = elements, sim | |
local random, floor, cos, sin, atan2, sqrt, min, max, abs = math.random, math.floor, math.cos, math.sin, math.atan2, math.sqrt, math.min, math.max, math.abs | |
local pi = math.pi | |
local function round(x) | |
return floor(x + 0.5) | |
end | |
local function alloc(...) | |
local els = {} | |
for _, id in ipairs({...}) do | |
table.insert(els, elements.allocate("tachyons", id)) | |
end | |
return unpack(els) | |
end | |
local function inBounds(x, y) | |
return max(min(x, sim.XRES - 5), 4), max(min(y, sim.YRES - 5), 4) | |
end | |
local NTCH, TACH = alloc("ntch", "tach") | |
local TIME_TOLERANCE = 1 | |
local TACH_SPEED, TACH_TOP_SPEED, TACH_LIFETIME = 4, 4, 190 | |
local speedFactor = (TACH_TOP_SPEED/TACH_SPEED)^(1/TACH_LIFETIME) | |
-- 'Doomed' negatachyons have been grandfather paradoxed and must be erased from the timeline. | |
local doomedTachyons, doomedCount = {}, 0 | |
-- On every frame, all tachyons and negatachyons add themselves to these lists for tracking purposes. | |
local tachyons, negatachyons = {}, {} | |
local newTachyons = {} -- Tachyons whose life is below TIME_TOLERANCE | |
local rewinding = false | |
local rewindTick = 0 | |
-- Tachyons that die during the rewinding phase are tracked to reconstruct the negatachyons at the right times. | |
local tachyonDeaths = {} | |
local function doomTachyon(tach) | |
local uuid = sim.partProperty(tach, sim.FIELD_TMP2) | |
if doomedTachyons[uuid] then return end | |
doomedTachyons[uuid] = tach | |
doomedCount = doomedCount + 1 | |
end | |
local function pickAngle(self) | |
local angle = random(0, 7) / 4 * pi | |
local speed = 4 | |
sim.partProperty(self, sim.FIELD_VX, cos(angle) * TACH_SPEED) | |
sim.partProperty(self, sim.FIELD_VY, sin(angle) * TACH_SPEED) | |
end | |
local function accelerate(self) | |
local vx, vy = sim.partProperty(self, sim.FIELD_VX), sim.partProperty(self, sim.FIELD_VY) | |
local speed = sqrt(vx * vx + vy * vy) | |
speed = speed * speedFactor | |
local angle = atan2(vy, vx) | |
sim.partProperty(self, sim.FIELD_VX, cos(angle) * speed) | |
sim.partProperty(self, sim.FIELD_VY, sin(angle) * speed) | |
end | |
local function decelerate(self) | |
local vx, vy = sim.partProperty(self, sim.FIELD_VX), sim.partProperty(self, sim.FIELD_VY) | |
local speed = sqrt(vx * vx + vy * vy) | |
speed = speed / speedFactor | |
local angle = atan2(vy, vx) | |
sim.partProperty(self, sim.FIELD_VX, cos(angle) * speed) | |
sim.partProperty(self, sim.FIELD_VY, sin(angle) * speed) | |
end | |
local function cancelNegatachyons(self, x, y) | |
for _, other in ipairs(negatachyons) do | |
if sim.partProperty(other, sim.FIELD_TYPE) == NTCH then | |
local otherX, otherY = sim.partProperty(other, sim.FIELD_X), sim.partProperty(other, sim.FIELD_Y) | |
local vx, vy = sim.partProperty(other, sim.FIELD_VX), sim.partProperty(other, sim.FIELD_VY) | |
local dx, dy = otherX - x, otherY - y | |
local speed = max(sqrt(vx * vx + vy * vy), TACH_SPEED) | |
local dist = sqrt(dx * dx + dy * dy) | |
local dirx, diry = dx / dist, dy / dist | |
local originId = sim.partProperty(self, sim.FIELD_TMP4) | |
local otherOId = sim.partProperty(other, sim.FIELD_TMP4) | |
local originX, originY = originId % sim.XRES, floor(originId / sim.XRES) | |
local otherOX, otherOY = otherOId % sim.XRES, floor(otherOId / sim.XRES) | |
if originId == otherOId then | |
local success = true | |
if dist > 0 then | |
for i = 0, dist, speed do | |
local obstacle = sim.pmap(otherX - dirx * dist, otherY - diry * dist) | |
if obstacle and sim.can_move(NTCH, obstacle) ~= 2 then | |
success = false | |
break | |
end | |
end | |
end | |
if success then | |
local otherX, otherY = sim.partProperty(other, sim.FIELD_X), sim.partProperty(other, sim.FIELD_Y) | |
sim.partProperty(other, sim.FIELD_TMP3, 1) | |
sim.partKill(other) | |
sim.partProperty(self, sim.FIELD_TMP3, 1) | |
sim.partKill(self) | |
return true | |
end | |
end | |
end | |
end | |
end | |
local function decay(x, y) | |
for i = 1, 8 do | |
sim.partProperty(sim.partCreate(-3, x, y, elem.DEFAULT_PT_PHOT), sim.FIELD_CTYPE, 32767) | |
end | |
end | |
local function graphicsFunc(self, r, g, b) | |
return 1, 0x00010001, 255, r, g, b, 255, r, g, b | |
end | |
elem.element(TACH, elem.element(elem.DEFAULT_PT_NEUT)) | |
elem.property(TACH, "Name", "TACH") | |
elem.property(TACH, "Description", "Tachyons. Reverse the flow of time, decays into photons. Use sparingly.") | |
elem.property(TACH, "Color", 0x4400FF) | |
elem.property(TACH, "Collision", -0.99) | |
elem.property(TACH, "Properties", elem.TYPE_ENERGY) | |
elem.property(TACH, "Create", function (self, x, y, type, v) | |
sim.partProperty(self, sim.FIELD_TEMP, 1273.15) | |
if v and v ~= 0 then return end | |
if rewinding then | |
sim.partProperty(self, sim.FIELD_TMP3, 1) | |
sim.partKill(self) | |
return | |
end | |
sim.partProperty(self, sim.FIELD_TMP4, round(y) * sim.XRES + round(x)) | |
pickAngle(self) | |
table.insert(tachyons, self) | |
table.insert(newTachyons, self) | |
end) | |
elem.property(TACH, "Update", function (self, x, y) | |
local life = sim.partProperty(self, sim.FIELD_LIFE) | |
if life > TACH_LIFETIME then | |
sim.partKill(self) | |
return | |
end | |
sim.partProperty(self, sim.FIELD_LIFE, life + 1) | |
accelerate(self) | |
table.insert(tachyons, self) | |
if life <= 10 then | |
table.insert(newTachyons, self) | |
end | |
end) | |
elem.property(TACH, "Graphics", graphicsFunc) | |
elem.property(TACH, "ChangeType", function (self, x, y, type, new) | |
if new ~= 0 then return end | |
if sim.partProperty(self, sim.FIELD_TMP3) == 1 then return end | |
if rewindTick > 0 then | |
local props = {} | |
for _, prop in ipairs {sim.FIELD_X, sim.FIELD_Y, sim.FIELD_VX, sim.FIELD_VY, sim.FIELD_TEMP, sim.FIELD_LIFE, sim.FIELD_TMP4} do | |
props[prop] = sim.partProperty(self, prop) | |
end | |
-- props[sim.FIELD_X], props[sim.FIELD_Y] = inBounds(props[sim.FIELD_X], props[sim.FIELD_Y]) | |
table.insert(tachyonDeaths[rewindTick], props) | |
end | |
end) | |
elem.element(NTCH, elem.element(TACH)) | |
elem.property(NTCH, "Name", "NTCH") | |
elem.property(NTCH, "Description", "Negative tachyons, tachyons travelling forward in time.") | |
elem.property(NTCH, "MenuVisible", 0) | |
elem.property(NTCH, "Properties", elem.TYPE_ENERGY) | |
elem.property(NTCH, "Create", function (self, x, y, type, v) | |
local uuid = random(-2147483647, 2147483647) | |
if uuid <= 0 then uuid = uuid - 1 end | |
sim.partProperty(self, sim.FIELD_TMP2, uuid) | |
if not v or v == 0 then | |
sim.partProperty(self, sim.FIELD_TMP3, 1) | |
sim.partProperty(self, sim.FIELD_LIFE, TACH_LIFETIME) | |
pickAngle(self) | |
end | |
end) | |
elem.property(NTCH, "Update", function (self, x, y) | |
local life = sim.partProperty(self, sim.FIELD_LIFE) | |
if life <= TIME_TOLERANCE then | |
sim.partProperty(self, sim.FIELD_VX, 0) | |
sim.partProperty(self, sim.FIELD_VY, 0) | |
if sim.partProperty(self, sim.FIELD_TMP3) == 1 then | |
decay(x, y) | |
sim.partKill(self) | |
return | |
end | |
end | |
if life <= 0 then | |
for part in sim.parts() do | |
if sim.partProperty(part, sim.FIELD_TYPE) == NTCH then | |
local partLife = sim.partProperty(part, sim.FIELD_LIFE) | |
if partLife <= TIME_TOLERANCE then | |
sim.partKill(part) | |
end | |
end | |
end | |
sim.partKill(self) | |
return | |
end | |
if life > TIME_TOLERANCE then | |
decelerate(self) | |
end | |
sim.partProperty(self, sim.FIELD_LIFE, life - 1) | |
table.insert(negatachyons, self) | |
end) | |
elem.property(NTCH, "Graphics", graphicsFunc) | |
elem.property(NTCH, "ChangeType", function (self, x, y, type, new) | |
-- A negatachyon was destroyed. If it was destroyed too soon, we must erase it from the timeline. | |
if new ~= 0 then return end | |
if sim.partProperty(self, sim.FIELD_TMP3) == 1 then return end | |
if rewinding then return end | |
-- This is an (attempted) workaround for weird behaviour when particles disappear off the simulation edges. | |
x, y = inBounds(x, y) | |
if sim.partProperty(self, sim.FIELD_LIFE) > TIME_TOLERANCE then | |
-- I think this part works around a TPT bug where particles are destroyed instead of bouncing...? | |
local part = sim.partCreate(-3, x, y, NTCH, 1) | |
local vx, vy = -sim.partProperty(self, sim.FIELD_VX), -sim.partProperty(self, sim.FIELD_VY) | |
sim.partProperty(part, sim.FIELD_VX, vx) | |
sim.partProperty(part, sim.FIELD_VY, vy) | |
local life = sim.partProperty(self, sim.FIELD_LIFE) | |
if part > self then life = life + 1 end | |
sim.partProperty(part, sim.FIELD_LIFE, life) | |
local uuid = sim.partProperty(self, sim.FIELD_TMP2) | |
sim.partProperty(part, sim.FIELD_TMP2, uuid) | |
sim.partProperty(part, sim.FIELD_TMP4, sim.partProperty(self, sim.FIELD_TMP4)) | |
return | |
end | |
doomTachyon(self) | |
end) | |
-- Time is playing forwards. Create negatachyons at each point where tachyons were destroyed while rewinding | |
local function unwind() | |
if rewindTick <= 0 then return end | |
for _, data in ipairs(tachyonDeaths[rewindTick]) do | |
local x, y, vx, vy = data[sim.FIELD_X], data[sim.FIELD_Y], -data[sim.FIELD_VX], -data[sim.FIELD_VY] | |
x, y = x + vx, y + vy | |
if sim.edgeMode() == 0 then | |
x, y = min(max(x, 4), sim.XRES - 5), min(max(y, 4), sim.YRES - 5) | |
end | |
local part = sim.partCreate(-3, x, y, NTCH, 1) | |
sim.partProperty(part, sim.FIELD_VX, vx) | |
sim.partProperty(part, sim.FIELD_VY, vy) | |
sim.partProperty(part, sim.FIELD_TEMP, data[sim.FIELD_TEMP]) | |
sim.partProperty(part, sim.FIELD_LIFE, data[sim.FIELD_LIFE] + TIME_TOLERANCE) | |
sim.partProperty(part, sim.FIELD_TMP4, data[sim.FIELD_TMP4]) | |
decay(data[sim.FIELD_X], data[sim.FIELD_Y]) | |
end | |
rewindTick = rewindTick - 1 | |
end | |
event.register(event.beforesim, function () | |
if not rewinding then | |
sim.takeSnapshot() | |
end | |
end) | |
event.register(event.aftersim, function () | |
-- Adjacent tachyon/negatachyon pairs are assumed to be the same particle and removed | |
-- If there is no time paradox, eventually *all* tachyons will be cancelled out this way | |
if sim.elementCount(NTCH) > 0 and sim.elementCount(TACH) > 0 then | |
for _, part in ipairs(newTachyons) do | |
if part and sim.partExists(part) and sim.partProperty(part, sim.FIELD_TYPE) == TACH and sim.partProperty(part, sim.FIELD_TMP3) == 0 then | |
cancelNegatachyons(part, sim.partProperty(part, sim.FIELD_X), sim.partProperty(part, sim.FIELD_Y)) | |
end | |
end | |
end | |
tachyons, negatachyons = {}, {} | |
newTachyons = {} | |
rewinding = sim.elementCount(TACH) > 0 or doomedCount > 0 | |
if not rewinding then | |
unwind() | |
return | |
end | |
-- Rewinding: collect all existing tachyons so we can reinstate them "later" | |
local props = {sim.FIELD_X, sim.FIELD_Y, sim.FIELD_VX, sim.FIELD_VY, sim.FIELD_LIFE, sim.FIELD_TMP4} | |
local tachyonData = {} | |
for part in sim.parts() do | |
if sim.partProperty(part, sim.FIELD_TYPE) == TACH then | |
local data = {} | |
for _, prop in ipairs(props) do | |
data[prop] = sim.partProperty(part, prop) | |
end | |
tachyonData[part] = data | |
end | |
end | |
if not sim.historyRestore() then | |
-- If we can't rewind any further, kill all present tachyons and unwind | |
for part, _ in pairs(tachyonData) do | |
sim.partKill(part) | |
end | |
doomedTachyons, doomedCount = {}, 0 | |
unwind() | |
return | |
end | |
rewindTick = rewindTick + 1 | |
tachyonDeaths[rewindTick] = {} | |
-- Erase negatachyons from the timeline if their UUIDs correspond to that of a grandfather paradoxed tachyon | |
local clearedTachyons = {} | |
for uuid, part in pairs(doomedTachyons) do | |
if not (part and sim.partExists(part) and sim.partProperty(part, sim.FIELD_TYPE) == NTCH and sim.partProperty(part, sim.FIELD_TMP2) == uuid) then | |
for candidate in sim.parts() do | |
if sim.partProperty(candidate, sim.FIELD_TYPE) == NTCH and sim.partProperty(candidate, sim.FIELD_TMP2) == uuid then | |
part = candidate | |
doomedTachyons[uuid] = part | |
break | |
end | |
end | |
end | |
if part and sim.partExists(part) and sim.partProperty(part, sim.FIELD_TYPE) == NTCH and sim.partProperty(part, sim.FIELD_TMP2) == uuid then | |
sim.partProperty(part, sim.FIELD_TMP3, 1) | |
sim.partKill(part) | |
else | |
clearedTachyons[uuid] = true | |
end | |
end | |
for uuid, _ in pairs(clearedTachyons) do | |
doomedTachyons[uuid] = nil | |
doomedCount = doomedCount - 1 | |
end | |
-- Replace all the tachyons now that we've rewound | |
for _, data in pairs(tachyonData) do | |
local part = sim.partCreate(-3, data[sim.FIELD_X], data[sim.FIELD_Y], TACH, 1) | |
for prop, value in pairs(data) do | |
sim.partProperty(part, prop, value) | |
end | |
end | |
end) | |
local function vhsLine(y, alpha) | |
local x = 0 | |
local streak = random(1, 2) == 1 | |
while x < graphics.WIDTH do | |
local length = streak and random(1, 10) or random(1, 50) | |
if streak then | |
graphics.drawLine(x, y, x + length, y, 255, 255, 255, alpha * 128) | |
end | |
streak = not streak | |
x = x + length | |
end | |
end | |
local function vhsBand(startY, endY) | |
for y = startY, endY do | |
local dist = (y - startY) / (endY - startY) | |
local y = y % graphics.HEIGHT | |
vhsLine(y, min((1 - abs(1 - dist * 2) * 2) * 5, 1)) | |
end | |
end | |
local vhsTick = 0 | |
event.register(event.tick, function () | |
if rewinding then | |
graphics.drawText(16, 30, "REWIND <<") | |
local offset = vhsTick | |
local h = graphics.HEIGHT | |
vhsBand(h * 1.75/3 + offset, h * 2/3 + offset) | |
vhsBand(h * 2.15/3 + offset, h * 2.2/3 + offset) | |
vhsTick = vhsTick + 1 | |
else | |
vhsTick = 0 | |
end | |
end) | |
local w, h = 255, 110 | |
local info = Window:new(graphics.WIDTH / 2 - w / 2, graphics.HEIGHT / 2 - h / 2, w, h) | |
local label = Label:new(27.5, 25, 200, 44, [[ | |
For tachyons to work you must: | |
1. Go to Simulation Options. | |
2. Scroll down and click "Open Data Folder". | |
3. Open powder.pref. | |
4. Find the "UndoHistoryLimit" and set it to 200. | |
Actual UNDO functionality will no longer work.]]) | |
local ok = Button:new(0, h - 15, w, 15, "OK") | |
ok:action(function() interface.closeWindow(info) end) | |
info:addComponent(label) | |
info:addComponent(ok) | |
interface.showWindow(info) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment