├── .gitignore ├── src ├── hidden.png ├── webpack.config.js ├── play.html ├── polyfill.js ├── package.json ├── achievements.css ├── index.html └── achievements.js ├── patch.sh ├── .github └── workflows │ └── publish.yml ├── README.md ├── LICENSE └── download.sh /.gitignore: -------------------------------------------------------------------------------- 1 | /download 2 | /game -------------------------------------------------------------------------------- /src/hidden.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ading2210/sheepy-web-port/HEAD/src/hidden.png -------------------------------------------------------------------------------- /src/webpack.config.js: -------------------------------------------------------------------------------- 1 | const NodePolyfillPlugin = require("node-polyfill-webpack-plugin"); 2 | 3 | module.exports = { 4 | entry: [ 5 | './scripts/main.js', 6 | './scripts/c3runtime.js' 7 | ], 8 | plugins: [ 9 | new NodePolyfillPlugin() 10 | ], 11 | target: 'web', 12 | resolve: { 13 | fallback: { 14 | "fs": false, 15 | "child_process": false 16 | } 17 | }, 18 | }; -------------------------------------------------------------------------------- /patch.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #apply patches to the game 4 | 5 | #don't load the original c3runtime 6 | sed -i 's/"scripts\/c3runtime.js"//' game/scripts/main.js 7 | 8 | #custom callback for achievements 9 | sed -i -r '/ActivateAchievement\(achievement\)/a \{activate_achievement(achievement)\}, _()' game/scripts/c3runtime.js 10 | 11 | #prevent memory leak when loading audio 12 | sed -i 's/!isMusic||audioDomHandler.IsPlayMusicAsSound()||needsSoftwareDecode/false/' game/scripts/main.js 13 | 14 | #copy modified assets in src folder 15 | cp src/* game/ 16 | 17 | #bundle it for the web 18 | cd game 19 | npm i 20 | npx webpack -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | on: [push] 2 | 3 | jobs: 4 | publish: 5 | runs-on: ubuntu-latest 6 | permissions: 7 | contents: read 8 | deployments: write 9 | name: Publish to Cloudflare Pages 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v3 13 | 14 | - name: Build 15 | run: | 16 | sudo apt-get install -y jq curl wget pcregrep 17 | ./build.sh 18 | 19 | - name: Publish to Cloudflare Pages 20 | uses: cloudflare/pages-action@v1 21 | with: 22 | apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} 23 | accountId: b8079ffa92c97010f2a8d759e24cc782 24 | projectName: sheepy 25 | directory: game -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sheepy: A Short Adventure - Unofficial Web Port 2 | 3 | This is a port of the game [Sheepy: A Short Adventure](https://store.steampowered.com/app/1568400/Sheepy_A_Short_Adventure/) to the web. Since the original game uses the [Construct](https://www.construct.net/en) game engine with no native code, only minimal changes are needed to get it to run on web browsers. 4 | 5 | ## Features: 6 | - Local achievements 7 | - Reduced memory usage (3GB -> 300MB on the title screen) 8 | - Runs in Firefox/Chrome on any OS 9 | - Controller support 10 | 11 | ## Building: 12 | To build this port for yourself, run `./build.sh`. You must be on Linux with `pcregrep`, `curl`, `jq`, and NodeJS installed. 13 | 14 | ## License: 15 | The copyright for the original game belongs to MrSuicideSheep, but the patches for this port are under the MIT license. 16 | 17 | This repository does not contain any of the game's code, only the modifications needed to build this port are included. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2024 ading2210 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /src/play.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Sheepy: A Short Adventure - Unofficial Web Port 6 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /download.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #download the game from itch.io 4 | 5 | mkdir -p download 6 | mkdir -p game 7 | 8 | main_html="$(curl "https://mrsuicidesheep.itch.io/sheepy")" 9 | csrf="$(echo "$main_html" | pcregrep -o1 '')" 10 | api_url="https://mrsuicidesheep.itch.io/sheepy/file/9759425?source=game_download&after_download_lightbox=1&as_props=1" 11 | 12 | game_json="$(curl "$api_url" \ 13 | --compressed -X POST -H 'User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0' \ 14 | -H 'Accept: */*' \ 15 | -H 'Accept-Language: en-US,en;q=0.5' \ 16 | -H 'Accept-Encoding: gzip, deflate, br' \ 17 | -H 'Content-Type: application/x-www-form-urlencoded; charset=UTF-8' \ 18 | -H 'X-Requested-With: XMLHttpRequest' \ 19 | -H 'Origin: https://mrsuicidesheep.itch.io' \ 20 | -H 'Sec-Fetch-Dest: empty' \ 21 | -H 'Sec-Fetch-Mode: cors' \ 22 | -H 'Sec-Fetch-Site: same-origin' \ 23 | --data-raw "csrf_token=$csrf")" 24 | download_url="$(echo $game_json | jq -r '.url')" 25 | zip_file="download/sheepy.zip" 26 | 27 | wget -c $download_url -O $zip_file 28 | 29 | #extract the nw.js bundle from the game files 30 | 31 | bundle_name="SHEEPY A Short Adventure/package.nw" 32 | bundle_file="download/bundle.zip" 33 | unzip -p "$zip_file" "$bundle_name" > $bundle_file 34 | unzip -o $bundle_file -d game -------------------------------------------------------------------------------- /src/polyfill.js: -------------------------------------------------------------------------------- 1 | window.process = { 2 | platform: "linux", 3 | execPath: "/", 4 | mainModule: { 5 | filename: "/", 6 | }, 7 | }; 8 | 9 | window.nw = { 10 | App: { 11 | clearCache() {}, 12 | manifest: { 13 | "c3-app-icons": [ 14 | { 15 | height: 256, 16 | src: "icons\\icon-256.png", 17 | width: 256, 18 | }, 19 | ], 20 | "c3-stream-mode": true, 21 | name: "SheepyAShortAdventure", 22 | version: "1.0.0", 23 | window: { 24 | frame: true, 25 | height: 360, 26 | icon: "icons/icon-256.png", 27 | kiosk: false, 28 | position: "center", 29 | resizable: true, 30 | show: true, 31 | title: "SheepyAShortAdventure", 32 | width: 640, 33 | }, 34 | }, 35 | }, 36 | 37 | Window: { 38 | get() { 39 | return { 40 | title: "SheepyAShortAdventure", 41 | x: 0, 42 | y: 0, 43 | get width() { 44 | return window.innerWidth; 45 | }, 46 | get height() { 47 | return window.innerHeight; 48 | }, 49 | on() {}, 50 | }; 51 | }, 52 | }, 53 | 54 | Clipboard: { 55 | get() { 56 | return { 57 | get() { 58 | return ""; 59 | }, 60 | }; 61 | }, 62 | }, 63 | 64 | Shell: { 65 | openExternal(url) { 66 | window.open(url); 67 | } 68 | } 69 | }; 70 | -------------------------------------------------------------------------------- /src/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "index.html", 3 | "name": "sheepy-web-port", 4 | "description": "", 5 | "version": "1.0.0", 6 | "window": { 7 | "icon": "icons/icon-256.png", 8 | "title": "SheepyAShortAdventure", 9 | "width": 640, 10 | "height": 360, 11 | "position": "center", 12 | "resizable": true, 13 | "frame": true, 14 | "kiosk": false, 15 | "show": true 16 | }, 17 | "user-agent": "Mozilla/5.0 (%osinfo) AppleWebKit/%webkit_ver (KHTML, like Gecko, Chrome, Safari) NWjs/%nwver", 18 | "chromium-args": "--enable-node-worker --disable-plugins --disable-internal-flash --disable-popup-blocking --allow-file-access-from-files --disable-features=HardwareMediaKeyHandling --ignore-gpu-blacklist --in-process-gpu --disable-windows10-custom-titlebar --auto-open-devtools-for-tabs", 19 | "c3-steam-mode": true, 20 | "c3-app-icons": [ 21 | { 22 | "src": "icons\\icon-16.png", 23 | "width": 16, 24 | "height": 16 25 | }, 26 | { 27 | "src": "icons\\icon-32.png", 28 | "width": 32, 29 | "height": 32 30 | }, 31 | { 32 | "src": "icons\\icon-114.png", 33 | "width": 114, 34 | "height": 114 35 | }, 36 | { 37 | "src": "icons\\icon-128.png", 38 | "width": 128, 39 | "height": 128 40 | }, 41 | { 42 | "src": "icons\\icon-256.png", 43 | "width": 256, 44 | "height": 256 45 | } 46 | ], 47 | "dependencies": { 48 | "node-polyfill-webpack-plugin": "^3.0.0", 49 | "webpack": "^5.90.2", 50 | "webpack-cli": "^5.1.4" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/achievements.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "04b03"; 3 | src: url("fonts/04b_03__.ttf"); 4 | } 5 | 6 | * { 7 | font-family: "04b03", monospace; 8 | color: white; 9 | box-sizing: border-box; 10 | } 11 | 12 | ::-webkit-scrollbar { 13 | width: 8px; 14 | } 15 | ::-webkit-scrollbar-track { 16 | background: #101010; 17 | } 18 | ::-webkit-scrollbar-thumb { 19 | background: #cccccc; 20 | } 21 | 22 | body { 23 | overflow-x: hidden; 24 | } 25 | 26 | #achievements_container { 27 | height: 300px; 28 | width: 100%; 29 | overflow: auto; 30 | border: 1px solid white; 31 | padding: 8px; 32 | margin-bottom: 30px; 33 | } 34 | #achievements_container > h3 { 35 | margin: 0px; 36 | } 37 | 38 | .achievements_grid { 39 | display: none; 40 | width: 100%; 41 | grid-template-columns: repeat(3, 1fr); 42 | gap: 8px; 43 | margin-top: 4px; 44 | margin-bottom: 16px; 45 | } 46 | 47 | .achievement_img { 48 | width: 64px; 49 | height: 64px; 50 | } 51 | 52 | .achievement_div { 53 | display: flex; 54 | } 55 | 56 | .achievement_text > p { 57 | margin: 0px; 58 | } 59 | .achievement_text { 60 | padding: 4px; 61 | padding-left: 8px; 62 | } 63 | .achievement_description { 64 | font-size: 15px; 65 | } 66 | 67 | @keyframes notification_animation { 68 | 0% {right: -270px;} 69 | 10% {right: 0px;} 70 | 90% {right: 0px;} 71 | 100% {right: -270px;} 72 | } 73 | 74 | .notification { 75 | position: absolute; 76 | display: flex; 77 | bottom: 0px; 78 | right: -270px; 79 | width: 270px; 80 | padding: 8px; 81 | background-color: #101010; 82 | animation-name: notification_animation; 83 | animation-duration: 5s; 84 | } 85 | 86 | .notification_img { 87 | width: 48px; 88 | height: 48px; 89 | } -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Sheepy: A Short Adventure - Unofficial Web Port 9 | 55 | 56 | 57 | 58 |

Sheepy: A Short Adventure - Unofficial Web Port

59 | 60 | 61 | 62 | 63 | 64 | Launch Game 65 | 66 | 67 |

Achievements:

68 |
69 |

Unlocked:

70 |
71 |

Locked:

72 |
73 |
74 | 75 | 76 |

Disclaimer:

77 |
78 |

This is an fan made port and it is unaffiliated with MrSuicideSheep. Any bug reports should be directed here instead of towards the original developers.

79 |

Please support the original developers by visiting the Itch.io page and the Steam listing for the original game.

80 |
81 | 82 | -------------------------------------------------------------------------------- /src/achievements.js: -------------------------------------------------------------------------------- 1 | //achievements list taken from the steam web api 2 | const achievements = { 3 | vinyl0: { 4 | defaultvalue: 0, 5 | displayName: "Vinyl #0", 6 | hidden: 1, 7 | icon: "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/1568400/afe81040f9fe00f80756d191141e36967cdaee80.jpg", 8 | icongray: 9 | "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/1568400/17b14ae91518763b1219334fcbf7e58baa87d073.jpg", 10 | }, 11 | vinyl1: { 12 | defaultvalue: 0, 13 | displayName: "Vinyl #1", 14 | hidden: 1, 15 | icon: "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/1568400/c2c57cdedbbae835042ae6c31a9140b70e00f247.jpg", 16 | icongray: 17 | "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/1568400/17b14ae91518763b1219334fcbf7e58baa87d073.jpg", 18 | }, 19 | vinyl2: { 20 | defaultvalue: 0, 21 | displayName: "Vinyl #2", 22 | hidden: 1, 23 | icon: "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/1568400/423960b689a6c4685cd234dd504eb497b864582f.jpg", 24 | icongray: 25 | "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/1568400/17b14ae91518763b1219334fcbf7e58baa87d073.jpg", 26 | }, 27 | vinyl3: { 28 | defaultvalue: 0, 29 | displayName: "Vinyl #3", 30 | hidden: 1, 31 | icon: "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/1568400/f670e671a4e93483e3b708166afddbcc773ede90.jpg", 32 | icongray: 33 | "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/1568400/17b14ae91518763b1219334fcbf7e58baa87d073.jpg", 34 | }, 35 | vinyl4: { 36 | defaultvalue: 0, 37 | displayName: "Vinyl #4", 38 | hidden: 1, 39 | icon: "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/1568400/c3e65e902f37446c2c14a5cc702667a2f6e97066.jpg", 40 | icongray: 41 | "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/1568400/17b14ae91518763b1219334fcbf7e58baa87d073.jpg", 42 | }, 43 | vinyl5: { 44 | defaultvalue: 0, 45 | displayName: "Vinyl #5", 46 | hidden: 1, 47 | icon: "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/1568400/ea47a9dac3ee03c7399a398e53193909d28145a9.jpg", 48 | icongray: 49 | "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/1568400/17b14ae91518763b1219334fcbf7e58baa87d073.jpg", 50 | }, 51 | phone: { 52 | defaultvalue: 0, 53 | displayName: "Hello?", 54 | hidden: 0, 55 | description: '"Your estimated wait time is forever."', 56 | icon: "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/1568400/70aad69cdf709b79e9c37ecd5cfbb1ea1affb77e.jpg", 57 | icongray: 58 | "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/1568400/958137ceb9cb55380792f35be3e210f1c238cb5b.jpg", 59 | }, 60 | fly: { 61 | defaultvalue: 0, 62 | displayName: "I believe I can fly", 63 | hidden: 0, 64 | description: "...but no you can't, because you're a sheep.", 65 | icon: "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/1568400/52f20c3538aa36fceae73d24875e700b70384300.jpg", 66 | icongray: 67 | "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/1568400/06331c2e6081b48a1ec489024381d72d9e9d7114.jpg", 68 | }, 69 | skill1: { 70 | defaultvalue: 0, 71 | displayName: "Double jump!", 72 | hidden: 0, 73 | description: '"Taking You Higher"', 74 | icon: "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/1568400/fc99bbd5ca67df2681020c31a53e55a7ca6d46ac.jpg", 75 | icongray: 76 | "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/1568400/2b8c474865546ffe7fd8d38d23889ecf96741bc0.jpg", 77 | }, 78 | skill2: { 79 | defaultvalue: 0, 80 | displayName: "Time to run!", 81 | hidden: 0, 82 | description: "GOTTA GO FAST", 83 | icon: "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/1568400/4dcee3b7a7da712f7367a7f00613f245e871bb2a.jpg", 84 | icongray: 85 | "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/1568400/7844c98da2f6759f5dd34636e1ab2c04522f7c24.jpg", 86 | }, 87 | skill3: { 88 | defaultvalue: 0, 89 | displayName: "Weird magic...", 90 | hidden: 0, 91 | description: "But now you can fly.", 92 | icon: "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/1568400/35bdac75e830dcf56cda9af1514df6b0f5bb8b8a.jpg", 93 | icongray: 94 | "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/1568400/a3f9ed201b9e4849b199d6dc50a8a441b31ab746.jpg", 95 | }, 96 | scary: { 97 | defaultvalue: 0, 98 | displayName: "What?", 99 | hidden: 0, 100 | description: "Something happened here...", 101 | icon: "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/1568400/fa542b5828d7d5695c58186fad5930a6de67b612.jpg", 102 | icongray: 103 | "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/1568400/09843cec25d32987fa493c89eb73862cca7c5fe5.jpg", 104 | }, 105 | patches1: { 106 | defaultvalue: 0, 107 | displayName: "Hi Patches!", 108 | hidden: 0, 109 | description: "You made a new friend!", 110 | icon: "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/1568400/a8593c37b19ff5541ef610ccffea7f9468a8bbf6.jpg", 111 | icongray: 112 | "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/1568400/fe39040b0c219c70bed0f4fb0147b29fa7e9e436.jpg", 113 | }, 114 | elevator: { 115 | defaultvalue: 0, 116 | displayName: "It's working!", 117 | hidden: 0, 118 | description: "Such an engineer.", 119 | icon: "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/1568400/8a5610af54930a6473cad7ec7bf387c6d5182d2e.jpg", 120 | icongray: 121 | "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/1568400/b46e2ddd272fcf75e6639d0b0813c9a1f5ce8c9d.jpg", 122 | }, 123 | crystals: { 124 | defaultvalue: 0, 125 | displayName: "Shine bright like a crystal", 126 | hidden: 0, 127 | description: "You lit all the crystals!", 128 | icon: "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/1568400/b3d745e33fcc9d68ffebf7533007b3ed07dfaabe.jpg", 129 | icongray: 130 | "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/1568400/4f411906def1757627dc420f1f31c5d4ee8bbe38.jpg", 131 | }, 132 | chair: { 133 | defaultvalue: 0, 134 | displayName: "You spin me right 'round", 135 | hidden: 0, 136 | description: "BABY, RIGHT 'ROUND", 137 | icon: "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/1568400/bb5f856f9f5d3adcb12b5a2129ab9a77ab7c2f8a.jpg", 138 | icongray: 139 | "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/1568400/efc6374ff23a7bbf9cf74f33742baf5161962489.jpg", 140 | }, 141 | speedrun1: { 142 | defaultvalue: 0, 143 | displayName: "That's a record!", 144 | hidden: 0, 145 | description: "You beat the game in 45 minutes.", 146 | icon: "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/1568400/75b31f64095c93704f6bf7b290c38c3a091c22a2.jpg", 147 | icongray: 148 | "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/1568400/ba93178f33d687d74825930c0561d0f1227e6e8b.jpg", 149 | }, 150 | speedrun2: { 151 | defaultvalue: 0, 152 | displayName: "Hello, speedrunner.", 153 | hidden: 1, 154 | icon: "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/1568400/f857d0e0e192bd5b331917f844bc54dc1cf5b468.jpg", 155 | icongray: 156 | "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/1568400/ba93178f33d687d74825930c0561d0f1227e6e8b.jpg", 157 | }, 158 | flower: { 159 | defaultvalue: 0, 160 | displayName: "It's just a flower...", 161 | hidden: 0, 162 | description: "It feels nostalgic here.", 163 | icon: "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/1568400/630b370e045df6f4f0ab898aa29847cd07f68c95.jpg", 164 | icongray: 165 | "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/1568400/01535c76d710a05b8808cea7abe6e419e871a869.jpg", 166 | }, 167 | door: { 168 | defaultvalue: 0, 169 | displayName: "Open the door!", 170 | hidden: 0, 171 | description: "You have activated the 6 levers.", 172 | icon: "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/1568400/09a771d48b2edc13cb5f21fc3732838365a60e57.jpg", 173 | icongray: 174 | "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/1568400/b1dded870974f751892b5fe7f520c7014c089274.jpg", 175 | }, 176 | records: { 177 | defaultvalue: 0, 178 | displayName: "Lore master", 179 | hidden: 0, 180 | description: "You listened to all the tapes.", 181 | icon: "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/1568400/d5f451648bd89cbe53e635a41cabd7cfcf5a46ee.jpg", 182 | icongray: 183 | "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/1568400/499dc4fd289f8f5738a917e6c8d28086cc057009.jpg", 184 | }, 185 | trap: { 186 | defaultvalue: 0, 187 | displayName: "Ancient trap", 188 | hidden: 0, 189 | description: "Archaeologist is a dangerous job.", 190 | icon: "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/1568400/c6599dc96e651a628352a9ff39bdcbb9bdcad453.jpg", 191 | icongray: 192 | "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/1568400/33c4e634589932c2feb3c243c318542364aae797.jpg", 193 | }, 194 | run: { 195 | defaultvalue: 0, 196 | displayName: "The way of the rabbit", 197 | hidden: 0, 198 | description: "Run as fast as you can.", 199 | icon: "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/1568400/a6279dd4f5e74229918f5c7c36bada8ea43247e5.jpg", 200 | icongray: 201 | "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/1568400/60997977b01ae56380304f1d904307429820bd68.jpg", 202 | }, 203 | patches2: { 204 | defaultvalue: 0, 205 | displayName: "Complicated relationship", 206 | hidden: 0, 207 | description: "You showed Patches who's the boss.", 208 | icon: "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/1568400/de39309fd2e46573943ea765786a85d2721e657f.jpg", 209 | icongray: 210 | "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/1568400/d801cd3dd5be37e587ed6d25b5c2d28aa4c6a86a.jpg", 211 | }, 212 | paris: { 213 | defaultvalue: 0, 214 | displayName: "Oh!", 215 | hidden: 0, 216 | description: "I know this place!", 217 | icon: "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/1568400/40cc45e6e8448bac7c8062b052874cb44736d7b4.jpg", 218 | icongray: 219 | "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/1568400/0c177147f90e1553101244b91481f547a06f972b.jpg", 220 | }, 221 | easteregg: { 222 | defaultvalue: 0, 223 | displayName: "You found Sheepy!", 224 | hidden: 0, 225 | description: "Wait, another Sheepy?", 226 | icon: "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/1568400/2033c5f047b0cf2248ceea18d7ca6783b652cbcc.jpg", 227 | icongray: 228 | "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/1568400/c3ec492cfa67722e7e46599ded2f63ccb9db15e9.jpg", 229 | }, 230 | portal: { 231 | defaultvalue: 0, 232 | displayName: "The End", 233 | hidden: 0, 234 | description: "You finished the game.", 235 | icon: "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/1568400/06238508f23f31998858d8fddf6f73a6f8ae1e82.jpg", 236 | icongray: 237 | "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/1568400/ee9399e24a07f5c971ceed446172275bc15fce18.jpg", 238 | }, 239 | nodeath: { 240 | defaultvalue: 0, 241 | displayName: "SHEEPY STRONG", 242 | hidden: 0, 243 | description: "You finished the game without dying.", 244 | icon: "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/1568400/74753d1df9dbbc9b1f899934992b83a37683b0d3.jpg", 245 | icongray: 246 | "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/1568400/f8586a9fd0319a260b8a84c91727455872388b0f.jpg", 247 | }, 248 | dontgiveup: { 249 | defaultvalue: 0, 250 | displayName: "It's a hard one...", 251 | hidden: 0, 252 | description: "You can do it!", 253 | icon: "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/1568400/a3cb3f2456fc678f7c052f03091caea8e321a669.jpg", 254 | icongray: 255 | "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/1568400/629665d2f9ca555d00ce011becd8bd641bf4c61c.jpg", 256 | }, 257 | voice: { 258 | defaultvalue: 0, 259 | displayName: "Your destiny...", 260 | hidden: 0, 261 | description: "... is awaiting you.", 262 | icon: "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/1568400/81d66f545121ddf2c53b775c1011ad0e3d85d421.jpg", 263 | icongray: 264 | "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/1568400/3e9c3d39a927f4d8bf8ffc955f20fbd932493bbb.jpg", 265 | }, 266 | }; 267 | 268 | const cached_imgs = {}; 269 | const channel = new BroadcastChannel("achievements"); 270 | var achievement_status = {}; 271 | 272 | function load_achievements() { 273 | if (!localStorage.getItem("achievements")) { 274 | for (let achievement_name in achievements) { 275 | achievement_status[achievement_name] = false; 276 | } 277 | save_achievements(); 278 | } 279 | else { 280 | achievement_status = JSON.parse(localStorage.getItem("achievements")); 281 | } 282 | } 283 | 284 | function save_achievements() { 285 | localStorage.setItem("achievements", JSON.stringify(achievement_status)); 286 | } 287 | 288 | function activate_achievement(name) { 289 | if (achievement_status[name]) return; 290 | achievement_status[name] = true; 291 | let achievement = achievements[name]; 292 | save_achievements(); 293 | 294 | let notification = document.createElement("div"); 295 | notification.className = "notification"; 296 | 297 | let img = cached_imgs[name]; 298 | img.className = "notification_img"; 299 | let text_div = document.createElement("div"); 300 | text_div.className = "achievement_text"; 301 | let title_element = document.createElement("p"); 302 | title_element.className = "achievement_title"; 303 | let description_element = document.createElement("p"); 304 | description_element.className = "achievement_description"; 305 | 306 | title_element.innerText = "Achievement Unlocked!"; 307 | description_element.innerText = achievement.displayName; 308 | 309 | text_div.append(title_element); 310 | text_div.append(description_element); 311 | notification.append(img); 312 | notification.append(text_div); 313 | 314 | document.body.append(notification); 315 | setTimeout(() => { 316 | notification.remove(); 317 | }, 6000) 318 | 319 | //notify other tabs 320 | channel.postMessage(name); 321 | } 322 | 323 | function achievement_div(achievement) { 324 | let name = achievement.name; 325 | let div = document.createElement("div"); 326 | div.className = "achievement_div"; 327 | let img = document.createElement("img"); 328 | img.className = "achievement_img" 329 | 330 | if (achievement_status[name]) 331 | img.src = achievement.icon; 332 | else 333 | img.src = achievement.icongray; 334 | 335 | let text_div = document.createElement("div"); 336 | text_div.className = "achievement_text"; 337 | let title_element = document.createElement("p"); 338 | title_element.className = "achievement_title"; 339 | let description_element = document.createElement("p"); 340 | description_element.className = "achievement_description"; 341 | 342 | if (achievement.hidden && !achievement_status[name]) { 343 | title_element.innerText = "Hidden Achievement"; 344 | img.src = "hidden.png"; 345 | } 346 | else { 347 | title_element.innerText = achievement.displayName; 348 | description_element.innerText = achievement.description || ""; 349 | } 350 | 351 | text_div.append(title_element); 352 | text_div.append(description_element); 353 | div.append(img); 354 | div.append(text_div); 355 | 356 | return div; 357 | } 358 | 359 | function populate_grid(achievements_list, grid) { 360 | grid.innerHTML = ""; 361 | if (!achievements_list.length) { 362 | grid.style.display = "none"; 363 | return; 364 | } 365 | 366 | for (let achievement of achievements_list) { 367 | let div = achievement_div(achievement); 368 | grid.append(div); 369 | } 370 | grid.style.display = "grid"; 371 | } 372 | 373 | function populate_achievements() { 374 | //reorder achievements 375 | let unlocked = []; 376 | let locked = []; 377 | let hidden = []; 378 | 379 | for (let name in achievements) { 380 | let achievement = achievements[name]; 381 | achievement.name = name; 382 | 383 | if (achievement_status[name]) 384 | unlocked.push(achievement); 385 | else if (achievement.hidden) 386 | hidden.push(achievement); 387 | else 388 | locked.push(achievement); 389 | } 390 | locked = locked.concat(hidden); 391 | 392 | let unlocked_header = document.getElementById("unlocked_header"); 393 | let locked_header = document.getElementById("locked_header"); 394 | 395 | unlocked_header.style.display = unlocked.length ? "block" : "none"; 396 | locked_header.style.display = locked.length ? "block" : "none"; 397 | 398 | populate_grid(unlocked, document.getElementById("unlocked_grid")); 399 | populate_grid(locked, document.getElementById("locked_grid")); 400 | } 401 | 402 | function preload_images() { 403 | for (let [name, achievement] of Object.entries(achievements)) { 404 | let img = new Image(); 405 | img.src = achievement.icon; 406 | cached_imgs[name] = img; 407 | } 408 | } 409 | 410 | function index_load() { 411 | populate_achievements(); 412 | channel.onmessage = () => { 413 | load_achievements(); 414 | populate_achievements(); 415 | }; 416 | } 417 | 418 | //disable back/forward cache to reduce memory usage 419 | window.addEventListener("unload", function(){}); 420 | window.addEventListener("beforeunload", function(){}); 421 | 422 | load_achievements(); --------------------------------------------------------------------------------