Change game state: pauseGame()}> Pause unpauseGame()}> Unpause
<${Heading} title="Whitelist players" />
setWhitelistAddress((e.target as HTMLInputElement).value)}
placeholder="Address to whitelist"
> addAddressToWhitelist(whitelistAddress)}>
Whitelist Address
<${Heading} title="Give Planets" />
Planet: <${PlanetLink} planetId=${(selectedPlanet as Planet)?.locationId} /> to
<${Select}
style=${{ flex: '1' }}
value=${targetAccount}
onChange=${(e: InputEvent) => setTargetAccount((e.target as HTMLSelectElement).value)}
items=${accountOptions(allPlayers)}
/>
takeOwnership(selectedPlanet, targetAccount)}>
Give Planet
<${Heading} title="Give Spaceships" />
<${Select}
style=${{ flex: '1' }}
value=${selectedShip}
onChange=${(e: InputEvent) => setSelectedShip((e.target as HTMLSelectElement).value)}
items=${shipOptions()}
/>
to
<${Select}
style=${{ flex: '1' }}
value=${targetAccount}
onChange=${(e: InputEvent) => setTargetAccount((e.target as HTMLSelectElement).value)}
items=${accountOptions(allPlayers)}
/>
${'On planet: '}
<${PlanetLink} planetId=${(selectedPlanet as Planet)?.locationId} />
createArtifact(
targetAccount,
selectedArtifact,
selectedPlanet,
artifactRarity,
artifactBiome
)}
>
Give Artifact
<${PlanetCreator} />
`;
}
class Plugin implements DFPlugin {
async render(container: HTMLDivElement) {
container.style.width = '525px';
render(html`<${App} />`, container);
}
}
export default Plugin;
================================================
FILE: embedded_plugins/Getting-Started.ts
================================================
/**
* Hi there!
*
* Looks like you've found the Dark Forest plugins system.
* Read through this script to learn how to write plugins!
*
* Most importantly, you have access these globals:
* 1. df - Just like the df object in your console.
* 2. ui - For interacting with the game's user interface.
*
* Let's log these to the console when you run your plugin!
*/
console.log(df, ui);
/**
* Plugins are just TypeScript (or modern JavaScript, if you prefer), so you can use imports, too!
*/
// @ts-ignore
import confetti from 'https://cdn.skypack.dev/canvas-confetti';
/**
* A plugin is a Class with render and destroy methods.
* Other than that, you are free to do whatever, so be careful!
*/
class Readme implements DFPlugin {
private canvas: HTMLCanvasElement;
/**
* A constructor can be used to keep track of information.
*/
constructor() {
this.canvas = document.createElement('canvas');
this.canvas.width = 400;
this.canvas.height = 150;
}
/**
* A plugin's render function is called once.
* Here, you can insert custom html into a game modal.
* You render any sort of UI that makes sense for the plugin!
*/
async render(div: HTMLDivElement) {
div.style.width = '400px';
const firstTextDiv = document.createElement('div');
firstTextDiv.innerText =
'This is an example plugin. Check out its source by' +
' clicking "edit" button that is to the right of the' +
' README plugin in the Plugin Manager modal! ';
const secondTextDiv = document.createElement('div');
secondTextDiv.innerText = '... Or, click the button below to get a free artifact!';
const myButton = document.createElement('df-button');
myButton.innerText = 'give me an artifact';
myButton.addEventListener('click', async () => {
await confetti.create(this.canvas)({
origin: { x: 0.5, y: 1 },
});
const ctx = this.canvas.getContext('2d');
if (ctx) {
ctx.fillStyle = 'white';
ctx.font = '20px Sans-serif';
ctx.fillText('Gotcha!', 150, 60);
}
});
div.appendChild(firstTextDiv);
div.appendChild(document.createElement('br'));
div.appendChild(secondTextDiv);
div.appendChild(document.createElement('br'));
div.appendChild(this.canvas);
div.appendChild(myButton);
}
/**
* When this is unloaded, the game calls the destroy method.
* So you can clean up everything nicely!
*/
destroy() {
const ctx = this.canvas.getContext('2d');
if (ctx) {
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
}
}
}
/**
* For the game to know about your plugin, you must export it!
*
* Use `export default` to expose your plugin Class.
*/
export default Readme;
================================================
FILE: embedded_plugins/Locate-Artifacts.ts
================================================
/**
* Remember, you have access these globals:
* 1. df - Just like the df object in your console.
* 2. ui - For interacting with the game's user interface.
*
* Let's log these to the console when you run your plugin!
*/
console.log(df, ui);
class ArtifactsFinder implements DFPlugin {
private planetList: HTMLDivElement;
renderPlanets = () => {
this.planetList.innerHTML = '';
// keep track of how many we have found
let count = 0;
const countText = document.createElement('div');
this.planetList.appendChild(countText);
for (const planet of df.getAllPlanets()) {
// @ts-ignore
if (planet.location) {
if (df.isPlanetMineable(planet)) {
// is there any other filtering you'd want to do?
// sometimes planets have artifacts deposited on them!
// somtimes a planet's artifact has already been mined.
// see if you can modify this plugin to make it do what
// you want!
const planetEntry = document.createElement('div');
this.planetList.appendChild(planetEntry);
// hint: have a hard time finding planets?
// ui.centerCoords might help...
planetEntry.innerText =
'(' +
// @ts-ignore
planet.location.coords.x +
', ' +
// @ts-ignore
planet.location.coords.y +
')';
count++;
}
}
}
if (count === 0) {
countText.innerText = 'you have not found any artifacts yet';
} else {
countText.innerText = 'you have found ' + count + ' artifacts';
}
};
async render(container: HTMLDivElement) {
console.log('rendered 1 artifacts finder');
const findArtifactsButton = document.createElement('df-button');
findArtifactsButton.innerText = 'find me some artifacts!';
container.appendChild(findArtifactsButton);
container.appendChild(document.createElement('br'));
container.appendChild(document.createElement('br'));
findArtifactsButton.addEventListener('click', this.renderPlanets);
this.planetList = document.createElement('div');
container.appendChild(this.planetList);
this.planetList.style.maxHeight = '300px';
this.planetList.style.width = '400px';
this.planetList.style.overflowX = 'hidden';
this.planetList.style.overflowY = 'scroll';
console.log('rendered artifacts finder');
}
}
/**
* And don't forget to export it!
*/
export default ArtifactsFinder;
================================================
FILE: embedded_plugins/Rage-Cage.ts
================================================
class RageCage implements DFPlugin {
private img: HTMLImageElement;
private loaded: boolean;
/**
* As you saw in the README plugin, you can render
* arbitrary HTML UI into a Dark Forest modal.
*/
async render(div: HTMLDivElement) {
// once this plugin is run, let's load a nice image
// from the internet, in order to draw it on top of
// planets
this.img = document.createElement('img');
this.loaded = false;
div.appendChild(this.img);
this.img.addEventListener('load', () => {
// we should only use the image once
// it actually loads.
this.loaded = true;
div.innerText = 'welcome to nicolas cage world';
});
this.img.src =
'https://upload.wikimedia.org/wikipedia/' + 'commons/c/c0/Nicolas_Cage_Deauville_2013.jpg';
// hide the image, it doesn't need to show up
// in the modal, we only need this img element
// to load the image.
this.img.style.display = 'none';
div.style.width = '100px';
div.style.height = '100px';
div.innerText = 'loading, please wait!';
// check out the helpful functions that appear
// in the Viewport class!
console.log(ui.getViewport());
}
/**
* In addition to rendering HTML UI into a div, plugins
* can draw directly onto the game UI. This function is
* optional, but if it exists, it is called in sync with
* the rest of the game, and allows you to draw onto an
* HTML5 canvas that lays on top of the rest of the game.
*
* In the example below, we render an image on top of every
* planet.
*
* ctx is an instance of CanvasRenderingContext2D.
*/
draw(ctx: CanvasRenderingContext2D) {
// don't draw anything until nic cage loads
if (!this.loaded) return;
// the viewport class provides helpful functions for
// interacting with the currently-visible area of the
// game
const viewport = ui.getViewport();
const planets = ui.getPlanetsInViewport();
for (const p of planets) {
// use the Viewport class to determine the pixel
// coordinates of the planet on the screen
const pixelCenter = viewport.worldToCanvasCoords(
// @ts-ignore
p.location.coords
);
// how many pixels is the radius of the planet?
const trueRadius = viewport.worldToCanvasDist(ui.getRadiusOfPlanetLevel(p.planetLevel));
// draw nicolas cage on top of the planet
ctx.drawImage(
this.img,
50,
50,
400,
400,
pixelCenter.x - trueRadius,
pixelCenter.y - trueRadius,
trueRadius * 2,
trueRadius * 2
);
}
}
}
export default RageCage;
================================================
FILE: embedded_plugins/Remote-Explorer.ts
================================================
// organize-imports-ignore
import type { Chunk, WorldCoords } from '@darkforest_eth/types';
//@ts-ignore
import { locationIdFromDecStr } from 'https://cdn.skypack.dev/@darkforest_eth/serde';
import {
html,
render,
useEffect,
useState,
//@ts-ignore
} from 'https://unpkg.com/htm/preact/standalone.module.js';
import type MinerManager from '../src/Backend/Miner/MinerManager';
import type { MinerWorkerMessage } from '../src/_types/global/GlobalTypes';
type ExtendedMinerManager = MinerManager & {
url: string;
id: number;
chunkSize: number;
patternType: string;
};
const {
MinerManager: Miner,
SwissCheesePattern,
SpiralPattern,
TowardsCenterPattern,
TowardsCenterPatternV2,
} = df.getConstructors();
const NEW_CHUNK = 'DiscoveredNewChunk';
function getPattern(coords: WorldCoords, patternType: string, chunkSize: number) {
if (patternType === 'swiss') {
return new SwissCheesePattern(coords, chunkSize);
} else if (patternType === 'spiral') {
return new SpiralPattern(coords, chunkSize);
} else if (patternType === 'towardsCenter') {
return new TowardsCenterPattern(coords, chunkSize);
} else {
return new TowardsCenterPatternV2(coords, chunkSize);
}
}
class RemoteWorker implements Worker {
private url: string;
constructor(url: string) {
this.url = url;
}
async postMessage(msg: string) {
const msgJson: MinerWorkerMessage = JSON.parse(msg);
const resp = await fetch(this.url, {
method: 'POST',
body: JSON.stringify({
chunkFootprint: msgJson.chunkFootprint,
planetRarity: msgJson.planetRarity,
planetHashKey: msgJson.planetHashKey,
}),
headers: {
'Content-Type': 'application/json',
},
});
const exploredChunk = await resp.json();
const chunkCenter = {
x: exploredChunk.chunkFootprint.bottomLeft.x + exploredChunk.chunkFootprint.sideLength / 2,
y: exploredChunk.chunkFootprint.bottomLeft.y + exploredChunk.chunkFootprint.sideLength / 2,
};
exploredChunk.perlin = df.spaceTypePerlin(chunkCenter, false);
for (const planetLoc of exploredChunk.planetLocations) {
planetLoc.hash = locationIdFromDecStr(planetLoc.hash);
planetLoc.perlin = df.spaceTypePerlin({ x: planetLoc.coords.x, y: planetLoc.coords.y }, true);
planetLoc.biomebase = df.biomebasePerlin(
{ x: planetLoc.coords.x, y: planetLoc.coords.y },
true
);
}
this.onmessage({ data: JSON.stringify([exploredChunk, msgJson.jobId]) });
}
onmessage(_a: { data: string }) {
console.warn('Unimplemented: onmessage');
}
terminate() {
console.warn('Unimplemented: terminate');
}
onmessageerror() {
console.warn('Unimplemented: onmessageerror');
}
addEventListener() {
console.warn('Unimplemented: addEventListener');
}
removeEventListener() {
console.warn('Unimplemented: removeEventListener');
}
dispatchEvent(_event: Event): boolean {
return false;
}
onerror() {
console.warn('Unimplemented: onerror');
}
}
function Target() {
const wrapper = {
width: '1em',
height: '1em',
display: 'inline-block',
position: 'relative',
verticalAlign: 'text-bottom',
};
const svg = {
width: '100%',
height: '100%',
};
const path = {
fill: 'white',
};
return html`
`;
}
function MinerUI({
miner,
onRemove,
}: {
miner: ExtendedMinerManager;
onRemove: (miner: ExtendedMinerManager) => void;
}) {
const [hashRate, setHashRate] = useState(0);
useEffect(() => {
const calcHash = (chunk: Chunk, miningTimeMillis: number) => {
df.addNewChunk(chunk);
const hashRate = chunk.chunkFootprint.sideLength ** 2 / (miningTimeMillis / 1000);
setHashRate(Math.floor(hashRate));
const res = miner.getCurrentlyExploringChunk();
if (res) {
const { bottomLeft, sideLength } = res;
ui?.setExtraMinerLocation?.(miner.id, {
x: bottomLeft.x + sideLength / 2,
y: bottomLeft.y + sideLength / 2,
});
} else {
ui?.removeExtraMinerLocation?.(miner.id);
}
};
miner.on(NEW_CHUNK, calcHash);
return () => {
miner.off(NEW_CHUNK, calcHash);
};
}, [miner]);
const wrapper = {
paddingBottom: '10px',
display: 'flex',
justifyContent: 'space-between',
whiteSpace: 'nowrap',
};
const buttonWrapper = {
width: '50px',
display: 'flex',
justifyContent: 'space-between',
};
const remove = () => {
onRemove(miner);
};
const [targeting, setTargeting] = useState(false);
const target = () => setTargeting(true);
useEffect(() => {
const hover = () => {
const coords = ui.getHoveringOverCoords();
if (coords) {
ui?.setExtraMinerLocation?.(miner.id, coords);
}
};
const click = () => {
window.removeEventListener('mousemove', hover);
window.removeEventListener('click', click);
const coords = ui.getHoveringOverCoords();
if (coords) {
const pattern = getPattern(coords, miner.patternType, miner.chunkSize);
miner.setMiningPattern(pattern);
}
miner.startExplore();
setTargeting(false);
};
if (targeting) {
miner.stopExplore();
window.addEventListener('mousemove', hover);
window.addEventListener('click', click);
}
return () => {
window.removeEventListener('mousemove', hover);
window.removeEventListener('click', click);
};
}, [targeting, miner]);
return html`
`;
}
class RemoteExplorerPlugin implements DFPlugin {
private miners: ExtendedMinerManager[];
private id: number;
constructor() {
this.miners = [];
this.id = 0;
this.addMiner('http://0.0.0.0:8000/mine', 'spiral', 256);
}
addMiner = (url: string, patternType = 'spiral', chunkSize = 256) => {
// TODO: Somehow set a default coords
const pattern = getPattern({ x: 0, y: 0 }, patternType, chunkSize);
const miner = Miner.create(
df.getChunkStore(),
pattern,
df.getWorldRadius(),
df.planetRarity,
df.getHashConfig(),
false,
() => new RemoteWorker(url)
) as ExtendedMinerManager;
miner.url = url;
miner.id = this.id++;
miner.chunkSize = chunkSize;
miner.patternType = patternType;
miner.startExplore();
this.miners.push(miner);
return this.miners;
};
removeMiner = (miner: ExtendedMinerManager) => {
this.miners = this.miners.filter((m) => {
if (m === miner) {
ui?.removeExtraMinerLocation?.(m.id);
m.stopExplore();
m.destroy();
return false;
} else {
return true;
}
});
return this.miners;
};
async render(container: HTMLDivElement) {
container.style.minWidth = '450px';
container.style.width = 'auto';
render(
html`
<${App}
initialMiners=${this.miners}
addMiner=${this.addMiner}
removeMiner=${this.removeMiner}
/>
`,
container
);
}
destroy() {
for (const miner of this.miners) {
ui?.removeExtraMinerLocation?.(miner.id);
miner.stopExplore();
miner.destroy();
}
}
}
export default RemoteExplorerPlugin;
================================================
FILE: embedded_plugins/Renderer-Showcase.ts
================================================
/* eslint-disable */
/**
* Below is a list of class definitions for renderers.
* These are blank renderers as they have no functionality.
* The result of using these renderers is the same as disabling the renderer.
*/
import {
engineConsts,
EngineUtils,
GameGLManager,
GenericRenderer,
glsl,
//@ts-ignore
} from 'https://cdn.skypack.dev/@darkforest_eth/renderer';
import {
AsteroidRendererType,
AttribType,
BackgroundRendererType,
BeltRendererType,
BlackDomainRendererType,
CaptureZoneRendererType,
CanvasCoords,
Chunk,
CircleRendererType,
GameViewport,
LineRendererType,
LocatablePlanet,
LocationId,
MineBodyRendererType,
MineRendererType,
PerlinRendererType,
Planet,
PlanetRendererType,
PlanetRenderInfo,
PlanetRenderManagerType,
QuasarBodyRendererType,
QuasarRayRendererType,
QuasarRendererType,
RectRendererType,
RenderedArtifact,
RendererType,
RGBAVec,
RGBVec,
RingRendererType,
RuinsRendererType,
SpaceRendererType,
SpacetimeRipRendererType,
SpriteRendererType,
TextAlign,
TextAnchor,
TextRendererType,
UIRendererType,
UniformType,
UnminedRendererType,
VoyageRendererType,
WorldCoords,
WormholeRendererType,
//@ts-ignore
} from 'https://cdn.skypack.dev/@darkforest_eth/types';
//@ts-ignore
import { html, render } from 'https://unpkg.com/htm/preact/standalone.module.js';
// Line 78 - 350: Blank Renderer
// Line 350 - 651: Circle Renderer
// Line 626 - End: Plugin
// Line 78 - 376
// "Blank" renderer class definitions
// When passing in these renderers into the Dark Forest API, the result would be the same as disabling that type of renderer.
class PlanetRenderer implements PlanetRendererType {
rendererType = RendererType.Planet;
queuePlanetBody(planet: Planet, centerW: WorldCoords, radiusW: number): void {}
flush(): void {}
}
class MineRenderer implements MineRendererType {
rendererType = RendererType.Mine;
queueMine(planet: Planet, centerW: WorldCoords, radiusW: number): void {}
flush(): void {}
}
class SpacetimeRipRenderer implements SpacetimeRipRendererType {
rendererType = RendererType.SpacetimeRip;
queueRip(planet: Planet, centerW: WorldCoords, radiusW: number): void {}
flush(): void {}
}
class QuasarRenderer implements QuasarRendererType {
rendererType = RendererType.Quasar;
queueQuasar(planet: Planet, centerW: WorldCoords, radiusW: number): void {}
flush(): void {}
}
class RuinsRenderer implements RuinsRendererType {
rendererType = RendererType.Ruins;
queueRuins(planet: Planet, centerW: WorldCoords, radiusW: number): void {}
flush(): void {}
}
class AsteroidRenderer implements AsteroidRendererType {
rendererType = RendererType.Asteroid;
queueAsteroid(planet: Planet, centerW: CanvasCoords, radiusW: number, color?: RGBVec): void {}
flush(): void {}
}
class RingRenderer implements RingRendererType {
rendererType = RendererType.Ring;
queueRingAtIdx(
planet: Planet,
centerW: WorldCoords,
radiusW: number,
color?: RGBVec,
beltIdx?: number,
angle?: number
): void {}
flush(): void {}
}
class SpriteRenderer implements SpriteRendererType {
rendererType = RendererType.Sprite;
//drawing artifacts around world
queueArtifactWorld(
artifact: RenderedArtifact,
posW: CanvasCoords,
widthW: number,
alpha?: number,
atFrame?: number | undefined,
color?: RGBVec | undefined,
theta?: number | undefined,
viewport?: GameViewport
): void {}
//drawing artifacts when traveling with voyagers
queueArtifact(
artifact: RenderedArtifact,
pos: CanvasCoords,
width?: number,
alpha?: number,
atFrame?: number | undefined,
color?: RGBVec | undefined,
theta?: number | undefined
): void {}
flush(): void {}
}
class BlackDomainRenderer implements BlackDomainRendererType {
rendererType = RendererType.BlackDomain;
queueBlackDomain(planet: Planet, centerW: WorldCoords, radiusW: number): void {}
flush(): void {}
}
class TextRenderer implements TextRendererType {
rendererType = RendererType.Text;
queueTextWorld(
text: string,
coords: WorldCoords,
color?: RGBAVec,
offY?: number, // measured in text units - constant screen-coord offset that it useful for drawing nice things
align?: TextAlign,
anchor?: TextAnchor,
zIdx?: number
): void {}
flush(): void {}
}
class VoyageRenderer implements VoyageRendererType {
rendererType = RendererType.Voyager;
queueVoyages(): void {}
flush(): void {}
}
class WormholeRenderer implements WormholeRendererType {
rendererType = RendererType.Wormhole;
queueWormholes(): void {}
flush(): void {}
}
class MineBodyRenderer implements MineBodyRendererType {
rendererType = RendererType.MineBody;
queueMineScreen(planet: Planet, center: WorldCoords, radius: number, z: number): void {}
flush(): void {}
setUniforms(): void {}
}
class BeltRenderer implements BeltRendererType {
rendererType = RendererType.Belt;
queueBeltAtIdx(
planet: Planet,
center: WorldCoords | CanvasCoords,
radius?: number,
color?: RGBVec,
beltIdx?: number,
angle?: number,
screen?: boolean
): void {}
flush(): void {}
setUniforms(): void {}
}
class BackgroundRenderer implements BackgroundRendererType {
rendererType = RendererType.Background;
queueChunks(
exploredChunks: Iterable,
highPerfMode: boolean,
drawChunkBorders: boolean,
disableFancySpaceEffect: boolean,
innerNebulaColor?: string,
nebulaColor?: string,
spaceColor?: string,
deepSpaceColor?: string,
deadSpaceColor?: string
): void {}
flush(): void {}
}
class SpaceRenderer implements SpaceRendererType {
rendererType = RendererType.Space;
queueChunk(chunk: Chunk): void {}
setColorConfiguration(
innerNebulaColor: string,
nebulaColor: string,
spaceColor: string,
deepSpaceColor: string,
deadSpaceColor: string
): void {}
flush(): void {}
}
class UnminedRenderer implements UnminedRendererType {
rendererType = RendererType.Unmined;
queueRect(
{ x, y }: CanvasCoords,
width: number,
height: number,
color: RGBVec,
zIdx: number
): void {}
flush(): void {}
}
class PerlinRenderer implements PerlinRendererType {
rendererType = RendererType.Perlin;
queueChunk(chunk: Chunk): void {}
flush(): void {}
}
class LineRenderer implements LineRendererType {
rendererType = RendererType.Line;
queueLineWorld(
start: WorldCoords,
end: WorldCoords,
color?: RGBAVec,
width?: number,
zIdx?: number,
dashed?: boolean
): void {}
flush(): void {}
}
class RectRenderer implements RectRendererType {
rendererType = RendererType.Rect;
queueRectCenterWorld(
center: WorldCoords,
width: number,
height: number,
color?: RGBVec,
stroke?: number,
zIdx?: number
): void {}
flush(): void {}
}
class CircleRenderer implements CircleRendererType {
rendererType = RendererType.Circle;
queueCircleWorld(
center: CanvasCoords,
radius: number,
color?: RGBAVec,
stroke?: number,
angle?: number, // percent of arc to render
dashed?: boolean
): void {}
queueCircleWorldCenterOnly(
center: WorldCoords,
radius: number, // canvas coords
color?: RGBAVec
): void {}
flush(): void {}
}
class UIRenderer implements UIRendererType {
rendererType = RendererType.UI;
queueBorders(): void {}
queueSelectedRangeRing(): void {}
queueSelectedRect(): void {}
queueHoveringRect(): void {}
queueMousePath(): void {}
drawMiner(): void {}
flush(): void {}
}
class PlanetRenderManager implements PlanetRenderManagerType {
rendererType = RendererType.PlanetManager;
queueRangeRings(planet: LocatablePlanet): void {}
queuePlanets(
cachedPlanets: Map,
now: number,
highPerfMode: boolean,
disableEmojis: boolean,
disableHats: boolean
): void {}
flush(): void {}
}
class QuasarBodyRenderer implements QuasarBodyRendererType {
rendererType = RendererType.QuasarBody;
queueQuasarBody(
planet: Planet,
centerW: WorldCoords,
radiusW: number,
z?: number,
angle?: number
): void {}
flush(): void {}
}
class QuasarRayRenderer implements QuasarRayRendererType {
rendererType = RendererType.QuasarRay;
queueQuasarRay(
planet: Planet,
centerW: WorldCoords,
radiusW: number,
z?: number,
top?: boolean,
angle?: number
): void {}
flush(): void {}
}
class CaptureZoneRenderer implements CaptureZoneRendererType{
rendererType = RendererType.CaptureZone;
queueCaptureZones(): void {}
flush(): void {}
}
// line 350 - 351
// Circle Renderer Definitions using WebGl
// Program Definition
// A program is what we use to organizie the attributes and shaders of WebGl Programs
const u = {
matrix: 'u_matrix', // matrix to convert from world coords to clipspace
};
const a = {
position: 'a_position', // as [posx, posy, rectposx, rectposy]
color: 'a_color',
props: 'a_props', // as [stroke, angle, dash]
eps: 'a_eps',
planetInfo: 'a_planetInfo', //as [planetlevel, radius]
PlanetUpgrades: 'a_planetUpgrades', //as [defense:number , range: number, speed: number]
PlanetResources: 'a_planetResources', //as [energy:number , energy cap: number, silver: number, silver cap: number]
};
const GENERIC_PLANET_PROGRAM_DEFINITION = {
uniforms: {
matrix: { name: u.matrix, type: UniformType.Mat4 },
},
attribs: {
position: {
dim: 4,
type: AttribType.Float,
normalize: false,
name: a.position,
},
eps: {
dim: 1,
type: AttribType.Float,
normalize: false,
name: a.eps,
},
color: {
dim: 4,
type: AttribType.UByte,
normalize: true,
name: a.color,
},
props: {
dim: 2,
type: AttribType.Float,
normalize: false,
name: a.props,
},
planetInfo: {
dim: 2,
type: AttribType.UByte,
normalize: false,
name: a.planetInfo,
},
planetUpgrades: {
dim: 3,
type: AttribType.UByte,
normalize: false,
name: a.PlanetUpgrades,
},
planetResources: {
dim: 4,
type: AttribType.UByte,
normalize: false,
name: a.PlanetResources,
},
},
vertexShader: glsl`
in vec4 a_position;
in vec4 a_color;
in vec2 a_props;
in float a_eps;
in vec2 a_planetInfo;
in vec4 a_planetResources;
uniform mat4 u_matrix;
out float v_planetLevel;
out vec4 v_color;
out vec2 v_rectPos;
out float v_angle;
out float v_dash;
out float v_eps;
out float energy;
out float energy_cap;
void main() {
gl_Position = u_matrix * vec4(a_position.xy, 0.0, 1.0);
v_rectPos = a_position.zw;
v_color = a_color;
v_angle = a_props.x;
v_dash = a_props.y;
v_eps = a_eps;
v_planetLevel = a_planetInfo[0];
energy = a_planetResources[0];
energy_cap = a_planetResources[1];
}
`,
fragmentShader: glsl`
#define PI 3.1415926535
precision highp float;
out vec4 outColor;
in vec4 v_color;
in vec2 v_rectPos;
in float v_angle;
in float v_dash;
in float v_eps;
in float v_planetLevel;
in float energy;
in float energy_cap;
void main() {
vec4 color = v_color;
float dist = length(v_rectPos);
if (dist > 1.0) discard; // if it's outside the circle
// anti-aliasing if barely in the circle
float ratio = (1.0 - dist) / v_eps;
if (ratio < 1.) {
color.a *= ratio;
}
/* get angle for both angle + dash checks */
float angle = atan(v_rectPos.y, v_rectPos.x);
// add 5pi/2 to translate it to [-PI/2, 3PI / 2]
float check = angle + (5.0 * PI / 2.0);
check -= (check > 2.0 * PI ? 2.0 * PI : 0.0);
float pct = check / (2.0 * PI);
/* do angle check */
if (v_angle != 1.0 && pct > v_angle) discard;
/* do dash check */
bool isDash = v_dash > 0.0;
float interval = angle / v_dash;
float modulo = interval - 2.0 * floor(interval / 2.0);
bool isGap = modulo > 1.0;
if (isDash && isGap) discard;
/* now draw it */
outColor = vec4(1,1.0/energy_cap*energy,0,1);
}
`,
};
class CirclePlanetRenderer extends GenericRenderer<
typeof GENERIC_PLANET_PROGRAM_DEFINITION,
GameGLManager
> {
quadBuffer: number[];
viewport: GameViewport;
rendererType: number;
vertexShader: string;
fragmentShader: string;
manager: GameGLManager;
constructor(glManager: GameGLManager, n: number) {
super(glManager, GENERIC_PLANET_PROGRAM_DEFINITION);
//@ts-ignore
this.verts = 0; //found in generic renderer
this.manager = glManager;
const { vertexShader: vert, fragmentShader: frag } = GENERIC_PLANET_PROGRAM_DEFINITION;
this.vertexShader = vert;
this.fragmentShader = frag;
this.rendererType = n;
this.viewport = this.manager.renderer.getViewport();
this.quadBuffer = EngineUtils.makeEmptyDoubleQuad();
}
public queuePlanet(
center: CanvasCoords,
radius: number,
planet: Planet,
angle = 1, // percent of arc to render
dashed = false
): void {
const color = [255, 255, 255, 255] as RGBAVec;
const {
position: posA,
color: colorA,
props: propsA,
eps: epsA,
planetInfo: planetInfoA,
planetUpgrades: planetUpgradesA,
planetResources: planetResourcesA,
//@ts-ignore
} = this.attribManagers;
const { x, y } = center;
// 1 on either side for antialiasing
const r = radius + 1;
const { x1, y1 } = { x1: x - r, y1: y - r };
const { x2, y2 } = { x2: x + r, y2: y + r };
// prettier-ignore
EngineUtils.makeDoubleQuadBuffered(
this.quadBuffer,
x1, y1, x2, y2, -1, -1, 1, 1
);
//@ts-ignore
posA.setVertex(this.quadBuffer, this.verts);
// convert pixels to radians
const interval = engineConsts.dashLength;
const pixPerRad = radius;
const dashRad = interval / pixPerRad;
const dash = dashed ? dashRad : -1;
const eps = 1 / radius;
const resources = [planet.energy, planet.energyCap, planet.silver, planet.silverCap];
for (let i = 0; i < 6; i++) {
//@ts-ignore
colorA.setVertex(color, this.verts + i);
//@ts-ignore
propsA.setVertex([angle, dash], this.verts + i);
//@ts-ignore
planetInfoA.setVertex([planet.planetLevel, radius], this.verts + i);
//@ts-ignore
planetUpgradesA.setVertex(planet.upgradeState, this.verts + i);
//@ts-ignore
planetResourcesA.setVertex(resources, this.verts + i);
//@ts-ignore
epsA.setVertex([eps], this.verts + i);
}
//@ts-ignore
this.verts += 6;
}
public queueGenericPlanet(
planet: Planet,
center: WorldCoords,
radius: number, // world coords
stroke = -1,
angle = 1,
dashed = false
) {
const centerCanvas = this.viewport.worldToCanvasCoords(center);
const rCanvas = this.viewport.worldToCanvasDist(radius);
this.queuePlanet(centerCanvas, rCanvas, planet, angle, dashed);
}
public setUniforms() {
//@ts-ignore
this.uniformSetters.matrix(this.manager.projectionMatrix);
}
public queuePlanetBody(planet: Planet, centerW: WorldCoords, radiusW: number): void {
this.queueGenericPlanet(planet, centerW, radiusW);
}
public queueMine(planet: Planet, centerW: WorldCoords, radiusW: number): void {
this.queueGenericPlanet(planet, centerW, radiusW);
}
public queueRip(planet: Planet, centerW: WorldCoords, radiusW: number): void {
this.queueGenericPlanet(planet, centerW, radiusW);
}
public queueQuasar(planet: Planet, centerW: WorldCoords, radiusW: number): void {
this.queueGenericPlanet(planet, centerW, radiusW);
}
public queueRuins(planet: Planet, centerW: WorldCoords, radiusW: number): void {
this.queueGenericPlanet(planet, centerW, radiusW);
}
public queueAsteroid(planet: Planet, centerW: CanvasCoords, radiusW: number): void {
this.queueGenericPlanet(planet, centerW, radiusW);
}
}
/**
* 626-END: Plugin
*/
export default class EmbeddedRendererShowcase implements DFPlugin {
CirclePlanetLibrary: { [key: string]: any };
circleSelector: string;
circleChecker: { [key: string]: boolean };
constructor() {
let glMan = ui.getGlManager();
this.circleSelector = 'Planet';
if (glMan) {
this.CirclePlanetLibrary = {
Planet: new CirclePlanetRenderer(glMan, RendererType.Planet),
Mine: new CirclePlanetRenderer(glMan, RendererType.Mine),
SpacetimeRip: new CirclePlanetRenderer(glMan, RendererType.SpacetimeRip),
Quasar: new CirclePlanetRenderer(glMan, RendererType.Quasar),
Ruins: new CirclePlanetRenderer(glMan, RendererType.Ruins),
};
}
this.circleChecker = {};
for (let key in this.CirclePlanetLibrary) {
this.circleChecker[key] = false;
}
}
async render(div: HTMLDivElement) {
div.style.width = '500px';
render(
html`
Disabling Renderers
Whats actually happening here is that the current renderer is being replaced with a
'blank' renderer. A blank renderer is a renderer who's flush function has no
functionality. So the behavior is similar to disabling the current renderer.
{
disable();
}}
>
Disable
Circle Planets
This replaces the current planet renderer with a renderer that uses WebGL to create a
circle where the color of the circle changes on the percentage of energy on the planet.
{
const btn = document.getElementById('CirclePlanetBtn');
if (this.circleChecker[this.circleSelector] === false) {
ui.setCustomRenderer(this.CirclePlanetLibrary[this.circleSelector]);
this.circleChecker[this.circleSelector] = true;
btn!.innerText = 'Disable';
} else {
ui.disableCustomRenderer(this.CirclePlanetLibrary[this.circleSelector]);
this.circleChecker[this.circleSelector] = false;
btn!.innerText = 'Enable';
}
}}
>
Enable
Renderer Descriptions
`,
div
);
}
destroy(): void {
currentPlanet = 'Planet';
for (let key in rendererLibrary) {
ui.disableCustomRenderer(rendererLibrary[key]);
}
for (let key in this.CirclePlanetLibrary) {
ui.disableCustomRenderer(this.CirclePlanetLibrary[key]);
}
}
}
let selectStyle = {
outline: 'none',
background: '#151515',
color: '#838383',
borderRadius: '4px',
border: '1px solid #777',
width: '100px',
padding: '2px 6px',
cursor: 'pointer',
margin: '10px',
};
let inputStyle = {
background: 'rgb(8,8,8)',
width: '400px',
padding: '3px 5px',
};
let buttonStyle = { height: '25px', padding: '3px 5px', margin: '10px', 'text-align': 'center' };
let rendererLibrary: { [key: string]: any } = {
Planet: new PlanetRenderer(),
Mine: new MineRenderer(),
SpacetimeRip: new SpacetimeRipRenderer(),
Ruins: new RuinsRenderer(),
Quasar: new QuasarRenderer(),
Asteroid: new AsteroidRenderer(),
MineBody: new MineBodyRenderer(),
MineBelt: new BeltRenderer(),
Background: new BackgroundRenderer(),
UnminedBackground: new UnminedRenderer(),
SpaceBackground: new SpaceRenderer(),
Artifact: new SpriteRenderer(),
AllPlanets: new PlanetRenderManager(),
Text: new TextRenderer(),
Voyager: new VoyageRenderer(),
Perlin: new PerlinRenderer(),
Wormhole: new WormholeRenderer(),
BlackDomain: new BlackDomainRenderer(),
Rectangles: new RectRenderer(),
Line: new LineRenderer(),
Circle: new CircleRenderer(),
Ring: new RingRenderer(),
UI: new UIRenderer(),
QuasarBody: new QuasarBodyRenderer(),
QuasarRay: new QuasarRayRenderer(),
CaptureZone: new CaptureZoneRenderer(),
};
let disabled: { [key: string]: boolean } = {};
for (let key in rendererLibrary) {
disabled[key] = false;
}
let currentPlanet: string = 'Planet';
function disable() {
const btn = document.getElementById('RegPlanetBtn');
if (disabled[currentPlanet] === false) {
ui.setCustomRenderer(rendererLibrary[currentPlanet]);
disabled[currentPlanet] = true;
btn!.innerText = 'Enable';
} else {
ui.disableCustomRenderer(rendererLibrary[currentPlanet]);
disabled[currentPlanet] = false;
btn!.innerText = 'Disable';
}
}
const cellStyle = {
width: '25%',
float: 'left',
};
const discriptionStyle = {
TextAlign: 'justify',
};
// Used for discription of each type of renderer
let rendererDescription: { [key: string]: any } = {
Blank: '',
Planet: 'basic planets',
Mine: 'asteroid fields',
SpacetimeRip: 'Spacetime Rips',
Ruins: 'foundries',
Quasar: 'quasars',
Asteroid: 'asteroid that hover around different planets',
MineBody: 'the body/asteroids of asteroid fields',
MineBelt: 'the belts/rings around asteroid fields',
Background: 'the background of the game',
UnminedBackground: 'unmined space chunks (part of the background)',
SpaceBackground: 'mined space chunks (part of the background)',
Artifact: 'artifacts',
AllPlanets: 'All planet types',
Text: 'text that are displayed on the game canvas',
Voyager: 'Voyages. The transfer of energy between planets.',
Perlin:
'the game background. Perlin is the method in which we generate the location of space biomes.',
Wormhole: 'visual effects of wormholes. Wormholes are generated by a special type of artifact.',
BlackDomain:
'visual effects of black domains. Black Domains are created by a special type of artifact',
Rectangles: 'all rectangles drawn in game: indicators for selection of planet, ',
Line: ' all lines drawn in game: The line that connect planets during voyages and wormholes ',
Circle:
'all circles drawn in the game: The voyager(circle) in voyages, Circles used to indicate the range a planets has, The boarder of the game world. ',
Ring: 'rings that indicate the level of a planet',
UI: 'in game user interface: game borders, range indicators, selection indicators, mouse path, miner',
QuasarBody: 'the body of the Quasar',
QuasarRay: 'the ray of the Quasar',
CaptureZone: 'the capture zones'
};
/* eslint-enable */
================================================
FILE: index.html
================================================
Dark Forest
================================================
FILE: last_updated.txt
================================================
last updated: Mon Apr 11 18:20:58 UTC 2022
================================================
FILE: netlify.toml
================================================
[build]
command = "yarn build"
functions = "functions"
publish = "dist"
[[redirects]]
from = "/*"
to = "/index.html"
status = 200
[[redirects]]
from = "/archive/8d8a7d2a/*"
to = "https://6247dc3ca1de3b6ce0c8e184--df-prod.netlify.app/:splat"
status = 200
## (optional) Settings for Netlify Dev
## https://github.com/netlify/cli/blob/master/docs/netlify-dev.md#project-detection
#[dev]
# command = "yarn start" # Command to start your dev server
# port = 3000 # Port that the dev server will be listening on
# publish = "dist" # Folder with the static content for _redirect file
## more info on configuring this file: https://www.netlify.com/docs/netlify-toml-reference/
================================================
FILE: package.json
================================================
{
"name": "client",
"version": "6.7.29",
"private": true,
"license": "GPL-3.0",
"author": "0xPARC ",
"dependencies": {
"@darkforest_eth/constants": "6.7.29",
"@darkforest_eth/contracts": "6.7.29",
"@darkforest_eth/events": "6.7.29",
"@darkforest_eth/gamelogic": "6.7.29",
"@darkforest_eth/hashing": "6.7.29",
"@darkforest_eth/hexgen": "6.7.29",
"@darkforest_eth/network": "6.7.29",
"@darkforest_eth/procedural": "6.7.29",
"@darkforest_eth/renderer": "6.7.29",
"@darkforest_eth/serde": "6.7.29",
"@darkforest_eth/settings": "6.7.29",
"@darkforest_eth/snarks": "6.7.29",
"@darkforest_eth/types": "6.7.29",
"@darkforest_eth/ui": "6.7.29",
"@darkforest_eth/whitelist": "6.7.29",
"@lit-labs/react": "^1.0.0",
"animejs": "^3.2.1",
"auto-bind": "^4.0.0",
"bad-words": "^3.0.4",
"big-integer": "^1.6.48",
"canvas-confetti": "^1.4.0",
"color": "^3.0.2",
"delay": "^5.0.0",
"email-validator": "^2.0.4",
"emoji-picker-react": "^3.4.8",
"ethers": "^5.5.1",
"events": "^3.0.0",
"fastq": "^1.10.0",
"file-saver": "^2.0.5",
"gl-matrix": "^3.3.0",
"htm": "^3.0.4",
"idb": "^5.0.1",
"js-quadtree": "^3.3.5",
"json-stable-stringify": "^1.0.1",
"jszip": "^3.5.0",
"lodash": "^4.17.15",
"mnemonist": "^0.38.1",
"p-defer": "^3.0.0",
"p-timeout": "^4.0.0",
"preact": "^10.5.13",
"prismjs": "^1.22.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-loader-spinner": "^4.0.0",
"react-router-dom": "^5.3.0",
"react-simple-code-editor": "^0.11.0",
"react-sortablejs": "^6.0.0",
"react-timeago": "^6.2.1",
"sortablejs": "^1.10.2",
"styled-components": "^5.3.3",
"ts-dedent": "^2.0.0",
"uuid": "^8.3.2"
},
"scripts": {
"declarations": "tsc -p tsconfig.decs.json",
"test": "exit 0",
"lint": "eslint .",
"format": "prettier --write .",
"start": "webpack-dev-server --mode development --hot",
"build": "webpack --mode production",
"clean": "del-cli dist node_modules declarations public/contracts tsconfig.ref.tsbuildinfo",
"docs": "typedoc && yarn format",
"deploy": "netlify build && netlify deploy",
"deploy:prod": "netlify build && netlify deploy --prod"
},
"browserslist": {
"production": [
"last 1 chrome version",
"last 1 firefox version"
],
"development": [
"last 1 chrome version",
"last 1 firefox version"
]
},
"engines": {
"node": ">=16"
},
"devDependencies": {
"@babel/core": "^7.17.5",
"@babel/preset-env": "^7.16.11",
"@babel/preset-react": "^7.16.7",
"@babel/preset-typescript": "^7.16.7",
"@types/animejs": "^3.1.3",
"@types/color": "^3.0.2",
"@types/gl-matrix": "^3.2.0",
"@types/json-stable-stringify": "^1.0.32",
"@types/lodash": "^4.14.160",
"@types/prismjs": "^1.16.2",
"@types/react": "^17.0.34",
"@types/react-dom": "^17.0.11",
"@types/react-router-dom": "^5.3.2",
"@types/react-timeago": "^4.1.3",
"@types/sortablejs": "^1.10.6",
"@types/styled-components": "^5.1.15",
"@types/uuid": "^8.3.0",
"@types/webpack-env": "^1.16.3",
"babel-loader": "^8.2.3",
"babel-plugin-styled-components": "^2.0.5",
"copy-webpack-plugin": "^9.0.1",
"css-loader": "^6.5.1",
"del-cli": "^4.0.1",
"dotenv": "^10.0.0",
"eslint": "^7.30.0",
"fork-ts-checker-webpack-plugin": "^7.2.1",
"html-webpack-plugin": "^5.5.0",
"netlify-cli": "^3.8.5",
"prettier": "^2.3.0",
"raw-loader": "^4.0.2",
"resolve-package-path": "^4.0.3",
"source-map-loader": "^3.0.0",
"style-loader": "^3.3.1",
"typedoc": "^0.22.8",
"typedoc-plugin-markdown": "3.11.x",
"typescript": "4.5.x",
"webpack": "^5.62.1",
"webpack-bundle-analyzer": "^4.5.0",
"webpack-cli": "^4.9.1",
"webpack-dev-server": "^4.4.0"
}
}
================================================
FILE: plugins/PluginTemplate.ts
================================================
/**
* Remember, you have access these globals:
* 1. df - Just like the df object in your console.
* 2. ui - For interacting with the game's user interface.
*
* Let's log these to the console when you run your plugin!
*/
console.log(df, ui);
class PluginTemplate implements DFPlugin {
constructor() {}
/**
* Called when plugin is launched with the "run" button.
*/
async render(container: HTMLDivElement) {}
/**
* Called when plugin modal is closed.
*/
destroy() {}
}
/**
* And don't forget to export it!
*/
export default PluginTemplate;
================================================
FILE: plugins/README.md
================================================
# Plugins
TypeScript or JavaScript files in this directory will automatically be bundled and served as plugins when running `df-plugin-dev-server` in the root of the game `client`!
Check out [Plugin Development](../README.md#plugin-development) in the main README for the steps to set it up.
================================================
FILE: plugins/RendererPlugin.md
================================================
# Pluginnable Renderers
This article is about creating custom renderers for Dark Forest through the use of our plugin system.
# Background
Dark Forest uses WebGL to visualize the game. WebGL is a JavaScript API that is used for rendering high-performance 3D graphics.
Only certain browsers support WebGL. Click here to check: https://get.webgl.org/WebGL/.
Click here to Learn more about WebGL: https://WebGLfundamentals.org/webgl/lessons/webgl-getting-WebGL.html
# What is a Renderer
Renderers are classes that are used to draw specific types of entities such as planets or asteroids onto the Dark Forest game canvas. Custom made renderers can be passed into the Dark Forest API to replace the current renderer for that entity type.
Renderers have two main methods:
- Queue (a method that starts with `queue` followed by the name of the object it's rendering): The game calls the queue method for each instance of an object specified by the renderer type and accepts as an input any information relevant to the object. This information is then added to some implementation of a queue within the renderer for later use in the flush method.
- Flush: Flush is called every frame in the game. When called, the entities in the queue should be rendered onto the game canvas and the queue cleared.
For the game to recognize if the class is a renderer, it has to follow one of the renderer interfaces that can be found in `@darkforest_eth/types`
For instance `MineRendererType` is an interface for mine renderers. Mine renderers are used to draw Asteroid Fields which is a type of planet (not to be confused with Asteroids).

The `MineRendererType` has 2 abstract methods:
- `queueMine` is called by the game to queue an Asteroid Field to be drawn. The method has 3 parameters:
- `planet`: A Planet object that contains information about the current Asteroid Field
- `centerW`: a set of x and y coordinates that represent the position of the center of the Asteroid Field relative to the game world.
- `radiusW`: the size of the Asteroid Field relative to the game world.
- `flush` called by the game to draw all queued up Asteroid Field.
Here is Dark Forest's current implementation of its mine renderer: https://github.com/darkforest-eth/packages/blob/master/renderer/src/Entities/MineRenderer.ts
## Draw Order
### The Order in Which Renderer Managers and Renderers Are Flushed
1. Background
- Unmined
- Space
2. Capture Zones
3. Line Renderer
4. Planet Manager
- Planet
- Asteroid
- Mine
- Spacetime Rip
- Ruins
- Ring
- Quasar
- Black Domain
5. UI
6. Voyager
7. Wormhole
8. Circle
9. Rect
10. Text
11. Sprite (Artifacts)
### When Multiple Renderers Implementing the Same Interface Are Passed Into the API:
When you replace a renderer in the game, the renderer is then put on our rendering stack. The rendering stack is used to determine which renderer to use if multiple renderers loaded for the same entity at the same time. The game will use the top most renderer. A renderer at any position in the stack can be popped.
# Example Plugin
We will run through the creation of a renderer that will replace the renderer for Asteroid Fields.
To create a renderer it has to follow one of the renderer interfaces that can be found in `@darkforest_eth/types`. For the Asteroid Fields, we will be using the `MineRendererType`.
```javascript
import { RendererType } from 'https://cdn.skypack.dev/@darkforest_eth/types';
class GenericMineRenderer {
constructor() {
this.rendererType = RendererType.Mine;
}
queueMine(planet, centerW, radiusW) {}
flush() {}
}
```
After implementing the interface the code should look similar to the code above. The code above can be considered a fully functional renderer. The resulting behavior would be as if you were disabling the renderer for the Asteroid Renderer.
Before:

After:

If you noticed in the queue function, the coordinate of the planet and the size of planet is relative to the game world. However when drawn on the canvas, the size and location of planet change based on the position of the players camera. We provide developers a way to easily transform between coordinate systems via the viewport class. You can get the viewport from the global ui class. `ui.getViewport()`. An example of the use of the viewport can be seen in the code of the `queueMine` function below.
```javascript
import { RendererType } from 'https://cdn.skypack.dev/@darkforest_eth/types';
class GenericMineRenderer {
constructor() {
this.viewport = ui.getViewport();
this.rendererType = RendererType.Mine;
}
queueMine(planet, centerW, radiusW) {
const centerCanvas = this.viewport.worldToCanvasCoords(centerW);
const rCanvas = this.viewport.worldToCanvasDist(radiusW);
}
flush() {}
}
```
The `Viewport` class is used to translate between game world and canvas for distances and coordinates: https://github.com/darkforest-eth/packages/blob/master/types/interfaces/GameViewport.md
These are the 2 functions used above.
- `worldToCanvasCoords` will translate from game world coordinates to canvas coordinates. In the code above we are translating the location of the Asteroid Field to its location relative to the canvas.
- `worldToCanvasDist` will translate a distance relative to the game world to the pixel distance on the canvas.
After implementing the above code you should be ready to start implementing code that will draw on the game canvas.
To do this you will need to be able to access the `WebGLRenderingContext`.
## WebGL Code
The next few sections will be about writing the WebGL Code.
We will be creating WebGL code that renders a circle instead of the original asteroid field. The color of the asteroid field while change based on the current energy level of the planet. The RGB value of the color is determined by this equation `red: 255 green: 255/(energy cap) \*(current energy) blue: 0`.
The goal of this section is to give you a basic understanding of how the WebGL code works. If you want to dive deeper into WebGL check out this website:
https://WebGLfundamentals.org/webgl/lessons/webgl-how-it-works.html
## Definitions
Clip space: The 3D coordinate system used by WebGL for its canvas. All coordinates range fro`m -1 to 1.
Vertex shader: The vertex shader is used to determine the location of a vertex on the WebGL clip space. WebGL will draw a shape onto the canvas based on the passed in vertices. For instance, the triangle draw mode will connect every 3 consecutive vertices to create a triangle.
Fragment Shader: Once a shape is created, the fragment shader is used to determine the color of each individual pixel in the shape.
Attribute Variables: The attributes are values that are passed in from outside the shader program to the vertex shader. Each vertex has its on set of attribute variables.
Uniform Variables: The uniforms are effectively global variables you set before the execution of the program. All vertices share the same uniform variables.
Varying Variables: Variables that will be transferred to be used in the fragment shader
The code below contains all of the objects that we will explore to demonstrate WebGL rendering. The two shaders are wrapped into a RendererProgram class. The RendererProgram is how the Dark Forest team structures our code. You do not have to follow this structure to create a working WebGL Program.
## Fragment and Vertex Shaders
```javascript
import { glsl } from 'https://cdn.skypack.dev/@darkforest_eth/renderer';
import {
RendererProgram,
AttribeType,
UniformType,
} from 'https://cdn.skypack.dev/@darkforest_eth/types';
const program = {
uniforms: {
matrix: { name: 'u_matrix', type: UniformType.Mat4 },
},
attribs: {
position: {
dim: 4,
type: AttribType.Float,
normalize: false,
name: 'a_position', //the name of the attribute variable
},
energyInfo: {
dim: 2,
type: AttribType.Float,
normalize: false,
name: 'a_energy',
},
},
vertexShader: glsl`
in vec4 a_position;
in vec2 a_energy;
uniform mat4 u_matrix;
out vec2 v_rectPos;
out float v_energy;
out float v_energy_cap;
void main() {
// converting from canvas coordinates too clip space
gl_Position = u_matrix * vec4(a_position.xy, 0.0, 1.0);
//setting the varrying variables for use in the fragment shader
v_energy = a_energy.x;
v_energy_cap = a_energy.y;
v_rectPos = a_position.zw;
}
`,
fragmentShader: glsl`
#define PI 3.1415926535
precision highp float;
out vec4 outColor;
in vec2 v_rectPos;
in float v_energy;
in float v_energy_cap;
void main() {
float dist = length(v_rectPos);
// if it's outside the circle
if (dist > 1.0) discard;
//determine the color of the pixel using rgb values
//[red,green,blue,opacity] the range of the numbers is from 0 to 1
outColor = vec4(1,1.0/v_energy_cap*v_energy,0,1);
}
`,
};
```
For the both the Vertex and Fragment shader `glsl` is used. `glsl` formats the string to be readable by the shader compiler.
### Vertex Shader
`a_position` and `a_energy` are attributes and `u_matrix` is a uniform for this shader program.
- `a_position` is a vector that contains the location of the vertex being drawn
- `a_energy` is a vector that contains information about the energy of the Asteroid Field.
The `u_matrix` is a projection matrix. It is used to do some fancy math in line 10:
`gl_Position = u_matrix * vec4(a_position.xy, 0.0, 1.0);`
This is transforming coordinates from the canvas into clip space coordinates. If you want to read more about how this math done, checkout this link: https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/WebGL_model_view_projection.
With the combination of some code we will write later, the vertex shader will draw a square at the coordinates we decide.
### Fragment Shader
The fragment shader is called for every pixel in the square. The `outColor` is the color of the pixel. This fragment draw a circle with a clip space radius of 1. The fragment shader checks if the pixel is one unit away from the center of the square by use of the length function and `discard`s the pixel away if it is not.
## Renderer Code
```javascript
import { EngineUtils, GenericRenderer, glsl } from 'https://cdn.skypack.dev/@darkforest_eth/renderer';
import { AttribType, RendererType, UniformType,
} from 'https://cdn.skypack.dev/@darkforest_eth/types';
class GenericMineRenderer extends GenericRenderer {
constructor(gl, vp) {
super(gl, program);
this.rendererType = RendererType.Mine;
this.gl = gl;
this.viewport = vp;
this.quadBuffer = EngineUtils.makeEmptyDoubleQuad();
}
```
The `GameGLManager` is a wrapper class that contains the WebGLRenderingContext: https://github.com/darkforest-eth/packages/blob/master/renderer/classes/GameGLManager.md
`GenericRenderer` is a class that was created by the Dark Forest team for the purposes of organization. `GenericRenderer` sets up a lot of the commonly used WebGL code. You do not have to use this to create functioning WebGL Code. You can look at the `GenericRenderer` Code here: https://github.com/darkforest-eth/packages/blob/master/renderer/src/WebGL/GenericRenderer.ts
The `quadBuffer` is an array that will contain the vertices of the square being drawn.
`EngineUtils` is a tool the Dark Forest team created for commonly used WebGL operations: https://github.com/darkforest-eth/packages/blob/master/renderer/src/EngineUtils.ts
```typescript
public flush(drawMode: DrawMode = DrawMode.Triangles) {
if (this.verts === 0) return;
const { gl } = this.manager;
gl.useProgram(this.program);
this.setUniforms();
for (const attrib in this.attribManagers) {
this.attribManagers[attrib].bufferData(this.verts);
}
// draw
gl.drawArrays(drawMode, 0, this.verts);
this.verts = 0;
}
```
This is the flush function which was inherited by `GenericRenderer`.
The default `DrawMode` is to use triangles. This means whenever 3 vertices are put into the vertex shader it will draw a triangle.
As stated above the goal of the vertex shader was to draw a square. To do that with triangle we need to draw 2 triangles connected by one their sides. The result of this will require us to import 6 vertices.
Square:

```javascript
queueMine(planet: Planet, centerW: WorldCoords, radiusW: number): void {
//converting from game coordinates to canvas coordinates
const centerCanvas = this.viewport.worldToCanvasCoords(centerW);
const rCanvas = this.viewport.worldToCanvasDist(radiusW);
this.queueCircle(planet, centerCanvas, rCanvas);
}
queueCircle(planet: Planet, center: CanvasCoords, radius: number) {
const { position: posA, energyInfo: energyA } = this.attribManagers;
const r = radius + 1;
const { x, y } = center;
//calculating the top left and bottom right of the bounding square.
const { x1, y1 } = { x1: x - r, y1: y - r };
const { x2, y2 } = { x2: x + r, y2: y + r };
//creating an array of all the vertices for the triangle 14
EngineUtils.makeDoubleQuadBuffered(
this.quadBuffer,
x1, y1, x2, y2, -1, -1, 1, 1
);
//passing in the vertices to the vertex shader
posA.setVertex(this.quadBuffer, this.verts);
//passing in the energy information. 6 times because we passed in 6 vertices.
for (let i = 0; i < 6; i++) {
energyA.setVertex([planet.energy, planet.energyCap], this.verts + i);
}
this.verts += 6;
}
setUniforms() {
if (!this.gl) return;
this.uniformSetters.matrix(this.gl.projectionMatrix);
}
```
The `makeDoubleQuadBuffered` code creates an array of size 24. We are then inserting the array to be imported into fragment shader through `a_position` attribute.
Since `a_position` is imported as a `vec4` the `quadbuffer` is split evenly into 6 vectors of size 4, one for each vertex. Each vector is formatted like this:
[canvas x, canvas y, clip space x, clip space y]
In relation to the square, the coordinates of the 6 vertices are the top left, top right, bottom left, bottom right of the square. Where the top right and bottom left repeat twice.
```javascript
for (let i = 0; i < 6; i++) {
energyA.setVertex([planet.energy, planet.energyCap], this.verts + i);
}
this.verts += 6;
```
For each vertex we are importing the information about the planet's energy to the `a_energy` attribute.
## Final Renderer Code
The final code for the renderer looks like this:
```javascript
import {
EngineUtils,
GenericRenderer,
glsl,
} from 'https://cdn.skypack.dev/@darkforest_eth/renderer';
import {
AttribType,
RendererType,
UniformType,
} from 'https://cdn.skypack.dev/@darkforest_eth/types';
class GenericMineRenderer extends GenericRenderer {
constructor(gl, vp) {
super(gl, program);
this.rendererType = RendererType.Mine;
this.gl = gl;
this.viewport = vp;
this.quadBuffer = EngineUtils.makeEmptyDoubleQuad();
}
queueMine(planet, centerW, radiusW) {
//converting from game coordinates to canvas coordinates
const centerCanvas = this.viewport.worldToCanvasCoords(centerW);
const rCanvas = this.viewport.worldToCanvasDist(radiusW);
this.queueCircle(planet, centerCanvas, rCanvas);
}
queueCircle(planet, center, radius) {
const { position: posA, energyInfo: energyA } = this.attribManagers;
const r = radius + 1;
const { x, y } = center;
//calculating the top left and bottom right of the bounding square.
const { x1, y1 } = { x1: x - r, y1: y - r };
const { x2, y2 } = { x2: x + r, y2: y + r };
//creating an array of all the vertices for the triangle 14
EngineUtils.makeDoubleQuadBuffered(this.quadBuffer, x1, y1, x2, y2, -1, -1, 1, 1);
//passing in the vertices to the vertex shader
posA.setVertex(this.quadBuffer, this.verts);
//passing in the energy information. 6 times because we passed in 6 vertices.
for (let i = 0; i < 6; i++) {
energyA.setVertex([planet.energy, planet.energyCap], this.verts + i);
}
this.verts += 6;
}
setUniforms() {
if (!this.gl) return;
this.uniformSetters.matrix(this.gl.projectionMatrix);
}
}
const program = {
uniforms: {
matrix: { name: 'u_matrix', type: UniformType.Mat4 },
},
attribs: {
position: {
dim: 4,
type: AttribType.Float,
normalize: false,
name: 'a_position', //the name of the attribute variable
},
energyInfo: {
dim: 2,
type: AttribType.Float,
normalize: false,
name: 'a_energy',
},
},
vertexShader: glsl`
in vec4 a_position;
in vec2 a_energy;
uniform mat4 u_matrix;
out vec2 v_rectPos;
out float v_energy;
out float v_energy_cap;
void main() {
// converting from canvas coordinates too clip space
gl_Position = u_matrix * vec4(a_position.xy, 0.0, 1.0);
//setting the varrying variables for use in the fragment shader
v_energy = a_energy.x;
v_energy_cap = a_energy.y;
v_rectPos = a_position.zw;
}
`,
fragmentShader: glsl`
#define PI 3.1415926535
precision highp float;
out vec4 outColor;
in vec2 v_rectPos;
in float v_energy;
in float v_energy_cap;
void main() {
float dist = length(v_rectPos);
// if it's outside the circle
if (dist > 1.0) discard;
//determine the color of the pixel using rgb values
//[red,green,blue,opacity] the range of the numbers is from 0 to 1
outColor = vec4(1,1.0/v_energy_cap*v_energy,0,1);
}
`,
};
```
Once we are done implementing the renderer we can call `ui.setCustomRenderer` to start rendering our own Asteroid Field Renderer.
```javascript
export default class ExamplePlugin {
constructor() {
const gl = ui.getGlManager();
if (!gl) return;
this.mineRenderer = new GenericMineRenderer(gl, ui.getViewport());
ui.setCustomRenderer(this.mineRenderer);
}
async render(div) {}
destroy() {
console.log('destroying renderer plugin');
ui.disableCustomRenderer(this.mineRenderer);
}
}
```
The code above is an example of implementing the renderer with plugins. When the plugin is started it creates a new instance of our custom Asteroid Field Renderer and then uses `setCustomRenderer` to activate it. When the plugin is destroyed we disable the renderer by using `disableCustomRenderer`.
The result of running the plugin can be seen below.
Before:

After:

# Documentation Links
## Renderer Types
All renderer types can be found here: https://github.com/darkforest-eth/packages/tree/master/types#type-declaration-27
To find the interface for each renderer go to the link below. The name of the interface is the entity name followed by `RendererType`: https://github.com/darkforest-eth/packages/tree/master/types#interfaces
## Dark Forest Renderers
Below are examples of Dark Forest renderers
Basic Planets
- Renderer Code: https://github.com/darkforest-eth/packages/blob/master/renderer/src/Entities/PlanetRenderer.ts
- WebGL Code: https://github.com/darkforest-eth/packages/blob/master/renderer/src/Programs/PlanetProgram.ts
Background Renderer:
- Background Renderer is a renderer manager that contains multiple renderer used to draw different sections of the background.
- Renderer Code: https://github.com/darkforest-eth/packages/blob/master/renderer/src/Entities/BackgroundRenderer.ts
- Unmined Renderer:
- Renderer Code: https://github.com/darkforest-eth/packages/blob/master/renderer/src/Entities/UnminedRenderer.ts
- WebGL Code: https://github.com/darkforest-eth/packages/blob/master/renderer/src/Programs/UnminedProgram.ts
- Space Renderer:
- Renderer Code: https://github.com/darkforest-eth/packages/blob/master/renderer/src/Entities/SpaceRenderer.ts
- WebGL Code: https://github.com/darkforest-eth/packages/blob/master/renderer/src/Programs/SpaceProgram.ts
If you want to look at more renderers, you can find all the Dark Forest renderers here:
https://github.com/darkforest-eth/packages/tree/master/renderer/src/Entities
The WebGl Code for those renderers can be found here: https://github.com/darkforest-eth/packages/tree/master/renderer/src/Programs
## Rendering Tools
Generic Renderer is base class that Dark Forest team created for all of its renderers: https://github.com/darkforest-eth/packages/blob/master/renderer/src/WebGL/GenericRenderer.ts
Engine Utils contains function for common WebGl Operations : https://github.com/darkforest-eth/packages/blob/master/renderer/classes/EngineUtils.md
https://github.com/darkforest-eth/packages/blob/master/renderer/src/EngineUtils.ts
The viewport class is used to translate between game and canvas coordinate systems: https://github.com/darkforest-eth/packages/blob/master/types/interfaces/GameViewport.md
Our Wrapper class for the WebGL2RenderingContext
GameGlManager: https://github.com/darkforest-eth/packages/blob/master/renderer/classes/GameGLManager.md
If you want to mess with canvas 2D rendering you can access a `CanvasRenderingContext2D` by calling `ui.get2dRenderer()` WARNING: all images drawn with the `CanvasRenderingContext2D` will all be on top of WebGl
================================================
FILE: public/manifest.json
================================================
{
"name": "Dark Forest",
"short_name": "Dark Forest",
"background_color": "#080808",
"theme_color": "#080808",
"icons": [
{
"src": "/favicons.ico/android-icon-36x36.png",
"sizes": "36x36",
"type": "image/png",
"density": "0.75"
},
{
"src": "/favicons.ico/android-icon-48x48.png",
"sizes": "48x48",
"type": "image/png",
"density": "1.0"
},
{
"src": "/favicons.ico/android-icon-72x72.png",
"sizes": "72x72",
"type": "image/png",
"density": "1.5"
},
{
"src": "/favicons.ico/android-icon-96x96.png",
"sizes": "96x96",
"type": "image/png",
"density": "2.0"
},
{
"src": "/favicons.ico/android-icon-144x144.png",
"sizes": "144x144",
"type": "image/png",
"density": "3.0"
},
{
"src": "/favicons.ico/android-icon-192x192.png",
"sizes": "192x192",
"type": "image/png",
"density": "4.0"
},
{
"src": "/favicons.ico/favicon-96x96.png",
"sizes": "96x96",
"type": "image/png",
"density": "2.0"
}
]
}
================================================
FILE: public/robots.txt
================================================
# https://www.robotstxt.org/robotstxt.html
User-agent: *
================================================
FILE: src/Backend/GameLogic/ArrivalUtils.ts
================================================
import { CONTRACT_PRECISION } from '@darkforest_eth/constants';
import { hasOwner, isActivated, isEmojiFlagMessage } from '@darkforest_eth/gamelogic';
import {
ArrivalType,
Artifact,
ArtifactType,
EmojiFlagBody,
Planet,
PlanetMessage,
PlanetType,
QueuedArrival,
Upgrade,
} from '@darkforest_eth/types';
import _ from 'lodash';
import { ContractConstants } from '../../_types/darkforest/api/ContractsAPITypes';
// TODO: planet class, cmon, let's go
export const blocksLeftToProspectExpiration = (
currentBlockNumber: number,
prospectedBlockNumber?: number
) => {
return (prospectedBlockNumber || 0) + 255 - currentBlockNumber;
};
// TODO: Planet. Class.
export const prospectExpired = (currentBlockNumber: number, prospectedBlockNumber: number) => {
return blocksLeftToProspectExpiration(currentBlockNumber, prospectedBlockNumber) <= 0;
};
export const isFindable = (planet: Planet, currentBlockNumber?: number): boolean => {
return (
currentBlockNumber !== undefined &&
planet.planetType === PlanetType.RUINS &&
planet.prospectedBlockNumber !== undefined &&
!planet.hasTriedFindingArtifact &&
!prospectExpired(currentBlockNumber, planet.prospectedBlockNumber)
);
};
export const isProspectable = (planet: Planet): boolean => {
return planet.planetType === PlanetType.RUINS && planet.prospectedBlockNumber === undefined;
};
const getSilverOverTime = (
planet: Planet,
startTimeMillis: number,
endTimeMillis: number
): number => {
if (!hasOwner(planet)) {
return planet.silver;
}
if (planet.silver > planet.silverCap) {
return planet.silverCap;
}
const timeElapsed = endTimeMillis / 1000 - startTimeMillis / 1000;
return Math.min(timeElapsed * planet.silverGrowth + planet.silver, planet.silverCap);
};
const getEnergyAtTime = (planet: Planet, atTimeMillis: number): number => {
if (planet.energy === 0) {
return 0;
}
if (!hasOwner(planet)) {
return planet.energy;
}
if (planet.planetType === PlanetType.SILVER_BANK) {
if (planet.energy > planet.energyCap) {
return planet.energyCap;
}
}
const timeElapsed = atTimeMillis / 1000 - planet.lastUpdated;
const denominator =
Math.exp((-4 * planet.energyGrowth * timeElapsed) / planet.energyCap) *
(planet.energyCap / planet.energy - 1) +
1;
return planet.energyCap / denominator;
};
export const updatePlanetToTime = (
planet: Planet,
planetArtifacts: Artifact[],
atTimeMillis: number,
contractConstants: ContractConstants,
setPlanet: (p: Planet) => void = () => {}
): void => {
if (atTimeMillis < planet.lastUpdated * 1000) {
return;
}
if (planet.pausers === 0) {
planet.silver = getSilverOverTime(planet, planet.lastUpdated * 1000, atTimeMillis);
planet.energy = getEnergyAtTime(planet, atTimeMillis);
}
planet.lastUpdated = atTimeMillis / 1000;
const photoidActivationTime = contractConstants.PHOTOID_ACTIVATION_DELAY * 1000;
const activePhotoid = planetArtifacts.find(
(a) =>
a.artifactType === ArtifactType.PhotoidCannon &&
isActivated(a) &&
atTimeMillis - a.lastActivated * 1000 >= photoidActivationTime
);
if (activePhotoid && !planet.localPhotoidUpgrade) {
planet.localPhotoidUpgrade = activePhotoid.timeDelayedUpgrade;
applyUpgrade(planet, activePhotoid.timeDelayedUpgrade);
}
setPlanet(planet);
};
export const applyUpgrade = (planet: Planet, upgrade: Upgrade, unApply = false) => {
if (unApply) {
planet.speed /= upgrade.energyCapMultiplier / 100;
planet.energyGrowth /= upgrade.energyGroMultiplier / 100;
planet.range /= upgrade.rangeMultiplier / 100;
planet.speed /= upgrade.speedMultiplier / 100;
planet.defense /= upgrade.defMultiplier / 100;
} else {
planet.speed *= upgrade.energyCapMultiplier / 100;
planet.energyGrowth *= upgrade.energyGroMultiplier / 100;
planet.range *= upgrade.rangeMultiplier / 100;
planet.speed *= upgrade.speedMultiplier / 100;
planet.defense *= upgrade.defMultiplier / 100;
}
};
/**
* @param previous The previously calculated state of a planet
* @param current The current calculated state of the planet
* @param arrival The Arrival that caused the state change
*/
export interface PlanetDiff {
previous: Planet;
current: Planet;
arrival: QueuedArrival;
}
export const arrive = (
toPlanet: Planet,
artifactsOnPlanet: Artifact[],
arrival: QueuedArrival,
arrivingArtifact: Artifact | undefined,
contractConstants: ContractConstants
): PlanetDiff => {
// this function optimistically simulates an arrival
if (toPlanet.locationId !== arrival.toPlanet) {
throw new Error(`attempted to apply arrival for wrong toPlanet ${toPlanet.locationId}`);
}
// update toPlanet energy and silver right before arrival
updatePlanetToTime(toPlanet, artifactsOnPlanet, arrival.arrivalTime * 1000, contractConstants);
const prevPlanet = _.cloneDeep(toPlanet);
if (toPlanet.destroyed) {
return { arrival: arrival, previous: toPlanet, current: toPlanet };
}
// apply energy
const { energyArriving } = arrival;
if (arrival.player !== toPlanet.owner) {
if (arrival.arrivalType === ArrivalType.Wormhole) {
// if this is a wormhole arrival to a planet that isn't owned by the initiator of
// the move, then don't move any energy
}
// attacking enemy - includes emptyAddress
else if (
toPlanet.energy >
Math.floor((energyArriving * CONTRACT_PRECISION * 100) / toPlanet.defense) /
CONTRACT_PRECISION
) {
// attack reduces target planet's garrison but doesn't conquer it
toPlanet.energy -=
Math.floor((energyArriving * CONTRACT_PRECISION * 100) / toPlanet.defense) /
CONTRACT_PRECISION;
} else {
// conquers planet
toPlanet.owner = arrival.player;
toPlanet.energy =
energyArriving -
Math.floor((toPlanet.energy * CONTRACT_PRECISION * toPlanet.defense) / 100) /
CONTRACT_PRECISION;
}
} else {
// moving between my own planets
toPlanet.energy += energyArriving;
}
if (toPlanet.planetType === PlanetType.SILVER_BANK || toPlanet.pausers !== 0) {
if (toPlanet.energy > toPlanet.energyCap) {
toPlanet.energy = toPlanet.energyCap;
}
}
// apply silver
if (toPlanet.silver + arrival.silverMoved > toPlanet.silverCap) {
toPlanet.silver = toPlanet.silverCap;
} else {
toPlanet.silver += arrival.silverMoved;
}
// transfer artifact if necessary
if (arrival.artifactId) {
toPlanet.heldArtifactIds.push(arrival.artifactId);
}
if (arrivingArtifact) {
if (arrivingArtifact.artifactType === ArtifactType.ShipMothership) {
toPlanet.energyGrowth *= 2;
} else if (arrivingArtifact.artifactType === ArtifactType.ShipWhale) {
toPlanet.silverGrowth *= 2;
} else if (arrivingArtifact.artifactType === ArtifactType.ShipTitan) {
toPlanet.pausers++;
}
arrivingArtifact.onPlanetId = toPlanet.locationId;
}
return { arrival, current: toPlanet, previous: prevPlanet };
};
/**
* @todo ArrivalUtils has become a dumping ground for functions that should just live inside of a
* `Planet` class.
*/
export function getEmojiMessage(
planet: Planet | undefined
): PlanetMessage | undefined {
return planet?.messages?.find(isEmojiFlagMessage);
}
================================================
FILE: src/Backend/GameLogic/CaptureZoneGenerator.ts
================================================
import { monomitter, Monomitter } from '@darkforest_eth/events';
import { CaptureZone, Chunk, LocationId } from '@darkforest_eth/types';
import bigInt from 'big-integer';
import { utils } from 'ethers';
import GameManager, { GameManagerEvent } from './GameManager';
export type CaptureZonesGeneratedEvent = {
changeBlock: number;
nextChangeBlock: number;
zones: CaptureZone[];
};
/**
* Given a game start block and a zone change block interval, decide when to generate new Capture Zones.
*/
export class CaptureZoneGenerator {
private gameManager: GameManager;
private zones: Set;
private capturablePlanets: Set;
private lastChangeBlock: number;
private nextChangeBlock: number;
private changeInterval: number;
public readonly generated$: Monomitter;
constructor(gameManager: GameManager, gameStartBlock: number, changeInterval: number) {
this.gameManager = gameManager;
this.changeInterval = changeInterval;
this.nextChangeBlock = gameStartBlock;
this.generated$ = monomitter();
this.capturablePlanets = new Set();
this.zones = new Set();
gameManager.on(GameManagerEvent.DiscoveredNewChunk, this.onNewChunk.bind(this));
}
/**
* Call when a new block is received to check if generation is needed.
* @param blockNumber Current block number.
*/
async generate(blockNumber: number) {
this.setNextGenerationBlock(blockNumber);
const newZones = await this._generate(this.nextChangeBlock - this.changeInterval);
this.zones = newZones;
this.updateCapturablePlanets();
this.generated$.publish({
changeBlock: this.lastChangeBlock,
nextChangeBlock: this.nextChangeBlock,
zones: Array.from(this.zones),
});
}
private setNextGenerationBlock(blockNumber: number) {
const totalGameBlocks = blockNumber - this.gameManager.getContractConstants().GAME_START_BLOCK;
const numPastIntervals = Math.floor(totalGameBlocks / this.changeInterval);
this.nextChangeBlock =
this.gameManager.getContractConstants().GAME_START_BLOCK +
(numPastIntervals + 1) * this.changeInterval;
}
private async _generate(blockNumber: number) {
const block = await this.gameManager.getEthConnection().getProvider().getBlock(blockNumber);
const worldRadius = await this.gameManager.getContractAPI().getWorldRadius();
const captureZones = new Set();
const ringSize = 5000;
const ringCount = Math.floor(worldRadius / ringSize);
const zonesPerRing =
this.gameManager.getContractConstants().CAPTURE_ZONES_PER_5000_WORLD_RADIUS;
for (let ring = 0; ring < ringCount; ring++) {
const nonceBase = ring * zonesPerRing;
for (let j = 0; j < zonesPerRing; j++) {
const nonce = nonceBase + j;
const blockAndNonceHash = utils.solidityKeccak256(
['bytes32', 'uint256'],
[block.hash, nonce]
);
// Chop off 0x and convert to BigInt
const seed = bigInt(blockAndNonceHash.substring(2, blockAndNonceHash.length), 16);
// Last 3 hex characters
const angleSeed = seed.mod(0xfff);
// Max value of 0xfff is 4095
// 4095 / 651 is max radians in circle
// Mult by 1e18 to convert to big number math
const angleRads = angleSeed.multiply(1e18).divide(651);
// Next 6 hex characters
const distanceSeed = seed.minus(angleSeed).divide(4096).mod(0xffffff);
// 16777215 is value of FFFFFF
// Clamp distance within ring radius
const divisor = Math.floor(16777215 / ringSize);
// Add in distance from origin point
const distance = distanceSeed.divide(divisor).add(ring * ringSize);
// Bring it back down to number
const angleNumber = Number(angleRads) / 1e18;
const distanceNumber = Number(distance);
const coords = {
x: Math.floor(distanceNumber * Math.cos(angleNumber)),
y: Math.floor(distanceNumber * Math.sin(angleNumber)),
};
captureZones.add({
coords,
radius: this.gameManager.getContractConstants().CAPTURE_ZONE_RADIUS,
});
}
}
this.lastChangeBlock = blockNumber;
return captureZones;
}
private updateCapturablePlanets() {
this.capturablePlanets = new Set();
for (const zone of this.getZones()) {
const planetsInZone = this.gameObjects.getPlanetsInWorldCircle(zone.coords, zone.radius);
for (const planet of planetsInZone) {
this.capturablePlanets.add(planet.locationId);
}
}
}
private get gameObjects() {
return this.gameManager.getGameObjects();
}
private onNewChunk(chunk: Chunk) {
for (const worldLocation of chunk.planetLocations) {
for (const zone of this.getZones()) {
const { x: planetX, y: planetY } = worldLocation.coords;
const { x: zoneX, y: zoneY } = zone.coords;
const distance = Math.sqrt((planetX - zoneX) ** 2 + (planetY - zoneY) ** 2);
if (distance <= zone.radius) {
this.capturablePlanets.add(worldLocation.hash);
}
}
}
}
/**
* Is the given planet inside of a Capture Zone.
*/
public isInZone(locationId: LocationId) {
return this.capturablePlanets.has(locationId);
}
/**
* The next block that will trigger a Capture Zone generation.
*/
public getNextChangeBlock() {
return this.nextChangeBlock;
}
public getZones() {
return this.zones;
}
}
================================================
FILE: src/Backend/GameLogic/ContractsAPI.ts
================================================
import { EMPTY_LOCATION_ID } from '@darkforest_eth/constants';
import { DarkForest } from '@darkforest_eth/contracts/typechain';
import {
aggregateBulkGetter,
ContractCaller,
EthConnection,
ethToWei,
TxCollection,
TxExecutor,
} from '@darkforest_eth/network';
import {
address,
artifactIdFromEthersBN,
artifactIdToDecStr,
decodeArrival,
decodeArtifact,
decodeArtifactPointValues,
decodePlanet,
decodePlanetDefaults,
decodePlayer,
decodeRevealedCoords,
decodeUpgradeBranches,
locationIdFromEthersBN,
locationIdToDecStr,
} from '@darkforest_eth/serde';
import {
Artifact,
ArtifactId,
ArtifactType,
AutoGasSetting,
DiagnosticUpdater,
EthAddress,
LocationId,
Planet,
Player,
QueuedArrival,
RevealedCoords,
Setting,
Transaction,
TransactionId,
TxIntent,
VoyageId,
} from '@darkforest_eth/types';
import { BigNumber as EthersBN, ContractFunction, Event, providers } from 'ethers';
import { EventEmitter } from 'events';
import _ from 'lodash';
import NotificationManager from '../../Frontend/Game/NotificationManager';
import { openConfirmationWindowForTransaction } from '../../Frontend/Game/Popups';
import { getSetting } from '../../Frontend/Utils/SettingsHooks';
import {
ContractConstants,
ContractEvent,
ContractsAPIEvent,
PlanetTypeWeightsBySpaceType,
} from '../../_types/darkforest/api/ContractsAPITypes';
import { loadDiamondContract } from '../Network/Blockchain';
import { eventLogger, EventType } from '../Network/EventLogger';
interface ContractsApiConfig {
connection: EthConnection;
contractAddress: EthAddress;
}
/**
* Roughly contains methods that map 1:1 with functions that live in the contract. Responsible for
* reading and writing to and from the blockchain.
*
* @todo don't inherit from {@link EventEmitter}. instead use {@link Monomitter}
*/
export class ContractsAPI extends EventEmitter {
/**
* Don't allow users to submit txs if balance falls below this amount/
*/
private static readonly MIN_BALANCE = ethToWei(0.002);
/**
* Instrumented {@link ThrottledConcurrentQueue} for blockchain reads.
*/
private readonly contractCaller: ContractCaller;
/**
* Instrumented {@link ThrottledConcurrentQueue} for blockchain writes.
*/
public readonly txExecutor: TxExecutor;
/**
* Our connection to the blockchain. In charge of low level networking, and also of the burner
* wallet.
*/
public readonly ethConnection: EthConnection;
/**
* The contract address is saved on the object upon construction
*/
private contractAddress: EthAddress;
get contract() {
return this.ethConnection.getContract(this.contractAddress);
}
public constructor({ connection, contractAddress }: ContractsApiConfig) {
super();
this.contractCaller = new ContractCaller();
this.ethConnection = connection;
this.contractAddress = contractAddress;
this.txExecutor = new TxExecutor(
connection,
this.getGasFeeForTransaction.bind(this),
this.beforeQueued.bind(this),
this.beforeTransaction.bind(this),
this.afterTransaction.bind(this)
);
this.setupEventListeners();
}
/**
* We pass this function into {@link TxExecutor} to calculate what gas fee we should use for the
* given transaction. The result is either a number, measured in gwei, represented as a string, or
* a string representing that we want to use an auto gas setting.
*/
private getGasFeeForTransaction(tx: Transaction): AutoGasSetting | string {
if (
(tx.intent.methodName === 'initializePlayer' || tx.intent.methodName === 'getSpaceShips') &&
tx.intent.contract.address === this.contract.address
) {
return '50';
}
const config = {
contractAddress: this.contractAddress,
account: this.ethConnection.getAddress(),
};
return getSetting(config, Setting.GasFeeGwei);
}
/**
* This function is called by {@link TxExecutor} before a transaction is queued.
* It gives the client an opportunity to prevent a transaction from being queued based
* on business logic or user interaction.
*
* Reject the promise to prevent the queued transaction from being queued.
*/
private async beforeQueued(
id: TransactionId,
intent: TxIntent,
overrides?: providers.TransactionRequest
): Promise {
const address = this.ethConnection.getAddress();
if (!address) throw new Error("can't send a transaction, no signer");
const balance = await this.ethConnection.loadBalance(address);
if (balance.lt(ContractsAPI.MIN_BALANCE)) {
const notifsManager = NotificationManager.getInstance();
notifsManager.balanceEmpty();
throw new Error('xDAI balance too low!');
}
const gasFeeGwei = EthersBN.from(overrides?.gasPrice || '1000000000');
await openConfirmationWindowForTransaction({
contractAddress: this.contractAddress,
connection: this.ethConnection,
id,
intent,
overrides,
from: address,
gasFeeGwei,
});
}
/**
* This function is called by {@link TxExecutor} before each transaction. It gives the client an
* opportunity to prevent a transaction from going through based on business logic or user
* interaction. To prevent the queued transaction from being submitted, throw an Error.
*/
private async beforeTransaction(tx: Transaction): Promise {
this.emit(ContractsAPIEvent.TxProcessing, tx);
}
private async afterTransaction(_txRequest: Transaction, txDiagnosticInfo: unknown) {
eventLogger.logEvent(EventType.Transaction, txDiagnosticInfo);
}
public destroy(): void {
this.removeEventListeners();
}
private makeCall(contractViewFunction: ContractFunction, args: unknown[] = []): Promise {
return this.contractCaller.makeCall(contractViewFunction, args);
}
public async setupEventListeners(): Promise {
const { contract } = this;
const filter = {
address: contract.address,
topics: [
[
contract.filters.ArrivalQueued(null, null, null, null, null).topics,
contract.filters.ArtifactActivated(null, null, null).topics,
contract.filters.ArtifactDeactivated(null, null, null).topics,
contract.filters.ArtifactDeposited(null, null, null).topics,
contract.filters.ArtifactFound(null, null, null).topics,
contract.filters.ArtifactWithdrawn(null, null, null).topics,
contract.filters.LocationRevealed(null, null, null, null).topics,
contract.filters.PlanetHatBought(null, null, null).topics,
contract.filters.PlanetProspected(null, null).topics,
contract.filters.PlanetSilverWithdrawn(null, null, null).topics,
contract.filters.PlanetTransferred(null, null, null).topics,
contract.filters.PlanetInvaded(null, null).topics,
contract.filters.PlanetCaptured(null, null).topics,
contract.filters.PlayerInitialized(null, null).topics,
contract.filters.AdminOwnershipChanged(null, null).topics,
contract.filters.AdminGiveSpaceship(null, null).topics,
contract.filters.PauseStateChanged(null).topics,
contract.filters.LobbyCreated(null, null).topics,
].map((topicsOrUndefined) => (topicsOrUndefined || [])[0]),
] as Array>,
};
const eventHandlers = {
[ContractEvent.PauseStateChanged]: (paused: boolean) => {
this.emit(ContractsAPIEvent.PauseStateChanged, paused);
},
[ContractEvent.AdminOwnershipChanged]: (location: EthersBN, _newOwner: string) => {
this.emit(ContractsAPIEvent.PlanetUpdate, locationIdFromEthersBN(location));
},
[ContractEvent.AdminGiveSpaceship]: (
location: EthersBN,
_newOwner: string,
_type: ArtifactType
) => {
this.emit(ContractsAPIEvent.PlanetUpdate, locationIdFromEthersBN(location));
},
[ContractEvent.ArtifactFound]: (
_playerAddr: string,
rawArtifactId: EthersBN,
loc: EthersBN
) => {
const artifactId = artifactIdFromEthersBN(rawArtifactId);
this.emit(ContractsAPIEvent.ArtifactUpdate, artifactId);
this.emit(ContractsAPIEvent.PlanetUpdate, locationIdFromEthersBN(loc));
},
[ContractEvent.ArtifactDeposited]: (
_playerAddr: string,
rawArtifactId: EthersBN,
loc: EthersBN
) => {
const artifactId = artifactIdFromEthersBN(rawArtifactId);
this.emit(ContractsAPIEvent.ArtifactUpdate, artifactId);
this.emit(ContractsAPIEvent.PlanetUpdate, locationIdFromEthersBN(loc));
},
[ContractEvent.ArtifactWithdrawn]: (
_playerAddr: string,
rawArtifactId: EthersBN,
loc: EthersBN
) => {
const artifactId = artifactIdFromEthersBN(rawArtifactId);
this.emit(ContractsAPIEvent.ArtifactUpdate, artifactId);
this.emit(ContractsAPIEvent.PlanetUpdate, locationIdFromEthersBN(loc));
},
[ContractEvent.ArtifactActivated]: (
_playerAddr: string,
rawArtifactId: EthersBN,
loc: EthersBN
) => {
const artifactId = artifactIdFromEthersBN(rawArtifactId);
this.emit(ContractsAPIEvent.ArtifactUpdate, artifactId);
this.emit(ContractsAPIEvent.PlanetUpdate, locationIdFromEthersBN(loc));
},
[ContractEvent.ArtifactDeactivated]: (
_playerAddr: string,
rawArtifactId: EthersBN,
loc: EthersBN
) => {
const artifactId = artifactIdFromEthersBN(rawArtifactId);
this.emit(ContractsAPIEvent.ArtifactUpdate, artifactId);
this.emit(ContractsAPIEvent.PlanetUpdate, locationIdFromEthersBN(loc));
},
[ContractEvent.PlayerInitialized]: async (player: string, locRaw: EthersBN, _: Event) => {
this.emit(ContractsAPIEvent.PlayerUpdate, address(player));
this.emit(ContractsAPIEvent.PlanetUpdate, locationIdFromEthersBN(locRaw));
this.emit(ContractsAPIEvent.RadiusUpdated);
},
[ContractEvent.PlanetTransferred]: async (
_senderAddress: string,
planetId: EthersBN,
receiverAddress: string,
_: Event
) => {
this.emit(
ContractsAPIEvent.PlanetTransferred,
locationIdFromEthersBN(planetId),
address(receiverAddress)
);
},
[ContractEvent.ArrivalQueued]: async (
playerAddr: string,
arrivalId: EthersBN,
fromLocRaw: EthersBN,
toLocRaw: EthersBN,
_artifactIdRaw: EthersBN,
_: Event
) => {
this.emit(
ContractsAPIEvent.ArrivalQueued,
arrivalId.toString() as VoyageId,
locationIdFromEthersBN(fromLocRaw),
locationIdFromEthersBN(toLocRaw)
);
this.emit(ContractsAPIEvent.PlayerUpdate, address(playerAddr));
this.emit(ContractsAPIEvent.RadiusUpdated);
},
[ContractEvent.PlanetUpgraded]: async (
_playerAddr: string,
location: EthersBN,
_branch: EthersBN,
_toBranchLevel: EthersBN,
_: Event
) => {
this.emit(ContractsAPIEvent.PlanetUpdate, locationIdFromEthersBN(location));
},
[ContractEvent.PlanetInvaded]: async (_playerAddr: string, location: EthersBN, _: Event) => {
this.emit(ContractsAPIEvent.PlanetUpdate, locationIdFromEthersBN(location));
},
[ContractEvent.PlanetCaptured]: async (_playerAddr: string, location: EthersBN, _: Event) => {
this.emit(ContractsAPIEvent.PlanetUpdate, locationIdFromEthersBN(location));
},
[ContractEvent.PlanetHatBought]: async (
_playerAddress: string,
location: EthersBN,
_: Event
) => this.emit(ContractsAPIEvent.PlanetUpdate, locationIdFromEthersBN(location)),
[ContractEvent.LocationRevealed]: async (
revealerAddr: string,
location: EthersBN,
_x: EthersBN,
_y: EthersBN,
_: Event
) => {
this.emit(ContractsAPIEvent.PlanetUpdate, locationIdFromEthersBN(location));
this.emit(
ContractsAPIEvent.LocationRevealed,
locationIdFromEthersBN(location),
address(revealerAddr.toLowerCase())
);
this.emit(ContractsAPIEvent.PlayerUpdate, address(revealerAddr));
},
[ContractEvent.PlanetSilverWithdrawn]: async (
player: string,
location: EthersBN,
_amount: EthersBN,
_: Event
) => {
this.emit(ContractsAPIEvent.PlanetUpdate, locationIdFromEthersBN(location));
this.emit(ContractsAPIEvent.PlayerUpdate, address(player));
},
[ContractEvent.LobbyCreated]: (ownerAddr: string, lobbyAddr: string) => {
this.emit(ContractsAPIEvent.LobbyCreated, address(ownerAddr), address(lobbyAddr));
},
};
this.ethConnection.subscribeToContractEvents(contract, eventHandlers, filter);
}
public removeEventListeners(): void {
const { contract } = this;
contract.removeAllListeners(ContractEvent.PlayerInitialized);
contract.removeAllListeners(ContractEvent.ArrivalQueued);
contract.removeAllListeners(ContractEvent.PlanetUpgraded);
contract.removeAllListeners(ContractEvent.PlanetHatBought);
contract.removeAllListeners(ContractEvent.PlanetTransferred);
contract.removeAllListeners(ContractEvent.ArtifactFound);
contract.removeAllListeners(ContractEvent.ArtifactDeposited);
contract.removeAllListeners(ContractEvent.ArtifactWithdrawn);
contract.removeAllListeners(ContractEvent.ArtifactActivated);
contract.removeAllListeners(ContractEvent.ArtifactDeactivated);
contract.removeAllListeners(ContractEvent.LocationRevealed);
contract.removeAllListeners(ContractEvent.PlanetSilverWithdrawn);
contract.removeAllListeners(ContractEvent.PlanetInvaded);
contract.removeAllListeners(ContractEvent.PlanetCaptured);
}
public getContractAddress(): EthAddress {
return this.contractAddress;
}
async getConstants(): Promise {
const {
DISABLE_ZK_CHECKS,
PLANETHASH_KEY,
SPACETYPE_KEY,
BIOMEBASE_KEY,
PERLIN_LENGTH_SCALE,
PERLIN_MIRROR_X,
PERLIN_MIRROR_Y,
} = await this.makeCall(this.contract.getSnarkConstants);
const {
ADMIN_CAN_ADD_PLANETS,
WORLD_RADIUS_LOCKED,
WORLD_RADIUS_MIN,
MAX_NATURAL_PLANET_LEVEL,
TIME_FACTOR_HUNDREDTHS,
PERLIN_THRESHOLD_1,
PERLIN_THRESHOLD_2,
PERLIN_THRESHOLD_3,
INIT_PERLIN_MIN,
INIT_PERLIN_MAX,
SPAWN_RIM_AREA,
BIOME_THRESHOLD_1,
BIOME_THRESHOLD_2,
SILVER_SCORE_VALUE,
// TODO: Actually put this in game constants
// PLANET_LEVEL_THRESHOLDS,
PLANET_RARITY,
PLANET_TRANSFER_ENABLED,
PHOTOID_ACTIVATION_DELAY,
LOCATION_REVEAL_COOLDOWN,
SPACE_JUNK_ENABLED,
SPACE_JUNK_LIMIT,
PLANET_LEVEL_JUNK,
ABANDON_SPEED_CHANGE_PERCENT,
ABANDON_RANGE_CHANGE_PERCENT,
// Capture Zones
GAME_START_BLOCK,
CAPTURE_ZONES_ENABLED,
CAPTURE_ZONE_COUNT,
CAPTURE_ZONE_CHANGE_BLOCK_INTERVAL,
CAPTURE_ZONE_RADIUS,
CAPTURE_ZONE_PLANET_LEVEL_SCORE,
CAPTURE_ZONE_HOLD_BLOCKS_REQUIRED,
CAPTURE_ZONES_PER_5000_WORLD_RADIUS,
} = await this.makeCall(this.contract.getGameConstants);
const TOKEN_MINT_END_SECONDS = (
await this.makeCall(this.contract.TOKEN_MINT_END_TIMESTAMP)
).toNumber();
const adminAddress = address(await this.makeCall(this.contract.adminAddress));
const upgrades = decodeUpgradeBranches(await this.makeCall(this.contract.getUpgrades));
const PLANET_TYPE_WEIGHTS: PlanetTypeWeightsBySpaceType =
await this.makeCall(this.contract.getTypeWeights);
const rawPointValues = await this.makeCall(this.contract.getArtifactPointValues);
const ARTIFACT_POINT_VALUES = decodeArtifactPointValues(rawPointValues);
const planetDefaults = decodePlanetDefaults(await this.makeCall(this.contract.getDefaultStats));
const planetLevelThresholds = (
await this.makeCall(this.contract.getPlanetLevelThresholds)
).map((x: EthersBN) => x.toNumber());
const planetCumulativeRarities = (
await this.makeCall(this.contract.getCumulativeRarities)
).map((x: EthersBN) => x.toNumber());
const constants: ContractConstants = {
ADMIN_CAN_ADD_PLANETS,
WORLD_RADIUS_LOCKED,
WORLD_RADIUS_MIN: WORLD_RADIUS_MIN.toNumber(),
DISABLE_ZK_CHECKS,
PLANETHASH_KEY: PLANETHASH_KEY.toNumber(),
SPACETYPE_KEY: SPACETYPE_KEY.toNumber(),
BIOMEBASE_KEY: BIOMEBASE_KEY.toNumber(),
PERLIN_LENGTH_SCALE: PERLIN_LENGTH_SCALE.toNumber(),
PERLIN_MIRROR_X,
PERLIN_MIRROR_Y,
CLAIM_PLANET_COOLDOWN: 0,
TOKEN_MINT_END_SECONDS,
MAX_NATURAL_PLANET_LEVEL: MAX_NATURAL_PLANET_LEVEL.toNumber(),
TIME_FACTOR_HUNDREDTHS: TIME_FACTOR_HUNDREDTHS.toNumber(),
PERLIN_THRESHOLD_1: PERLIN_THRESHOLD_1.toNumber(),
PERLIN_THRESHOLD_2: PERLIN_THRESHOLD_2.toNumber(),
PERLIN_THRESHOLD_3: PERLIN_THRESHOLD_3.toNumber(),
INIT_PERLIN_MIN: INIT_PERLIN_MIN.toNumber(),
INIT_PERLIN_MAX: INIT_PERLIN_MAX.toNumber(),
BIOME_THRESHOLD_1: BIOME_THRESHOLD_1.toNumber(),
BIOME_THRESHOLD_2: BIOME_THRESHOLD_2.toNumber(),
SILVER_SCORE_VALUE: SILVER_SCORE_VALUE.toNumber(),
PLANET_LEVEL_THRESHOLDS: [
planetLevelThresholds[0],
planetLevelThresholds[1],
planetLevelThresholds[2],
planetLevelThresholds[3],
planetLevelThresholds[4],
planetLevelThresholds[5],
planetLevelThresholds[6],
planetLevelThresholds[7],
planetLevelThresholds[8],
planetLevelThresholds[9],
],
PLANET_RARITY: PLANET_RARITY.toNumber(),
PLANET_TRANSFER_ENABLED,
PLANET_TYPE_WEIGHTS,
ARTIFACT_POINT_VALUES,
SPACE_JUNK_ENABLED,
SPACE_JUNK_LIMIT: SPACE_JUNK_LIMIT.toNumber(),
PLANET_LEVEL_JUNK: [
PLANET_LEVEL_JUNK[0].toNumber(),
PLANET_LEVEL_JUNK[1].toNumber(),
PLANET_LEVEL_JUNK[2].toNumber(),
PLANET_LEVEL_JUNK[3].toNumber(),
PLANET_LEVEL_JUNK[4].toNumber(),
PLANET_LEVEL_JUNK[5].toNumber(),
PLANET_LEVEL_JUNK[6].toNumber(),
PLANET_LEVEL_JUNK[7].toNumber(),
PLANET_LEVEL_JUNK[8].toNumber(),
PLANET_LEVEL_JUNK[9].toNumber(),
],
ABANDON_SPEED_CHANGE_PERCENT: ABANDON_RANGE_CHANGE_PERCENT.toNumber(),
ABANDON_RANGE_CHANGE_PERCENT: ABANDON_SPEED_CHANGE_PERCENT.toNumber(),
PHOTOID_ACTIVATION_DELAY: PHOTOID_ACTIVATION_DELAY.toNumber(),
SPAWN_RIM_AREA: SPAWN_RIM_AREA.toNumber(),
LOCATION_REVEAL_COOLDOWN: LOCATION_REVEAL_COOLDOWN.toNumber(),
defaultPopulationCap: planetDefaults.populationCap,
defaultPopulationGrowth: planetDefaults.populationGrowth,
defaultRange: planetDefaults.range,
defaultSpeed: planetDefaults.speed,
defaultDefense: planetDefaults.defense,
defaultSilverGrowth: planetDefaults.silverGrowth,
defaultSilverCap: planetDefaults.silverCap,
defaultBarbarianPercentage: planetDefaults.barbarianPercentage,
planetLevelThresholds,
planetCumulativeRarities,
upgrades,
adminAddress,
// Capture Zones
GAME_START_BLOCK: GAME_START_BLOCK.toNumber(),
CAPTURE_ZONES_ENABLED,
CAPTURE_ZONE_COUNT: CAPTURE_ZONE_COUNT.toNumber(),
CAPTURE_ZONE_CHANGE_BLOCK_INTERVAL: CAPTURE_ZONE_CHANGE_BLOCK_INTERVAL.toNumber(),
CAPTURE_ZONE_RADIUS: CAPTURE_ZONE_RADIUS.toNumber(),
CAPTURE_ZONE_PLANET_LEVEL_SCORE: [
CAPTURE_ZONE_PLANET_LEVEL_SCORE[0].toNumber(),
CAPTURE_ZONE_PLANET_LEVEL_SCORE[1].toNumber(),
CAPTURE_ZONE_PLANET_LEVEL_SCORE[2].toNumber(),
CAPTURE_ZONE_PLANET_LEVEL_SCORE[3].toNumber(),
CAPTURE_ZONE_PLANET_LEVEL_SCORE[4].toNumber(),
CAPTURE_ZONE_PLANET_LEVEL_SCORE[5].toNumber(),
CAPTURE_ZONE_PLANET_LEVEL_SCORE[6].toNumber(),
CAPTURE_ZONE_PLANET_LEVEL_SCORE[7].toNumber(),
CAPTURE_ZONE_PLANET_LEVEL_SCORE[8].toNumber(),
CAPTURE_ZONE_PLANET_LEVEL_SCORE[9].toNumber(),
],
CAPTURE_ZONE_HOLD_BLOCKS_REQUIRED: CAPTURE_ZONE_HOLD_BLOCKS_REQUIRED.toNumber(),
CAPTURE_ZONES_PER_5000_WORLD_RADIUS: CAPTURE_ZONES_PER_5000_WORLD_RADIUS.toNumber(),
};
return constants;
}
public async getPlayers(
onProgress?: (fractionCompleted: number) => void
): Promise