mirror of
https://github.com/tobspr/shapez.io.git
synced 2025-06-13 13:04:03 +00:00
Merge 2fa40fb3a6
into a1c6a99df0
This commit is contained in:
commit
a1c62cc069
1
.gitignore
vendored
1
.gitignore
vendored
@ -48,6 +48,7 @@ gulp/runnable-texturepacker.jar
|
||||
tmp_standalone_files
|
||||
tmp_standalone_files_china
|
||||
tmp_standalone_files_wegame
|
||||
contributors.json
|
||||
|
||||
# Local config
|
||||
config.local.js
|
||||
|
180
gulp/contributors.js
Normal file
180
gulp/contributors.js
Normal file
@ -0,0 +1,180 @@
|
||||
const fs = require("fs");
|
||||
const fsp = require("fs/promises");
|
||||
const path = require("path");
|
||||
const nodeFetch = require("node-fetch");
|
||||
|
||||
const APILink = "https://api.github.com/repos/tobspr/shapez.io";
|
||||
const numOfReqPerPage = 100; // Max is 100, change to something lower if loads are too long
|
||||
const personalAccessToken = "PUT TOKEN HERE";
|
||||
|
||||
const JSONFileLocation = path.join(__dirname, "..", "contributors.json");
|
||||
|
||||
function fetch(url) {
|
||||
return nodeFetch(url, {
|
||||
headers: [["Authorization", "token " + personalAccessToken]],
|
||||
});
|
||||
}
|
||||
|
||||
function JSONFileExists() {
|
||||
return fs.existsSync(JSONFileLocation);
|
||||
}
|
||||
|
||||
function readJSONFile() {
|
||||
return fsp.readFile(JSONFileLocation, { encoding: "utf-8" }).then(res => JSON.parse(res));
|
||||
}
|
||||
|
||||
function writeJSONFile(translators, contributors) {
|
||||
return fsp.writeFile(
|
||||
JSONFileLocation,
|
||||
JSON.stringify({
|
||||
lastUpdatedAt: Date.now(),
|
||||
//@ts-ignore
|
||||
translators: Array.from(translators, ([username, value]) => ({ username, value })),
|
||||
//@ts-ignore
|
||||
contributors: Array.from(contributors, ([username, value]) => ({ username, value })),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function getTotalNumOfPRs() {
|
||||
return fetch(APILink + "/pulls?state=closed&per_page=1&page=0").then(res => {
|
||||
// This part is very messy
|
||||
let link = res.headers.get("Link");
|
||||
link = link.slice(link.indexOf(",") + 1); // Gets rid of the first "next" link
|
||||
return parseInt(link.slice(link.indexOf("&page=") + 6, link.indexOf(">")));
|
||||
});
|
||||
}
|
||||
|
||||
function shouldDownloadPRs() {
|
||||
if (!JSONFileExists()) return Promise.resolve(true);
|
||||
else {
|
||||
return readJSONFile().then(res => Date.now() - res.lastUpdatedAt > 1000 * 60 * 30); // once every 30 min
|
||||
}
|
||||
}
|
||||
|
||||
function PRIsTranslation(link) {
|
||||
// We just say that if a PR only edits translation files, its a translation, all others are something else
|
||||
return fetch(link + "/files")
|
||||
.then(res => res.json())
|
||||
.then(res => {
|
||||
if (res.message) {
|
||||
console.log("GITHUB HAS RATE-LIMITED THIS MACHINE, PLEASE WAIT ABOUT AN HOUR");
|
||||
throw new Error("rate-limit reached");
|
||||
}
|
||||
|
||||
for (let file of res) {
|
||||
if (!file.filename.startsWith("translations/")) return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
async function sortPRs(prs) {
|
||||
const contributors = new Map();
|
||||
const translators = new Map();
|
||||
|
||||
const clearLine = () => {
|
||||
process.stdout.moveCursor(0, -1); // up one line
|
||||
process.stdout.clearLine(1);
|
||||
};
|
||||
|
||||
for (let i = 0; i < prs.length; i++) {
|
||||
let map;
|
||||
|
||||
if (await PRIsTranslation(prs[i].url)) map = translators;
|
||||
else map = contributors;
|
||||
|
||||
if (!map.has(prs[i].username)) map.set(prs[i].username, []);
|
||||
|
||||
map.get(prs[i].username).push(prs[i]);
|
||||
|
||||
if (i !== 0) clearLine();
|
||||
console.log(`PR's Downloaded: ${i} out of ${prs.length} (${prs.length - i} left)`);
|
||||
}
|
||||
|
||||
clearLine();
|
||||
console.log("Downloaded All PR's");
|
||||
|
||||
return {
|
||||
contributors,
|
||||
translators,
|
||||
};
|
||||
}
|
||||
|
||||
function reqPage(page) {
|
||||
return fetch(APILink + `/pulls?state=closed&per_page=${numOfReqPerPage}&page=${page}`)
|
||||
.then(res => res.json())
|
||||
.then(async res => {
|
||||
const prs = [];
|
||||
|
||||
for (let i = 0; i < res.length; i++) {
|
||||
if (!res[i].merged_at) {
|
||||
continue;
|
||||
} // Skip files that were never merged
|
||||
|
||||
const prInfo = {
|
||||
username: res[i].user.login,
|
||||
html_url: res[i].html_url,
|
||||
url: res[i].url,
|
||||
title: res[i].title,
|
||||
};
|
||||
|
||||
prs.push(prInfo);
|
||||
// if (await PRIsTranslation(res[i].url)) {
|
||||
// translations.push(prInfo);
|
||||
// } else {
|
||||
// others.push(prInfo);
|
||||
// }
|
||||
}
|
||||
|
||||
return prs;
|
||||
});
|
||||
}
|
||||
|
||||
async function downloadAllPrs() {
|
||||
const totalNumOfPrs = await getTotalNumOfPRs();
|
||||
const numOfPageReqs = Math.ceil(totalNumOfPrs / numOfReqPerPage);
|
||||
|
||||
const prs = [];
|
||||
|
||||
for (let i = 1; i < numOfPageReqs + 1; i++) {
|
||||
prs.push(...(await reqPage(i))); // Yes promise.all would be good, but I wanna keep them in order (at least for now)
|
||||
}
|
||||
|
||||
return prs;
|
||||
}
|
||||
|
||||
async function tryToUpdateContributors() {
|
||||
if (personalAccessToken === "PUT TOKEN HERE") {
|
||||
console.log("A github token was not provided, writing default contributors.json");
|
||||
await writeJSONFile([], []);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(await shouldDownloadPRs())) {
|
||||
console.log("Not updating contributors to prevent github API from rate-limiting this computer");
|
||||
console.log("If you wish to force a contributors update, use contributors.build.force");
|
||||
return;
|
||||
}
|
||||
|
||||
await updateContributors();
|
||||
}
|
||||
|
||||
async function updateContributors() {
|
||||
const allPrs = await downloadAllPrs();
|
||||
console.log(`Discovered ${allPrs.length} PRs`);
|
||||
|
||||
const sorted = await sortPRs(allPrs);
|
||||
|
||||
await writeJSONFile(sorted.translators, sorted.contributors);
|
||||
console.log("Wrote JSON File");
|
||||
}
|
||||
|
||||
function gulpTaskContributors($, gulp) {
|
||||
gulp.task("contributors.build", cb => tryToUpdateContributors().then(() => cb));
|
||||
gulp.task("contributors.build.force", cb => updateContributors().then(() => cb));
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
gulpTaskContributors,
|
||||
};
|
@ -74,6 +74,8 @@ releaseUploader.gulptasksReleaseUploader($, gulp, buildFolder);
|
||||
const translations = require("./translations");
|
||||
translations.gulptasksTranslations($, gulp, buildFolder);
|
||||
|
||||
const contributors = require("./contributors");
|
||||
contributors.gulpTaskContributors($, gulp, buildFolder);
|
||||
///////////////////// BUILD TASKS /////////////////////
|
||||
|
||||
// Cleans up everything
|
||||
|
@ -15,7 +15,7 @@
|
||||
"@babel/preset-env": "^7.5.4",
|
||||
"@types/cordova": "^0.0.34",
|
||||
"@types/filesystem": "^0.0.29",
|
||||
"@types/node": "^12.7.5",
|
||||
"@types/node": "^15.12.4",
|
||||
"ajv": "^6.10.2",
|
||||
"audiosprite": "^0.7.2",
|
||||
"babel-core": "^6.26.3",
|
||||
@ -41,6 +41,7 @@
|
||||
"ignore-loader": "^0.1.2",
|
||||
"lz-string": "^1.4.4",
|
||||
"markdown-loader": "^5.1.0",
|
||||
"node-fetch": "^2.6.1",
|
||||
"node-sri": "^1.1.1",
|
||||
"phonegap-plugin-mobile-accessibility": "^1.0.5",
|
||||
"postcss": ">=5.0.0",
|
||||
|
@ -994,11 +994,16 @@
|
||||
resolved "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz"
|
||||
integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==
|
||||
|
||||
"@types/node@*", "@types/node@^12.7.5":
|
||||
"@types/node@*":
|
||||
version "12.7.5"
|
||||
resolved "https://registry.npmjs.org/@types/node/-/node-12.7.5.tgz"
|
||||
integrity sha512-9fq4jZVhPNW8r+UYKnxF1e2HkDWOWKM5bC2/7c9wPV835I0aOrVbS/Hw/pWPk2uKrNXQqg9Z959Kz+IYDd5p3w==
|
||||
|
||||
"@types/node@^15.12.4":
|
||||
version "15.12.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-15.12.4.tgz#e1cf817d70a1e118e81922c4ff6683ce9d422e26"
|
||||
integrity sha512-zrNj1+yqYF4WskCMOHwN+w9iuD12+dGm0rQ35HLl9/Ouuq52cEtd0CH9qMgrdNmi5ejC1/V7vKEXYubB+65DkA==
|
||||
|
||||
"@types/q@^1.5.1":
|
||||
version "1.5.2"
|
||||
resolved "https://registry.npmjs.org/@types/q/-/q-1.5.2.tgz"
|
||||
@ -8318,6 +8323,11 @@ no-case@^2.2.0:
|
||||
dependencies:
|
||||
lower-case "^1.1.1"
|
||||
|
||||
node-fetch@^2.6.1:
|
||||
version "2.6.1"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
|
||||
integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
|
||||
|
||||
node-libs-browser@^2.2.1:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz"
|
||||
|
@ -30,6 +30,7 @@
|
||||
@import "states/mobile_warning";
|
||||
@import "states/changelog";
|
||||
@import "states/puzzle_menu";
|
||||
@import "states/credits.scss";
|
||||
|
||||
@import "ingame_hud/buildings_toolbar";
|
||||
@import "ingame_hud/building_placer";
|
||||
|
35
src/css/states/credits.scss
Normal file
35
src/css/states/credits.scss
Normal file
@ -0,0 +1,35 @@
|
||||
#state_CreditsState {
|
||||
.container .content {
|
||||
text-align: center;
|
||||
|
||||
.tobspr .title {
|
||||
}
|
||||
|
||||
.section {
|
||||
@include S(margin-top, 10px);
|
||||
@include S(padding, 5px);
|
||||
background-color: #f8f8f8;
|
||||
@include DarkThemeOverride {
|
||||
background: rgba(0, 10, 20, 0.1);
|
||||
}
|
||||
|
||||
.title {
|
||||
@include Heading;
|
||||
|
||||
color: #555;
|
||||
@include DarkThemeOverride {
|
||||
color: #eee;
|
||||
}
|
||||
}
|
||||
|
||||
.people {
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
transition: max-height 0.5s ease-out;
|
||||
}
|
||||
.people > :first-child {
|
||||
@include S(margin-top, 8px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -34,6 +34,7 @@ import { RestrictionManager } from "./core/restriction_manager";
|
||||
import { PuzzleMenuState } from "./states/puzzle_menu";
|
||||
import { ClientAPI } from "./platform/api";
|
||||
import { LoginState } from "./states/login";
|
||||
import { CreditsState } from "./states/credits";
|
||||
|
||||
/**
|
||||
* @typedef {import("./platform/achievement_provider").AchievementProviderInterface} AchievementProviderInterface
|
||||
@ -165,6 +166,7 @@ export class Application {
|
||||
ChangelogState,
|
||||
PuzzleMenuState,
|
||||
LoginState,
|
||||
CreditsState,
|
||||
];
|
||||
|
||||
for (let i = 0; i < states.length; ++i) {
|
||||
|
@ -35,6 +35,14 @@ export class AboutState extends TextualGameState {
|
||||
{ preventClick: true }
|
||||
);
|
||||
});
|
||||
|
||||
const stateChangers = this.htmlElement.querySelectorAll("a[state]");
|
||||
console.log(stateChangers);
|
||||
stateChangers.forEach(element => {
|
||||
this.trackClicks(element, () => this.moveToState(element.getAttribute("state")), {
|
||||
preventClick: true,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getDefaultPreviousState() {
|
||||
|
112
src/js/states/credits.js
Normal file
112
src/js/states/credits.js
Normal file
@ -0,0 +1,112 @@
|
||||
import { TextualGameState } from "../core/textual_game_state";
|
||||
import { contributors, translators } from "../../../contributors.json";
|
||||
import { T } from "../translations";
|
||||
|
||||
export class CreditsState extends TextualGameState {
|
||||
constructor() {
|
||||
super("CreditsState");
|
||||
}
|
||||
|
||||
getStateHeaderTitle() {
|
||||
return T.credits.title;
|
||||
}
|
||||
|
||||
getMainContentHTML() {
|
||||
return `
|
||||
<div class="section tobspr">
|
||||
<button class="title">${T.credits.tobspr}</button>
|
||||
<div class="people">
|
||||
<div class="entry">${this.linkify("https://github.com/tobspr", "Tobias Springer")}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="special-shout-out section">
|
||||
<button class="title">${T.credits.specialThanks.title}:</button>
|
||||
<div class="people">
|
||||
<div class="entry">${this.linkify(
|
||||
"https://soundcloud.com/pettersumelius",
|
||||
"Peppsen"
|
||||
)} - ${T.credits.specialThanks.descriptions.peppsen}</div>
|
||||
<div class="entry">Add some other people here (Whoever you think deserves it)</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="translators section">
|
||||
<button class="title">${T.credits.translators.title}:</button>
|
||||
<div class="people">
|
||||
${this.getGithubHTML(translators)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="contributors section">
|
||||
<button class="title">${T.credits.contributors.title}:</button>
|
||||
<div class="people">
|
||||
${this.getGithubHTML(contributors)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
linkify(href, text) {
|
||||
return `<a href="${href}" target="_blank">${text}</a>`;
|
||||
}
|
||||
|
||||
getGithubHTML(list) {
|
||||
let html = "";
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
html += `
|
||||
${i === 0 ? "" : "<br>"}
|
||||
<div class="entry">
|
||||
${this.linkify(`https://github.com/${list[i].username}`, list[i].username)}: <br> ${list[
|
||||
i
|
||||
].value
|
||||
.map(pr => {
|
||||
return `${this.linkify(pr.html_url, this.getGoodTitle(pr.title))}, `;
|
||||
})
|
||||
.reduce((p, c) => p + c)
|
||||
.slice(0, -2)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
getGoodTitle(title) {
|
||||
if (title.endsWith(".")) return title.slice(0, -1);
|
||||
|
||||
return title;
|
||||
}
|
||||
|
||||
onEnter() {
|
||||
// Allow the user to close any section by clicking on the title
|
||||
const buttons = this.htmlElement.querySelectorAll("button.title");
|
||||
buttons.forEach(button => {
|
||||
/** @type {HTMLElement} */
|
||||
//@ts-ignore
|
||||
const people = button.nextElementSibling;
|
||||
|
||||
button.addEventListener("click", e => {
|
||||
if (people.style.maxHeight) {
|
||||
people.style.maxHeight = null;
|
||||
} else {
|
||||
people.style.maxHeight = people.scrollHeight + "px";
|
||||
}
|
||||
});
|
||||
|
||||
// Set them to open at the start
|
||||
people.style.maxHeight = people.scrollHeight + "px";
|
||||
});
|
||||
|
||||
// Link stuff
|
||||
const links = this.htmlElement.querySelectorAll("a[href]");
|
||||
links.forEach(link => {
|
||||
this.trackClicks(
|
||||
link,
|
||||
() => this.app.platformWrapper.openExternalLink(link.getAttribute("href")),
|
||||
{ preventClick: true }
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
getDefaultPreviousState() {
|
||||
return "AboutState";
|
||||
}
|
||||
}
|
@ -1368,6 +1368,8 @@ about:
|
||||
body: >-
|
||||
This game is open source and developed by <a href="https://github.com/tobspr" target="_blank">Tobias Springer</a> (this is me).<br><br>
|
||||
|
||||
Have a look at the <a state="CreditsState">Credits</a> to see every who has helped out by translating or contributing.<br><br>
|
||||
|
||||
If you want to contribute, check out <a href="<githublink>" target="_blank">shapez.io on GitHub</a>.<br><br>
|
||||
|
||||
This game wouldn't have been possible without the great Discord community around my games - You should really join the <a href="<discordlink>" target="_blank">Discord server</a>!<br><br>
|
||||
@ -1379,6 +1381,18 @@ about:
|
||||
changelog:
|
||||
title: Changelog
|
||||
|
||||
credits:
|
||||
title: Credits
|
||||
tobspr: Main Programer and Artist
|
||||
specialThanks:
|
||||
title: Special Thanks To
|
||||
descriptions:
|
||||
peppsen: Created the awesome soundtrack
|
||||
translators:
|
||||
title: Translators
|
||||
contributors:
|
||||
title: Contributors
|
||||
|
||||
demo:
|
||||
features:
|
||||
restoringGames: Restoring savegames
|
||||
|
Loading…
Reference in New Issue
Block a user