diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 737f7cb..0fbb298 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -3,8 +3,6 @@ name: 'Publish Tauri App' on: workflow_dispatch: -# This workflow will trigger on each push to the `release` branch to create or update a GitHub release, build your app, and upload the artifacts to the release. - jobs: publish-tauri: permissions: @@ -17,7 +15,7 @@ jobs: # args: '--target aarch64-apple-darwin' #- platform: 'macos-latest' # for Intel based macs. # args: '--target x86_64-apple-darwin' - - platform: 'ubuntu-22.04' # for Tauri v1 you could replace this with ubuntu-20.04. + - platform: 'ubuntu-20.04' args: '' - platform: 'windows-latest' args: '' @@ -34,17 +32,13 @@ jobs: - name: install Rust stable uses: dtolnay/rust-toolchain@stable with: - # Those targets are only used on macos runners so it's in an `if` to slightly speed up windows and linux builds. targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }} - name: install dependencies (ubuntu only) - if: matrix.platform == 'ubuntu-22.04' # This must match the platform value defined above. + if: matrix.platform == 'ubuntu-20.04' run: | sudo apt-get update - sudo apt-get install -y libwebkit2gtk-4.0-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf - # webkitgtk 4.0 is for Tauri v1 - webkitgtk 4.1 is for Tauri v2. - # You can remove the one that doesn't apply to your app to speed up the workflow a bit. - + sudo apt-get install -y libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf - run: | mkdir -p tauri/src @@ -52,6 +46,7 @@ jobs: cp -r assets/ tauri/src/ cp -r styles/ tauri/src/ cp -r src/ tauri/src/ + cp -r lib/ tauri/src/ cd tauri npm install @@ -59,7 +54,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: - tagName: app-v__VERSION__ # the action automatically replaces \_\_VERSION\_\_ with the app version. + tagName: app-v__VERSION__ # automatically replaces \_\_VERSION\_\_ with the app version releaseName: 'App v__VERSION__' releaseBody: 'See the assets to download this version and install.' releaseDraft: true diff --git a/.gitignore b/.gitignore index ab39c6e..4397437 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ .vscode/ -package-lock.json Cargo.lock -README.md \ No newline at end of file +*.d.ts +jsconfig.json \ No newline at end of file diff --git a/README.md b/README.md index 973b966..6a797a4 100644 --- a/README.md +++ b/README.md @@ -1,50 +1,62 @@ -# Teti README - -Hosted on github pages [here](https://titanplayz100.github.io/teti/) - -The info page can be found [here](https://titanplayz100.github.io/teti/info.html) - -> [!WARNING] -> Firefox is not supported (due to import assertions) - -## Desktop App -### NEW! - -Releases are now run through a workflow. They are **up to date** and contain all the latest features. - -App build using Tauri, feel free to open issues and PRs. - -## Data Formats (for my convenience) -### Gamemode Structure (gamemodes.json) -```js -gamemodes = { - "gamemode_name": { - settings: {}, // settings that override the default * settings - displayName: "", // name shown on gamemode selection - objectiveText: "", // subtext displayed on right side - goalStat: "", // stat being tracked (valid property in stats class) - target: "", // target (valid target in settings) - result: "", // displayed as result (another valid stat in stats class) - - // TO BE ADDED LATER - music: "", // custom song that can play - compmusic: "", // custom song that played on pb pace - startBoard: "", // starting board, tetrio map format - effects: [], // custom background / effects - } -} -``` - -Add functionality mainly in `features/modes.js`. -You can modify existing modules as well from other files - -### Adding Audio (sfxlist.json) -```json -{ - { - "name": "", - "path": "assets/sfx/." - } -} -``` -Use with `this.game.sounds.playSound()` +# Teti README + +Hosted on github pages [here](https://titanplayz100.github.io/teti/) + +The info page can be found [here](https://titanplayz100.github.io/teti/info.html) + +> [!WARNING] +> Firefox is not supported (due to import assertions) + +## Desktop App +Releases are now run through a workflow. They are **up to date** and contain all the latest features. + +App build using Tauri, feel free to open issues and PRs. + +## Data Formats (for my convenience) +### Gamemode Structure (gamemodes.json) +```js +gamemodes = { + "gamemode_name": { + settings: {}, // settings that override the default * settings + displayName: "", // name shown on gamemode selection + objectiveText: "", // subtext displayed on right side + goalStat: "", // stat being tracked (valid property in stats class) + target: "", // target (valid target in settings) + result: "", // displayed as result (another valid stat in stats class) + + // TO BE ADDED LATER + music: "", // custom song that can play + compmusic: "", // custom song that played on pb pace + startBoard: "", // starting board, tetrio map format + effects: [], // custom background / effects + } +} +``` + +Add functionality mainly in `features/modes.js`. +You can modify existing modules as well from other files + +### Adding Audio (sfxlist.json) +```json +{ + { + "name": "", + "path": "assets/sfx/." + } +} +``` +Use with `this.game.sounds.playSound()` + + +## Types for PIXIjs +Workaround to use types when using pixijs imported through script tag: +- download [pixi.js.d.ts](https://github.com/pixijs/pixijs/releases) from pixijs repo and place file in /src +- setup jsconfig.json to automatically detect pixi.js.d.ts for types + +```json +{ + "typeAcquisition": { + "include": ["pixi.js.d.ts"] + } +} +``` \ No newline at end of file diff --git a/assets/icons/play.svg b/assets/icons/play.svg new file mode 100644 index 0000000..5422941 --- /dev/null +++ b/assets/icons/play.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/reset.svg b/assets/icons/reset.svg index 1556771..0b47741 100644 --- a/assets/icons/reset.svg +++ b/assets/icons/reset.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/particle.png b/assets/particle.png new file mode 100644 index 0000000..3021288 Binary files /dev/null and b/assets/particle.png differ diff --git a/index.html b/index.html index 7628220..5fc10e2 100644 --- a/index.html +++ b/index.html @@ -1,71 +1,33 @@ + teti - teti + + - - - + + + + + +
-
+

⚠ GO DOWN

- -
-
- - - - - - -

60S LEFT

-
- - -
-

Next

-

Hold

-

-

-

-

-

-

-

-

-

-

-

-

-

-

-

-

- -
- -
- - -
- +

Teti

@@ -75,9 +37,18 @@

Teti

OUT OF FOCUSClick To Return
+
+

history: 0

+ + +
+ + @@ -146,17 +117,17 @@

Teti

Background URL/Hex

Block Skin URL/Name

Board Opacity

-

Board Size

+

Board Size

Board Bounciness

Enable Grid

Grid opacity

Grid Type

Shadow opacity

Coloured Shadow

-

Coloured Borders

Rainbow effect on fast pace

Show Lock Bar

Show out of focus text

+

Action Text

Enable Particles

Particle Volume

Particle Size

@@ -183,7 +154,6 @@

Teti

Infinite Hold

Allow Queue Modification

Line Clear Delay

-

Line Clear Delay

Allspin

Allspin's are mini

Allow history (undo/redo)

diff --git a/info.md b/info.md index 89123ee..70742b9 100644 --- a/info.md +++ b/info.md @@ -17,6 +17,7 @@ This project started out as a learning experience, because surprisingly it was m ## Acknowledgements Thanks [existancepy](https://github.com/existancepy) for all the fixes. Thanks [ItzBlack6093](https://github.com/ItzBlack6093) for adding many modes and fixes. +Thanks [ItzBlack6093](https://github.com/ItzBlack6093) for adding many modes and fixes. Feel free to contribute with features and fixes, and open issues. @@ -69,22 +70,66 @@ Skins from [YHF](https://you.have.fail/ed/at/tetrioplus/) Things that I am working on based on other changes - ready set go start option - arcade mode - -Maybe: -- cooler action text (maybe if i switch to pixijs then i can make text better) - -Future thoughts (honestly only if i have time): -- I honestly might want to break my rule and start using a framework for rendering. - - (turns out pixijs is much better for games than canvas 2d, and howler for audio) -- I have mixed feelings, because this would make the project technically no longer purely made by me (though the info page is a bit iffy) -- Maybe I don't care and will switch anyway, and maybe in the end it will pay off. -- this would make my most recent changes irrelivent - - such as piece flash and reset animation cause i have to redo them in pixi - - in the future itll probably pay off +- rotation centres + +- issue holding softdrop and left/right removes piece dropping cooldown + +- CHANGE GAME CLASS TO BE EXPORTED MODULES + - remove all this.game references in replacement with just the class being imported + - **no longer shall the shackles of past object oriented java code bound the multiparadigm eutopia which is javascript** + +Zenith mode additions +- add fatigue: +8:00 - 2 permanent garbage +9:00 - receive 25% more garbage +10:00 - 3 more permanent garbage (5 total) +11:00 - debuff increased to 50% more garbage +12:00 - 5 more permanent garbage (10 total) + +- maybe add a "currently in hyperspeed" graphic +- simulated garbage recieving option + - amount of garbage sent to player increases with floor + - garbage is random bursts + - attacks 8 or larger split using {!!} thing (0.5s gap) +- option for expert mode style + +- update climb bar colours: +Red (#e43921) +Yellow (#ffb400) +Green (#82fc40) +Blue (#3ca6ff) +Magenta (#ff46da) +Khaki (#ffc48e) +"Teal Green" (#99ffc6) +"Cyan Blue" (#00f7ff) +"Soft Pink" (#ffbbea) +White (#ffffff) *** ## Updates +#### v1.3.5 +- New board design inspired from [this reddit post](https://www.reddit.com/r/Tetris/comments/1g80adg/tetris_ui_concept/) +- added continue button in menu, and changed ingame buttons to use pixijs now +- experimenting with animation library called gsap +- changed action text and stats text to use pixi now +- changed PC text to be more TETR.IO-like +- changed spike text to show in the middle of the board (like techmino) +- RIP coloured queues +- better page loading using defer and lazy loading +- Experimental: pressed keys are stored in a queue, and evry pixi tick they all fire, might be more responsive +- changed timeLeftText animation to use gsap and pixi +- added setting for toggling action text + +#### v1.3.4 => PIXI.js +- COMPLETE REWRITE OF ALL GRAPHICS +- Everything is now rendered through pixijs + - this includes the board, next queue, hold queue, and particles + - ye turns out there is a high learning curve with pixi (i lost it several times) + - everything should now be much more performant and smooth yay +- some things were changed due to this + - need to add border colour back + #### v1.3.3 - Thanks to itzblack for the Zenith Tower mode - added option for line clear delay diff --git a/src/data/blocksprites.json b/src/data/blocksprites.json new file mode 100644 index 0000000..7d967f9 --- /dev/null +++ b/src/data/blocksprites.json @@ -0,0 +1,20 @@ +{ + "frames": { + "z": { "frame": { "x": 0, "y": 0, "w": 30, "h": 30 } }, + "l": { "frame": { "x": 31, "y": 0, "w": 30, "h": 30 } }, + "o": { "frame": { "x": 62, "y": 0, "w": 30, "h": 30 } }, + "s": { "frame": { "x": 93, "y": 0, "w": 30, "h": 30 } }, + "i": { "frame": { "x": 124, "y": 0, "w": 30, "h": 30 } }, + "j": { "frame": { "x": 155, "y": 0, "w": 30, "h": 30 } }, + "t": { "frame": { "x": 186, "y": 0, "w": 30, "h": 30 } }, + "shadow": { "frame": { "x": 217, "y": 0, "w": 30, "h": 30 } }, + "hold": { "frame": { "x": 248, "y": 0, "w": 30, "h": 30 } }, + "g": { "frame": { "x": 279, "y": 0, "w": 30, "h": 30 } }, + "darkg": { "frame": { "x": 310, "y": 0, "w": 30, "h": 30 } }, + "topout": { "frame": { "x": 341, "y": 0, "w": 30, "h": 30 } } + }, + "meta": { + "size": { "w": 372, "h": 30 }, + "scale": "1" + } +} \ No newline at end of file diff --git a/src/data/defaultSettings.json b/src/data/defaultSettings.json index ade8d14..44c952a 100644 --- a/src/data/defaultSettings.json +++ b/src/data/defaultSettings.json @@ -3,20 +3,20 @@ "background": "#080B0C", "boardOpacity": 100, "showGrid": true, - "gridopacity": 20, + "gridopacity": 30, "gridType":"round", "shadowOpacity": 20, "boardHeight": 80, "colouredShadow": false, - "colouredQueues": true, "lockBar": true, "rainbowPB": true, - "boardBounce": 1, + "boardBounce": 2, "outoffocus": true, "particles":true, "particleVolume": 60, "particleSize":1, - "skin":"tetrio" + "skin":"tetrio", + "actionText": true }, "game": { "gravitySpeed": 950, diff --git a/src/data/gamemodes.json b/src/data/gamemodes.json index 3be3c76..902a85b 100644 --- a/src/data/gamemodes.json +++ b/src/data/gamemodes.json @@ -1,147 +1,166 @@ -{ - "*": { - "settings": { - "gravitySpeed": 950, - "lockDelay": 600, - "maxLockMovements": 15, - "nextPieces": 5, - "allowLockout": false, - "preserveARR": true, - "allowHold": true, - "infiniteHold": false, - "allowQueueModify": false, - "allspin": false, - "allspinminis": false, - "history": false, - "sidebar": ["time", "apm", "pps"], - "stride": false - }, - "displayName": "Unset", - "objectiveText": "", - "goalStat": "", - "target": "", - "result": "", - "music": "", - "compmusic": "", - "startBoard": "", - "effects": [] - }, - "custom": { - "displayName": "Zen / Custom" - }, - "sprint": { - "displayName": "Sprint", - "objectiveText": "Lines", - "goalStat": "clearlines", - "target": "requiredLines", - "result": "time", - "settings": { - "requiredLines": 40, - "stride":true - } - }, - "ultra": { - "displayName": "Ultra", - "objectiveText": "Score", - "goalStat": "time", - "target": "timeLimit", - "result": "score", - "settings": { - "timeLimit": 120, - "sidebar": ["time", "score", "pps"] - } - }, - "attacker": { - "displayName": "Attacker", - "objectiveText": "Damage", - "goalStat": "attack", - "target": "requiredAttack", - "result": "time", - "settings": { - "requiredAttack": 100 - } - }, - "digger": { - "displayName": "Digger", - "objectiveText": "Remaining", - "goalStat": "cleargarbage", - "target": "requiredGarbage", - "result": "time", - "settings": { - "requiredGarbage": 100, - "sidebar": ["time", "dss", "pps"] - } - }, - "survival": { - "displayName": "Survival", - "objectiveText": "received", - "goalStat": "clearlines", - "target": "gameEnd", - "result": "time", - "settings": { - "survivalRate": 60, - "sidebar": ["time", "lpm", "pps"] - } - }, - "backfire": { - "displayName": "Backfire", - "objectiveText": "Sent", - "goalStat": "attack", - "target": "requiredAttack", - "result": "time", - "settings": { - "requiredAttack": 100, - "backfireMulti": 1 - } - }, - "combo": { - "displayName": "4w / Combo", - "objectiveText": "Time", - "goalStat": "time", - "target": "combobreak", - "result": "maxCombo", - "settings": { - "allspin": true, - "allspinminis": false, - "sidebar": ["combo", "pps"] - } - }, - "lookahead": { - "displayName": "Lookahead", - "objectiveText": "Lines", - "goalStat": "clearlines", - "target": "requiredLines", - "result": "time", - "settings": { - "requiredLines": 40, - "lookAheadPieces": 3, - "gravitySpeed": 1001, - "sidebar": ["time", "kpp", "pps"] - } - }, - "race": { - "displayName": "Race", - "objectiveText": "Level", - "goalStat": "tgm_level", - "target": "raceTarget", - "result": "grade", - "settings": { - "raceTarget": 999, - "gravitySpeed": 0, - "clearDelay": 420 - } - }, - "zenith": { - "displayName": "Climb", - "objectiveText": "Altitude", - "goalStat": "altitude", - "target": "requiredAltitude", - "result": "time", - "settings": { - "requiredAltitude": 1650, - "gravitySpeed": 950, - "allspin": true, - "allspinminis": true - } - } -} +{ + "*": { + "settings": { + "gravitySpeed": 950, + "lockDelay": 600, + "maxLockMovements": 15, + "nextPieces": 5, + "allowLockout": false, + "preserveARR": true, + "allowHold": true, + "infiniteHold": false, + "allowQueueModify": false, + "allspin": false, + "allspinminis": false, + "history": false, + "sidebar": ["time", "apm", "pps"], + "stride": false, + "clearDelay": 0 + }, + "displayName": "Unset", + "objectiveText": "", + "goalStat": "", + "target": "", + "result": "", + + "music": "", + "compmusic": "", + "startBoard": "", + "effects": [] + }, + "custom": { + "displayName": "Zen / Custom" + }, + "sprint": { + "displayName": "Sprint", + "objectiveText": "Lines", + "goalStat": "clearlines", + "target": "requiredLines", + "result": "time", + "settings": { + "requiredLines": 40, + "stride":true + } + }, + "ultra": { + "displayName": "Ultra", + "objectiveText": "Score", + "goalStat": "time", + "target": "timeLimit", + "result": "score", + "settings": { + "timeLimit": 120, + "sidebar": ["time", "score", "pps"] + } + }, + "attacker": { + "displayName": "Attacker", + "objectiveText": "Damage", + "goalStat": "attack", + "target": "requiredAttack", + "result": "time", + "settings": { + "requiredAttack": 100 + } + }, + "digger": { + "displayName": "Digger", + "objectiveText": "Remaining", + "goalStat": "cleargarbage", + "target": "requiredGarbage", + "result": "time", + "settings": { + "requiredGarbage": 100, + "sidebar": ["time", "dss", "pps"] + } + }, + "survival": { + "displayName": "Survival", + "objectiveText": "received", + "goalStat": "clearlines", + "target": "gameEnd", + "result": "time", + "settings": { + "survivalRate": 60, + "sidebar": ["time", "lpm", "pps"] + } + }, + "backfire": { + "displayName": "Backfire", + "objectiveText": "Sent", + "goalStat": "attack", + "target": "requiredAttack", + "result": "time", + "settings": { + "requiredAttack": 100, + "backfireMulti": 1 + } + }, + "combo": { + "displayName": "4w / Combo", + "objectiveText": "Time", + "goalStat": "time", + "target": "combobreak", + "result": "maxCombo", + "settings": { + "allspin": true, + "allspinminis": false, + "sidebar": ["combo", "pps"] + } + }, + "lookahead": { + "displayName": "Lookahead", + "objectiveText": "Lines", + "goalStat": "clearlines", + "target": "requiredLines", + "result": "time", + "settings": { + "requiredLines": 40, + "lookAheadPieces": 3, + "gravitySpeed": 1001, + "sidebar": ["time", "kpp", "pps"] + } + }, + "race": { + "displayName": "Race", + "objectiveText": "Level", + "goalStat": "tgm_level", + "target": "raceTarget", + "result": "grade", + "settings": { + "raceTarget": 999, + "gravitySpeed": 0, + "clearDelay": 420 + } + }, + "zenith": { + "displayName": "Climb", + "objectiveText": "Altitude", + "goalStat": "altitude", + "target": "requiredAltitude", + "result": "time", + "settings": { + "requiredAltitude": 1650, + "gravitySpeed": 950, + "allspin": true, + "allspinminis": true + } + }, + "classic": { + "displayName": "Classic", + "objectiveText": "Score", + "goalStat": "score", + "target": "clearlines", + "result": "score", + "settings": { + "requiredLines": 999, + "gravitySpeed": 999, + "lockDelay": 30, + "maxLockMovements": 1, + "nextPieces": 1, + "allowHold": false, + "sidebar": ["time", "score", "pps"], + "clearDelay": 500 + } + } +} diff --git a/src/display/boardEffects.js b/src/display/boardEffects.js index 72ad0de..96bde5f 100644 --- a/src/display/boardEffects.js +++ b/src/display/boardEffects.js @@ -1,112 +1,105 @@ -import { pbTrackingStat } from "../data/data.js"; -import { Game } from "../game.js"; - -export class BoardEffects { - X = 0; - Y = 0; - dX = 0; - dY = 0; - friction = 0.75; - springConstant = 0.02; - targetX = 0; - targetY = 0; - R = 0; - dR = 0; - targetR = 0; - - hasPace = true; - paceCooldown = 0; - - divBoard = document.getElementById("board"); - divDanger = document.getElementById("dangerOverlay"); - border = document.getElementById('backborder') - backboard = document.getElementById('backboard') - - /** - * - * @param {Game} game - */ - constructor(game) { - this.game = game; - } - - move(forceX, forceY) { - this.dX += forceX; - this.dY += forceY; - - const newdx = this.targetX - this.X; - const newdy = this.targetY - this.Y; - const fX = newdx * this.springConstant; - const fY = newdy * this.springConstant; - - this.dX += fX; - this.dY += fY; - this.dX *= this.friction; - this.dY *= this.friction; - this.X += this.dX; - this.Y += this.dY; - - this.X = this.clamp(this.X, 0.5); - this.Y = this.clamp(this.Y, 0.5); - - if (this.X != 0 || this.Y != 0) { - this.divBoard.style.translate = `${this.X}px ${this.Y}px` - } - } - - rotate(torque) { - this.dR += torque; - let newangle = this.targetR - this.R; - const fangle = newangle * this.springConstant; - - this.dR += fangle; - this.dR *= this.friction; - this.R += this.dR; - this.R = this.clamp(this.R, 0.1); - - if (this.R != 0) { - this.divBoard.style.rotate = `${this.R}deg` - } - } - - clamp(num, min) { - if (num < min && num > -min) return 0; - return num - } - - rainbowBoard() { - const stats = this.game.stats; - const pbs = this.game.profilestats.personalBests; - const gamemode = this.game.settings.game.gamemode; - - if (!this.game.settings.display.rainbowPB || - !this.game.settings.game.competitiveMode || - stats.time < 0.5 || pbs[gamemode] == undefined) return; - if (this.paceCooldown > 0) { this.paceCooldown--; return; } - - const trackingStat = pbTrackingStat[this.game.modes.modeJSON.goalStat]; - const current = stats[trackingStat]; - const pbpace = pbs[gamemode].pbstats[trackingStat]; - if (current < pbpace && this.hasPace) { - this.game.sounds.playSound("pbend"); - this.toggleRainbow(false); - } else if (current >= pbpace && !this.hasPace) { - this.game.sounds.playSound("pbstart"); - this.toggleRainbow(true); - } - - this.paceCooldown = this.game.tickrate * 3; - } - - toggleRainbow(pace) { - this.border.style.setProperty('--blur-size', pace ? `0.3vmin` : `0vmin`) - this.border.style.setProperty('--blur-strength', pace ? '0.7vmin' : '0') - this.backboard.style.setProperty('--blur-strength', pace ? '0.5vmin' : '0') - this.hasPace = pace; - } - - toggleDangerBoard(inDanger) { - this.border.classList.toggle("boardDanger", inDanger); - this.divDanger.style.opacity = inDanger ? "0.1" : "0"; - } +import { pbTrackingStat } from "../data/data.js"; +import { Game } from "../game.js"; + +export class BoardEffects { + X = 0; + Y = 0; + dX = 0; + dY = 0; + friction = 0.7; + springConstant = 0.015; + targetX = 0; + targetY = 0; + R = 0; + dR = 0; + targetR = 0; + + hasPace = true; + paceCooldown = 0; + + divBoard = document.getElementById("board"); + + /** + * + * @param {Game} game + */ + constructor(game) { + this.game = game; + } + + move(forceX, forceY) { + this.dX += forceX; + this.dY += forceY; + + const newdx = this.targetX - this.X; + const newdy = this.targetY - this.Y; + const fX = newdx * this.springConstant; + const fY = newdy * this.springConstant; + + this.dX += fX; + this.dY += fY; + this.dX *= this.friction; + this.dY *= this.friction; + this.X += this.dX; + this.Y += this.dY; + + this.X = this.clamp(this.X, 0.5); + this.Y = this.clamp(this.Y, 0.5); + + if (this.X != 0 || this.Y != 0) { + this.game.pixi.app.canvas.style.translate = `${this.X}px ${this.Y}px` + } + } + + rotate(torque) { + this.dR += torque; + let newangle = this.targetR - this.R; + const fangle = newangle * this.springConstant; + + this.dR += fangle; + this.dR *= this.friction; + this.R += this.dR; + this.R = this.clamp(this.R, 0.1); + + if (this.R != 0) { + this.game.pixi.app.canvas.style.rotate = `${this.R}deg` + } + } + + clamp(num, min) { + if (num < min && num > -min) return 0; + return num + } + + rainbowBoard() { + const stats = this.game.stats; + const pbs = this.game.profilestats.personalBests; + const gamemode = this.game.settings.game.gamemode; + + if (!this.game.settings.display.rainbowPB || + !this.game.settings.game.competitiveMode || + stats.time < 0.5 || pbs[gamemode] == undefined) return; + if (this.paceCooldown > 0) { this.paceCooldown--; return; } + + const trackingStat = pbTrackingStat[this.game.modes.modeJSON.goalStat]; + const current = stats[trackingStat]; + const pbpace = pbs[gamemode].pbstats[trackingStat]; + if (current < pbpace && this.hasPace) { + this.game.sounds.playSound("pbend"); + this.toggleRainbow(false); + } else if (current >= pbpace && !this.hasPace) { + this.game.sounds.playSound("pbstart"); + this.toggleRainbow(true); + } + + this.paceCooldown = this.game.tickrate * 3; + } + + toggleRainbow(pace) { + // todo add back + // this.border.style.setProperty('--blur-size', pace ? `0.3vmin` : `0vmin`) + // this.border.style.setProperty('--blur-strength', pace ? '0.7vmin' : '0') + // this.backboard.style.setProperty('--blur-strength', pace ? '0.5vmin' : '0') + // this.hasPace = pace; + } } \ No newline at end of file diff --git a/src/display/particles.js b/src/display/particles.js index f2d3f87..1cb236a 100644 --- a/src/display/particles.js +++ b/src/display/particles.js @@ -1,227 +1,233 @@ -import { Game } from "../game.js"; - -class Point { - constructor(particleInfo, ctx) { - const { x, y, colour, size, life, dx, dy, sway, xF, yF, swayF, gravity, twinkle, twinkleTime } = particleInfo; - this.x = x; - this.y = y; - this.size = size; - this.colour = colour; - this.maxLife = life; - this.life = life; - this.dx = dx; - this.dy = dy; - this.sway = sway ?? 0; - this.frictionX = xF ?? 1; - this.frictionY = yF ?? 1; - this.frictionSway = swayF ?? 1; - this.gravity = gravity ?? 0; - this.twinkle = twinkle ?? false; - this.twinkleTime = twinkleTime ?? this.life; - - this.ctx = ctx; - } - - draw() { - this.ctx.globalAlpha = Math.max(0, this.life / this.maxLife); - this.ctx.fillStyle = this.colour; - this.ctx.beginPath(); - this.ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); - this.ctx.fill(); - } - - update() { - this.life -= 1; - this.x += this.dx + Math.sin(this.sway * this.life); - this.y += this.dy; - this.dx *= this.frictionX; - this.dy *= this.frictionY; - this.sway *= this.frictionSway - this.dy += this.gravity; - if (this.twinkle && this.life < this.twinkleTime) this.size = Math.abs((Math.sin(this.life / 15))) * 2; - } -} - -export class Particles { - particles = []; - - /** - * @param {Game} game - */ - constructor(game) { - this.game = game; - this.ctx = this.game.renderer.ctx; - } - - initBoard() { - this.boardWidth = this.game.renderer.boardWidth; - this.boardHeight = this.game.renderer.boardHeight; - this.minosize = this.game.boardrender.minoSize; - } - - spawnParticles(posX, posY, type, pieceWidth = 1, cw = false, colour = "white") { - if (!this.game.settings.display.particles) return - this.volume = this.game.settings.display.particleVolume; - this.size = this.game.settings.display.particleSize; - const [x, y] = [posX * this.minosize, (40 - posY) * this.minosize]; - if (type == "drop") this.creatDropParticles(x, y, colour, this.minosize * pieceWidth, -this.boardHeight); - if (type == "lock") this.createLockParticles(x, y, colour, this.minosize * pieceWidth, 10); - if (type == "clear") this.createClearParticles(x, y, colour, this.boardWidth, -10); - if (type == "pc") this.createPCParticles(x, y, this.boardWidth, 10); - if (type == "dangerboard") this.createDangerBoardParticles(x, this.boardHeight, colour, this.boardWidth, 10); - if (type == "dangersides") this.createDangerSidesParticles(x, y, "red", this.boardWidth, 0, 1); - if (type == "spin") this.createSpinParticles(x, y, colour, cw, this.minosize * pieceWidth, -this.minosize * pieceWidth); - if (type == "spike") this.createSpikeParticles(x, this.boardHeight, colour, this.boardWidth, -this.boardHeight); - if (type == "BTB") this.createBTBParticle(x, y, "gold", this.boardWidth, 0, this.boardHeight); - } - - creatDropParticles(x, y, colour, len, height) { - for (let i = 0; i < this.volume / 3; i++) { - const posX = x + Math.random() * len - len / 2; - const posY = y + Math.random() * height / 2; - const life = Math.random() * 35 + 70; - const dx = Math.random() * 1 - 0.5; - const dy = Math.random() * -1.2 - 2.4; - const sway = Math.random() * 0.04 - 0.02; - - const placeParticle = { x: posX, y: posY, colour, size: this.size, life, dx, dy, sway, xF: 0.95, yF: 0.95, swayF: 0.96 } - const particle = new Point(placeParticle, this.ctx); - this.particles.push(particle); - } - } - - createLockParticles(x, y, colour, len, height) { - for (let i = 0; i < this.volume / 4; i++) { - const posX = x + Math.random() * len - len / 2; - const posY = y + Math.random() * height; - const life = Math.random() * 15 + 30; - const dx = Math.random() * 1 - 0.5 + (posX - x) / 50; - const dy = Math.random() * -0.7 - 1.4; - - const clearParticle = { x: posX, y: posY, colour, size: this.size, life, dx, dy, xF: 0.96, yF: 0.96, gravity: 0.05 } - const particle = new Point(clearParticle, this.ctx); - this.particles.push(particle); - } - } - - createClearParticles(x, y, colour, len, height) { - for (let i = 0; i < this.volume; i++) { - const posX = x + Math.random() * len - const posY = y + Math.random() * height; - const life = Math.random() * 20 + 40; - const dx = Math.random() * 1.5 - 0.75 + (posX - x) / 200; - const dy = Math.random() * -0.8 - 1.5; - - const clearParticle = { x: posX, y: posY, colour, size: this.size, life, dx, dy, yF: 0.99, gravity: 0.1 } - const particle = new Point(clearParticle, this.ctx); - this.particles.push(particle); - } - } - - createPCParticles(x, y, len, height) { - for (let i = 0; i < this.volume; i++) { - const posX = x + Math.random() * len - const posY = y + Math.random() * height; - const life = Math.random() * 100 + 200; - const dx = Math.random() * 1.5 - 0.75; - const dy = Math.random() * -8 - 2; - const colour = `hsl(${Math.random() * 360}, 80%, 60%)` - - const pcParticle = { x: posX, y: posY, colour, size: this.size, life, dx, dy, xF: 0.98, yF: 0.98, twinkle: true, twinkleTime: 130 } - const particle = new Point(pcParticle, this.ctx); - this.particles.push(particle); - } - } - - createDangerBoardParticles(x, y, colour, len, height) { - for (let i = 0; i < this.volume / 10; i++) { - if (Math.random() > this.volume / 500) continue - const posX = x + Math.random() * len - const posY = y + Math.random() * height / 2; - const life = Math.random() * 100 + 200; - const dx = Math.random() * 1 - 0.5; - const dy = Math.random() * -1.2 - 2.4; - const sway = Math.random() * 0.005 - 0.0025; - - const dangerParticle = { x: posX, y: posY, colour, size: this.size, life, dx, dy, sway, swayF: 0.98 } - const particle = new Point(dangerParticle, this.ctx); - this.particles.push(particle); - } - } - - createDangerSidesParticles(x, y, colour, width, len, height) { - for (let i = 0; i < this.volume / 10; i++) { - if (Math.random() > this.volume / 250) continue - const direction = Math.random() > 0.5; - - const posX = (direction ? 0 : width) + x + Math.random() * len - const posY = y + Math.random() * height; - const life = Math.random() * 15 + 30; - const dx = (direction ? 1 : -1) * (Math.random() * 2 + 2); - const dy = Math.random() * 2 - 1; - - const dangerSideParticle = { x: posX, y: posY, colour, size: this.size, life, dx, dy, gravity: 0.05 } - const particle = new Point(dangerSideParticle, this.ctx); - this.particles.push(particle); - } - } - - createSpinParticles(x, y, colour, cw, len, height) { - len *= 0.5; - height *= 0.5; - for (let i = 0; i < this.volume / 3; i++) { - const posX = x + Math.random() * len - len / 2 - const posY = y + Math.random() * height - height / 2; - const life = Math.random() * 35 + 70; - let dx = Math.random() * 1 - 0.5 + (posY - y) / 30; - let dy = Math.random() * 1 - 0.5 + (posX - x) / 30; - if (cw) { dx *= -1 } else { dy *= -1 } - - const spinParticle = { x: posX, y: posY, colour, size: this.size, life, dx, dy, xF: 0.98, yF: 0.98 } - const particle = new Point(spinParticle, this.ctx); - this.particles.push(particle); - } - } - - createSpikeParticles(x, y, colour, len, height) { - for (let i = 0; i < this.volume / 2; i++) { - const posX = x + Math.random() * len - const posY = y + Math.random() * height / 2; - const life = Math.random() * 35 + 70; - const dx = Math.random() * 1 - 0.5; - const dy = Math.random() * 1 - 0.5; - - const spikeParticle = { x: posX, y: posY, colour, size: this.size, life, dx, dy, xF: 0.96, yF: 0.96, twinkle: true } - const particle = new Point(spikeParticle, this.ctx); - this.particles.push(particle); - } - } - - createBTBParticle(x, y, colour, width, len, height) { - for (let i = 0; i < this.volume * 2; i++) { - const direction = Math.random() > 0.5; - - const posX = (direction ? 0 : width) + x + Math.random() * len - const posY = y + Math.random() * height; - const life = Math.random() * 25 + 50; - const dx = (direction ? 1 : -1) * (Math.random() * 2); - const dy = Math.random() * 2 - 1; - - const BTBParticle = { x: posX, y: posY, colour, size: this.size, life, dx, dy, gravity: 0.15 } - const particle = new Point(BTBParticle, this.ctx); - this.particles.push(particle); - } - } - - clearParticles() { - this.particles = []; - } - - update() { - this.particles = this.particles.filter(p => p.life > 0); - this.particles.forEach(particle => { - particle.update(); - particle.draw(); - }); - } +import { Game } from "../game.js"; + +class Point { + constructor(particleInfo, particleInstance) { + const { x, y, colour, life, dx, dy, sway, xF, yF, swayF, gravity, twinkle, twinkleTime } = particleInfo; + this.x = x; + this.y = y; + this.maxSize = particleInstance.size; + this.size = this.maxSize; + this.colour = colour; + this.maxLife = life; + this.life = life; + this.dx = dx; + this.dy = dy; + this.sway = sway ?? 0; + this.frictionX = xF ?? 1; + this.frictionY = yF ?? 1; + this.frictionSway = swayF ?? 1; + this.gravity = gravity ?? 0; + this.twinkle = twinkle ?? false; + this.twinkleTime = twinkleTime ?? this.life; + + this.particle = particleInstance.game.pixi.createParticleSprite() + this.particle.tint = colour; + particleInstance.container.addChild(this.particle); + particleInstance.particles.push(this); + } + + draw() { + this.particle.alpha = Math.max(0, this.life / this.maxLife); + this.particle.x = this.x; + this.particle.y = this.y; + this.particle.scale.set(0.5 * this.size) + } + + update() { + this.life -= 1; + this.x += this.dx + Math.sin(this.sway * this.life); + this.y += this.dy; + this.dx *= this.frictionX; + this.dy *= this.frictionY; + this.sway *= this.frictionSway + this.dy += this.gravity; + if (this.twinkle && this.life < this.twinkleTime) + this.size = Math.abs((Math.sin(this.life / 15))) * this.maxSize; + } +} + +export class Particles { + /** @type {Point[]} */ + particles = []; + + /** + * @param {Game} game + */ + constructor(game) { + this.game = game; + } + + initBoard() { + this.container = this.game.pixi.app.stage.getChildByLabel("particles"); + } + + spawnParticles(posX, posY, type, pieceWidth = 1, cw = false, colour = "white") { + if (!this.game.settings.display.particles) return; + + const boardWidth = this.game.pixi.width; + const boardHeight = this.game.pixi.height * 2; + const minosize = this.game.pixi.minoSize; + const [x, y] = [posX * minosize, (40 - posY) * minosize]; + this.volume = this.game.settings.display.particleVolume; + this.size = this.game.settings.display.particleSize; + if (type == "drop") this.creatDropParticles(x, y, colour, minosize * pieceWidth, -boardHeight); + if (type == "lock") this.createLockParticles(x, y, colour, minosize * pieceWidth, 10); + if (type == "clear") this.createClearParticles(x, y, colour, boardWidth, -10); + if (type == "pc") this.createPCParticles(x, y, boardWidth, 10); + if (type == "dangerboard") this.createDangerBoardParticles(x, boardHeight, colour, boardWidth, 10); + if (type == "dangersides") this.createDangerSidesParticles(x, y, "red", boardWidth, 0, 1); + if (type == "spin") this.createSpinParticles(x, y, colour, cw, minosize * pieceWidth, -minosize * pieceWidth); + if (type == "spike") this.createSpikeParticles(x, boardHeight, colour, boardWidth, -boardHeight); + if (type == "BTB") this.createBTBParticle(x, y, "gold", boardWidth, 0, boardHeight); + } + + creatDropParticles(x, y, colour, len, height) { + for (let i = 0; i < this.volume / 3; i++) { + const posX = x + Math.random() * len + const posY = y + Math.random() * height / 2; + const life = Math.random() * 35 + 70; + const dx = Math.random() * 0.8 - 0.2; + const dy = Math.random() * -1.2 - 2.4; + const sway = Math.random() * 0.04 - 0.02; + + const placeParticle = { x: posX, y: posY, colour, life, dx, dy, sway, xF: 0.93, yF: 0.93, swayF: 0.93 } + new Point(placeParticle, this); + } + } + + createLockParticles(x, y, colour, len, height) { + for (let i = 0; i < this.volume / 4; i++) { + const posX = x + Math.random() * len; + const posY = y + Math.random() * height; + const life = Math.random() * 15 + 30; + const dx = Math.random() * 1 - 0.5 + (posX - x - len / 2) / 30 + const dy = Math.random() * -0.7 - 1.4; + + const clearParticle = { x: posX, y: posY, colour, life, dx, dy, xF: 0.96, yF: 0.96, gravity: 0.05 } + new Point(clearParticle, this); + } + } + + createClearParticles(x, y, colour, len, height) { + for (let i = 0; i < this.volume; i++) { + const posX = x + Math.random() * len + const posY = y + Math.random() * height; + const life = Math.random() * 20 + 40; + const dx = Math.random() * 1.5 - 0.75 + (posX - x - len / 2) / 150; + const dy = Math.random() * -0.8 - 1.5; + + const clearParticle = { x: posX, y: posY, colour, life, dx, dy, yF: 0.99, gravity: 0.1 } + new Point(clearParticle, this); + } + } + + createPCParticles(x, y, len, height) { + for (let i = 0; i < this.volume; i++) { + const posX = x + Math.random() * len + const posY = y + Math.random() * height; + const life = Math.random() * 100 + 200; + const dx = Math.random() * 1.5 - 0.75; + const dy = Math.random() * -8 - 2; + const colour = `hsl(${Math.random() * 360}, 80%, 60%)` + + const pcParticle = { x: posX, y: posY, colour, life, dx, dy, xF: 0.98, yF: 0.98, twinkle: true, twinkleTime: 130 } + new Point(pcParticle, this); + } + } + + createDangerBoardParticles(x, y, colour, len, height) { + for (let i = 0; i < this.volume / 10; i++) { + if (Math.random() > this.volume / 500) continue + const posX = x + Math.random() * len + const posY = y + Math.random() * height / 2; + const life = Math.random() * 100 + 200; + const dx = Math.random() * 1 - 0.5; + const dy = Math.random() * -1.2 - 2.4; + const sway = Math.random() * 0.005 - 0.0025; + + const dangerParticle = { x: posX, y: posY, colour, life, dx, dy, sway, swayF: 0.98 } + new Point(dangerParticle, this); + } + } + + createDangerSidesParticles(x, y, colour, width, len, height) { + for (let i = 0; i < this.volume / 10; i++) { + if (Math.random() > this.volume / 250) continue + const direction = Math.random() > 0.5; + + const posX = (direction ? 0 : width) + x + Math.random() * len + const posY = y + Math.random() * height; + const life = Math.random() * 15 + 30; + const dx = (direction ? 1 : -1) * (Math.random() * 2 + 2); + const dy = Math.random() * 2 - 1; + + const dangerSideParticle = { x: posX, y: posY, colour, life, dx, dy, gravity: 0.05 } + new Point(dangerSideParticle, this); + } + } + + createSpinParticles(x, y, colour, cw, len, height) { + len *= 0.5; + height *= 0.5; + for (let i = 0; i < this.volume / 5; i++) { + const posX = x + Math.random() * len + const posY = y + Math.random() * height + const life = Math.random() * 35 + 70; + let dx = Math.random() * 1 - 0.5 + (posY - y) / 50; + let dy = Math.random() * 1 - 0.5 + (posX - x) / 50; + if (cw) { dx *= -1 } else { dy *= -1 } + + const spinParticle = { x: posX, y: posY, colour, life, dx, dy, xF: 0.98, yF: 0.98 } + new Point(spinParticle, this); + } + } + + createSpikeParticles(x, y, colour, len, height) { + for (let i = 0; i < this.volume / 2; i++) { + const posX = x + Math.random() * len + const posY = y + Math.random() * height / 2; + const life = Math.random() * 35 + 70; + const dx = Math.random() * 1 - 0.5; + const dy = Math.random() * 1 - 0.5; + + const spikeParticle = { x: posX, y: posY, colour, life, dx, dy, xF: 0.96, yF: 0.96, twinkle: true } + new Point(spikeParticle, this); + } + } + + createBTBParticle(x, y, colour, width, len, height) { + for (let i = 0; i < this.volume * 2; i++) { + const direction = Math.random() > 0.5; + + const posX = (direction ? 0 : width) + x + Math.random() * len + const posY = y + Math.random() * height; + const life = Math.random() * 25 + 50; + const dx = (direction ? -1 : 1) * (Math.random() * 2); + const dy = Math.random() * 2 - 1; + + const BTBParticle = { x: posX, y: posY, colour, life, dx, dy, gravity: 0.15 } + new Point(BTBParticle, this); + } + } + + clearParticles() { // there was a memory leak... so i fixed it + if (this.container == undefined) return; + const c = [...this.container.children] + c.forEach(child => { child.destroy(); this.container.removeChild(child); }); + this.particles = new Array(); + } + + update() { + this.particles.forEach(particle => { + if (particle.life <= 0) { // something here worked to fix memory leak + this.container.removeChild(particle.particle); + // particle.particle.destroy(); + } + }) + this.particles = this.particles.filter(p => p.life > 0); + this.particles.forEach(particle => { + particle.update(); + particle.draw(); + }); + } } \ No newline at end of file diff --git a/src/display/pixirender.js b/src/display/pixirender.js new file mode 100644 index 0000000..5604c32 --- /dev/null +++ b/src/display/pixirender.js @@ -0,0 +1,688 @@ +import { Game } from '../game.js'; +import { defaultSkins } from '../data/data.js'; +import blocksprites from '../data/blocksprites.json' with { type: 'json' }; +import { clearSplash } from '../main.js'; + +export class PixiRender { + textures = {}; + minoSize; + width; + height; + boardAlpha = 1; + queueAlpha = 1; + justPlacedCoords = []; + justPlacedAlpha = 1; + shadowSprites = {}; + editButtonVisible = false; + currentlyFlashing = {} + + divlock = document.getElementById("lockTimer"); + + /** + * @param {Game} game + */ + constructor(game) { + this.game = game; + } + + async init() { + this.app = new PIXI.Application(); + await this.app.init({ backgroundAlpha: 0, resizeTo: window, autoDensity: true }); + document.body.prepend(this.app.canvas); + + // grid + const grid = new PIXI.Container(); + this.app.stage.addChild(grid); + grid.label = "grid"; + + // board + this.board = new PIXI.Container(); + this.app.stage.addChild(this.board); + this.board.label = "board"; + + const clickArea = new PIXI.Container(); + this.app.stage.addChild(clickArea); + clickArea.label = "clickArea"; + + const next = new PIXI.Container(); + this.app.stage.addChild(next); + next.label = "next"; + + const hold = new PIXI.Container(); + this.app.stage.addChild(hold); + hold.label = "hold"; + + const textContainer = new PIXI.Container(); + this.app.stage.addChild(textContainer); + textContainer.label = "textContainer"; + + // particles + const particles = new PIXI.Container(); + this.app.stage.addChild(particles); + particles.label = "particles"; + this.game.particles.initBoard(); + + // init + await this.generateTextures(); + this.resize(); + this.generateAllSprites("board", this.game.board.boardState, 39, [0, 0]); + this.generateAllSprites("hold", this.game.renderer.holdQueueGrid, 2, [0, 0]); + this.generateAllSprites("next", this.game.renderer.nextQueueGrid, 15, [0, 0]); + this.game.renderer.updateNext(); + this.game.renderer.updateHold(); + + this.app.ticker.add(time => this.tick(time)); + + gsap.registerPlugin(PixiPlugin); + PixiPlugin.registerPIXI(PIXI); + } + + resize() { + const grid = this.app.stage.getChildByLabel("grid"); + const next = this.app.stage.getChildByLabel("next"); + const hold = this.app.stage.getChildByLabel("hold"); + const board = this.app.stage.getChildByLabel("board"); + const clickArea = this.app.stage.getChildByLabel("clickArea"); + const particles = this.app.stage.getChildByLabel("particles"); + const textContainer = this.app.stage.getChildByLabel("textContainer"); + + // clear + grid.children.forEach(child => child.destroy()); + grid.removeChildren(); + clickArea.children.forEach(child => child.destroy()); + clickArea.removeChildren(); + textContainer.removeChildren(); + + // resize + const scale = Number(this.game.settings.display.boardHeight) / 100; + let height = this.app.screen.height * 0.6 * scale; // equivilant to 60vh + let width = height / 2; + const screenHeight = Math.floor(this.app.screen.height / 2); + const screenWidth = Math.floor(this.app.screen.width / 2); + this.minoSize = height / 20; + this.width = width; + this.height = height; + + // board + const rect = new PIXI.Graphics().rect(0, 0, width, height * 2) + board.addChild(rect); + board.x = screenWidth; + board.y = screenHeight; + board.pivot.x = width / 2; + board.pivot.y = height * 3 / 2; + + + + // hold + const clickRectHold = new PIXI.Graphics().rect(0, 0, width * 2 / 5, height * 3 / 20).fill("transparent"); + clickRectHold.interactive = true; + clickRectHold.cursor = 'pointer' + clickRectHold.on("pointerdown", () => this.game.modals.openModal("queueModify")); + clickRectHold.label = "invincible" + hold.addChild(clickRectHold); + hold.x = screenWidth; + hold.y = screenHeight + height * 2 / 20; + hold.pivot.x = width / 2 + width * 2 / 5; + hold.pivot.y = height / 2; + + // next + const clickRectNext = new PIXI.Graphics().rect(0, 0, width * 2 / 5, height * 16 / 20).fill("transparent"); + clickRectNext.interactive = true; + clickRectNext.cursor = 'pointer' + clickRectNext.on("pointerdown", () => this.game.modals.openModal("queueModify")); + clickRectNext.label = "invincible" + next.addChild(clickRectNext); + next.x = screenWidth; + next.y = screenHeight + height * 1 / 20; + next.pivot.x = width / 2 - width * 11 / 10; + next.pivot.y = height / 2; + + const rect2 = new PIXI.Graphics().rect(0, 0, width, height).fill("transparent"); + rect2.interactive = true; + rect2.cursor = 'pointer' + rect2.on("pointerdown", () => console.log("click")); + clickArea.addChild(rect2); + clickArea.x = screenWidth; + clickArea.y = screenHeight; + clickArea.pivot.x = width / 2; + clickArea.pivot.y = height / 2; + + // grid and outline + const { textHold, textNext } = this.textGraphics(width); + const { settings, reset, edit } = this.buttonGraphics(width); + + this.boardDanger = new PIXI.Graphics() + .rect(0, 0, width, height) + .fill("red") + this.boardDanger.alpha = 0; + grid.addChild(this.boardDanger); + + this.border = new PIXI.Graphics() + .lineTo(0, height).lineTo(width, height).lineTo(width, 0) + .stroke({ color: 0xffffff, width: 2, alignment: 0.5 }) + grid.addChild(this.border); + + const rectHold = new PIXI.Graphics() + .moveTo(0, height * 1 / 4).lineTo(width * 2 / 5, height * 1 / 4) + .stroke({ color: 0xffffff, width: 1 }) + grid.addChild(rectHold); + rectHold.x = - width * 2 / 5; + grid.addChild(textHold); + + const rectNext = new PIXI.Graphics() + .moveTo(0, height * 17 / 20).lineTo(width * 2 / 5, height * 17 / 20) + .stroke({ color: 0xffffff, width: 1 }) + grid.addChild(rectNext); + rectNext.x = width; + grid.addChild(textNext); + + grid.addChild(reset); + grid.addChild(settings); + grid.addChild(edit); + this.editButton = edit; + + grid.x = screenWidth; + grid.y = screenHeight; + grid.pivot.x = width / 2; + grid.pivot.y = height / 2; + + // particles + particles.x = screenWidth; + particles.y = screenHeight; + particles.pivot.x = width / 2; + particles.pivot.y = height * 3 / 2; + + // action text + textContainer.x = screenWidth; + textContainer.y = screenHeight; + textContainer.pivot.x = width / 2; + textContainer.pivot.y = height / 2; + + textContainer.addChild(this.actionTexts.cleartext.sprite) + textContainer.addChild(this.actionTexts.combotext.sprite) + textContainer.addChild(this.actionTexts.btbtext.sprite) + textContainer.addChild(this.actionTexts.spiketext.sprite) + textContainer.addChild(this.actionTexts.pctext.sprite) + textContainer.addChild(this.actionTexts.timeleft.sprite) + this.statTexts.forEach((text) => { + textContainer.addChild(text.stat); + textContainer.addChild(text.statText); + textContainer.addChild(text.statSecondary); + }); + textContainer.addChild(this.objectiveTexts[0]) + textContainer.addChild(this.objectiveTexts[1]) + + this.generateGrid(); + this.resetAnimGraphic(); + this.generateClickMinos(clickArea); + } + + // TEXT + textGraphics(width) { + const style = new PIXI.TextStyle({ fontFamily: "Major Mono Display", fontSize: 18, fill: 0xffffff, fontWeight: "bold", letterSpacing: 1 }); + + const textHold = new PIXI.Text({ text: "hold", style }); + textHold.x = - width * 3.5 / 10; + textHold.resolution = 2; + + const textNext = new PIXI.Text({ text: "next", style }); + textNext.x = width * 11 / 10; + textNext.resolution = 2; + + const actionStyle = new PIXI.TextStyle({ fontFamily: "Major Mono Display", fontSize: 24, fill: "white", fontWeight: "bold" }); + const actionText = (pos) => { + const text = new PIXI.Text({ text: "", style: actionStyle }); + text.resolution = 2; + text.alpha = 0; + text.y = pos * width * 1.5 / 10 + width * 3 / 5; + text.anchor.x = 1; + text.text = "" + return { sprite: text, animation: gsap.timeline() }; + } + + const pcstyle = new PIXI.TextStyle({ fontFamily: "Major Mono Display", fontSize: 24, fill: "yellow", fontWeight: "bold", stroke: { color: "orange", width: 2 } }); + const pcText = new PIXI.Text({ text: "perfect \n clear", style: pcstyle }); + pcText.resolution = 2; + pcText.alpha = 0; + pcText.x = this.width / 2; + pcText.y = this.height / 2; + pcText.pivot.x = pcText.width / 2 - 10; + pcText.pivot.y = pcText.height / 2; + + const timeLeftStyle = new PIXI.TextStyle({ fontFamily: "Montserrat", fontSize: 20, fill: "gold", fontWeight: "bold" }); + const timeLeftText = new PIXI.Text({ text: "", style: timeLeftStyle }); + timeLeftText.resolution = 2; + timeLeftText.alpha = 0; + timeLeftText.x = this.width / 2; + timeLeftText.y = this.height * 1 / 8; + timeLeftText.anchor.x = 0.5; + + const spiketext = actionText(3); + spiketext.sprite.x = this.width / 2; + spiketext.sprite.y = this.height * 1 / 4; + spiketext.sprite.anchor.x = 0.5; + spiketext.sprite.anchor.y = 0.5; + + this.actionTexts = { + cleartext: actionText(0), + combotext: actionText(1), + btbtext: actionText(2), + spiketext, + pctext: { sprite: pcText, animation: gsap.timeline() }, + timeleft: { sprite: timeLeftText, animation: gsap.timeline() } + } + + const statStyle = new PIXI.TextStyle({ fontFamily: "Major Mono Display", fontSize: 16, fill: "white", fontWeight: "bold" }); + const statTextStyle = new PIXI.TextStyle({ fontFamily: "Montserrat", fontSize: 18, fill: "#999999" }); + const statSecondaryStyle = new PIXI.TextStyle({ fontFamily: "Major Mono Display", fontSize: 26, fill: "white", fontWeight: "bold" }); + const statText = (pos) => { + const stat = new PIXI.Text({ text: "", style: statStyle }); + stat.resolution = 2; + stat.position.set(-width * 1 / 20, this.height - pos * this.height * 2.5 / 20) + stat.anchor.x = 1; + const statText = new PIXI.Text({ text: "", style: statTextStyle }); + statText.resolution = 2; + statText.position.set(-width * 1 / 20, this.height - pos * this.height * 2.5 / 20 - this.height * 2 / 40) + statText.anchor.x = 1; + const statSecondary = new PIXI.Text({ text: "", style: statSecondaryStyle }); + statSecondary.resolution = 2; + statSecondary.position.set(-width * 7 / 20, this.height - pos * this.height * 2.5 / 20 - this.height * 1 / 40) + statSecondary.anchor.x = 1; + return { stat, statText, statSecondary }; + } + + this.statTexts = [ + statText(0.5), + statText(1.5), + statText(2.5) + ] + + const objectiveText = new PIXI.Text({ text: "", style: statSecondaryStyle }); + objectiveText.resolution = 2; + objectiveText.position.set(width * 11 / 10, this.height - this.height * 3 / 40) + + const objectiveNameText = new PIXI.Text({ text: "", style: statTextStyle }); + objectiveNameText.resolution = 2; + objectiveNameText.position.set(width * 11 / 10, this.height - this.height * 5 / 40) + + this.objectiveTexts = [objectiveText, objectiveNameText]; + + return { textHold, textNext }; + } + + resetActionTexts() { + Object.keys(this.actionTexts).forEach(key => { + this.actionTexts[key].animation.pause(); + gsap.to(this.actionTexts[key].sprite, { + duration: 0.2, pixi: { alpha: 0 }, + onComplete: () => this.actionTexts[key].animation.kill() + }) + }) + if (this.timeLeftTextSplit) this.timeLeftTextSplit.forEach(s => { + s.animation.kill(); + s.sprite.destroy(); + }); + this.timeLeftTextSplit = undefined; + } + + showActionText(type, message) { + this.actionTexts[type].animation.kill(); + const text = this.actionTexts[type].sprite; + text.text = message; + this.actionTexts[type].animation = gsap.timeline({ onComplete: () => this.game.mechanics.spikeCounter = 0 }) + .to(text, { duration: 0.2, pixi: { alpha: 1, x: - this.width * 1 / 10, scaleX: 1 }, ease: "power1.inOut" }) + .to(text, { duration: 2, pixi: { alpha: 1, scaleX: 1.1 } }) + .to(text, { duration: 0.2, pixi: { alpha: 0, x: 0 }, ease: "power1.inOut" }) + } + + showSpikeText(num) { + this.actionTexts.spiketext.animation.kill(); + const { colour, power } = this.spikeTextStyle(); + const text = this.actionTexts.spiketext.sprite; + text.text = num; + this.actionTexts.spiketext.animation = gsap.timeline() + .to(text, { duration: 0.1, pixi: { alpha: 0.8, tint: colour, scale: (1 + power / 8) } }) + .to(text, { duration: (1 + power / 8) / power, pixi: { alpha: 0.2 }, ease: "power2.in", repeat: power - 1 }) + .to(text, { duration: 0.2, pixi: { alpha: 0 }, ease: "power1.inOut" }) + } + + spikeTextStyle() { + if (this.game.mechanics.spikeCounter >= 20) return { colour: "#fad9f7", power: 12 }; + if (this.game.mechanics.spikeCounter >= 15) return { colour: "#7ac9fa", power: 8 }; + if (this.game.mechanics.spikeCounter >= 10) return { colour: "#faa823", power: 4 }; + return { colour: "white", power: 1 }; + } + + showPCText() { + this.actionTexts.pctext.animation.kill(); + const pc = this.actionTexts.pctext.sprite; + this.actionTexts.pctext.animation = gsap.timeline() + .to(pc, { duration: 0, pixi: { alpha: 1, scale: 0, rotation: -180 } }) + .to(pc, { duration: 0.5, pixi: { scale: 1.6, rotation: 360 }, ease: "power1.inOut" }) + .to(pc, { duration: 3.5 / 15, pixi: { tint: "orange" }, ease: "power1.inOut", repeat: 15, yoyo: true }) + .to(pc, { duration: 2.5, pixi: { scale: 0, alpha: 0 }, ease: "power3.in" }, "1.5") + } + + showTimeLeftText(msg) { + const textContainer = this.app.stage.getChildByLabel("textContainer"); + this.actionTexts.timeleft.animation.kill(); + const text = this.actionTexts.timeleft.sprite; + text.text = msg; + const split = this.splitSprite(text) + this.timeLeftTextSplit = split.map((s, i) => { + textContainer.addChild(s); + const animation = gsap.timeline({ onComplete: () => s.destroy() }) + .to(s, { duration: 0, pixi: { alpha: 1, x: s.x, tint: "white" } }) + .to(s, { duration: 3, pixi: { x: s.x + 8 * (i - split.length / 2) } }) + .to(s, { duration: 3 / 20, pixi: { tint: "red" }, repeat: 20, yoyo: true, ease: "none" }, "0") + .to(s, { duration: 0.5, pixi: { alpha: 0 } }, "2.5") + return { sprite: s, animation } + }); + } + + // GRAPHICS and GENERATORS + buttonGraphics(width) { + const iconframe = (texture, scale, y) => { + const icon = new PIXI.Sprite(texture) + icon.scale.set(scale) + icon.x = width * 1.525 + icon.y = y + icon.interactive = true + icon.cursor = 'pointer' + icon.alpha = 0.6 + icon.on("pointerover", () => gsap.to(icon, { duration: 0.1, pixi: { alpha: 1, scale: scale * 1.1 }, ease: "power1.inOut" })) + icon.on("pointerout", () => gsap.to(icon, { duration: 0.3, pixi: { alpha: 0.6, scale: scale }, ease: "power1.inOut" })) + return icon + } + + const reset = iconframe(this.resetIcon, 0.23, 0) + reset.on("pointerdown", () => this.game.controls.retry(true)); + const settings = iconframe(this.settingsIcon, 0.18, width * 3 / 20) + settings.on("pointerdown", () => this.game.modals.openModal("settingsPanel")); + const edit = iconframe(this.editIcon, 0.21, width * 6 / 20) + edit.on("pointerdown", () => this.game.modals.openModal("editMenu")); + edit.visible = this.editButtonVisible + + return { settings, reset, edit }; + } + + toggleEditButton(bool) { + this.editButtonVisible = bool + this.editButton.visible = this.editButtonVisible; + } + + resetAnimGraphic() { + if (this.resetTriangle) this.resetTriangle.destroy(); + const triangleGraphic2 = new PIXI.Graphics().poly([0, 0, 100, 0, 0, 100]).fill(0xffffff); + triangleGraphic2.rotation = Math.PI * 3 / 2 + triangleGraphic2.y = this.height * 2 + triangleGraphic2.label = "invincible" + this.resetTriangle = triangleGraphic2; + + if (this.resetMask) this.resetMask.destroy(); + const maskTriangle = new PIXI.Graphics().poly([-this.height, 0, this.width, 0, this.width, this.height + this.width]).fill(0xffffff, 0.4); + maskTriangle.x = this.width; + maskTriangle.y = this.height; + maskTriangle.pivot.x = this.width; + maskTriangle.label = "invincible"; + this.resetMask = maskTriangle; + } + + async generateTextures() { + let url = this.game.settings.display.skin; + if (defaultSkins.includes(url)) url = `./assets/skins/${url}.png`; + const texture = await PIXI.Assets.load(url); + const spritesheet = new PIXI.Spritesheet(texture, blocksprites); + await spritesheet.parse(); + this.textures = spritesheet.textures; + + this.settingsIcon = await PIXI.Assets.load('./assets/icons/settings.svg'); + this.resetIcon = await PIXI.Assets.load('./assets/icons/reset.svg'); + this.editIcon = await PIXI.Assets.load('./assets/icons/edit.svg'); + + const triangleGraphic = new PIXI.Graphics().poly([0, 0, 10, 0, 0, 10]).fill(0xffffff, 0.4); + this.triangle = this.app.renderer.generateTexture(triangleGraphic); + + this.game.particles.texture = await PIXI.Assets.load('./assets/particle.png'); + clearSplash(); + } + + generateAllSprites(type, array, yPosChange, [dx, dy] = [0, 0]) { + const container = this.app.stage.getChildByLabel(type); + const shadowArray = []; + array.forEach((row, y) => { + const shadowRow = [] + row.forEach((col, x) => { + const posX = x * this.minoSize; + const posY = (yPosChange - y) * this.minoSize; + const sprite = new PIXI.Sprite(this.textures['g']); + sprite.position.set(posX + dx, posY + dy); + sprite.setSize(this.minoSize); + sprite.visible = false; + sprite.label = "invincible"; + shadowRow.push(sprite); + container.addChild(sprite); + }); + shadowArray.push(shadowRow); + }); + this.shadowSprites[type] = shadowArray + } + + generateClickMinos(clickArea) { + for (let y = 0; y < 20; y++) { + for (let x = 0; x < 10; x++) { + const mino = new PIXI.Sprite(this.textures['g']); + mino.interactive = true; + mino.on("mousedown", () => this.game.boardeditor.mouseDown([x, y], mino)); + mino.on("mouseenter", () => this.game.boardeditor.mouseEnter([x, y], mino)); + mino.on("mouseleave", () => this.game.boardeditor.mouseLeave([x, y], mino)); + clickArea.addChild(mino); + mino.position.set(x * this.minoSize, y * this.minoSize); + mino.setSize(this.minoSize); + mino.alpha = 0; + } + } + } + + generateGrid() { + if (this.game.settings.display.showGrid === false) return; + const grid = this.app.stage.getChildByLabel("grid"); + const type = this.game.settings.display.gridType; + const opacity = this.game.settings.display.gridopacity / 100; + const gridGraphic = new PIXI.Graphics(); + + if (type == "square") { + gridGraphic.rect(0, 0, this.minoSize, this.minoSize) + .stroke({ color: 0xffffff, width: 1, alpha: opacity }) + } else if (type == "round") { + gridGraphic.roundRect(0, 0, this.minoSize, this.minoSize, 8) + .stroke({ color: 0xffffff, width: 1, alpha: opacity }) + } else if (type == "dot") { + gridGraphic.circle(2, 2, 1) + .fill(0xffffff, opacity) + } + const texture = this.app.renderer.generateTexture(gridGraphic); + + for (let y = 0; y < 20; y++) { + for (let x = 0; x < 10; x++) { + const gridSquare = new PIXI.Sprite(texture); + gridSquare.x = x * this.minoSize; + gridSquare.y = y * this.minoSize; + grid.addChild(gridSquare); + } + } + } + + // RENDER CLOCK + tick(time) { + this.game.controls.runKeyQueue(); + this.render("board", this.game.board.boardState); + this.game.boardeffects.move(0, 0); + this.game.boardeffects.rotate(0); + this.game.particles.update(); + this.game.renderer.dangerParticles(); + this.updateAlpha(); + if (this.game.settings.game.gamemode == "ultra" && Math.floor(this.game.stats.time) == 60) this.game.renderer.renderTimeLeft("60S LEFT"); + if (this.game.settings.game.gamemode == "ultra" && Math.floor(this.game.stats.time) == 90) this.game.renderer.renderTimeLeft("30S LEFT"); + } + + // RENDERING + render(type, array) { + const container = this.app.stage.getChildByLabel(type); + const shadowArray = this.shadowSprites[type]; + + [...container.children].forEach(child => { + if (child.label.split(" ")[0] == "invincible") return; + child.destroy(); // fixed a memory leak (-2 hours) + container.removeChild(child); + }); + + array.forEach((row, y) => { + row.forEach((col, x) => { + const cell = col.split(" "); + const sprite = shadowArray[y][x]; + sprite.visible = false; + if (cell.includes("A") || cell.includes("S")) { // active or stopped piece + sprite.visible = true; + sprite.texture = this.getTexture(type, cell); + sprite.alpha = this.getOpacity(cell, type, x, y) ?? this.queueAlpha; + } else if (cell.includes("NP") && this.game.renderer.inDanger) { // next piece overlay + sprite.visible = true; + sprite.texture = this.textures["topout"]; + sprite.alpha = 0.32; + } else if (cell.includes("Sh")) { // shadow piece + const pieceName = this.game.settings.display.colouredShadow ? this.game.falling.piece.name : "shadow"; + sprite.visible = true; + sprite.texture = this.textures[pieceName]; + sprite.alpha = this.getShadowOpacity(); + } + }); + }); + } + + getTexture(type, cell) { + const piece = this.game.hold.occured && type == "hold" ? "hold" : cell[1].toLowerCase() + return this.textures[piece]; + } + + flash(coords) { + coords.forEach(([x, y]) => { + const triangle = new PIXI.Sprite(this.triangle); + triangle.x = x * this.minoSize; + triangle.y = (39 - y) * this.minoSize; + triangle.label = "invincible"; + this.board.addChild(triangle); + this.currentlyFlashing[`${x},${y}`] = gsap.timeline({ onComplete: () => this.board.removeChild(triangle) }) + .to(triangle, { duration: 0, pixi: { width: this.minoSize, height: this.minoSize } }) + .to(triangle, { duration: 0.15, pixi: { width: 0, height: 0 }, ease: "power1.inOut", }) + }) + } + + endFlash([x, y]) { + const a = this.currentlyFlashing[`${x},${y}`]; + a.totalProgress(1).kill(); + } + + getOpacity(cell, type, x, y) { + if (type != "board") return; + if (this.divlock.value != 0 && cell.includes("A") && this.game.settings.game.gamemode != "lookahead") { + return 1 - (this.divlock.value / 250); + } + if (this.game.settings.game.gamemode == "lookahead") { + for (let [posX, posY] of this.justPlacedCoords) { + if (posX == x && posY == y) { + return Math.max(this.justPlacedAlpha, this.boardAlpha).toFixed(2); + } + } + } + return this.boardAlpha.toFixed(2); + } + + getShadowOpacity() { + const opacity = this.game.settings.display.shadowOpacity / 100; + if (this.game.settings.game.gamemode == "lookahead") return (opacity * this.boardAlpha).toFixed(2); + return opacity; + } + + updateAlpha() { + if (this.game.settings.game.gamemode != 'lookahead') return; + const update = (type, amount) => { + if (this.game.stats.checkInvis()) { + if (this[type] <= 0) { + this[type] = 1; + this.game.renderer.updateNext(); + this.game.renderer.updateHold(); + } + } else { + if (this[type] > 0) { + this[type] += -amount / this.game.tickrate; + this.game.renderer.updateNext(); + this.game.renderer.updateHold(); + } else { + this[type] = 0; + } + } + } + update("boardAlpha", 3) + update("queueAlpha", 3) + update("justPlacedAlpha", 6) + } + + createParticleSprite() { + return new PIXI.Sprite(this.game.particles.texture); + } + + toggleDangerBG(danger) { + gsap.to(this.boardDanger, { duration: 0.2, pixi: { alpha: danger ? 0.1 : 0 } }); + gsap.to(this.border, { duration: 0.2, pixi: { tint: danger ? "red" : "none" } }); + } + + resetAnimation() { + this.board.mask = this.resetMask; + this.board.addChild(this.resetMask); + this.board.addChild(this.resetTriangle); + this.game.stopGameTimers(); + this.game.controls.resetting = true; + + const animateOpacity = () => { + gsap.timeline() + .to(this.board, { duration: 0, pixi: { alpha: 0 } }) + .to(this.board, { duration: 0.2, pixi: { alpha: 1 } }) + } + + gsap.timeline({ onComplete: () => { this.endResetAnimation(); animateOpacity(); } }) + .to(this.resetMask, { duration: 0, pixi: { scale: 1 } }) + .to(this.resetMask, { duration: 0.4, pixi: { scale: 0 }, ease: "power1.inOut", }) + + gsap.timeline() + .to(this.resetTriangle, { duration: 0, pixi: { scale: 0 } }) + .to(this.resetTriangle, { duration: 0.4, pixi: { scale: 9 }, ease: "power1.inOut", }) + } + + endResetAnimation() { + this.game.startGame(); + this.game.controls.resetting = false; + this.board.mask = null; + this.board.removeChild(this.resetMask); + this.board.removeChild(this.resetTriangle); + } + + /** @param {PIXI.Text} textSprite */ + splitSprite(textSprite) { + const target = textSprite; + const textContent = textSprite.text; + let currentX = target.x - target.width / 2; + target.text = ""; + const textChars = textContent.split(""); + /**@type {PIXI.Text[]} */ + let chars = [] + textChars.forEach((char) => { + const charSprite = new PIXI.Text({ text: char, style: target.style }); + charSprite.x = currentX; + charSprite.y = target.y; + currentX += charSprite.width; + chars.push(charSprite); + }); + return chars + } +} diff --git a/src/display/renderBoard.js b/src/display/renderBoard.js deleted file mode 100644 index f62d7e1..0000000 --- a/src/display/renderBoard.js +++ /dev/null @@ -1,160 +0,0 @@ -import { Game } from "../game.js"; - -export class BoardRenderer { - boardAlpha = 1; - queueAlpha = 1; - justPlacedCoords = []; - justPlacedAlpha = 1; - minoSize; - texture; - flashTimes = []; - - divlock = document.getElementById("lockTimer"); - - /** - * - * @param {Game} game - */ - constructor(game) { - this.game = game; - } - - // board rendering - getOpacity(cell, cntx, x, y) { - if (cntx != this.game.renderer.ctx) return; - if (this.divlock.value != 0 && cell.includes("A") && this.game.settings.game.gamemode != "lookahead") { - return 1 - (this.divlock.value / 250); - } - if (this.game.settings.game.gamemode == "lookahead") { - for (let [posX, posY] of this.justPlacedCoords) { - if (posX == x && posY == y && cntx == this.game.renderer.ctx) { - return Math.max(this.justPlacedAlpha, this.boardAlpha).toFixed(2); - } - } - } - - return this.boardAlpha.toFixed(2); - } - - setMinoFlash(cntx, x, y, posX, posY) { - if (cntx != this.game.renderer.ctx) return; - for (let { c, t } of this.flashTimes) { - if (c[0] == x && c[1] == y) { - const dx = (t / 15) * this.minoSize; - posX = posX + this.minoSize; - posY = posY + this.minoSize; - this.drawTriangle(posX, posY, posX - dx, posY - dx, cntx); - } - } - } - - drawTriangle(posX, posY, x, y, cntx) { - cntx.globalAlpha = 0.4; - cntx.fillStyle = "#ffffff" - cntx.beginPath(); - cntx.moveTo(posX, posY); - cntx.lineTo(posX, y); - cntx.lineTo(x, posY); - cntx.lineTo(posX, posY); - cntx.fill(); - } - - toHex(num) { - const hex = Math.round((+num * 255) / 100).toString(16); - return hex.length > 1 ? hex : 0 + hex; - } - - loadImage(src) { - this.texture = new Image(372, 30); - this.texture.src = src; - this.texture.onload = () => { - this.game.renderer.updateNext(); - } - } - - getPiece(cntx, cell) { - return this.game.hold.occured && cntx == this.game.renderer.ctxH ? "hold" : cell; - } - - getTexture(name) { - const pieces = ["z", "l", "o", "s", "i", "j", "t", "shadow", "hold", "g", "darkg", "topout"] - const x = pieces.indexOf(name.toLowerCase()) * 31; - const y = 0; - const width = 30; - const height = 30; - return { x, y, width, height }; - } - - getShadowOpacity() { - const opacity = this.game.settings.display.shadowOpacity / 100; - if (this.game.settings.game.gamemode == "lookahead") return (opacity * this.boardAlpha).toFixed(2); - return opacity; - } - - removeCoords([x, y]) { - this.justPlacedCoords = this.justPlacedCoords.filter(c => !(c[0] == x && c[1] == y)); - this.flashTimes = this.flashTimes.filter(({ c, t }) => !(c[0] == x && c[1] == y)); - } - - renderBorder(ctx, x, y) { - const type = this.game.settings.display.gridType; - if (type == "round") { - ctx.beginPath(); - ctx.roundRect(x, y, this.minoSize - 1, this.minoSize - 1, this.minoSize / 4); - ctx.stroke(); - } else if (type == "square") { - ctx.beginPath(); - ctx.strokeRect(x, y, this.minoSize - 1, this.minoSize - 1, this.minoSize / 4); - ctx.stroke(); - } else if (type == "dot") { - ctx.beginPath(); - ctx.arc(x + this.minoSize, y + this.minoSize, 1, 0, 2 * Math.PI); - ctx.stroke(); - ctx.beginPath(); - ctx.arc(x, y, 1, 0, 2 * Math.PI); - ctx.stroke(); - } - } - - /** - * @param {CanvasRenderingContext2D} cntx - */ - renderToCanvas(cntx, grid, yPosChange, [dx, dy] = [0, 0], width, height) { - cntx.clearRect(0, 0, width, height); - grid.forEach((row, y) => { - row.forEach((col, x) => { - const [posX, posY] = [x * this.minoSize, (yPosChange - y) * this.minoSize]; - const cell = col.split(" "); - cntx.lineWidth = 1; - - if (cell.includes("A") || cell.includes("S")) { // active piece or stopped piece - cntx.globalAlpha = this.getOpacity(cell, cntx, x, y) ?? this.queueAlpha.toFixed(2); - const p = this.getTexture(this.getPiece(cntx, cell[1])); - cntx.drawImage(this.texture, p.x, p.y, p.width, p.height, posX + dx, posY + dy, this.minoSize, this.minoSize); - this.setMinoFlash(cntx, x, y, posX + dx, posY + dy); - } - else if (cell.includes("NP") && this.game.renderer.inDanger) { // next piece overlay - cntx.globalAlpha = 0.32; - const p = this.getTexture("topout"); - cntx.drawImage(this.texture, p.x, p.y, p.width, p.height, posX + dx, posY + dy, this.minoSize, this.minoSize); - } - else if (cell.includes("Sh")) { // shadow piece - cntx.globalAlpha = this.getShadowOpacity(); - const piece = this.game.settings.display.colouredShadow ? this.game.falling.piece.name : "shadow"; - const p = this.getTexture(piece); - cntx.drawImage(this.texture, p.x, p.y, p.width, p.height, posX + dx, posY + dy, this.minoSize, this.minoSize); - } - else if (y < 20 && this.game.settings.display.showGrid && cntx == this.game.renderer.ctx) { // grid - cntx.globalAlpha = 1 - cntx.strokeStyle = "#ffffff" + this.toHex(this.game.settings.display.gridopacity); - this.renderBorder(cntx, posX, posY); - } - }); - }); - - // flash - this.flashTimes = this.flashTimes - .filter(({ c, t }) => t > 0) - .map(({ c, t }) => { return { c, t: t - 1 }; }); - } -} \ No newline at end of file diff --git a/src/display/renderer.js b/src/display/renderer.js index fd2a6fb..a97b508 100644 --- a/src/display/renderer.js +++ b/src/display/renderer.js @@ -1,369 +1,198 @@ -import { Game } from "../game.js"; -import pieces from "../data/pieces.json" with { type: "json" }; -import { defaultSkins, statDecimals, statsSecondary as statsSecondaries } from "../data/data.js"; - -export class Renderer { - boardHeight; - boardWidth; - holdHeight; - holdWidth; - nextHeight; - nextWidth; - holdQueueGrid = []; - nextQueueGrid = []; - inDanger; - texttimeouts = {}; - resetAnimLength = 30; - resetAnimCurrent = 30; - - sidebarStats; - sidebarFixed; - sidebarSecondary; - /** @type {CanvasRenderingContext2D} */ - ctx; - ctxN; - ctxH; - - canvasField = document.getElementById("playingfield"); - canvasNext = document.getElementById("next"); - canvasHold = document.getElementById("hold"); - divBoard = document.getElementById("board"); - divBackboard = document.getElementById("backboard"); - divLinesSent = document.getElementById("linessent"); - elementEditPieces = document.getElementById("editMenuPieces"); - - elementStats1 = document.getElementById("stats1"); - elementStats2 = document.getElementById("stats2"); - elementStats3 = document.getElementById("stats3"); - elementStatname1 = document.getElementById("statName1"); - elementStatname2 = document.getElementById("statName2"); - elementStatname3 = document.getElementById("statName3"); - elementSmallStat1 = document.getElementById("smallStat1"); - elementSmallStat2 = document.getElementById("smallStat2"); - elementSmallStat3 = document.getElementById("smallStat3"); - - /** - * @param {Game} game - */ - constructor(game) { - this.game = game; - this.board = game.board; - - this.ctx = this.canvasField.getContext("2d"); - this.ctxN = this.canvasNext.getContext("2d"); - this.ctxH = this.canvasHold.getContext("2d"); - } - - renderingLoop() { - this.game.boardrender.renderToCanvas(this.ctx, this.game.board.boardState, 39, [0, 0], this.boardWidth, this.boardHeight); - this.game.boardeffects.move(0, 0); - this.game.boardeffects.rotate(0); - this.game.particles.update(); - this.dangerParticles(); - this.resetAnimation(); - requestAnimationFrame(this.renderingLoop.bind(this)) - if(this.game.settings.game.gamemode == "ultra" && Math.floor(this.game.stats.time) == 60) this.renderTimeLeft("60S LEFT") - if(this.game.settings.game.gamemode == "ultra" && Math.floor(this.game.stats.time) == 90) this.renderTimeLeft("30S LEFT") - } - - sizeCanvas() { - this.renderStyles(); - [this.canvasField, this.canvasNext, this.canvasHold].forEach(c => { - c.width = Math.round(c.offsetWidth / 10) * 10; - c.height = Math.round(c.offsetHeight / 40) * 40; - }); - this.divBoard.style.width = `${this.canvasField.width}px`; - this.divBoard.style.height = `${this.canvasField.height / 2}px`; - this.game.boardrender.minoSize = this.canvasField.width / 10; - this.boardWidth = this.canvasField.offsetWidth; - this.boardHeight = this.canvasField.offsetHeight; - this.nextWidth = this.canvasNext.offsetWidth; - this.nextHeight = this.canvasNext.offsetHeight; - this.holdWidth = this.canvasHold.offsetWidth; - this.holdHeight = this.canvasHold.offsetHeight; - } - - updateNext() { - this.nextQueueGrid = [...Array(15)].map(() => [...Array(4)].map(() => "")); - - const next5 = this.game.bag.getFirstFive(); - next5.forEach((name, idx) => { - const piece = this.getPiece(name); - let [dx, dy] = [0, 3 * (4 - idx)]; - if (piece.name == "o") [dx, dy] = [dx + 1, dy + 1]; // shift o piece - const coords = this.board.pieceToCoords(piece.shape1); - coords.forEach(([x, y]) => (this.nextQueueGrid[y + dy][x + dx] = "A " + piece.name)); - }); - - this.game.boardrender.renderToCanvas(this.ctxN, this.nextQueueGrid, 15, [0, 0], this.nextWidth, this.nextHeight); - if (this.game.settings.game.gamemode == 'lookahead' || !this.game.settings.display.colouredQueues) return; - this.canvasNext.style.outlineColor = this.game.bag.nextPiece().colour; - } - - getPiece(name) { - if (name == "G") return { colour: "gray" } - return pieces.filter(p => p.name == name)[0]; - } - - updateHold() { - this.holdQueueGrid = [...Array(3)].map(() => [...Array(4)].map(() => "")); - this.clearHold(); - if (this.game.hold.piece == undefined) return; - - const name = this.game.hold.piece.name; - const isO = name == "o", - isI = name == "i"; - const [dx, dy] = [isO ? 1 : 0, isO ? 1 : isI ? -1 : 0]; - const coords = this.board.pieceToCoords(this.game.hold.piece.shape1); - - coords.forEach(([x, y]) => (this.holdQueueGrid[y + dy][x + dx] = "A " + name)); - const len = Math.round(this.game.boardrender.minoSize / 2); - const [shiftX, shiftY] = [isO || isI ? 0 : len, isI ? 0 : len]; - - this.game.boardrender.renderToCanvas(this.ctxH, this.holdQueueGrid, 2, [shiftX, shiftY], this.holdWidth, this.holdHeight); - if (this.game.settings.game.gamemode == 'lookahead' || !this.game.settings.display.colouredQueues) return; - const colour = this.game.hold.occured ? "gray" : this.game.hold.piece.colour - this.canvasHold.style.outline = `0.2vh solid ${colour}`; - } - - clearHold() { - this.ctxH.clearRect(0, 0, this.canvasHold.offsetWidth + 10, this.canvasHold.offsetHeight); - } - - renderDanger() { - const condition = - this.game.board.getMinos("S").some(c => c[1] > 16) && // any mino if above row 16 - this.game.settings.game.gamemode != 'combo'; // not combo mode - if (condition && !this.inDanger) { - this.game.sounds.playSound("damage_alert"); - } - this.game.boardeffects.toggleDangerBoard(condition) - this.inDanger = condition; - } - - renderActionText(damagetype, isBTB, isPC, damage, linecount) { - if (damagetype != "") this.setText("cleartext", damagetype, 2000); - if (this.game.stats.combo > 0) - this.setText("combotext", `Combo ${this.game.stats.combo}`, 2000); - if (isBTB && this.game.stats.btbCount > 0) - this.setText("btbtext", `BTB ${this.game.stats.btbCount} `, 2000); - if (isPC) this.setText("pctext", "Perfect Clear", 2000); - if (damage > 0) this.setText("linessent", `${this.game.mechanics.spikeCounter}`, 1500); - - if (this.game.mechanics.spikeCounter > 0) this.spikePattern("white", 1); - if (this.game.mechanics.spikeCounter >= 10) this.spikePattern("red", 1.1); - if (this.game.mechanics.spikeCounter >= 20) this.spikePattern("lime", 1.2); - - // audio - if (isPC) this.game.sounds.playSound("allclear"); - if (this.game.stats.btbCount == 2 && isBTB) this.game.sounds.playSound("btb_1"); - if (linecount >= 4 && this.game.stats.btbCount > 0) { - this.game.sounds.playSound("clearbtb"); - } else if (linecount >= 4) { - this.game.sounds.playSound("clearquad"); - } else if (linecount > 0 && this.game.mechanics.isTspin) { - this.game.sounds.playSound("clearspin"); - } else if (linecount > 0 && this.game.mechanics.isAllspin && this.game.settings.game.allspin) { - this.game.sounds.playSound("clearspin"); - } else if (linecount > 0) { - this.game.sounds.playSound("clearline"); - } - if (this.game.mechanics.spikeCounter >= 15) this.game.sounds.playSound("thunder", false); - if (this.game.stats.combo > 0) - this.game.sounds.playSound(`combo_${this.game.stats.combo > 16 ? 16 : this.game.stats.combo}`); - } - - resetActionText() { - ['btbtext', 'cleartext', 'combotext', 'pctext', 'linessent'].forEach(id => { - document.getElementById(id).style.opacity = "0"; - }) - } - - spikePattern(colour, size) { - this.divLinesSent.style.color = colour; - this.divLinesSent.style.textShadow = `0 0 1vh ${colour}`; - this.divLinesSent.style.fontSize = `${3.5 * size}vh`; - } - - setText(id, text, duration) { - const textbox = document.getElementById(id); - textbox.textContent = text; - textbox.style.transform = "translateX(-2%)"; - textbox.style.opacity = "1"; - if (this.texttimeouts[id] != 0) this.stopTimeout(id); - this.texttimeouts[id] = setTimeout(() => { - textbox.style.opacity = "0"; - textbox.style.transform = "translateX(2%)"; - this.game.mechanics.spikeCounter = 0; - }, duration); - } - - stopTimeout(name) { - clearTimeout(this.texttimeouts[name]); - this.texttimeouts[name] = 0; - } - - renderStyles() { - // custom background - const bg = this.game.settings.display.background; - if (bg == "") bg = "#080B0C"; - document.body.style.background = (bg[0] == "#") ? bg : `url("${bg}") no-repeat center center` - document.body.style.backgroundSize = "cover"; - - const height = Number(this.game.settings.display.boardHeight) + 10; - this.divBoard.style.transform = `scale(${height}%) translate(-50%, -50%)`; - this.canvasHold.style.outline = `0.2vh solid #dbeaf3`; - - // board opacity - const background = `rgba(0, 0, 0, ${Number(this.game.settings.display.boardOpacity) / 100})`; - this.divBackboard.style.backgroundColor = background; - document.body.style.setProperty('--background', background); - - // skins - let skin = this.game.settings.display.skin; - if (defaultSkins.includes(skin)) skin = `./assets/skins/${skin}.png`; - this.game.boardrender.loadImage(skin); - - // sidebar constants - this.sidebarStats = this.game.settings.game.sidebar; - this.sidebarFixed = this.sidebarStats.map(stat => this.createReverseLookup(statDecimals)[stat]); - this.sidebarSecondary = this.sidebarStats.map(stat => statsSecondaries[stat] ?? "None"); - - this.sidebarStats.forEach((stat, index) => { - if (stat == "None") stat = "" - this[`elementStatname${index + 1}`].textContent = stat; - }) - } - - renderSidebar() { - this.sidebarStats.forEach((stat, index) => { - if (stat == "None") { // no stat - this[`elementStats${index + 1}`].textContent = ""; - return; - }; - const displayStat = this.game.stats[stat].toFixed(this.sidebarFixed[index]); - this[`elementStats${index + 1}`].textContent = displayStat; - - if (this.sidebarSecondary[index]) { - const displaySecond = this.game.stats[this.sidebarSecondary[index]] - this[`elementSmallStat${index + 1}`].textContent = displaySecond; - } - }) - } - - renderTimeLeft(text){ - const e = document.getElementById("timeLeftText") - if (this.texttimeouts["timeLeft"] != 0){ - this.stopTimeout("timeLeft"); - //e.classList.remove("warn"); - } - e.textContent = text - e.classList.add("warn") - this.texttimeouts["timeLeft"] = setTimeout(() => { - e.classList.remove("warn"); - }, 3000); - } - - createReverseLookup(obj) { - const reverseLookup = {} - for (const [key, array] of Object.entries(obj)) { - array.forEach(item => { - reverseLookup[item] = key; - }); - } - return reverseLookup - } - - updateAlpha() { - if (this.game.settings.game.gamemode != 'lookahead') return; - const update = (type, amount) => { - if (this.game.stats.checkInvis()) { - if (this.game.boardrender[type] <= 0) { - this.game.boardrender[type] = 1; - this.updateNext(); - this.updateHold(); - } - } else { - if (this.game.boardrender[type] > 0) { - this.game.boardrender[type] += -amount / this.game.tickrate; - this.updateNext(); - this.updateHold(); - } else { - this.game.boardrender[type] = 0; - } - } - } - update("boardAlpha", 3) - update("queueAlpha", 3) - update("justPlacedAlpha", 6) - - } - - setEditPieceColours() { - const elPieces = [...this.elementEditPieces.children]; - elPieces.forEach(elpiece => { - const pieceid = elpiece.id.split("_")[0]; - elpiece.style.backgroundColor = this.getPiece(pieceid).colour - }) - } - - bounceBoard(direction) { - const force = Number(this.game.settings.display.boardBounce); - const forces = { "LEFT": [-force, 0], "RIGHT": [force, 0], "DOWN": [0, force], }; - this.game.boardeffects.move(...forces[direction]); - } - - rotateBoard(type) { - const force = Number(this.game.settings.display.boardBounce) * 0.5; - const forces = { "CW": force, "CCW": -force } - this.game.boardeffects.rotate(forces[type]); - } - - dangerParticles() { - if (!this.inDanger) return; - this.game.particles.spawnParticles(0, 0, "dangerboard"); - this.game.particles.spawnParticles(0, 20, "dangersides"); - } - - resetAnimation() { - if (this.resetAnimCurrent >= this.resetAnimLength * 2) return; - this.resetAnimCurrent++; - if (this.game.boardrender.boardAlpha < 0.99) this.game.boardrender.boardAlpha += 2 / this.resetAnimLength; - if (this.resetAnimCurrent > this.resetAnimLength) return; - - - const progress = this.resetAnimCurrent / this.resetAnimLength; - const startY = this.boardHeight / 2; - const dx = this.boardWidth; - const dy = dx + this.boardHeight / 2; - - const fillTriangle = (p, colour) => { - this.ctx.globalAlpha = 1; - this.ctx.fillStyle = colour; - this.ctx.beginPath(); - this.ctx.moveTo(0, startY - dy * p); - this.ctx.lineTo(dy * p, startY); - this.ctx.lineTo(0, startY + dy * p); - this.ctx.lineTo(0, 0); - this.ctx.fill(); - } - // fill triangle - const progress1 = this.easeInOutCubic(progress); - fillTriangle(progress1, 'white'); - - // clear smaller triangle - const progress2 = this.easeInOutCubic(progress - 0.1); - fillTriangle(progress2, 'black'); - - - if (this.resetAnimCurrent == this.resetAnimLength) { // finished - this.game.startGame(); - this.game.controls.resetting = false; - this.game.boardrender.boardAlpha = 0; - } - } - - easeInOutCubic(x) { - return -(Math.cos(Math.PI * x) - 1) / 2; - } -} +import { Game } from "../game.js"; +import pieces from "../data/pieces.json" with { type: "json" }; +import { statDecimals, statsSecondary as statsSecondaries } from "../data/data.js"; + +export class Renderer { + holdQueueGrid = []; + nextQueueGrid = []; + inDanger; + texttimeouts = {}; + + sidebarStats; + sidebarFixed; + sidebarSecondary; + + divBoard = document.getElementById("board"); + elementEditPieces = document.getElementById("editMenuPieces"); + /** + * @param {Game} game + */ + constructor(game) { + this.game = game; + this.board = game.board; + this.nextQueueGrid = [...Array(15)].map(() => [...Array(4)].map(() => "")); + this.holdQueueGrid = [...Array(3)].map(() => [...Array(4)].map(() => "")); + } + + updateNext() { + this.nextQueueGrid = [...Array(15)].map(() => [...Array(4)].map(() => "")); + + const next5 = this.game.bag.getFirstFive(); + next5.forEach((name, idx) => { + const piece = this.getPiece(name); + let [dx, dy] = [0, 3 * (4 - idx)]; + if (piece.name == "o") [dx, dy] = [dx + 1, dy + 1]; // shift o piece + const coords = this.board.pieceToCoords(piece.shape1); + coords.forEach(([x, y]) => (this.nextQueueGrid[y + dy][x + dx] = "A " + piece.name)); + }); + + this.game.pixi.render("next", this.nextQueueGrid); + } + + getPiece(name) { + if (name == "G") return { colour: "gray" } + return pieces.filter(p => p.name == name)[0]; + } + + updateHold() { + this.holdQueueGrid = [...Array(3)].map(() => [...Array(4)].map(() => "")); + this.clearHold(); + if (this.game.hold.piece == undefined) return; + + const name = this.game.hold.piece.name; + const isO = name == "o", + isI = name == "i"; + const [dx, dy] = [isO ? 1 : 0, isO ? 1 : isI ? -1 : 0]; + const coords = this.board.pieceToCoords(this.game.hold.piece.shape1); + coords.forEach(([x, y]) => (this.holdQueueGrid[y + dy][x + dx] = "A " + name)); + + this.game.pixi.render("hold", this.holdQueueGrid); + } + + clearHold() { + this.game.pixi.render("hold", this.holdQueueGrid); + } + + renderDanger() { + const condition = + this.game.board.getMinos("S").some(c => c[1] > 16) && // any mino if above row 16 + this.game.settings.game.gamemode != 'combo'; // not combo mode + if (condition && !this.inDanger) { + this.game.sounds.playSound("damage_alert"); + } + this.game.pixi.toggleDangerBG(condition); + this.inDanger = condition; + } + + dangerParticles() { + if (!this.inDanger) return; + this.game.particles.spawnParticles(0, 0, "dangerboard"); + this.game.particles.spawnParticles(0, 20, "dangersides"); + } + + renderActionText(damagetype, isBTB, isPC, damage, linecount) { + // audio + if (isPC) this.game.sounds.playSound("allclear"); + if (this.game.stats.btbCount == 2 && isBTB) this.game.sounds.playSound("btb_1"); + if (linecount >= 4 && this.game.stats.btbCount > 0) { + this.game.sounds.playSound("clearbtb"); + } else if (linecount >= 4) { + this.game.sounds.playSound("clearquad"); + } else if (linecount > 0 && this.game.mechanics.isTspin) { + this.game.sounds.playSound("clearspin"); + } else if (linecount > 0 && this.game.mechanics.isAllspin && this.game.settings.game.allspin) { + this.game.sounds.playSound("clearspin"); + } else if (linecount > 0) { + this.game.sounds.playSound("clearline"); + } + if (this.game.mechanics.spikeCounter >= 15) this.game.sounds.playSound("thunder", false); + if (this.game.stats.combo > 0) + this.game.sounds.playSound(`combo_${this.game.stats.combo > 16 ? 16 : this.game.stats.combo}`); + + // text + if (this.game.settings.display.actionText == false) return; + if (damagetype != "") this.game.pixi.showActionText("cleartext", damagetype); + if (this.game.stats.combo > 0) this.game.pixi.showActionText("combotext", `Combo ${this.game.stats.combo}`); + if (isBTB && this.game.stats.btbCount > 0) this.game.pixi.showActionText("btbtext", `btb ${this.game.stats.btbCount} `); + if (this.game.mechanics.spikeCounter > 4 && linecount > 0) this.game.pixi.showSpikeText(`${this.game.mechanics.spikeCounter}`); + if (isPC) this.game.pixi.showPCText(); + + } + + renderStyles(settings = false) { + // custom background + const bg = this.game.settings.display.background; + if (bg == "") bg = "#080B0C"; + document.body.style.background = (bg[0] == "#") ? bg : `url("${bg}") no-repeat center center` + document.body.style.backgroundSize = "cover"; + + const height = Number(this.game.settings.display.boardHeight); + this.divBoard.style.transform = `scale(${height}%) translate(-50%, -50%)`; + + // todo add board background + // board opacity + // const background = `rgba(0, 0, 0, ${Number(this.game.settings.display.boardOpacity) / 100})`; + // this.divBackboard.style.backgroundColor = background; + // document.body.style.setProperty('--background', background); + + // sidebar constants + this.sidebarStats = this.game.settings.game.sidebar; + this.sidebarFixed = this.sidebarStats.map(stat => this.createReverseLookup(statDecimals)[stat]); + this.sidebarSecondary = this.sidebarStats.map(stat => statsSecondaries[stat] ?? "None"); + + this.sidebarStats.forEach((stat, index) => { + if (stat == "None") stat = "" + this.game.pixi.statTexts[index].statText.text = stat.toUpperCase(); + }) + + if (settings) this.game.pixi.resize(); + } + + renderSidebar() { + this.sidebarStats.forEach((stat, index) => { + if (stat == "None") { // no stat + this.game.pixi.statTexts[index].stat.text = ""; + return; + }; + + let displayStat = this.game.stats[stat].toFixed(this.sidebarFixed[index]) ?? ""; + if (stat == "time") displayStat = this.formatTime(Number(displayStat), this.sidebarFixed[index]); // reformat time + this.game.pixi.statTexts[index].stat.text = displayStat; + + if (this.sidebarSecondary[index]) { + const displaySecond = this.game.stats[this.sidebarSecondary[index]] ?? "" + this.game.pixi.statTexts[index].statSecondary.text = displaySecond; + } + }) + } + + createReverseLookup(obj) { + const reverseLookup = {} + for (const [key, array] of Object.entries(obj)) { + array.forEach(item => { + reverseLookup[item] = key; + }); + } + return reverseLookup + } + + formatTime(s, d) { + const minutes = Math.floor(s / 60); + const seconds = (s - minutes * 60).toFixed(d) + return `${minutes>0?minutes:""}:${seconds < 10 ? "0" : ""}${seconds}` + } + + renderTimeLeft(text){ + this.game.pixi.showTimeLeftText(text); + } + + setEditPieceColours() { + const elPieces = [...this.elementEditPieces.children]; + elPieces.forEach(elpiece => { + const pieceid = elpiece.id.split("_")[0]; + elpiece.style.backgroundColor = this.getPiece(pieceid).colour + }) + } + + bounceBoard(direction) { + const force = Number(this.game.settings.display.boardBounce); + const forces = { "LEFT": [-force, 0], "RIGHT": [force, 0], "DOWN": [0, force], }; + this.game.boardeffects.move(...forces[direction]); + } + + rotateBoard(type) { + const force = Number(this.game.settings.display.boardBounce) * 0.5; + const forces = { "CW": force, "CCW": -force } + this.game.boardeffects.rotate(forces[type]); + } +} diff --git a/src/features/editboard.js b/src/features/editboard.js index ebff330..c89c5f0 100644 --- a/src/features/editboard.js +++ b/src/features/editboard.js @@ -1,119 +1,89 @@ -import { Game } from "../game.js"; - -export class BoardEditor { - clickareasdiv = document.getElementById("clickareas"); - mousedown = false; - currentMode = "fill"; - fillPiece = 'G'; - fillRow = false; - override = false; - - elementEditButton = document.getElementById("editButton"); - - /** - * @param {Game} game - */ - constructor(game) { - this.game = game; - this.board = game.board; - } - - addListeners() { - // delegate listeners for efficiency - document.body.addEventListener("mousedown", (e) => { - if (e.target.classList.contains('clickmino')) { - const j = Number(e.target.dataset.x) - const i = Number(e.target.dataset.y) - if (this.game.settings.game.gamemode != 'custom') return; - if (this.fillRow) { this.fillWholeRow([j, 19 - i]) } - else { this.fillCell([j, 19 - i]); } - } - }); - - document.body.addEventListener("mouseenter", (e) => { - if (e.target.classList.contains('clickmino')) { - const j = Number(e.target.dataset.x) - const i = Number(e.target.dataset.y) - if (this.game.settings.game.gamemode != 'custom') return; - e.target.classList.add('highlighting') - if (this.mousedown) { - if (this.fillRow) { this.fillWholeRow([j, 19 - i]) } - else { this.fillCell([j, 19 - i]); } - } - } - }, true); - - document.body.addEventListener("mouseleave", (e) => { - if (e.target.classList.contains('clickmino')) { - e.target.classList.remove('highlighting') - } - }, true); - - document.body.addEventListener("mouseup", () => { - if (this.mousedown) this.game.history.save(); - this.mousedown = false; - }); - - for (let i = 0; i < 20; i++) { - for (let j = 0; j < 10; j++) { - const clickarea = document.createElement("div"); - clickarea.classList.add("clickmino"); - clickarea.dataset.x = j.toString(); - clickarea.dataset.y = i.toString(); - this.clickareasdiv.appendChild(clickarea); - } - } - } - - fillCell([x, y]) { - if (!this.mousedown) this.currentMode = this.board.checkMino([x, y], "S") ? "remove" : "fill"; - this.mousedown = true; - if (this.board.checkMino([x, y], "A")) return; - if (!this.override && this.currentMode == "fill" && this.board.checkMino([x, y], "S")) return; - this.board.setValue([x, y], this.currentMode == "fill" ? "S " + this.fillPiece : ""); - this.game.mechanics.setShadow(); - } - - fillWholeRow([x, y]) { - for (let i = 0; i < 10; i++) { - this.board.setValue([i, y], x == i ? "" : "S G"); - this.mousedown = true; - } - } - - convertToMap() { - const board = this.game.board.boardState; - const next = this.game.bag.nextPieces; - const hold = this.game.hold.piece == null ? "" : this.game.hold.piece.name; - const currPiece = this.game.falling.piece.name; - - let boardstring = board.toReversed().flatMap(row => { - return row.map(col => { - col = col.replace("Sh", "").replace("NP", "").replace("G", "#"); - if (col.length == 1) col = "" - if (col[0] == "A") col = ""; - if (col[0] == "S") col = col[2]; - if (col.trim() == "") col = "_"; - return col; - }) - }).join("") - return `${boardstring}?${currPiece},${next.flat()}?${hold}` - - } - - convertFromMap(string) { - let [board, next, hold] = string.split("?"); - board = board.match(/.{1,10}/g).toReversed().map(row => { - return row.split("").map(col => { - col = col.replace("#", "G").replace("_", "") - if (col != "") col = `S ${col}` - return col - }); - }) - return { board, next, hold } - } - - setEditButton(bool) { - this.elementEditButton.style.display = bool ? "flex" : "none"; - } +import { Game } from "../game.js"; + +export class BoardEditor { + mousedown = false; + currentMode = "fill"; + fillPiece = 'G'; + fillRow = false; + override = false; + + /** + * @param {Game} game + */ + constructor(game) { + this.game = game; + this.board = game.board; + } + + mouseDown([x, y], sprite) { + if (this.game.settings.game.gamemode != 'custom') return; + if (this.fillRow) { this.fillWholeRow([x, 19 - y]) } + else { this.fillCell([x, 19 - y]); } + } + + mouseEnter([x, y], sprite) { + if (this.game.settings.game.gamemode != 'custom') return; + sprite.alpha = 0.5; + if (this.mousedown) { + if (this.fillRow) { this.fillWholeRow([x, 19 - y]) } + else { this.fillCell([x, 19 - y]); } + } + } + + mouseLeave(e, sprite) { + sprite.alpha = 0; + } + + mouseUp(e) { + if (this.mousedown) this.game.history.save(); + this.mousedown = false; + } + + fillCell([x, y]) { + if (!this.mousedown) this.currentMode = this.board.checkMino([x, y], "S") ? "remove" : "fill"; + this.mousedown = true; + if (this.board.checkMino([x, y], "A")) return; + if (!this.override && this.currentMode == "fill" && this.board.checkMino([x, y], "S")) return; + this.board.setValue([x, y], this.currentMode == "fill" ? "S " + this.fillPiece : ""); + this.game.mechanics.setShadow(); + } + + fillWholeRow([x, y]) { + for (let i = 0; i < 10; i++) { + this.board.setValue([i, y], x == i ? "" : "S G"); + this.mousedown = true; + } + } + + convertToMap() { + const board = this.game.board.boardState; + const next = this.game.bag.nextPieces; + const hold = this.game.hold.piece == null ? "" : this.game.hold.piece.name; + const currPiece = this.game.falling.piece.name; + + let boardstring = board.toReversed().flatMap(row => { + return row.map(col => { + col = col.replace("Sh", "").replace("NP", "").replace("G", "#"); + if (col.length == 1) col = "" + if (col[0] == "A") col = ""; + if (col[0] == "S") col = col[2]; + if (col.trim() == "") col = "_"; + return col; + }) + }).join("") + return `${boardstring}?${currPiece},${next.flat()}?${hold}` + + } + + convertFromMap(string) { + let [board, next, hold] = string.split("?"); + board = board.match(/.{1,10}/g).toReversed().map(row => { + return row.split("").map(col => { + col = col.replace("#", "G").replace("_", "") + if (col != "") col = `S ${col}` + return col + }); + }) + return { board, next, hold } + } } \ No newline at end of file diff --git a/src/features/history.js b/src/features/history.js index e758536..434e91d 100644 --- a/src/features/history.js +++ b/src/features/history.js @@ -1,191 +1,186 @@ -import { Game } from "../game.js"; - -export class History { - /** - * stores every game state indexed - * @type {string[]} - */ - historyStates = []; - /** - * stores connections by assigning index as the node, and the value as an array of every connection to other nodes - * @type {Number[][]} - */ - historyConnections = []; - currentState = 0; - selectedbranch = 0; - - historyelement = document.getElementById("history"); - choiceselement = document.getElementById("redochoices"); - - /** - * @param {Game} game - */ - constructor(game) { - this.game = game; - } - - save() { - if (!this.game.settings.game.history) return; - const map = this.convertToMapCompressed(); - this.pushHistory(map); - this.updateUI(); - } - - pushHistory(map) { - this.historyStates.push(map); - const prevState = this.currentState; - this.currentState = this.historyStates.length - 1; - if (this.currentState == 0) return; - - if (this.historyConnections[prevState] == null) this.historyConnections[prevState] = []; - this.historyConnections[prevState].push(this.currentState); - } - - load() { - const state = this.historyStates[this.currentState] - this.convertFromMapCompressed(state); - this.updateUI() - } - - undo() { - if (this.currentState == 0 || !this.game.settings.game.history) return; - this.historyConnections.forEach((next, ind) => { - if (next.includes(this.currentState)) { - this.currentState = ind; - } - }) - this.game.sounds.playSound("undo"); - this.game.boardeffects.move(-1, 0); - this.load() - } - - redo() { - if (!this.game.settings.game.history) return; - const connection = this.historyConnections[this.currentState]; - if (connection == undefined) return; - this.currentState = this.selectedbranch || Math.max(...connection); - this.game.sounds.playSound("redo"); - this.game.boardeffects.move(1, 0); - this.load() - } - - goto(num) { - this.currentState = num; - this.load(); - } - - clearFuture(node) { - const futureNodes = this.historyConnections[node]; - if (futureNodes != undefined) { - futureNodes.forEach(node => this.clearFuture(node)) - } - this.historyStates[node] = undefined; - this.historyConnections[node] = undefined; - } - - updateUI() { - const branches = this.historyConnections[this.currentState] ?? []; - this.selectedbranch = Math.max(...branches); - this.historyelement.textContent = `history: ${this.currentState}`; - if (branches.length <= 1) { - this.choiceselement.style.opacity = "0"; - this.choiceselement.style.pointerEvents = "none"; - this.selectedbranch = 0; - return; - } - - this.choiceselement.style.opacity = "1"; - this.choiceselement.style.pointerEvents = "all"; - [...this.choiceselement.children].forEach(button => button.remove()) - branches.forEach(state => { - const button = document.createElement("button"); - button.classList.add("redochoice"); - if (state == this.selectedbranch) button.classList.add("selected"); - button.textContent = state.toString(); - button.onclick = () => this.setSelected(state, button); - this.choiceselement.appendChild(button) - }) - } - - setSelected(state, button) { - this.selectedbranch = state; - [...this.choiceselement.children].forEach(el => { - el.classList.remove("selected"); - }) - button.classList.add("selected"); - } - - compress(s) { - // saves anywhere from 50% worst case to 90% on average - // 250 blocks is 30kB ~~ 5 min of 3.3pps play is 120kB - let cs = ""; - let count = 1; - for (let i = 1; i < s.length; i++) { - if (s[i] == s[i - 1]) { - count++; - } else { - cs += `${count}${s[i - 1]}`; - count = 1; - } - } - cs += `${count}${s[s.length - 1]}`; - return cs; - } - - decompress(s) { - let ds = ""; - let int = ''; - for (let i = 0; i < s.length; i++) { - if (!isNaN(parseInt(s[i]))) { - int += s[i]; - continue; - } - const count = parseInt(int); - const char = s[i]; - ds += char.repeat(count); - int = ''; - } - return ds; - } - - convertToMapCompressed() { - const board = this.game.board.boardState; - const next = this.game.bag.nextPieces; - const hold = this.game.hold.piece == null ? "" : this.game.hold.piece.name; - const currPiece = this.game.falling.piece == null ? "" : this.game.falling.piece.name; - let boardstring = board.toReversed().flatMap(row => { - return row.map(col => { - col = col.replace("Sh", "").replace("NP", "").replace("G", "#"); - col = col.trim(); - if (col.length == 1) col = "" - if (col[0] == "A") col = ""; - if (col[0] == "S") col = col[2]; - if (col == "") col = "_"; - return col; - }) - }).join("") - return `${this.compress(boardstring)}?${currPiece},${next.flat()}?${hold}` - - } - - convertFromMapCompressed(string) { - let [board, next, hold] = string.split("?"); - board = this.decompress(board); - board = board.match(/.{1,10}/g).toReversed().map(row => { - return row.split("").map(col => { - col = col.replace("#", "G").replace("_", "") - if (col != "") col = `S ${col}` - return col - }); - }) - this.game.board.boardState = board; - this.game.bag.nextPieces = [next.split(","), []]; - this.game.hold.piece = this.game.renderer.getPiece(hold); - this.game.mechanics.spawnPiece(this.game.bag.randomiser()); - } - - setHistoryDiv(bool) { - this.historyelement.style.display = bool ? "block" : "none"; - } - +import { Game } from "../game.js"; + +export class History { + /** + * stores every game state indexed + * @type {string[]} + */ + historyStates = []; + /** + * stores connections by assigning index as the node, and the value as an array of every connection to other nodes + * @type {Number[][]} + */ + historyConnections = []; + currentState = 0; + selectedbranch = 0; + + historyelement = document.getElementById("history"); + choiceselement = document.getElementById("redochoices"); + + /** + * @param {Game} game + */ + constructor(game) { + this.game = game; + } + + save() { + if (!this.game.settings.game.history) return; + const map = this.convertToMapCompressed(); + this.pushHistory(map); + this.updateUI(); + } + + pushHistory(map) { + this.historyStates.push(map); + const prevState = this.currentState; + this.currentState = this.historyStates.length - 1; + if (this.currentState == 0) return; + + if (this.historyConnections[prevState] == null) this.historyConnections[prevState] = []; + this.historyConnections[prevState].push(this.currentState); + } + + load() { + const state = this.historyStates[this.currentState] + this.convertFromMapCompressed(state); + this.updateUI() + } + + undo() { + if (this.currentState == 0 || !this.game.settings.game.history) return; + this.historyConnections.forEach((next, ind) => { + if (next.includes(this.currentState)) { + this.currentState = ind; + } + }) + this.game.sounds.playSound("undo"); + this.game.boardeffects.move(-1, 0); + this.load() + } + + redo() { + if (!this.game.settings.game.history) return; + const connection = this.historyConnections[this.currentState]; + if (connection == undefined) return; + this.currentState = this.selectedbranch || Math.max(...connection); + this.game.sounds.playSound("redo"); + this.game.boardeffects.move(1, 0); + this.load() + } + + goto(num) { + this.currentState = num; + this.load(); + } + + clearFuture(node) { + const futureNodes = this.historyConnections[node]; + if (futureNodes != undefined) { + futureNodes.forEach(node => this.clearFuture(node)) + } + this.historyStates[node] = undefined; + this.historyConnections[node] = undefined; + } + + updateUI() { + const branches = this.historyConnections[this.currentState] ?? []; + this.selectedbranch = Math.max(...branches); + this.historyelement.textContent = `current: ${this.currentState}`; + if (branches.length <= 1) { + this.choiceselement.style.opacity = "0"; + this.choiceselement.style.pointerEvents = "none"; + this.selectedbranch = 0; + return; + } + + this.choiceselement.style.opacity = "1"; + this.choiceselement.style.pointerEvents = "all"; + [...document.getElementsByClassName("redochoice")].forEach(button => button.remove()) + branches.forEach(state => { + const button = document.createElement("button"); + button.classList.add("redochoice"); + if (state == this.selectedbranch) button.classList.add("selected"); + button.textContent = state.toString(); + button.onclick = () => this.setSelected(state, button); + this.choiceselement.appendChild(button) + }) + } + + setSelected(state, button) { + this.selectedbranch = state; + [...this.choiceselement.children].forEach(el => { + el.classList.remove("selected"); + }) + button.classList.add("selected"); + } + + compress(s) { + // saves anywhere from 50% worst case to 90% on average + // 250 blocks is 30kB ~~ 5 min of 3.3pps play is 120kB + let cs = ""; + let count = 1; + for (let i = 1; i < s.length; i++) { + if (s[i] == s[i - 1]) { + count++; + } else { + cs += `${count}${s[i - 1]}`; + count = 1; + } + } + cs += `${count}${s[s.length - 1]}`; + return cs; + } + + decompress(s) { + let ds = ""; + let int = ''; + for (let i = 0; i < s.length; i++) { + if (!isNaN(parseInt(s[i]))) { + int += s[i]; + continue; + } + const count = parseInt(int); + const char = s[i]; + ds += char.repeat(count); + int = ''; + } + return ds; + } + + convertToMapCompressed() { + const board = this.game.board.boardState; + const next = this.game.bag.nextPieces; + const hold = this.game.hold.piece == null ? "" : this.game.hold.piece.name; + const currPiece = this.game.falling.piece == null ? "" : this.game.falling.piece.name; + let boardstring = board.toReversed().flatMap(row => { + return row.map(col => { + col = col.replace("Sh", "").replace("NP", "").replace("G", "#"); + col = col.trim(); + if (col.length == 1) col = "" + if (col[0] == "A") col = ""; + if (col[0] == "S") col = col[2]; + if (col == "") col = "_"; + return col; + }) + }).join("") + return `${this.compress(boardstring)}?${currPiece},${next.flat()}?${hold}` + + } + + convertFromMapCompressed(string) { + let [board, next, hold] = string.split("?"); + board = this.decompress(board); + board = board.match(/.{1,10}/g).toReversed().map(row => { + return row.split("").map(col => { + col = col.replace("#", "G").replace("_", "") + if (col != "") col = `S ${col}` + return col + }); + }) + this.game.board.boardState = board; + this.game.bag.nextPieces = [next.split(","), []]; + this.game.hold.piece = this.game.renderer.getPiece(hold); + this.game.mechanics.spawnPiece(this.game.bag.randomiser()); + } } \ No newline at end of file diff --git a/src/features/modes.js b/src/features/modes.js index 51d0c57..6dbafa4 100644 --- a/src/features/modes.js +++ b/src/features/modes.js @@ -1,141 +1,141 @@ -import { Game } from "../game.js"; -import gamemodeJSON from "../data/gamemodes.json" with { type: "json" }; -import { gameoverResultText, gameoverText, resultSuffix } from "../data/data.js"; - -export class Modes { - elementobjectives = document.getElementById("objective"); - divObjectiveText = document.getElementById("objectiveText"); - modeJSON; - customSettings; - - /** - * @param {Game} game - */ - constructor(game) { - this.game = game; - } - - checkFinished() { - const goals = this.game.settings.game; - const stats = this.game.stats; - - // hardcoded objectives - let combobreak = this.game.stats.combo == -1 && stats.clearlines >= 1 && this.modeJSON.target == 'combobreak'; - let gameend = this.game.ended && this.modeJSON.target == 'gameEnd'; - - let stat = stats[this.modeJSON.goalStat] - let goal = goals[this.modeJSON.target] - let result = stats[this.modeJSON.result] - - if (stat >= goal || combobreak || gameend) { - if(this.game.settings.game.gamemode != "race" ) result = Math.round(result * 1000) / 1000 - stat = Math.round(stat * 1000) / 1000 - this.game.profilestats.setPB(result); - const text = this.statText(this.modeJSON.goalStat, stat, this.modeJSON.result, result) - const suffix = resultSuffix[this.modeJSON.result] - this.game.endGame(result + suffix, text); - } - - if (this.game.settings.game.gamemode == 'ultra') { // changes ultra sidebar - stat = stats.score; - goal = undefined - } - this.setObjectiveText(stat, goal); - } - - statText(stat, value, result, resultvalue) { - const front = gameoverText[stat].replace("_", value); - const back = gameoverResultText[result].replace("_", resultvalue); - return front + back; - } - - setObjectiveText(statValue, resultValue) { - if (statValue != undefined) statValue = Math.round(statValue * 1000) / 1000 - let modetext = (statValue == undefined ? '' : statValue) - + (resultValue == undefined ? '' : `/${resultValue}`) - this.elementobjectives.textContent = modetext; - } - - loadModes() { - let currentGamemode = this.game.settings.game.gamemode; - if (typeof currentGamemode == 'number') { // backwards compatibility - this.game.settings.game.gamemode = 'sprint' - currentGamemode = 'sprint' - } - this.setGamemode(currentGamemode); - this.divObjectiveText.textContent = this.modeJSON.objectiveText; - } - - setGamemode(mode) { - this.game.settings.game.gamemode = mode; - const competitive = this.game.settings.game.competitiveMode; - const custom = JSON.parse(localStorage.getItem('customGame')); - - if (competitive) { - if (custom == null) { - localStorage.setItem('customGame', JSON.stringify(this.game.settings.game)); - } - this.modeJSON = this.getGamemodeJSON(mode); - this.game.settings.game = { ...this.game.settings.game, ...this.modeJSON.settings }; - } else { - if (custom != null) { - this.game.settings.game = custom; - this.game.settings.game.competitiveMode = false; - localStorage.removeItem('customGame'); - } - this.modeJSON = this.getGamemodeJSON(mode); - } - this.toggleDialogState(competitive); - this.game.menuactions.saveSettings(); - } - - toggleDialogState(enabled) { - document.getElementById('game').disabled = enabled; - document.getElementById('goals').disabled = enabled; - } - - getGamemodeJSON(mode) { - const modeinfo = gamemodeJSON[mode]; - const allinfo = gamemodeJSON["*"]; - - let info = {} - Object.keys(allinfo).forEach(key => info[key] = modeinfo[key] ?? allinfo[key]); - info.settings = { ...allinfo.settings, ...modeinfo.settings } - - return info; - } - - getGamemodeNames() { - return Object.keys(gamemodeJSON).filter(key => key != "*"); - } - - getSuffix(mode) { - const modeinfo = gamemodeJSON[mode] ?? {}; - return resultSuffix[modeinfo.result] ?? " (legacy)"; - } - - diggerAddGarbage(removed) { - if (this.game.stats.getRemainingGarbage() > 10 && this.game.settings.game.gamemode == "digger") - this.game.mechanics.addGarbage(removed); - } - - set4WCols(start) { - if (this.game.settings.game.gamemode == 'combo') this.game.board.setComboBoard(start); - - } - - startSurvival() { - const time = (60 * 1000) / this.game.settings.game.survivalRate; - if (this.game.settings.game.gamemode == 'survival') - this.game.survivalTimer = setInterval(() => this.game.mechanics.addGarbage(1), time); - } - - diggerGarbageSet(start) { - const rows = - this.game.settings.game.requiredGarbage < 10 - ? this.game.settings.game.requiredGarbage - : 10; - if (this.game.stats.getRemainingGarbage() > 0 && start && this.game.settings.game.gamemode == 'digger') - this.game.mechanics.addGarbage(rows); - } +import { Game } from "../game.js"; +import gamemodeJSON from "../data/gamemodes.json" with { type: "json" }; +import { gameoverResultText, gameoverText, resultSuffix } from "../data/data.js"; + +export class Modes { + modeJSON; + customSettings; + + /** + * @param {Game} game + */ + constructor(game) { + this.game = game; + } + + checkFinished() { + const goals = this.game.settings.game; + const stats = this.game.stats; + + // hardcoded objectives + let combobreak = this.game.stats.combo == -1 && stats.clearlines >= 1 && this.modeJSON.target == 'combobreak'; + let gameend = this.game.ended && this.modeJSON.target == 'gameEnd'; + + let stat = stats[this.modeJSON.goalStat] + let goal = goals[this.modeJSON.target] + let result = stats[this.modeJSON.result] + + if (stat >= goal || combobreak || gameend) { + if(this.game.settings.game.gamemode != "race" ) result = Math.round(result * 1000) / 1000 + stat = Math.round(stat * 1000) / 1000 + this.game.profilestats.setPB(result); + const text = this.statText(this.modeJSON.goalStat, stat, this.modeJSON.result, result) + const suffix = resultSuffix[this.modeJSON.result] + this.game.endGame(result + suffix, text); + } + + if (this.game.settings.game.gamemode == 'ultra') { // changes ultra sidebar + stat = stats.score; + goal = undefined + } + this.setObjectiveText(stat, goal); + } + + statText(stat, value, result, resultvalue) { + const front = gameoverText[stat].replace("_", value); + const back = gameoverResultText[result].replace("_", resultvalue); + return front + back; + } + + setObjectiveText(statValue, resultValue) { + if (statValue != undefined) statValue = Math.round(statValue * 1000) / 1000 + let modetext = (statValue == undefined ? '' : statValue) + + (resultValue == undefined ? '' : `/${resultValue}`) + this.game.pixi.objectiveTexts[0].text = modetext; + } + + loadModes() { + let currentGamemode = this.game.settings.game.gamemode; + if (typeof currentGamemode == 'number') { // backwards compatibility + this.game.settings.game.gamemode = 'sprint' + currentGamemode = 'sprint' + } + this.setGamemode(currentGamemode); + + this.game.pixi.objectiveTexts[1].text = this.modeJSON.objectiveText.toUpperCase(); + this.game.pixi.toggleEditButton(this.game.settings.game.gamemode == 'custom'); + } + + setGamemode(mode) { + this.game.settings.game.gamemode = mode; + const competitive = this.game.settings.game.competitiveMode; + const custom = JSON.parse(localStorage.getItem('customGame')); + + if (competitive) { + if (custom == null) { + localStorage.setItem('customGame', JSON.stringify(this.game.settings.game)); + } + this.modeJSON = this.getGamemodeJSON(mode); + this.game.settings.game = { ...this.game.settings.game, ...this.modeJSON.settings }; + } else { + if (custom != null) { + this.game.settings.game = custom; + this.game.settings.game.competitiveMode = false; + localStorage.removeItem('customGame'); + } + this.modeJSON = this.getGamemodeJSON(mode); + } + this.toggleDialogState(competitive); + this.game.menuactions.saveSettings(); + } + + toggleDialogState(enabled) { + document.getElementById('game').disabled = enabled; + document.getElementById('goals').disabled = enabled; + } + + getGamemodeJSON(mode) { + const modeinfo = gamemodeJSON[mode]; + const allinfo = gamemodeJSON["*"]; + + let info = {} + Object.keys(allinfo).forEach(key => info[key] = modeinfo[key] ?? allinfo[key]); + info.settings = { ...allinfo.settings, ...modeinfo.settings } + + return info; + } + + getGamemodeNames() { + return Object.keys(gamemodeJSON).filter(key => key != "*"); + } + + getSuffix(mode) { + const modeinfo = gamemodeJSON[mode] ?? {}; + return resultSuffix[modeinfo.result] ?? " (legacy)"; + } + + diggerAddGarbage(removed) { + if (this.game.stats.getRemainingGarbage() > 10 && this.game.settings.game.gamemode == "digger") + this.game.mechanics.addGarbage(removed); + } + + set4WCols(start) { + if (this.game.settings.game.gamemode == 'combo') this.game.board.setComboBoard(start); + + } + + startSurvival() { + const time = (60 * 1000) / this.game.settings.game.survivalRate; + if (this.game.settings.game.gamemode == 'survival') + this.game.survivalTimer = setInterval(() => this.game.mechanics.addGarbage(1), time); + } + + diggerGarbageSet(start) { + const rows = + this.game.settings.game.requiredGarbage < 10 + ? this.game.settings.game.requiredGarbage + : 10; + if (this.game.stats.getRemainingGarbage() > 0 && start && this.game.settings.game.gamemode == 'digger') + this.game.mechanics.addGarbage(rows); + } } \ No newline at end of file diff --git a/src/features/profileStats.js b/src/features/profileStats.js index 9baf6b9..2028fb2 100644 --- a/src/features/profileStats.js +++ b/src/features/profileStats.js @@ -1,86 +1,87 @@ -import { lowerIsBetter } from "../data/data.js"; -import { Game } from "../game.js"; - -export class ProfileStats { - personalBests = {}; - notSaved = ['game', 'level', 'combo'] - - /** - * @param {Game} game - */ - constructor(game) { - this.game = game; - } - - setPB(score) { - this.game.elementGameEndTitle.textContent = 'GAME ENDED'; - const gamemode = this.game.settings.game.gamemode - const gamemodeStats = this.personalBests[gamemode] ?? {}; - const currentScore = Number(gamemodeStats.score); - const lower = lowerIsBetter[this.game.modes.modeJSON.result]; - - if (!this.game.settings.game.competitiveMode) return; - - if (isNaN(currentScore) || (lower && score < currentScore) || (!lower && score > currentScore)) { - let gameStatsKeys = Object.getOwnPropertyNames(this.game.stats) - gameStatsKeys = gameStatsKeys.filter(key => key != 'game') - const gameStats = {}; - gameStatsKeys.forEach(key => gameStats[key] = this.game.stats[key]) - const ts = new Date().toJSON(); - this.personalBests[gamemode] = { score, pbstats: gameStats, version: this.game.version, ts }; - this.game.elementGameEndTitle.textContent = 'NEW PB!'; - setTimeout(() => this.game.sounds.playSound("personalbest"), 1000); - - this.game.modals.generate.notif("PB Saved", `PB on ${gamemode} saved`, "success"); - } - } - - loadPBs() { - const stats = JSON.parse(localStorage.getItem("stats")) ?? {}; - this.personalBests = stats.pbs ?? {}; - } - - removePB(mode) { - delete this.personalBests[mode]; - this.saveSession(); - } - - saveSession() { - const prevstats = JSON.parse(localStorage.getItem("stats")) ?? {}; - const statsData = prevstats.lifetime ?? {}; - - Object.getOwnPropertyNames(this.game.stats).forEach(key => { - if (this.notSaved.includes(key)) return; - - if (key == 'clearCols' || key == 'tspins') { - if (statsData[key] == undefined || typeof statsData[key] != 'object') statsData[key] = [] - this.game.stats[key].forEach((_, index) => { - if (statsData[key][index] == undefined) statsData[key][index] = 0 - statsData[key][index] += this.game.stats[key][index]; - }); - return; - } - if (key == 'clearPieces') { - if (statsData[key] == undefined) { - statsData[key] = this.game.stats[key]; - return; - } - Object.keys(this.game.stats[key]).forEach(piece => { - this.game.stats[key][piece].forEach((_, index) => { - if (statsData[key][piece] == undefined) statsData[key][piece] = [0, 0, 0, 0]; - statsData[key][piece][index] += this.game.stats[key][piece][index]; - }); - }); - return; - } - if (key == "maxBTB" || key == "maxCombo") { - if (statsData[key] < this.game.stats[key] ) statsData[key] = this.game.stats[key]; - return; - } - if (this.game.stats[key] < 0) this.game.stats[key] = 0 - statsData[key] += this.game.stats[key]; - }) - const stats = { pbs: this.personalBests, lifetime: statsData } - localStorage.setItem("stats", JSON.stringify(stats)); - } +import { lowerIsBetter } from "../data/data.js"; +import { Game } from "../game.js"; + +export class ProfileStats { + personalBests = {}; + notSaved = ['game', 'level', 'combo'] + elementGameEndTitle = document.getElementById("gameEndTitle"); + + /** + * @param {Game} game + */ + constructor(game) { + this.game = game; + } + + setPB(score) { + this.elementGameEndTitle.textContent = 'GAME ENDED'; + const gamemode = this.game.settings.game.gamemode + const gamemodeStats = this.personalBests[gamemode] ?? {}; + const currentScore = Number(gamemodeStats.score); + const lower = lowerIsBetter[this.game.modes.modeJSON.result]; + + if (!this.game.settings.game.competitiveMode) return; + + if (isNaN(currentScore) || (lower && score < currentScore) || (!lower && score > currentScore)) { + let gameStatsKeys = Object.getOwnPropertyNames(this.game.stats) + gameStatsKeys = gameStatsKeys.filter(key => key != 'game') + const gameStats = {}; + gameStatsKeys.forEach(key => gameStats[key] = this.game.stats[key]) + const ts = new Date().toJSON(); + this.personalBests[gamemode] = { score, pbstats: gameStats, version: this.game.version, ts }; + this.elementGameEndTitle.textContent = 'NEW PB!'; + setTimeout(() => this.game.sounds.playSound("personalbest"), 1000); + + this.game.modals.generate.notif("PB Saved", `PB on ${gamemode} saved`, "success"); + } + } + + loadPBs() { + const stats = JSON.parse(localStorage.getItem("stats")) ?? {}; + this.personalBests = stats.pbs ?? {}; + } + + removePB(mode) { + delete this.personalBests[mode]; + this.saveSession(); + } + + saveSession() { + const prevstats = JSON.parse(localStorage.getItem("stats")) ?? {}; + const statsData = prevstats.lifetime ?? {}; + + Object.getOwnPropertyNames(this.game.stats).forEach(key => { + if (this.notSaved.includes(key)) return; + + if (key == 'clearCols' || key == 'tspins') { + if (statsData[key] == undefined || typeof statsData[key] != 'object') statsData[key] = [] + this.game.stats[key].forEach((_, index) => { + if (statsData[key][index] == undefined) statsData[key][index] = 0 + statsData[key][index] += this.game.stats[key][index]; + }); + return; + } + if (key == 'clearPieces') { + if (statsData[key] == undefined) { + statsData[key] = this.game.stats[key]; + return; + } + Object.keys(this.game.stats[key]).forEach(piece => { + this.game.stats[key][piece].forEach((_, index) => { + if (statsData[key][piece] == undefined) statsData[key][piece] = [0, 0, 0, 0]; + statsData[key][piece][index] += this.game.stats[key][piece][index]; + }); + }); + return; + } + if (key == "maxBTB" || key == "maxCombo") { + if (statsData[key] < this.game.stats[key] ) statsData[key] = this.game.stats[key]; + return; + } + if (this.game.stats[key] < 0) this.game.stats[key] = 0 + statsData[key] += this.game.stats[key]; + }) + const stats = { pbs: this.personalBests, lifetime: statsData } + localStorage.setItem("stats", JSON.stringify(stats)); + } } \ No newline at end of file diff --git a/src/features/sounds.js b/src/features/sounds.js index 8ac415b..43f357f 100644 --- a/src/features/sounds.js +++ b/src/features/sounds.js @@ -1,127 +1,126 @@ -import sfxobj from "../data/sfxlist.json" with { type: "json" }; -import { songsobj } from "../data/data.js"; -import { Game } from "../game.js"; - -export class Sounds { - sfx = {}; - songs = []; - songNames = []; - curSongIdx = 0; - elSongProgress = document.getElementById("songProgress"); - elSongText = document.getElementById("songText"); - - lowpassfilter; - audioContext; - - - /** - * @param {Game} game - */ - constructor(game) { - this.game = game; - } - - /** - * - * @param {string} audioName - * Name of audio as specified in sfxlist.json - * @param {Boolean} replace - * If true, stops currently playing audio and starts new one - * If false, skips if audio is already playing - */ - playSound(audioName, replace = true, silent = false) { - if (this.sfx[audioName] == undefined) return; - this.sfx[audioName].muted = silent; - this.sfx[audioName].volume = this.game.settings.volume.sfxLevel / 1000; - if (!replace && !this.sfx[audioName].ended && this.sfx[audioName].currentTime != 0) return; - this.sfx[audioName].currentTime = 0; - this.sfx[audioName].play(); - } - - startSong() { - this.elSongText.textContent = `Now Playing ${this.songNames[this.curSongIdx]}`; - this.songs[this.curSongIdx].onended = () => { - this.endSong(); - this.startSong(); - }; - this.songs[this.curSongIdx].volume = this.game.settings.volume.audioLevel / 1000; - this.songs[this.curSongIdx].play(); - } - - endSong() { - this.songs[this.curSongIdx].pause(); - this.songs[this.curSongIdx].currentTime = 0; - this.songs[this.curSongIdx].onended = () => { }; - this.curSongIdx = (this.curSongIdx + 1) % this.songs.length; - } - - pauseSong() { - if (this.songs[this.curSongIdx].paused) { - this.songs[this.curSongIdx].play(); - this.elSongText.textContent = `Playing ${this.songNames[this.curSongIdx]}`; - } else { - this.songs[this.curSongIdx].pause(); - this.elSongText.textContent = `Not Playing`; - - } - } - - addMenuSFX() { - let hoverSFX = (e) => { - document.querySelectorAll(e).forEach(el => (el.addEventListener("mouseenter", () => this.game.sounds.playSound("menutap")))); - }; - let clickSFX = (e) => { - document.querySelectorAll(e).forEach(el => (el.addEventListener("click", () => this.game.sounds.playSound("menuclick")))); - }; - hoverSFX(".settingRow"); - hoverSFX(".closeDialogButton"); - hoverSFX(".gamemodeSelect"); - hoverSFX(".settingPanelButton"); - clickSFX(".settingPanelButton"); - clickSFX(".closeDialogButton"); - } - - initSounds() { - setInterval(() => { - if (this.songs[this.curSongIdx].currentTime == 0) return; - this.elSongProgress.value = - (this.songs[this.curSongIdx].currentTime * 100) / this.songs[this.curSongIdx].duration; - }, 2000); - - // preload all sfx - sfxobj.forEach(file => { - const name = file.name.split(".")[0]; - const a = new Audio(file.path); - this.sfx[name] = a; - this.playSound(name, false, true); - }) - - - this.audioContext = new window.AudioContext(); - this.lowpassfilter = this.audioContext.createBiquadFilter(); - this.lowpassfilter.type = "lowpass"; - this.lowpassfilter.frequency.value = 20000; - - songsobj.forEach(file => { - const songaudio = new Audio(file.path); - this.songs.push(songaudio); - this.songNames.push(file.name.split(".")[0]); - - const track = this.audioContext.createMediaElementSource(songaudio); - track.connect(this.lowpassfilter); - this.lowpassfilter.connect(this.audioContext.destination); - }) - // this.playSound("allclear") // wait literally how is this fine by chrome - } - - setAudioLevel() { - this.songs[this.curSongIdx].volume = Number(this.game.settings.volume.audioLevel) / 1000; - } - - toggleSongMuffle(muffled) { - const currentTime = this.audioContext.currentTime; - this.lowpassfilter.frequency.cancelScheduledValues(currentTime); - this.lowpassfilter.frequency.setValueAtTime(this.lowpassfilter.frequency.value, currentTime); - this.lowpassfilter.frequency.exponentialRampToValueAtTime(muffled ? 300 : 20000, currentTime + 1); - } -} +import sfxobj from "../data/sfxlist.json" with { type: "json" }; +import { songsobj } from "../data/data.js"; +import { Game } from "../game.js"; + +export class Sounds { + sfx = {}; + songs = []; + songNames = []; + curSongIdx = 0; + elSongProgress = document.getElementById("songProgress"); + elSongText = document.getElementById("songText"); + + lowpassfilter; + audioContext; + + + /** + * @param {Game} game + */ + constructor(game) { + this.game = game; + } + + /** + * + * @param {string} audioName + * Name of audio as specified in sfxlist.json + * @param {Boolean} replace + * If true, stops currently playing audio and starts new one + * If false, skips if audio is already playing + */ + playSound(audioName, replace = true, silent = false) { + if (this.sfx[audioName] == undefined) return; + this.sfx[audioName].muted = silent; + this.sfx[audioName].volume = this.game.settings.volume.sfxLevel / 1000; + if (!replace && !this.sfx[audioName].ended && this.sfx[audioName].currentTime != 0) return; + this.sfx[audioName].currentTime = 0; + this.sfx[audioName].play(); + } + + startSong() { + this.elSongText.textContent = `Now Playing ${this.songNames[this.curSongIdx]}`; + this.songs[this.curSongIdx].onended = () => { + this.endSong(); + this.startSong(); + }; + this.songs[this.curSongIdx].volume = this.game.settings.volume.audioLevel / 1000; + this.songs[this.curSongIdx].play(); + } + + endSong() { + this.songs[this.curSongIdx].pause(); + this.songs[this.curSongIdx].currentTime = 0; + this.songs[this.curSongIdx].onended = () => { }; + this.curSongIdx = (this.curSongIdx + 1) % this.songs.length; + } + + pauseSong() { + if (this.songs[this.curSongIdx].paused) { + this.songs[this.curSongIdx].play(); + this.elSongText.textContent = `Playing ${this.songNames[this.curSongIdx]}`; + } else { + this.songs[this.curSongIdx].pause(); + this.elSongText.textContent = `Not Playing`; + + } + } + + addMenuSFX() { + let hoverSFX = (e) => { + document.querySelectorAll(e).forEach(el => (el.addEventListener("mouseenter", () => this.game.sounds.playSound("menutap")))); + }; + let clickSFX = (e) => { + document.querySelectorAll(e).forEach(el => (el.addEventListener("click", () => this.game.sounds.playSound("menuclick")))); + }; + hoverSFX(".settingRow"); + hoverSFX(".closeDialogButton"); + hoverSFX(".gamemodeSelect"); + hoverSFX(".settingPanelButton"); + clickSFX(".settingPanelButton"); + clickSFX(".closeDialogButton"); + } + + initSounds() { + setInterval(() => { + if (this.songs[this.curSongIdx].currentTime == 0) return; + this.elSongProgress.value = + (this.songs[this.curSongIdx].currentTime * 100) / this.songs[this.curSongIdx].duration; + }, 2000); + + // preload all sfx + sfxobj.forEach(file => { + const name = file.name.split(".")[0]; + const a = new Audio(file.path); + this.sfx[name] = a; + // this.playSound(name, false, true); + }) + + + this.audioContext = new window.AudioContext(); + this.lowpassfilter = this.audioContext.createBiquadFilter(); + this.lowpassfilter.type = "lowpass"; + this.lowpassfilter.frequency.value = 20000; + + songsobj.forEach(file => { + const songaudio = new Audio(file.path); + this.songs.push(songaudio); + this.songNames.push(file.name.split(".")[0]); + + const track = this.audioContext.createMediaElementSource(songaudio); + track.connect(this.lowpassfilter); + this.lowpassfilter.connect(this.audioContext.destination); + }) + } + + setAudioLevel() { + this.songs[this.curSongIdx].volume = Number(this.game.settings.volume.audioLevel) / 1000; + } + + toggleSongMuffle(muffled) { + const currentTime = this.audioContext.currentTime; + this.lowpassfilter.frequency.cancelScheduledValues(currentTime); + this.lowpassfilter.frequency.setValueAtTime(this.lowpassfilter.frequency.value, currentTime); + this.lowpassfilter.frequency.exponentialRampToValueAtTime(muffled ? 300 : 20000, currentTime + 1); + } +} diff --git a/src/game.js b/src/game.js index 3d8776f..727820f 100644 --- a/src/game.js +++ b/src/game.js @@ -1,170 +1,174 @@ -import { Bag } from "./mechanics/bag.js"; -import { Board } from "./mechanics/board.js"; -import { Controls } from "./movement/controls.js"; -import { Hold } from "./mechanics/hold.js"; -import { Mechanics } from "./mechanics/mechanics.js"; -import { MenuActions } from "./menus/menuactions.js"; -import { ModalActions } from "./menus/modals.js"; -import { Movement } from "./movement/movement.js"; -import { Renderer } from "./display/renderer.js"; -import { Settings } from "./features/settings.js"; -import { Sounds } from "./features/sounds.js"; -import { Falling } from "./mechanics/fallingpiece.js"; -import { GameStats } from "./features/stats.js"; -import { BoardEditor } from "./features/editboard.js"; -import { History } from "./features/history.js"; -import { BoardEffects } from "./display/boardEffects.js"; -import { ProfileStats } from "./features/profileStats.js"; -import { Modes } from "./features/modes.js"; -import { BoardRenderer } from "./display/renderBoard.js"; -import { Particles } from "./display/particles.js"; -import { Zenith, Grandmaster } from "./mechanics/gamemode_extended.js"; - -export class Game { - started; - ended; - gameTimer = 0; // id of timeout - survivalTimer = 0; // id of timeout - gravityTimer = 0; - zenithTimer = 0; - grandmasterTimer = 0; - version = '1.3.2'; - tickrate = 60; - - elementReason = document.getElementById("reason"); - elementResult = document.getElementById("result"); - elementGameEndTitle = document.getElementById("gameEndTitle"); - - - constructor() { - this.boardeffects = new BoardEffects(this); - this.profilestats = new ProfileStats(this); - this.stats = new GameStats(this); - this.falling = new Falling(this); - this.settings = new Settings(this); - this.hold = new Hold(this); - this.sounds = new Sounds(this); - this.board = new Board(this); - this.bag = new Bag(this); - this.mechanics = new Mechanics(this); - this.menuactions = new MenuActions(this); - this.modals = new ModalActions(this); - this.movement = new Movement(this); - this.renderer = new Renderer(this); - this.boardrender = new BoardRenderer(this); - this.particles = new Particles(this); - this.boardeditor = new BoardEditor(this); - this.controls = new Controls(this); - this.history = new History(this); - this.modes = new Modes(this); - this.zenith = new Zenith(this); - this.grandmaster = new Grandmaster(this); - - this.renderer.sizeCanvas(); - this.particles.initBoard(); - this.renderer.setEditPieceColours(); - this.sounds.initSounds(); - this.startGame(); - this.renderer.renderingLoop(); - this.boardeditor.addListeners(); - this.menuactions.addRangeListener(); - this.modals.generate.addMenuListeners(); - this.modals.generate.generateGamemodeMenu(); - this.modals.generate.generateStatList(); - this.modals.generate.generateSkinList(); - this.sounds.addMenuSFX(); - this.profilestats.loadPBs(); - this.versionChecker(); - } - - startGame() { - this.menuactions.loadSettings(); - this.resetState(); - this.renderer.renderStyles(); - this.mechanics.spawnPiece(this.bag.randomiser(true), true); - this.history.save(); - } - - stopGameTimers() { //stop all the game's timers - clearInterval(this.gravityTimer); - clearInterval(this.gameTimer); - clearInterval(this.survivalTimer); - clearInterval(this.zenithTimer); - clearInterval(this.grandmasterTimer); - this.mechanics.locking.lockingPause(); - } - - endGame(top, bottom = "Better luck next time") { - const dead = ["Lockout", "Topout", "Blockout"].includes(top); // survival mode end instead of lose - if (this.settings.game.gamemode == 'survival' && dead) { - this.ended = true; - return; - } - - if (top == "Topout" || top == "Blockout" || top == "Lockout") { - this.sounds.playSound("topout"); - this.sounds.playSound("failure"); - } else if (top == undefined) { - return; - } else { - this.sounds.playSound("finish"); - } - - this.ended = true; - this.modals.openModal("gameEnd"); - this.stopGameTimers() - this.elementReason.textContent = top; - this.elementResult.textContent = bottom; - this.profilestats.saveSession(); - } - - resetState() { - this.boardeffects.hasPace = true; - this.boardeffects.paceCooldown = 0; - this.boardrender.boardAlpha = 1; - this.boardrender.queueAlpha = 1; - this.renderer.inDanger = false; - this.started = false; - this.ended = false; - - this.board.resetBoard(); - this.mechanics.locking.clearLockDelay(); - this.boardeffects.toggleRainbow(false); - this.renderer.resetActionText(); - this.renderer.renderDanger(); - this.particles.clearParticles(); - this.renderer.clearHold(); - this.stopGameTimers(); - - this.bag = new Bag(this); - this.mechanics = new Mechanics(this); - this.falling = new Falling(this); - this.hold = new Hold(this); - this.stats = new GameStats(this); - this.history = new History(this); - this.zenith = new Zenith(this); - this.grandmaster = new Grandmaster(this); - - - this.renderer.renderSidebar(); - this.modes.checkFinished(); - this.stats.updateStats(); - this.renderer.updateAlpha(); - this.boardeffects.rainbowBoard(); - } - - gameClock() { - this.renderer.renderSidebar(); - this.modes.checkFinished(); - this.stats.updateStats(); - this.renderer.updateAlpha(); - this.boardeffects.rainbowBoard(); - } - - versionChecker() { - const userver = window.localStorage.getItem('version'); - document.getElementById('updatetext').style.display = this.version == userver ? "none" : "block"; - window.localStorage.setItem('version', this.version); - } - -} +import { Bag } from "./mechanics/bag.js"; +import { Board } from "./mechanics/board.js"; +import { Controls } from "./movement/controls.js"; +import { Hold } from "./mechanics/hold.js"; +import { Mechanics } from "./mechanics/mechanics.js"; +import { MenuActions } from "./menus/menuactions.js"; +import { ModalActions } from "./menus/modals.js"; +import { Movement } from "./movement/movement.js"; +import { Renderer } from "./display/renderer.js"; +import { Settings } from "./features/settings.js"; +import { Sounds } from "./features/sounds.js"; +import { Falling } from "./mechanics/fallingpiece.js"; +import { GameStats } from "./features/stats.js"; +import { BoardEditor } from "./features/editboard.js"; +import { History } from "./features/history.js"; +import { BoardEffects } from "./display/boardEffects.js"; +import { ProfileStats } from "./features/profileStats.js"; +import { Modes } from "./features/modes.js"; +import { Particles } from "./display/particles.js"; +import { Zenith, Grandmaster } from "./mechanics/gamemode_extended.js"; +import { PixiRender } from "./display/pixirender.js"; + +export class Game { + started; + ended; + gameTimer = 0; // id of timeout + survivalTimer = 0; // id of timeout + gravityTimer = 0; + zenithTimer = 0; + grandmasterTimer = 0; + version = '1.3.5'; + tickrate = 60; + + elementReason = document.getElementById("reason"); + elementResult = document.getElementById("result"); + elementGameEndTitle = document.getElementById("gameEndTitle"); + + + constructor() { + this.boardeffects = new BoardEffects(this); + this.profilestats = new ProfileStats(this); + this.stats = new GameStats(this); + this.falling = new Falling(this); + this.settings = new Settings(this); + this.hold = new Hold(this); + this.sounds = new Sounds(this); + this.board = new Board(this); + this.bag = new Bag(this); + this.mechanics = new Mechanics(this); + this.menuactions = new MenuActions(this); + this.modals = new ModalActions(this); + this.movement = new Movement(this); + this.renderer = new Renderer(this); + this.particles = new Particles(this); + this.boardeditor = new BoardEditor(this); + this.controls = new Controls(this); + this.history = new History(this); + this.modes = new Modes(this); + this.zenith = new Zenith(this); + this.grandmaster = new Grandmaster(this);; + this.pixi = new PixiRender(this); + this.init(); + } + + async init() { + this.menuactions.loadSettings(); + this.board.resetBoard(); + await this.pixi.init(); + this.modes.loadModes(); + this.renderer.renderStyles(); + this.renderer.setEditPieceColours(); + this.sounds.initSounds(); + this.startGame(); + this.menuactions.addRangeListener(); + this.modals.generate.addMenuListeners(); + this.modals.generate.generateGamemodeMenu(); + this.modals.generate.generateStatList(); + this.modals.generate.generateSkinList(); + this.sounds.addMenuSFX(); + this.profilestats.loadPBs(); + this.versionChecker(); + } + + startGame() { + this.menuactions.loadSettings(); + this.modes.loadModes(); + this.resetState(); + this.renderer.renderStyles(); + this.mechanics.spawnPiece(this.bag.randomiser(true), true); + this.history.save(); + } + + stopGameTimers() { //stop all the game's timers + clearInterval(this.gravityTimer); + clearInterval(this.gameTimer); + clearInterval(this.survivalTimer); + clearInterval(this.zenithTimer); + clearInterval(this.grandmasterTimer); + this.mechanics.locking.lockingPause(); + } + + endGame(top, bottom = "Better luck next time") { + const dead = ["Lockout", "Topout", "Blockout"].includes(top); // survival mode end instead of lose + if (this.settings.game.gamemode == 'survival' && dead) { + this.ended = true; + return; + } + + if (top == "Topout" || top == "Blockout" || top == "Lockout") { + this.sounds.playSound("topout"); + this.sounds.playSound("failure"); + } else if (top == undefined) { + return; + } else { + this.sounds.playSound("finish"); + } + + this.ended = true; + this.modals.openModal("gameEnd"); + this.stopGameTimers() + this.elementReason.textContent = top; + this.elementResult.textContent = bottom; + this.profilestats.saveSession(); + } + + resetState() { + this.boardeffects.hasPace = true; + this.boardeffects.paceCooldown = 0; + this.pixi.boardAlpha = 1; + this.pixi.queueAlpha = 1; + this.renderer.inDanger = false; + this.started = false; + this.ended = false; + + this.board.resetBoard(); + this.mechanics.locking.clearLockDelay(); + this.boardeffects.toggleRainbow(false); + this.renderer.renderDanger(); + this.particles.clearParticles(); + this.renderer.clearHold(); + this.stopGameTimers(); + this.pixi.resetActionTexts(); + + this.bag = new Bag(this); + this.mechanics = new Mechanics(this); + this.falling = new Falling(this); + this.hold = new Hold(this); + this.stats = new GameStats(this); + this.history = new History(this); + this.zenith = new Zenith(this); + this.grandmaster = new Grandmaster(this); + + + this.renderer.renderSidebar(); + this.modes.checkFinished(); + this.stats.updateStats(); + this.pixi.updateAlpha(); + this.boardeffects.rainbowBoard(); + } + + gameClock() { + this.renderer.renderSidebar(); + this.modes.checkFinished(); + this.stats.updateStats(); + this.boardeffects.rainbowBoard(); + } + + versionChecker() { + const userver = window.localStorage.getItem('version'); + document.getElementById('updatetext').style.display = this.version == userver ? "none" : "block"; + window.localStorage.setItem('version', this.version); + } + +} diff --git a/src/main.js b/src/main.js index fa50ce4..86d1cd3 100644 --- a/src/main.js +++ b/src/main.js @@ -1,63 +1,68 @@ -import { Game } from "./game.js"; - -const game = new Game(); - -console.log("%cTETI", "color: #DFC0F3;\n\t\t\t\t\t display: block;\n\t\t\t\t\t font-size: 5em;\n\t\t\t\t\t font-weight: 900;\n\t\t\t\t\t text-shadow: 0px 0px 2px #9150BA;\n\t\t\t\t\t background-color: #45345088;\n\t\t\t\t\t padding: 0 0.25em;\n\t\t\t\t\t border-radius: 3px;"), - - -// allow html to access functions -window["menu"] = game.menuactions; -window["modal"] = game.modals; -window["songs"] = game.sounds; - -const elementSplashScreen = document.getElementById("splashScreen"); -const elementSplashText = document.getElementById("splashText"); - -window.addEventListener("keydown", event => { - if (event.key == undefined) return; - let key = event.key.length > 1 ? event.key : event.key.toLowerCase(); // 1 letter words are lowercase - if (event.altKey) key = "Alt+" + key; - if (event.ctrlKey) key = "Ctrl+" + key; - - game.controls.onKeyDownRepeat(event, key); - if (event.repeat) return; - game.controls.onKeyDown(event, key); -}) - -window.addEventListener("keyup", event => { - if (event.key == undefined) return; - let key = event.key.length > 1 ? event.key : event.key.toLowerCase(); - game.controls.onKeyUp(event, key); -}); - -window.addEventListener('mousemove', () => { - game.controls.toggleCursor(true); -}) - -document.onresize = () => { - game.renderer.sizeCanvas(); - game.renderer.updateNext(); - game.renderer.updateHold(); -} - -// splash menu -window.addEventListener("DOMContentLoaded", () => { - elementSplashText.textContent = "Ready"; - elementSplashScreen.style.opacity = 0; - elementSplashScreen.style.scale = 1.2; - elementSplashScreen.style.display = "none"; - document.getElementById("ignoreText").style.opacity = 0.5; -}) - -window.addEventListener("focus", function () { - document.getElementById("nofocus").style.display = "none"; -}); - -window.addEventListener("blur", function () { - if (!game.settings.display.outoffocus) return - document.getElementById("nofocus").style.display = "block"; -}); - -window.onerror = (msg, url, lineNo, columnNo, error) => { - game.modals.generate.notif(error, msg + ". ln "+ lineNo, "error"); -} +import { Game } from "./game.js"; + +const game = new Game(); + +console.log("%cTETI", "color: #DFC0F3;\n\t\t\t\t\t display: block;\n\t\t\t\t\t font-size: 5em;\n\t\t\t\t\t font-weight: 900;\n\t\t\t\t\t text-shadow: 0px 0px 2px #9150BA;\n\t\t\t\t\t background-color: #45345088;\n\t\t\t\t\t padding: 0 0.25em;\n\t\t\t\t\t border-radius: 3px;"), + + +// allow html to access functions +window["menu"] = game.menuactions; +window["modal"] = game.modals; +window["songs"] = game.sounds; + +window.addEventListener("keydown", event => { + if (event.key == undefined) return; + let key = event.key.length > 1 ? event.key : event.key.toLowerCase(); // 1 letter words are lowercase + if (event.altKey) key = "Alt+" + key; + if (event.ctrlKey) key = "Ctrl+" + key; + + game.controls.onKeyDownRepeat(event, key); + if (event.repeat) return; + game.controls.onKeyDown(event, key); +}); + +window.addEventListener("keyup", event => { + if (event.key == undefined) return; + let key = event.key.length > 1 ? event.key : event.key.toLowerCase(); + game.controls.onKeyUp(event, key); +}); + +window.addEventListener('mousemove', () => { + game.controls.toggleCursor(true); +}) + +document.body.addEventListener("mouseup", (e) => { + game.boardeditor.mouseUp(e); +}); + +window.addEventListener("resize", () => { + setTimeout(() => { + game.pixi.resize(); + game.renderer.updateNext(); + game.renderer.updateHold(); + }, 0); +}) + + +const elementSplashScreen = document.getElementById("splashScreen"); +const elementSplashText = document.getElementById("splashText"); +export function clearSplash() { + elementSplashText.textContent = "Ready"; + elementSplashScreen.style.opacity = 0; + elementSplashScreen.style.scale = 1.2; + elementSplashScreen.style.display = "none"; + document.getElementById("ignoreText").style.opacity = 0.5; +} + +window.addEventListener("focus", function () { + document.getElementById("nofocus").style.display = "none"; +}); + +window.addEventListener("blur", function () { + if (!game.settings.display.outoffocus) return + document.getElementById("nofocus").style.display = "block"; +}); + +window.onerror = (msg, url, lineNo, columnNo, error) => { + game.modals.generate.notif(error, msg + ". ln " + lineNo, "error"); +} diff --git a/src/mechanics/board.js b/src/mechanics/board.js index 23016e3..33426ad 100644 --- a/src/mechanics/board.js +++ b/src/mechanics/board.js @@ -1,127 +1,131 @@ -import { Game } from "../game.js"; - -export class Board { - /** - * @type {string[][]} - */ - boardState = []; - - /** - * @param {Game} game - */ - constructor(game) { - this.game = game; - } - // modify board - checkMino([x, y], val) { - return this.boardState[y][x].split(" ").includes(val); - } - - MinoToNone(val) { - this.getMinos(val).forEach(([x, y]) => this.rmValue([x, y], val)); - } - - EradicateMinoCells(val) { - this.getCoords(this.boardState, c => c.includes(val), [0, 0]).forEach(([x, y]) => this.rmValue([x, y], val)); - } - - addMinos(val, c, [dx, dy]) { - c.forEach(([x, y]) => this.setValue([x + dx, y + dy], val)); - } - - addValFront([x, y], val) { - this.boardState[y][x] = `${val} ${this.boardState[y][x]}`; - } - - addValue([x, y], val) { - this.boardState[y][x] = (this.boardState[y][x] + " " + val).trim(); - } - - setValue([x, y], val) { - this.boardState[y][x] = val; - } - - rmValue([x, y], val) { - this.boardState[y][x] = this.boardState[y][x].replace(val, "").trim(); - } - - getMinos(name) { - return this.getCoords(this.boardState, c => c.split(" ").includes(name), [0, 0]); - } - - pieceToCoords(arr, [dx, dy] = [0, 0]) { - return this.getCoords(arr.toReversed(), c => c == 1, [dx, dy]); - } - - setCoordEmpty([x, y]) { - this.boardState[y][x] = ""; - } - - resetBoard() { - this.boardState = [...Array(40)].map(() => [...Array(10)].map(() => "")); - } - - getFullRows() { - const rows = this.getMinos("S") - .map(coord => coord[1]) - .reduce((prev, curr) => ((prev[curr] = ++prev[curr] || 1), prev), {}); - return Object.keys(rows) - .filter(key => rows[key] >= 10) - .map(row => +row) - .toReversed(); - } - - getCoords(array, filter, [dx, dy]) { - const coords = []; - array.forEach((row, y) => - row.forEach((col, x) => { - if (filter(col)) coords.push([x + dx, y + dy]); - }) - ); - return coords; - } - - moveMinos(coords, dir, size, value = "") { - const getChange = ([x, y], a) => { - return { RIGHT: [x + a, y], LEFT: [x - a, y], DOWN: [x, y - a], UP: [x, y + a] }; - }; - const newcoords = coords.map(c => getChange(c, size)[dir]); - - if (newcoords.some(([x, y]) => y > 39)) { - this.game.endGame("Topout"); - return; - } - - const valTable = coords.map(([x, y]) => (value ? value : this.boardState[y][x])); - coords.forEach((c, idx) => this.rmValue(c, valTable[idx])); - - newcoords.forEach((c, idx) => - value ? this.addValue(c, valTable[idx]) : this.setValue(c, valTable[idx]) - ); - this.game.mechanics.spawnOverlay(); - } - - setComboBoard(start) { - // 4w sides - this.boardState.forEach((row, y) => - row.forEach((col, x) => { - if (x < 3 || x > 6) this.addMinos("S G", [[x, y]], [0, 0]); - }) - ); - - if (!start) return; - // garbage pattern - const validCoords = [[[0, 0], [1, 0], [2, 0], [3, 0]], [[0, 1], [1, 1], [2, 1], [3, 1]]]; - const garbAmount = Math.random() > 0.5 ? 3 : 6; - const garbCoords = []; - for (let i = 0; i < garbAmount; i++) { - const y = Math.random() > 0.5 ? 0 : 1; - if (validCoords[y].length == 1) { i--; continue; } - const coord = validCoords[y].splice(Math.floor(Math.random() * validCoords[y].length), 1); - garbCoords.push(coord[0]); - } - - this.addMinos("S G", garbCoords.map(([x, y]) => [x + 3, y]), [0, 0]); - this.game.mechanics.setShadow(); - } -} +import { Game } from "../game.js"; + +export class Board { + /** + * @type {string[][]} + */ + boardState = []; + + /** + * @param {Game} game + */ + constructor(game) { + this.game = game; + } + // modify board + checkMino([x, y], val) { + return this.boardState[y][x].split(" ").includes(val); + } + + MinoToNone(val) { + this.getMinos(val).forEach(([x, y]) => this.rmValue([x, y], val)); + } + + EradicateMinoCells(val) { + this.getCoords(this.boardState, c => c.includes(val), [0, 0]).forEach(([x, y]) => this.rmValue([x, y], val)); + } + + addMinos(val, c, [dx, dy]) { + c.forEach(([x, y]) => this.setValue([x + dx, y + dy], val)); + } + + addValFront([x, y], val) { + this.boardState[y][x] = `${val} ${this.boardState[y][x]}`; + } + + addValue([x, y], val) { + this.boardState[y][x] = (this.boardState[y][x] + " " + val).trim(); + } + + setValue([x, y], val) { + this.boardState[y][x] = val; + } + + rmValue([x, y], val) { + this.boardState[y][x] = this.boardState[y][x].replace(val, "").trim(); + } + + getMinos(name) { + return this.getCoords(this.boardState, c => c.split(" ").includes(name), [0, 0]); + } + + pieceToCoords(arr, [dx, dy] = [0, 0]) { + return this.getCoords(arr.toReversed(), c => c == 1, [dx, dy]); + } + + setCoordEmpty([x, y]) { + this.boardState[y][x] = ""; + } + + resetBoard() { + this.boardState = [...Array(40)].map(() => [...Array(10)].map(() => "")); + } + + getFullRows() { + const rows = this.getMinos("S") + .map(coord => coord[1]) + .reduce((prev, curr) => ((prev[curr] = ++prev[curr] || 1), prev), {}); + return Object.keys(rows) + .filter(key => rows[key] >= 10) + .map(row => +row) + .toReversed(); + } + + getCoords(array, filter, [dx, dy]) { + const coords = []; + array.forEach((row, y) => + row.forEach((col, x) => { + if (filter(col)) coords.push([x + dx, y + dy]); + }) + ); + return coords; + } + + moveMinos(coords, dir, size, value = "") { + const getChange = ([x, y], a) => { + return { RIGHT: [x + a, y], LEFT: [x - a, y], DOWN: [x, y - a], UP: [x, y + a] }; + }; + const newcoords = coords.map(c => getChange(c, size)[dir]); + + if (newcoords.some(([x, y]) => y > 39)) { + this.game.endGame("Topout"); + return; + } + + const valTable = coords.map(([x, y]) => (value ? value : this.boardState[y][x])); + coords.forEach((c, idx) => this.rmValue(c, valTable[idx])); + + newcoords.forEach((c, idx) => + value ? this.addValue(c, valTable[idx]) : this.setValue(c, valTable[idx]) + ); + this.game.mechanics.spawnOverlay(); + } + + setComboBoard(start) { + // 4w sides + + const board = JSON.parse(JSON.stringify(this.boardState)); + board.forEach((row, y) => { + row.forEach((col, x) => { + if ((x > 2 && x < 7) || y > 30) return; + this.setValue([x, y], 'S G') + }) + }) + + if (!start) return; + // garbage pattern + const validCoords = [[[0, 0], [1, 0], [2, 0], [3, 0]], [[0, 1], [1, 1], [2, 1], [3, 1]]]; + const garbAmount = Math.random() > 0.5 ? 3 : 6; + const garbCoords = []; + for (let i = 0; i < garbAmount; i++) { + const y = Math.random() > 0.5 ? 0 : 1; + if (validCoords[y].length == 1) { i--; continue; } + const coord = validCoords[y].splice(Math.floor(Math.random() * validCoords[y].length), 1); + garbCoords.push(coord[0]); + } + + this.addMinos("S G", garbCoords.map(([x, y]) => [x + 3, y]), [0, 0]); + this.game.mechanics.setShadow(); + + } +} diff --git a/src/mechanics/clearlines.js b/src/mechanics/clearlines.js index 3964865..6288322 100644 --- a/src/mechanics/clearlines.js +++ b/src/mechanics/clearlines.js @@ -24,7 +24,7 @@ export class ClearLines { removedGarbage++; stopped.filter(c => c[1] == row).forEach(([x, y]) => { // clear rows this.game.mechanics.board.setCoordEmpty([x, y]); - this.game.boardrender.removeCoords([x, y]); + this.game.pixi.endFlash([x, y]); }); this.game.mechanics.board.moveMinos(stopped.filter(c => c[1] > row), "DOWN", 1); } diff --git a/src/mechanics/gamemode_extended.js b/src/mechanics/gamemode_extended.js index 251c268..0ea9167 100644 --- a/src/mechanics/gamemode_extended.js +++ b/src/mechanics/gamemode_extended.js @@ -109,8 +109,9 @@ export class Zenith { climbSpeedBar.value = point climbSpeedBar.max = require - document.styleSheets[1].cssRules[24].style.backgroundColor = color[speed - 1] - document.styleSheets[1].cssRules[23].style.backgroundColor = color[speed] + // changes css variable, better selection + document.getElementById("climbSpeedBar").style.setProperty("--background-colour", color[speed-1]) + document.getElementById("climbSpeedBar").style.setProperty("--bar-colour", color[speed]) } } diff --git a/src/mechanics/locking.js b/src/mechanics/locking.js index add4963..330f7b5 100644 --- a/src/mechanics/locking.js +++ b/src/mechanics/locking.js @@ -1,135 +1,137 @@ -import { Game } from "../game.js"; - -export class LockPiece { - divLockTimer = document.getElementById("lockTimer"); - divLockCounter = document.getElementById("lockCounter"); - lockCount; - timings = { lockdelay: 0, lockingTimer: 0, clearDelay: 0 } - - startTime = 0; - remaining = 0; - - /** - * @param {Game} game - */ - constructor(game) { - this.game = game; - } - - incrementLock() { - if (this.timings.lockdelay != 0) { - this.lockCount++; - this.game.mechanics.locking.clearLockDelay(false); - if (this.game.settings.game.maxLockMovements != 0 && this.game.settings.display.lockBar) { - const amountToAdd = 100 / this.game.settings.game.maxLockMovements; - this.divLockCounter.value += amountToAdd; - } - } - if (this.game.movement.checkCollision(this.game.mechanics.board.getMinos("A"), "DOWN")) { - this.game.mechanics.locking.scheduleLock(); - } - } - - scheduleLock() { - const LockMoves = - this.game.settings.game.maxLockMovements == 0 - ? Infinity - : this.game.settings.game.maxLockMovements; - if (this.lockCount >= LockMoves) { - this.game.mechanics.locking.lockPiece(); - return; - } - if (this.game.settings.game.lockDelay == 0) return; - - this.lockDelayStart(this.game.settings.game.lockDelay); - } - - lockDelayStart(delay) { - clearTimeout(this.timings.lockdelay); - clearInterval(this.timings.lockingTimer); - this.startTime = Date.now(); - this.timings.lockdelay = setTimeout( - () => this.game.mechanics.locking.lockPiece(), - delay); - this.timings.lockingTimer = setInterval(() => { - const amountToAdd = 1000 / this.game.settings.game.lockDelay; - if (this.game.settings.display.lockBar) this.divLockTimer.value += amountToAdd; - }, 10); - } - - lockingPause() { - if (this.timings.lockdelay == 0) return; - this.remaining = this.game.settings.game.lockDelay - (Date.now() - this.startTime); - clearTimeout(this.timings.lockdelay); - clearInterval(this.timings.lockingTimer); - } - - lockingResume() { - if (this.timings.lockdelay == 0) return; - this.lockDelayStart(this.remaining); - } - - lockPiece() { - const lockCoords = this.game.mechanics.board.getMinos("A"); - this.game.boardrender.justPlacedCoords = lockCoords; - this.game.boardrender.justPlacedAlpha = 1; - - lockCoords.forEach(([x, y]) => { - this.game.boardrender.flashTimes.push({ c: [x, y], t: 15 }) - this.game.mechanics.board.rmValue([x, y], "A"); - this.game.mechanics.board.addValFront([x, y], "S"); - }); - - this.game.mechanics.locking.clearLockDelay(); - clearInterval(this.game.gravityTimer); - const cleared = this.game.mechanics.clear.clearLines(lockCoords); - - this.game.endGame( // check stopped overlap next - this.game.mechanics.checkDeath( - this.game.mechanics.board.getMinos("S"), - this.game.mechanics.board.getMinos("NP") - ) - ); - this.game.endGame( // check lockout - this.game.mechanics.checkDeath( - lockCoords, - this.game.mechanics.board.getMinos("NP") - ) - ); - this.game.stats.pieceCount++; - this.game.hold.occured = false; - this.game.mechanics.isTspin = false; - this.game.mechanics.isAllspin = false; - this.game.mechanics.isMini = false; - this.game.falling.moved = false; - if (this.game.stats.tgm_level % 100 != 99 && this.game.stats.tgm_level != this.game.settings.game.raceTarget - 1) this.game.stats.tgm_level++; - - const xvals = [...new Set(lockCoords.map(([x, y]) => x))]; - const yval = Math.min(...lockCoords.map(([x, y]) => y)); - this.game.particles.spawnParticles(Math.min(...xvals) + 1, yval, "lock", xvals.length); - this.game.renderer.renderDanger(); - - const delay = (cleared > 0) ? this.game.settings.game.clearDelay : 0; - this.timings.clearDelay = setTimeout(() => { - this.game.mechanics.spawnPiece(this.game.bag.randomiser()); - this.game.history.save(); - this.timings.clearDelay = 0; - }, delay); - } - - clearLockDelay(clearCount = true) { - clearInterval(this.timings.lockingTimer); - this.stopTimeout("lockdelay"); - this.divLockTimer.value = 0; - if (!clearCount) return; - this.divLockCounter.value = 0; - this.lockCount = 0; - if (this.game.settings.game.preserveARR) return; - this.game.controls.resetMovements(); - } - - stopTimeout(name) { - clearTimeout(this.timings[name]); - this.timings[name] = 0; - } -} +import { Game } from "../game.js"; + +export class LockPiece { + divLockTimer = document.getElementById("lockTimer"); + divLockCounter = document.getElementById("lockCounter"); + lockCount; + timings = { lockdelay: 0, lockingTimer: 0, clearDelay: 0 } + + startTime = 0; + remaining = 0; + + /** + * @param {Game} game + */ + constructor(game) { + this.game = game; + } + + incrementLock() { + if (this.timings.lockdelay != 0) { + this.lockCount++; + this.game.mechanics.locking.clearLockDelay(false); + if (this.game.settings.game.maxLockMovements != 0 && this.game.settings.display.lockBar) { + const amountToAdd = 100 / this.game.settings.game.maxLockMovements; + this.divLockCounter.value += amountToAdd; + } + } + if (this.game.movement.checkCollision(this.game.mechanics.board.getMinos("A"), "DOWN")) { + this.game.mechanics.locking.scheduleLock(); + } + } + + scheduleLock() { + const LockMoves = + this.game.settings.game.maxLockMovements == 0 + ? Infinity + : this.game.settings.game.maxLockMovements; + if (this.lockCount >= LockMoves) { + this.game.mechanics.locking.lockPiece(); + return; + } + if (this.game.settings.game.lockDelay == 0) return; + + this.lockDelayStart(this.game.settings.game.lockDelay); + } + + lockDelayStart(delay) { + clearTimeout(this.timings.lockdelay); + clearInterval(this.timings.lockingTimer); + this.startTime = Date.now(); + this.timings.lockdelay = setTimeout( + () => this.game.mechanics.locking.lockPiece(), + delay); + this.timings.lockingTimer = setInterval(() => { + const amountToAdd = 1000 / this.game.settings.game.lockDelay; + if (this.game.settings.display.lockBar) this.divLockTimer.value += amountToAdd; + }, 10); + } + + lockingPause() { + if (this.timings.lockdelay == 0) return; + this.remaining = this.game.settings.game.lockDelay - (Date.now() - this.startTime); + clearTimeout(this.timings.lockdelay); + clearInterval(this.timings.lockingTimer); + } + + lockingResume() { + if (this.timings.lockdelay == 0) return; + this.lockDelayStart(this.remaining); + } + + lockPiece() { + const lockCoords = this.game.mechanics.board.getMinos("A"); + this.game.pixi.justPlacedCoords = lockCoords; + this.game.pixi.justPlacedAlpha = 1; + + lockCoords.forEach(([x, y]) => { + this.game.mechanics.board.rmValue([x, y], "A"); + this.game.mechanics.board.addValFront([x, y], "S"); + }); + this.game.pixi.flash(lockCoords); + + this.game.mechanics.locking.clearLockDelay(); + clearInterval(this.game.gravityTimer); + const cleared = this.game.mechanics.clear.clearLines(lockCoords); + this.game.endGame( // check stopped overlap next + this.game.mechanics.checkDeath( + this.game.mechanics.board.getMinos("S"), + this.game.mechanics.board.getMinos("NP") + ) + ); + this.game.endGame( // check lockout + this.game.mechanics.checkDeath( + lockCoords, + this.game.mechanics.board.getMinos("NP") + ) + ); + this.game.stats.pieceCount++; + this.game.hold.occured = false; + this.game.mechanics.isTspin = false; + this.game.mechanics.isAllspin = false; + this.game.mechanics.isMini = false; + this.game.falling.moved = false; + if (this.game.stats.tgm_level % 100 != 99 && this.game.stats.tgm_level != this.game.settings.game.raceTarget - 1) this.game.stats.tgm_level++; + + + const xvals = [...new Set(lockCoords.map(([x, y]) => x))]; + const yval = Math.min(...lockCoords.map(([x, y]) => y)); + this.game.particles.spawnParticles(Math.min(...xvals), yval, "lock", xvals.length); + this.game.renderer.renderDanger(); + + const delay = (cleared > 0) ? this.game.settings.game.clearDelay : 0; + const onClear = () => { + this.game.mechanics.spawnPiece(this.game.bag.randomiser()); + this.game.history.save(); + this.timings.clearDelay = 0; + } + if (delay == 0) onClear(); + else this.timings.clearDelay = setTimeout(() => onClear(), delay); + } + + clearLockDelay(clearCount = true) { + clearInterval(this.timings.lockingTimer); + this.stopTimeout("lockdelay"); + this.divLockTimer.value = 0; + if (!clearCount) return; + this.divLockCounter.value = 0; + this.lockCount = 0; + if (this.game.settings.game.preserveARR) return; + this.game.controls.resetMovements(); + } + + stopTimeout(name) { + clearTimeout(this.timings[name]); + this.timings[name] = 0; + } +} diff --git a/src/menus/menuactions.js b/src/menus/menuactions.js index 96f5e5c..c92e70a 100644 --- a/src/menus/menuactions.js +++ b/src/menus/menuactions.js @@ -1,240 +1,236 @@ -import { Game } from "../game.js"; -import { toExpValue } from "./modals.js"; - -export class MenuActions { - bindingKey; - menus; - elementSelectKeyText = document.getElementById("selectkeytext"); - controlUsed = false; - altUsed = false; - - /** - * @param {Game} game - */ - constructor(game) { - this.game = game; - } - - // sliders - sliderChange(el) { - const text = el.parentElement.children[0].textContent.split(":")[0]; - let value = el.value; - if (el.classList[2] == "exp") value = toExpValue(value); - if (el.classList[2] == "exp" && value > 1000) value = "None"; - el.parentElement.children[0].textContent = `${text}: ${value}`; - } - - addRangeListener() { - document.body.addEventListener("click", (event) => { - if (event.target.dataset.isRange == "true") { - const el = event.target.children[1] - this.game.modals.selectedRangeElement = el; - this.menus.openModal("changeRangeValue"); - document.getElementById("rangeValue").value = el.value; - } - }) - } - - rangeClickInit(el) { - el.parentElement.dataset.isRange = true; - } - - buttonInput(el) { - document.getElementById("frontdrop").showModal(); - this.bindingKey = el.id; - } - - checkValue(el, el2 = this.game.modals.selectedRangeElement) { - this.game.modals.selectedRangeElement = el2; - if (el.value == "") return; - if (el.value < Number(el2.min)) el.value = Number(el2.min); - if (el.value > Number(el2.max)) el.value = Number(el2.max); - } - - // keybinds - checkKeybind(event) { - if (!event.ctrlKey) this.controlUsed = false; - if (!event.altKey) this.altUsed = false; - this.elementSelectKeyText.textContent = "Click to remove keybind"; - - } - - setKeybind(event) { - if (!this.controlUsed && event.ctrlKey) { - this.controlUsed = true; - this.elementSelectKeyText.textContent += ". Control modifier used"; - return; - } - if (!this.altUsed && event.altKey) { - this.altUsed = true; - this.elementSelectKeyText.textContent += ". Alt modifier used"; - return; - } - - const key = (event.ctrlKey ? "Ctrl+" : "") + (event.altKey ? "Alt+" : "") + event.key; - document.getElementById(this.bindingKey).textContent = key; - for (let i in this.game.settings.control) { - if (i == this.bindingKey) continue; - const otherKeys = document.getElementById(i); - if (otherKeys.textContent == key) otherKeys.textContent = "None"; - } - this.menus.closeDialog(document.getElementById("frontdrop")); - this.game.modals.open = true; - this.bindingKey = undefined; - this.controlUsed = false; - this.altUsed = false; - this.elementSelectKeyText.textContent = "Click to remove keybind"; - } - - // settings - saveSettings() { - const data = this.game.settings.save(); - localStorage.setItem("settings", JSON.stringify(data)); - } - - loadSettings() { - const data = localStorage.getItem("settings") ?? "{}"; - this.game.settings.load(JSON.parse(data)) - this.game.modes.loadModes(); - this.game.history.setHistoryDiv(this.game.settings.game.gamemode == 'custom'); - this.game.boardeditor.setEditButton(this.game.settings.game.gamemode == 'custom'); - } - - setGamemode(mode) { - this.game.modes.setGamemode(mode); - this.game.modes.loadModes(); - this.game.history.setHistoryDiv(this.game.settings.game.gamemode == 'custom'); - this.game.boardeditor.setEditButton(this.game.settings.game.gamemode == 'custom'); - } - - downloadSettings() { - this.saveSettings(); - let el = document.createElement("a"); - const text = localStorage.getItem("settings"); - el.setAttribute("href", "data:text/plain;charset=utf-8," + encodeURIComponent(text)); - el.setAttribute("download", "settings.teti"); - document.body.appendChild(el); - el.click(); - document.body.removeChild(el); - } - - uploadSettings(el) { - const reader = new FileReader(); - reader.readAsText(el.files[0]); - reader.onload = () => { - localStorage.setItem("settings", reader.result.toString()); - this.loadSettings(); - this.game.modals.generate.notif("Settings Loaded", "User settings have successfully loaded", "message"); - }; - } - - resetSettings(group) { - this.game.settings.reset(group); - this.saveSettings(); - location.reload(); - this.game.modals.generate.notif("Settings Reset", `${group} settings have been reset to default`, "message"); - } - - // menu - toggleDialog() { - if (this.game.menuactions.bindingKey != undefined) return; - if (!this.game.modals.open) { - this.menus.openModal("settingsPanel"); - return; - } - document.querySelectorAll("dialog[open]").forEach(e => this.menus.closeDialog(e)); - document.querySelectorAll("scrollSettings[open]").forEach(e => this.menus.closeDialog(e)); - if (this.game.started && !this.game.ended) this.game.movement.firstMovement(); - } - - newGame(key, modal) { - if (key == this.game.settings.control.resetKey) { - this.game.modals.closeModal(modal); - } - } - - openPage(url) { - window.open("https://" + url, "blank_") - } - - // edit menu - openEditMenu() { - if (this.game.modals.open) { - if (document.querySelectorAll("#editMenu[open]").length == 0) return; - this.toggleDialog(); - return; - } - if (this.game.settings.game.gamemode != 'custom') return - this.menus.openModal("editMenu"); - } - - changeEditPiece(pieceName) { this.game.boardeditor.fillPiece = pieceName; } - - addGarbageRow() { - this.game.mechanics.addGarbage(1); - this.game.mechanics.setShadow(); - } - - removeLastRow() { - this.game.mechanics.clear.clearRow(0); - this.game.mechanics.setShadow(); - } - - clearGarbage() { - this.game.mechanics.board.EradicateMinoCells("S G"); - this.game.mechanics.setShadow(); - } - - setBoard() { - const input = prompt("Enter Map String Here:") - const { board, next, hold } = this.game.boardeditor.convertFromMap(input); - this.game.board.boardState = board; - this.game.bag.nextPieces = [next.split(","), []]; - this.game.hold.piece = this.game.renderer.getPiece(hold); - this.game.mechanics.spawnPiece(this.game.bag.randomiser()); - this.game.modals.generate.notif("Map Loaded", "Custom map has successfully loaded", "message"); - } - - getBoardString() { - const exportstring = this.game.boardeditor.convertToMap(); - navigator.clipboard.writeText(exportstring) - this.game.modals.generate.notif("Map Exported", "Custom map has been copied to your clipboard", "message"); - alert("TETR.IO Map String:\n" + exportstring) - } - - // stats - exportStats() { - let stats = {} - Object.getOwnPropertyNames(this.game.stats).forEach(key => { - if (key == "game") return; - stats[key] = this.game.stats[key]; - }) - - let el = document.createElement("a"); - el.setAttribute("href", "data:text/plain;charset=utf-8," + encodeURIComponent(JSON.stringify(stats))); - el.setAttribute("download", "stats.json"); - document.body.appendChild(el); - el.click(); - document.body.removeChild(el); - this.game.modals.generate.notif("Stats Exported", "The current game's stats have been exported.", "message"); - } - - closeStats() { - this.menus.closeDialog(document.getElementById("gameStatsDialog")); - this.game.modals.open = true; - } - - exportLifetime() { - this.game.profilestats.saveSession(); - const data = localStorage.getItem("stats"); - const day = (new Date()).toLocaleDateString().replace("/", "-"); - - let el = document.createElement("a"); - el.setAttribute("href", "data:text/plain;charset=utf-8," + encodeURIComponent(JSON.stringify(data))); - el.setAttribute("download", `teti_stats_${day}.json`); - document.body.appendChild(el); - el.click(); - document.body.removeChild(el); - this.game.modals.generate.notif("Lifetime Stats Exported", "All your lifetime stats and PBs have been exported. Enjoy the many stats you can analyse!", "success"); - } -} +import { Game } from "../game.js"; +import { toExpValue } from "./modals.js"; + +export class MenuActions { + bindingKey; + menus; + elementSelectKeyText = document.getElementById("selectkeytext"); + controlUsed = false; + altUsed = false; + + /** + * @param {Game} game + */ + constructor(game) { + this.game = game; + } + + // sliders + sliderChange(el) { + const text = el.parentElement.children[0].textContent.split(":")[0]; + let value = el.value; + if (el.classList[2] == "exp") value = toExpValue(value); + if (el.classList[2] == "exp" && value > 1000) value = "None"; + el.parentElement.children[0].textContent = `${text}: ${value}`; + } + + addRangeListener() { + document.body.addEventListener("click", (event) => { + if (event.target.dataset.isRange == "true") { + const el = event.target.children[1] + this.game.modals.selectedRangeElement = el; + this.menus.openModal("changeRangeValue"); + document.getElementById("rangeValue").value = el.value; + } + }) + } + + rangeClickInit(el) { + el.parentElement.dataset.isRange = true; + } + + buttonInput(el) { + document.getElementById("frontdrop").showModal(); + this.bindingKey = el.id; + } + + checkValue(el, el2 = this.game.modals.selectedRangeElement) { + this.game.modals.selectedRangeElement = el2; + if (el.value == "") return; + if (el.value < Number(el2.min)) el.value = Number(el2.min); + if (el.value > Number(el2.max)) el.value = Number(el2.max); + } + + // keybinds + checkKeybind(event) { + if (!event.ctrlKey) this.controlUsed = false; + if (!event.altKey) this.altUsed = false; + this.elementSelectKeyText.textContent = "Click to remove keybind"; + + } + + setKeybind(event) { + if (!this.controlUsed && event.ctrlKey) { + this.controlUsed = true; + this.elementSelectKeyText.textContent += ". Control modifier used"; + return; + } + if (!this.altUsed && event.altKey) { + this.altUsed = true; + this.elementSelectKeyText.textContent += ". Alt modifier used"; + return; + } + + const key = (event.ctrlKey ? "Ctrl+" : "") + (event.altKey ? "Alt+" : "") + event.key; + document.getElementById(this.bindingKey).textContent = key; + for (let i in this.game.settings.control) { + if (i == this.bindingKey) continue; + const otherKeys = document.getElementById(i); + if (otherKeys.textContent == key) otherKeys.textContent = "None"; + } + this.menus.closeDialog(document.getElementById("frontdrop")); + this.game.modals.open = true; + this.bindingKey = undefined; + this.controlUsed = false; + this.altUsed = false; + this.elementSelectKeyText.textContent = "Click to remove keybind"; + } + + // settings + saveSettings() { + const data = this.game.settings.save(); + localStorage.setItem("settings", JSON.stringify(data)); + } + + loadSettings() { + const data = localStorage.getItem("settings") ?? "{}"; + this.game.settings.load(JSON.parse(data)) + } + + setGamemode(mode) { + this.game.modes.setGamemode(mode); + this.game.modes.loadModes(); + } + + downloadSettings() { + this.saveSettings(); + let el = document.createElement("a"); + const text = localStorage.getItem("settings"); + el.setAttribute("href", "data:text/plain;charset=utf-8," + encodeURIComponent(text)); + el.setAttribute("download", "settings.teti"); + document.body.appendChild(el); + el.click(); + document.body.removeChild(el); + } + + uploadSettings(el) { + const reader = new FileReader(); + reader.readAsText(el.files[0]); + reader.onload = () => { + localStorage.setItem("settings", reader.result.toString()); + this.loadSettings(); + this.game.modes.loadModes(); + this.game.modals.generate.notif("Settings Loaded", "User settings have successfully loaded", "message"); + }; + } + + resetSettings(group) { + this.game.settings.reset(group); + this.saveSettings(); + location.reload(); + this.game.modals.generate.notif("Settings Reset", `${group} settings have been reset to default`, "message"); + } + + // menu + toggleDialog() { + if (this.game.menuactions.bindingKey != undefined) return; + if (!this.game.modals.open) { + this.menus.openModal("settingsPanel"); + return; + } + document.querySelectorAll("dialog[open]").forEach(e => this.menus.closeDialog(e)); + document.querySelectorAll("scrollSettings[open]").forEach(e => this.menus.closeDialog(e)); + if (this.game.started && !this.game.ended) this.game.movement.firstMovement(); + } + + newGame(key, modal) { + if (key == this.game.settings.control.resetKey) { + this.game.modals.closeModal(modal); + } + } + + openPage(url) { + window.open("https://" + url, "blank_") + } + + // edit menu + openEditMenu() { + if (this.game.modals.open) { + if (document.querySelectorAll("#editMenu[open]").length == 0) return; + this.toggleDialog(); + return; + } + if (this.game.settings.game.gamemode != 'custom') return + this.menus.openModal("editMenu"); + } + + changeEditPiece(pieceName) { this.game.boardeditor.fillPiece = pieceName; } + + addGarbageRow() { + this.game.mechanics.addGarbage(1); + this.game.mechanics.setShadow(); + } + + removeLastRow() { + this.game.mechanics.clear.clearRow(0); + this.game.mechanics.setShadow(); + } + + clearGarbage() { + this.game.mechanics.board.EradicateMinoCells("S G"); + this.game.mechanics.setShadow(); + } + + setBoard() { + const input = prompt("Enter Map String Here:") + const { board, next, hold } = this.game.boardeditor.convertFromMap(input); + this.game.board.boardState = board; + this.game.bag.nextPieces = [next.split(","), []]; + this.game.hold.piece = this.game.renderer.getPiece(hold); + this.game.mechanics.spawnPiece(this.game.bag.randomiser()); + this.game.modals.generate.notif("Map Loaded", "Custom map has successfully loaded", "message"); + } + + getBoardString() { + const exportstring = this.game.boardeditor.convertToMap(); + navigator.clipboard.writeText(exportstring) + this.game.modals.generate.notif("Map Exported", "Custom map has been copied to your clipboard", "message"); + alert("TETR.IO Map String:\n" + exportstring) + } + + // stats + exportStats() { + let stats = {} + Object.getOwnPropertyNames(this.game.stats).forEach(key => { + if (key == "game") return; + stats[key] = this.game.stats[key]; + }) + + let el = document.createElement("a"); + el.setAttribute("href", "data:text/plain;charset=utf-8," + encodeURIComponent(JSON.stringify(stats))); + el.setAttribute("download", "stats.json"); + document.body.appendChild(el); + el.click(); + document.body.removeChild(el); + this.game.modals.generate.notif("Stats Exported", "The current game's stats have been exported.", "message"); + } + + closeStats() { + this.menus.closeDialog(document.getElementById("gameStatsDialog")); + this.game.modals.open = true; + } + + exportLifetime() { + this.game.profilestats.saveSession(); + const data = localStorage.getItem("stats"); + const day = (new Date()).toLocaleDateString().replace("/", "-"); + + let el = document.createElement("a"); + el.setAttribute("href", "data:text/plain;charset=utf-8," + encodeURIComponent(JSON.stringify(data))); + el.setAttribute("download", `teti_stats_${day}.json`); + document.body.appendChild(el); + el.click(); + document.body.removeChild(el); + this.game.modals.generate.notif("Lifetime Stats Exported", "All your lifetime stats and PBs have been exported. Enjoy the many stats you can analyse!", "success"); + } +} diff --git a/src/menus/modals.js b/src/menus/modals.js index 223a7fb..53a323b 100644 --- a/src/menus/modals.js +++ b/src/menus/modals.js @@ -1,128 +1,128 @@ -import { Game } from "../game.js"; -import { GenerateMenus } from "./generate.js"; - -export class ModalActions { - open; - closing; - selectedRangeElement; - pieceNames = ["s", "z", "i", "j", "l", "o", "t"]; - - settingPanel = document.getElementById("settingsPanel"); - options = [...document.getElementsByClassName("option")]; - - - /** - * @param {Game} game - */ - constructor(game) { - this.game = game; - this.actions = this.game.menuactions; - game.menuactions.menus = this; - this.generate = new GenerateMenus(game); - } - - openModal(id) { - //ensure that everything has been closed before trying to open the settings panel - if (id == "settingsPanel" && this.closing) return; - if (id == "queueModify" && !this.game.settings.game.allowQueueModify) return; - this.game.stopGameTimers() - this.getOptions(id).forEach(setting => { - let settingType = this.getSettingType(id); - let newval; - if (this.game.settings.hasOwnProperty(settingType)) newval = this.game.settings[settingType][setting.id] - if (setting.classList[2] == "exp") newval = toLogValue(newval); - if (setting.classList[2] == "statoption") newval = this.game.settings.game.sidebar[setting.id[10]-1]; - if (setting.id == "nextQueue") newval = this.game.bag.getQueue(); - if (setting.id == "holdQueue") newval = this.game.hold.getHold(); - if (setting.id == "rowfillmode") newval = this.game.boardeditor.fillRow; - setting.value = newval; - if (setting.classList[1] == "keybind") setting.textContent = newval; - if (setting.classList[1] == "check") setting.checked = newval; - if (setting.classList[1] == "range") { - this.actions.sliderChange(setting); - this.actions.rangeClickInit(setting); - } - }); - - document.getElementById(id).showModal(); - - if (id == "gameStatsDialog") this.generate.displayStats(); - if (id == "gamemodeDialog") this.generate.highlightGamemodeInMenu(); - if (id == "competitiveDialog") this.generate.renderPBs(); - if (id != "settingsPanel" && this.settingPanel.open) this.closeDialog(this.settingPanel); - this.open = true; - this.game.sounds.toggleSongMuffle(this.open); - } - - getOptions(id) { - const settings = [...this.options].filter(item => item.parentElement.parentElement.parentElement.id == id) - return settings; - } - - getSettingType(id) { - let type = id.replace("Dialog", ""); - if (id == "gamemodeDialog" || id == "goalsDialog" || id == "competitiveDialog") type = "game"; - return type; - } - - closeModal(id) { - this.getOptions(id).forEach(setting => { - let settingType = this.getSettingType(id); - let val = setting.value; - if (setting.classList[1] == "number" && val == "") val = this.selectedRangeElement.min; - if (setting.classList[1] == "check") val = setting.checked; - if (setting.classList[1] == "keybind") { - val = setting.textContent.length > 1 - ? setting.textContent - : setting.textContent.toLowerCase(); - } - if (setting.classList[2] == "exp") val = toExpValue(val); - if (setting.classList[2] == "statoption") this.game.settings.game.sidebar[setting.id[10]-1] = val; - if (setting.id == "nextQueue") this.game.bag.setQueue(val, this.pieceNames); - if (setting.id == "holdQueue") this.game.hold.setNewHold(val); - if (setting.id == "rowfillmode") this.game.boardeditor.fillRow = val; - if (setting.id == "override") this.game.boardeditor.override = val; - - if (id == "changeRangeValue") { - this.selectedRangeElement.value = document.getElementById("rangeValue").value; - this.actions.sliderChange(this.selectedRangeElement); - } - if (setting.id == "audioLevel") this.game.sounds.setAudioLevel(); - - if (!this.game.settings.hasOwnProperty(settingType)) return; - this.game.settings[settingType][setting.id] = val; - }); - - this.closeDialog(document.getElementById(id)); - if (id != 'changeRangeValue' && id != "frontdrop" && this.game.started && !this.game.ended) - this.game.movement.firstMovement(); - this.actions.saveSettings(); - if (id == "displayDialog") this.game.renderer.renderStyles(); - - const restartMenus = ["gameDialog", "gamemodeDialog", "gameEnd", "goalsDialog", "competitiveDialog"]; - if (restartMenus.includes(id)) this.game.controls.retry(false); - if (id == "changeRangeValue") this.open = true; - } - - closeDialog(element) { - this.closing = true //track if the closing animation is still ongoing - const closingAnimation = () => { - element.removeEventListener("animationend", closingAnimation); - element.classList.remove("closingAnimation"); - element.close(); - this.closing = false - }; - this.open = false; - this.game.sounds.toggleSongMuffle(this.open); - element.classList.add("closingAnimation"); - element.addEventListener("animationend", closingAnimation); - } -} - -function toLogValue(y) { - return Math.round(Math.log2(y + 1) * 10); -} - -export function toExpValue(x) { - return Math.round(Math.pow(2, 0.1 * x) - 1); +import { Game } from "../game.js"; +import { GenerateMenus } from "./generate.js"; + +export class ModalActions { + open; + closing; + selectedRangeElement; + pieceNames = ["s", "z", "i", "j", "l", "o", "t"]; + + settingPanel = document.getElementById("settingsPanel"); + options = [...document.getElementsByClassName("option")]; + + + /** + * @param {Game} game + */ + constructor(game) { + this.game = game; + this.actions = this.game.menuactions; + game.menuactions.menus = this; + this.generate = new GenerateMenus(game); + } + + openModal(id) { + //ensure that everything has been closed before trying to open the settings panel + if (id == "settingsPanel" && this.closing) return; + if (id == "queueModify" && !this.game.settings.game.allowQueueModify) return; + this.game.stopGameTimers() + this.getOptions(id).forEach(setting => { + let settingType = this.getSettingType(id); + let newval; + if (this.game.settings.hasOwnProperty(settingType)) newval = this.game.settings[settingType][setting.id] + if (setting.classList[2] == "exp") newval = toLogValue(newval); + if (setting.classList[2] == "statoption") newval = this.game.settings.game.sidebar[setting.id[10]-1]; + if (setting.id == "nextQueue") newval = this.game.bag.getQueue(); + if (setting.id == "holdQueue") newval = this.game.hold.getHold(); + if (setting.id == "rowfillmode") newval = this.game.boardeditor.fillRow; + setting.value = newval; + if (setting.classList[1] == "keybind") setting.textContent = newval; + if (setting.classList[1] == "check") setting.checked = newval; + if (setting.classList[1] == "range") { + this.actions.sliderChange(setting); + this.actions.rangeClickInit(setting); + } + }); + + document.getElementById(id).showModal(); + + if (id == "gameStatsDialog") this.generate.displayStats(); + if (id == "gamemodeDialog") this.generate.highlightGamemodeInMenu(); + if (id == "competitiveDialog") this.generate.renderPBs(); + if (id != "settingsPanel" && this.settingPanel.open) this.closeDialog(this.settingPanel); + this.open = true; + this.game.sounds.toggleSongMuffle(this.open); + } + + getOptions(id) { + const settings = [...this.options].filter(item => item.parentElement.parentElement.parentElement.id == id) + return settings; + } + + getSettingType(id) { + let type = id.replace("Dialog", ""); + if (id == "gamemodeDialog" || id == "goalsDialog" || id == "competitiveDialog") type = "game"; + return type; + } + + closeModal(id) { + this.getOptions(id).forEach(setting => { + let settingType = this.getSettingType(id); + let val = setting.value; + if (setting.classList[1] == "number" && val == "") val = this.selectedRangeElement.min; + if (setting.classList[1] == "check") val = setting.checked; + if (setting.classList[1] == "keybind") { + val = setting.textContent.length > 1 + ? setting.textContent + : setting.textContent.toLowerCase(); + } + if (setting.classList[2] == "exp") val = toExpValue(val); + if (setting.classList[2] == "statoption") this.game.settings.game.sidebar[setting.id[10]-1] = val; + if (setting.id == "nextQueue") this.game.bag.setQueue(val, this.pieceNames); + if (setting.id == "holdQueue") this.game.hold.setNewHold(val); + if (setting.id == "rowfillmode") this.game.boardeditor.fillRow = val; + if (setting.id == "override") this.game.boardeditor.override = val; + + if (id == "changeRangeValue") { + this.selectedRangeElement.value = document.getElementById("rangeValue").value; + this.actions.sliderChange(this.selectedRangeElement); + } + if (setting.id == "audioLevel") this.game.sounds.setAudioLevel(); + + if (!this.game.settings.hasOwnProperty(settingType)) return; + this.game.settings[settingType][setting.id] = val; + }); + + this.closeDialog(document.getElementById(id)); + if (id != 'changeRangeValue' && id != "frontdrop" && this.game.started && !this.game.ended) + this.game.movement.firstMovement(); + this.actions.saveSettings(); + if (id == "displayDialog") this.game.renderer.renderStyles(true); + + const restartMenus = ["gameDialog", "gamemodeDialog", "gameEnd", "goalsDialog", "competitiveDialog"]; + if (restartMenus.includes(id)) this.game.controls.retry(false); + if (id == "changeRangeValue") this.open = true; + } + + closeDialog(element) { + this.closing = true //track if the closing animation is still ongoing + const closingAnimation = () => { + element.removeEventListener("animationend", closingAnimation); + element.classList.remove("closingAnimation"); + element.close(); + this.closing = false + }; + this.open = false; + this.game.sounds.toggleSongMuffle(this.open); + element.classList.add("closingAnimation"); + element.addEventListener("animationend", closingAnimation); + } +} + +function toLogValue(y) { + return Math.round(Math.log2(y + 1) * 10); +} + +export function toExpValue(x) { + return Math.round(Math.pow(2, 0.1 * x) - 1); } \ No newline at end of file diff --git a/src/movement/controls.js b/src/movement/controls.js index fc44288..248ab50 100644 --- a/src/movement/controls.js +++ b/src/movement/controls.js @@ -9,6 +9,9 @@ export class Controls { timings = { arr: 0, das: 0, sd: 0 }; // timeout and interval ids menuKey = "Escape"; // html modals close using escape cursorVisible = true; + resetting = false; + + keyQueue = []; /** * @param {Game} game @@ -30,19 +33,26 @@ export class Controls { if (key == keys.resetKey) this.retry(true); if (this.game.ended) return; - if (key == keys.cwKey) this.moves.rotate("CW"); - else if (key == keys.ccwKey) this.moves.rotate("CCW"); - else if (key == keys.rotate180Key) this.moves.rotate("180"); - else if (key == keys.hdKey) this.moves.harddrop(); - else if (key == keys.holdKey) this.game.mechanics.switchHold(); - else if (key == keys.rightKey) this.startDas("RIGHT"); - else if (key == keys.leftKey) this.startDas("LEFT"); - else if (key == keys.sdKey) this.startArrSD(); - + this.keyQueue.push(key); this.toggleCursor(false); this.game.stats.inputs++; } + runKeyQueue() { + const keys = this.game.settings.control; + this.keyQueue.forEach(key => { + if (key == keys.cwKey) this.moves.rotate("CW"); + else if (key == keys.ccwKey) this.moves.rotate("CCW"); + else if (key == keys.rotate180Key) this.moves.rotate("180"); + else if (key == keys.hdKey) this.moves.harddrop(); + else if (key == keys.holdKey) this.game.mechanics.switchHold(); + else if (key == keys.rightKey) this.startDas("RIGHT"); + else if (key == keys.leftKey) this.startDas("LEFT"); + else if (key == keys.sdKey) this.startArrSD(); + }); + this.keyQueue = []; + } + onKeyDownRepeat(event, key) { // allows for arr undo/redo const keys = this.game.settings.control; if (event.key == this.menuKey) event.preventDefault(); @@ -64,8 +74,7 @@ export class Controls { this.directionState[direction] = "das"; this.stopTimeout("das"); this.stopInterval("arr"); - this.timings.das = setTimeout(() => - Promise.resolve().then(() => this.startArr(direction)), // feels faster with promise but idk + this.timings.das = setTimeout(() => this.startArr(direction), this.game.settings.handling.das ); } @@ -145,8 +154,6 @@ export class Controls { this.timings[name] = 0; } - resetting = false; - retry(animation) { if (this.resetting) return; // no overlap this.game.ended = true; @@ -154,12 +161,9 @@ export class Controls { if (!animation || this.game.settings.game.stride) { this.game.startGame(); - return; + } else { + this.game.pixi.resetAnimation() } - - this.game.stopGameTimers() - this.resetting = true; - this.game.renderer.resetAnimCurrent = 0; // start animation } toggleCursor(enable) { diff --git a/src/movement/movement.js b/src/movement/movement.js index 2b29bcc..9a950b7 100644 --- a/src/movement/movement.js +++ b/src/movement/movement.js @@ -1,162 +1,162 @@ -import { spinChecks } from "../data/data.js"; -import { Game } from "../game.js"; - -export class Movement { - /** - * @param {Game} game - */ - constructor(game) { - this.game = game; - } - - firstMovement() { - this.game.zenith.startZenithMode(); - this.game.grandmaster.startGrandmasterTimer(); - if(this.game.zenith.tickPass == 0 && this.game.settings.game.gamemode == "zenith") this.game.renderer.renderTimeLeft("FLOOR 1") - this.game.started = true; - this.game.mechanics.startGravity(); - this.game.modes.startSurvival(); - this.game.mechanics.locking.lockingResume(); - this.game.gameTimer = setInterval(() => - this.game.gameClock(), (1000 / this.game.tickrate) - ); - } - - checkCollision(coords, action, collider) { - collider = collider ?? this.game.board.getMinos("S"); - for (let [x, y] of coords) { - if ( - (action == "RIGHT" && x > 8) || - (action == "LEFT" && x < 1) || - (action == "DOWN" && y < 1) || - (action == "ROTATE" && x < 0) || - x > 9 || - y < 0 || - (action == "PLACE" && y > 19) - ) - return true; - for (let [x2, y2] of collider) { - const col = (dx, dy) => x + dx == x2 && y + dy == y2; - if ( - (action == "RIGHT" && col(1, 0)) || - (action == "LEFT" && col(-1, 0)) || - (action == "DOWN" && col(0, -1)) || - ((action == "ROTATE" || action == "SPAWN") && col(0, 0)) - ) - return true; - } - } - } - - checkTspin(rotation, [x, y], [dx, dy]) { - if (this.game.falling.piece.name != "t") return false; - this.game.mechanics.isMini = false; - const minos = spinChecks[(rotation + 1) % 4] - .concat(spinChecks[rotation - 1]) - .map(([ddx, ddy]) => this.checkCollision([[ddx + x, ddy + y]], "ROTATE")); - if (minos[2] && minos[3] && (minos[0] || minos[1])) return true; - if ((minos[2] || minos[3]) && minos[0] && minos[1]) { - if ((dx == 1 || dx == -1) && dy == -2) return true; - this.game.mechanics.isMini = true; - return true; - } - } - - checkAllspin(pieceCoords) { - if (this.game.falling.piece.name == "t") return false; - const directions = [[1, 0], [0, 1], [-1, 0], [0, -1]]; - const validSpin = directions.every(([dx, dy]) => - this.checkCollision(pieceCoords.map(([x, y]) => [x + dx, y + dy]), "ROTATE")); - if (validSpin && this.game.settings.game.allspinminis) this.game.mechanics.isMini = true; - return validSpin; - } - - rotate(type) { - if (this.game.falling.piece.name == "o") return; - const newRotation = this.game.falling.getRotateState(type); - const kickdata = this.game.falling.getKickData(type, newRotation); - const rotatingCoords = this.game.falling.getNewCoords(newRotation); - const change = kickdata.find(([dx, dy]) => - !this.checkCollision(rotatingCoords.map(c => [c[0] + dx, c[1] + dy]), "ROTATE")); - if (!change) return; - this.game.board.MinoToNone("A"); - this.game.board.addMinos(this.game.falling.newName(), rotatingCoords, change); - this.game.falling.updateLocation(change); - this.game.mechanics.isTspin = this.checkTspin(newRotation, this.game.falling.location, change); - this.game.mechanics.isAllspin = this.checkAllspin(this.game.board.getMinos("A")); - this.game.falling.rotation = newRotation; - this.game.mechanics.locking.incrementLock(); - this.game.stats.rotates++; - this.game.sounds.playSound("rotate"); - this.game.mechanics.setShadow(); - if (this.game.settings.game.gravitySpeed == 0) this.game.mechanics.startGravity(); - this.game.controls.startArr("current"); - this.game.controls.checkSD(); - if (this.game.mechanics.isTspin || (this.game.mechanics.isAllspin && this.game.settings.game.allspin)) { - this.game.renderer.rotateBoard(type); - this.game.particles.spawnParticles(this.game.falling.location[0], this.game.falling.location[1] + 2, - "spin", 5, type == "CW", this.game.falling.piece.colour); - this.game.sounds.playSound("spin"); - } - } - - movePieceSide(direction, max = 1) { - this.game.controls.checkSD(); - const minos = this.game.board.getMinos("A"); - let amount = 0; - const check = dx => !this.checkCollision(minos.map(([x, y]) => [x + dx, y]), direction); - while (check(amount) && Math.abs(amount) < max) direction == "RIGHT" ? amount++ : amount--; - if (!check(amount)) this.game.renderer.bounceBoard(direction); - if (amount == 0) { - this.game.controls.stopInterval("arr"); - return; - } - this.game.board.moveMinos(minos, "RIGHT", amount); - this.game.falling.updateLocation([amount, 0]); - this.game.mechanics.isTspin = false; - this.game.mechanics.isAllspin = false; - this.game.mechanics.isMini = false; - this.game.mechanics.locking.incrementLock(); - this.game.sounds.playSound("move"); - this.game.mechanics.setShadow(); - this.game.controls.checkSD(); - if (this.game.settings.game.gravitySpeed == 0) this.game.mechanics.startGravity(); - } - - movePieceDown(sonic, scoring = false) { - const minos = this.game.board.getMinos("A"); - if (this.checkCollision(minos, "DOWN")) return; - this.game.board.moveMinos(minos, "DOWN", 1); - - this.game.mechanics.isTspin = false; - this.game.mechanics.isAllspin = false; - this.game.mechanics.isMini = false; - this.game.falling.updateLocation([0, -1]); - if (this.checkCollision(this.game.board.getMinos("A"), "DOWN")) { - this.game.mechanics.locking.scheduleLock(); - this.game.renderer.bounceBoard("DOWN"); - this.game.controls.startArr("current"); - } - if (scoring && sonic) this.game.stats.score += 1; - if (sonic) this.movePieceDown(true, scoring); - } - - harddrop() { - const minos = this.game.board.getMinos("A"); - let amount = 0; - while (!this.checkCollision(minos.map(([x, y]) => [x, y - amount]), "DOWN")) amount++; - if (amount > 0) { - this.game.mechanics.isTspin = false; - this.game.mechanics.isAllspin = false; - this.game.mechanics.isMini = false; - } - this.game.board.moveMinos(minos, "DOWN", amount); - this.game.falling.updateLocation([0, -amount]); - this.game.stats.score += 2 * amount; - this.game.sounds.playSound("harddrop"); - this.game.renderer.bounceBoard('DOWN'); - const xvals = [...new Set(minos.map(([x, y]) => x))]; - this.game.particles.spawnParticles(Math.min(...xvals)+1, this.game.falling.location[1], "drop", xvals.length); - this.game.mechanics.locking.lockPiece(); - } -} +import { spinChecks } from "../data/data.js"; +import { Game } from "../game.js"; + +export class Movement { + /** + * @param {Game} game + */ + constructor(game) { + this.game = game; + } + + firstMovement() { + this.game.zenith.startZenithMode(); + this.game.grandmaster.startGrandmasterTimer(); + if(this.game.zenith.tickPass == 0 && this.game.settings.game.gamemode == "zenith") this.game.renderer.renderTimeLeft("FLOOR 1") + this.game.started = true; + this.game.mechanics.startGravity(); + this.game.modes.startSurvival(); + this.game.mechanics.locking.lockingResume(); + this.game.gameTimer = setInterval(() => + this.game.gameClock(), (1000 / this.game.tickrate) + ); + } + + checkCollision(coords, action, collider) { + collider = collider ?? this.game.board.getMinos("S"); + for (let [x, y] of coords) { + if ( + (action == "RIGHT" && x > 8) || + (action == "LEFT" && x < 1) || + (action == "DOWN" && y < 1) || + (action == "ROTATE" && x < 0) || + x > 9 || + y < 0 || + (action == "PLACE" && y > 19) + ) + return true; + for (let [x2, y2] of collider) { + const col = (dx, dy) => x + dx == x2 && y + dy == y2; + if ( + (action == "RIGHT" && col(1, 0)) || + (action == "LEFT" && col(-1, 0)) || + (action == "DOWN" && col(0, -1)) || + ((action == "ROTATE" || action == "SPAWN") && col(0, 0)) + ) + return true; + } + } + } + + checkTspin(rotation, [x, y], [dx, dy]) { + if (this.game.falling.piece.name != "t") return false; + this.game.mechanics.isMini = false; + const minos = spinChecks[(rotation + 1) % 4] + .concat(spinChecks[rotation - 1]) + .map(([ddx, ddy]) => this.checkCollision([[ddx + x, ddy + y]], "ROTATE")); + if (minos[2] && minos[3] && (minos[0] || minos[1])) return true; + if ((minos[2] || minos[3]) && minos[0] && minos[1]) { + if ((dx == 1 || dx == -1) && dy == -2) return true; + this.game.mechanics.isMini = true; + return true; + } + } + + checkAllspin(pieceCoords) { + if (this.game.falling.piece.name == "t") return false; + const directions = [[1, 0], [0, 1], [-1, 0], [0, -1]]; + const validSpin = directions.every(([dx, dy]) => + this.checkCollision(pieceCoords.map(([x, y]) => [x + dx, y + dy]), "ROTATE")); + if (validSpin && this.game.settings.game.allspinminis) this.game.mechanics.isMini = true; + return validSpin; + } + + rotate(type) { + if (this.game.falling.piece.name == "o") return; + const newRotation = this.game.falling.getRotateState(type); + const kickdata = this.game.falling.getKickData(type, newRotation); + const rotatingCoords = this.game.falling.getNewCoords(newRotation); + const change = kickdata.find(([dx, dy]) => + !this.checkCollision(rotatingCoords.map(c => [c[0] + dx, c[1] + dy]), "ROTATE")); + if (!change) return; + this.game.board.MinoToNone("A"); + this.game.board.addMinos(this.game.falling.newName(), rotatingCoords, change); + this.game.falling.updateLocation(change); + this.game.mechanics.isTspin = this.checkTspin(newRotation, this.game.falling.location, change); + this.game.mechanics.isAllspin = this.checkAllspin(this.game.board.getMinos("A")); + this.game.falling.rotation = newRotation; + this.game.mechanics.locking.incrementLock(); + this.game.stats.rotates++; + this.game.sounds.playSound("rotate"); + this.game.mechanics.setShadow(); + if (this.game.settings.game.gravitySpeed == 0) this.game.mechanics.startGravity(); + this.game.controls.startArr("current"); + this.game.controls.checkSD(); + if (this.game.mechanics.isTspin || (this.game.mechanics.isAllspin && this.game.settings.game.allspin)) { + this.game.renderer.rotateBoard(type); + this.game.particles.spawnParticles(...this.game.falling.location, + "spin", 5, type == "CW", this.game.falling.piece.colour); + this.game.sounds.playSound("spin"); + } + } + + movePieceSide(direction, max = 1) { + this.game.controls.checkSD(); + const minos = this.game.board.getMinos("A"); + let amount = 0; + const check = dx => !this.checkCollision(minos.map(([x, y]) => [x + dx, y]), direction); + while (check(amount) && Math.abs(amount) < max) direction == "RIGHT" ? amount++ : amount--; + if (!check(amount)) this.game.renderer.bounceBoard(direction); + if (amount == 0) { + this.game.controls.stopInterval("arr"); + return; + } + this.game.board.moveMinos(minos, "RIGHT", amount); + this.game.falling.updateLocation([amount, 0]); + this.game.mechanics.isTspin = false; + this.game.mechanics.isAllspin = false; + this.game.mechanics.isMini = false; + this.game.mechanics.locking.incrementLock(); + this.game.sounds.playSound("move"); + this.game.mechanics.setShadow(); + this.game.controls.checkSD(); + if (this.game.settings.game.gravitySpeed == 0) this.game.mechanics.startGravity(); + } + + movePieceDown(sonic, scoring = false) { + const minos = this.game.board.getMinos("A"); + if (this.checkCollision(minos, "DOWN")) return; + this.game.board.moveMinos(minos, "DOWN", 1); + + this.game.mechanics.isTspin = false; + this.game.mechanics.isAllspin = false; + this.game.mechanics.isMini = false; + this.game.falling.updateLocation([0, -1]); + if (this.checkCollision(this.game.board.getMinos("A"), "DOWN")) { + this.game.mechanics.locking.scheduleLock(); + this.game.renderer.bounceBoard("DOWN"); + this.game.controls.startArr("current"); + } + if (scoring && sonic) this.game.stats.score += 1; + if (sonic) this.movePieceDown(true, scoring); + } + + harddrop() { + const minos = this.game.board.getMinos("A"); + let amount = 0; + while (!this.checkCollision(minos.map(([x, y]) => [x, y - amount]), "DOWN")) amount++; + if (amount > 0) { + this.game.mechanics.isTspin = false; + this.game.mechanics.isAllspin = false; + this.game.mechanics.isMini = false; + } + this.game.board.moveMinos(minos, "DOWN", amount); + this.game.falling.updateLocation([0, -amount]); + this.game.stats.score += 2 * amount; + this.game.sounds.playSound("harddrop"); + this.game.renderer.bounceBoard('DOWN'); + const xvals = [...new Set(minos.map(([x, y]) => x))]; + this.game.particles.spawnParticles(Math.min(...xvals), this.game.falling.location[1], "drop", xvals.length); + this.game.mechanics.locking.lockPiece(); + } +} diff --git a/styles/boards.css b/styles/boards.css index 5ac38c4..35ff511 100644 --- a/styles/boards.css +++ b/styles/boards.css @@ -1,437 +1,164 @@ -#board { - position: fixed; - top: 50%; - left: 50%; - transform-origin: 0 0; - transform: scale(1.1) translate(-50%, -50%); - height: 60vh; - aspect-ratio: 0.5; -} - -#backboard, -#backborder { - position: absolute; - height: 100%; - width: 100%; - background-color: var(--background); - outline: 0.2vh solid var(--cl-blue); - box-shadow: 0 0 5vh var(--l-gray); -} - -@property --angle { - syntax: ""; - initial-value: 0deg; - inherits: false; -} - -#backboard { - --blur-size: 7vmin; - --spin-speed: 6s; - --blur-radius: 20vmin; - --blur-strength: 0; - --colours: #ff4545, #00ff99, #006aff, #ff0095, #ff4545; -} - -#backborder { - --blur-size: 0.3vmin; - --spin-speed: 2s; - --blur-radius: 0.3vmin; - --blur-strength: 0; - --colours: #ff4545, #00ff99, #006aff, #ff0095, #ff4545; -} - -#backboard::before, -#backborder::before { - content: " "; - position: absolute; - width: 100%; - height: 100%; - background-image: conic-gradient(from var(--angle), var(--colours)); - padding: var(--blur-size); - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - border-radius: 1vmin; - animation: var(--spin-speed) spin linear infinite; - filter: blur(var(--blur-radius)); - opacity: var(--blur-strength); - transition: all 1s ease; -} - -#backboard { - outline: none; -} - -@keyframes spin { - from { - --angle: 0deg; - } - - to { - --angle: 360deg; - } -} - -#backboard::after, -#backborder::after { - content: " "; - position: absolute; - width: 100%; - height: 100%; - background-color: var(--background); -} - -#backborder.boardDanger { - outline: 0.2vh solid red !important; - box-shadow: 0 0 3vh red !important; - transition: all 0.5s ease; -} - -#dangerOverlay { - position: absolute; - background-color: red; - transition: all 0.2s ease; - width: 100%; - height: 100%; - opacity: 0; -} - -#playingfield { - position: fixed; - width: 100%; - height: 200%; - top: -100%; -} - -#clickareas { - position: absolute; - width: 100%; - height: 100%; - display: grid; - grid-template-columns: repeat(10, 1fr); - grid-template-rows: repeat(20, 1fr); -} - -.clickmino { - user-select: none; - z-index: 10; -} - -.highlighting { - background-color: #7d7d7d4e; -} - -#grid { - position: fixed; - width: 100%; - height: 100%; -} - -#next { - position: fixed; - left: 101%; - display: grid; - grid-template-columns: repeat(4, 1fr); - grid-template-rows: repeat(15, 1fr); - height: 79%; - width: 40%; - border-radius: 0 1.5vh 1.5vh 0; - outline: 0.2vh solid var(--cl-blue); - background-color: black; - transition: all 0.3s ease; -} - -#hold { - position: fixed; - left: -40%; - display: grid; - grid-template-columns: repeat(4, 1fr); - grid-template-rows: repeat(3, 1fr); - height: 15%; - width: 39.5%; - border-radius: 1.5vh 0 0 1.5vh; - background-color: black; -} - -#lockTimer, -#lockCounter { - position: absolute; - bottom: -0.8vh; - width: 100%; - height: 0.8vh; - border-radius: 0.5vh; -} - -#lockCounter { - bottom: -1.4vh; - height: 0.6vh; -} - -#lockTimer::-webkit-progress-bar, -#lockCounter::-webkit-progress-bar, -#garbageQueue::-webkit-progress-bar { - background-color: var(--invis); -} - -#lockTimer::-moz-progress-bar, -#lockCounter::-moz-progress-bar, -#garbageQueue::-moz-progress-bar { - background-color: var(--invis); -} - -#lockTimer::-webkit-progress-value, -#lockCounter::-webkit-progress-value { - background-color: var(--cl-blue); - border-radius: 0.7vh; -} - -#lockTimer::-moz-progress-value, -#lockCounter::-moz-progress-value { - background-color: var(--cl-blue); - border-radius: 0.7vh; -} - -#climbSpeedBar { - position: absolute; - width: 32vh; - height: 1vh; - border-radius: 0.5vh; - transform-origin: 0 0; - left: -2%; - bottom: -5%; -} - -#climbSpeedBar::-webkit-progress-value { - background-color: red; - border-radius: 0.7vh; -} - -#climbSpeedBar::-webkit-progress-bar { - background-color: var(--invis); -} - -#timeLeftText { - font-family: "Montserrat", sans-serif; - font-weight: bold; - color: gold; - font-size: 1em; - width: 100%; - text-align: center; - bottom: 75%; - opacity: 0; - transition: all 0.3s ease; -} - -#timeLeftText.warn { - animation: timeLeft 3s; -} - -@keyframes timeLeft { - 0% { - opacity: 1; - letter-spacing: 0px; - color: gold; - } - - 5%{ - color: red; - } - - 10%{ - color: gold; - - } - - 15%{ - color: red; - } - - 20%{ - color: gold; - - } - - 25%{ - color: red; - } - - 30%{ - color: gold; - - } - - 35%{ - color: red; - } - - 40%{ - color: gold; - - } - - 45%{ - color: red; - } - - 50%{ - color: gold; - - } - - 55%{ - color: red; - } - - 60%{ - color: gold; - - } - - 65%{ - color: red; - } - - 70% { - opacity: 1; - color:gold - } - - 75%{ - color: red; - } - - 80%{ - color: gold; - - } - - 85%{ - color: red; - } - - 90%{ - color: gold; - - } - - 95%{ - color: red; - } - - - 100% { - opacity: 0; - letter-spacing: 5px; - color: gold; - } -} - - -#garbageQueue { - position: absolute; - width: 122vh; - height: 0.5vh; - border-radius: 0.5vh; - transform-origin: 0 0; - transform: rotate(-90deg); - left: -2%; - bottom: -1%; -} - -#garbageQueue::-webkit-progress-value { - background-color: red; - border-radius: 0.7vh; -} - -#garbageQueue::-moz-progress-value { - background-color: red; - border-radius: 0.7vh; -} - -.text { - position: fixed; - font-size: 1.6em; - margin: 0; - user-select: none; -} - -#warningText { - font-family: "Montserrat", sans-serif; - font-weight: bold; - color: red; - font-size: 1em; - width: 100%; - text-align: center; - bottom: 100%; - opacity: 0; - transition: all 0.3s ease; -} - -#warningText.warn { - opacity: 0.5; - animation: warn 0.3s infinite; -} - -@keyframes warn { - 0% { - opacity: 0.5; - } - - 50% { - opacity: 0; - } -} - -.nextText { - left: 110%; - bottom: 100%; -} - -.holdText { - right: 110%; - bottom: 100%; -} - -.objectiveText { - left: 110%; - bottom: 10%; -} - -#redochoices { - position: absolute; - top: 100%; - left: 110%; - width: 40vw; - opacity: 0; - pointer-events: none; - transition: all 0.3s ease; -} - -.redochoice { - height: 3.5vh; - aspect-ratio: 1/1; - border: 0.3vh solid var(--l-gray); - border-radius: 10vmin; - transition: all 0.3s ease-out; - background-color: var(--invis); - outline: none; - color: var(--l-gray); - margin: 0.3vmin; -} - -.redochoice:hover { - border-color: var(--cl-blue); - color: var(--cl-blue); - transition: all 0.05s ease; -} - -.redochoice.selected { - border-color: var(--p-green); - color: var(--p-green); -} - -.redochoice.selected:hover { - border-color: var(--green); - color: var(--green); - transition: all 0.05s ease; +#board { + position: fixed; + top: 50%; + left: 50%; + transform-origin: 0 0; + transform: scale(1.1) translate(-50%, -50%); + height: 60vh; + aspect-ratio: 0.5; + pointer-events: none; +} + +#lockTimer, +#lockCounter { + position: absolute; + bottom: -0.8vh; + width: 100%; + height: 0.8vh; + border-radius: 0.5vh; +} + +#lockCounter { + bottom: -1.4vh; + height: 0.6vh; +} + +:is(#lockTimer, #lockCounter, #garbageQueue)::-webkit-progress-bar { + background-color: var(--invis); +} + +:is(#lockTimer, #lockCounter, #garbageQueue)::-moz-progress-bar { + background-color: var(--invis); +} + +:is(#lockTimer, #lockCounter)::-webkit-progress-value { + background-color: var(--cl-blue); + border-radius: 0.7vh; +} + +:is(#lockTimer, #lockCounter)::-moz-progress-value { + background-color: var(--cl-blue); + border-radius: 0.7vh; +} + +#climbSpeedBar { + /* css variables which can be changed with js */ + --bar-colour: red; + --background-colour: #00000000; + position: absolute; + width: 32vh; + height: 1vh; + border-radius: 0.5vh; + transform-origin: 0 0; + left: -2%; + bottom: -5%; +} + +#climbSpeedBar::-webkit-progress-value { + background-color: var(--bar-colour); + border-radius: 0.7vh; +} + +#climbSpeedBar::-webkit-progress-bar { + background-color: var(--background-colour); + border-radius: 0.7vh; +} + +#garbageQueue { + position: absolute; + width: 122vh; + height: 0.5vh; + border-radius: 0.5vh; + transform-origin: 0 0; + transform: rotate(-90deg); + left: -2%; + bottom: -1%; +} + +#garbageQueue::-webkit-progress-value { + background-color: red; + border-radius: 0.7vh; +} + +#garbageQueue::-moz-progress-value { + background-color: red; + border-radius: 0.7vh; +} + +.text { + position: fixed; + font-size: 1.6em; + margin: 0; + user-select: none; +} + +#warningText { + font-family: "Montserrat", sans-serif; + font-weight: bold; + color: red; + font-size: 1em; + width: 100%; + text-align: center; + bottom: 100%; + opacity: 0; + transition: all 0.3s ease; +} + +#warningText.warn { + opacity: 0.5; + animation: warn 0.3s infinite; +} + +@keyframes warn { + 0% { + opacity: 0.5; + } + + 50% { + opacity: 0; + } +} + +#redochoices { + position: absolute; + display: flex; + bottom: 8vh; + width: 100%; + flex-direction: row; + text-align: center; + justify-content: center; + align-items: center; + flex-wrap: wrap; + opacity: 0; + transition: all 0.3s ease; + user-select: none; +} + +.redochoice { + height: 3.5vh; + aspect-ratio: 1/1; + border: 0.3vh solid var(--l-gray); + border-radius: 10vmin; + transition: all 0.3s ease-out; + background-color: var(--invis); + outline: none; + color: var(--l-gray); + margin: 0.3vmin; +} + +.redochoice:hover { + border-color: var(--cl-blue); + color: var(--cl-blue); + transition: all 0.05s ease; +} + +.redochoice.selected { + border-color: var(--p-green); + color: var(--p-green); +} + +.redochoice.selected:hover { + border-color: var(--green); + color: var(--green); + transition: all 0.05s ease; } \ No newline at end of file diff --git a/styles/menus.css b/styles/menus.css index f4cd5bb..49f6a61 100644 --- a/styles/menus.css +++ b/styles/menus.css @@ -1,349 +1,309 @@ -/* main menu */ -#settingsPanel[open] { - display: flex; - align-items: center; - justify-content: center; - flex-wrap: wrap; - flex-direction: row; - border: none; - width: 100vw; - height: 50vh; - background-color: var(--invis); - overflow: visible; -} - -#settingsPanel>br { - width: 100%; - content: ' '; -} - -button>img { - -webkit-user-drag: none; -} - -.settingPanelButton { - display: flex; - align-items: center; - justify-content: center; - flex-direction: column; - margin: 1%; - height: 18vh; - aspect-ratio: 1/1; - border: 0.3vh solid var(--l-gray); - border-radius: 1.5vh; - transition: all 0.5s ease-out; - background-color: var(--invis); - padding: 0.5vh; - outline: none; -} - - -.settingPanelButton>p { - color: white; - margin: 0; - padding: 0; - opacity: 0.3; - transform: translateY(1vh); - letter-spacing: 0.1em; -} - -.settingPanelButton>img { - height: 75%; -} - -.settingPanelButton:hover { - border: 0.3vh solid var(--cl-blue); - transition: all 0.1s ease; -} - -.settingPanelButton:disabled, -.smallPanelButton:disabled { - opacity: 0.5; - cursor: not-allowed; -} - -.smallPanelButton, -.smallerPanelButton { - height: 6vh; - width: 6vh; - border-radius: 1vmin; - display: flex; - align-items: center; - justify-content: center; - flex-wrap: wrap; - flex-direction: row; -} - -.smallerPanelButton { - height: 4vh; - width: 4vh; - border-radius: 0.7vh; - border-width: 0.2vh; -} - -.smallerPanelButton:hover { - border-width: 0.2vh; -} - -.smallerPanelButton>img { - height: 100% -} - -.spanleft { - flex: 1; - text-align: right; - padding-right: 1vw; - width: 9vw; -} - -.spanright { - flex: 1; - padding-left: 1vw; -} - -/* songs */ -#songSelector { - display: flex; - align-items: center; - justify-content: center; - width: 40vw; - height: 15vh; - overflow: visible; - white-space: nowrap; - opacity: 0.4; - transition: all 0.5s ease; -} - -#songSelector:hover { - opacity: 0.8; - transition: all 0.1s ease; -} - -#songText { - position: absolute; - transform: translateY(-4vh); - align-self: center; - padding: 0; - margin: 0; -} - -#songProgress { - height: 0.8vh; - border-radius: 5vh; -} - -#songProgress::-webkit-progress-bar { - background-color: var(--e-black); - border-radius: 5vh; -} - -#songProgress::-moz-progress-bar { - background-color: var(--e-black); - border-radius: 5vh; -} - - -#songProgress::-webkit-progress-value { - background-color: var(--cl-blue); - border-radius: 5vh; -} - -#songProgress::-moz-progress-value { - background-color: var(--cl-blue); - border-radius: 5vh; -} - -/* option styles */ -.range { - appearance: none; - -webkit-appearance: none; - height: 1vh; - width: 40%; - right: 3%; - border: 0.2vmin solid white; - background: var(--e-black); - outline: none; - -webkit-transition: 0.2s; - transition: opacity 0.2s; -} - -.range:hover { - border-color: var(--p-green); -} - -.check { - width: 3.5vh; - height: 3.5vh; - appearance: none; - -webkit-appearance: none; - background-color: var(--almost-invis); - border: 0.2vmin solid white; - border-radius: 3vh; - transition: all 0.1s ease; -} - -.check:hover { - border-color: var(--p-green); -} - -.check:checked { - background-color: var(--p-green); - border: none; -} - -.textInput, -.dropdown, -.number { - padding: 0.6vh; - background-color: var(--almost-invis); - border: 0.1vmin solid white; - color: rgb(225, 225, 225); - font-size: 1em; -} - -.number { - width: 20%; - appearance: initial; - -moz-appearance: textfield; -} - -.textInput:hover, -.textInput:focus, -.dropdown:hover, -.dropdown:focus, -.number:hover, -.number:focus { - border-radius: 0; - outline: none; - border-color: var(--p-green); -} - -.keybind { - padding: 0.5vh; - background-color: var(--invis); - border: none; - font-size: 1em; - color: var(--c-blue); -} - -.settingRow:hover>.keybind { - color: var(--p-green); -} - -/* change keybinds */ -#frontdrop { - width: 100%; - height: 100%; - position: fixed; - background-color: var(--slight-invis); - overflow: hidden; - color: var(--cl-blue); - outline: none; - user-select: none; - border: none; - animation: fadein 0.4s forwards ease; - font-family: "Montserrat", sans-serif; -} - -#frontdrop.closingAnimation { - animation: fadeout 0.2s forwards ease; -} - -#selectkeydiv { - display: flex; - width: 100%; - height: 100%; - justify-content: center; - align-items: center; - font-size: 5em; - text-shadow: 0 0 3vh white; -} - -#selectkeybigtext { - transform: translateY(-8vh); -} - -#selectkeytext { - position: absolute; - top: 46vh; - font-size: 0.3em; - text-shadow: 0 0 3vh white; -} - -/* stats */ - -.actiontext, -.statstext { - position: fixed; - font-size: 3vh; - text-shadow: 0 0 3vh var(--vl-gray); - margin: 0; - text-align: right; - width: 200%; - user-select: none; - transition: all 0.2s ease; - right: 105%; -} - -.actiontext { - opacity: 0; -} - -.smalltext { - position: absolute; - font-size: 0.9em; - color: var(--l-gray); - user-select: none; -} - -#smallStat1, -#smallStat2, -#smallStat3 { - right: 150%; - font-size: 2.1vh; - text-align: right; -} - -#statName1, -#statName2, -#statName3 { - text-align: right; - right: 105%; - transform: translateY(0.5vh); -} - -.statText, -#explanationText, -#updatetext { - display: flex; - margin: 0; - opacity: 0.4; -} - -.statText { - opacity: 1; -} - -/* focus */ -#nofocus { - font-family: "Montserrat", sans-serif; - display: none; - opacity: 1; - text-align: center; - position: fixed; - left: 50%; - top: 50%; - transform: translate(-50%, -50%); - border: 3px solid red; - color: red; - font-size: 2.5em; - padding: .2em; - background-color: #000A; - transition: 0.2s; -} - -#nofocus span { - display: block; - color: #fff; - font-size: .6em; +/* main menu */ +#settingsPanel[open] { + display: flex; + align-items: center; + justify-content: center; + flex-wrap: wrap; + flex-direction: row; + border: none; + width: 100vw; + height: 50vh; + background-color: var(--invis); + overflow: visible; +} + +#settingsPanel>br { + width: 100%; + content: ' '; +} + +button>img { + -webkit-user-drag: none; +} + +.settingPanelButton { + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + margin: 1%; + height: 18vh; + aspect-ratio: 1/1; + border: 0.3vh solid var(--l-gray); + border-radius: 1.5vh; + transition: all 0.5s ease-out; + background-color: var(--invis); + padding: 0.5vh; + outline: none; +} + + +.settingPanelButton>p { + color: white; + margin: 0; + padding: 0; + opacity: 0.3; + transform: translateY(1vh); + letter-spacing: 0.1em; +} + +.settingPanelButton>img { + height: 75%; +} + +.settingPanelButton:hover { + border: 0.3vh solid var(--cl-blue); + transition: all 0.1s ease; +} + +.settingPanelButton:disabled, +.smallPanelButton:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.smallPanelButton, +.smallerPanelButton { + height: 6vh; + width: 6vh; + border-radius: 1vmin; + display: flex; + align-items: center; + justify-content: center; + flex-wrap: wrap; + flex-direction: row; +} + +.smallerPanelButton { + height: 4vh; + width: 4vh; + border-radius: 0.7vh; + border-width: 0.2vh; +} + +.smallerPanelButton:hover { + border-width: 0.2vh; +} + +.smallerPanelButton>img { + height: 100% +} + +.spanleft { + flex: 1; + text-align: right; + padding-right: 1vw; + width: 9vw; +} + +.spanright { + flex: 1; + padding-left: 1vw; +} + +/* songs */ +#songSelector { + display: flex; + align-items: center; + justify-content: center; + width: 40vw; + height: 15vh; + overflow: visible; + white-space: nowrap; + opacity: 0.4; + transition: all 0.5s ease; +} + +#songSelector:hover { + opacity: 0.8; + transition: all 0.1s ease; +} + +#songText { + position: absolute; + transform: translateY(-4vh); + align-self: center; + padding: 0; + margin: 0; +} + +#songProgress { + height: 0.8vh; + border-radius: 5vh; +} + +#songProgress::-webkit-progress-bar { + background-color: var(--e-black); + border-radius: 5vh; +} + +#songProgress::-moz-progress-bar { + background-color: var(--e-black); + border-radius: 5vh; +} + + +#songProgress::-webkit-progress-value { + background-color: var(--cl-blue); + border-radius: 5vh; +} + +#songProgress::-moz-progress-value { + background-color: var(--cl-blue); + border-radius: 5vh; +} + +/* option styles */ +.range { + appearance: none; + -webkit-appearance: none; + height: 1vh; + width: 40%; + right: 3%; + border: 0.2vmin solid white; + background: var(--e-black); + outline: none; + -webkit-transition: 0.2s; + transition: opacity 0.2s; +} + +.range:hover { + border-color: var(--p-green); +} + +.check { + width: 3.5vh; + height: 3.5vh; + appearance: none; + -webkit-appearance: none; + background-color: var(--almost-invis); + border: 0.2vmin solid white; + border-radius: 3vh; + transition: all 0.1s ease; +} + +.check:hover { + border-color: var(--p-green); +} + +.check:checked { + background-color: var(--p-green); + border: none; +} + +.textInput, +.dropdown, +.number { + padding: 0.6vh; + background-color: var(--almost-invis); + border: 0.1vmin solid white; + color: rgb(225, 225, 225); + font-size: 1em; +} + +.number { + width: 20%; + appearance: initial; + -moz-appearance: textfield; +} + +.textInput:hover, +.textInput:focus, +.dropdown:hover, +.dropdown:focus, +.number:hover, +.number:focus { + border-radius: 0; + outline: none; + border-color: var(--p-green); +} + +.keybind { + padding: 0.5vh; + background-color: var(--invis); + border: none; + font-size: 1em; + color: var(--c-blue); +} + +.settingRow:hover>.keybind { + color: var(--p-green); +} + +/* change keybinds */ +#frontdrop { + width: 100%; + height: 100%; + position: fixed; + background-color: var(--slight-invis); + overflow: hidden; + color: var(--cl-blue); + outline: none; + user-select: none; + border: none; + animation: fadein 0.4s forwards ease; + font-family: "Montserrat", sans-serif; +} + +#frontdrop.closingAnimation { + animation: fadeout 0.2s forwards ease; +} + +#selectkeydiv { + display: flex; + width: 100%; + height: 100%; + justify-content: center; + align-items: center; + font-size: 5em; + text-shadow: 0 0 3vh white; +} + +#selectkeybigtext { + transform: translateY(-8vh); +} + +#selectkeytext { + position: absolute; + top: 46vh; + font-size: 0.3em; + text-shadow: 0 0 3vh white; +} + +/* do not remove, used in menu */ +.statText { + opacity: 1; + display: flex; + margin: 0.5vmin; +} + +/* focus */ +#nofocus { + font-family: "Montserrat", sans-serif; + display: none; + opacity: 1; + text-align: center; + position: fixed; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + border: 3px solid red; + color: red; + font-size: 2.5em; + padding: .2em; + background-color: #000A; + transition: 0.2s; +} + +#nofocus span { + display: block; + color: #fff; + font-size: .6em; +} + +#explanationText, +#updatetext { + display: flex; + margin: 0; + opacity: 0.4; } \ No newline at end of file diff --git a/styles/settings.css b/styles/settings.css index 57aa1b0..371372c 100644 --- a/styles/settings.css +++ b/styles/settings.css @@ -1,226 +1,225 @@ -/* dialog */ -.scrollSettings { - font-family: "Montserrat", sans-serif; - height: 90vh; - aspect-ratio: 2/3; - background-color: var(--almost-invis); - padding: 0.5vmin; - color: white; - user-select: none; - outline: none; - text-shadow: 0 0 5vh var(--vl-gray); - border: 0.5vmin solid transparent; - animation: zoomin 0.3s forwards ease; - background-clip: padding-box; - overflow: hidden; -} - -.scrollSettings.closingAnimation { - animation: zoomout 0.3s forwards ease; -} - -.scrollSettings::backdrop { - background-color: #000000b5; - backdrop-filter: blur(5px); - animation: fadein 0.4s forwards ease; -} - -.scrollSettings.closingAnimation::backdrop { - animation: fadeout 0.4s forwards ease; -} - -/* border */ -.scrollSettings::before, -.scrollSettings::after { - content: ''; - position: absolute; - top: 0; - bottom: 0; - width: 0.3vmin; - background: linear-gradient(to bottom, transparent, white 35%, white 65%, transparent); - box-sizing: border-box; -} - -.scrollSettings::before { - left: 0; -} - -.scrollSettings::after { - right: 0; -} - -/* top */ -.settingsTop { - font-size: 1.5em; - text-align: center; - height: 10%; - width: 100%; -} - -/* settings list */ -.settingsBox { - height: 70%; - width: 100%; - overflow-x: hidden; - overflow-y: scroll; - font-size: 1.1em; - padding: 0px; - margin: 0px; - display: flex; - flex-direction: column; - box-sizing: border-box; - padding-top: 8vh; - padding-bottom: 10vh; -} - -.settingRow, -.closeDialogButton, -.settingsReset, -.gamemodeSelect { - display: flex; - align-items: center; - height: 4.2vh; - --s: 2vmin; - --t: 0.3vmin; - --g: 1vmin; - - padding: calc(var(--g) + var(--t)); - outline: var(--t) solid #ffffff; - outline-offset: calc(var(--g)/-1); - mask: conic-gradient(at var(--s) var(--s), #0000 75%, #000 0) 0 0/calc(100% - var(--s)) calc(100% - var(--s)), - linear-gradient(#000 0 0) content-box; - transition: scale 0.6s, opacity 0.6s, outline-offset 0.3s; -} - -.settingRow:hover, -.closeDialogButton:hover, -.settingsReset:hover, -.gamemodeSelect:hover { - outline-offset: calc(-1*var(--t)); - animation: jump 1.5s infinite ease 0.4s; - transition: outline-offset 0.1s, scale 0.6s; -} - -.settingRow>p { - width: 50%; - display: flex; - justify-content: flex-end; - text-align: right; - padding-right: 1.5vw; - pointer-events: none; -} - -.settingRow>.option { - position: unset; - display: flex; - justify-content: flex-start; - transform: none; -} - -/* footer */ -.settingsFooter { - height: 20%; - width: 100%; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 1vh; -} - -.settingsFooter>p { - margin: 0; - font-size: 0.8em; - opacity: 0.4; - width: 100%; - text-align: center; - -} - -.closeDialogButton, -.settingsReset { - height: auto; - background-color: var(--invis); - border: none; - padding: 2vh; - font-size: 1.4em; - color: white; - transition: all 0.4s ease; -} - -/* buttons and misc */ -.settingsReset { - position: absolute; - right: 1vw; - bottom: 1vh; - padding: 1vh; - padding-top: 1.5vh; - display: flex; - justify-content: center; - align-items: center; - height: 6vh; - aspect-ratio: 1/1; -} - -#gamemodeDialog>.settingsBox { - align-items: center; - padding-top: 0; - padding-bottom: 0; -} - -.gamemodeSelect { - height: 8vh; - width: 80%; - background-color: var(--invis); - border: none; - padding: 1.3vh; - font-size: 1.3em; - color: white; - transition: all 0.4s ease; - justify-content: center; -} - -.gamemodeSelect:hover { - outline-color: var(--p-green); - color: var(--p-green); -} - -.gamemodeSelect.selected { - outline-color: var(--green); - color: var(--green); -} - -#editMenu>.settingsBox { - padding-top: 0; -} - -#gameStatsDialog>.settingsFooter { - flex-direction: row; - height: 15%; -} - -#gameEnd>.settingsFooter { - position: absolute; - bottom: 10%; - width: 100%; - flex-direction: row; - height: auto; -} - -#changeRangeValue>.settingsBox { - padding-top: 0; - padding-bottom: 0; - height: auto; - align-items: center; -} - -.settingRow:has(.dropdown) { - animation: none; -} - -.dropdown { - width: 20%; - margin: 1%; - background: black; - color: white; -} +/* dialog */ +.scrollSettings { + font-family: "Montserrat", sans-serif; + height: 90vh; + aspect-ratio: 2/3; + background-color: var(--almost-invis); + padding: 0.5vmin; + color: white; + user-select: none; + outline: none; + text-shadow: 0 0 5vh var(--vl-gray); + border: 0.5vmin solid transparent; + animation: zoomin 0.3s forwards ease; + background-clip: padding-box; + overflow: hidden; +} + +.scrollSettings.closingAnimation { + animation: zoomout 0.3s forwards ease; +} + +.scrollSettings::backdrop { + background-color: #000000b5; + backdrop-filter: blur(5px); + animation: fadein 0.4s forwards ease; +} + +.scrollSettings.closingAnimation::backdrop { + animation: fadeout 0.4s forwards ease; +} + +/* border */ +.scrollSettings::before, +.scrollSettings::after { + content: ''; + position: absolute; + top: 0; + bottom: 0; + width: 0.3vmin; + background: linear-gradient(to bottom, transparent, white 35%, white 65%, transparent); + box-sizing: border-box; +} + +.scrollSettings::before { + left: 0; +} + +.scrollSettings::after { + right: 0; +} + +/* top */ +.settingsTop { + font-size: 1.5em; + text-align: center; + height: 10%; + width: 100%; +} + +/* settings list */ +.settingsBox { + height: 70%; + width: 100%; + overflow-x: hidden; + overflow-y: scroll; + font-size: 1.1em; + padding: 0px; + margin: 0px; + display: flex; + flex-direction: column; + box-sizing: border-box; + padding-top: 8vh; + padding-bottom: 10vh; +} + +.settingRow, +.closeDialogButton, +.settingsReset, +.gamemodeSelect { + display: flex; + align-items: center; + height: 4.2vh; + --s: 2vmin; + --t: 0.3vmin; + --g: 1vmin; + + padding: calc(var(--g) + var(--t)); + outline: var(--t) solid #ffffff; + outline-offset: calc(var(--g)/-1); + mask: conic-gradient(at var(--s) var(--s), #0000 75%, #000 0) 0 0/calc(100% - var(--s)) calc(100% - var(--s)), + linear-gradient(#000 0 0) content-box; + transition: scale 0.6s, opacity 0.6s, outline-offset 0.3s; +} + +.settingRow:hover, +.closeDialogButton:hover, +.settingsReset:hover, +.gamemodeSelect:hover { + outline-offset: calc(-1*var(--t)); + animation: jump 1.5s infinite ease 0.4s; + transition: outline-offset 0.1s, scale 0.6s; +} + +.settingRow>p { + width: 50%; + display: flex; + justify-content: flex-end; + text-align: right; + padding-right: 1.5vw; + pointer-events: none; +} + +.settingRow>.option { + position: unset; + display: flex; + justify-content: flex-start; + transform: none; +} + +/* footer */ +.settingsFooter { + height: 20%; + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 1vh; +} + +.settingsFooter>p { + margin: 0; + font-size: 0.8em; + opacity: 0.4; + width: 100%; + text-align: center; + +} + +.closeDialogButton, +.settingsReset { + height: auto; + background-color: var(--invis); + border: none; + padding: 2vh; + font-size: 1.4em; + color: white; + transition: all 0.4s ease; +} + +/* buttons and misc */ +.settingsReset { + position: absolute; + right: 1vw; + bottom: 1vh; + padding: 1vh; + display: flex; + justify-content: center; + align-items: center; + height: 6vh; + aspect-ratio: 1/1; +} + +#gamemodeDialog>.settingsBox { + align-items: center; + padding-top: 0; + padding-bottom: 0; +} + +.gamemodeSelect { + height: 8vh; + width: 80%; + background-color: var(--invis); + border: none; + padding: 1.3vh; + font-size: 1.3em; + color: white; + transition: all 0.4s ease; + justify-content: center; +} + +.gamemodeSelect:hover { + outline-color: var(--p-green); + color: var(--p-green); +} + +.gamemodeSelect.selected { + outline-color: var(--green); + color: var(--green); +} + +#editMenu>.settingsBox { + padding-top: 0; +} + +#gameStatsDialog>.settingsFooter { + flex-direction: row; + height: 15%; +} + +#gameEnd>.settingsFooter { + position: absolute; + bottom: 10%; + width: 100%; + flex-direction: row; + height: auto; +} + +#changeRangeValue>.settingsBox { + padding-top: 0; + padding-bottom: 0; + height: auto; + align-items: center; +} + +.settingRow:has(.dropdown) { + animation: none; +} + +.dropdown { + width: 20%; + margin: 1%; + background: black; + color: white; +} diff --git a/styles/style.css b/styles/style.css index ff7a80c..815b7b5 100644 --- a/styles/style.css +++ b/styles/style.css @@ -1,241 +1,200 @@ -@import url("https://fonts.googleapis.com/css2?family=Montserrat&display=swap"); -@import url("https://fonts.googleapis.com/css2?family=Major+Mono+Display&display=swap"); - -:root { - --night: #080b0c; - --e-black: #1b1e22; - --c-blue: #accbe1; - --cl-blue: #dbeaf3; - --l-gray: #ffffff50; - --vl-gray: #ffffffc4; - --invis: #00000000; - --almost-invis: #00000024; - --slight-invis: #000000cf; - --p-green: #53b565; - --green: #30f24d; - --gray: #808080; -} - -body { - color: var(--cl-blue); - font-family: "Major Mono Display", monospace; - margin: 0; - height: 100vh; - width: 100vw; - overflow: hidden; - background: var(--night); - --background: black; -} - -#splashScreen { - position: fixed; - width: 100vw; - height: 100vh; - top: 0; - left: 0; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - background-color: var(--slight-invis); - backdrop-filter: blur(10px); - z-index: 1; - text-shadow: 0 0 8vh rgb(255, 255, 255); - transition: all 0.5s ease, display 1.5s allow-discrete; - scale: 1; -} - -#splashScreen>h1 { - font-size: 7em; - animation: pulse 2s infinite ease; - opacity: 1; -} - -#ignoreText { - opacity: 0; - transition: all 1s ease; -} - -#openSettingsButton, -#editButton { - top: 5%; - position: fixed; - left: 127%; - display: flex; - justify-content: right; - padding-right: 3%; - align-items: center; - height: 8%; - width: 24%; - border: 0.2vh solid white; - border-radius: 0 1.5vh 1.5vh 0; - transition: all 0.5s ease-out; - background-color: var(--invis); - outline: none; - opacity: 0.6; -} - -#editButton { - top: 15%; -} - -#openSettingsButton:hover, -#editButton:hover { - left: 133%; - opacity: 1; - transition: all 0.2s ease; -} - -#openSettingsButton>img { - height: 70%; - user-select: none; -} - -#editButton>img { - height: 100%; - user-select: none; -} - -.dialog img, -.scrollSettings img { - opacity: 0.6; - transition: all 0.3s ease; -} - -.dialog>*:hover img, -.scrollSettings img:hover { - opacity: 1; -} - -/* scrollbars */ -::-webkit-scrollbar { - width: 0.4vw; -} - -::-webkit-scrollbar-track { - border-radius: 1vh; - background: var(--invis); - margin-top: 1vh; - margin-bottom: 1vh; -} - -::-webkit-scrollbar-thumb { - border-radius: 1vh; - background: var(--vl-gray); -} - -::-webkit-scrollbar-thumb:hover { - background: white; - transition: all 0.2s ease; -} - -@-moz-document url-prefix() { - * { - scrollbar-width: thin; - scrollbar-color: var(--vl-gray) var(--invis); - } -} - -/* sliders */ -.range::-webkit-slider-thumb { - -webkit-appearance: none; - appearance: none; - width: 1.5vh; - height: 2.5vh; - background: white; - cursor: pointer; -} - -.range::-moz-range-thumb { - -moz-appearance: none; - appearance: none; - width: 1.5vh; - height: 2.5vh; - background: white; - cursor: pointer; -} - -.range::-webkit-slider-thumb:hover { - background: var(--p-green); -} - -/* remove number arrows */ -.number::-webkit-outer-spin-button, -.number::-webkit-inner-spin-button { - -webkit-appearance: none; - margin: 0; -} - -/* animations */ -@keyframes jump { - 0% { - transform: translateY(0vmin); - } - - 50% { - transform: translateY(0.2vmin); - } - - 100% { - transform: translateY(0vmin); - } -} - -@keyframes fadein { - from { - opacity: 0; - } - - to { - opacity: 1; - } -} - - -@keyframes zoomin { - from { - opacity: 0; - transform: scale(0.9); - } - - to { - opacity: 1; - transform: scale(1); - } -} - -@keyframes fadeout { - from { - opacity: 1; - } - - to { - opacity: 0; - } -} - -@keyframes zoomout { - from { - opacity: 1; - transform: scale(1); - } - - to { - opacity: 0; - transform: scale(0.9); - } -} - -@keyframes pulse { - 0% { - opacity: 1; - } - - 50% { - opacity: 0.75; - } - - 100% { - opacity: 1; - } +@import url("https://fonts.googleapis.com/css2?family=Major+Mono+Display&display=swap"); + +:root { + --night: #080b0c; + --e-black: #1b1e22; + --c-blue: #accbe1; + --cl-blue: #dbeaf3; + --l-gray: #ffffff50; + --vl-gray: #ffffffc4; + --invis: #00000000; + --almost-invis: #00000024; + --slight-invis: #000000cf; + --p-green: #53b565; + --green: #30f24d; + --gray: #808080; +} + +body { + color: var(--cl-blue); + font-family: "Major Mono Display", monospace; + margin: 0; + /* height: 100vh; */ + /* width: 100vw; */ + overflow: hidden; + background: var(--night); + --background: black; +} + +#splashScreen { + position: fixed; + width: 100vw; + height: 100vh; + top: 0; + left: 0; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + background-color: var(--slight-invis); + backdrop-filter: blur(10px); + z-index: 1; + text-shadow: 0 0 8vh rgb(255, 255, 255); + transition: all 0.5s ease, display 1.5s allow-discrete; + scale: 1; +} + +#splashScreen>h1 { + font-size: 7em; + animation: pulse 2s infinite ease; + opacity: 1; +} + +#ignoreText { + opacity: 0; + transition: all 1s ease; +} + +.dialog img, +.scrollSettings img { + opacity: 0.6; + transition: all 0.3s ease; +} + +.dialog>*:hover img, +.scrollSettings img:hover { + opacity: 1; +} + +/* scrollbars */ +::-webkit-scrollbar { + width: 0.4vw; +} + +::-webkit-scrollbar-track { + border-radius: 1vh; + background: var(--invis); + margin-top: 1vh; + margin-bottom: 1vh; +} + +::-webkit-scrollbar-thumb { + border-radius: 1vh; + background: var(--vl-gray); +} + +::-webkit-scrollbar-thumb:hover { + background: white; + transition: all 0.2s ease; +} + +@-moz-document url-prefix() { + * { + scrollbar-width: thin; + scrollbar-color: var(--vl-gray) var(--invis); + } +} + +/* sliders */ +.range::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 1.5vh; + height: 2.5vh; + background: white; + cursor: pointer; +} + +.range::-moz-range-thumb { + -moz-appearance: none; + appearance: none; + width: 1.5vh; + height: 2.5vh; + background: white; + cursor: pointer; +} + +.range::-webkit-slider-thumb:hover { + background: var(--p-green); +} + +/* remove number arrows */ +.number::-webkit-outer-spin-button, +.number::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + +/* animations */ +@keyframes jump { + 0% { + transform: translateY(0vmin); + } + + 50% { + transform: translateY(0.2vmin); + } + + 100% { + transform: translateY(0vmin); + } +} + +@keyframes fadein { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + + +@keyframes zoomin { + from { + opacity: 0; + transform: scale(0.9); + } + + to { + opacity: 1; + transform: scale(1); + } +} + +@keyframes fadeout { + from { + opacity: 1; + } + + to { + opacity: 0; + } +} + +@keyframes zoomout { + from { + opacity: 1; + transform: scale(1); + } + + to { + opacity: 0; + transform: scale(0.9); + } +} + +@keyframes pulse { + 0% { + opacity: 1; + } + + 50% { + opacity: 0.75; + } + + 100% { + opacity: 1; + } } \ No newline at end of file