diff --git a/content/productivity/protect_planets/index.md b/content/productivity/protect_planets/index.md new file mode 100644 index 00000000..7a727635 --- /dev/null +++ b/content/productivity/protect_planets/index.md @@ -0,0 +1,6 @@ +--- +title: Protect Planets +date: +subtitle: Monitoring if your planet was attacked, and try to get it back when your planet was occupied +version: 0.6.4 +--- diff --git a/content/productivity/protect_planets/protect_planets.js b/content/productivity/protect_planets/protect_planets.js new file mode 100644 index 00000000..f3646696 --- /dev/null +++ b/content/productivity/protect_planets/protect_planets.js @@ -0,0 +1,283 @@ +// Protect Planets +// +// Monitoring if your planet was attacked +// Try to get it back when your planet was occupied +// +// Written by goldenfiredo, goldenfiredo@gmail.com +// + +import { + PlanetType, +} from "https://cdn.skypack.dev/@darkforest_eth/types"; + +const minProtectPlanetLevel = 2; +const minAttackPlanetLevel = 3; +const minEnergyPercentForAttacking = 90; +const usedEnergyPercentForAttacking = 70; +const maxAttackingPlanetNumber = 6; +const minArrivedEnergy = 1000; + +class Plugin { + + constructor() { + this.running = false; + this.intervalId = ''; + this.minPlanetLevel = minProtectPlanetLevel; + this.displayWarning = true; + this.attacking = true; + this.planetIdsUnderAttack = []; + this.planetIdsOccupied = []; + this.planetIdsAttacked = df.getAllVoyages().filter( + e => e.player !== df.account && + df.getPlanetWithId(e.toPlanet).owner === df.account + ).map(e => e.toPlanet); + } + + attack(planetOccupied) { + if (getPlanetUnconfirmedMoves(planetOccupied.locationId).length > 0) { + + return; + } + + if (getPlanetArrivals(planetOccupied.locationId).length > 0) { + + return; + } + + // 1. find the best planet to attack + let candidates = df.getMyPlanets().filter( + p => p.planetLevel > planetOccupied.planetLevel + && p.energy * usedEnergyPercentForAttacking / 100 > planetOccupied.energyCap + && p.planetType !== PlanetType.SILVER_BANK + && p.unconfirmedDepartures.length === 0 + ).map( + from => [from, distance(from, planetOccupied)] + ).sort( + (a, b) => a[1] - b[1] + ); + + for (const candidate of candidates) { + let planet = candidate[0]; + let energyNeeded = Math.ceil( + df.getEnergyNeededForMove(planet.locationId, planetOccupied.locationId, planetOccupied.energyCap + 100)); + if (planet.energy * 7 / 10 >= energyNeeded) { + df.move(planet.locationId, planetOccupied.locationId, energyNeeded, 0); + + return; + } + } + + // 2. no best planet, find the nearest planet to attack occupied planet, up to maxAttackingPlanetNumber + candidates = df.getMyPlanets().filter( + p => p.planetLevel >= minAttackPlanetLevel + && p.energy >= p.energyCap * minEnergyPercentForAttacking / 100 + && p.planetType !== PlanetType.SILVER_BANK + && p.unconfirmedDepartures.length === 0 + ).map( + from => [from, distance(from, planetOccupied)] + ).sort( + (a, b) => a[1] - b[1] + ); + + let sumEnergy = 0; + let planetsWillAttack = []; + for (const candidate of candidates) { + let planet = candidate[0]; + let energyArrived = df.getEnergyArrivingForMove( + planet.locationId, planetOccupied.locationId, undefined, + Math.floor(planet.energy * usedEnergyPercentForAttacking / 100)); + if (energyArrived >= minArrivedEnergy) { + planetsWillAttack.push(planet); + sumEnergy += energyArrived; + if (sumEnergy >= planetOccupied.energyCap + 100 || planetsWillAttack.length >= maxAttackingPlanetNumber) break; + } + } + + for (const planet of planetsWillAttack) { + df.move(planet.locationId, planetOccupied.locationId, + Math.floor(planet.energy * usedEnergyPercentForAttacking / 100), 0); + } + } + + protect() { + df.terminal.current.println("[Protect Planets] check attacking", 2); + + let planetIdsUnderAttack = df.getAllVoyages().filter( + e => e.arrivalTime > Date.now() / 1000 && + e.player !== df.account && + df.getPlanetWithId(e.toPlanet).owner === df.account && + df.getPlanetWithId(e.toPlanet).planetLevel >= this.minPlanetLevel + ).map(e => e.toPlanet); + + this.planetIdsUnderAttack = planetIdsUnderAttack; + + for (const id of planetIdsUnderAttack) { + if (!this.planetIdsAttacked.includes(id)) { + this.planetIdsAttacked.push(id); + } + } + + for (const id of this.planetIdsAttacked) { + if (df.getPlanetWithId(id).owner !== df.account && !this.planetIdsOccupied.includes(id)) { + this.planetIdsOccupied.push(id) + } + } + + this.planetIdsOccupied = this.planetIdsOccupied + .sort((a, b) => df.getPlanetWithId(b).planetLevel - df.getPlanetWithId(a).planetLevel); + + // remove occupied planets + this.planetIdsAttacked = this.planetIdsAttacked.filter( + id => df.getPlanetWithId(id).owner === df.account + ); + + if (!this.attacking) { + return; + } + + for (const id of this.planetIdsOccupied) { + let planet = df.getPlanetWithId(id); + if (planet.owner !== df.account) { + this.attack(planet); + } + } + } + + stop() { + this.planetIdsUnderAttack = []; + if (this.intervalId != '') { + clearInterval(this.intervalId); + this.intervalId = ''; + } + this.running = false; + } + + async render(container) { + container.style.width = '540px'; + + let levelLabel = document.createElement('label'); + levelLabel.innerText = 'Min. level planets to protect'; + levelLabel.style.display = 'block'; + + let level = document.createElement('select'); + level.style.background = 'rgb(8,8,8)'; + level.style.width = '100%'; + level.style.marginTop = '10px'; + level.style.marginBottom = '10px'; + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].forEach(lvl => { + let opt = document.createElement('option'); + opt.value = `${lvl}`; + opt.innerText = `Level ${lvl}`; + level.appendChild(opt); + }); + + level.value = `${this.minPlanetLevel}`; + level.onchange = (evt) => { + try { + this.minPlanetLevel = parseInt(evt.target.value); + } catch (e) { + console.error('could not parse planet level', e); + } + } + + let warningLabel = document.createElement('label'); + warningLabel.innerText = 'Display warning when my planet was under attack'; + warningLabel.style.paddingRight = "10px"; + warningLabel.style.marginLeft = '10px'; + warningLabel.style.marginTop = '20px'; + + let warningCheckbox = document.createElement('input'); + warningCheckbox.type = "checkbox"; + warningCheckbox.style.marginRight = "10px"; + warningCheckbox.checked = true; + warningCheckbox.onchange = (evt) => { + if (evt.target.checked) { + this.displayWarning = true; + } else { + this.displayWarning = false; + } + }; + + let attackingLabel = document.createElement('label'); + attackingLabel.innerText = 'Get it back when my planet was occupied'; + attackingLabel.style.paddingRight = "10px"; + attackingLabel.style.marginLeft = '10px'; + + let attackingCheckbox = document.createElement('input'); + attackingCheckbox.type = "checkbox"; + attackingCheckbox.style.marginRight = "10px"; + attackingCheckbox.checked = true; + attackingCheckbox.onchange = (evt) => { + if (evt.target.checked) { + this.attacking = true; + } else { + this.attacking = false; + } + }; + + let startBtn = document.createElement('button'); + startBtn.style.width = '100%'; + startBtn.style.marginBottom = '10px'; + startBtn.style.marginTop = '30px'; + startBtn.innerHTML = 'start' + startBtn.onclick = () => { + if (this.running) { + this.stop(); + startBtn.innerHTML = 'start'; + } else { + this.running = true; + this.protect(); + this.intervalId = setInterval(this.protect.bind(this), 30 * 1000); + startBtn.innerHTML = 'stop'; + } + }; + + container.appendChild(levelLabel); + container.appendChild(level); + container.appendChild(warningLabel); + container.appendChild(warningCheckbox); + container.appendChild(attackingLabel); + container.appendChild(attackingCheckbox); + container.appendChild(startBtn); + } + + draw(ctx) { + let count = this.planetIdsUnderAttack.length; + if (this.displayWarning && count > 0) { + const viewport = ui.getViewport(); + ctx.save(); + ctx.fillStyle = "red"; + ctx.font = "24px Sans-serif"; + let msg = ""; + if (count === 1) msg = "You have 1 planet under attack"; + else msg = "You have " + count + " planets under attack"; + ctx.fillText(msg, viewport.viewportWidth/2 - 200, viewport.viewportHeight - 30); + ctx.restore(); + } + } + + destroy() { + this.stop(); + } +} +export default Plugin; + + +function getPlanetArrivals(planetId) { + return df.getAllVoyages() + .filter(arrival => arrival.toPlanet === planetId && + arrival.player === df.account && + arrival.arrivalTime > Date.now() / 1000 + ); +} + +function getPlanetUnconfirmedMoves(planetId) { + return df.getUnconfirmedMoves() + .filter(move => move.to === planetId && move.owner === df.account); +} + +function distance(from, to) { + let fromloc = from.location; + let toloc = to.location; + return Math.sqrt((fromloc.coords.x - toloc.coords.x) ** 2 + (fromloc.coords.y - toloc.coords.y) ** 2); +} \ No newline at end of file diff --git a/content/productivity/protect_planets/screenshot.png b/content/productivity/protect_planets/screenshot.png new file mode 100644 index 00000000..ed424066 Binary files /dev/null and b/content/productivity/protect_planets/screenshot.png differ diff --git a/content/productivity/upgrade-planet/index.md b/content/productivity/upgrade-planet/index.md new file mode 100644 index 00000000..250caaa0 --- /dev/null +++ b/content/productivity/upgrade-planet/index.md @@ -0,0 +1,6 @@ +--- +title: Upgrade Planet +date: +subtitle: Move silver to selected planet then upgrade it automatically +version: 0.6.4 +--- diff --git a/content/productivity/upgrade-planet/screenshot.png b/content/productivity/upgrade-planet/screenshot.png new file mode 100644 index 00000000..e474ae9f Binary files /dev/null and b/content/productivity/upgrade-planet/screenshot.png differ diff --git a/content/productivity/upgrade-planet/upgrade_planet.js b/content/productivity/upgrade-planet/upgrade_planet.js new file mode 100644 index 00000000..dc2af051 --- /dev/null +++ b/content/productivity/upgrade-planet/upgrade_planet.js @@ -0,0 +1,307 @@ +// +// Auto Upgrade Planet +// +// Move silver to selected planet then upgrade it automatically +// +// Written by goldenfiredo, @goldenfiredo + +import { + html, + render, + useState, + useLayoutEffect, +} from 'https://unpkg.com/htm/preact/standalone.module.js'; + +import { + canPlanetUpgrade, + getPlanetRank, +} from 'https://plugins.zkga.me/utils/utils.js'; + +import { + PlanetType, + SpaceType +} from "https://cdn.skypack.dev/@darkforest_eth/types"; + +const { getPlanetName } = df.getProcgenUtils(); + +const minSilverPercent = 95; +const minEnergyPercent = 95; +const minPlanetLevel = 3; + +function planetShort(locationId) { + return locationId.substring(0, 32) + '...'; +} + +function allowUpgrade(planet) { + if (!planet) return false; + if (planet.planetType !== PlanetType.PLANET) return false; + if (planet.owner !== df.account) return false; + return true; +} + +function upgraded(planet) { + return getPlanetRank(planet) >= getPlanetMaxRank(planet) +} + +function distance(from, to) { + let fromloc = from.location; + let toloc = to.location; + return Math.sqrt((fromloc.coords.x - toloc.coords.x) ** 2 + (fromloc.coords.y - toloc.coords.y) ** 2); +} + +function getPlanetMaxRank(planet) { + if (!planet) return 0; + if (planet.spaceType === SpaceType.NEBULA) return 3; + if (planet.spaceType === SpaceType.SPACE) return 4; + return 5; +} + +function upgradeRequiredSilver(planet) { + const maxRank = getPlanetMaxRank(planet); + const silverPerRank = []; + + for (let i = 0; i < maxRank; i++) { + silverPerRank[i] = Math.floor((i + 1) * 0.2 * planet.silverCap); + } + + return silverPerRank[getPlanetRank(planet)]; +} + +function getPlanetArrivals(planetId) { + return df.getAllVoyages() + .filter(arrival => arrival.toPlanet === planetId) + .filter(p => p.arrivalTime > Date.now() / 1000); +} + +function isUpgrading(planet) { + return df.getUnconfirmedUpgrades().filter(p => p.locationId === planet.locationId).length > 0; +} + +function sendSilver(planet, rankSpan) { + df.terminal.current.println("[Upgrade Planet] send silver", 2); + + let target = df.getPlanetWithId(planet.locationId); + let rank = getPlanetRank(target); + let maxRank = getPlanetMaxRank(target); + if (rank < maxRank) + rankSpan.innerHTML = 'rank: ' + getPlanetRank(target); + else + rankSpan.innerHTML = 'rank: ' + getPlanetRank(target) + ', upgrade end'; + + let requiredSilver = rank + 1 == maxRank ? upgradeRequiredSilver(target) : target.silverCap; + let totalAmount = Math.min(target.silverCap - target.silver, requiredSilver - target.silver); + if (totalAmount <= 0) { + return; + } + + if (upgraded(target)) { + return; + } + + if (isUpgrading(target)) { + return; + } + + const allArrivals = getPlanetArrivals(target.locationId); + if (allArrivals.length + df.getUnconfirmedMoves().filter(move => move.to === target.locationId).length > 0) { + return; + } + + const candidates = df.getMyPlanets().filter( + p => p.planetType === PlanetType.SILVER_MINE + && p.planetLevel >= minPlanetLevel + && (p.silver >= Math.floor(p.silverCap * minSilverPercent / 100) || p.silver >= totalAmount) + ); + + let sources = [] + for (let i = 0; i < candidates.length; i++) { + let from = candidates[i]; + const t = df.getPlanetsInRange(from.locationId, minEnergyPercent).filter( + p => p.locationId === target.locationId + ); + + if (t.length == 0) continue; + + sources.push(from); + } + + const sorted = sources.map( + source => [source, distance(source, target)] + ).sort( + (a, b) => a[1] - b[1] + ); + + let sentAmount = 0; + + for (let i = 0; i < sorted.length; ++i) { + const source = sorted[i][0]; + const sourceId = source.locationId; + + const energyNeeded = Math.ceil(df.getEnergyNeededForMove(sourceId, target.locationId, 50)); + if (source.energy - energyNeeded < 0) { + continue; + } + + let silverAmount = Math.min(Math.floor(source.silver), Math.floor(totalAmount - sentAmount)); + + df.terminal.current.println("[Upgrade Planet] move: " + sourceId + " -> " + target.locationId + " silver amount: " + silverAmount, 2); + + df.move(sourceId, target.locationId, energyNeeded, silverAmount); + + sentAmount += silverAmount; + if (sentAmount >= totalAmount) break; + } + +} + +function upgradePlanetPattern(planet, pattern) { + const rank = planet.upgradeState.reduce((a, b) => a + b, 0); + if (pattern.length <= rank) return; + const upgradeBranch = ["d", "r", "s"].indexOf(pattern[rank]); + df.upgrade(planet.locationId, upgradeBranch); +} + +function upgradePlanet(target, patternInput) { + let planet = df.getPlanetWithId(target.locationId); + if (isUpgrading(planet)) { + return; + } + + if (planet && canPlanetUpgrade(planet)) { + upgradePlanetPattern(planet, ([...patternInput.value])); + } +} + +function App() { + let wrapper = { + display: 'flex', + justifyContent: 'space-between', + }; + + let [target, setTarget] = useState(false); + let [selectedPlanet, setSelectedPlanet] = useState(ui.getSelectedPlanet()); + useLayoutEffect(() => { + const sub = ui.selectedPlanetId$.subscribe(() => { + setSelectedPlanet(ui.getSelectedPlanet()); + }); + + return sub.unsubscribe; + }, []); + + function select() { + setTarget(selectedPlanet); + } + + function stop() { + if (!running) return; + + if (sendIntervalId != '') { + clearInterval(sendIntervalId); + sendIntervalId = ''; + } + + if (upgradeIntervalId != '') { + clearInterval(upgradeIntervalId); + upgradeIntervalId = ''; + } + + running = false; + selectBtn.disabled = false; + selectBtn.style = 'opacity: 1'; + startBtn.disabled = false; + startBtn.style = 'opacity: 1'; + patternInput.disabled = false; + } + + function start() { + if (running) return; + + running = true; + selectBtn.disabled = true; + selectBtn.style = 'opacity: 0.5'; + startBtn.disabled = true; + startBtn.style = 'opacity: 0.5'; + patternInput.disabled = true; + + if (sendIntervalId != '') { + clearInterval(sendIntervalId); + } + sendIntervalId = setInterval(sendSilver, 2 * 60 * 1000, target, rankSpan); + sendSilver(target, rankSpan); + + if (upgradeIntervalId != '') { + clearInterval(upgradeIntervalId); + } + upgradeIntervalId = setInterval(upgradePlanet, 0.5 * 60 * 1000, target, patternInput); + upgradePlanet(target, patternInput); + } + + function locate() { + ui.centerLocationId(target.locationId); + } + + return html` +
+
+ +
+ ${allowUpgrade(target) ? html` +
${getPlanetName(target)} +
+
upgrade pattern: + + +
` + : html`
No planet selected or selected planet cannot be upgraded` + } +
+
+
+
+
+ ${allowUpgrade(target) && !upgraded(target) ? html`` + : html`` + } + +
+
+ `; +} + +let running = false; +let sendIntervalId = ''; +let upgradeIntervalId = ''; + +class Plugin { + constructor() { + this.container = null; + } + + async render(container) { + container.parentElement.style.minHeight = 'unset'; + container.style.width = '300px'; + container.style.minHeight = 'unset'; + + this.container = container; + + render(html`<${App} />`, container); + } + + destroy() { + if (sendIntervalId != '') { + clearInterval(sendIntervalId); + sendIntervalId = ''; + } + + if (upgradeIntervalId != '') { + clearInterval(upgradeIntervalId); + upgradeIntervalId = ''; + } + + running = false; + + render(null, this.container); + } +} + +export default Plugin; \ No newline at end of file