diff --git a/README.md b/README.md index 6e902476..7e374175 100644 --- a/README.md +++ b/README.md @@ -2,28 +2,30 @@ shapez.io Logo -This is the source code for shapez.io, an open source base building game inspired by factorio. +This is the source code for shapez.io, an open source base building game inspired by Factorio. Your goal is to produce shapes by cutting, rotating, merging and painting parts of shapes. ## Playing -You can already play it on https://shapez.io +You can already play it [here](https://shapez.io). ## Building -- Make sure ffmpeg is on your path -- Install yarn and node 10 +- Make sure git `git lfs` extension is on your path +- Run `git lfs pull` to download sound assets +- Make sure `ffmpeg` is on your path +- Install Yarn and Node.js 10 - Run `yarn` in the root folder, then run `yarn` in the `gulp/` folder -- Cd into `gulp` and run `yarn gulp`: It should now open in your browser +- Cd into `gulp` and run `yarn gulp` - it should now open in your browser -**Notice**: This will give you a debug build with several debugging flags enabled. If you want to disable them, check `config.js` +**Notice**: This will produce a debug build with several debugging flags enabled. If you want to disable them, modify `config.js`. ## Contributing Since this game is in the more or less early development, I will only accept pull requests which add an immediate benefit. Please understand that low quality PR's might be closed by me with a short comment explaining why. -If you want to add a new feature or in generally contribute I recommend to get in touch with me on discord: +If you want to add a new feature or in generally contribute I recommend to get in touch with me on Discord: discord logo @@ -34,10 +36,10 @@ If you want to add a new feature or in generally contribute I recommend to get i The game is based on a custom engine which itself is based on the YORG.io 3 game egine (Actually it shares almost the same core). The code within the engine is relatively clean with some code for the actual game on top being hacky. -This project is based on ES5. Some es6 features are used but most of them are too slow, especially when polyfilled. For example, `.forEach` is only used within non-critical loops since its slower than a plain for loop. +This project is based on ES5. Some ES2015 features are used but most of them are too slow, especially when polyfilled. For example, `Array.prototype.forEach` is only used within non-critical loops since its slower than a plain for loop. ### Assets -For most assets I use photoshop, you can find them in `assets/`. +For most assets I use Adobe Photoshop, you can find them in `assets/`. -You will need a texture packer license in order to regenerate the atlas. If you don't have one but you want to contribute assets, let me know and I might compile it for you. +You will need a Texture Packer license in order to regenerate the atlas. If you don't have one but want to contribute assets, let me know and I might compile it for you. diff --git a/artwork/itch.io/screenshots/7.png b/artwork/itch.io/screenshots/7.png index bab1b536..0df86bcc 100644 Binary files a/artwork/itch.io/screenshots/7.png and b/artwork/itch.io/screenshots/7.png differ diff --git a/artwork/itch.io/screenshots/8.png b/artwork/itch.io/screenshots/8.png new file mode 100644 index 00000000..1b3bbdc2 Binary files /dev/null and b/artwork/itch.io/screenshots/8.png differ diff --git a/artwork/thirdparty/kongregate/iframe.html b/artwork/thirdparty/kongregate/iframe.html new file mode 100644 index 00000000..6b741420 --- /dev/null +++ b/artwork/thirdparty/kongregate/iframe.html @@ -0,0 +1,17 @@ + + + + Iframe test + + + + + diff --git a/artwork/thirdparty/kongregate/index.html b/artwork/thirdparty/kongregate/index.html index 03a38db1..ce19f275 100644 --- a/artwork/thirdparty/kongregate/index.html +++ b/artwork/thirdparty/kongregate/index.html @@ -2,11 +2,11 @@ Redirecting to shapez.io - + Redirecting you to - shapez.io + shapez.io diff --git a/gulp/gulpfile.js b/gulp/gulpfile.js index eebc102f..d87ba792 100644 --- a/gulp/gulpfile.js +++ b/gulp/gulpfile.js @@ -2,7 +2,7 @@ const nodeVersion = process.versions.node.split(".")[0]; if (nodeVersion !== "10") { - console.error("This cli requires exactly Node 10. You are using node " + nodeVersion); + console.error("This cli requires exactly Node.js 10. You are using Node.js " + nodeVersion); process.exit(1); } @@ -14,6 +14,23 @@ const path = require("path"); const deleteEmpty = require("delete-empty"); const execSync = require("child_process").execSync; +const lfsOutput = execSync("git lfs install", { encoding: "utf-8" }); +if (!lfsOutput.toLowerCase().includes("git lfs initialized")) { + console.error(` + Git LFS is not installed, unable to build. + + To install Git LFS on Linux: + - Arch: + sudo pacman -S git-lfs + - Debian/Ubuntu: + sudo apt install git-lfs + + For other systems, see: + https://github.com/git-lfs/git-lfs/wiki/Installation + `); + process.exit(1); +} + // Load other plugins dynamically const $ = require("gulp-load-plugins")({ scope: ["devDependencies"], diff --git a/gulp/standalone.js b/gulp/standalone.js index 00d2b685..bb02b320 100644 --- a/gulp/standalone.js +++ b/gulp/standalone.js @@ -142,6 +142,11 @@ function gulptasksStandalone($, gulp, buildFolder) { return; } + fs.writeFileSync( + path.join(appPath, "LICENSE"), + fs.readFileSync(path.join(__dirname, "..", "LICENSE")) + ); + const playablePath = appPath + "_playable"; fse.copySync(appPath, playablePath); fs.writeFileSync(path.join(playablePath, "steam_appid.txt"), "1134480"); @@ -174,8 +179,8 @@ function gulptasksStandalone($, gulp, buildFolder) { "standalone.package.prod", $.sequence("standalone.prepare", [ "standalone.package.prod.win64", - // "standalone.package.prod.win32", // "standalone.package.prod.linux64", + // "standalone.package.prod.win32", // "standalone.package.prod.linux32", // "standalone.package.prod.darwin64" ]) diff --git a/src/css/ingame_hud/dialogs.scss b/src/css/ingame_hud/dialogs.scss index d2315063..868dac58 100644 --- a/src/css/ingame_hud/dialogs.scss +++ b/src/css/ingame_hud/dialogs.scss @@ -117,6 +117,10 @@ overflow-y: auto; pointer-events: all; @include S(width, 350px); + + > strong { + font-weight: bold; + } } > .buttons { @@ -143,6 +147,32 @@ background-color: $colorRedBright; color: #fff; } + + &.timedButton { + pointer-events: none; + cursor: default; + position: relative; + overflow: hidden; + &::after { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: unset; + z-index: 5; + content: " "; + display: inline-block; + background: rgba(#fff, 0.6); + @include InlineAnimation(5s linear) { + 0% { + width: 100%; + } + 100% { + width: 0%; + } + } + } + } } } } diff --git a/src/css/states/changelog.scss b/src/css/states/changelog.scss index d4a948bf..69b7864c 100644 --- a/src/css/states/changelog.scss +++ b/src/css/states/changelog.scss @@ -19,6 +19,13 @@ .changes { @include SuperSmallText; @include S(padding-left, 20px); + strong { + background: $colorBlueBright; + color: #fff; + text-transform: uppercase; + @include S(padding, 1px, 2px); + @include S(margin-right, 3px); + } } } } diff --git a/src/css/states/preload.scss b/src/css/states/preload.scss index 2c8a302e..075a363f 100644 --- a/src/css/states/preload.scss +++ b/src/css/states/preload.scss @@ -7,11 +7,14 @@ } .changelogDialogEntry { + margin-top: 10px; width: 100%; flex-direction: column; text-align: left; padding: 10px; box-sizing: border-box; + background: #eef1f4; + .version { @include Heading; } @@ -25,7 +28,14 @@ .changes { @include SuperSmallText; - @include S(padding-left, 20px); + @include S(padding-left, 15px); + strong { + background: $colorBlueBright; + color: #fff; + text-transform: uppercase; + @include S(padding, 1px, 2px); + @include S(margin-right, 3px); + } } } diff --git a/src/js/application.js b/src/js/application.js index 58804ee5..27be4b1c 100644 --- a/src/js/application.js +++ b/src/js/application.js @@ -146,11 +146,7 @@ export class Application { } this.analytics = new GoogleAnalyticsImpl(this); - if (queryParamOptions.betaMode) { - this.gameAnalytics = new NoGameAnalytics(this); - } else { - this.gameAnalytics = new ShapezGameAnalytics(this); - } + this.gameAnalytics = new ShapezGameAnalytics(this); } /** @@ -232,6 +228,7 @@ export class Application { * @param {Event} event */ handleVisibilityChange(event) { + window.focus(); const pageVisible = !document[pageHiddenPropName]; if (pageVisible !== this.pageVisible) { this.pageVisible = pageVisible; @@ -271,6 +268,7 @@ export class Application { onAppRenderableStateChanged(renderable) { logger.log("Application renderable:", renderable); + window.focus(); if (!renderable) { this.stateMgr.getCurrentState().onAppPause(); } else { @@ -301,8 +299,7 @@ export class Application { logSection("BEFORE UNLOAD HANDLER", "#f77"); if (!G_IS_DEV && this.stateMgr.getCurrentState().getHasUnloadConfirmation()) { - if (G_IS_STANDALONE) { - } else { + if (!G_IS_STANDALONE) { // Need to show a "Are you sure you want to exit" event.preventDefault(); event.returnValue = "Are you sure you want to exit?"; @@ -314,6 +311,7 @@ export class Application { * Boots the application */ boot() { + console.log("Booting ..."); this.registerStates(); this.registerEventListeners(); @@ -330,6 +328,8 @@ export class Application { this.ticker.frameEmitted.add(this.onFrameEmitted, this); this.ticker.bgFrameEmitted.add(this.onBackgroundFrame, this); this.ticker.start(); + + window.focus(); } /** diff --git a/src/js/changelog.js b/src/js/changelog.js index 3b3bb229..44475e63 100644 --- a/src/js/changelog.js +++ b/src/js/changelog.js @@ -1,4 +1,20 @@ export const CHANGELOG = [ + { + version: "1.0.4", + date: "26.05.2020", + entries: [ + "Balancing Reduce cost of first painting upgrade, and change 'Shape Processing' to 'Cutting, Rotating & Stacking'", + "Tutorial Add dialog after completing level 2 to check out the upgrades tab.", + "Misc Allow changing the keybindings in the demo version", + ], + }, + { + version: "1.0.3", + date: "24.05.2020", + entries: [ + "Balancing Reduced the amount of shapes required for the first 5 levels to make it easier to get into the game.", + ], + }, { version: "1.0.2", date: "23.05.2020", diff --git a/src/js/core/click_detector.js b/src/js/core/click_detector.js index a4aece50..557c1f28 100644 --- a/src/js/core/click_detector.js +++ b/src/js/core/click_detector.js @@ -155,6 +155,7 @@ export class ClickDetector { * @param {Event} event */ internalPreventClick(event) { + window.focus(); event.preventDefault(); } @@ -301,6 +302,8 @@ export class ClickDetector { * @param {TouchEvent|MouseEvent} event */ internalOnPointerDown(event) { + window.focus(); + if (!this.internalEventPreHandler(event, 1)) { return false; } @@ -369,6 +372,8 @@ export class ClickDetector { * @param {TouchEvent|MouseEvent} event */ internalOnPointerEnd(event) { + window.focus(); + if (!this.internalEventPreHandler(event, 0)) { return false; } diff --git a/src/js/core/config.js b/src/js/core/config.js index 70f9ee3b..99770c4b 100644 --- a/src/js/core/config.js +++ b/src/js/core/config.js @@ -94,14 +94,15 @@ export const globalConfig = { // showChunkBorders: true, // rewardsInstant: true, // allBuildingsUnlocked: true, - upgradesNoCost: true, + // upgradesNoCost: true, // disableUnlockDialog: true, // disableLogicTicks: true, // testClipping: true, // framePausesBetweenTicks: 40, // testTranslations: true, // enableEntityInspector: true, - testAds: true, + // testAds: true, + // disableMapOverview: true, /* dev:end */ }, @@ -123,3 +124,8 @@ export const IS_MOBILE = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent); // Automatic calculations globalConfig.minerSpeedItemsPerSecond = globalConfig.beltSpeedItemsPerSecond / 5; + +if (globalConfig.debug.disableMapOverview) { + globalConfig.mapChunkOverviewMinZoom = 0; + globalConfig.mapChunkPrerenderMinZoom = 0; +} diff --git a/src/js/core/input_distributor.js b/src/js/core/input_distributor.js index 8886947b..e0152774 100644 --- a/src/js/core/input_distributor.js +++ b/src/js/core/input_distributor.js @@ -201,7 +201,7 @@ export class InputDistributor { // TAB event.keyCode === 9 || // F1 - F10 - (event.keyCode >= 112 && event.keyCode < 122 && !G_IS_DEV) + (event.keyCode >= 112 && event.keyCode < 122) ) { event.preventDefault(); } diff --git a/src/js/core/loader.js b/src/js/core/loader.js index 5c8e17ec..8888ecbf 100644 --- a/src/js/core/loader.js +++ b/src/js/core/loader.js @@ -84,7 +84,7 @@ class LoaderImpl { return Promise.race([ new Promise((resolve, reject) => { - setTimeout(reject, G_IS_DEV ? 3000 : 60000); + setTimeout(reject, G_IS_DEV ? 500 : 10000); }), new Promise(resolve => { diff --git a/src/js/core/query_parameters.js b/src/js/core/query_parameters.js index 7bc09b37..b3dab1b3 100644 --- a/src/js/core/query_parameters.js +++ b/src/js/core/query_parameters.js @@ -3,13 +3,8 @@ const options = queryString.parse(location.search); export let queryParamOptions = { embedProvider: null, - betaMode: null, }; if (options.embed) { queryParamOptions.embedProvider = options.embed; } - -if (!G_IS_RELEASE && options.betamode) { - queryParamOptions.betaMode = true; -} diff --git a/src/js/game/camera.js b/src/js/game/camera.js index c95970c1..7fb25b32 100644 --- a/src/js/game/camera.js +++ b/src/js/game/camera.js @@ -715,10 +715,6 @@ export class Camera extends BasicSerializableObject { if (G_IS_DEV && globalConfig.debug.disableZoomLimits) { return; } - if (queryParamOptions.betaMode) { - return; - } - const wrapper = this.root.app.platformWrapper; assert(Number.isFinite(this.zoomLevel), "Invalid zoom level *before* clamp: " + this.zoomLevel); diff --git a/src/js/game/hub_goals.js b/src/js/game/hub_goals.js index 6c155165..f1fc15c9 100644 --- a/src/js/game/hub_goals.js +++ b/src/js/game/hub_goals.js @@ -145,9 +145,6 @@ export class HubGoals extends BasicSerializableObject { if (G_IS_DEV && globalConfig.debug.allBuildingsUnlocked) { return true; } - if (queryParamOptions.betaMode) { - return true; - } return !!this.gainedRewards[reward]; } @@ -233,9 +230,6 @@ export class HubGoals extends BasicSerializableObject { if (G_IS_DEV && globalConfig.debug.upgradesNoCost) { return true; } - if (queryParamOptions.betaMode) { - return true; - } const tierData = handle.tiers[currentLevel]; @@ -282,8 +276,6 @@ export class HubGoals extends BasicSerializableObject { if (G_IS_DEV && globalConfig.debug.upgradesNoCost) { // Dont take resources - } else if (queryParamOptions.betaMode) { - // Same } else { for (let i = 0; i < tierData.required.length; ++i) { const requirement = tierData.required[i]; diff --git a/src/js/game/hud/parts/unlock_notification.js b/src/js/game/hud/parts/unlock_notification.js index 612d900d..570d04d9 100644 --- a/src/js/game/hud/parts/unlock_notification.js +++ b/src/js/game/hud/parts/unlock_notification.js @@ -8,6 +8,7 @@ import { enumHubGoalRewards } from "../../tutorial_goals"; import { BaseHUDPart } from "../base_hud_part"; import { DynamicDomAttach } from "../dynamic_dom_attach"; import { enumHubGoalRewardsToContentUnlocked } from "../../tutorial_goals_mappings"; +import { InputReceiver } from "../../../core/input_receiver"; export class HUDUnlockNotification extends BaseHUDPart { initialize() { @@ -25,6 +26,8 @@ export class HUDUnlockNotification extends BaseHUDPart { } createElements(parent) { + this.inputReciever = new InputReceiver("unlock-notification"); + this.element = makeDiv(parent, "ingame_HUD_UnlockNotification", []); const dialog = makeDiv(this.element, null, ["dialog"]); @@ -47,6 +50,7 @@ export class HUDUnlockNotification extends BaseHUDPart { * @param {enumHubGoalRewards} reward */ showForLevel(level, reward) { + this.root.app.inputMgr.makeSureAttachedAndOnTop(this.inputReciever); this.elemTitle.innerText = T.ingame.levelCompleteNotification.levelTitle.replace( "", ("" + level).padStart(2, "0") @@ -92,6 +96,7 @@ export class HUDUnlockNotification extends BaseHUDPart { } cleanup() { + this.root.app.inputMgr.makeSureDetached(this.inputReciever); if (this.buttonShowTimeout) { clearTimeout(this.buttonShowTimeout); this.buttonShowTimeout = null; @@ -101,10 +106,19 @@ export class HUDUnlockNotification extends BaseHUDPart { requestClose() { this.root.app.adProvider.showVideoAd().then(() => { this.close(); + if (this.root.hubGoals.level === 3) { + const { showUpgrades } = this.root.hud.parts.dialogs.showInfo( + T.dialogs.upgradesIntroduction.title, + T.dialogs.upgradesIntroduction.desc, + ["showUpgrades:good:timeout"] + ); + showUpgrades.add(() => this.root.hud.parts.shop.show()); + } }); } close() { + this.root.app.inputMgr.makeSureDetached(this.inputReciever); if (this.buttonShowTimeout) { clearTimeout(this.buttonShowTimeout); this.buttonShowTimeout = null; diff --git a/src/js/game/tutorial_goals.js b/src/js/game/tutorial_goals.js index 83aca89c..b2f5b0ef 100644 --- a/src/js/game/tutorial_goals.js +++ b/src/js/game/tutorial_goals.js @@ -30,20 +30,23 @@ export const enumHubGoalRewards = { }; export const tutorialGoals = [ + // 1 // Circle { shape: "CuCuCuCu", // belts t1 - required: 60, + required: 35, reward: enumHubGoalRewards.reward_cutter_and_trash, }, + // 2 // Cutter { shape: "----CuCu", // - required: 80, + required: 50, reward: enumHubGoalRewards.no_reward, }, + // 3 // Rectangle { shape: "RuRuRuRu", // miners t1 @@ -51,12 +54,14 @@ export const tutorialGoals = [ reward: enumHubGoalRewards.reward_splitter, }, + // 4 { shape: "RuRu----", // processors t2 - required: 350, + required: 150, reward: enumHubGoalRewards.reward_rotater, }, + // 5 // Rotater { shape: "Cu----Cu", // belts t2 @@ -64,25 +69,29 @@ export const tutorialGoals = [ reward: enumHubGoalRewards.reward_tunnel, }, + // 6 { shape: "Cu------", // miners t2 - required: 1000, + required: 700, reward: enumHubGoalRewards.reward_painter, }, + // 7 // Painter { shape: "CrCrCrCr", // unused - required: 1500, + required: 1300, reward: enumHubGoalRewards.reward_rotater_ccw, }, + // 8 { shape: "RbRb----", // painter t2 required: 2500, reward: enumHubGoalRewards.reward_mixer, }, + // 9 // Mixing (purple) { shape: "CpCpCpCp", // belts t3 @@ -90,6 +99,7 @@ export const tutorialGoals = [ reward: enumHubGoalRewards.reward_splitter_compact, }, + // 10 // Star shape + cyan { shape: "ScScScSc", // miners t3 @@ -97,6 +107,7 @@ export const tutorialGoals = [ reward: enumHubGoalRewards.reward_stacker, }, + // 11 // Stacker { shape: "CgScScCg", // processors t3 @@ -104,36 +115,42 @@ export const tutorialGoals = [ reward: enumHubGoalRewards.reward_miner_chainable, }, + // 12 { shape: "RpRpRpRp:CwCwCwCw", // painting t3 required: 7000, reward: enumHubGoalRewards.reward_underground_belt_tier_2, }, + // 13 { shape: "SrSrSrSr:CyCyCyCy", // unused required: 7850, reward: enumHubGoalRewards.reward_storage, }, + // 14 { shape: "SrSrSrSr:CyCyCyCy:SwSwSwSw", // belts t4 (two variants) required: 8000, reward: enumHubGoalRewards.reward_cutter_quad, }, + // 15 { shape: "CbRbRbCb:CwCwCwCw:WbWbWbWb", // miner t4 (two variants) required: 9000, reward: enumHubGoalRewards.reward_painter_double, }, + // 16 { shape: "WrRgWrRg:CwCrCwCr:SgSgSgSg", // processors t4 (two varinats) required: 10000, reward: enumHubGoalRewards.reward_painter_quad, }, + // 17 { shape: finalGameShape, required: 50000, diff --git a/src/js/game/upgrades.js b/src/js/game/upgrades.js index 4260f7f9..8864c5e2 100644 --- a/src/js/game/upgrades.js +++ b/src/js/game/upgrades.js @@ -97,7 +97,7 @@ export const UPGRADES = { painting: { tiers: [ { - required: [{ shape: "WrWrWrWr", amount: 2000 }], + required: [{ shape: "WrWrWrWr", amount: 500 }], improvement: 1, }, { diff --git a/src/js/globals.d.ts b/src/js/globals.d.ts index 7e84d86a..977e8ff3 100644 --- a/src/js/globals.d.ts +++ b/src/js/globals.d.ts @@ -107,8 +107,6 @@ declare interface Window { assert(condition: boolean, failureMessage: string); coreThreadLoadedCb(); - - gameanalytics: typeof import("./game_analytics"); } declare interface Navigator { diff --git a/src/js/platform/browser/game_analytics.js b/src/js/platform/browser/game_analytics.js index 2efc458f..be1a8940 100644 --- a/src/js/platform/browser/game_analytics.js +++ b/src/js/platform/browser/game_analytics.js @@ -137,7 +137,8 @@ export class ShapezGameAnalytics extends GameAnalyticsInterface { ingameTime: root.time.now(), category, value, - gameDump: this.generateGameDump(root), + version: G_BUILD_VERSION, + gameDump: this.generateGameDump(root, category === "sync"), }); } @@ -152,14 +153,15 @@ export class ShapezGameAnalytics extends GameAnalyticsInterface { /** * Generates a game dump * @param {GameRoot} root + * @param {boolean=} metaOnly */ - generateGameDump(root) { + generateGameDump(root, metaOnly = false) { let staticEntities = []; const entities = root.entityMgr.getAllWithComponent(StaticMapEntityComponent); // Limit the entities - if (entities.length < 5000) { + if (!metaOnly && entities.length < 500) { for (let i = 0; i < entities.length; ++i) { const entity = entities[i]; const staticComp = entity.components.StaticMapEntity; diff --git a/src/js/platform/browser/sound.js b/src/js/platform/browser/sound.js index 32985bb5..508dcf8c 100644 --- a/src/js/platform/browser/sound.js +++ b/src/js/platform/browser/sound.js @@ -23,7 +23,7 @@ class SoundSpritesContainer { } return (this.loadingPromise = Promise.race([ new Promise((resolve, reject) => { - setTimeout(reject, G_IS_DEV ? 5000 : 60000); + setTimeout(reject, G_IS_DEV ? 500 : 5000); }), new Promise(resolve => { this.howl = new Howl({ @@ -100,7 +100,7 @@ class MusicInstance extends MusicInstanceInterface { load() { return Promise.race([ new Promise((resolve, reject) => { - setTimeout(reject, G_IS_DEV ? 5000 : 60000); + setTimeout(reject, G_IS_DEV ? 500 : 5000); }), new Promise((resolve, reject) => { this.howl = new Howl({ diff --git a/src/js/platform/browser/storage_indexed_db.js b/src/js/platform/browser/storage_indexed_db.js index 3d29c246..0c1dbe4f 100644 --- a/src/js/platform/browser/storage_indexed_db.js +++ b/src/js/platform/browser/storage_indexed_db.js @@ -34,10 +34,12 @@ export class StorageImplBrowserIndexedDB extends StorageInterface { reject("Indexed DB access error"); }; + // @ts-ignore request.onsuccess = event => resolve(event.target.result); request.onupgradeneeded = /** @type {IDBVersionChangeEvent} */ event => { /** @type {IDBDatabase} */ + // @ts-ignore const database = event.target.result; const objectStore = database.createObjectStore("files", { diff --git a/src/js/platform/browser/wrapper.js b/src/js/platform/browser/wrapper.js index 32c0e5f3..fa3f80b3 100644 --- a/src/js/platform/browser/wrapper.js +++ b/src/js/platform/browser/wrapper.js @@ -1,5 +1,5 @@ import { Math_min } from "../../core/builtins"; -import { globalConfig, IS_MOBILE } from "../../core/config"; +import { globalConfig, IS_MOBILE, IS_DEBUG, IS_DEMO } from "../../core/config"; import { createLogger } from "../../core/logging"; import { queryParamOptions } from "../../core/query_parameters"; import { clamp } from "../../core/utils"; @@ -19,6 +19,8 @@ export class PlatformWrapperImplBrowser extends PlatformWrapperInterface { iframed: false, externalLinks: true, iogLink: true, + unlimitedSavegames: IS_DEMO ? false : true, + showDemoBadge: IS_DEMO, }; if (!G_IS_STANDALONE && queryParamOptions.embedProvider) { @@ -35,6 +37,8 @@ export class PlatformWrapperImplBrowser extends PlatformWrapperInterface { case "iogames.space": { this.embedProvider.id = "iogames.space"; this.embedProvider.iogLink = true; + this.embedProvider.unlimitedSavegames = true; + this.embedProvider.showDemoBadge = false; break; } @@ -71,6 +75,14 @@ export class PlatformWrapperImplBrowser extends PlatformWrapperInterface { return super.initialize().then(() => this.initializeAdProvider()); } + getHasUnlimitedSavegames() { + return this.embedProvider.unlimitedSavegames; + } + + getShowDemoBadges() { + return this.embedProvider.showDemoBadge; + } + onSentryLoaded() { logger.log("Initializing sentry"); window.Sentry.init({ diff --git a/src/js/platform/wrapper.js b/src/js/platform/wrapper.js index a7b5c807..5754a8a2 100644 --- a/src/js/platform/wrapper.js +++ b/src/js/platform/wrapper.js @@ -29,6 +29,17 @@ export class PlatformWrapperInterface { return false; } + /** + * Whether the user has unlimited savegames + */ + getHasUnlimitedSavegames() { + return true; + } + + getShowDemoBadges() { + return false; + } + /** * Returns the strength of touch pans with the mouse */ diff --git a/src/js/savegame/savegame_interface.js b/src/js/savegame/savegame_interface.js index aeec062f..8671471d 100644 --- a/src/js/savegame/savegame_interface.js +++ b/src/js/savegame/savegame_interface.js @@ -26,7 +26,6 @@ export class BaseSavegameInterface { */ getSchemaUncached() { throw new Error("Implement get schema"); - return {}; } getValidator() { diff --git a/src/js/states/keybindings.js b/src/js/states/keybindings.js index aa6ec2d6..0f7fcf9e 100644 --- a/src/js/states/keybindings.js +++ b/src/js/states/keybindings.js @@ -82,10 +82,10 @@ export class KeybindingsState extends TextualGameState { } editKeybinding(id) { - if (IS_DEMO) { - this.dialogs.showFeatureRestrictionInfo(T.demo.features.customizeKeybindings); - return; - } + // if (IS_DEMO) { + // this.dialogs.showFeatureRestrictionInfo(T.demo.features.customizeKeybindings); + // return; + // } const dialog = new Dialog({ app: this.app, diff --git a/src/js/states/main_menu.js b/src/js/states/main_menu.js index 1c9d5010..9dd18909 100644 --- a/src/js/states/main_menu.js +++ b/src/js/states/main_menu.js @@ -54,7 +54,11 @@ export class MainMenuState extends GameState { @@ -93,7 +97,7 @@ export class MainMenuState extends GameState { G_IS_BROWSER && this.app.platformWrapper instanceof PlatformWrapperImplBrowser && this.app.platformWrapper.embedProvider.iogLink - ? `More .io games` + ? `.io games` : "" } @@ -104,7 +108,11 @@ export class MainMenuState extends GameState { } requestImportSavegame() { - if (IS_DEMO && this.app.savegameMgr.getSavegamesMetaData().length > 0) { + if ( + IS_DEMO && + this.app.savegameMgr.getSavegamesMetaData().length > 0 && + !this.app.platformWrapper.getHasUnlimitedSavegames() + ) { this.app.analytics.trackUiClick("importgame_slot_limit_show"); this.dialogs.showWarning(T.dialogs.oneSavegameLimit.title, T.dialogs.oneSavegameLimit.desc); return; @@ -122,6 +130,7 @@ export class MainMenuState extends GameState { const closeLoader = this.dialogs.showLoadingDialog(); const reader = new FileReader(); reader.addEventListener("load", event => { + // @ts-ignore const contents = event.target.result; let realContent; @@ -372,7 +381,11 @@ export class MainMenuState extends GameState { } onPlayButtonClicked() { - if (IS_DEMO && this.app.savegameMgr.getSavegamesMetaData().length > 0) { + if ( + IS_DEMO && + this.app.savegameMgr.getSavegamesMetaData().length > 0 && + !this.app.platformWrapper.getHasUnlimitedSavegames() + ) { this.app.analytics.trackUiClick("startgame_slot_limit_show"); this.dialogs.showWarning(T.dialogs.oneSavegameLimit.title, T.dialogs.oneSavegameLimit.desc); return; diff --git a/src/js/states/preload.js b/src/js/states/preload.js index c12972e0..43c84eb0 100644 --- a/src/js/states/preload.js +++ b/src/js/states/preload.js @@ -186,7 +186,8 @@ export class PreloadState extends GameState { return G_BUILD_VERSION; }) .then(version => { - this.app.storage.writeFileAsync("lastversion.bin", version); + logger.log("Last version:", version, "App version:", G_BUILD_VERSION); + this.app.storage.writeFileAsync("lastversion.bin", G_BUILD_VERSION); return version; }) .then(version => { diff --git a/translations/base-en.yaml b/translations/base-en.yaml index d3fe6aca..2318c342 100644 --- a/translations/base-en.yaml +++ b/translations/base-en.yaml @@ -89,6 +89,7 @@ dialogs: getStandalone: Get Standalone deleteGame: I know what I do viewUpdate: View Update + showUpgrades: Show Upgrades importSavegameError: title: Import Error @@ -165,6 +166,12 @@ dialogs: desc: >- Whenever you need help or are stuck, check out the 'Show hint' button in the lower left and I'll give my best to help you! + upgradesIntroduction: + title: Unlock Upgrades + desc: >- + All shapes you produce can be used to unlock upgrades - Don't destroy your old factories! + The upgrades tab can be found on the top right corner of the screen. + ingame: # This is shown in the top left corner and displays useful keybindings in # every situation @@ -277,7 +284,7 @@ shopUpgrades: name: Extraction description: Speed +% processors: - name: Shape Processing + name: Cutting, Rotating & Stacking description: Speed +% painting: name: Mixing & Painting @@ -507,7 +514,7 @@ keybindings: resetKeybindings: Reset Keyinbindings categoryLabels: - general: Appplication + general: Application ingame: Game placement: Placement massSelect: Mass Delete diff --git a/translations/base-fr.yaml b/translations/base-fr.yaml new file mode 100644 index 00000000..a425d848 --- /dev/null +++ b/translations/base-fr.yaml @@ -0,0 +1,582 @@ +# +# GAME TRANSLATIONS +# +# Contributing: +# +# If you want to contribute, please make a pull request on this respository +# and I will have a look. +# +# Placeholders: +# +# Do *not* replace placeholders! Placeholders have a special syntax like +# `Hotkey: `. They are encapsulated within angle brackets. The correct +# translation for this one in German for example would be: `Taste: ` (notice +# how the placeholder stayed '' and was not replaced!) +# +# Adding a new language: +# +# If you want to add a new language, ask me in the discord and I will setup +# the basic structure so the game also detects it. +# + +global: + loading: Chargement + error: Erreur + + # How big numbers are rendered, e.g. "10,000" + thousandsDivider: "." + + # Shown for infinitely big numbers + infinite: inf + + time: + # Used for formatting past time dates + oneSecondAgo: il y a une seconde + xSecondsAgo: il y a secondes + oneMinuteAgo: il y a une minute + xMinutesAgo: il y a minutes + oneHourAgo: il y a une heure + xHoursAgo: il y a heures + oneDayAgo: il y a un jour + xDaysAgo: il y a jours + + # Short formats for times, e.g. '5h 23m' + secondsShort: s + minutesAndSecondsShort: m s + hoursAndMinutesShort: h s + + xMinutes: minutes + + keys: + tab: TAB + control: CTRL + alt: ALT + escape: ESC + shift: SHIFT + space: ESPACE + +demoBanners: + # This is the "advertisement" shown in the main menu and other various places + title: Salut! + intro: >- + Si vous appreciez ce jeu, merci de penser à acheter la version complète! + advantages: + - Pas de publicité + - Sauvegardes illimitées + - Mode sombre & plus + - >- + Donnez-moi l'opportunité de développer shapez.io ❤️ + +mainMenu: + play: Jouer + changelog: Historique + importSavegame: Importer + openSourceHint: Ce jeu est open source! + discordLink: Serveur Discord officiel + + # This is shown when using firefox and other browsers which are not supported. + browserWarning: >- + Désolé, mais ce jeu est connu pour tourner lentement sur votre browser! Procurez-vous la version autonome ou téléchargez Chrome pour une meilleure expérience. + +dialogs: + buttons: + ok: OK + delete: Effacer + cancel: Annuler + later: Plus tard + restart: Relancer + reset: Réinitialiser + getStandalone: Se procurer la version autonome + deleteGame: Je sais ce que je fais + viewUpdate: Voir les mises-à-jour + showUpgrades: Montrer les améliorations + + importSavegameError: + title: Erreur d'importation + text: >- + Impossible d'importer votre sauvegarde: + + importSavegameSuccess: + title: Sauvegarde importée + text: >- + Votre sauvegarde a été importée avec succès. + + gameLoadFailure: + title: Le jeu est cassé + text: >- + Impossible de charger votre sauvegarde: + + confirmSavegameDelete: + title: Confirmez la suppression + text: >- + Etes-vous certains de vouloir supprimer votre partie? + + savegameDeletionError: + title: Impossible de supprimer + text: >- + Impossible de supprimer votre sauvegarde: + + restartRequired: + title: Redémarrage requis + text: >- + Vous devez relancer le jeu pour appliquer les modifications. + + editKeybinding: + title: Changer les contrôles + desc: Appuyez sur la touche que vous voulez assigner, ou Escape pour annuler. + + resetKeybindingsConfirmation: + title: Réinitialiser les contrôles + desc: Ceci réinitialisera les touches par défaut. Veuillez confirmer. + + keybindingsResetOk: + title: Réinitialisation des contrôles + desc: Les contrôles ont été réinitialisés par leur état par défaut respectifs! + + featureRestriction: + title: Version Démo + desc: Vous avez essayé d'accéder à la fonction () qui n'est pas disponible dans la démo. Considérez l'achat de la version complète pour une expérience optimale! + + saveNotPossibleInDemo: + desc: Votre partie a été sauvegardée, mais la charger n'est possible que dans la version complète. Considérez l'achat de la version complète pour une expérience optimale! + + leaveNotPossibleInDemo: + title: Version Démo + desc: Votre partie a été sauvée mais nous ne pourrez pas la charger dans la démo. Charger les parties n'est disponible que dans la version complète. Etes-vous certain? + + newUpdate: + title: Mise-à-jour disponible + desc: Une mise-à-jour est disponible pour ce jeu! + + demoExplanation: + title: Note du développeur + desc: Je développe ce jeu pendant mon temps libre, et j'espère que vous l'appréciez! Si c'est le cas, merci de considérez l'achat de la version complète! + + oneSavegameLimit: + title: Sauvegardes limitées + desc: Vous ne pouvez avoir qu'une seule sauvegarde en même temps dans la version démo. Merci de soit effacer l'actuelle ou de vous procurer la version complète! + + updateSummary: + title: Nouvel mise-à-jour! + desc: >- + Voici les modifications depuis votre dernière session: + + hintDescription: + title: Tutorial + desc: >- + Si vous avez besoin d'aide ou êtes coincé, vérifiez le bouton 'Aide' dans le coin inférieur gauche et j'essayerai de vous aider au mieux! + + upgradesIntroduction: + title: Débloquer les améliorations + desc: >- + Toutes les formes que vous produisez peuvent être utilisées pour débloquer des améliorations - Ne détruisez pas vos anciennes usines! + L'onglet des améliorations se trouve dans le coin supérieur droit de l'écran. + +ingame: + # This is shown in the top left corner and displays useful keybindings in + # every situation + keybindingsOverlay: + centerMap: Centrer + moveMap: Déplacer + removeBuildings: Effacer + stopPlacement: Arrêter le placement + rotateBuilding: Tourner le bâtiment + placeMultiple: Placement multiple + reverseOrientation: Changer l'orientation + disableAutoOrientation: Désactiver l'orientation automatique + toggleHud: Basculet l'ATH + placeBuilding: Placer un bâtiment + + # Everything related to placing buildings (I.e. as soon as you selected a building + # from the toolbar) + buildingPlacement: + # Buildings can have different variants which are unlocked at later levels, + # and this is the hint shown when there are multiple variants available. + cycleBuildingVariants: Appuyez sur pour changer de variante. + + # Shows the hotkey in the ui, e.g. "Hotkey: Q" + hotkeyLabel: >- + Raccourci: + + infoTexts: + speed: Vitesse + range: Portée + storage: Espace de stockage + oneItemPerSecond: 1 forme / seconde + itemsPerSecond: formes / s + itemsPerSecondDouble: (x2) + + tiles: cases + + # The notification when completing a level + levelCompleteNotification: + # is replaced by the actual level, so this gets 'Level 03' for example. + levelTitle: Niveau + completed: Terminé + unlockText: débloqué! + buttonNextLevel: Niveau suivant + + # Notifications on the lower right + notifications: + newUpgrade: Une nouvelle amélioration est disponible! + gameSaved: Votre partie a été sauvegardée. + + # Mass delete information, this is when you hold CTRL and then drag with your mouse + # to select multiple buildings to delete + massDelete: + infoText: Appuyez sur pour effacer les bâtiments sélectionnés et pour annuler. + + # The "Upgrades" window + shop: + title: Améliorations + buttonUnlock: Améliorer + + # Gets replaced to e.g. "Tier IX" + tier: Echelon + + # The roman number for each tier + tierLabels: [I, II, III, IV, V, VI, VII, VIII, IX, X] + + maximumLevel: Niveau maximum + + # The "Statistics" window + statistics: + title: Statistiques + dataSources: + stored: + title: Stocké + description: Affiche le nombre de formes stockée dans votre bâtiment central. + produced: + title: Produit + description: Affiche tous les formes que votre usine entière produit, en incluant les formes intermédiaires. + delivered: + title: Délivré + description: Affiche les formes qui ont étés délivrées dans votres bâtiment central. + noShapesProduced: Aucune forme n'a été produite jusqu'à présent. + + # Displays the shapes per minute, e.g. '523 / m' + shapesPerMinute: / m + + # Settings menu, when you press "ESC" + settingsMenu: + playtime: Temps de jeu + + buildingsPlaced: Bâtiments + beltsPlaced: Convoyeurs + + buttons: + continue: Continuer + settings: Options + menu: Retourner au menu + + # Bottom left tutorial hints + tutorialHints: + title: Besoin d'aide? + showHint: Indice + hideHint: Fermer + +# All shop upgrades +shopUpgrades: + belt: + name: Convoyeurs, Distributeurs et Tunnels + description: Vitesse +% + miner: + name: Extraction + description: Vitesse +% + processors: + name: Découpage, Rotation et Empilage + description: Vitesse +% + painting: + name: Mélange et Peinture + description: Vitesse +% + +# Buildings and their name / description +buildings: + belt: + default: + name: &belt Convoyeurs + description: Transporte les objects, maintenez et fites glisser pour en placer plusieurs. + + miner: # Internal name for the Extractor + default: + name: &miner Extracteurs + description: Placez-le au dessus d'une forme ou couleur pour l'extraire. + + chainable: + name: Extracteur en série + description: Placez-le au dessus d'une forme ou couleur pour l'extraire. Peut être mis en série. + + underground_belt: # Internal name for the Tunnel + default: + name: &underground_belt Tunnel + description: Permet de faire passer des ressources en dessous de bâtiment et convoyeurs. + + tier2: + name: Tunnel Echelon II + description: Permet de faire passer des ressources en dessous de bâtiment et convoyeurs. + + splitter: # Internal name for the Balancer + default: + name: &splitter Balancier + description: Multifonctionnel - Distribue de manière égale toutes les entrées vers toutes les sorties. + + compact: + name: Fusionneur (compact) + description: Fusionne deux convoyeurs en un. + + compact-inverse: + name: Fusionneur (compact) + description: Fusionne deux convoyeurs en un. + + cutter: + default: + name: &cutter Découpeur + description: Coupe une forme de haut en bas et sort les deux parties. Si vous n'utilisez qu'une seule partie, assurez-vous de détruite l'autre ou cela coincera! + quad: + name: Découpeur (Quatre) + description: Coupe une forme en 4 parts. Si vous n'utilisez qu'une seule partie, assurez-vous de détruite les autres ou cela coincera! + + rotater: + default: + name: &rotater Pivoteur + description: Fait pivoter une forme de 90 degrés vers la droite. + ccw: + name: Pivoteur inversé + description: Fait pivoter une forme de 90 degrés vers la gauche. + + stacker: + default: + name: &stacker Combineur + description: Combine deux formes. Si elles ne peuvent pas êtres combinées, la forme de droite est placée sur la forme de gauche. + + mixer: + default: + name: &mixer Mixeur de couleur + description: Mixe deux couleurs en utilisant le mélange additif. + + painter: + default: + name: &painter Peintre + description: Colorie la forme entière de gauche avec la couleur de droite. + double: + name: Peintre (Double) + description: Colorie les deux formes de gauche avec la couleur de droite. + quad: + name: Peintre (Quatre) + description: Permet de colorier chaque quadrant d'une forme avec une couleur différente. + + trash: + default: + name: &trash Poubelle + description: Accepte des formes de n'importe quel côté et le détruit... pour toujours. + + storage: + name: Stockage + description: Stocke les formes en trop jusqu'à une certaine capacité. Peut être utilisé comme tampon. + +storyRewards: + # Those are the rewards gained from completing the store + reward_cutter_and_trash: + title: Découper des formes + desc: Vous venez de débloquer le découpeur - il coupe des formes en deux de haut en bas regardless of its orientation!

Be sure to get rid of the waste, or otherwise it will stall - A cet effet, je vous donne la poubelle, qui détruit tout ce que vous y mettez! + + reward_rotater: + title: Rotation + desc: Le pivoteur a été débloqué! Il pivote les formes de 90 degrés vers la droite. + + reward_painter: + title: Peintre + desc: >- + Le peintre a été débloqué - Extrayez des pigments de couleur (comme vous le faites avec les formes) et combinez les avec une forme dans un peintre pour les colorier!

PS: Si vous êtes daltonien, je travaille déjà sur une solution! + + reward_mixer: + title: Mixeur de couleurs + desc: Le mixeur a été débloqué - Combinez deux couleurs en utilisant le mélange additif avec ce bâtiment! + + reward_stacker: + title: Combineur + desc: Vous pouvez maintenant combiner deux formes avec le combineur! Les deux entrées sont combinée et si elles peuvent êtres mises l'une à cpôté de l'autre, elles sont fusionnées. Sinon, la forme de droite est placée au dessus de la forme de gauche. + + reward_splitter: + title: Découpeur/Fusionneur + desc: Le balancier multifonctionnel a été débloqué - Il peut être utilisé pour construire de plus grandes usines en découpant et fusionnant les formes sur plusieurs convoyeurs!

+ + reward_tunnel: + title: Tunnel + desc: Le tunnel a été débloqué - Vous pouvez maintenant faire passer des formes vous les convoyeurs et les bâtimentts avec ça! + + reward_rotater_ccw: + title: Pivoteur inversé + desc: Vous avez débloqué une variante du pivoteur - Elle permet de faire pivoter vers la gauche! Pour le construite, sélectionnez le pivoteur et appuyez sur 'T' pour changer sa variante! + + reward_miner_chainable: + title: Extracteur en série + desc: Vous avez débloqué l'extracteur en série! Il permet de transférer ses resources à d'autres extracteurs pour extraire les ressources plus efficacement! + + reward_underground_belt_tier_2: + title: Tunnel échelon II + desc: Vous avez débloqué une nouvelle variante du tunnel - Elle a une portée plus grande, et vous pouvez également mixer ces tunnels maintenant! + + reward_splitter_compact: + title: Balancier compacte + desc: >- + Vous avez débloqué une variante compacte du balancier - Elle accepte deux entrées et les rassemble en une sortie! + + reward_cutter_quad: + title: Quadruple découpeur + desc: Vous avez débloqué une variante du découpeur - Elle permet de de découper les formes en quatres parties à la place de simplement deux! + + reward_painter_double: + title: Double peintre + desc: Vous avez débloqué une variante du peintre - Elle fonctionne comme le peintre de base, mais elle permet de traiter deux formes à la fois en ne consommant qu'une couleur au lieu de deux! + + reward_painter_quad: + title: Quadruple peintre + desc: Vous avez débloqué une variante du peintre - Elle permet de colorier chaque partie d'une forme individuellement! + + reward_storage: + title: Tampon de stockage + desc: Vous avez débloqué une variante de la poubelle - Elle permet de stocker des formes jusqu'à une certaine limite! + + reward_freeplay: + title: Mode libre + desc: Vous l'avez fait! Vous avez débloqué le mode libre! Cela veut dire que dorénavant, les formes sont générées aléatoirement! (Ne vous en faites pas, plus de contenu est prévu pour la version complète!) + + # Special reward, which is shown when there is no reward actually + no_reward: + title: Niveau suivant + desc: >- + Ce niveau n'a pas de récompense, mais le prochain oui!

PS: Vous ne devriez pas détruires votre usine actuelle - Vous aurez besoin de toutes ces formes plus tard pour débloquer les améliorations! + + no_reward_freeplay: + title: Niveau suivant + desc: >- + Bravo! D'ailleurs, plus de contenu est prévu pour la version complète! + +settings: + title: Options + categories: + game: Jeu + app: Application + + versionBadges: + dev: Developpement + staging: Test + prod: Production + buildDate: Créé le + + labels: + uiScale: + title: Taille de l'interface + description: >- + Change la taille de l'interface utilisateur. Cette interface se redimensionnera suivant la résolution de votre appareil, mais cette option contrôle le facteur de résolution. + + fullscreen: + title: Plein écran + description: >- + Il est recommandé de jouer au jeu en plein écran pour obtenir la meilleur expérience possible. Seulement disponible dans la version complète. + + soundsMuted: + title: Sons désactivés + description: >- + Si coché, tous les sons seront désactivés. + + musicMuted: + title: Musique désactivée + description: >- + Si coché, toute la musique sera désactivée. + + theme: + title: Thème + description: >- + Choisissez votre thème (clair / sombre). + + refreshRate: + title: Simulation Target + description: >- + Si vous avez un moniteur à 144hz, changez le taux de rafraichissement ici pour que le jeu fonctionne correctement à cette haute fréquence. Ceci pourrait diminuer vos IPS si votre ordinateur est trop lent. + + alwaysMultiplace: + title: Placement multiple + description: >- + Si activé, tous les bâtiments resterons sélectionnés tant que vous n'avez pas annulé. Ceci revient à garder la touche SHIFT appuyée en permanence. + + offerHints: + title: Indices + description: >- + ffiche ou non le bouton 'Afficher un indice' dans le coin inférieir gauche. + +keybindings: + title: Contrôles + hint: >- + Astuce: Soyez sûr d'utiliser CTRL, SHIFT et ALT! Ces touches activent différentes options de placement. + + resetKeybindings: Réinitialiser les contrôles + + categoryLabels: + general: Application + ingame: Jeu + placement: Placement + massSelect: Suppression de masse + buildings: Raccourcis bâtiment + placementModifiers: Modificateurs de placement + + mappings: + confirm: Confirmer + back: Retour + mapMoveUp: Aller en haut + mapMoveRight: Aller à droite + mapMoveDown: Aller en bas + mapMoveLeft: Aller à gauche + + centerMap: Centrer la carte + + mapZoomIn: Zoom avant + mapZoomOut: Zoom arrière + + menuOpenShop: Améliorations + menuOpenStats: Statistiques + + toggleHud: Basculer l'ATH + toggleFPSInfo: Basculer IPS et informations débogage + belt: *belt + splitter: *splitter + underground_belt: *underground_belt + miner: *miner + cutter: *cutter + rotater: *rotater + stacker: *stacker + mixer: *mixer + painter: *painter + trash: *trash + + abortBuildingPlacement: Annuler le placement + rotateWhilePlacing: Pivoter + cycleBuildingVariants: Faire défiler les variantes + confirmMassDelete: Confirmer la suppression de masse + cycleBuildings: Faire défiler les bâtiments + + massSelectStart: Cliquez et maintenez pour commencer + massSelectSelectMultiple: Séléctionner plusieurs zones + + placementDisableAutoOrientation: Désactiver l'orientation automatique + placeMultiple: Rester en mode placement + placeInverse: Inverser le mode d'orientation automatique + +about: + title: A propos de ce jeu + +changelog: + title: Historique + +demo: + features: + restoringGames: Charger des sauvegardes + importingGames: Importer des sauvegardes + oneGameLimit: Limité à une sauvegarde + customizeKeybindings: Personalisation des contrôles + + settingNotAvailable: Indisponible dans la démo. + +# +# French translation version v0.1 based on english v1.0.4 by Didier WEERTS 'The Corsaire' \ No newline at end of file diff --git a/version b/version index e6d5cb83..a6a3a43c 100644 --- a/version +++ b/version @@ -1 +1 @@ -1.0.2 \ No newline at end of file +1.0.4 \ No newline at end of file