code:
// Node tool created in order to simplify level designs (Created especially for level 4).
// Note: This tool is pretty hack for now, needs improvement (mostly performance-wise)

const fs = require('fs');
const util = require('util');
const readFile = util.promisify(fs.readFile);
const matUtil = require('./mat-utils.js');

/** Summary:
 * HORIZONTAL_MARGINS * 2 + CELL_SIZE * MAP_WIDTH = 1024px
 * VERTICAL_MARGINS * 2 + CELL_SIZE * MAP_HEIGHT = 576px
 */
const MAP_WIDTH = 23;
const MAP_HEIGHT = 13;
const VERTICAL_MARGINS = 2;
const HORIZONTAL_MARGINS = 6;
const CELL_SIZE = 44;

// Loads the level's matrix from the given path.
async function readLvlFile(path) {
  try {
    const text = await readFile(path, 'utf8');
    return text.replace(/[\ \|]/g, '').split('\n').filter(l => l[0] != '+').filter(f => f.length);

  } catch (err) {
    console.log('Error', err);
    process.exit(1);
  }
}

// Finds horizontal rows of Xs, and returns a list of blocks describing them.
function collectXsRows(mat, minLength) {
  const blocks = [];
  mat.forEach((row, i) => {
    const r = /X+/g;
    let match;
    while (match = r.exec(row)) {
      // console.log(match);
      if (match[0].length < minLength) continue;

      const block = { row: i, col: match.index, width: match[0].length, height: 1 };
      blocks.push(block);
    }
  });
  return blocks;
}

function posToCoord(row, col) {
  return {
    x: col ? HORIZONTAL_MARGINS + col * CELL_SIZE : 0,
    y: row ? VERTICAL_MARGINS + row * CELL_SIZE : 0
  }
}

function blockToCode(block) {
  const coordinates = posToCoord(block.row, block.col);
  let w = CELL_SIZE * block.width;
  if (block.col == 0) w += HORIZONTAL_MARGINS;
  if (block.col + block.width == MAP_WIDTH) w += HORIZONTAL_MARGINS;
  let h = CELL_SIZE * block.height;
  if (block.row == 0) h += VERTICAL_MARGINS;
  if (block.row + block.height == MAP_HEIGHT) h += VERTICAL_MARGINS;
  return `new Obstacle(${coordinates.x}, ${coordinates.y}, ${w}, ${h}),`;
}

function generateBlocks(levelMat, preferVertical = true, longFirst = false) {
  const transposeBlock = block => {
    return { row: block.col, col: block.row, width: block.height, height: block.width }
  };

  if (!longFirst && !preferVertical) {
    const rowBlocks = collectXsRows(levelMat, 2);
    rowBlocks.forEach(b => matUtil.fillBlock(levelMat, b, '-'));
    const transposedMat = matUtil.transpose(levelMat);
    const colBlocks = collectXsRows(transposedMat, 1).map(transposeBlock);
    return [...rowBlocks, ...colBlocks];

  } else if (!longFirst && preferVertical) {
    const transposedMat = matUtil.transpose(levelMat);
    const colBlocks = collectXsRows(transposedMat, 2);
    colBlocks.forEach(b => matUtil.fillBlock(transposedMat, b, '-'));
    levelMat = matUtil.transpose(transposedMat);
    const rowBlocks = collectXsRows(levelMat, 1);
    return [...rowBlocks, ...colBlocks.map(transposeBlock)];
  }

  // Long first
  const preferVerticalCoeff = preferVertical ? 1 : -1;
  const findBlocks = mat => {
    const transposedMat = matUtil.transpose(mat);
    const rowBlocks = collectXsRows(mat, 1);
    const colBlocks = collectXsRows(transposedMat, 2).map(transposeBlock);
    return [...rowBlocks, ...colBlocks];
  }

  // Sort blocks by area, break equality by height
  const sortBlocks = blocks => blocks.sort((b1, b2) => {
    const areaDiff = b2.width * b2.height - b1.width * b1.height;
    return areaDiff > 0 ? 1 : (areaDiff == 0 && (b2.height - b1.height) * preferVerticalCoeff > 0) ? 1 : -1;
  });

  const finalBlocks = [];
  let blocks = sortBlocks(findBlocks(levelMat));
  while (blocks.length > 0) {
    finalBlocks.push(blocks[0]);
    matUtil.fillBlock(levelMat, blocks[0], '-');
    blocks = sortBlocks(findBlocks(levelMat)); // TODO: Should be done only if the new top block is missing Xs
    // console.log(blocks);
  }
  return finalBlocks;
}

async function main() {
  const levelPath = process.argv[2];
  let levelMap = await readLvlFile(levelPath);

  // console.log('input:', levelMap);

  console.log('\nitems: [')
  const items = matUtil.findSymbol(levelMap, 'O').map(item => posToCoord(item.row, item.col));
  items.forEach(item => {
    console.log(`new Piece(${item.x + CELL_SIZE / 2}, ${item.y + CELL_SIZE / 2}, 40, 'res/imgs/items/paper.svg'),`);
  })
  console.log('],');

  console.log('viruses: [');
  const viruses = matUtil.findSymbol(levelMap, 'C').map(virus => posToCoord(virus.row, virus.col));
  viruses.forEach(virus => {
    console.log(`new Piece(${virus.x + CELL_SIZE / 2}, ${virus.y + CELL_SIZE / 2}, 40, 'res/imgs/virus.svg'),`);
  });
  console.log('],');

  console.log('\n// Obstacles:');
  const blocks = generateBlocks(levelMap, /*preferVertical = */false, /*longFirst = */true);
  console.log(blocks.map(blockToCode).join('\n'));

}

// console.log(blockToCode({ row: 0, col: 13, width: 1, height: 1 }));
// console.log(blockToCode({ row: 5, col: 9, width: 1, height: 1 }));

main();


code:
/**
 * Utility for manipulating characters matrices represented as an array of strings. Format example:
 * const mat = [
 *   'abcd',
 *   'efgh',
 * ];
 */

 // Transposes the given matrix.
function transpose(mat) {
  const transposed = [];
  for (let col = 0; col < mat[0].length; col++) {
    transposed[col] = mat.map(row => row[col]).join('');
  }
  return transposed;
}

// Fills the given block in the matrix with the given symbol.
function fillBlock(mat, block, symbol) {
  for (let i = block.row; i < block.row + block.height; i++) {
    mat[i] = mat[i].substring(0, block.col)
      + symbol.repeat(block.width)
      + mat[i].substring(block.col + block.width);
  }
}

// Finds all the occurrences of the given symbol in the matrix.
function findSymbol(mat, symbol) {
  const pieces = [];
  mat.forEach((row, i) => {
    const r = new RegExp(symbol, 'g');
    let match;
    while (match = r.exec(row)) {
      pieces.push({ row: i, col: match.index });
    }
  });
  return pieces;
}

module.exports = {transpose, fillBlock, findSymbol};


code:
function boxCollides(box1, box2) {
  return !(
    box1.x + box1.width <= box2.x ||
    box1.x > box2.x + box2.width ||
    box1.y + box1.height <= box2.y ||
    box1.y > box2.y + box2.height
  );
}

// Returns true if box1 completely contains in box2.
function boxContains(box1, box2) {
  return box2.x > box1.x &&
    box2.y > box1.y &&
    box2.x + box2.width <= box1.x + box1.width &&
    box2.y + box2.height <= box1.y + box1.height;
}

function arcCollides(arc1, arc2) {
  return Math.hypot(arc1.x - arc2.x, arc1.y - arc2.y) <= arc1.r + arc2.r;
}

export { boxCollides, boxContains, arcCollides };


code:
class Keyboard {
  constructor() {
    this.pressed = {};
    this.recentPressed = null;

    document.addEventListener('keydown', evt => {
      if (evt.key.substr(0, 5) == "Arrow" || evt.key == ' ') evt.preventDefault();
      if (evt.repeat) return;
      this.pressed[evt.code] = true;
      this.recentPressed = evt.code;
    });

    document.addEventListener('keyup', evt => {
      delete this.pressed[evt.code];
      if (evt.code == this.recentPressed) {
        this.recentPressed = null;
      }
    });

    window.addEventListener('blur', evt => {
      this.pressed = {};
      this.recentPressed = null;
    });
  }

  isDown(key) {
    return key in this.pressed;
  }

  getRecentDown() {
    return this.recentPressed;
  }
}

export default new Keyboard();


code:
class Resources {
  constructor() {
    this.cache = {};
  }

  async loadImage(path) {
    if (path in this.cache) {
      return Promise.resolve(this.cache[path]);
    }
    return new Promise((res, rej) => {
      const img = new Image();
      img.onload = () => {
        this.cache[path] = img;
        res(img);
      };
      img.src = path;
    });
  }

  async loadImages(paths) {
    return Promise.all(paths.map(path => this.loadImage(path)));
  }

  get(path) {
    return this.cache[path];
  }

}
export default new Resources();


code:
class Sounds {
  constructor() {
    this.muted = false;

    this.bgMain = this.createAudio('res/sounds/background.mp3', 0.4);
    this.bgMain.loop = true;
    this.lastCough = -1;
    this.bgCoughs = [
      this.createAudio('res/sounds/cough-boy9.mp3', 0.5),
      this.createAudio('res/sounds/cough-female602.mp3', 0.3),
      this.createAudio('res/sounds/clearing-throat-male2.mp3', 0.4),
      this.createAudio('res/sounds/clearing-throat-female.mp3', 0.4),
    ];

    this.audios = {};
  }

  playBackground() {
    this.bgMain.play();
    this.bgInterval = setInterval(() => {
      let coughIndex = (this.lastCough + getRandom(1, this.bgCoughs.length - 1)) % this.bgCoughs.length;
      this.lastCough = coughIndex;
      this.bgCoughs[this.lastCough].play();
    }, 7000);
  }

  stopBackground() {
    clearInterval(this.bgInterval);
    this.bgMain.pause();
    this.bgMain.currentTime = 0;
    this.bgCoughs.forEach(cough => {
      cough.pause();
      cough.currentTime = 0;
    });
  }

  play(src, volume) {
    if (src in this.audios) {
      this.audios[src].currentTime = 0;
    } else {
      this.audios[src] = this.createAudio(src, volume);
    }
    return new Promise((res, rej) => {
      this.audios[src].onended = () => res();
      this.audios[src].play();
    });
  }

  createAudio(src, volume = 1) {
    let audio = new Audio(src);
    audio.volume = volume;
    audio.muted = this.muted;
    return audio;
  }

  getAllAudios() {
    return [...this.bgCoughs, this.bgMain, ...Object.values(this.audios)];
  }

  mute() {
    this.muted = true;
    this.getAllAudios().forEach(audio => audio.muted = true);
  }

  unmute() {
    this.muted = false;
    this.getAllAudios().forEach(audio => audio.muted = false);
  }
}

export default new Sounds();

function getRandom(min, max) {
  return min + Math.floor(Math.random() * (max - min + 1));
}


code:
<!DOCTYPE html>

    <meta charset="utf-8">
    <meta name="games" content="Games">
    <title>Games | justasussydude</title>
    <link rel="stylesheet" href="style.css">

      Cookie Clicker
      Cookie Clicker Classic
      2048
      Corona Market
      Repository

justasussydude || Games

      This page is for cool browser games I added here. Not all of them are mine, and not all of them are relinquished to the public domain. As such, the games here that are not Unlicensed will have their licenses available in a file called "COPYING" in their directory.


code:
body {
  font-family: sans-serif;
  margin: auto;
  max-width: 1280px;
}

.navbar {
  background-color: #313236;
  border-radius: 2px;
  max-width: 4096px;
}

.navbar a {
  color: #aaa;
  display: inline-block;
  font-size: 15px;
  padding: 10px;
  text-decoration: none;
}

.navbar a:hover {
  color: #ffffff;
}


code:
<!DOCTYPE html>

    <meta charset="utf-8">
    <meta name="homepage" content="Homepage">
    <title>Home | justasussydude</title>
    <link rel="stylesheet" href="style.css">

      Social links
      Browser games (working as of 06.12.2021)
      Repository
      My Odysee page
      My Odysee RSS feed
      My blog

justasussydude || Home

      Hello world!


code:
**/Jquery.js
**/mousetrap.js
**/sugarCubeConfig.js
**/format.js
**/displayVariables.js
**/dTree.min.js
**/optionsMacro.js
temp.**
**.temp
**/001-lib

path: .eslintignore

license: mit


code:
{
	"root": true,
	"env": {
		"browser": true,
		"es6": true,
		"jquery": true
	},
	"extends": ["eslint:recommended"],
	"plugins": [
		"jsdoc",
		"sonarjs"
	],
	"globals": {
		"SugarCube": false,
		"Config": false,
		"State": false,
		"Story": false,
		"settings": false,
		"setup": false,
		"App": false,
		"prehistory": true,
		"predisplay": true,
		"prerender": true,
		"postrender": true,
		"postdisplay": true
	},
	"parserOptions": {
		"ecmaVersion": 2021,
		"sourceType": "script",
		"ecmaFeatures": {
			"impliedStrict": true
		}
	},
	"settings": {
		"jsdoc": {
			"mode": "typescript"
		}
	},
	"rules": {
		"no-prototype-builtins": "off",
		"semi": ["warn", "always"],
		"semi-spacing": "warn",
		"semi-style": "warn",
		"block-spacing": ["warn", "always"],
		"curly": ["warn", "all"],
		"eqeqeq": ["warn", "smart"],
		"space-before-function-paren": ["warn", {
			"anonymous": "never",
			"named": "never",
			"asyncArrow": "always"
		}],
		"template-curly-spacing": ["warn", "never"],
		"no-trailing-spaces": "warn",
		"no-mixed-spaces-and-tabs": ["warn", "smart-tabs"],
		"no-unneeded-ternary": "warn",
		"camelcase": "warn",
		"padded-blocks": ["warn", "never"],
		"comma-spacing": "warn",
		"comma-style": "warn",
		"object-curly-newline": ["warn",
			{
				"ObjectExpression":{
					"minProperties": 4,
					"consistent": true
				},
				"ImportDeclaration": {
					"minProperties": 4,
					"consistent": true
				},
				"ExportDeclaration": {
					"minProperties": 4,
					"consistent": true
				},
				"ObjectPattern": {
					"minProperties": 8,
					"consistent": true
				}
			}
		],
		"object-curly-spacing": "warn",
		"no-var": "warn",
		"spaced-comment": "warn",
		"no-undef": "off",
		"indent": ["error", "tab", { "SwitchCase": 1 }],
		"no-unused-vars": ["error", {"args": "none"}],
		"no-unused-expressions": "error",
		"block-scoped-var": "error",
		"dot-notation": "warn",
		"eol-last": "warn",
		"unicode-bom": [
			"error",
			"never"
		],
		"brace-style": [
			"warn",
			"1tbs",
			{
				"allowSingleLine": true
			}
		],
		"no-sequences": "warn",
		"no-unreachable-loop": "warn",
		"generator-star-spacing": [
			"warn",
			{
				"before": false,
				"after": true,
				"anonymous": "neither"
			}
		],
		"one-var": ["warn", "never"],
		"keyword-spacing": "warn",
		"no-template-curly-in-string": "error",
		"operator-linebreak": ["warn", "after", { "overrides": { "?": "before", ":": "before" }}],
		"jsdoc/check-alignment": 1,
		"jsdoc/check-examples": 0, // interferes with SugarCube examples
		"jsdoc/check-indentation": 0,
		"jsdoc/check-param-names": 1,
		"jsdoc/check-property-names": 1,
		"jsdoc/check-syntax": 1,
		"jsdoc/check-tag-names": 1,
		"jsdoc/check-types": 1,
		"jsdoc/implements-on-classes": 1,
		"jsdoc/match-description": 0, // rejects SugarCube syntax
		"jsdoc/newline-after-description": 0,
		"jsdoc/no-types": 0,
		"jsdoc/no-undefined-types": 0,
		"jsdoc/require-jsdoc": 0,
		"jsdoc/require-param": 0,
		"jsdoc/require-param-description": 0,
		"jsdoc/require-param-name": 1,
		"jsdoc/require-param-type": 1,
		"jsdoc/require-returns": 0,
		"jsdoc/require-returns-check": 1,
		"jsdoc/require-returns-description": 0,
		"jsdoc/require-returns-type": 1,
		"jsdoc/valid-types": 1,
		"sonarjs/no-all-duplicated-branches": "error",
		"sonarjs/no-element-overwrite": "error",
		"sonarjs/no-extra-arguments": "error",
		"sonarjs/no-identical-conditions": "error",
		"sonarjs/no-identical-expressions": "error",
		"sonarjs/no-one-iteration-loop": "error",
		"sonarjs/no-use-of-empty-return-value": "error",
		"sonarjs/no-collection-size-mischeck": "warn",
		"sonarjs/no-same-line-conditional": "warn",
		"sonarjs/no-unused-collection": "warn"
	}
}


code:
*.bat	text eol=crlf
*.js	text eol=lf
*.tw	text eol=lf
*.sh	text eol=lf
*.py	text eol=lf
*.txt	text eol=lf

path: .gitattributes


code:
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# Vim stuff
[._]*.s[a-v][a-z]
[._]*.sw[a-p]
[._]s[a-rt-v][a-z]
[._]ss[a-gi-z]
[._]sw[a-p]
Session.vim
Sessionx.vim
.netrwhist
*~
tags
[._]*.un~

# celery beat schedule file
celerybeat-schedule

# dotenv
.env

# virtualenv
venv/
ENV/

# Spyder project settings
.spyderproject

# Rope project settings
.ropeproject

# Start.tw
src/002-config/start.tw

# eslint
node_modules
package-lock.json
yarn.lock

# Visual Studio Code
.vscode/settings.json
.vscode/tasks.json
*.code-workspace

# WebStorm
.idea

# Gulp
build/

# backup
*.bak
*.gho
*.ori
*.orig
*.tmp

# misc
fc-pregmod
bin/*
*.outline
*.todo
TODO.txt
temp.*
*.temp
src/002-config/fc-version.js.commitHash.js

# macOS devices
*.icloud

# legacy
devNotes/legacy files/*

path: .gitignore


code:
workflow:
  rules:
    # only run when on the main repo
    - if: '$CI_PROJECT_ID == "3528"'
      when: always

.official-releases:
  rules:
    - if: $CI_COMMIT_TAG =~ /\A\d+\.\d+\.\d+/

stages:
  - build
  - upload
  - release

variables:
  FC_PREGMOD_ARCHIVE: "FC_pregmod_${CI_COMMIT_TAG}.zip"
  # precompiled / assets
  # the URLs are the same as inside the game, change both if updating
  WEBGL_NAME: "WebGL art assets"
  WEBGL_URL: "https://mega.nz/folder/bwozybJa#h8fEEdKtXLAz-ZkcK1XbRA"
  RENDER_NAME: "Rendered imagepack (outdated)"
  RENDER_URL: "https://mega.nz/file/upoAlBaZ#EbZ5wCixxZxBhMN_ireJTXt0SIPOywO2JW9XzTIPhe0"

build:
  stage: build
  image: debian:latest
  rules:
    # official release
    - if: $CI_COMMIT_TAG =~ /\A\d+\.\d+\.\d+/
    # latest
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
  script:
    - ./compile.sh --ci --minify
  artifacts:
    paths:
      - bin/FC_pregmod.html

upload:
  stage: upload
  image: gitgud.io:5050/pregmodfan/fc-pregmod:megacmd
  extends: .official-releases
  script:
    - mega-login $MEGA_MAIL $MEGA_PWD
    - zip bin/$FC_PREGMOD_ARCHIVE bin/FC_pregmod.html
    - mega-put bin/$FC_PREGMOD_ARCHIVE /
    - echo "MEGA_URL=$(mega-export -a /$FC_PREGMOD_ARCHIVE | grep -o https.*)" >> variables.env
  artifacts:
    # carry MEGA_URL over to the release job
    reports:
      dotenv: variables.env

release:
  stage: release
  image: registry.gitlab.com/gitlab-org/release-cli:latest
  extends: .official-releases
  script:
    # extract changes from changelog
    # get line of version header + 1
    - START=$(awk 'match($1,"##") && match($2,v){print NR+1; exit}' v="0.10.7.1-$CI_COMMIT_TAG" CHANGELOG.md)
    # drop everything above the release including the release header
    - tail -n +$START CHANGELOG.md > tmp
    # get line of next version header - 1
    - END=$(awk 'match($1,"##") && match($2,v){print NR-1; exit}' v="0.10.7.1" tmp)
    # drop everything after current release
    - DESCRIPTION=$(head -n $END tmp)
    # create release
    # assets are displayed in inverse order to how they are defined here
    - |
      release-cli create --name "$CI_COMMIT_TAG" --description "$DESCRIPTION" \
        --tag-name "$CI_COMMIT_TAG" \
        --assets-link "{\"name\":\"${RENDER_NAME}\",\"url\":\"${RENDER_URL}\"}" \
        --assets-link "{\"name\":\"${WEBGL_NAME}\",\"url\":\"${WEBGL_URL}\"}" \
        --assets-link "{\"name\":\"${FC_PREGMOD_ARCHIVE}\",\"url\":\"${MEGA_URL}\"}"


code:
/label ~Bug

## Expected Behavior
<!--- If you're describing a bug, tell us what should happen -->

## Current Behavior
<!-- If describing a bug, tell us what happens instead of the expected behavior. Provide a Screenshots if possible. -->

## Steps to Reproduce
<!-- Provide an unambiguous sequence of steps to reproduce the bug. -->
1.
2.
3.
4.

## Additional information
<!-- Release ID and commit can be found on the starting splash screen or the options page. -->
* Release ID:
* Commit: (if available)
* Save file


code:
/label ~Feature

### Is your proposal related to a problem?
<!--
  Provide a clear and concise description of what the problem is.
  For example, "I'm always frustrated when..."
-->

### Describe the solution you'd like
<!--
  Provide a clear and concise description of what you want to happen.
-->

### Describe alternatives you've considered
<!--
  Let us know about other solutions you've tried or researched.
-->

### Additional context
<!--
  Is there anything else you can add about the proposal?
  You might want to link to related issues here, if you haven't already.
-->


code:
/label ~Question

<!-- Make sure to read the ingame encyclopedia before asking your question -->


code:
[submodule "submodules/sugarcube-2"]
	path = submodules/sugarcube-2
	url = https://gitgud.io/Arkerthan/sugarcube-2.git
	branch = fc

path: .gitmodules


brace_style: collapse,preserve-inline

path: .jsbeautifyrc


code:
// Minified files
**/dTree.min.js
**/js/displayVariables.js

path: .jshintignore