Skip to content

Commit

Permalink
Implement layer customisation
Browse files Browse the repository at this point in the history
  • Loading branch information
Nolway committed Mar 16, 2022
1 parent c00521c commit 37764b9
Show file tree
Hide file tree
Showing 56 changed files with 910 additions and 689 deletions.
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
Binary file removed assets/layers/accessory/None#500.png
Diff not rendered.
Binary file removed assets/layers/hair/None#50.png
Diff not rendered.
Binary file removed assets/layers/hat/None#10.png
Diff not rendered.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
"license": "AGPL-3.0",
"scripts": {
"dev": "tsc --watch",
"generate": "ts-node ./src/generate.ts",
"reset": "ts-node ./src/reset.ts",
"generate": "ts-node ./src/bin/generate.ts",
"reset": "ts-node ./src/bin/reset.ts",
"lint": "eslint --ext .js,.ts .",
"format": "prettier --ignore-path .gitignore --write \"**/*.+(js|ts|json)\"",
"prepare": "husky install"
Expand Down
182 changes: 182 additions & 0 deletions src/bin/generate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import fs from "fs";
import chalk from "chalk";
import { Config, ConfigCollection, isConfig } from "../guards/ConfigGuards";
import { LoadedLayers, Woka, WokaTexture } from "../guards/WokaGuards";
import { configPath } from "../env";
import { layersDirPath } from "../env";
import { generateBuildDirectories } from "../utils/DirectoryUtils";
import { maxCombination } from "../utils/LayerUtils";
import { WokaGenerator } from "../generators/files/WokaGenerator";
import { MetadataGenerator } from "../generators/files/MetadataGenerator";
import { CropGenerator } from "../generators/files/CropGenerator";
import { AvatarGenerator } from "../generators/files/AvatarGenerator";

let loadedLayers: LoadedLayers;
const wokasGenerated: Woka[] = [];

async function run(): Promise<void> {
if (!fs.existsSync(configPath)) {
throw new Error("No config.ts fund! You can copy the config.dist.ts to config.ts");
}

const configFile = await import(configPath);
const config = isConfig.parse(configFile.default);

generateBuildDirectories();

await generate(config);
}

async function generate(config: Config): Promise<void> {
loadedLayers = await loadLayers(config.collection);

// Check max combination
const max = maxCombination(loadedLayers);
console.log(`${max} combinations can be generated`);

if (max < config.collection.size) {
throw new Error(
`You want to generate a collection of ${config.collection.size} Woka but you can only generate ${max}`
);
}

// Initiate generators
const wokaGenerator = new WokaGenerator(config, loadedLayers);
const metadataGenerator = new MetadataGenerator(config.blockchain);
const cropGenerator = new CropGenerator(config.collection.crop);
const avatarGenerator = new AvatarGenerator(config.collection);

// Generate the Woka collection
wokasGenerated.push(...wokaGenerator.generateCollection());

for (const woka of wokasGenerated) {
await wokaGenerator.generateTileset(woka);
await wokaGenerator.exportLocal(woka);
console.log(`Edition ${woka.edition} woka has been generated with DNA: ${woka.dna}`);

const metadata = metadataGenerator.generate(woka);
await metadataGenerator.exportLocal(metadata);
console.log(`Edition ${woka.edition} metadata has been generated`);

await cropGenerator.generate(woka);
await cropGenerator.exportLocal(woka);
console.log(`Edition ${woka.edition} crop has been created!`);

const backgrounds = await avatarGenerator.getLocalBackgrounds();
await avatarGenerator.generate(woka, backgrounds);
await avatarGenerator.exportLocal(woka);
console.log(`Edition ${woka.edition} avatar has been generated`);
}
}

async function loadLayers(config: ConfigCollection): Promise<LoadedLayers> {
const loadedLayers: LoadedLayers = {};

const folders = (await fs.promises.readdir(layersDirPath)).filter((fileName) =>
fs.statSync(layersDirPath + fileName).isDirectory()
);

if (!folders || folders.length < 1) {
throw new Error("Any layer found on layers folder");
}

for (const layer of config.layers) {
if (!folders.includes(layer.name) && !layer.skip?.allow) {
throw new Error(`Undefined ${layer.name} layer on layers folder`);
}

loadedLayers[layer.name] = await loadLayer(config, layer.name);

if (layer.skip?.allow) {
loadedLayers[layer.name].push({
name: layer.skip.value,
weight: layer.skip.rarity,
file: undefined,
});
}
}

console.log(chalk.green("All layers assets has been loaded"));

return loadedLayers;
}

async function loadLayer(config: ConfigCollection, layer: string): Promise<WokaTexture[]> {
const layerDirPath = layersDirPath + layer + "/";
const files = await fs.promises.readdir(layerDirPath);
const loadedTextures: WokaTexture[] = [];

for (const file of files) {
const filePath = layerDirPath + file;

if (fs.statSync(filePath).isDirectory()) {
continue;
}

const parts = file.split(".");

if (parts[parts.length - 1] !== "png") {
throw new Error(`${file} layer must be a PNG`);
}

let weight = 100;
let name = "";

if (config.rarity) {
switch (config.rarity.method) {
case "delimiter": {
const nameSplited = parts[0].split("#");
name = nameSplited[0];
weight = Number(nameSplited.pop());
if (isNaN(weight)) {
weight = 100;
}
break;
}
case "random": {
name = parts[0];
weight = Math.floor(Math.random() * 100);
break;
}
case "none": {
name = parts[0];
break;
}
}
} else {
name = parts[0];
}

loadedTextures.push({
name,
weight,
file: filePath,
});
}

console.log(`${layer} layer has been loaded`);

return loadedTextures;
}

function sortTexturesByWeight(textures: WokaTexture[]) {
return textures.sort(function (a, b) {
return a.weight - b.weight;
});
}

run()
.then(() => {
for (const layer of Object.keys(loadedLayers)) {
console.log("\n" + chalk.bold(`${layer.charAt(0).toUpperCase() + layer.slice(1)} parts rarity:`));
console.table(sortTexturesByWeight(loadedLayers[layer]), ["name", "weight"]);
}

console.log("\n" + chalk.bold("Generated Wokas:"));
console.table(wokasGenerated, ["edition", "dna"]);

console.log(chalk.green("All files has been generated"));
})
.catch((err) => {
console.error(chalk.red(err));
});
10 changes: 10 additions & 0 deletions src/bin/reset.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import fs from "fs";
import chalk from "chalk";
import { removeBuildDirectory } from "../utils/DirectoryUtils";

if (fs.existsSync("build")) {
removeBuildDirectory();
console.log(chalk.green("Build folder has been removed"));
} else {
console.log(chalk.red("Build folder doesn't exist"));
}
145 changes: 95 additions & 50 deletions src/config.dist.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,87 @@
import { Config } from "./guards/ConfigGuards";

const config: Config = {
blockchain: {
type: "ethereum", // ethereum|solana
metadata: {
/* Ethereum */
// Optional: Manage the NFT name
name: {
prefix: "My awesome Woka edition ", // Optional: NFT name prefix
suffix: " wow !", // Optional: NFT name suffix
},
description: "Awesome Woka", // Description of your NFT
image: "ipfs://mylink/", // Base URI to your avatar files
/* Avalanche */
/*
name: {
prefix: "My awesome Woka edition ",
suffix: " wow !",
},
description: "Awesome Woka",
image: "ipfs://mylink/",
*/
},
},
collection: {
size: 10, // Number of NFTs to be created
crop: {
size: 512, // Optional: Resize of Woka in pixels to stick it above background (must be smaller than the background)
marging: {
// Optional: Add a margin to the woka in pixel
left: 64,
right: 64,
top: 64,
bottom: 64,
},
},
rarity: {
/**
blockchain: {
type: "ethereum", // ethereum|avalanche
metadata: {
/* Ethereum */
// Optional: Manage the NFT name
name: {
prefix: "My awesome Woka edition ", // Optional: NFT name prefix
suffix: " wow !", // Optional: NFT name suffix
},
description: "Awesome Woka", // Description of your NFT
image: "ipfs://mylink/", // Base URI to your avatar files
},
},
collection: {
size: 10, // Number of NFTs to be created
layers: [
// Layers used to generate the collection in order
{
name: "Body", // Attribute generated on metadata & folder name on assets/layers/
},
{
name: "Eyes",
},
{
name: "Hair",
skip: {
// Optional: Define a none value (required if have constraint)
allow: true,
value: "None",
rarity: 50,
},
},
{
name: "Clothes",
},
{
name: "Hat",
skip: {
allow: true,
value: "None",
rarity: 10,
},
constraints: {
/*linked: { // Optional: Use the layer texture of an other layer by name
layer: "Hair",
textures: [
{
on: "Redhead",
with: "Crown",
}
],
},
with: [ // Optional: Use the layer only if has all parts required
"Hair"
],
without: [ // Optional: Use the layer only if has all parts not required is none
"Hair"
],*/
},
},
{
name: "Accessory", // Attribute generated on metadata & folder name on assets/layers/
skip: {
allow: true,
value: "None",
rarity: 500,
},
},
],
crop: {
size: 512, // Optional: Resize of Woka in pixels to stick it above background (must be smaller than the background)
marging: {
// Optional: Add a margin to the woka in pixel
left: 64,
right: 64,
top: 64,
bottom: 64,
},
},
rarity: {
/**
* Give a rarity to a part.
* A table will be displayed after the generation to known how rare a part is.
*
Expand All @@ -45,29 +90,29 @@ const config: Config = {
* if a file doesn't have a delimiter, the rarity will be set to 100.
* none: Don't give a rarity.
*/
method: "delimiter",
/* random */
edges: {
// Only on random & optional: Delimit the rarity
min: 1, // Min rarity, cannot be under 1
max: 100, // Max rarity
},
},
background: {
/**
method: "delimiter",
/* random */
edges: {
// Only on random & optional: Delimit the rarity
min: 1, // Min rarity, cannot be under 1
max: 100, // Max rarity
},
},
background: {
/**
* Optional: Method for adding a background (transparent by default)
* none: Transparent background.
* image: Get a random background from the backgrounds folder.
* linked: Use the background with the same name in the backgrounds folder.
* color: Use a color.
*/
method: "image",
/*color: {
method: "image",
/*color: {
hex: "#EACCFF",
alpha: 1,
},*/
},
},
},
},
};

export default config;
14 changes: 14 additions & 0 deletions src/env/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export const basePath = process.cwd();

export const configPath = `${basePath}/src/config.ts`;

// Build directories
export const buildDirPath = `${basePath}/build/`;
export const wokasDirPath = `${buildDirPath}wokas/`;
export const dataDirPath = `${buildDirPath}data/`;
export const cropsDirPath = `${buildDirPath}crops/`;
export const avatarsDirPath = `${buildDirPath}avatars/`;

// Assets directories
export const backgroundDirPath = `${basePath}/assets/backgrounds/`;
export const layersDirPath = `${basePath}/assets/layers/`;
Loading

0 comments on commit 37764b9

Please sign in to comment.