mirror of
				https://github.com/tobspr/shapez.io.git
				synced 2025-06-13 13:04:03 +00:00 
			
		
		
		
	Integrate with addition of new setting in master.
This commit is contained in:
		
						commit
						6f7d0a4444
					
				
							
								
								
									
										12
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								README.md
									
									
									
									
									
								
							@ -1,15 +1,15 @@
 | 
			
		||||
# shapez.io
 | 
			
		||||
 | 
			
		||||
<img src="https://i.imgur.com/Y5Z2iqQ.png" alt="shapez.io Logo">
 | 
			
		||||
<img src="https://i.imgur.com/W25Fkl0.png" alt="shapez.io Screenshot">
 | 
			
		||||
 | 
			
		||||
This is the source code for shapez.io, an open source base building game inspired by Factorio.
 | 
			
		||||
 | 
			
		||||
Your goal is to produce shapes by cutting, rotating, merging and painting parts of shapes.
 | 
			
		||||
 | 
			
		||||
## Playing
 | 
			
		||||
 | 
			
		||||
You can already play it [here](https://shapez.io).
 | 
			
		||||
-   [Trello Board & Roadmap](https://trello.com/b/ISQncpJP/shapezio)
 | 
			
		||||
-   [Free web version](https://shapez.io)
 | 
			
		||||
-   [itch.io Page](https://tobspr.itch.io/shapezio)
 | 
			
		||||
-   [Steam Page](https://steam.shapez.io)
 | 
			
		||||
-   [Official Discord](https://discord.com/invite/HN7EVzV) <- _Highly recommended to join!_
 | 
			
		||||
 | 
			
		||||
## Building
 | 
			
		||||
 | 
			
		||||
@ -46,3 +46,5 @@ This project is based on ES5. Some ES2015 features are used but most of them are
 | 
			
		||||
For most assets I use Adobe Photoshop, you can find them in `assets/`.
 | 
			
		||||
 | 
			
		||||
You will need a <a href="https://www.codeandweb.com/texturepacker" target="_blank">Texture Packer</a> license in order to regenerate the atlas. If you don't have one but want to contribute assets, let me know and I might compile it for you. I'm currently switching to an open source solution but I can't give an estimate when thats done.
 | 
			
		||||
 | 
			
		||||
<img src="https://i.imgur.com/W25Fkl0.png" alt="shapez.io Screenshot">
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										9
									
								
								gulp/.itch.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								gulp/.itch.toml
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,9 @@
 | 
			
		||||
[[actions]]
 | 
			
		||||
name = "play"
 | 
			
		||||
path = "shapezio.exe"
 | 
			
		||||
platform = "windows"
 | 
			
		||||
 | 
			
		||||
[[actions]]
 | 
			
		||||
name = "play"
 | 
			
		||||
path = "play.sh"
 | 
			
		||||
platform = "linux"
 | 
			
		||||
@ -121,6 +121,8 @@ function gulptasksStandalone($, gulp, buildFolder) {
 | 
			
		||||
     * @param {boolean=} isRelease
 | 
			
		||||
     */
 | 
			
		||||
    function packageStandalone(platform, arch, cb, isRelease = false) {
 | 
			
		||||
        const tomlFile = fs.readFileSync(path.join(__dirname, ".itch.toml"));
 | 
			
		||||
 | 
			
		||||
        packager({
 | 
			
		||||
            dir: tempDestBuildDir,
 | 
			
		||||
            appCopyright: "Tobias Springer",
 | 
			
		||||
@ -150,17 +152,25 @@ function gulptasksStandalone($, gulp, buildFolder) {
 | 
			
		||||
                        fs.readFileSync(path.join(__dirname, "..", "LICENSE"))
 | 
			
		||||
                    );
 | 
			
		||||
 | 
			
		||||
                    const playablePath = appPath + "_playable";
 | 
			
		||||
                    fse.copySync(appPath, playablePath);
 | 
			
		||||
                    fs.writeFileSync(path.join(playablePath, "steam_appid.txt"), "1134480");
 | 
			
		||||
                    fs.writeFileSync(
 | 
			
		||||
                        path.join(playablePath, "play.bat"),
 | 
			
		||||
                        "start shapezio --dev --disable-direct-composition --in-process-gpu\r\n"
 | 
			
		||||
                    );
 | 
			
		||||
                    fs.writeFileSync(
 | 
			
		||||
                        path.join(playablePath, "play_local.bat"),
 | 
			
		||||
                        "start shapezio --local --dev --disable-direct-composition --in-process-gpu\r\n"
 | 
			
		||||
                    );
 | 
			
		||||
                    fs.writeFileSync(path.join(appPath, ".itch.toml"), tomlFile);
 | 
			
		||||
 | 
			
		||||
                    if (platform === "linux" || platform === "darwin") {
 | 
			
		||||
                        fs.writeFileSync(path.join(appPath, "play.sh"), "#!/usr/bin/env bash\n./shapezio\n");
 | 
			
		||||
                        fs.chmodSync(path.join(appPath, "play.sh"), 0o775);
 | 
			
		||||
                    } else if (platform === "win32") {
 | 
			
		||||
                        // Optional: Create a playable copy. Shouldn't be required
 | 
			
		||||
                        // const playablePath = appPath + "_playable";
 | 
			
		||||
                        // fse.copySync(appPath, playablePath);
 | 
			
		||||
                        // fs.writeFileSync(path.join(playablePath, "steam_appid.txt"), "1134480");
 | 
			
		||||
                        // fs.writeFileSync(
 | 
			
		||||
                        //     path.join(playablePath, "play.bat"),
 | 
			
		||||
                        //     "start shapezio --dev --disable-direct-composition --in-process-gpu\r\n"
 | 
			
		||||
                        // );
 | 
			
		||||
                        // fs.writeFileSync(
 | 
			
		||||
                        //     path.join(playablePath, "play_local.bat"),
 | 
			
		||||
                        //     "start shapezio --local --dev --disable-direct-composition --in-process-gpu\r\n"
 | 
			
		||||
                        // );
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                cb();
 | 
			
		||||
@ -182,10 +192,10 @@ function gulptasksStandalone($, gulp, buildFolder) {
 | 
			
		||||
        "standalone.package.prod",
 | 
			
		||||
        $.sequence("standalone.prepare", [
 | 
			
		||||
            "standalone.package.prod.win64",
 | 
			
		||||
            // "standalone.package.prod.linux64",
 | 
			
		||||
            "standalone.package.prod.linux64",
 | 
			
		||||
            "standalone.package.prod.darwin64",
 | 
			
		||||
            // "standalone.package.prod.win32",
 | 
			
		||||
            // "standalone.package.prod.linux32",
 | 
			
		||||
            // "standalone.package.prod.darwin64"
 | 
			
		||||
        ])
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -11,7 +11,8 @@
 | 
			
		||||
        "tslint": "cd src/js && tsc",
 | 
			
		||||
        "lint": "npx eslint src/js",
 | 
			
		||||
        "prettier-all": "prettier --write src/**/*.* && prettier --write gulp/**/*.*",
 | 
			
		||||
        "publishOnItch": "butler push tmp_standalone_files/shapez.io-standalone-win32-x64 tobspr/shapezio:windows --userversion-file version",
 | 
			
		||||
        "publishOnItchWindows": "butler push tmp_standalone_files/shapez.io-standalone-win32-x64 tobspr/shapezio:windows --userversion-file version",
 | 
			
		||||
        "publishOnItchLinux": "butler push tmp_standalone_files/shapez.io-standalone-linux-x64 tobspr/shapezio:linux-experimental --userversion-file version",
 | 
			
		||||
        "publishOnSteam": "cd gulp/steampipe && ./upload.bat",
 | 
			
		||||
        "publishStandalone": "yarn publishOnItch && yarn publishOnSteam",
 | 
			
		||||
        "publishWeb": "cd gulp && yarn main.deploy.prod",
 | 
			
		||||
 | 
			
		||||
@ -11,7 +11,11 @@
 | 
			
		||||
    border-bottom-width: 0;
 | 
			
		||||
    transition: transform 0.12s ease-in-out;
 | 
			
		||||
 | 
			
		||||
    background: rgba(mix(#ddd, $colorBlueBright, 80%), 0.69);
 | 
			
		||||
    background: rgba(mix(#ddd, $colorBlueBright, 90%), 0.75);
 | 
			
		||||
 | 
			
		||||
    @include DarkThemeOverride {
 | 
			
		||||
        background: #222428;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &:not(.visible) {
 | 
			
		||||
        transform: translateX(-50%) translateY(#{D(100px)});
 | 
			
		||||
 | 
			
		||||
@ -6,8 +6,12 @@
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    align-items: flex-start;
 | 
			
		||||
    color: #fff;
 | 
			
		||||
    text-shadow: #{D(1px)} #{D(1px)} 0 rgba(0, 10, 20, 0.1);
 | 
			
		||||
    color: #333438;
 | 
			
		||||
    // text-shadow: #{D(1px)} #{D(1px)} 0 rgba(0, 10, 20, 0.1);
 | 
			
		||||
 | 
			
		||||
    @include DarkThemeOverride {
 | 
			
		||||
        color: #fff;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    > .binding {
 | 
			
		||||
        display: inline-grid;
 | 
			
		||||
@ -42,10 +46,13 @@
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        label {
 | 
			
		||||
            color: $accentColorDark;
 | 
			
		||||
            color: #333438;
 | 
			
		||||
            @include SuperSmallText;
 | 
			
		||||
            text-transform: uppercase;
 | 
			
		||||
            color: #fff;
 | 
			
		||||
            // color: #fff;
 | 
			
		||||
            @include DarkThemeOverride {
 | 
			
		||||
                color: #fff;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            @include S(margin-left, 5px);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -16,8 +16,8 @@
 | 
			
		||||
        grid-template-columns: auto 1fr;
 | 
			
		||||
        grid-template-rows: 1fr 1fr;
 | 
			
		||||
        @include S(margin-bottom, 4px);
 | 
			
		||||
        color: #fff;
 | 
			
		||||
        text-shadow: #{D(1px)} #{D(1px)} 0 rgba(0, 10, 20, 0.2);
 | 
			
		||||
        color: #333438;
 | 
			
		||||
        // text-shadow: #{D(1px)} #{D(1px)} 0 rgba(0, 10, 20, 0.2);
 | 
			
		||||
 | 
			
		||||
        &.unpinable {
 | 
			
		||||
            > canvas {
 | 
			
		||||
@ -59,7 +59,7 @@
 | 
			
		||||
 | 
			
		||||
        > .goalLabel {
 | 
			
		||||
            @include S(font-size, 7px);
 | 
			
		||||
            opacity: 0.5;
 | 
			
		||||
            opacity: 0.9;
 | 
			
		||||
            align-self: start;
 | 
			
		||||
            justify-self: start;
 | 
			
		||||
            font-weight: normal;
 | 
			
		||||
@ -81,7 +81,6 @@
 | 
			
		||||
                    display: inline-block;
 | 
			
		||||
                    @include S(width, 8px);
 | 
			
		||||
                    @include S(height, 8px);
 | 
			
		||||
                    opacity: 0.8;
 | 
			
		||||
                    @include S(top, 4px);
 | 
			
		||||
                    @include S(left, -7px);
 | 
			
		||||
                    background: uiResource("icons/current_goal_marker.png") center center / contain no-repeat;
 | 
			
		||||
 | 
			
		||||
@ -38,13 +38,13 @@
 | 
			
		||||
        @include SuperSmallText;
 | 
			
		||||
        pointer-events: all;
 | 
			
		||||
        cursor: pointer;
 | 
			
		||||
        color: #000;
 | 
			
		||||
        color: #333438;
 | 
			
		||||
        @include S(padding-left, 11px);
 | 
			
		||||
        display: grid;
 | 
			
		||||
        grid-template-columns: 1fr auto;
 | 
			
		||||
        align-items: center;
 | 
			
		||||
        background: uiResource("icons/waypoint.png") left 50% / #{D(8px)} no-repeat;
 | 
			
		||||
        opacity: 0.5;
 | 
			
		||||
        opacity: 0.7;
 | 
			
		||||
        @include S(margin-bottom, 1px);
 | 
			
		||||
        font-weight: bold;
 | 
			
		||||
        &:hover {
 | 
			
		||||
 | 
			
		||||
@ -36,6 +36,9 @@
 | 
			
		||||
                @include S(padding, 1px, 2px);
 | 
			
		||||
                @include S(margin-right, 3px);
 | 
			
		||||
            }
 | 
			
		||||
            a {
 | 
			
		||||
                color: $colorBlueBright;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -40,6 +40,9 @@
 | 
			
		||||
        <meta http-equiv="Expires" content="0" />
 | 
			
		||||
        <link rel="shortcut icon" type="image/x-icon" href="favicon.ico" />
 | 
			
		||||
        <link rel="canonical" href="https://shapez.io" />
 | 
			
		||||
 | 
			
		||||
        <!-- a/b testing -->
 | 
			
		||||
        <script src="https://www.googleoptimize.com/optimize.js?id=OPT-M5NHCV7"></script>
 | 
			
		||||
    </head>
 | 
			
		||||
 | 
			
		||||
    <body oncontextmenu="return false" style="background: #393747;"></body>
 | 
			
		||||
 | 
			
		||||
@ -1,11 +1,25 @@
 | 
			
		||||
export const CHANGELOG = [
 | 
			
		||||
    {
 | 
			
		||||
        version: "1.1.2",
 | 
			
		||||
        date: "unreleased",
 | 
			
		||||
        version: "1.1.3",
 | 
			
		||||
        date: "01.06.2020",
 | 
			
		||||
        entries: [
 | 
			
		||||
            "Added setting to configure zoom / mouse wheel / touchpad sensitivity",
 | 
			
		||||
            "Fix belts being too slow when copied via blueprint (by Dimava)",
 | 
			
		||||
            "Allow binding mouse buttons to actions (by Dimava)",
 | 
			
		||||
            "Increase readability of certain HUD elements",
 | 
			
		||||
        ],
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        version: "1.1.2",
 | 
			
		||||
        date: "30.05.2020",
 | 
			
		||||
        entries: [
 | 
			
		||||
            "The official trailer is now ready! Check it out <a href='https://www.youtube.com/watch?v=KyorY1uIqiQ' target='_blank'>here</a>!",
 | 
			
		||||
            "The <a href='https://steam.shapez.io' target='_blank'>steam page</a> is now live!",
 | 
			
		||||
            "Experimental linux builds are now available! Please give me feedback on them in the discord",
 | 
			
		||||
            "Allow hovering pinned shapes to enlarge them",
 | 
			
		||||
            "Allow deselecting blueprints with right click and 'Q'",
 | 
			
		||||
            "Move default key for deleting from 'X' to 'DEL'",
 | 
			
		||||
            "Show confirmation when deleting > 100 buildings",
 | 
			
		||||
            "Show confirmation when deleting more than 100 buildings",
 | 
			
		||||
            "Reintroduce 'SPACE' keybinding to center on map",
 | 
			
		||||
            "Improved keybinding hints",
 | 
			
		||||
            "Fixed some keybindings showing as 'undefined'",
 | 
			
		||||
 | 
			
		||||
@ -311,7 +311,7 @@ export class ClickDetector {
 | 
			
		||||
        const position = /** @type {typeof ClickDetector} */ (this.constructor).extractPointerPosition(event);
 | 
			
		||||
 | 
			
		||||
        if (event instanceof MouseEvent) {
 | 
			
		||||
            const isRightClick = event.which == 3;
 | 
			
		||||
            const isRightClick = event.button === 2;
 | 
			
		||||
            if (isRightClick) {
 | 
			
		||||
                // Ignore right clicks
 | 
			
		||||
                this.rightClick.dispatch(position, event);
 | 
			
		||||
@ -384,7 +384,7 @@ export class ClickDetector {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (event instanceof MouseEvent) {
 | 
			
		||||
            const isRightClick = event.which == 3;
 | 
			
		||||
            const isRightClick = event.button === 2;
 | 
			
		||||
            if (isRightClick) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
@ -83,7 +83,7 @@ export const globalConfig = {
 | 
			
		||||
 | 
			
		||||
    debug: {
 | 
			
		||||
        /* dev:start */
 | 
			
		||||
        fastGameEnter: true,
 | 
			
		||||
        // fastGameEnter: true,
 | 
			
		||||
        // noArtificialDelays: true,
 | 
			
		||||
        // disableSavegameWrite: true,
 | 
			
		||||
        // showEntityBounds: true,
 | 
			
		||||
 | 
			
		||||
@ -141,8 +141,13 @@ export class InputDistributor {
 | 
			
		||||
    bindToEvents() {
 | 
			
		||||
        window.addEventListener("popstate", this.handleBackButton.bind(this), false);
 | 
			
		||||
        document.addEventListener("backbutton", this.handleBackButton.bind(this), false);
 | 
			
		||||
        window.addEventListener("keydown", this.handleKeydown.bind(this));
 | 
			
		||||
        window.addEventListener("keyup", this.handleKeyup.bind(this));
 | 
			
		||||
 | 
			
		||||
        window.addEventListener("keydown", this.handleKeyMouseDown.bind(this));
 | 
			
		||||
        window.addEventListener("keyup", this.handleKeyMouseUp.bind(this));
 | 
			
		||||
 | 
			
		||||
        window.addEventListener("mousedown", this.handleKeyMouseDown.bind(this));
 | 
			
		||||
        window.addEventListener("mouseup", this.handleKeyMouseUp.bind(this));
 | 
			
		||||
 | 
			
		||||
        window.addEventListener("blur", this.handleBlur.bind(this));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -182,25 +187,28 @@ export class InputDistributor {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @param {KeyboardEvent} event
 | 
			
		||||
     * @param {KeyboardEvent | MouseEvent} event
 | 
			
		||||
     */
 | 
			
		||||
    handleKeydown(event) {
 | 
			
		||||
    handleKeyMouseDown(event) {
 | 
			
		||||
        const keyCode = event instanceof MouseEvent ? event.button + 1 : event.keyCode;
 | 
			
		||||
        if (
 | 
			
		||||
            event.keyCode === 9 || // TAB
 | 
			
		||||
            event.keyCode === 16 || // SHIFT
 | 
			
		||||
            event.keyCode === 17 || // CTRL
 | 
			
		||||
            event.keyCode === 18 || // ALT
 | 
			
		||||
            (event.keyCode >= 112 && event.keyCode < 122) // F1 - F10
 | 
			
		||||
            keyCode === 4 || // MB4
 | 
			
		||||
            keyCode === 5 || // MB5
 | 
			
		||||
            keyCode === 9 || // TAB
 | 
			
		||||
            keyCode === 16 || // SHIFT
 | 
			
		||||
            keyCode === 17 || // CTRL
 | 
			
		||||
            keyCode === 18 || // ALT
 | 
			
		||||
            (keyCode >= 112 && keyCode < 122) // F1 - F10
 | 
			
		||||
        ) {
 | 
			
		||||
            event.preventDefault();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const isInitial = !this.keysDown.has(event.keyCode);
 | 
			
		||||
        this.keysDown.add(event.keyCode);
 | 
			
		||||
        const isInitial = !this.keysDown.has(keyCode);
 | 
			
		||||
        this.keysDown.add(keyCode);
 | 
			
		||||
 | 
			
		||||
        if (
 | 
			
		||||
            this.forwardToReceiver("keydown", {
 | 
			
		||||
                keyCode: event.keyCode,
 | 
			
		||||
                keyCode: keyCode,
 | 
			
		||||
                shift: event.shiftKey,
 | 
			
		||||
                alt: event.altKey,
 | 
			
		||||
                initial: isInitial,
 | 
			
		||||
@ -210,8 +218,7 @@ export class InputDistributor {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const code = event.keyCode;
 | 
			
		||||
        if (code === 27) {
 | 
			
		||||
        if (keyCode === 27) {
 | 
			
		||||
            // Escape key
 | 
			
		||||
            event.preventDefault();
 | 
			
		||||
            event.stopPropagation();
 | 
			
		||||
@ -220,13 +227,14 @@ export class InputDistributor {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @param {KeyboardEvent} event
 | 
			
		||||
     * @param {KeyboardEvent | MouseEvent} event
 | 
			
		||||
     */
 | 
			
		||||
    handleKeyup(event) {
 | 
			
		||||
        this.keysDown.delete(event.keyCode);
 | 
			
		||||
    handleKeyMouseUp(event) {
 | 
			
		||||
        const keyCode = event instanceof MouseEvent ? event.button + 1 : event.keyCode;
 | 
			
		||||
        this.keysDown.delete(keyCode);
 | 
			
		||||
 | 
			
		||||
        this.forwardToReceiver("keyup", {
 | 
			
		||||
            keyCode: event.keyCode,
 | 
			
		||||
            keyCode: keyCode,
 | 
			
		||||
            shift: event.shiftKey,
 | 
			
		||||
            alt: event.altKey,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
@ -24,9 +24,7 @@ export const BOTTOM = new Vector(0, 1);
 | 
			
		||||
export const LEFT = new Vector(-1, 0);
 | 
			
		||||
export const ALL_DIRECTIONS = [TOP, RIGHT, BOTTOM, LEFT];
 | 
			
		||||
 | 
			
		||||
export const thousand = 1000;
 | 
			
		||||
export const million = 1000 * 1000;
 | 
			
		||||
export const billion = 1000 * 1000 * 1000;
 | 
			
		||||
const bigNumberSuffixTranslationKeys = ["thousands", "millions", "billions", "trillions"];
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Returns the build id
 | 
			
		||||
@ -435,21 +433,20 @@ export function formatBigNumber(num, divider = ".") {
 | 
			
		||||
 | 
			
		||||
    if (num < 1000) {
 | 
			
		||||
        return sign + "" + num;
 | 
			
		||||
    } else {
 | 
			
		||||
        let leadingDigits = num;
 | 
			
		||||
        let suffix = "";
 | 
			
		||||
        for (let suffixIndex = 0; suffixIndex < bigNumberSuffixTranslationKeys.length; ++suffixIndex) {
 | 
			
		||||
            leadingDigits = leadingDigits / 1000;
 | 
			
		||||
            suffix = T.global.suffix[bigNumberSuffixTranslationKeys[suffixIndex]];
 | 
			
		||||
            if (leadingDigits < 1000) {
 | 
			
		||||
                break;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        const leadingDigitsRounded = round1Digit(leadingDigits);
 | 
			
		||||
        const leadingDigitsNoTrailingDecimal = leadingDigitsRounded.toString().replace(".0", "");
 | 
			
		||||
        return sign + leadingDigitsNoTrailingDecimal + suffix;
 | 
			
		||||
    }
 | 
			
		||||
    if (num > 10000) {
 | 
			
		||||
        return Math_floor(num / 1000.0) + "k";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let rest = num;
 | 
			
		||||
    let out = "";
 | 
			
		||||
 | 
			
		||||
    while (rest >= 1000) {
 | 
			
		||||
        out = (rest % 1000).toString().padStart(3, "0") + (out !== "" ? divider : "") + out;
 | 
			
		||||
        rest = Math_floor(rest / 1000);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    out = rest + divider + out;
 | 
			
		||||
    return sign + out;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 | 
			
		||||
@ -440,11 +440,11 @@ export class Camera extends BasicSerializableObject {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.touchPostMoveVelocity = new Vector(0, 0);
 | 
			
		||||
        if (event.which === 1) {
 | 
			
		||||
        if (event.button === 0) {
 | 
			
		||||
            this.combinedSingleTouchStartHandler(event.clientX, event.clientY);
 | 
			
		||||
        } else if (event.which === 2) {
 | 
			
		||||
        } else if (event.button === 1) {
 | 
			
		||||
            this.downPreHandler.dispatch(new Vector(event.clientX, event.clientY), enumMouseButton.middle);
 | 
			
		||||
        } else if (event.which === 3) {
 | 
			
		||||
        } else if (event.button === 2) {
 | 
			
		||||
            this.downPreHandler.dispatch(new Vector(event.clientX, event.clientY), enumMouseButton.right);
 | 
			
		||||
        }
 | 
			
		||||
        return false;
 | 
			
		||||
@ -464,7 +464,7 @@ export class Camera extends BasicSerializableObject {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (event.which === 1) {
 | 
			
		||||
        if (event.button === 0) {
 | 
			
		||||
            this.combinedSingleTouchMoveHandler(event.clientX, event.clientY);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -503,7 +503,7 @@ export class Camera extends BasicSerializableObject {
 | 
			
		||||
            // event.stopPropagation();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const delta = Math.sign(event.deltaY) * -0.15;
 | 
			
		||||
        const delta = Math.sign(event.deltaY) * -0.15 * this.root.app.settings.getScrollWheelSensitivity();
 | 
			
		||||
        assert(Number.isFinite(delta), "Got invalid delta in mouse wheel event: " + event.deltaY);
 | 
			
		||||
        assert(Number.isFinite(this.zoomLevel), "Got invalid zoom level *before* wheel: " + this.zoomLevel);
 | 
			
		||||
        this.zoomLevel *= 1 + delta;
 | 
			
		||||
 | 
			
		||||
@ -44,7 +44,7 @@ export class ItemEjectorComponent extends Component {
 | 
			
		||||
 | 
			
		||||
        return new ItemEjectorComponent({
 | 
			
		||||
            slots: slotsCopy,
 | 
			
		||||
            instantEject: false,
 | 
			
		||||
            instantEject: this.instantEject,
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -48,8 +48,8 @@ export class GameHUD {
 | 
			
		||||
        this.parts = {
 | 
			
		||||
            processingOverlay: new HUDProcessingOverlay(this.root),
 | 
			
		||||
            buildingsToolbar: new HUDBuildingsToolbar(this.root),
 | 
			
		||||
            buildingPlacer: new HUDBuildingPlacer(this.root),
 | 
			
		||||
            blueprintPlacer: new HUDBlueprintPlacer(this.root),
 | 
			
		||||
            buildingPlacer: new HUDBuildingPlacer(this.root),
 | 
			
		||||
            unlockNotification: new HUDUnlockNotification(this.root),
 | 
			
		||||
            gameMenu: new HUDGameMenu(this.root),
 | 
			
		||||
            massSelector: new HUDMassSelector(this.root),
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
import { DrawParameters } from "../../../core/draw_parameters";
 | 
			
		||||
//www.youtube.com/watch?v=KyorY1uIqiQimport { DrawParameters } from "../../../core/draw_parameters";
 | 
			
		||||
import { STOP_PROPAGATION } from "../../../core/signal";
 | 
			
		||||
import { TrackedState } from "../../../core/tracked_state";
 | 
			
		||||
import { Vector } from "../../../core/vector";
 | 
			
		||||
@ -36,6 +36,9 @@ export class HUDBlueprintPlacer extends BaseHUDPart {
 | 
			
		||||
            .getBinding(KEYMAPPINGS.placement.abortBuildingPlacement)
 | 
			
		||||
            .add(this.abortPlacement, this);
 | 
			
		||||
        keyActionMapper.getBinding(KEYMAPPINGS.placement.rotateWhilePlacing).add(this.rotateBlueprint, this);
 | 
			
		||||
        keyActionMapper
 | 
			
		||||
            .getBinding(KEYMAPPINGS.placement.abortBuildingPlacement)
 | 
			
		||||
            .add(this.abortPlacement, this);
 | 
			
		||||
 | 
			
		||||
        this.root.camera.downPreHandler.add(this.onMouseDown, this);
 | 
			
		||||
        this.root.camera.movePreHandler.add(this.onMouseMove, this);
 | 
			
		||||
 | 
			
		||||
@ -89,6 +89,16 @@ for (const categoryId in KEYMAPPINGS) {
 | 
			
		||||
 */
 | 
			
		||||
export function getStringForKeyCode(code) {
 | 
			
		||||
    switch (code) {
 | 
			
		||||
        case 1:
 | 
			
		||||
            return "LMB";
 | 
			
		||||
        case 2:
 | 
			
		||||
            return "MMB";
 | 
			
		||||
        case 3:
 | 
			
		||||
            return "RMB";
 | 
			
		||||
        case 4:
 | 
			
		||||
            return "MB4";
 | 
			
		||||
        case 5:
 | 
			
		||||
            return "MB5";
 | 
			
		||||
        case 8:
 | 
			
		||||
            return "⌫";
 | 
			
		||||
        case 9:
 | 
			
		||||
 | 
			
		||||
@ -66,11 +66,6 @@ export class GoogleAnalyticsImpl extends AnalyticsInterface {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    trackUiClick(elementName) {
 | 
			
		||||
        // Only track a fraction of clicks to not annoy google analytics
 | 
			
		||||
        if (Math_random() < 0.9) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const stateKey = this.app.stateMgr.getCurrentState().key;
 | 
			
		||||
        const fullSelector = stateKey + ">" + elementName;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -8,6 +8,7 @@ import { createLogger } from "../core/logging";
 | 
			
		||||
import { ExplainedResult } from "../core/explained_result";
 | 
			
		||||
import { THEMES, THEME, applyGameTheme } from "../game/theme";
 | 
			
		||||
import { IS_DEMO } from "../core/config";
 | 
			
		||||
import { T } from "../translations";
 | 
			
		||||
 | 
			
		||||
const logger = createLogger("application_settings");
 | 
			
		||||
 | 
			
		||||
@ -18,27 +19,45 @@ export const uiScales = [
 | 
			
		||||
    {
 | 
			
		||||
        id: "super_small",
 | 
			
		||||
        size: 0.6,
 | 
			
		||||
        label: "Super small",
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        id: "small",
 | 
			
		||||
        size: 0.8,
 | 
			
		||||
        label: "Small",
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        id: "regular",
 | 
			
		||||
        size: 1,
 | 
			
		||||
        label: "Regular",
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        id: "large",
 | 
			
		||||
        size: 1.2,
 | 
			
		||||
        label: "Large",
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        id: "huge",
 | 
			
		||||
        size: 1.4,
 | 
			
		||||
        label: "Huge",
 | 
			
		||||
    },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
export const scrollWheelSensitivities = [
 | 
			
		||||
    {
 | 
			
		||||
        id: "super_slow",
 | 
			
		||||
        scale: 0.25,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        id: "slow",
 | 
			
		||||
        scale: 0.5,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        id: "regular",
 | 
			
		||||
        scale: 1,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        id: "fast",
 | 
			
		||||
        scale: 2,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        id: "super_fast",
 | 
			
		||||
        scale: 4,
 | 
			
		||||
    },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
@ -47,7 +66,7 @@ export const allApplicationSettings = [
 | 
			
		||||
    new EnumSetting("uiScale", {
 | 
			
		||||
        options: uiScales.sort((a, b) => a.size - b.size),
 | 
			
		||||
        valueGetter: scale => scale.id,
 | 
			
		||||
        textGetter: scale => scale.label,
 | 
			
		||||
        textGetter: scale => T.settings.labels.uiScale.scales[scale.id],
 | 
			
		||||
        category: categoryApp,
 | 
			
		||||
        restartRequired: false,
 | 
			
		||||
        changeCb:
 | 
			
		||||
@ -56,6 +75,7 @@ export const allApplicationSettings = [
 | 
			
		||||
             */
 | 
			
		||||
            (app, id) => app.updateAfterUiScaleChanged(),
 | 
			
		||||
    }),
 | 
			
		||||
 | 
			
		||||
    new BoolSetting(
 | 
			
		||||
        "fullscreen",
 | 
			
		||||
        categoryApp,
 | 
			
		||||
@ -86,6 +106,18 @@ export const allApplicationSettings = [
 | 
			
		||||
         */
 | 
			
		||||
        (app, value) => app.sound.setMusicMuted(value)
 | 
			
		||||
    ),
 | 
			
		||||
    new EnumSetting("scrollWheelSensitivity", {
 | 
			
		||||
        options: scrollWheelSensitivities.sort((a, b) => a.scale - b.scale),
 | 
			
		||||
        valueGetter: scale => scale.id,
 | 
			
		||||
        textGetter: scale => T.settings.labels.scrollWheelSensitivity.sensitivity[scale.id],
 | 
			
		||||
        category: categoryApp,
 | 
			
		||||
        restartRequired: false,
 | 
			
		||||
        changeCb:
 | 
			
		||||
            /**
 | 
			
		||||
             * @param {Application} app
 | 
			
		||||
             */
 | 
			
		||||
            (app, id) => app.updateAfterUiScaleChanged(),
 | 
			
		||||
    }),
 | 
			
		||||
 | 
			
		||||
    // GAME
 | 
			
		||||
    new EnumSetting("theme", {
 | 
			
		||||
@ -133,6 +165,7 @@ class SettingsStorage {
 | 
			
		||||
        this.musicMuted = false;
 | 
			
		||||
        this.theme = "light";
 | 
			
		||||
        this.refreshRate = "60";
 | 
			
		||||
        this.scrollWheelSensitivity = "regular";
 | 
			
		||||
 | 
			
		||||
        this.alwaysMultiplace = false;
 | 
			
		||||
        this.abortPlacementOnDeletion = true;
 | 
			
		||||
@ -209,6 +242,17 @@ export class ApplicationSettings extends ReadWriteProxy {
 | 
			
		||||
        return 1;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getScrollWheelSensitivity() {
 | 
			
		||||
        const id = this.getAllSettings().scrollWheelSensitivity;
 | 
			
		||||
        for (let i = 0; i < scrollWheelSensitivities.length; ++i) {
 | 
			
		||||
            if (scrollWheelSensitivities[i].id === id) {
 | 
			
		||||
                return scrollWheelSensitivities[i].scale;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        logger.error("Unknown scroll wheel sensitivity id:", id);
 | 
			
		||||
        return 1;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getIsFullScreen() {
 | 
			
		||||
        return this.getAllSettings().fullscreen;
 | 
			
		||||
    }
 | 
			
		||||
@ -295,7 +339,7 @@ export class ApplicationSettings extends ReadWriteProxy {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getCurrentVersion() {
 | 
			
		||||
        return 8;
 | 
			
		||||
        return 9;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @param {{settings: SettingsStorage, version: number}} data */
 | 
			
		||||
@ -318,10 +362,15 @@ export class ApplicationSettings extends ReadWriteProxy {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (data.version < 8) {
 | 
			
		||||
            data.settings.abortPlacementOnDeletion = true;
 | 
			
		||||
            data.settings.scrollWheelSensitivity = "regular";
 | 
			
		||||
            data.version = 8;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (data.version < 9) {
 | 
			
		||||
            data.settings.abortPlacementOnDeletion = true;
 | 
			
		||||
            data.version = 9;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return ExplainedResult.good();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -10,8 +10,9 @@ import { BaseSavegameInterface } from "./savegame_interface";
 | 
			
		||||
import { createLogger } from "../core/logging";
 | 
			
		||||
import { globalConfig } from "../core/config";
 | 
			
		||||
import { SavegameInterface_V1000 } from "./schemas/1000";
 | 
			
		||||
import { getSavegameInterface } from "./savegame_interface_registry";
 | 
			
		||||
import { getSavegameInterface, savegameInterfaces } from "./savegame_interface_registry";
 | 
			
		||||
import { SavegameInterface_V1001 } from "./schemas/1001";
 | 
			
		||||
import { SavegameInterface_V1002 } from "./schemas/1002";
 | 
			
		||||
 | 
			
		||||
const logger = createLogger("savegame");
 | 
			
		||||
 | 
			
		||||
@ -30,6 +31,11 @@ export class Savegame extends ReadWriteProxy {
 | 
			
		||||
 | 
			
		||||
        /** @type {import("./savegame_typedefs").SavegameData} */
 | 
			
		||||
        this.currentData = this.getDefaultData();
 | 
			
		||||
 | 
			
		||||
        assert(
 | 
			
		||||
            savegameInterfaces[Savegame.getCurrentVersion()],
 | 
			
		||||
            "Savegame interface not defined: " + Savegame.getCurrentVersion()
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    //////// RW Proxy Impl //////////
 | 
			
		||||
@ -38,14 +44,14 @@ export class Savegame extends ReadWriteProxy {
 | 
			
		||||
     * @returns {number}
 | 
			
		||||
     */
 | 
			
		||||
    static getCurrentVersion() {
 | 
			
		||||
        return 1001;
 | 
			
		||||
        return 1002;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @returns {typeof BaseSavegameInterface}
 | 
			
		||||
     */
 | 
			
		||||
    static getReaderClass() {
 | 
			
		||||
        return SavegameInterface_V1001;
 | 
			
		||||
        return savegameInterfaces[Savegame.getCurrentVersion()];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -82,6 +88,11 @@ export class Savegame extends ReadWriteProxy {
 | 
			
		||||
            data.version = 1001;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (data.version === 1001) {
 | 
			
		||||
            SavegameInterface_V1002.migrate1001to1002(data);
 | 
			
		||||
            data.version = 1002;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return ExplainedResult.good();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -2,11 +2,13 @@ import { BaseSavegameInterface } from "./savegame_interface";
 | 
			
		||||
import { SavegameInterface_V1000 } from "./schemas/1000";
 | 
			
		||||
import { createLogger } from "../core/logging";
 | 
			
		||||
import { SavegameInterface_V1001 } from "./schemas/1001";
 | 
			
		||||
import { SavegameInterface_V1002 } from "./schemas/1002";
 | 
			
		||||
 | 
			
		||||
/** @type {Object.<number, typeof BaseSavegameInterface>} */
 | 
			
		||||
const interfaces = {
 | 
			
		||||
export const savegameInterfaces = {
 | 
			
		||||
    1000: SavegameInterface_V1000,
 | 
			
		||||
    1001: SavegameInterface_V1001,
 | 
			
		||||
    1002: SavegameInterface_V1002,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const logger = createLogger("savegame_interface_registry");
 | 
			
		||||
@ -27,7 +29,7 @@ export function getSavegameInterface(savegame) {
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const interfaceClass = interfaces[version];
 | 
			
		||||
    const interfaceClass = savegameInterfaces[version];
 | 
			
		||||
    if (!interfaceClass) {
 | 
			
		||||
        logger.warn("Version", version, "has no implemented interface!");
 | 
			
		||||
        return null;
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										37
									
								
								src/js/savegame/schemas/1002.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								src/js/savegame/schemas/1002.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,37 @@
 | 
			
		||||
import { createLogger } from "../../core/logging.js";
 | 
			
		||||
import { T } from "../../translations.js";
 | 
			
		||||
import { SavegameInterface_V1001 } from "./1001.js";
 | 
			
		||||
 | 
			
		||||
const schema = require("./1002.json");
 | 
			
		||||
const logger = createLogger("savegame_interface/1002");
 | 
			
		||||
 | 
			
		||||
export class SavegameInterface_V1002 extends SavegameInterface_V1001 {
 | 
			
		||||
    getVersion() {
 | 
			
		||||
        return 1002;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getSchemaUncached() {
 | 
			
		||||
        return schema;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @param {import("../savegame_typedefs.js").SavegameData} data
 | 
			
		||||
     */
 | 
			
		||||
    static migrate1001to1002(data) {
 | 
			
		||||
        logger.log("Migrating 1001 to 1002");
 | 
			
		||||
        const dump = data.dump;
 | 
			
		||||
        if (!dump) {
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const entities = dump.entities;
 | 
			
		||||
        for (let i = 0; i < entities.length; ++i) {
 | 
			
		||||
            const entity = entities[i];
 | 
			
		||||
            const beltComp = entity.components.Belt;
 | 
			
		||||
            const ejectorComp = entity.components.ItemEjector;
 | 
			
		||||
            if (beltComp && ejectorComp) {
 | 
			
		||||
                ejectorComp.instantEject = true;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										5
									
								
								src/js/savegame/schemas/1002.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								src/js/savegame/schemas/1002.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,5 @@
 | 
			
		||||
{
 | 
			
		||||
    "type": "object",
 | 
			
		||||
    "required": [],
 | 
			
		||||
    "additionalProperties": true
 | 
			
		||||
}
 | 
			
		||||
@ -105,6 +105,10 @@ export class KeybindingsState extends TextualGameState {
 | 
			
		||||
                event.preventDefault();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (event.target && event.target.tagName === "BUTTON" && keyCode === 1) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (
 | 
			
		||||
                // Enter
 | 
			
		||||
                keyCode === 13 ||
 | 
			
		||||
@ -122,8 +126,8 @@ export class KeybindingsState extends TextualGameState {
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        dialog.inputReciever.backButton.add(() => {});
 | 
			
		||||
 | 
			
		||||
        this.dialogs.internalShowDialog(dialog);
 | 
			
		||||
 | 
			
		||||
        this.app.sound.playUiSound(SOUNDS.dialogOk);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -26,6 +26,13 @@ global:
 | 
			
		||||
    # How big numbers are rendered, e.g. "10,000"
 | 
			
		||||
    thousandsDivider: ","
 | 
			
		||||
 | 
			
		||||
    # The suffix for large numbers, e.g. 1.3k, 400.2M, etc.
 | 
			
		||||
    suffix:
 | 
			
		||||
        thousands: k
 | 
			
		||||
        millions: M
 | 
			
		||||
        billions: B
 | 
			
		||||
        trillions: T
 | 
			
		||||
 | 
			
		||||
    # Shown for infinitely big numbers
 | 
			
		||||
    infinite: inf
 | 
			
		||||
 | 
			
		||||
@ -127,7 +134,7 @@ dialogs:
 | 
			
		||||
 | 
			
		||||
    editKeybinding:
 | 
			
		||||
        title: Change Keybinding
 | 
			
		||||
        desc: Press the key you want to assign, or escape to cancel.
 | 
			
		||||
        desc: Press the key or mouse button you want to assign, or escape to cancel.
 | 
			
		||||
 | 
			
		||||
    resetKeybindingsConfirmation:
 | 
			
		||||
        title: Reset keybindings
 | 
			
		||||
@ -511,6 +518,23 @@ settings:
 | 
			
		||||
            title: Interface scale
 | 
			
		||||
            description: >-
 | 
			
		||||
                Changes the size of the user interface. The interface will still scale based on your device resolution, but this setting controls the amount of scale.
 | 
			
		||||
            scales:
 | 
			
		||||
                super_small: Super small
 | 
			
		||||
                small: Small
 | 
			
		||||
                regular: Regular
 | 
			
		||||
                large: Large
 | 
			
		||||
                huge: Huge
 | 
			
		||||
 | 
			
		||||
        scrollWheelSensitivity:
 | 
			
		||||
            title: Zoom sensitivity
 | 
			
		||||
            description: >-
 | 
			
		||||
                Changes how sensitive the zoom is (Either mouse wheel or trackpad).
 | 
			
		||||
            sensitivity:
 | 
			
		||||
                super_slow: Super slow
 | 
			
		||||
                slow: Slow
 | 
			
		||||
                regular: Regular
 | 
			
		||||
                fast: Fast
 | 
			
		||||
                super_fast: Super fast
 | 
			
		||||
 | 
			
		||||
        fullscreen:
 | 
			
		||||
            title: Fullscreen
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user