Updated to include latest changes
BIN
res/ui/interactive_tutorial.cn.noinline/1_1_extractor.gif
Normal file
|
After Width: | Height: | Size: 120 KiB |
BIN
res/ui/interactive_tutorial.cn.noinline/1_2_conveyor.gif
Normal file
|
After Width: | Height: | Size: 297 KiB |
BIN
res/ui/interactive_tutorial.cn.noinline/1_3_expand.gif
Normal file
|
After Width: | Height: | Size: 993 KiB |
|
After Width: | Height: | Size: 809 KiB |
BIN
res/ui/interactive_tutorial.cn.noinline/21_2_switch_to_wires.gif
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
res/ui/interactive_tutorial.cn.noinline/21_3_place_button.gif
Normal file
|
After Width: | Height: | Size: 531 KiB |
BIN
res/ui/interactive_tutorial.cn.noinline/21_4_press_button.gif
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
res/ui/interactive_tutorial.cn.noinline/2_1_place_cutter.gif
Normal file
|
After Width: | Height: | Size: 502 KiB |
BIN
res/ui/interactive_tutorial.cn.noinline/2_2_place_trash.gif
Normal file
|
After Width: | Height: | Size: 575 KiB |
BIN
res/ui/interactive_tutorial.cn.noinline/2_3_more_cutters.gif
Normal file
|
After Width: | Height: | Size: 776 KiB |
BIN
res/ui/interactive_tutorial.cn.noinline/3_1_rectangles.gif
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
@ -137,16 +137,20 @@
|
||||
|
||||
button.continue {
|
||||
background: #555;
|
||||
@include S(margin-right, 10px);
|
||||
}
|
||||
|
||||
button.menu {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
button.nextPuzzle {
|
||||
background-color: $colorGreenBright;
|
||||
}
|
||||
|
||||
> button {
|
||||
@include S(min-width, 100px);
|
||||
@include S(padding, 10px, 20px);
|
||||
@include S(padding, 8px, 16px);
|
||||
@include S(margin, 0, 6px);
|
||||
@include IncreasedClickArea(0px);
|
||||
}
|
||||
}
|
||||
|
||||
41
src/css/ingame_hud/puzzle_next.scss
Normal file
@ -0,0 +1,41 @@
|
||||
#ingame_HUD_PuzzleNextPuzzle {
|
||||
position: absolute;
|
||||
@include S(top, 17px);
|
||||
@include S(right, 10px);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
backdrop-filter: blur(D(1px));
|
||||
padding: D(3px);
|
||||
|
||||
> .button {
|
||||
@include ButtonText;
|
||||
@include IncreasedClickArea(0px);
|
||||
pointer-events: all;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
color: #333438;
|
||||
transition: all 0.12s ease-in-out;
|
||||
text-transform: uppercase;
|
||||
transition-property: opacity, transform;
|
||||
@include PlainText;
|
||||
@include S(padding-right, 25px);
|
||||
opacity: 1;
|
||||
|
||||
@include DarkThemeInvert;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.9 !important;
|
||||
}
|
||||
|
||||
&.pressed {
|
||||
transform: scale(0.95) !important;
|
||||
}
|
||||
|
||||
& {
|
||||
/* @load-async */
|
||||
background: uiResource("icons/state_next_button.png") right center / D(15px) no-repeat;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -65,7 +65,7 @@
|
||||
@import "ingame_hud/puzzle_play_settings";
|
||||
@import "ingame_hud/puzzle_play_metadata";
|
||||
@import "ingame_hud/puzzle_complete_notification";
|
||||
@import "ingame_hud/puzzle_import_export";
|
||||
@import "ingame_hud/puzzle_next";
|
||||
|
||||
// prettier-ignore
|
||||
$elements:
|
||||
@ -84,8 +84,8 @@ ingame_HUD_PinnedShapes,
|
||||
ingame_HUD_GameMenu,
|
||||
ingame_HUD_KeybindingOverlay,
|
||||
ingame_HUD_PuzzleBackToMenu,
|
||||
ingame_HUD_PuzzleNextPuzzle,
|
||||
ingame_HUD_PuzzleEditorReview,
|
||||
ingame_HUD_PuzzleImportExport,
|
||||
ingame_HUD_PuzzleEditorControls,
|
||||
ingame_HUD_PuzzleEditorTitle,
|
||||
ingame_HUD_PuzzleEditorSettings,
|
||||
@ -135,11 +135,9 @@ body.uiHidden {
|
||||
#ingame_HUD_PlacementHints,
|
||||
#ingame_HUD_GameMenu,
|
||||
#ingame_HUD_PinnedShapes,
|
||||
#ingame_HUD_PuzzleEditorSettings,
|
||||
#ingame_HUD_PuzzlePlaySettings,
|
||||
#ingame_HUD_PuzzleEditorControls,
|
||||
#ingame_HUD_PuzzleImportExport,
|
||||
#ingame_HUD_PuzzlePlayMetadata,
|
||||
#ingame_HUD_PuzzleBackToMenu,
|
||||
#ingame_HUD_PuzzleNextPuzzle,
|
||||
#ingame_HUD_PuzzleEditorReview,
|
||||
#ingame_HUD_Notifications,
|
||||
#ingame_HUD_TutorialHints,
|
||||
#ingame_HUD_Waypoints,
|
||||
|
||||
@ -571,7 +571,7 @@
|
||||
box-sizing: border-box;
|
||||
@include S(grid-gap, 4px);
|
||||
|
||||
&.china {
|
||||
&.noLinks {
|
||||
grid-template-columns: auto 1fr;
|
||||
}
|
||||
|
||||
@ -580,6 +580,7 @@
|
||||
display: grid;
|
||||
justify-content: center;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
text-align: center;
|
||||
|
||||
> .disclaimer {
|
||||
grid-column: 2 / 3;
|
||||
|
||||
@ -19,8 +19,73 @@
|
||||
}
|
||||
|
||||
> .container {
|
||||
.searchForm {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
color: #333;
|
||||
background: $accentColorBright;
|
||||
@include S(padding, 5px);
|
||||
@include S(border-radius, $globalBorderRadius);
|
||||
flex-wrap: wrap;
|
||||
|
||||
input.search {
|
||||
color: #333;
|
||||
margin: 0;
|
||||
display: inline-block;
|
||||
flex-grow: 1;
|
||||
@include S(padding, 5px, 10px);
|
||||
@include S(min-width, 50px);
|
||||
|
||||
&::placeholder {
|
||||
color: #aaa;
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
color: #333;
|
||||
border: 0;
|
||||
@include S(padding, 5px);
|
||||
@include S(border-radius, $globalBorderRadius);
|
||||
@include S(padding, 7px, 10px);
|
||||
@include S(margin-left, 5px);
|
||||
@include PlainText;
|
||||
}
|
||||
|
||||
.filterCompleted {
|
||||
@include S(margin-left, 20px);
|
||||
pointer-events: all;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-transform: uppercase;
|
||||
@include PlainText;
|
||||
@include S(margin-right, 10px);
|
||||
|
||||
input {
|
||||
@include S(width, 15px);
|
||||
@include S(height, 15px);
|
||||
@include S(margin-right, 5px);
|
||||
@include S(border-radius, $globalBorderRadius);
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
|
||||
button[type="submit"] {
|
||||
@include S(padding, 7px, 10px, 5px);
|
||||
@include S(margin-left, 20px);
|
||||
@include S(margin-top, 4px);
|
||||
@include S(margin-bottom, 4px);
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
> .mainContent {
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
> .categoryChooser {
|
||||
> .categories {
|
||||
@ -83,8 +148,8 @@
|
||||
@include S(grid-gap, 7px);
|
||||
@include S(margin-top, 10px);
|
||||
@include S(padding-right, 4px);
|
||||
@include S(height, 320px);
|
||||
overflow-y: scroll;
|
||||
flex-grow: 1;
|
||||
pointer-events: all;
|
||||
position: relative;
|
||||
|
||||
|
||||
@ -50,7 +50,8 @@
|
||||
}
|
||||
|
||||
button.categoryButton,
|
||||
button.about {
|
||||
button.about,
|
||||
button.privacy {
|
||||
background-color: $colorCategoryButton;
|
||||
color: #777a7f;
|
||||
|
||||
@ -68,6 +69,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
button.privacy {
|
||||
@include S(margin-top, 4px);
|
||||
}
|
||||
|
||||
.versionbar {
|
||||
@include S(margin-top, 10px);
|
||||
|
||||
@ -180,7 +185,8 @@
|
||||
.container .content {
|
||||
.sidebar {
|
||||
button.categoryButton,
|
||||
button.about {
|
||||
button.about,
|
||||
button.privacy {
|
||||
color: #ccc;
|
||||
background-color: darken($darkModeControlsBackground, 5);
|
||||
|
||||
|
||||
@ -1,13 +1,17 @@
|
||||
export const CHANGELOG = [
|
||||
// Not finished yet
|
||||
{
|
||||
version: "1.4.3",
|
||||
date: "preview",
|
||||
entries: [
|
||||
"You can now hold 'ALT' while hovering a building to see its output! (Thanks to Sense101)",
|
||||
"The map overview should now be much more performant! As a consequence, you can now zoom out farther! (Thanks to PFedak)",
|
||||
"Puzzle DLC: There is now a 'next puzzle' button!",
|
||||
"Puzzle DLC: There is now a search function!",
|
||||
"Edit signal dialog now has the previous signal filled (Thanks to EmeraldBlock)",
|
||||
"Further performance improvements (Thanks to PFedak)",
|
||||
"Improved puzzle validation (Thanks to Sense101)",
|
||||
"Input fields in dialogs should now automatically focus",
|
||||
"Fix selected building being deselected at level up (Thanks to EmeraldBlock)",
|
||||
"Updated translations",
|
||||
],
|
||||
},
|
||||
|
||||
@ -167,4 +167,25 @@ export class BufferMaintainer {
|
||||
});
|
||||
return canvas;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} param0
|
||||
* @param {string} param0.key
|
||||
* @param {string} param0.subKey
|
||||
* @returns {HTMLCanvasElement?}
|
||||
*
|
||||
*/
|
||||
getForKeyOrNullNoUpdate({ key, subKey }) {
|
||||
let parent = this.cache.get(key);
|
||||
if (!parent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Now search for sub key
|
||||
const cacheHit = parent.get(subKey);
|
||||
if (cacheHit) {
|
||||
return cacheHit.canvas;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,6 +17,8 @@ export const THIRDPARTY_URLS = {
|
||||
reddit: "https://www.reddit.com/r/shapezio",
|
||||
shapeViewer: "https://viewer.shapez.io",
|
||||
|
||||
privacyPolicy: "https://tobspr.io/privacy.html",
|
||||
|
||||
standaloneStorePage: "https://store.steampowered.com/app/1318690/shapezio/",
|
||||
stanaloneCampaignLink: "https://get.shapez.io",
|
||||
puzzleDlcStorePage: "https://store.steampowered.com/app/1625400/shapezio__Puzzle_DLC",
|
||||
@ -55,6 +57,7 @@ export const globalConfig = {
|
||||
|
||||
// Map
|
||||
mapChunkSize: 16,
|
||||
chunkAggregateSize: 4,
|
||||
mapChunkOverviewMinZoom: 0.9,
|
||||
mapChunkWorldSize: null, // COMPUTED
|
||||
|
||||
|
||||
@ -123,6 +123,4 @@ function catchErrors(message, source, lineno, colno, error) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!G_IS_DEV) {
|
||||
window.onerror = catchErrors;
|
||||
}
|
||||
window.onerror = catchErrors;
|
||||
|
||||
@ -89,6 +89,11 @@ export class RestrictionManager extends ReadWriteProxy {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (queryParamOptions.embedProvider === "gamedistribution") {
|
||||
// also full version on gamedistribution
|
||||
return false;
|
||||
}
|
||||
|
||||
if (G_IS_DEV) {
|
||||
return typeof window !== "undefined" && window.location.search.indexOf("demo") >= 0;
|
||||
}
|
||||
|
||||
@ -734,6 +734,10 @@ const romanLiteralsCache = ["0"];
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getRomanNumber(number) {
|
||||
if (G_WEGAME_VERSION) {
|
||||
return String(number);
|
||||
}
|
||||
|
||||
number = Math.max(0, Math.round(number));
|
||||
if (romanLiteralsCache[number]) {
|
||||
return romanLiteralsCache[number];
|
||||
|
||||
@ -11,6 +11,7 @@ import { typeItemSingleton } from "../item_resolver";
|
||||
* pos: Vector,
|
||||
* direction: enumDirection,
|
||||
* item: BaseItem,
|
||||
* lastItem: BaseItem,
|
||||
* progress: number?,
|
||||
* cachedDestSlot?: import("./item_acceptor").ItemAcceptorLocatedSlot,
|
||||
* cachedBeltPath?: BeltPath,
|
||||
@ -51,6 +52,7 @@ export class ItemEjectorComponent extends Component {
|
||||
clear() {
|
||||
for (const slot of this.slots) {
|
||||
slot.item = null;
|
||||
slot.lastItem = null;
|
||||
slot.progress = 0;
|
||||
}
|
||||
}
|
||||
@ -67,6 +69,7 @@ export class ItemEjectorComponent extends Component {
|
||||
pos: slot.pos,
|
||||
direction: slot.direction,
|
||||
item: null,
|
||||
lastItem: null,
|
||||
progress: 0,
|
||||
cachedDestSlot: null,
|
||||
cachedTargetEntity: null,
|
||||
@ -131,6 +134,7 @@ export class ItemEjectorComponent extends Component {
|
||||
return false;
|
||||
}
|
||||
this.slots[slotIndex].item = item;
|
||||
this.slots[slotIndex].lastItem = item;
|
||||
this.slots[slotIndex].progress = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -119,7 +119,7 @@ export class GameMode extends BasicSerializableObject {
|
||||
|
||||
/** @returns {number} */
|
||||
getMinimumZoom() {
|
||||
return 0.1;
|
||||
return 0.06;
|
||||
}
|
||||
|
||||
/** @returns {number} */
|
||||
|
||||
@ -16,6 +16,7 @@ import { HUDEntityDebugger } from "./parts/entity_debugger";
|
||||
import { HUDModalDialogs } from "./parts/modal_dialogs";
|
||||
import { enumNotificationType } from "./parts/notifications";
|
||||
import { HUDSettingsMenu } from "./parts/settings_menu";
|
||||
import { HUDShapeTooltip } from "./parts/shape_tooltip";
|
||||
import { HUDVignetteOverlay } from "./parts/vignette_overlay";
|
||||
import { TrailerMaker } from "./trailer_maker";
|
||||
|
||||
@ -49,6 +50,8 @@ export class GameHUD {
|
||||
blueprintPlacer: new HUDBlueprintPlacer(this.root),
|
||||
buildingPlacer: new HUDBuildingPlacer(this.root),
|
||||
|
||||
shapeTooltip: new HUDShapeTooltip(this.root),
|
||||
|
||||
// Must always exist
|
||||
settingsMenu: new HUDSettingsMenu(this.root),
|
||||
debugInfo: new HUDDebugInfo(this.root),
|
||||
@ -189,6 +192,7 @@ export class GameHUD {
|
||||
"colorBlindHelper",
|
||||
"changesDebugger",
|
||||
"minerHighlight",
|
||||
"shapeTooltip",
|
||||
];
|
||||
|
||||
for (let i = 0; i < partsOrder.length; ++i) {
|
||||
|
||||
25
src/js/game/hud/parts/HUDPuzzleNextPuzzle.js
Normal file
@ -0,0 +1,25 @@
|
||||
import { makeDiv } from "../../../core/utils";
|
||||
import { T } from "../../../translations";
|
||||
import { PuzzlePlayGameMode } from "../../modes/puzzle_play";
|
||||
import { BaseHUDPart } from "../base_hud_part";
|
||||
|
||||
export class HUDPuzzleNextPuzzle extends BaseHUDPart {
|
||||
createElements(parent) {
|
||||
this.element = makeDiv(parent, "ingame_HUD_PuzzleNextPuzzle");
|
||||
this.button = document.createElement("button");
|
||||
this.button.classList.add("button");
|
||||
this.button.innerText = T.ingame.puzzleCompletion.nextPuzzle;
|
||||
this.element.appendChild(this.button);
|
||||
|
||||
this.trackClicks(this.button, this.nextPuzzle);
|
||||
}
|
||||
|
||||
initialize() {}
|
||||
|
||||
nextPuzzle() {
|
||||
const gameMode = /** @type {PuzzlePlayGameMode} */ (this.root.gameMode);
|
||||
this.root.gameState.moveToState("PuzzleMenuState", {
|
||||
continueQueue: gameMode.nextPuzzles,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -128,7 +128,6 @@ export class HUDBuildingPlacerLogic extends BaseHUDPart {
|
||||
this.root.hud.signals.buildingsSelectedForCopy.add(this.abortPlacement, this);
|
||||
this.root.hud.signals.pasteBlueprintRequested.add(this.abortPlacement, this);
|
||||
this.root.signals.storyGoalCompleted.add(() => this.signals.variantChanged.dispatch());
|
||||
this.root.signals.storyGoalCompleted.add(() => this.currentMetaBuilding.set(null));
|
||||
this.root.signals.upgradePurchased.add(() => this.signals.variantChanged.dispatch());
|
||||
this.root.signals.editModeChanged.add(this.onEditModeChanged, this);
|
||||
this.root.signals.testModeChanged.add(this.abortPlacement, this);
|
||||
|
||||
@ -158,8 +158,13 @@ export class HUDInteractiveTutorial extends BaseHUDPart {
|
||||
|
||||
onHintChanged(hintId) {
|
||||
this.elementDescription.innerHTML = T.ingame.interactiveTutorial.hints[hintId];
|
||||
|
||||
const folder = G_WEGAME_VERSION
|
||||
? "interactive_tutorial.cn.noinline"
|
||||
: "interactive_tutorial.noinline";
|
||||
|
||||
this.elementGif.style.backgroundImage =
|
||||
"url('" + cachebust("res/ui/interactive_tutorial.noinline/" + hintId + ".gif") + "')";
|
||||
"url('" + cachebust("res/ui/" + folder + "/" + hintId + ".gif") + "')";
|
||||
this.element.classList.toggle("animEven");
|
||||
this.element.classList.toggle("animOdd");
|
||||
}
|
||||
|
||||
@ -6,13 +6,8 @@ import { InputReceiver } from "../../../core/input_receiver";
|
||||
import { makeDiv } from "../../../core/utils";
|
||||
import { SOUNDS } from "../../../platform/sound";
|
||||
import { T } from "../../../translations";
|
||||
import { enumColors } from "../../colors";
|
||||
import { ColorItem } from "../../items/color_item";
|
||||
import { finalGameShape, rocketShape } from "../../modes/regular";
|
||||
import { BaseHUDPart } from "../base_hud_part";
|
||||
import { DynamicDomAttach } from "../dynamic_dom_attach";
|
||||
import { ShapeItem } from "../../items/shape_item";
|
||||
import { ShapeDefinition } from "../../shape_definition";
|
||||
|
||||
export class HUDPuzzleCompleteNotification extends BaseHUDPart {
|
||||
initialize() {
|
||||
@ -68,10 +63,21 @@ export class HUDPuzzleCompleteNotification extends BaseHUDPart {
|
||||
this.menuBtn.classList.add("menu", "styledButton");
|
||||
this.menuBtn.innerText = T.ingame.puzzleCompletion.menuBtn;
|
||||
buttonBar.appendChild(this.menuBtn);
|
||||
|
||||
this.trackClicks(this.menuBtn, () => {
|
||||
this.close(true);
|
||||
});
|
||||
|
||||
const gameMode = /** @type {PuzzlePlayGameMode} */ (this.root.gameMode);
|
||||
if (gameMode.nextPuzzles.length > 0) {
|
||||
this.nextPuzzleBtn = document.createElement("button");
|
||||
this.nextPuzzleBtn.classList.add("nextPuzzle", "styledButton");
|
||||
this.nextPuzzleBtn.innerText = T.ingame.puzzleCompletion.nextPuzzle;
|
||||
buttonBar.appendChild(this.nextPuzzleBtn);
|
||||
|
||||
this.trackClicks(this.nextPuzzleBtn, () => {
|
||||
this.nextPuzzle();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
updateState() {
|
||||
@ -93,6 +99,15 @@ export class HUDPuzzleCompleteNotification extends BaseHUDPart {
|
||||
return this.visible;
|
||||
}
|
||||
|
||||
nextPuzzle() {
|
||||
const gameMode = /** @type {PuzzlePlayGameMode} */ (this.root.gameMode);
|
||||
gameMode.trackCompleted(this.userDidLikePuzzle, Math.round(this.timeOfCompletion)).then(() => {
|
||||
this.root.gameState.moveToState("PuzzleMenuState", {
|
||||
continueQueue: gameMode.nextPuzzles,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
close(toMenu) {
|
||||
/** @type {PuzzlePlayGameMode} */ (this.root.gameMode)
|
||||
.trackCompleted(this.userDidLikePuzzle, Math.round(this.timeOfCompletion))
|
||||
|
||||
@ -192,8 +192,9 @@ export class HUDPuzzleEditorSettings extends BaseHUDPart {
|
||||
assertAlways(false, "Failed to re-place building in trim");
|
||||
}
|
||||
|
||||
if (building.components.ConstantSignal) {
|
||||
result.components.ConstantSignal.signal = building.components.ConstantSignal.signal;
|
||||
for (const key in building.components) {
|
||||
/** @type {import("../../../core/global_registries").Component} */ (building
|
||||
.components[key]).copyAdditionalStateTo(result.components[key]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
112
src/js/game/hud/parts/shape_tooltip.js
Normal file
@ -0,0 +1,112 @@
|
||||
import { DrawParameters } from "../../../core/draw_parameters";
|
||||
import { enumDirectionToVector, Vector } from "../../../core/vector";
|
||||
import { Entity } from "../../entity";
|
||||
import { KEYMAPPINGS } from "../../key_action_mapper";
|
||||
import { THEME } from "../../theme";
|
||||
import { BaseHUDPart } from "../base_hud_part";
|
||||
|
||||
export class HUDShapeTooltip extends BaseHUDPart {
|
||||
createElements(parent) {}
|
||||
|
||||
initialize() {
|
||||
/** @type {Vector} */
|
||||
this.currentTile = new Vector(0, 0);
|
||||
|
||||
/** @type {Entity} */
|
||||
this.currentEntity = null;
|
||||
|
||||
this.isPlacingBuilding = false;
|
||||
|
||||
this.root.signals.entityQueuedForDestroy.add(() => {
|
||||
this.currentEntity = null;
|
||||
}, this);
|
||||
|
||||
this.root.hud.signals.selectedPlacementBuildingChanged.add(metaBuilding => {
|
||||
this.isPlacingBuilding = metaBuilding;
|
||||
}, this);
|
||||
}
|
||||
|
||||
isActive() {
|
||||
const hudParts = this.root.hud.parts;
|
||||
|
||||
// return false if any other placer is active
|
||||
return (
|
||||
this.root.keyMapper.getBinding(KEYMAPPINGS.ingame.showShapeTooltip).pressed &&
|
||||
!this.isPlacingBuilding &&
|
||||
!hudParts.massSelector.currentSelectionStartWorld &&
|
||||
hudParts.massSelector.selectedUids.size < 1 &&
|
||||
!hudParts.blueprintPlacer.currentBlueprint.get()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {DrawParameters} parameters
|
||||
*/
|
||||
draw(parameters) {
|
||||
if (this.isActive()) {
|
||||
const mousePos = this.root.app.mousePosition;
|
||||
|
||||
if (mousePos) {
|
||||
const tile = this.root.camera.screenToWorld(mousePos.copy()).toTileSpace();
|
||||
if (!tile.equals(this.currentTile)) {
|
||||
this.currentTile = tile;
|
||||
|
||||
const entity = this.root.map.getLayerContentXY(tile.x, tile.y, this.root.currentLayer);
|
||||
|
||||
if (entity && entity.components.ItemProcessor && entity.components.ItemEjector) {
|
||||
this.currentEntity = entity;
|
||||
} else {
|
||||
this.currentEntity = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.currentEntity) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ejectorComp = this.currentEntity.components.ItemEjector;
|
||||
const staticComp = this.currentEntity.components.StaticMapEntity;
|
||||
|
||||
const context = parameters.context;
|
||||
|
||||
for (let i = 0; i < ejectorComp.slots.length; ++i) {
|
||||
const slot = ejectorComp.slots[i];
|
||||
|
||||
if (!slot.lastItem || slot.lastItem._type != "shape") {
|
||||
continue;
|
||||
}
|
||||
|
||||
const drawPos = staticComp
|
||||
.localTileToWorld(slot.pos.add(enumDirectionToVector[slot.direction].multiplyScalar(1)))
|
||||
.toWorldSpaceCenterOfTile();
|
||||
|
||||
const slotCenterPos = staticComp
|
||||
.localTileToWorld(slot.pos.add(enumDirectionToVector[slot.direction].multiplyScalar(0.2)))
|
||||
.toWorldSpaceCenterOfTile();
|
||||
|
||||
context.fillStyle = THEME.shapeTooltip.outline;
|
||||
context.strokeStyle = THEME.shapeTooltip.outline;
|
||||
|
||||
context.lineWidth = 1.5;
|
||||
context.beginPath();
|
||||
context.moveTo(slotCenterPos.x, slotCenterPos.y);
|
||||
context.lineTo(drawPos.x, drawPos.y);
|
||||
context.stroke();
|
||||
|
||||
context.beginCircle(slotCenterPos.x, slotCenterPos.y, 3.5);
|
||||
context.fill();
|
||||
|
||||
context.fillStyle = THEME.shapeTooltip.background;
|
||||
context.strokeStyle = THEME.shapeTooltip.outline;
|
||||
|
||||
context.lineWidth = 1.2;
|
||||
context.beginCircle(drawPos.x, drawPos.y, 11 + 1.2 / 2);
|
||||
context.fill();
|
||||
context.stroke();
|
||||
slot.lastItem.drawItemCenteredClipped(drawPos.x, drawPos.y, parameters, 22);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -45,7 +45,7 @@ export class HUDWaypoints extends BaseHUDPart {
|
||||
*/
|
||||
createElements(parent) {
|
||||
// Create the helper box on the lower right when zooming out
|
||||
if (this.root.app.settings.getAllSettings().offerHints) {
|
||||
if (this.root.app.settings.getAllSettings().offerHints && !G_WEGAME_VERSION) {
|
||||
this.hintElement = makeDiv(
|
||||
parent,
|
||||
"ingame_HUD_Waypoints_Hint",
|
||||
@ -121,10 +121,12 @@ export class HUDWaypoints extends BaseHUDPart {
|
||||
}
|
||||
|
||||
// Catch mouse and key events
|
||||
this.root.camera.downPreHandler.add(this.onMouseDown, this);
|
||||
this.root.keyMapper
|
||||
.getBinding(KEYMAPPINGS.navigation.createMarker)
|
||||
.add(() => this.requestSaveMarker({}));
|
||||
if (!G_WEGAME_VERSION) {
|
||||
this.root.camera.downPreHandler.add(this.onMouseDown, this);
|
||||
this.root.keyMapper
|
||||
.getBinding(KEYMAPPINGS.navigation.createMarker)
|
||||
.add(() => this.requestSaveMarker({}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores at how much opacity the markers should be rendered on the map.
|
||||
|
||||
@ -32,6 +32,8 @@ export const KEYMAPPINGS = {
|
||||
toggleFPSInfo: { keyCode: 115 }, // F4
|
||||
|
||||
switchLayers: { keyCode: key("E") },
|
||||
|
||||
showShapeTooltip: { keyCode: 18 }, // ALT
|
||||
},
|
||||
|
||||
navigation: {
|
||||
|
||||
@ -3,6 +3,7 @@ import { Vector } from "../core/vector";
|
||||
import { BasicSerializableObject, types } from "../savegame/serialization";
|
||||
import { BaseItem } from "./base_item";
|
||||
import { Entity } from "./entity";
|
||||
import { MapChunkAggregate } from "./map_chunk_aggregate";
|
||||
import { MapChunkView } from "./map_chunk_view";
|
||||
import { GameRoot } from "./root";
|
||||
|
||||
@ -31,6 +32,11 @@ export class BaseMap extends BasicSerializableObject {
|
||||
* Mapping of 'X|Y' to chunk
|
||||
* @type {Map<string, MapChunkView>} */
|
||||
this.chunksById = new Map();
|
||||
|
||||
/**
|
||||
* Mapping of 'X|Y' to chunk aggregate
|
||||
* @type {Map<string, MapChunkAggregate>} */
|
||||
this.aggregatesById = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -55,6 +61,39 @@ export class BaseMap extends BasicSerializableObject {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the chunk aggregate containing a given chunk
|
||||
* @param {number} chunkX
|
||||
* @param {number} chunkY
|
||||
*/
|
||||
getAggregateForChunk(chunkX, chunkY, createIfNotExistent = false) {
|
||||
const aggX = Math.floor(chunkX / globalConfig.chunkAggregateSize);
|
||||
const aggY = Math.floor(chunkY / globalConfig.chunkAggregateSize);
|
||||
return this.getAggregate(aggX, aggY, createIfNotExistent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the given chunk aggregate by index
|
||||
* @param {number} aggX
|
||||
* @param {number} aggY
|
||||
*/
|
||||
getAggregate(aggX, aggY, createIfNotExistent = false) {
|
||||
const aggIdentifier = aggX + "|" + aggY;
|
||||
let storedAggregate;
|
||||
|
||||
if ((storedAggregate = this.aggregatesById.get(aggIdentifier))) {
|
||||
return storedAggregate;
|
||||
}
|
||||
|
||||
if (createIfNotExistent) {
|
||||
const instance = new MapChunkAggregate(this.root, aggX, aggY);
|
||||
this.aggregatesById.set(aggIdentifier, instance);
|
||||
return instance;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets or creates a new chunk if not existent for the given tile
|
||||
* @param {number} tileX
|
||||
|
||||
154
src/js/game/map_chunk_aggregate.js
Normal file
@ -0,0 +1,154 @@
|
||||
import { globalConfig } from "../core/config";
|
||||
import { DrawParameters } from "../core/draw_parameters";
|
||||
import { MapChunk } from "./map_chunk";
|
||||
import { GameRoot } from "./root";
|
||||
import { drawSpriteClipped } from "../core/draw_utils";
|
||||
|
||||
export const CHUNK_OVERLAY_RES = 3;
|
||||
|
||||
export class MapChunkAggregate {
|
||||
/**
|
||||
*
|
||||
* @param {GameRoot} root
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
*/
|
||||
constructor(root, x, y) {
|
||||
this.root = root;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
|
||||
/**
|
||||
* Whenever something changes, we increase this number - so we know we need to redraw
|
||||
*/
|
||||
this.renderIteration = 0;
|
||||
this.dirty = false;
|
||||
/** @type {Array<boolean>} */
|
||||
this.dirtyList = new Array(globalConfig.chunkAggregateSize ** 2).fill(true);
|
||||
this.markDirty(0, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks this chunk as dirty, rerendering all caches
|
||||
* @param {number} chunkX
|
||||
* @param {number} chunkY
|
||||
*/
|
||||
markDirty(chunkX, chunkY) {
|
||||
const relX = chunkX % globalConfig.chunkAggregateSize;
|
||||
const relY = chunkY % globalConfig.chunkAggregateSize;
|
||||
this.dirtyList[relY * globalConfig.chunkAggregateSize + relX] = true;
|
||||
if (this.dirty) {
|
||||
return;
|
||||
}
|
||||
this.dirty = true;
|
||||
++this.renderIteration;
|
||||
this.renderKey = this.x + "/" + this.y + "@" + this.renderIteration;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {HTMLCanvasElement} canvas
|
||||
* @param {CanvasRenderingContext2D} context
|
||||
* @param {number} w
|
||||
* @param {number} h
|
||||
* @param {number} dpi
|
||||
*/
|
||||
generateOverlayBuffer(canvas, context, w, h, dpi) {
|
||||
const prevKey = this.x + "/" + this.y + "@" + (this.renderIteration - 1);
|
||||
const prevBuffer = this.root.buffers.getForKeyOrNullNoUpdate({
|
||||
key: "agg@" + this.root.currentLayer,
|
||||
subKey: prevKey,
|
||||
});
|
||||
|
||||
const overlaySize = globalConfig.mapChunkSize * CHUNK_OVERLAY_RES;
|
||||
let onlyDirty = false;
|
||||
if (prevBuffer) {
|
||||
context.drawImage(prevBuffer, 0, 0);
|
||||
onlyDirty = true;
|
||||
}
|
||||
|
||||
for (let x = 0; x < globalConfig.chunkAggregateSize; x++) {
|
||||
for (let y = 0; y < globalConfig.chunkAggregateSize; y++) {
|
||||
if (onlyDirty && !this.dirtyList[globalConfig.chunkAggregateSize * y + x]) continue;
|
||||
this.root.map
|
||||
.getChunk(
|
||||
this.x * globalConfig.chunkAggregateSize + x,
|
||||
this.y * globalConfig.chunkAggregateSize + y,
|
||||
true
|
||||
)
|
||||
.generateOverlayBuffer(
|
||||
context,
|
||||
overlaySize,
|
||||
overlaySize,
|
||||
x * overlaySize,
|
||||
y * overlaySize
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.dirty = false;
|
||||
this.dirtyList.fill(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Overlay
|
||||
* @param {DrawParameters} parameters
|
||||
*/
|
||||
drawOverlay(parameters) {
|
||||
const aggregateOverlaySize =
|
||||
globalConfig.mapChunkSize * globalConfig.chunkAggregateSize * CHUNK_OVERLAY_RES;
|
||||
const sprite = this.root.buffers.getForKey({
|
||||
key: "agg@" + this.root.currentLayer,
|
||||
subKey: this.renderKey,
|
||||
w: aggregateOverlaySize,
|
||||
h: aggregateOverlaySize,
|
||||
dpi: 1,
|
||||
redrawMethod: this.generateOverlayBuffer.bind(this),
|
||||
});
|
||||
|
||||
const dims = globalConfig.mapChunkWorldSize * globalConfig.chunkAggregateSize;
|
||||
const extrude = 0.05;
|
||||
|
||||
// Draw chunk "pixel" art
|
||||
parameters.context.imageSmoothingEnabled = false;
|
||||
drawSpriteClipped({
|
||||
parameters,
|
||||
sprite,
|
||||
x: this.x * dims - extrude,
|
||||
y: this.y * dims - extrude,
|
||||
w: dims + 2 * extrude,
|
||||
h: dims + 2 * extrude,
|
||||
originalW: aggregateOverlaySize,
|
||||
originalH: aggregateOverlaySize,
|
||||
});
|
||||
|
||||
parameters.context.imageSmoothingEnabled = true;
|
||||
const resourcesScale = this.root.app.settings.getAllSettings().mapResourcesScale;
|
||||
|
||||
// Draw patch items
|
||||
if (
|
||||
this.root.currentLayer === "regular" &&
|
||||
resourcesScale > 0.05 &&
|
||||
this.root.camera.zoomLevel > 0.1
|
||||
) {
|
||||
const diameter = (70 / Math.pow(parameters.zoomLevel, 0.35)) * (0.2 + 2 * resourcesScale);
|
||||
|
||||
for (let x = 0; x < globalConfig.chunkAggregateSize; x++) {
|
||||
for (let y = 0; y < globalConfig.chunkAggregateSize; y++) {
|
||||
this.root.map
|
||||
.getChunk(
|
||||
this.x * globalConfig.chunkAggregateSize + x,
|
||||
this.y * globalConfig.chunkAggregateSize + y,
|
||||
true
|
||||
)
|
||||
.drawOverlayPatches(
|
||||
parameters,
|
||||
this.x * dims + x * globalConfig.mapChunkWorldSize,
|
||||
this.y * dims + y * globalConfig.mapChunkWorldSize,
|
||||
diameter
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -33,6 +33,7 @@ export class MapChunkView extends MapChunk {
|
||||
markDirty() {
|
||||
++this.renderIteration;
|
||||
this.renderKey = this.x + "/" + this.y + "@" + this.renderIteration;
|
||||
this.root.map.getAggregateForChunk(this.x, this.y, true).markDirty(this.x, this.y);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -82,73 +83,41 @@ export class MapChunkView extends MapChunk {
|
||||
}
|
||||
|
||||
/**
|
||||
* Overlay
|
||||
* @param {DrawParameters} parameters
|
||||
* @param {number} xoffs
|
||||
* @param {number} yoffs
|
||||
* @param {number} diameter
|
||||
*/
|
||||
drawOverlay(parameters) {
|
||||
const overlaySize = globalConfig.mapChunkSize * CHUNK_OVERLAY_RES;
|
||||
const sprite = this.root.buffers.getForKey({
|
||||
key: "chunk@" + this.root.currentLayer,
|
||||
subKey: this.renderKey,
|
||||
w: overlaySize,
|
||||
h: overlaySize,
|
||||
dpi: 1,
|
||||
redrawMethod: this.generateOverlayBuffer.bind(this),
|
||||
});
|
||||
|
||||
const dims = globalConfig.mapChunkWorldSize;
|
||||
const extrude = 0.05;
|
||||
|
||||
// Draw chunk "pixel" art
|
||||
parameters.context.imageSmoothingEnabled = false;
|
||||
drawSpriteClipped({
|
||||
parameters,
|
||||
sprite,
|
||||
x: this.x * dims - extrude,
|
||||
y: this.y * dims - extrude,
|
||||
w: dims + 2 * extrude,
|
||||
h: dims + 2 * extrude,
|
||||
originalW: overlaySize,
|
||||
originalH: overlaySize,
|
||||
});
|
||||
|
||||
parameters.context.imageSmoothingEnabled = true;
|
||||
const resourcesScale = this.root.app.settings.getAllSettings().mapResourcesScale;
|
||||
|
||||
// Draw patch items
|
||||
if (this.root.currentLayer === "regular" && resourcesScale > 0.05) {
|
||||
const diameter = (70 / Math.pow(parameters.zoomLevel, 0.35)) * (0.2 + 2 * resourcesScale);
|
||||
|
||||
for (let i = 0; i < this.patches.length; ++i) {
|
||||
const patch = this.patches[i];
|
||||
if (patch.item.getItemType() === "shape") {
|
||||
const destX = this.x * dims + patch.pos.x * globalConfig.tileSize;
|
||||
const destY = this.y * dims + patch.pos.y * globalConfig.tileSize;
|
||||
patch.item.drawItemCenteredClipped(destX, destY, parameters, diameter);
|
||||
}
|
||||
drawOverlayPatches(parameters, xoffs, yoffs, diameter) {
|
||||
for (let i = 0; i < this.patches.length; ++i) {
|
||||
const patch = this.patches[i];
|
||||
if (patch.item.getItemType() === "shape") {
|
||||
const destX = xoffs + patch.pos.x * globalConfig.tileSize;
|
||||
const destY = yoffs + patch.pos.y * globalConfig.tileSize;
|
||||
patch.item.drawItemCenteredClipped(destX, destY, parameters, diameter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {HTMLCanvasElement} canvas
|
||||
* @param {CanvasRenderingContext2D} context
|
||||
* @param {number} w
|
||||
* @param {number} h
|
||||
* @param {number} dpi
|
||||
* @param {number=} xoffs
|
||||
* @param {number=} yoffs
|
||||
*/
|
||||
generateOverlayBuffer(canvas, context, w, h, dpi) {
|
||||
generateOverlayBuffer(context, w, h, xoffs, yoffs) {
|
||||
context.fillStyle =
|
||||
this.containedEntities.length > 0
|
||||
? THEME.map.chunkOverview.filled
|
||||
: THEME.map.chunkOverview.empty;
|
||||
context.fillRect(0, 0, w, h);
|
||||
context.fillRect(xoffs, yoffs, w, h);
|
||||
|
||||
if (this.root.app.settings.getAllSettings().displayChunkBorders) {
|
||||
context.fillStyle = THEME.map.chunkBorders;
|
||||
context.fillRect(0, 0, w, 1);
|
||||
context.fillRect(0, 1, 1, h);
|
||||
context.fillRect(xoffs, yoffs, w, 1);
|
||||
context.fillRect(xoffs, yoffs + 1, 1, h);
|
||||
}
|
||||
|
||||
for (let x = 0; x < globalConfig.mapChunkSize; ++x) {
|
||||
@ -174,8 +143,8 @@ export class MapChunkView extends MapChunk {
|
||||
if (lowerContent) {
|
||||
context.fillStyle = lowerContent.getBackgroundColorAsResource();
|
||||
context.fillRect(
|
||||
x * CHUNK_OVERLAY_RES,
|
||||
y * CHUNK_OVERLAY_RES,
|
||||
xoffs + x * CHUNK_OVERLAY_RES,
|
||||
yoffs + y * CHUNK_OVERLAY_RES,
|
||||
CHUNK_OVERLAY_RES,
|
||||
CHUNK_OVERLAY_RES
|
||||
);
|
||||
@ -190,8 +159,8 @@ export class MapChunkView extends MapChunk {
|
||||
const isFilled = overlayMatrix[dx + dy * 3];
|
||||
if (isFilled) {
|
||||
context.fillRect(
|
||||
x * CHUNK_OVERLAY_RES + dx,
|
||||
y * CHUNK_OVERLAY_RES + dy,
|
||||
xoffs + x * CHUNK_OVERLAY_RES + dx,
|
||||
yoffs + y * CHUNK_OVERLAY_RES + dy,
|
||||
1,
|
||||
1
|
||||
);
|
||||
@ -206,8 +175,8 @@ export class MapChunkView extends MapChunk {
|
||||
data.rotationVariant
|
||||
);
|
||||
context.fillRect(
|
||||
x * CHUNK_OVERLAY_RES,
|
||||
y * CHUNK_OVERLAY_RES,
|
||||
xoffs + x * CHUNK_OVERLAY_RES,
|
||||
yoffs + y * CHUNK_OVERLAY_RES,
|
||||
CHUNK_OVERLAY_RES,
|
||||
CHUNK_OVERLAY_RES
|
||||
);
|
||||
@ -220,8 +189,8 @@ export class MapChunkView extends MapChunk {
|
||||
if (lowerContent) {
|
||||
context.fillStyle = lowerContent.getBackgroundColorAsResource();
|
||||
context.fillRect(
|
||||
x * CHUNK_OVERLAY_RES,
|
||||
y * CHUNK_OVERLAY_RES,
|
||||
xoffs + x * CHUNK_OVERLAY_RES,
|
||||
yoffs + y * CHUNK_OVERLAY_RES,
|
||||
CHUNK_OVERLAY_RES,
|
||||
CHUNK_OVERLAY_RES
|
||||
);
|
||||
@ -233,7 +202,7 @@ export class MapChunkView extends MapChunk {
|
||||
// Draw wires overlay
|
||||
|
||||
context.fillStyle = THEME.map.wires.overlayColor;
|
||||
context.fillRect(0, 0, w, h);
|
||||
context.fillRect(xoffs, yoffs, w, h);
|
||||
|
||||
for (let x = 0; x < globalConfig.mapChunkSize; ++x) {
|
||||
const wiresArray = this.wireContents[x];
|
||||
@ -244,8 +213,8 @@ export class MapChunkView extends MapChunk {
|
||||
}
|
||||
MapChunkView.drawSingleWiresOverviewTile({
|
||||
context,
|
||||
x: x * CHUNK_OVERLAY_RES,
|
||||
y: y * CHUNK_OVERLAY_RES,
|
||||
x: xoffs + x * CHUNK_OVERLAY_RES,
|
||||
y: yoffs + y * CHUNK_OVERLAY_RES,
|
||||
entity: content,
|
||||
tileSizePixels: CHUNK_OVERLAY_RES,
|
||||
});
|
||||
|
||||
@ -5,6 +5,7 @@ import { freeCanvas, makeOffscreenBuffer } from "../core/buffer_utils";
|
||||
import { Entity } from "./entity";
|
||||
import { THEME } from "./theme";
|
||||
import { MapChunkView } from "./map_chunk_view";
|
||||
import { MapChunkAggregate } from "./map_chunk_aggregate";
|
||||
|
||||
/**
|
||||
* This is the view of the map, it extends the map which is the raw model and allows
|
||||
@ -164,6 +165,40 @@ export class MapView extends BaseMap {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls a given method on all given chunks
|
||||
* @param {DrawParameters} parameters
|
||||
* @param {function} method
|
||||
*/
|
||||
drawVisibleAggregates(parameters, method) {
|
||||
const cullRange = parameters.visibleRect.allScaled(1 / globalConfig.tileSize);
|
||||
const top = cullRange.top();
|
||||
const right = cullRange.right();
|
||||
const bottom = cullRange.bottom();
|
||||
const left = cullRange.left();
|
||||
|
||||
const border = 0;
|
||||
const minY = top - border;
|
||||
const maxY = bottom + border;
|
||||
const minX = left - border;
|
||||
const maxX = right + border;
|
||||
|
||||
const aggregateTiles = globalConfig.chunkAggregateSize * globalConfig.mapChunkSize;
|
||||
const aggStartX = Math.floor(minX / aggregateTiles);
|
||||
const aggStartY = Math.floor(minY / aggregateTiles);
|
||||
|
||||
const aggEndX = Math.floor(maxX / aggregateTiles);
|
||||
const aggEndY = Math.floor(maxY / aggregateTiles);
|
||||
|
||||
// Render y from top down for proper blending
|
||||
for (let aggX = aggStartX; aggX <= aggEndX; ++aggX) {
|
||||
for (let aggY = aggStartY; aggY <= aggEndY; ++aggY) {
|
||||
const aggregate = this.root.map.getAggregate(aggX, aggY, true);
|
||||
method.call(aggregate, parameters);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the wires foreground
|
||||
* @param {DrawParameters} parameters
|
||||
@ -177,7 +212,7 @@ export class MapView extends BaseMap {
|
||||
* @param {DrawParameters} parameters
|
||||
*/
|
||||
drawOverlay(parameters) {
|
||||
this.drawVisibleChunks(parameters, MapChunkView.prototype.drawOverlay);
|
||||
this.drawVisibleAggregates(parameters, MapChunkAggregate.prototype.drawOverlay);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -30,6 +30,7 @@ import { HUDPuzzlePlaySettings } from "../hud/parts/puzzle_play_settings";
|
||||
import { MetaBlockBuilding } from "../buildings/block";
|
||||
import { MetaBuilding } from "../meta_building";
|
||||
import { gMetaBuildingRegistry } from "../../core/global_registries";
|
||||
import { HUDPuzzleNextPuzzle } from "../hud/parts/HUDPuzzleNextPuzzle";
|
||||
|
||||
const logger = createLogger("puzzle-play");
|
||||
const copy = require("clipboard-copy");
|
||||
@ -43,8 +44,9 @@ export class PuzzlePlayGameMode extends PuzzleGameMode {
|
||||
* @param {GameRoot} root
|
||||
* @param {object} payload
|
||||
* @param {import("../../savegame/savegame_typedefs").PuzzleFullData} payload.puzzle
|
||||
* @param {Array<number> | undefined} payload.nextPuzzles
|
||||
*/
|
||||
constructor(root, { puzzle }) {
|
||||
constructor(root, { puzzle, nextPuzzles }) {
|
||||
super(root);
|
||||
|
||||
/** @type {Array<typeof MetaBuilding>} */
|
||||
@ -95,6 +97,15 @@ export class PuzzlePlayGameMode extends PuzzleGameMode {
|
||||
root.signals.postLoadHook.add(this.loadPuzzle, this);
|
||||
|
||||
this.puzzle = puzzle;
|
||||
|
||||
/**
|
||||
* @type {Array<number>}
|
||||
*/
|
||||
this.nextPuzzles = nextPuzzles || [];
|
||||
|
||||
if (this.nextPuzzles.length > 0) {
|
||||
this.additionalHudParts.puzzleNext = HUDPuzzleNextPuzzle;
|
||||
}
|
||||
}
|
||||
|
||||
loadPuzzle() {
|
||||
|
||||
@ -575,7 +575,9 @@ export class RegularGameMode extends GameMode {
|
||||
}
|
||||
|
||||
if (this.root.app.settings.getAllSettings().offerHints) {
|
||||
this.additionalHudParts.tutorialHints = HUDPartTutorialHints;
|
||||
if (!G_WEGAME_VERSION) {
|
||||
this.additionalHudParts.tutorialHints = HUDPartTutorialHints;
|
||||
}
|
||||
this.additionalHudParts.interactiveTutorial = HUDInteractiveTutorial;
|
||||
}
|
||||
|
||||
|
||||
@ -334,14 +334,19 @@ export class ItemProcessorSystem extends GameSystemWithFilter {
|
||||
|
||||
const cutDefinitions = this.root.shapeDefinitionMgr.shapeActionCutHalf(inputDefinition);
|
||||
|
||||
const ejectorComp = payload.entity.components.ItemEjector;
|
||||
for (let i = 0; i < cutDefinitions.length; ++i) {
|
||||
const definition = cutDefinitions[i];
|
||||
if (!definition.isEntirelyEmpty()) {
|
||||
payload.outItems.push({
|
||||
item: this.root.shapeDefinitionMgr.getShapeItemFromDefinition(definition),
|
||||
requiredSlot: i,
|
||||
});
|
||||
|
||||
if (definition.isEntirelyEmpty()) {
|
||||
ejectorComp.slots[i].lastItem = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
payload.outItems.push({
|
||||
item: this.root.shapeDefinitionMgr.getShapeItemFromDefinition(definition),
|
||||
requiredSlot: i,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -355,14 +360,19 @@ export class ItemProcessorSystem extends GameSystemWithFilter {
|
||||
|
||||
const cutDefinitions = this.root.shapeDefinitionMgr.shapeActionCutQuad(inputDefinition);
|
||||
|
||||
const ejectorComp = payload.entity.components.ItemEjector;
|
||||
for (let i = 0; i < cutDefinitions.length; ++i) {
|
||||
const definition = cutDefinitions[i];
|
||||
if (!definition.isEntirelyEmpty()) {
|
||||
payload.outItems.push({
|
||||
item: this.root.shapeDefinitionMgr.getShapeItemFromDefinition(definition),
|
||||
requiredSlot: i,
|
||||
});
|
||||
|
||||
if (definition.isEntirelyEmpty()) {
|
||||
ejectorComp.slots[i].lastItem = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
payload.outItems.push({
|
||||
item: this.root.shapeDefinitionMgr.getShapeItemFromDefinition(definition),
|
||||
requiredSlot: i,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -59,5 +59,10 @@
|
||||
"outline": "#111418",
|
||||
"outlineWidth": 0.75,
|
||||
"circleBackground": "rgba(20, 30, 40, 0.3)"
|
||||
},
|
||||
|
||||
"shapeTooltip": {
|
||||
"background": "rgba(242, 245, 254, 0.9)",
|
||||
"outline": "#44464e"
|
||||
}
|
||||
}
|
||||
|
||||
@ -60,5 +60,10 @@
|
||||
"outline": "#55575a",
|
||||
"outlineWidth": 0.75,
|
||||
"circleBackground": "rgba(40, 50, 65, 0.1)"
|
||||
},
|
||||
|
||||
"shapeTooltip": {
|
||||
"background": "#dee1ea",
|
||||
"outline": "#54565e"
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,7 +12,9 @@ export const LANGUAGES = {
|
||||
"zh-CN": {
|
||||
// simplified chinese
|
||||
name: "简体中文",
|
||||
data: require("./built-temp/base-zh-CN.json"),
|
||||
data: G_WEGAME_VERSION
|
||||
? require("./built-temp/base-zh-CN-ISBN.json")
|
||||
: require("./built-temp/base-zh-CN.json"),
|
||||
code: "zh",
|
||||
region: "CN",
|
||||
},
|
||||
|
||||
@ -143,6 +143,20 @@ export class ClientAPI {
|
||||
return this._request("/v1/puzzles/list/" + category, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{ searchTerm: string; difficulty: string; duration: string }} searchOptions
|
||||
* @returns {Promise<import("../savegame/savegame_typedefs").PuzzleMetadata[]>}
|
||||
*/
|
||||
apiSearchPuzzles(searchOptions) {
|
||||
if (!this.isLoggedIn()) {
|
||||
return Promise.reject("not-logged-in");
|
||||
}
|
||||
return this._request("/v1/puzzles/search", {
|
||||
method: "POST",
|
||||
body: searchOptions,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} puzzleId
|
||||
* @returns {Promise<import("../savegame/savegame_typedefs").PuzzleFullData>}
|
||||
@ -169,7 +183,7 @@ export class ClientAPI {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} shortKey
|
||||
* @param {string} shortKey
|
||||
* @returns {Promise<import("../savegame/savegame_typedefs").PuzzleFullData>}
|
||||
*/
|
||||
apiDownloadPuzzleByKey(shortKey) {
|
||||
|
||||
@ -135,15 +135,7 @@ export class PlatformWrapperImplBrowser extends PlatformWrapperInterface {
|
||||
|
||||
openExternalLink(url, force = false) {
|
||||
logger.log("Opening external:", url);
|
||||
if (force || this.embedProvider.externalLinks) {
|
||||
window.open(url);
|
||||
} else {
|
||||
// Do nothing
|
||||
alert(
|
||||
"This platform does not allow opening external links. You can play on https://shapez.io directly to open them.\n\nClicked Link: " +
|
||||
url
|
||||
);
|
||||
}
|
||||
window.open(url);
|
||||
}
|
||||
|
||||
performRestart() {
|
||||
|
||||
@ -272,7 +272,7 @@ export const allApplicationSettings = [
|
||||
new EnumSetting("refreshRate", {
|
||||
options: refreshRateOptions,
|
||||
valueGetter: rate => rate,
|
||||
textGetter: rate => rate + " Hz",
|
||||
textGetter: rate => T.settings.tickrateHz.replace("<amount>", rate),
|
||||
category: enumCategories.performance,
|
||||
restartRequired: false,
|
||||
changeCb: (app, id) => {},
|
||||
|
||||
@ -64,6 +64,24 @@ export class Savegame extends ReadWriteProxy {
|
||||
return savegameInterfaces[Savegame.getCurrentVersion()];
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Application} app
|
||||
* @returns
|
||||
*/
|
||||
static createPuzzleSavegame(app) {
|
||||
return new Savegame(app, {
|
||||
internalId: "puzzle",
|
||||
metaDataRef: {
|
||||
internalId: "puzzle",
|
||||
lastUpdate: 0,
|
||||
version: 0,
|
||||
level: 0,
|
||||
name: "puzzle",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {number}
|
||||
*/
|
||||
|
||||
@ -17,6 +17,7 @@ import {
|
||||
waitNextFrame,
|
||||
} from "../core/utils";
|
||||
import { HUDModalDialogs } from "../game/hud/parts/modal_dialogs";
|
||||
import { PlatformWrapperImplBrowser } from "../platform/browser/wrapper";
|
||||
import { PlatformWrapperImplElectron } from "../platform/electron/wrapper";
|
||||
import { getApplicationSettingById } from "../profile/application_settings";
|
||||
import { T } from "../translations";
|
||||
@ -34,35 +35,57 @@ export class MainMenuState extends GameState {
|
||||
}
|
||||
|
||||
getInnerHTML() {
|
||||
const showLanguageIcon = !G_CHINA_VERSION && !G_WEGAME_VERSION;
|
||||
const showExitAppButton = G_IS_STANDALONE;
|
||||
const showUpdateLabel = !G_WEGAME_VERSION;
|
||||
const showBrowserWarning = !G_IS_STANDALONE && !isSupportedBrowser();
|
||||
const showPuzzleDLC = !G_WEGAME_VERSION && (G_IS_STANDALONE || G_IS_DEV);
|
||||
const showWegameFooter = G_WEGAME_VERSION;
|
||||
|
||||
let showExternalLinks = true;
|
||||
|
||||
if (G_IS_STANDALONE) {
|
||||
if (G_WEGAME_VERSION || G_CHINA_VERSION) {
|
||||
showExternalLinks = false;
|
||||
}
|
||||
} else {
|
||||
const wrapper = /** @type {PlatformWrapperImplBrowser} */ (this.app.platformWrapper);
|
||||
if (!wrapper.embedProvider.externalLinks) {
|
||||
showExternalLinks = false;
|
||||
}
|
||||
}
|
||||
|
||||
let showDiscordLink = showExternalLinks;
|
||||
if (G_CHINA_VERSION) {
|
||||
showDiscordLink = true;
|
||||
}
|
||||
|
||||
const showCrosspromo = !G_IS_STANDALONE && showExternalLinks;
|
||||
const showDemoAdvertisement =
|
||||
showExternalLinks && this.app.restrictionMgr.getIsStandaloneMarketingActive();
|
||||
|
||||
const ownsPuzzleDLC =
|
||||
G_IS_DEV ||
|
||||
(G_IS_STANDALONE &&
|
||||
/** @type { PlatformWrapperImplElectron}*/ (this.app.platformWrapper).dlcs.puzzle);
|
||||
|
||||
const bannerHtml = `
|
||||
<h3>${T.demoBanners.title}</h3>
|
||||
<p>${T.demoBanners.intro}</p>
|
||||
<a href="#" class="steamLink ${A_B_TESTING_LINK_TYPE}" target="_blank">Get the shapez.io standalone!</a>
|
||||
|
||||
<a href="#" class="steamLink ${A_B_TESTING_LINK_TYPE}" target="_blank">Get the shapez.io standalone!</a>
|
||||
`;
|
||||
|
||||
const showDemoBadges = this.app.restrictionMgr.getIsStandaloneMarketingActive();
|
||||
|
||||
const puzzleDlc =
|
||||
G_IS_STANDALONE &&
|
||||
/** @type { PlatformWrapperImplElectron
|
||||
}*/ (this.app.platformWrapper).dlcs.puzzle;
|
||||
|
||||
return `
|
||||
<div class="topButtons">
|
||||
${
|
||||
G_CHINA_VERSION || G_WEGAME_VERSION
|
||||
? ""
|
||||
: `<button class="languageChoose" data-languageicon="${this.app.settings.getLanguage()}"></button>`
|
||||
showLanguageIcon
|
||||
? `<button class="languageChoose" data-languageicon="${this.app.settings.getLanguage()}"></button>`
|
||||
: ""
|
||||
}
|
||||
|
||||
<button class="settingsButton"></button>
|
||||
${
|
||||
G_IS_STANDALONE || G_IS_DEV
|
||||
? `
|
||||
<button class="exitAppButton"></button>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
${showExitAppButton ? `<button class="exitAppButton"></button>` : ""}
|
||||
</div>
|
||||
|
||||
<video autoplay muted loop class="fullscreenBackgroundVideo">
|
||||
@ -71,27 +94,25 @@ export class MainMenuState extends GameState {
|
||||
|
||||
<div class="logo">
|
||||
<img src="${cachebust("res/" + getLogoSprite())}" alt="shapez.io Logo">
|
||||
${G_WEGAME_VERSION ? "" : `<span class="updateLabel">v${G_BUILD_VERSION}!</span>`}
|
||||
${showUpdateLabel ? `<span class="updateLabel">v${G_BUILD_VERSION}!</span>` : ""}
|
||||
</div>
|
||||
|
||||
<div class="mainWrapper ${showDemoBadges ? "demo" : "noDemo"}" data-columns="${
|
||||
G_IS_STANDALONE && !G_WEGAME_VERSION ? 2 : showDemoBadges ? 2 : 1
|
||||
}">
|
||||
<div class="mainWrapper" data-columns="${showDemoAdvertisement || showPuzzleDLC ? 2 : 1}">
|
||||
<div class="sideContainer">
|
||||
${showDemoBadges ? `<div class="standaloneBanner">${bannerHtml}</div>` : ""}
|
||||
${showDemoAdvertisement ? `<div class="standaloneBanner">${bannerHtml}</div>` : ""}
|
||||
</div>
|
||||
|
||||
<div class="mainContainer">
|
||||
${
|
||||
G_IS_STANDALONE || isSupportedBrowser()
|
||||
? ""
|
||||
: `<div class="browserWarning">${T.mainMenu.browserWarning}</div>`
|
||||
showBrowserWarning
|
||||
? `<div class="browserWarning">${T.mainMenu.browserWarning}</div>`
|
||||
: ""
|
||||
}
|
||||
<div class="buttons"></div>
|
||||
</div>
|
||||
|
||||
${
|
||||
(!G_WEGAME_VERSION && G_IS_STANDALONE && puzzleDlc) || G_IS_DEV
|
||||
showPuzzleDLC && ownsPuzzleDLC
|
||||
? `
|
||||
<div class="puzzleContainer">
|
||||
<img class="dlcLogo" src="${cachebust(
|
||||
@ -105,7 +126,7 @@ export class MainMenuState extends GameState {
|
||||
}
|
||||
|
||||
${
|
||||
!G_WEGAME_VERSION && G_IS_STANDALONE && !puzzleDlc
|
||||
showPuzzleDLC && !ownsPuzzleDLC
|
||||
? `
|
||||
<div class="puzzleContainer notOwned">
|
||||
<span class="badge">
|
||||
@ -129,8 +150,9 @@ export class MainMenuState extends GameState {
|
||||
</div>
|
||||
|
||||
${
|
||||
G_WEGAME_VERSION
|
||||
? `<div class='footer wegameDisclaimer'>
|
||||
showWegameFooter
|
||||
? `
|
||||
<div class='footer wegameDisclaimer'>
|
||||
<div class="disclaimer">
|
||||
健康游戏忠告
|
||||
<br>
|
||||
@ -142,46 +164,46 @@ export class MainMenuState extends GameState {
|
||||
</div>
|
||||
`
|
||||
: `
|
||||
<div class="footer ${G_CHINA_VERSION ? "china" : ""} ">
|
||||
|
||||
${
|
||||
G_CHINA_VERSION
|
||||
? ""
|
||||
: `
|
||||
<a class="githubLink boxLink" target="_blank">
|
||||
${T.mainMenu.openSourceHint}
|
||||
<span class="thirdpartyLogo githubLogo"></span>
|
||||
</a>`
|
||||
}
|
||||
<div class="footer ${showExternalLinks ? "" : "noLinks"} ">
|
||||
${
|
||||
showExternalLinks
|
||||
? `
|
||||
<a class="githubLink boxLink" target="_blank">
|
||||
${T.mainMenu.openSourceHint}
|
||||
<span class="thirdpartyLogo githubLogo"></span>
|
||||
</a>`
|
||||
: ""
|
||||
}
|
||||
|
||||
<a class="discordLink boxLink" target="_blank">
|
||||
${T.mainMenu.discordLink}
|
||||
<span class="thirdpartyLogo discordLogo"></span>
|
||||
</a>
|
||||
${
|
||||
showDiscordLink
|
||||
? `<a class="discordLink boxLink" target="_blank">
|
||||
|
||||
<div class="sidelinks">
|
||||
${G_CHINA_VERSION ? "" : `<a class="redditLink">${T.mainMenu.subreddit}</a>`}
|
||||
${T.mainMenu.discordLink}
|
||||
<span class="thirdpartyLogo discordLogo"></span>
|
||||
</a>`
|
||||
: ""
|
||||
}
|
||||
|
||||
${G_CHINA_VERSION ? "" : `<a class="changelog">${T.changelog.title}</a>`}
|
||||
<div class="sidelinks">
|
||||
${showExternalLinks ? `<a class="redditLink">${T.mainMenu.subreddit}</a>` : ""}
|
||||
|
||||
${G_CHINA_VERSION ? "" : `<a class="helpTranslate">${T.mainMenu.helpTranslate}</a>`}
|
||||
${showExternalLinks ? `<a class="changelog">${T.changelog.title}</a>` : ""}
|
||||
|
||||
${showExternalLinks ? `<a class="helpTranslate">${T.mainMenu.helpTranslate}</a>` : ""}
|
||||
</div>
|
||||
<div class="author">${T.mainMenu.madeBy.replace(
|
||||
"<author-link>",
|
||||
'<a class="producerLink" target="_blank">Tobias Springer</a>'
|
||||
)}</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="author">${T.mainMenu.madeBy.replace(
|
||||
"<author-link>",
|
||||
'<a class="producerLink" target="_blank">Tobias Springer</a>'
|
||||
)}</div>
|
||||
</div>
|
||||
|
||||
${
|
||||
G_IS_STANDALONE
|
||||
? ""
|
||||
: `
|
||||
<iframe id="crosspromo" src="https://crosspromo.tobspr.io?src=shapez_web"></iframe>
|
||||
|
||||
`
|
||||
}
|
||||
${
|
||||
showCrosspromo
|
||||
? `<iframe id="crosspromo" src="https://crosspromo.tobspr.io?src=shapez_web"></iframe>`
|
||||
: ""
|
||||
}
|
||||
`
|
||||
}
|
||||
`;
|
||||
@ -270,8 +292,6 @@ export class MainMenuState extends GameState {
|
||||
);
|
||||
}
|
||||
|
||||
const qs = this.htmlElement.querySelector.bind(this.htmlElement);
|
||||
|
||||
if (G_IS_DEV && globalConfig.debug.testPuzzleMode) {
|
||||
this.onPuzzleModeButtonClicked(true);
|
||||
return;
|
||||
@ -295,76 +315,38 @@ export class MainMenuState extends GameState {
|
||||
}
|
||||
});
|
||||
|
||||
this.trackClicks(qs(".settingsButton"), this.onSettingsButtonClicked);
|
||||
const clickHandling = {
|
||||
".settingsButton": this.onSettingsButtonClicked,
|
||||
".languageChoose": this.onLanguageChooseClicked,
|
||||
".redditLink": this.onRedditClicked,
|
||||
".changelog": this.onChangelogClicked,
|
||||
".helpTranslate": this.onTranslationHelpLinkClicked,
|
||||
".exitAppButton": this.onExitAppButtonClicked,
|
||||
".steamLink": this.onSteamLinkClicked,
|
||||
".discordLink": () => {
|
||||
this.app.analytics.trackUiClick("main_menu_link_discord");
|
||||
this.app.platformWrapper.openExternalLink(THIRDPARTY_URLS.discord);
|
||||
},
|
||||
".githubLink": () => {
|
||||
this.app.analytics.trackUiClick("main_menu_link_github");
|
||||
this.app.platformWrapper.openExternalLink(THIRDPARTY_URLS.github);
|
||||
},
|
||||
".producerLink": () => this.app.platformWrapper.openExternalLink("https://tobspr.io"),
|
||||
".puzzleDlcPlayButton": this.onPuzzleModeButtonClicked,
|
||||
".puzzleDlcGetButton": this.onPuzzleWishlistButtonClicked,
|
||||
".wegameDisclaimer > .rating": this.onWegameRatingClicked,
|
||||
};
|
||||
|
||||
if (!G_CHINA_VERSION && !G_WEGAME_VERSION) {
|
||||
this.trackClicks(qs(".languageChoose"), this.onLanguageChooseClicked);
|
||||
this.trackClicks(qs(".redditLink"), this.onRedditClicked);
|
||||
this.trackClicks(qs(".changelog"), this.onChangelogClicked);
|
||||
this.trackClicks(qs(".helpTranslate"), this.onTranslationHelpLinkClicked);
|
||||
}
|
||||
|
||||
if (G_IS_STANDALONE) {
|
||||
this.trackClicks(qs(".exitAppButton"), this.onExitAppButtonClicked);
|
||||
for (const key in clickHandling) {
|
||||
const handler = clickHandling[key];
|
||||
const element = this.htmlElement.querySelector(key);
|
||||
if (element) {
|
||||
this.trackClicks(element, handler, { preventClick: true });
|
||||
}
|
||||
}
|
||||
|
||||
this.renderMainMenu();
|
||||
this.renderSavegames();
|
||||
|
||||
const steamLink = this.htmlElement.querySelector(".steamLink");
|
||||
if (steamLink) {
|
||||
this.trackClicks(steamLink, () => this.onSteamLinkClicked(), { preventClick: true });
|
||||
}
|
||||
|
||||
const discordLink = this.htmlElement.querySelector(".discordLink");
|
||||
if (discordLink) {
|
||||
this.trackClicks(
|
||||
discordLink,
|
||||
() => {
|
||||
this.app.analytics.trackUiClick("main_menu_link_discord");
|
||||
this.app.platformWrapper.openExternalLink(THIRDPARTY_URLS.discord);
|
||||
},
|
||||
{ preventClick: true }
|
||||
);
|
||||
}
|
||||
|
||||
const githubLink = this.htmlElement.querySelector(".githubLink");
|
||||
if (githubLink) {
|
||||
this.trackClicks(
|
||||
githubLink,
|
||||
() => {
|
||||
this.app.analytics.trackUiClick("main_menu_link_github");
|
||||
this.app.platformWrapper.openExternalLink(THIRDPARTY_URLS.github);
|
||||
},
|
||||
{ preventClick: true }
|
||||
);
|
||||
}
|
||||
|
||||
const producerLink = this.htmlElement.querySelector(".producerLink");
|
||||
if (producerLink) {
|
||||
this.trackClicks(
|
||||
producerLink,
|
||||
() => this.app.platformWrapper.openExternalLink("https://tobspr.io"),
|
||||
{
|
||||
preventClick: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const puzzleModeButton = qs(".puzzleDlcPlayButton");
|
||||
if (puzzleModeButton) {
|
||||
this.trackClicks(puzzleModeButton, () => this.onPuzzleModeButtonClicked());
|
||||
}
|
||||
|
||||
const puzzleWishlistButton = qs(".puzzleDlcGetButton");
|
||||
if (puzzleWishlistButton) {
|
||||
this.trackClicks(puzzleWishlistButton, () => this.onPuzzleWishlistButtonClicked());
|
||||
}
|
||||
|
||||
const wegameRating = qs(".wegameDisclaimer > .rating");
|
||||
if (wegameRating) {
|
||||
this.trackClicks(wegameRating, () => this.onWegameRatingClicked());
|
||||
}
|
||||
}
|
||||
|
||||
renderMainMenu() {
|
||||
@ -535,9 +517,12 @@ export class MainMenuState extends GameState {
|
||||
downloadButton.classList.add("styledButton", "downloadGame");
|
||||
elem.appendChild(downloadButton);
|
||||
|
||||
const renameButton = document.createElement("button");
|
||||
renameButton.classList.add("styledButton", "renameGame");
|
||||
name.appendChild(renameButton);
|
||||
if (!G_WEGAME_VERSION) {
|
||||
const renameButton = document.createElement("button");
|
||||
renameButton.classList.add("styledButton", "renameGame");
|
||||
name.appendChild(renameButton);
|
||||
this.trackClicks(renameButton, () => this.requestRenameSavegame(games[i]));
|
||||
}
|
||||
|
||||
const resumeButton = document.createElement("button");
|
||||
resumeButton.classList.add("styledButton", "resumeGame");
|
||||
@ -546,7 +531,6 @@ export class MainMenuState extends GameState {
|
||||
this.trackClicks(deleteButton, () => this.deleteGame(games[i]));
|
||||
this.trackClicks(downloadButton, () => this.downloadGame(games[i]));
|
||||
this.trackClicks(resumeButton, () => this.resumeGame(games[i]));
|
||||
this.trackClicks(renameButton, () => this.requestRenameSavegame(games[i]));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -701,16 +685,12 @@ export class MainMenuState extends GameState {
|
||||
|
||||
onWegameRatingClicked() {
|
||||
this.dialogs.showInfo(
|
||||
"",
|
||||
"提示说明",
|
||||
`
|
||||
1)本游戏是一款休闲建造类单机游戏,适用于年满8周岁及以上的用户。<br>
|
||||
2)本游戏模拟简单的生产流水线,剧情简单且积极向上,没有基于真实
|
||||
历史和现实事件的改编内容。游戏玩法为摆放简单的部件,完成生产目标。
|
||||
游戏为单机作品,没有基于文字和语音的陌生人社交系统。<br>
|
||||
3)游戏中有用户实名认证系统,认证为未成年人的用户将接受以下管理:
|
||||
游戏为买断制,不存在后续充值付费内容。未成年人用户每日22点到次日
|
||||
8点不得使用,法定节假日每天不得使用超过3小时,其它时间每天使用游
|
||||
戏不得超过1.5小时。
|
||||
1)本游戏是一款休闲建造类单机游戏,画面简洁而乐趣充足。适用于年满8周岁及以上的用户,建议未成年人在家长监护下使用游戏产品。<br>
|
||||
2)本游戏模拟简单的生产流水线,剧情简单且积极向上,没有基于真实历史和现实事件的改编内容。游戏玩法为摆放简单的部件,完成生产目标。游戏为单机作品,没有基于文字和语音的陌生人社交系统。<br>
|
||||
3)本游戏中有用户实名认证系统,认证为未成年人的用户将接受以下管理:未满8周岁的用户不能付费;8周岁以上未满16周岁的未成年人用户,单次充值金额不得超过50元人民币,每月充值金额累计不得超过200元人民币;16周岁以上的未成年人用户,单次充值金额不得超过100元人民币,每月充值金额累计不得超过400元人民币。未成年人用户每日22点到次日8点不得使用,法定节假日每天不得使用超过3小时,其他时间每天不得使用超过1.5小时。<br>
|
||||
4)游戏功能说明:一款关于传送带自动化生产特定形状产品的工厂流水线模拟游戏,画面简洁而乐趣充足,可以让玩家在轻松愉快的氛围下获得各种游戏乐趣,体验完成目标的成就感。游戏没有失败功能,自动存档,不存在较强的挫折体验。
|
||||
`
|
||||
);
|
||||
}
|
||||
@ -726,11 +706,14 @@ export class MainMenuState extends GameState {
|
||||
});
|
||||
|
||||
const savegame = this.app.savegameMgr.getSavegameById(latestInternalId);
|
||||
savegame.readAsync().then(() => {
|
||||
this.moveToState("InGameState", {
|
||||
savegame,
|
||||
savegame
|
||||
.readAsync()
|
||||
.then(() => this.app.adProvider.showVideoAd())
|
||||
.then(() => {
|
||||
this.moveToState("InGameState", {
|
||||
savegame,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
onLeave() {
|
||||
|
||||
@ -14,17 +14,30 @@ const navigation = {
|
||||
categories: ["official", "top-rated", "trending", "trending-weekly", "new"],
|
||||
difficulties: ["easy", "medium", "hard"],
|
||||
account: ["mine", "completed"],
|
||||
search: ["search"],
|
||||
};
|
||||
|
||||
const logger = createLogger("puzzle-menu");
|
||||
|
||||
let lastCategory = "official";
|
||||
|
||||
let lastSearchOptions = {
|
||||
searchTerm: "",
|
||||
difficulty: "any",
|
||||
duration: "any",
|
||||
includeCompleted: false,
|
||||
};
|
||||
|
||||
export class PuzzleMenuState extends TextualGameState {
|
||||
constructor() {
|
||||
super("PuzzleMenuState");
|
||||
this.loading = false;
|
||||
this.activeCategory = "";
|
||||
|
||||
/**
|
||||
* @type {Array<import("../savegame/savegame_typedefs").PuzzleMetadata>}
|
||||
*/
|
||||
this.puzzles = [];
|
||||
}
|
||||
|
||||
getThemeMusic() {
|
||||
@ -100,13 +113,23 @@ export class PuzzleMenuState extends TextualGameState {
|
||||
activeCategory.classList.remove("active");
|
||||
}
|
||||
|
||||
this.htmlElement.querySelector(`[data-category="${category}"]`).classList.add("active");
|
||||
const categoryElement = this.htmlElement.querySelector(`[data-category="${category}"]`);
|
||||
if (categoryElement) {
|
||||
categoryElement.classList.add("active");
|
||||
}
|
||||
|
||||
const container = this.htmlElement.querySelector("#mainContainer");
|
||||
while (container.firstChild) {
|
||||
container.removeChild(container.firstChild);
|
||||
}
|
||||
|
||||
if (category === "search") {
|
||||
this.loading = false;
|
||||
|
||||
this.startSearch();
|
||||
return;
|
||||
}
|
||||
|
||||
const loadingElement = document.createElement("div");
|
||||
loadingElement.classList.add("loader");
|
||||
loadingElement.innerText = T.global.loading + "...";
|
||||
@ -161,23 +184,148 @@ export class PuzzleMenuState extends TextualGameState {
|
||||
}
|
||||
|
||||
const children = navigation[rootCategory];
|
||||
for (const category of children) {
|
||||
const button = document.createElement("button");
|
||||
button.setAttribute("data-category", category);
|
||||
button.classList.add("styledButton", "category", "child");
|
||||
button.innerText = T.puzzleMenu.categories[category];
|
||||
this.trackClicks(button, () => this.selectCategory(category));
|
||||
subContainer.appendChild(button);
|
||||
if (children.length > 1) {
|
||||
for (const category of children) {
|
||||
const button = document.createElement("button");
|
||||
button.setAttribute("data-category", category);
|
||||
button.classList.add("styledButton", "category", "child");
|
||||
button.innerText = T.puzzleMenu.categories[category];
|
||||
this.trackClicks(button, () => this.selectCategory(category));
|
||||
subContainer.appendChild(button);
|
||||
}
|
||||
}
|
||||
|
||||
if (rootCategory === "search") {
|
||||
this.renderSearchForm(subContainer);
|
||||
}
|
||||
|
||||
this.selectCategory(subCategory);
|
||||
}
|
||||
|
||||
renderSearchForm(parent) {
|
||||
const container = document.createElement("form");
|
||||
container.classList.add("searchForm");
|
||||
|
||||
// Search
|
||||
const searchField = document.createElement("input");
|
||||
searchField.value = lastSearchOptions.searchTerm;
|
||||
searchField.classList.add("search");
|
||||
searchField.setAttribute("type", "text");
|
||||
searchField.setAttribute("placeholder", T.puzzleMenu.search.placeholder);
|
||||
searchField.addEventListener("input", () => {
|
||||
lastSearchOptions.searchTerm = searchField.value.trim();
|
||||
});
|
||||
container.appendChild(searchField);
|
||||
|
||||
// Difficulty
|
||||
const difficultyFilter = document.createElement("select");
|
||||
for (const difficulty of ["any", "easy", "medium", "hard"]) {
|
||||
const option = document.createElement("option");
|
||||
option.value = difficulty;
|
||||
option.innerText = T.puzzleMenu.search.difficulties[difficulty];
|
||||
if (option.value === lastSearchOptions.difficulty) {
|
||||
option.setAttribute("selected", "selected");
|
||||
}
|
||||
difficultyFilter.appendChild(option);
|
||||
}
|
||||
difficultyFilter.addEventListener("change", () => {
|
||||
const option = difficultyFilter.value;
|
||||
lastSearchOptions.difficulty = option;
|
||||
});
|
||||
container.appendChild(difficultyFilter);
|
||||
|
||||
// Duration
|
||||
const durationFilter = document.createElement("select");
|
||||
for (const duration of ["any", "short", "medium", "long"]) {
|
||||
const option = document.createElement("option");
|
||||
option.value = duration;
|
||||
option.innerText = T.puzzleMenu.search.durations[duration];
|
||||
if (option.value === lastSearchOptions.duration) {
|
||||
option.setAttribute("selected", "selected");
|
||||
}
|
||||
durationFilter.appendChild(option);
|
||||
}
|
||||
durationFilter.addEventListener("change", () => {
|
||||
const option = durationFilter.value;
|
||||
lastSearchOptions.duration = option;
|
||||
});
|
||||
container.appendChild(durationFilter);
|
||||
|
||||
// Include completed
|
||||
const labelCompleted = document.createElement("label");
|
||||
labelCompleted.classList.add("filterCompleted");
|
||||
|
||||
const inputCompleted = document.createElement("input");
|
||||
inputCompleted.setAttribute("type", "checkbox");
|
||||
if (lastSearchOptions.includeCompleted) {
|
||||
inputCompleted.setAttribute("checked", "checked");
|
||||
}
|
||||
inputCompleted.addEventListener("change", () => {
|
||||
lastSearchOptions.includeCompleted = inputCompleted.checked;
|
||||
});
|
||||
|
||||
labelCompleted.appendChild(inputCompleted);
|
||||
|
||||
const text = document.createTextNode(T.puzzleMenu.search.includeCompleted);
|
||||
labelCompleted.appendChild(text);
|
||||
|
||||
container.appendChild(labelCompleted);
|
||||
|
||||
// Submit
|
||||
const submitButton = document.createElement("button");
|
||||
submitButton.classList.add("styledButton");
|
||||
submitButton.setAttribute("type", "submit");
|
||||
submitButton.innerText = T.puzzleMenu.search.action;
|
||||
container.appendChild(submitButton);
|
||||
|
||||
container.addEventListener("submit", event => {
|
||||
event.preventDefault();
|
||||
console.log("Search:", searchField.value.trim());
|
||||
this.startSearch();
|
||||
});
|
||||
|
||||
parent.appendChild(container);
|
||||
}
|
||||
|
||||
startSearch() {
|
||||
if (this.loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
|
||||
const container = this.htmlElement.querySelector("#mainContainer");
|
||||
while (container.firstChild) {
|
||||
container.removeChild(container.firstChild);
|
||||
}
|
||||
|
||||
const loadingElement = document.createElement("div");
|
||||
loadingElement.classList.add("loader");
|
||||
loadingElement.innerText = T.global.loading + "...";
|
||||
container.appendChild(loadingElement);
|
||||
|
||||
this.asyncChannel
|
||||
.watch(this.app.clientApi.apiSearchPuzzles(lastSearchOptions))
|
||||
.then(
|
||||
puzzles => this.renderPuzzles(puzzles),
|
||||
error => {
|
||||
this.dialogs.showWarning(
|
||||
T.dialogs.puzzleLoadFailed.title,
|
||||
T.dialogs.puzzleLoadFailed.desc + " " + error
|
||||
);
|
||||
this.renderPuzzles([]);
|
||||
}
|
||||
)
|
||||
.then(() => (this.loading = false));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {import("../savegame/savegame_typedefs").PuzzleMetadata[]} puzzles
|
||||
*/
|
||||
renderPuzzles(puzzles) {
|
||||
this.puzzles = puzzles;
|
||||
|
||||
const container = this.htmlElement.querySelector("#mainContainer");
|
||||
while (container.firstChild) {
|
||||
container.removeChild(container.firstChild);
|
||||
@ -224,15 +372,15 @@ export class PuzzleMenuState extends TextualGameState {
|
||||
difficulty.innerText = completionPercentage + "%";
|
||||
stats.appendChild(difficulty);
|
||||
|
||||
if (completionPercentage < 40) {
|
||||
difficulty.classList.add("stage--hard");
|
||||
difficulty.innerText = T.puzzleMenu.difficulties.hard;
|
||||
} else if (completionPercentage < 80) {
|
||||
difficulty.classList.add("stage--medium");
|
||||
difficulty.innerText = T.puzzleMenu.difficulties.medium;
|
||||
} else {
|
||||
if (puzzle.difficulty < 0.2) {
|
||||
difficulty.classList.add("stage--easy");
|
||||
difficulty.innerText = T.puzzleMenu.difficulties.easy;
|
||||
} else if (puzzle.difficulty > 0.6) {
|
||||
difficulty.classList.add("stage--hard");
|
||||
difficulty.innerText = T.puzzleMenu.difficulties.hard;
|
||||
} else {
|
||||
difficulty.classList.add("stage--medium");
|
||||
difficulty.innerText = T.puzzleMenu.difficulties.medium;
|
||||
}
|
||||
}
|
||||
|
||||
@ -276,7 +424,7 @@ export class PuzzleMenuState extends TextualGameState {
|
||||
|
||||
container.appendChild(elem);
|
||||
|
||||
this.trackClicks(elem, () => this.playPuzzle(puzzle));
|
||||
this.trackClicks(elem, () => this.playPuzzle(puzzle.id));
|
||||
}
|
||||
|
||||
if (puzzles.length === 0) {
|
||||
@ -329,20 +477,26 @@ export class PuzzleMenuState extends TextualGameState {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {import("../savegame/savegame_typedefs").PuzzleMetadata} puzzle
|
||||
* @param {number} puzzleId
|
||||
* @param {Array<number>=} nextPuzzles
|
||||
*/
|
||||
playPuzzle(puzzle) {
|
||||
playPuzzle(puzzleId, nextPuzzles) {
|
||||
const closeLoading = this.dialogs.showLoadingDialog();
|
||||
|
||||
this.app.clientApi.apiDownloadPuzzle(puzzle.id).then(
|
||||
this.asyncChannel.watch(this.app.clientApi.apiDownloadPuzzle(puzzleId)).then(
|
||||
puzzleData => {
|
||||
closeLoading();
|
||||
logger.log("Got puzzle:", puzzleData);
|
||||
this.startLoadedPuzzle(puzzleData);
|
||||
|
||||
nextPuzzles =
|
||||
nextPuzzles || this.puzzles.filter(puzzle => !puzzle.completed).map(puzzle => puzzle.id);
|
||||
nextPuzzles = nextPuzzles.filter(id => id !== puzzleId);
|
||||
|
||||
logger.log("Got puzzle:", puzzleData, "next puzzles:", nextPuzzles);
|
||||
this.startLoadedPuzzle(puzzleData, nextPuzzles);
|
||||
},
|
||||
err => {
|
||||
closeLoading();
|
||||
logger.error("Failed to download puzzle", puzzle.id, ":", err);
|
||||
logger.error("Failed to download puzzle", puzzleId, ":", err);
|
||||
this.dialogs.showWarning(
|
||||
T.dialogs.puzzleDownloadError.title,
|
||||
T.dialogs.puzzleDownloadError.desc + " " + err
|
||||
@ -355,18 +509,24 @@ export class PuzzleMenuState extends TextualGameState {
|
||||
*
|
||||
* @param {import("../savegame/savegame_typedefs").PuzzleFullData} puzzle
|
||||
*/
|
||||
startLoadedPuzzle(puzzle) {
|
||||
const savegame = this.createEmptySavegame();
|
||||
startLoadedPuzzle(puzzle, nextPuzzles) {
|
||||
const savegame = Savegame.createPuzzleSavegame(this.app);
|
||||
this.moveToState("InGameState", {
|
||||
gameModeId: enumGameModeIds.puzzlePlay,
|
||||
gameModeParameters: {
|
||||
puzzle,
|
||||
nextPuzzles,
|
||||
},
|
||||
savegame,
|
||||
});
|
||||
}
|
||||
|
||||
onEnter(payload) {
|
||||
if (payload.continueQueue) {
|
||||
logger.log("Continuing puzzle queue:", payload);
|
||||
this.playPuzzle(payload.continueQueue[0], payload.continueQueue.slice(1));
|
||||
}
|
||||
|
||||
// Find old category
|
||||
let rootCategory = "categories";
|
||||
for (const [id, children] of Object.entries(navigation)) {
|
||||
@ -391,26 +551,13 @@ export class PuzzleMenuState extends TextualGameState {
|
||||
this.trackClicks(this.htmlElement.querySelector("button.loadPuzzle"), () => this.loadPuzzle());
|
||||
}
|
||||
|
||||
createEmptySavegame() {
|
||||
return new Savegame(this.app, {
|
||||
internalId: "puzzle",
|
||||
metaDataRef: {
|
||||
internalId: "puzzle",
|
||||
lastUpdate: 0,
|
||||
version: 0,
|
||||
level: 0,
|
||||
name: "puzzle",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
loadPuzzle() {
|
||||
const shortKeyInput = new FormElementInput({
|
||||
id: "shortKey",
|
||||
label: null,
|
||||
placeholder: "",
|
||||
defaultValue: "",
|
||||
validator: val => ShapeDefinition.isValidShortKey(val),
|
||||
validator: val => ShapeDefinition.isValidShortKey(val) || val.startsWith("/"),
|
||||
});
|
||||
|
||||
const dialog = new DialogWithForm({
|
||||
@ -423,9 +570,16 @@ export class PuzzleMenuState extends TextualGameState {
|
||||
this.dialogs.internalShowDialog(dialog);
|
||||
|
||||
dialog.buttonSignals.ok.add(() => {
|
||||
const searchTerm = shortKeyInput.getValue();
|
||||
|
||||
if (searchTerm === "/apikey") {
|
||||
alert("Your api key is: " + this.app.clientApi.token);
|
||||
return;
|
||||
}
|
||||
|
||||
const closeLoading = this.dialogs.showLoadingDialog();
|
||||
|
||||
this.app.clientApi.apiDownloadPuzzleByKey(shortKeyInput.getValue()).then(
|
||||
this.app.clientApi.apiDownloadPuzzleByKey(searchTerm).then(
|
||||
puzzle => {
|
||||
closeLoading();
|
||||
this.startLoadedPuzzle(puzzle);
|
||||
@ -452,7 +606,7 @@ export class PuzzleMenuState extends TextualGameState {
|
||||
return;
|
||||
}
|
||||
|
||||
const savegame = this.createEmptySavegame();
|
||||
const savegame = Savegame.createPuzzleSavegame(this.app);
|
||||
this.moveToState("InGameState", {
|
||||
gameModeId: enumGameModeIds.puzzleEdit,
|
||||
gameModeParameters: {},
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { THIRDPARTY_URLS } from "../core/config";
|
||||
import { TextualGameState } from "../core/textual_game_state";
|
||||
import { formatSecondsToTimeAgo } from "../core/utils";
|
||||
import { allApplicationSettings, enumCategories } from "../profile/application_settings";
|
||||
@ -34,6 +35,8 @@ export class SettingsState extends TextualGameState {
|
||||
? ""
|
||||
: `
|
||||
<button class="styledButton about">${T.about.title}</button>
|
||||
<button class="styledButton privacy">Privacy Policy</button>
|
||||
|
||||
`
|
||||
}
|
||||
<div class="versionbar">
|
||||
@ -109,6 +112,9 @@ export class SettingsState extends TextualGameState {
|
||||
this.trackClicks(this.htmlElement.querySelector(".about"), this.onAboutClicked, {
|
||||
preventDefault: false,
|
||||
});
|
||||
this.trackClicks(this.htmlElement.querySelector(".privacy"), this.onPrivacyClicked, {
|
||||
preventDefault: false,
|
||||
});
|
||||
}
|
||||
|
||||
const keybindingsButton = this.htmlElement.querySelector(".editKeybindings");
|
||||
@ -180,6 +186,10 @@ export class SettingsState extends TextualGameState {
|
||||
this.moveToStateAddGoBack("AboutState");
|
||||
}
|
||||
|
||||
onPrivacyClicked() {
|
||||
this.app.platformWrapper.openExternalLink(THIRDPARTY_URLS.privacyPolicy);
|
||||
}
|
||||
|
||||
onKeybindingsClicked() {
|
||||
this.moveToStateAddGoBack("KeybindingsState");
|
||||
}
|
||||
|
||||
@ -12,7 +12,7 @@ export class WegameSplashState extends GameState {
|
||||
<div>抵制不良游戏,拒绝盗版游戏。</div>
|
||||
<div>注意自我保护,谨防受骗上当。</div>
|
||||
<div>适度游戏益脑,沉迷游戏伤身。</div>
|
||||
<div>适度游戏益脑,沉迷游戏伤身。</div>
|
||||
<div>合理安排时间,享受健康生活。</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@ -155,6 +155,23 @@ puzzleMenu:
|
||||
account: My Puzzles
|
||||
search: Search
|
||||
|
||||
search:
|
||||
action: Search
|
||||
placeholder: Enter a puzzle or author name
|
||||
includeCompleted: Include Completed
|
||||
|
||||
difficulties:
|
||||
any: Any Difficulty
|
||||
easy: Easy
|
||||
medium: Medium
|
||||
hard: Hard
|
||||
|
||||
durations:
|
||||
any: Any Duration
|
||||
short: Short (< 2 min)
|
||||
medium: Normal
|
||||
long: Long (> 10 min)
|
||||
|
||||
difficulties:
|
||||
easy: Easy
|
||||
medium: Medium
|
||||
@ -671,6 +688,7 @@ ingame:
|
||||
|
||||
continueBtn: Keep Playing
|
||||
menuBtn: Menu
|
||||
nextPuzzle: Next Puzzle
|
||||
|
||||
puzzleMetadata:
|
||||
author: Author
|
||||
@ -1094,7 +1112,7 @@ settings:
|
||||
staging: Staging
|
||||
prod: Production
|
||||
buildDate: Built <at-date>
|
||||
|
||||
tickrateHz: <amount> Hz
|
||||
rangeSliderPercentage: <amount> %
|
||||
|
||||
labels:
|
||||
@ -1377,6 +1395,8 @@ keybindings:
|
||||
placeMultiple: Stay in placement mode
|
||||
placeInverse: Invert automatic belt orientation
|
||||
|
||||
showShapeTooltip: Show shape output tooltip
|
||||
|
||||
about:
|
||||
title: About this Game
|
||||
body: >-
|
||||
|
||||
902
translations/base-zh-CN-ISBN.yaml
Normal file
@ -0,0 +1,902 @@
|
||||
steamPage:
|
||||
shortText: “唯一能限制您的,只有您的想象力!” 《异形工厂》(Shapez.io)
|
||||
是一款在无限拓展的地图上,通过建造各类工厂设施,来自动化生产与组合出愈加复杂图形的游戏。
|
||||
discordLinkShort: 官方 Discord 服务器
|
||||
intro: |-
|
||||
“奇形怪状,放飞想象!”
|
||||
“自动生产,尽情创造!”
|
||||
《异形工厂》(Shapez.io)是一款能让您尽情发挥创造力,充分享受思维乐趣的IO游戏。
|
||||
游戏很轻松,只需建造工厂,布好设施,无需操作即能自动创造出各种各样的几何图形。
|
||||
挑战很烧脑,随着等级提升,需要创造的图形将会越来越复杂,同时您还需要在无限扩展的地图中持续扩建优化您的工厂。
|
||||
以为这就是全部了吗? 不!图形的生产需求将会指数性增长,持续的扩大规模和熵增带来的无序,将会是令人头痛的问题!
|
||||
这还不是全部! 一开始我们创造了图形,然后我们需要学会提取和混合来让它们五颜六色。
|
||||
然后,还有吗? 当然,唯有思维,方能无限。
|
||||
|
||||
欢迎免费体验试玩版:“让您的想象力插上翅膀!”
|
||||
和最聪明的玩家一起挑战,请访问 Steam 游戏商城购买《异形工厂》(Shapez.io)的完整版,
|
||||
what_others_say: 来看看玩家们对《异形工厂》(Shapez.io)的评价
|
||||
nothernlion_comment: 非常棒的有游戏,我的游戏过程充满乐趣,不觉时间飞逝。
|
||||
notch_comment: 哦,天哪!我真得该去睡了!但我想我刚刚搞定如何在游戏里面制造一台电脑出来。
|
||||
steam_review_comment: 这是一个不知不觉偷走你时间,但你并不会想要追回的游戏。非常烧脑的挑战,让我这样的完美主义者停不下来,总是希望可以再高效一些。
|
||||
|
||||
global:
|
||||
loading: 加载中
|
||||
error: 错误
|
||||
thousandsDivider: ","
|
||||
decimalSeparator: .
|
||||
suffix:
|
||||
thousands: 千
|
||||
millions: 百万
|
||||
billions: 亿万
|
||||
trillions: 兆
|
||||
infinite: 无限
|
||||
time:
|
||||
oneSecondAgo: 1秒前
|
||||
xSecondsAgo: <x>秒前
|
||||
oneMinuteAgo: 1分钟前
|
||||
xMinutesAgo: <x>分钟前
|
||||
oneHourAgo: 1小时前
|
||||
xHoursAgo: <x>小时前
|
||||
oneDayAgo: 1天前
|
||||
xDaysAgo: <x>天前
|
||||
secondsShort: <seconds>秒
|
||||
minutesAndSecondsShort: <minutes>分 <seconds>秒
|
||||
hoursAndMinutesShort: <hours>时 <minutes>分
|
||||
xMinutes: <x>分钟
|
||||
keys:
|
||||
tab: TAB键
|
||||
control: CTRL键
|
||||
alt: ALT键
|
||||
escape: ESC键
|
||||
shift: SHIFT键
|
||||
space: 空格键
|
||||
demoBanners:
|
||||
title: 试玩版
|
||||
intro: 购买完整版以解锁所有游戏内容!
|
||||
mainMenu:
|
||||
play: 开始游戏
|
||||
changelog: 更新日志
|
||||
importSavegame: 读取存档
|
||||
openSourceHint: 本游戏已开源!
|
||||
discordLink: 官方Discord服务器
|
||||
helpTranslate: 帮助我们翻译!
|
||||
browserWarning: 很抱歉, 本游戏在当前浏览器上可能运行缓慢! 使用 Chrome 或者购买完整版以得到更好的体验。
|
||||
savegameLevel: 第<x>关
|
||||
savegameLevelUnknown: 未知关卡
|
||||
continue: 继续游戏
|
||||
newGame: 新游戏
|
||||
madeBy: 作者:<author-link>
|
||||
subreddit: Reddit
|
||||
savegameUnnamed: 存档未命名
|
||||
dialogs:
|
||||
buttons:
|
||||
ok: 确认
|
||||
delete: 删除
|
||||
cancel: 取消
|
||||
later: 以后
|
||||
restart: 重新开始
|
||||
reset: 重置
|
||||
getStandalone: 获取完整版
|
||||
deleteGame: 我没疯!我知道我在做什么!
|
||||
viewUpdate: 查看更新
|
||||
showUpgrades: 显示设施升级
|
||||
showKeybindings: 显示按键设置
|
||||
importSavegameError:
|
||||
title: 读取错误
|
||||
text: 未能读取您的存档:
|
||||
importSavegameSuccess:
|
||||
title: 读取成功
|
||||
text: 存档被成功读取
|
||||
gameLoadFailure:
|
||||
title: 存档损坏
|
||||
text: 未能读取您的存档:
|
||||
confirmSavegameDelete:
|
||||
title: 确认删除
|
||||
text: 您确定要删除这个游戏吗?<br><br> '<savegameName>' 等级 <savegameLevel><br><br> 该操作无法回退!
|
||||
savegameDeletionError:
|
||||
title: 删除失败
|
||||
text: 未能删除您的存档
|
||||
restartRequired:
|
||||
title: 需要重启游戏
|
||||
text: 您需要重启游戏以应用变更的设置。
|
||||
editKeybinding:
|
||||
title: 更改按键设定
|
||||
desc: 请按下您想要使用的按键以设定,或者按下 ESC 键来取消设定。
|
||||
resetKeybindingsConfirmation:
|
||||
title: 重置按键设定
|
||||
desc: 您将要重置所有按键设定,请确认。
|
||||
keybindingsResetOk:
|
||||
title: 重置按键设定
|
||||
desc: 成功重置所有按键设定!
|
||||
featureRestriction:
|
||||
title: 试玩版
|
||||
desc: 您尝试使用了<feature>一项功能。该功能在试玩版中不可用。请考虑购买完整版以获得更好的体验。
|
||||
oneSavegameLimit:
|
||||
title: 存档数量限制
|
||||
desc: 试玩版中只能保存一份存档。请删除旧存档或者购买完整版!
|
||||
updateSummary:
|
||||
title: 新内容更新啦!
|
||||
desc: "以下为游戏最新更新内容:"
|
||||
upgradesIntroduction:
|
||||
title: 解锁升级
|
||||
desc: <strong>您生产过的所有图形都能被用来解锁升级。</strong> 所以不要销毁您之前建造的工厂! 注意:升级菜单在屏幕右上角。
|
||||
massDeleteConfirm:
|
||||
title: 确认删除
|
||||
desc: 您将要删除很多设施,准确来说有<count>种! 您确定要这么做吗?
|
||||
blueprintsNotUnlocked:
|
||||
title: 尚未解锁
|
||||
desc: 您还没有解锁蓝图功能!通过第12关的挑战后可解锁蓝图。
|
||||
keybindingsIntroduction:
|
||||
title: 实用快捷键
|
||||
desc:
|
||||
"这个游戏有很多有用的快捷键设定。 以下是其中的一些介绍,记得在<strong>按键设置</strong>中查看其他按键设定!<br><br>
|
||||
<code class='keybinding'>CTRL键</code> + 拖动:选择区域以复制或删除。<br> <code
|
||||
class='keybinding'>SHIFT键</code>: 按住以放置多个同一种设施。<br> <code
|
||||
class='keybinding'>ALT键</code>: 反向放置传送带。<br>"
|
||||
createMarker:
|
||||
title: 创建地图标记
|
||||
desc:
|
||||
填写一个有意义的名称, 还可以同时包含一个形状的 <strong>短代码</strong> (您可以 <link>点击这里</link>
|
||||
生成短代码)
|
||||
titleEdit: 编辑地图标记
|
||||
markerDemoLimit:
|
||||
desc: 在试玩版中您只能创建两个地图标记。请获取完整版以创建更多标记。
|
||||
massCutConfirm:
|
||||
title: 确认剪切
|
||||
desc: 您将要剪切很多设施,准确来说有<count>种! 您确定要这么做吗?
|
||||
exportScreenshotWarning:
|
||||
title: 工厂截图
|
||||
desc: 您将要导出您整个工厂基地的截图。如果您已经建设了一个规模很大的基地,生成截图的过程将会很慢,且有可能导致游戏崩溃!
|
||||
massCutInsufficientConfirm:
|
||||
title: 确认剪切
|
||||
desc: 您没有足够的图形来粘贴这个区域!您确定要剪切吗?
|
||||
editSignal:
|
||||
title: 设置信号
|
||||
descItems: "选择一个预定义的项目:"
|
||||
descShortKey: ... 或者输入图形的 <strong>短代码</strong> (您可以 <link>点击这里</link> 生成短代码)
|
||||
renameSavegame:
|
||||
title: 重命名游戏存档
|
||||
desc: 您可以在此重命名游戏存档。
|
||||
tutorialVideoAvailable:
|
||||
title: 教程
|
||||
desc: 这个关卡有视频攻略! 您想查看这个视频攻略?
|
||||
tutorialVideoAvailableForeignLanguage:
|
||||
title: 教程
|
||||
desc: 这个关卡有英语版本的视频攻略! 您想查看这个视频攻略吗??
|
||||
ingame:
|
||||
keybindingsOverlay:
|
||||
moveMap: 移动地图
|
||||
selectBuildings: 选择区域
|
||||
stopPlacement: 停止放置
|
||||
rotateBuilding: 转动设施
|
||||
placeMultiple: 放置多个
|
||||
reverseOrientation: 反向放置
|
||||
disableAutoOrientation: 关闭自动定向
|
||||
toggleHud: 切换可视化界面
|
||||
placeBuilding: 放置设施
|
||||
createMarker: 创建地图标记
|
||||
delete: 销毁
|
||||
pasteLastBlueprint: 粘贴上一个蓝图
|
||||
lockBeltDirection: 启用传送带规划器
|
||||
plannerSwitchSide: 规划器换边
|
||||
cutSelection: 剪切
|
||||
copySelection: 复制
|
||||
clearSelection: 取消选择
|
||||
pipette: 吸取器
|
||||
switchLayers: 切换层
|
||||
buildingPlacement:
|
||||
cycleBuildingVariants: 按 <key> 键以选择设施的变型体。
|
||||
hotkeyLabel: "快捷键: <key>"
|
||||
infoTexts:
|
||||
speed: 速率
|
||||
range: 范围
|
||||
storage: 容量
|
||||
oneItemPerSecond: 1个/秒
|
||||
itemsPerSecond: <x>个/秒
|
||||
itemsPerSecondDouble: (2倍)
|
||||
tiles: <x>格
|
||||
levelCompleteNotification:
|
||||
levelTitle: 第<level>关
|
||||
completed: 完成
|
||||
unlockText: 解锁<reward>!
|
||||
buttonNextLevel: 下一关
|
||||
notifications:
|
||||
newUpgrade: 有新内容更新啦!
|
||||
gameSaved: 游戏已保存。
|
||||
freeplayLevelComplete: 第 <level>关 完成了!
|
||||
shop:
|
||||
title: 升级
|
||||
buttonUnlock: 升级
|
||||
tier: <x>级
|
||||
maximumLevel: 最高级(<currentMult>倍速率)
|
||||
statistics:
|
||||
title: 统计信息
|
||||
dataSources:
|
||||
stored:
|
||||
title: 已存储
|
||||
description: 所有图形已存储于中心。
|
||||
produced:
|
||||
title: 生产
|
||||
description: 所有图形已在工厂内生产,包括中间产物。
|
||||
delivered:
|
||||
title: 交付
|
||||
description: 图形已交付到中心基地。
|
||||
noShapesProduced: 您还没有生产任何图形。
|
||||
shapesDisplayUnits:
|
||||
second: <shapes> / 秒
|
||||
minute: <shapes> / 分
|
||||
hour: <shapes> / 小时
|
||||
settingsMenu:
|
||||
playtime: 游戏时间
|
||||
buildingsPlaced: 设施数量
|
||||
beltsPlaced: 传送带数量
|
||||
tutorialHints:
|
||||
title: 需要帮助?
|
||||
showHint: 显示帮助
|
||||
hideHint: 关闭
|
||||
blueprintPlacer:
|
||||
cost: 成本
|
||||
waypoints:
|
||||
waypoints: 地图标记
|
||||
hub: 中心
|
||||
description: 左键点击地图标记以跳转到该处,右键点击可删除地图标记。<br><br>按 <keybinding>
|
||||
在当前地点创建地图标记,或者在选定位置上<strong>右键</strong>创建地图标记。
|
||||
creationSuccessNotification: 成功创建地图标记。
|
||||
interactiveTutorial:
|
||||
title: 新手教程
|
||||
hints:
|
||||
1_1_extractor: 在<strong>圆形</strong>上放置一个<strong>开采器</strong>来获取圆形!<br><br>提示:<strong>按下鼠标左键</strong>选中<strong>开采器</strong>
|
||||
1_2_conveyor: 用<strong>传送带</strong>将您的开采器连接到中心基地上!<br><br>提示:选中<strong>传送带</strong>后<strong>按下鼠标左键可拖动</strong>布置传送带!
|
||||
1_3_expand:
|
||||
您可以放置更多的<strong>开采器</strong>和<strong>传送带</strong>来更有效率地完成关卡目标。<br><br>
|
||||
提示:按住 <strong>SHIFT</strong>
|
||||
键可放置多个<strong>开采器</strong>,注意用<strong>R</strong>
|
||||
键可旋转<strong>开采器</strong>的出口方向,确保开采的图形可以顺利传送。
|
||||
2_1_place_cutter: 现在放置一个<strong>切割器</strong>,这个设施可把<strong>圆形</strong>切成两半!<br><br>注意:无论如何放置,切割机总是<strong>从上到下</strong>切割。
|
||||
2_2_place_trash:
|
||||
使用切割机后产生的废弃图形会导致<strong>堵塞</strong>。<br><br>注意使用<strong>垃圾桶</strong>清除当前
|
||||
(!) 不需要的废物。
|
||||
2_3_more_cutters: 干的好!现在放置<strong>2个以上的切割机</strong>来加快当前缓慢的过程!<br><br>提示:用<strong>快捷键0-9</strong>可以快速选择各项设施!
|
||||
3_1_rectangles:
|
||||
现在让我们开采一些矩形!找到<strong>矩形地带</strong>并<strong>放置4个开采器</strong>并将它们用<strong>传送带</strong>连接到中心基地。<br><br>
|
||||
提示:选中<strong>传送带</strong>后按住<strong>SHIFT键</strong>可快速准确地规划<strong>传送带路线!</strong>
|
||||
21_1_place_quad_painter: 放置<strong>四口上色器</strong>并且获取一些<strong>圆形</strong>,<strong>白色</strong>和<strong>红色</strong>!
|
||||
21_2_switch_to_wires: 按 <strong>E</strong> 键选择<strong>电线层</strong>!<br><br>
|
||||
然后用导线连接上色器的<strong>四个输入口</strong>!
|
||||
21_3_place_button: 很好!现在放置一个<strong>开关</strong>并连接导线!
|
||||
21_4_press_button: 按下<strong>开关</strong>来<strong>产生正信号</strong>以激活<strong>上色器</strong>。<br><br>注:您不用连上所有的输入口!试着只接两个。
|
||||
colors:
|
||||
red: 红色
|
||||
green: 绿色
|
||||
blue: 蓝色
|
||||
yellow: 黄色
|
||||
purple: 紫色
|
||||
cyan: 青色
|
||||
white: 白色
|
||||
uncolored: 无色
|
||||
black: 黑色
|
||||
shapeViewer:
|
||||
title: 层
|
||||
empty: 空
|
||||
copyKey: 复制短代码
|
||||
connectedMiners:
|
||||
one_miner: 1 个开采器
|
||||
n_miners: <amount> 个开采器
|
||||
limited_items: 限制在 <max_throughput>
|
||||
watermark:
|
||||
title: 试玩版
|
||||
desc: 点击这里了解完整版内容
|
||||
get_on_steam: 在Steam商城购买
|
||||
standaloneAdvantages:
|
||||
title: 购买完整版!
|
||||
no_thanks: 不需要,谢谢
|
||||
points:
|
||||
levels:
|
||||
title: 12 个全新关卡!
|
||||
desc: 总共 26 个不同关卡!
|
||||
buildings:
|
||||
title: 18 个全新设施!
|
||||
desc: 呈现完全体的全自动工厂!
|
||||
upgrades:
|
||||
title: 20个等级升级
|
||||
desc: 试玩版只有5个等级!
|
||||
markers:
|
||||
title: 无限数量地图标记
|
||||
desc: 地图再大,不会迷路!
|
||||
wires:
|
||||
title: 电线更新包
|
||||
desc: 发挥创造力的全新维度!
|
||||
darkmode:
|
||||
title: 暗色模式
|
||||
desc: 优雅且护眼的配色!
|
||||
support:
|
||||
title: 支持作者
|
||||
desc: 我使用闲暇时间开发游戏!
|
||||
achievements:
|
||||
title: 成就
|
||||
desc: 挑战全成就解锁!
|
||||
shopUpgrades:
|
||||
belt:
|
||||
name: 传送、分发、隧道
|
||||
description: 效率 <currentMult>倍 → <newMult>倍
|
||||
miner:
|
||||
name: 开采
|
||||
description: 效率 <currentMult>倍 → <newMult>倍
|
||||
processors:
|
||||
name: 切割、旋转、堆叠
|
||||
description: 效率 <currentMult>倍 → <newMult>倍
|
||||
painting:
|
||||
name: 混色、上色
|
||||
description: 效率 <currentMult>倍 → <newMult>倍
|
||||
buildings:
|
||||
belt:
|
||||
default:
|
||||
name: 传送带
|
||||
description: 运送物品,选中后<strong>按住鼠标并拖动</strong>可一次性放置多个传送带。
|
||||
miner:
|
||||
default:
|
||||
name: 开采器
|
||||
description: 放置在<strong>图形</strong>或者<strong>颜色</strong>上进行开采。
|
||||
chainable:
|
||||
name: 开采器(链式)
|
||||
description: 放置在<strong>图形</strong>或者<strong>颜色</strong>上进行开采。它们可以被链接在一起。
|
||||
underground_belt:
|
||||
default:
|
||||
name: 隧道
|
||||
description: 可放置在<strong>传送带</strong>或<strong>设施</strong>下方以运送物品。
|
||||
tier2:
|
||||
name: 二级隧道
|
||||
description: 可放置在<strong>传送带</strong>或<strong>设施</strong>下方以运送物品。
|
||||
cutter:
|
||||
default:
|
||||
name: 切割机
|
||||
description: 始终将<strong>图形</strong>从上到下切开并分别输出。<strong>如果您只需要其中一半的图形,使用<strong>垃圾桶</strong>清除另一半图形,否则切割机会停止工作!</strong>
|
||||
quad:
|
||||
name: 切割机(四向)
|
||||
description: 将输入的图形切成四块。<strong>如果您只需要其中一块图形,使用<strong>垃圾桶</strong>清除其他图形,否则切割机会停止工作!</strong>
|
||||
rotater:
|
||||
default:
|
||||
name: 旋转机
|
||||
description: 将<strong>图形</strong>顺时针旋转90度。
|
||||
ccw:
|
||||
name: 旋转机(逆时针)
|
||||
description: 将<strong>图形</strong>逆时针旋转90度。
|
||||
rotate180:
|
||||
name: 旋转机 (180度)
|
||||
description: 将<strong>图形</strong>旋转180度。
|
||||
stacker:
|
||||
default:
|
||||
name: 堆叠机
|
||||
description: 将输入的<strong>图形</strong>在同一层内组合在一起。如果不能被直接组合,则右边输入<strong>图形</strong>会堆叠在左边输入<strong>图形</strong>上面。
|
||||
mixer:
|
||||
default:
|
||||
name: 混色器
|
||||
description: 用叠加混色法将两个<strong>颜色</strong>混合。
|
||||
painter:
|
||||
default:
|
||||
name: 上色器
|
||||
description: 将整个<strong>图形</strong>涂上输入的<strong>颜色</strong>。
|
||||
double:
|
||||
name: 上色器(双面)
|
||||
description: 使用顶部输入的<strong>颜色</strong>为左侧输入的<strong>图形</strong>上色。
|
||||
quad:
|
||||
name: 上色器(四口)
|
||||
description: 能够为<strong>图形</strong>的四个象限单独上色。记住只有通过电线层上带有<strong>正信号</strong>的插槽才可以上色!
|
||||
mirrored:
|
||||
name: 上色器 (镜像)
|
||||
description: 将整个<strong>图形</strong>涂上输入的颜色。
|
||||
trash:
|
||||
default:
|
||||
name: 垃圾桶
|
||||
description: 可以从所有四个方向上输入物品并永远清除它们。
|
||||
hub:
|
||||
deliver: 交付
|
||||
toUnlock: 解锁
|
||||
levelShortcut: LVL
|
||||
endOfDemo: 试玩版结束
|
||||
wire:
|
||||
default:
|
||||
name: 电线
|
||||
description: 可用来传输<strong>信号<strong>,信号可以是物品,颜色或者开关值(0或1)。
|
||||
不同颜色的<strong>电线</strong>不会互相连接
|
||||
second:
|
||||
name: 电线
|
||||
description: 可用来传输<strong>信号<strong>,信号可以是物品,颜色或者开关值(0或1)。
|
||||
不同颜色的<strong>电线</strong>不会互相连接
|
||||
balancer:
|
||||
default:
|
||||
name: 平衡器
|
||||
description: 多功能的设施:可将所有输入均匀地分配到所有输出上。
|
||||
merger:
|
||||
name: 合并器 (小型)
|
||||
description: 可将两条传送带合并为一条。
|
||||
merger-inverse:
|
||||
name: 合并器 (小型)
|
||||
description: 可将两条传送带合并为一条。
|
||||
splitter:
|
||||
name: 分离器 (小型)
|
||||
description: 可将一条传送带分成为两条。
|
||||
splitter-inverse:
|
||||
name: 分离器 (小型)
|
||||
description: 可将一条传送带分成为两条。
|
||||
storage:
|
||||
default:
|
||||
name: 存储器
|
||||
description: 储存多余的物品,直到储满。 优先处理左边的输出,并可以用作溢出门。
|
||||
wire_tunnel:
|
||||
default:
|
||||
name: 交叉电线
|
||||
description: 使两根<strong>电线</strong>交叉而不会连接起来。
|
||||
constant_signal:
|
||||
default:
|
||||
name: 恒定信号
|
||||
description: 发出固定信号,可以是<strong>图形</strong>、<strong>颜色</strong>、<strong>开关值(1 /
|
||||
0)</strong>。
|
||||
lever:
|
||||
default:
|
||||
name: 开关
|
||||
description: 可以在电线层上发出<strong>开关值(1 / 0)</strong>信号,以达到控制部件的作用,比如可以用来控制物品过滤器。
|
||||
logic_gate:
|
||||
default:
|
||||
name: 与门
|
||||
description: 如果输入<strong>都是</strong>正信号,则发出<strong>开(1)</strong>信号。(正信号:图形,颜色,开(1)信号)
|
||||
not:
|
||||
name: 非门
|
||||
description: 如果输入<strong>不是</strong>正信号,则发出<strong>开(1)</strong>信号。(正信号:图形,颜色,开(1)信号)
|
||||
xor:
|
||||
name: 异或门
|
||||
description: 如果输入<strong>只有一个</strong>正信号,则发出<strong>开(1)</strong>信号。(正信号:图形,颜色,开(1)信号)
|
||||
or:
|
||||
name: 或门
|
||||
description: 如果输入<strong>有一个</strong>是正信号,则发出<strong>开(1)</strong>信号。(正信号:图形,颜色,开(1)信号)
|
||||
transistor:
|
||||
default:
|
||||
name: 晶体管
|
||||
description: 如果侧边输入正信号,输入可以通过并转发。(正信号:图形,颜色,开(1)信号)
|
||||
mirrored:
|
||||
name: 晶体管
|
||||
description: 如果侧边输入正信号,输入可以通过并转发。(正信号:图形,颜色,开(1)信号)
|
||||
filter:
|
||||
default:
|
||||
name: 过滤器
|
||||
description: 在顶侧输出和<strong>信号</strong>匹配的内容,在右侧输出不匹配的内容。如果是开关量的话,开(1)信号从顶侧输出,关(0)信号从右侧输出。
|
||||
display:
|
||||
default:
|
||||
name: 显示器
|
||||
description: 在显示器上显示连接的<strong>信号</strong>(信号可以是:图形、颜色、开关值)。
|
||||
reader:
|
||||
default:
|
||||
name: 传送带读取器
|
||||
description: 可以读取<strong>传送带</strong>平均<strong>吞吐量</strong>。输出最后在<strong>电线层</strong>上读取的物品(一旦解锁。)
|
||||
analyzer:
|
||||
default:
|
||||
name: 图形分析器
|
||||
description: 分析<strong>图形</strong>最底层的右上象限并返回其<strong>图形</strong>和<strong>颜色</strong>。
|
||||
comparator:
|
||||
default:
|
||||
name: 比较器
|
||||
description: 如果输入的两个<strong>信号</strong>一样将输出开(1)信号,可以比较图形,颜色,和开关值。
|
||||
virtual_processor:
|
||||
default:
|
||||
name: 虚拟切割机
|
||||
description: 模拟将<strong>图形</strong>切割成两半。
|
||||
rotater:
|
||||
name: 模拟旋转机
|
||||
description: 模拟顺时针旋转<strong>图形</strong>。
|
||||
unstacker:
|
||||
name: 模拟拆分器
|
||||
description: 模拟提取最上层<strong>图形</strong>从右侧输出,提取其余的<strong>图形</strong>从左侧输出。
|
||||
stacker:
|
||||
name: 模拟堆叠机
|
||||
description: 模拟将右侧<strong>图形</strong>叠在左侧<strong>图形</strong>上。
|
||||
painter:
|
||||
name: 模拟上色器
|
||||
description: 模拟使用右侧输入的<strong>颜色</strong>给底部输入的<strong>图形</strong>上色
|
||||
item_producer:
|
||||
default:
|
||||
name: 物品生成器
|
||||
description: 仅在沙盒模式下可用,在常规层上输出<strong>电线层</strong>给定的<strong>信号</strong>。
|
||||
storyRewards:
|
||||
reward_cutter_and_trash:
|
||||
title: 切割图形
|
||||
desc: 恭喜!您解锁了<strong>切割机</strong>,不管如何放置,它只会从上到下切开<strong>图形</strong>!
|
||||
<br>注意一定要处理掉切割后废弃的<strong>图形</strong>,不然它会<strong>阻塞</strong>传送带,
|
||||
<br>使用<strong>垃圾桶</strong>,它会清除所有放进去的图形!
|
||||
reward_rotater:
|
||||
title: 旋转
|
||||
desc: 恭喜!您解锁了<strong>旋转机</strong>。它会顺时针将输入的<strong>图形旋转90度</strong>。
|
||||
reward_painter:
|
||||
title: 上色
|
||||
desc:
|
||||
恭喜!您解锁了<strong>上色器</strong>。开采一些颜色 (就像您开采图形一样),将其在上色器中与图形结合来将图形上色!
|
||||
<br>注意:如果您不幸患有色盲,可以在设置中启用<strong>色盲模式</strong>
|
||||
reward_mixer:
|
||||
title: 混合颜色
|
||||
desc: 恭喜!您解锁了<strong>混色器</strong>。它使用<strong>叠加混色法</strong>将两种颜色混合起来。
|
||||
reward_stacker:
|
||||
title: 堆叠
|
||||
desc: 恭喜!您解锁了<strong>堆叠机</strong>。它会将将输入的<strong>图形</strong>在同一层内组合在一起。
|
||||
<br>如果不能被直接组合,则右边输入<strong>图形</strong>会堆叠在左边输入<strong>图形</strong>上面。
|
||||
reward_splitter:
|
||||
title: 分离器(小型)
|
||||
desc: 您已经解锁了<strong>平衡器</strong>的变体<strong>分离器</strong>,它会把输入的东西一分为二!
|
||||
reward_tunnel:
|
||||
title: 隧道
|
||||
desc: 恭喜!您解锁了<strong>隧道</strong>。它可放置在<strong>传送带</strong>或<strong>设施</strong>下方以运送物品。
|
||||
reward_rotater_ccw:
|
||||
title: 逆时针旋转
|
||||
desc:
|
||||
恭喜!您解锁了<strong>旋转机</strong>的<strong>逆时针</strong>变体。它可以逆时针旋转<strong>图形</strong>。
|
||||
<br>选择<strong>旋转机</strong>然后按"T"键来选取这个变体。
|
||||
reward_miner_chainable:
|
||||
title: 链式开采器
|
||||
desc:
|
||||
您已经解锁了<strong>链式开采器</strong>!它能<strong>转发资源</strong>给其他的开采器,这样您就能更有效率的开采各类资源了!<br><br>
|
||||
注意:新的开采器已替换了工具栏里旧的开采器!
|
||||
reward_underground_belt_tier_2:
|
||||
title: 二级隧道
|
||||
desc: 恭喜!您解锁了<strong>二级隧道</strong>。这是隧道的一个变体。二级隧道有<strong>更长的传输距离</strong>。您还可以混用不同的隧道变体!
|
||||
reward_cutter_quad:
|
||||
title: 四向切割机
|
||||
desc: 恭喜!您解锁了<strong>切割机</strong>的<strong>四向</strong>变体。它可以将输入的<strong>图形</strong>切成四块而不只是左右两块!
|
||||
reward_painter_double:
|
||||
title: 双面上色器
|
||||
desc: 恭喜!您解锁了<strong>上色器</strong>的<strong>双面</strong>变体。它可以同时为两个图形上色,但每次只消耗一份颜色!
|
||||
reward_storage:
|
||||
title: 存储器
|
||||
desc: 您已经解锁了<strong>存储器</strong>,它能存满指定容量的物品!
|
||||
<br>它<strong>优先从左边</strong>输出,这样您就可以用它做一个<strong>溢流门</strong>了!
|
||||
reward_freeplay:
|
||||
title: 自由模式
|
||||
desc:
|
||||
成功了!您解锁了<strong>自由模式</strong>!挑战升级!这意味着现在将<strong>随机</strong>生成图形!
|
||||
从现在起,中心基地最为需要的是<strong>产量</strong>,我强烈建议您去制造一台能够自动交付所需图形的机器!<br><br>
|
||||
基地会在<strong>电线层</strong>输出需要的图形,您需要去分析图形并在此基础上自动配置您的工厂。
|
||||
reward_blueprints:
|
||||
title: 蓝图
|
||||
desc:
|
||||
您现在可以<strong>复制粘贴</strong>您的工厂的一部分了!按住 CTRL键并拖动鼠标来选择一块区域,然后按C键复制。
|
||||
<br><br>粘贴并<strong>不是免费的</strong>,您需要制造<strong>蓝图图形</strong>来负担。蓝图图形是您刚刚交付的图形。
|
||||
no_reward:
|
||||
title: 下一关
|
||||
desc: 这一关没有奖励,但是下一关有! <br><br>
|
||||
注意:最高明的规划师都不会破坏原有的工厂设施,您生产过的<strong>所有图形</strong>都会被用于<strong>解锁升级</strong>。
|
||||
no_reward_freeplay:
|
||||
title: 下一关
|
||||
desc: 恭喜您!另外,我们已经计划在完整版中加入更多内容!
|
||||
reward_balancer:
|
||||
title: 平衡器
|
||||
desc: 恭喜!您解锁了多功能<strong>平衡器</strong>,它能够<strong>分割和合并</strong>多个传送带的资源,可以用来建造更大的工厂!
|
||||
reward_merger:
|
||||
title: 合并器(小型)
|
||||
desc: 恭喜!您解锁了<strong>平衡器</strong>的变体<strong>合并器</strong>,它能合并两个输入到同一个传送带上!
|
||||
reward_belt_reader:
|
||||
title: 传送带读取器
|
||||
desc: 恭喜!您解锁了<strong>传送带读取器</strong>!它能够测量传送带上的生产率。
|
||||
<br><br>等您解锁了<strong>电线层</strong>后,它将会极其有用!
|
||||
reward_rotater_180:
|
||||
title: 旋转机(180度)
|
||||
desc: 恭喜!您解锁了<strong>旋转器(180度)</strong>!它能帮您把一个图形旋转180度(Surprise! :D)
|
||||
reward_display:
|
||||
title: 显示器
|
||||
desc: 恭喜!您已经解锁了<strong>显示器</strong>,它可以显示一个在<strong>电线层上连接的信号</strong>!
|
||||
<br>注意:您注意到<strong>传送读取器</strong>和<strong>存储器</strong>输出的他们最后读取的物品了吗?试着在显示屏上展示一下!"
|
||||
reward_constant_signal:
|
||||
title: 恒定信号
|
||||
desc:
|
||||
恭喜!您解锁了生成于电线层之上的<strong>恒定信号</strong>,把它连接到<strong>过滤器</strong>时非常有用。
|
||||
<br>比如,它能发出图形、颜色、开关值(1 / 0)的固定信号。
|
||||
reward_logic_gates:
|
||||
title: 逻辑门
|
||||
desc: 您解锁了<strong>逻辑门</strong>!它们是个好东西!<br>
|
||||
您可以用它们来进行'与,或,非,异或'操作。<br><br>作为奖励,我还给您解锁了<strong>晶体管</strong>!
|
||||
reward_virtual_processing:
|
||||
title: 模拟处理器
|
||||
desc: 我刚刚给了一大堆新设施,让您可以<strong>模拟形状的处理过程</strong>!<br>
|
||||
您现在可以在电线层上模拟切割机,旋转机,堆叠机和其他机器!<br> 有了这些,您可以选择下面三个方向来继续游戏:<br>
|
||||
-建立一个<strong>自动化机器</strong>以生产出任何中心基地需要图形(建议一试!)。<br>
|
||||
-用<strong>电线层</strong>做些酷炫的东西。<br> -继续正常游戏。<br> 放飞想象,尽情创造!
|
||||
reward_wires_painter_and_levers:
|
||||
title: 电线 & 四口上色器
|
||||
desc: 恭喜!您解锁了<strong>电线层</strong>:它是正常层之上的一个层,它将带来了许多新的机制!<br><br>
|
||||
首先我解锁了您的<strong>四口上色器</strong>,按<strong>E</strong>键切换到电线层,然后连接您想要染色的槽,用开关来控制开启。<br><br>
|
||||
<strong>提示</strong>:可在设置中打开电线层教程!"
|
||||
reward_filter:
|
||||
title: 物品过滤器
|
||||
desc:
|
||||
恭喜!您解锁了<strong>物品过滤器</strong>!它会根据在电线层上输入的信号决定是从上面还是右边输出物品。<br><br>
|
||||
您也可以输入开关值(1 / 0)信号来激活或者禁用它。
|
||||
reward_demo_end:
|
||||
title: 试玩结束
|
||||
desc: 恭喜!您已经通关了试玩版本! <br>更多挑战,请至Steam商城购买完整版!谢谢支持!
|
||||
settings:
|
||||
title: 设置
|
||||
categories:
|
||||
general: 通用
|
||||
userInterface: 用户界面
|
||||
advanced: 高级
|
||||
performance: 性能
|
||||
versionBadges:
|
||||
dev: 开发版本
|
||||
staging: 预览版本
|
||||
prod: 正式版本
|
||||
buildDate: 与<at-date>编译
|
||||
tickrateHz: <amount> 赫兹
|
||||
labels:
|
||||
uiScale:
|
||||
title: 用户界面大小
|
||||
description: 改变用户界面大小。用户界面会随着屏幕分辨率缩放,这个设置决定缩放比例。
|
||||
scales:
|
||||
super_small: 最小
|
||||
small: 较小
|
||||
regular: 正常
|
||||
large: 较大
|
||||
huge: 最大
|
||||
scrollWheelSensitivity:
|
||||
title: 缩放灵敏度
|
||||
description: 改变屏幕缩放灵敏度(用鼠标滚轮或者触控板控制缩放)。
|
||||
sensitivity:
|
||||
super_slow: 最低
|
||||
slow: 较低
|
||||
regular: 正常
|
||||
fast: 较高
|
||||
super_fast: 最高
|
||||
language:
|
||||
title: 语言
|
||||
description: 改变语言。官方中文版已更新,欢迎玩家继续提供更好的翻译意见。
|
||||
fullscreen:
|
||||
title: 全屏
|
||||
description: 全屏可获得更好的游戏体验。仅在完整版中可用。
|
||||
soundsMuted:
|
||||
title: 关闭音效
|
||||
description: 关闭所有音效。
|
||||
musicMuted:
|
||||
title: 关闭音乐
|
||||
description: 关闭所有音乐。
|
||||
theme:
|
||||
title: 界面主题
|
||||
description: 选择界面主题(深色或浅色)。
|
||||
themes:
|
||||
dark: 深色
|
||||
light: 浅色
|
||||
refreshRate:
|
||||
title: 模拟频率、刷新频率
|
||||
description: 如果您的显示器刷新频率是
|
||||
144赫兹,请在这里更改刷新频率,这样游戏可以正确地根据您的屏幕进行模拟。但是如果您的电脑性能不佳,提高刷新频率可能降低帧数。
|
||||
alwaysMultiplace:
|
||||
title: 多重放置
|
||||
description: 开启这个选项之后放下设施将不会取消设施选择。等同于一直按下 SHIFT 键。
|
||||
offerHints:
|
||||
title: 提示与教程
|
||||
description: 是否显示提示、教程以及一些其他的帮助理解游戏的用户界面元素。建议新手玩家开启。
|
||||
movementSpeed:
|
||||
title: 移动速度
|
||||
description: 改变摄像头的移动速度。
|
||||
speeds:
|
||||
super_slow: 最慢
|
||||
slow: 较慢
|
||||
regular: 正常
|
||||
fast: 较快
|
||||
super_fast: 非常快
|
||||
extremely_fast: 最快
|
||||
enableTunnelSmartplace:
|
||||
title: 智能隧道放置
|
||||
description: 启用后,放置隧道时会将多余的传送带移除。 此外,拖动隧道可以快速铺设隧道,以及移除不必要的隧道。
|
||||
vignette:
|
||||
title: 晕映
|
||||
description: 启用晕映功能,可将屏幕角落里的颜色变深,更容易阅读文本。
|
||||
autosaveInterval:
|
||||
title: 自动存档间隔
|
||||
description: 在这里控制您的游戏多长时间自动存档一次,你也可以完全关闭这个功能。建议打开。
|
||||
intervals:
|
||||
one_minute: 1分钟
|
||||
two_minutes: 2分钟
|
||||
five_minutes: 5分钟
|
||||
ten_minutes: 10分钟
|
||||
twenty_minutes: 20分钟
|
||||
disabled: 关闭
|
||||
compactBuildingInfo:
|
||||
title: 精简设施信息
|
||||
description: 缩小设施信息展示框。如果打开,放置设施时将不再显示说明和图片,只显示建造速度或其他数据。
|
||||
disableCutDeleteWarnings:
|
||||
title: 关闭剪切/删除警告
|
||||
description: 如果打开,将不再在剪切或者删除100+实体时显示警告信息。
|
||||
enableColorBlindHelper:
|
||||
title: 色盲模式
|
||||
description: 提供多种工具,帮助色盲玩家可正常进行游戏。
|
||||
rotationByBuilding:
|
||||
title: 记忆设施方向
|
||||
description: 每一类设施都会记住各自上一次的旋转方向。如果您经常在不同设施类型之间切换,这个设置会让游戏操控更加便捷。
|
||||
soundVolume:
|
||||
title: 音效音量
|
||||
description: 设置音效的音量
|
||||
musicVolume:
|
||||
title: 音乐音量
|
||||
description: 设置音乐的音量
|
||||
lowQualityMapResources:
|
||||
title: 低质量地图资源
|
||||
description: 放大时简化地图上资源的渲染以提高性能。开启甚至会让画面看起来更干净,低配置电脑玩家建议开启!
|
||||
disableTileGrid:
|
||||
title: 禁用网格
|
||||
description: 禁用平铺网格有助于提高性能。这也让游戏画面看起来更干净!
|
||||
clearCursorOnDeleteWhilePlacing:
|
||||
title: 右键取消
|
||||
description: 默认启用。在选择要放置的设施时,单击鼠标右键即可取消。如果禁用,则可以通过在放置设施时单击鼠标右键来删除设施。
|
||||
lowQualityTextures:
|
||||
title: 低质量纹理
|
||||
description: 使用低质量纹理提高游戏性能。但是这样游戏会以低画面质量运行!
|
||||
displayChunkBorders:
|
||||
title: 显示大块的边框
|
||||
description: 游戏将每一个大块分成16*16的小块,如果启用将会显示每个大块的边框。
|
||||
pickMinerOnPatch:
|
||||
title: 在资源块上选择开采器
|
||||
description: 默认开启,当在资源块上使用选取器时会选择开采器。
|
||||
simplifiedBelts:
|
||||
title: 简单的传送带
|
||||
description: 除非鼠标放在传送带上,不然不会渲染传送带上的物品。启用可提升游戏性能。但除非特别需要性能,否则不推荐启用。
|
||||
enableMousePan:
|
||||
title: 鼠标平移屏幕
|
||||
description: 在鼠标滑到屏幕边缘时可以移动地图。移动速度取决于移动速度设置。
|
||||
zoomToCursor:
|
||||
title: 鼠标位置缩放
|
||||
description: 启用后在鼠标所在位置进行屏幕缩放,否则在屏幕中间进行缩放。
|
||||
mapResourcesScale:
|
||||
title: 地图资源图形尺寸
|
||||
description: 控制地图总览时图形的尺寸(指缩小视野时)。
|
||||
rangeSliderPercentage: <amount> %
|
||||
keybindings:
|
||||
title: 按键设定
|
||||
hint: 提示:使用 CTRL、SHIFT、ALT!这些键在放置设施时有不同的效果。
|
||||
resetKeybindings: 重置按键设定
|
||||
categoryLabels:
|
||||
general: 通用
|
||||
ingame: 游戏
|
||||
navigation: 视角
|
||||
placement: 放置
|
||||
massSelect: 批量选择
|
||||
buildings: 设施快捷键
|
||||
placementModifiers: 放置设施修饰键
|
||||
mappings:
|
||||
confirm: 确认
|
||||
back: 返回
|
||||
mapMoveUp: 上
|
||||
mapMoveRight: 右
|
||||
mapMoveDown: 下
|
||||
mapMoveLeft: 左
|
||||
centerMap: 回到中心基地
|
||||
mapZoomIn: 放大
|
||||
mapZoomOut: 缩小
|
||||
createMarker: 创建地图标记
|
||||
menuOpenShop: 升级菜单
|
||||
menuOpenStats: 统计菜单
|
||||
toggleHud: 开关可视化界面
|
||||
toggleFPSInfo: 开关帧数与调试信息
|
||||
belt: 传送带
|
||||
underground_belt: 隧道
|
||||
miner: 开采器
|
||||
cutter: 切割机
|
||||
rotater: 旋转机
|
||||
stacker: 堆叠机
|
||||
mixer: 混色器
|
||||
painter: 上色器
|
||||
trash: 垃圾桶
|
||||
rotateWhilePlacing: 顺时针旋转
|
||||
rotateInverseModifier: "修饰键: 改为逆时针旋转"
|
||||
cycleBuildingVariants: 切换所选择设施变体
|
||||
confirmMassDelete: 确认批量删除
|
||||
cycleBuildings: 切换所选择设施
|
||||
massSelectStart: 开始批量选择
|
||||
massSelectSelectMultiple: 选择多个区域
|
||||
massSelectCopy: 复制区域
|
||||
placementDisableAutoOrientation: 取消自动定向
|
||||
placeMultiple: 继续放置
|
||||
placeInverse: 反向自动传送带方向
|
||||
pasteLastBlueprint: 粘贴上一张蓝图
|
||||
massSelectCut: 剪切区域
|
||||
exportScreenshot: 导出截图
|
||||
mapMoveFaster: 快速移动
|
||||
lockBeltDirection: 启用传送带规划
|
||||
switchDirectionLockSide: 规划器:换边
|
||||
pipette: 吸取器
|
||||
menuClose: 关闭菜单
|
||||
switchLayers: 切换层
|
||||
wire: 电线
|
||||
balancer: 平衡器
|
||||
storage: 存储器
|
||||
constant_signal: 恒定信号
|
||||
logic_gate: 逻辑门
|
||||
lever: 控制杆
|
||||
filter: 过滤器
|
||||
wire_tunnel: 电线隧道
|
||||
display: 显示器
|
||||
reader: 传送带读取器
|
||||
virtual_processor: 模拟切割机
|
||||
transistor: 晶体管
|
||||
analyzer: 图形分析器
|
||||
comparator: 比较器
|
||||
item_producer: 物品生产器 (沙盒模式)
|
||||
copyWireValue: 电线:复制指定电线上的值
|
||||
rotateToUp: "向上旋转"
|
||||
rotateToDown: "向下旋转"
|
||||
rotateToRight: "向右旋转"
|
||||
rotateToLeft: "向左旋转"
|
||||
about:
|
||||
title: 关于游戏
|
||||
body: >-
|
||||
本游戏由 <a href="https://github.com/tobspr" target="_blank">Tobias
|
||||
Springer</a>(我)开发,并且已经开源。<br><br>
|
||||
|
||||
如果您想参与开发,请查看 <a href="<githublink>" target="_blank">shapez.io on github</a>。<br><br>
|
||||
|
||||
这个游戏的开发获得了 Discord 社区内热情玩家的巨大支持。诚挚邀请您加入我们的 <a href="<discordlink>" target="_blank">Discord 服务器</a>!<br><br>
|
||||
|
||||
本游戏的音乐由 <a href="https://soundcloud.com/pettersumelius" target="_blank">Peppsen</a> 制作——他是个很棒的伙伴。<br><br>
|
||||
|
||||
最后,我想感谢我最好的朋友 <a href="https://github.com/niklas-dahl" target="_blank">Niklas</a> ——如果没有他的《异星工厂》(factorio)带给我的体验和启发,《异形工厂》(shapez.io)将不会存在。
|
||||
changelog:
|
||||
title: 版本日志
|
||||
demo:
|
||||
features:
|
||||
restoringGames: 恢复存档
|
||||
importingGames: 导入存档
|
||||
oneGameLimit: 最多一个存档
|
||||
customizeKeybindings: 按键设定
|
||||
exportingBase: 导出工厂截图
|
||||
settingNotAvailable: 在试玩版中不可用。
|
||||
tips:
|
||||
- 基地接受所有创造后输入的图形!并不限于现有的图形!
|
||||
- 让你的工厂尽量模块化,不然后期你会面对大麻烦!
|
||||
- 不要让设施太过靠近基地,不然可能会乱成一锅粥!
|
||||
- 如果堆叠不起作用,尝试切换输入的图形来重新组合。
|
||||
- 您可以通过 <b>R</b> 键切换传送带规化方向。
|
||||
- 按住 <b>CTRL</b> 键拖动传送带将始终保持它现有的传送方向。
|
||||
- 只要所有设施等级一致,效率也将一致。
|
||||
- 串行执行比并行执行更有效。
|
||||
- 在后面的游戏中您会解锁更多设施的变种!
|
||||
- 您可以使用<b>T</b>键切换不同的设施变种。
|
||||
- 对称是关键!
|
||||
- 您可以使用隧道构建不同层次的通道。
|
||||
- 试着建造紧凑型工厂,它会给您带来好处的!
|
||||
- 您可以按<b>T</b>来切换上色器的镜像变体。
|
||||
- 正确的设施比例将使效率最大化。
|
||||
- 在传送带和开采器等级一致时,5个开采器就可以占满一条传送带的运量。
|
||||
- 别忘了隧道!
|
||||
- 您不必为了充分发挥效率而平均分配物品。
|
||||
- 按住<b>SHIFT</b>键将激活传送带路线规划,这样可以更有效地规划如何放置长距离的传送带。
|
||||
- 切割机总是垂直切割图形,而不管图形方向如何。
|
||||
- 还记得吗?混合三原色能够获得白色。
|
||||
- 存储缓冲区优先处理左侧的输出。
|
||||
- 您值得花时间来构建可重复的设计!
|
||||
- 按住<b>CTRL</b>键能够放置多个设施。
|
||||
- 您可以按住<b>ALT</b>来反向放置传送带的方向。
|
||||
- 效率是关键!
|
||||
- 离基地越远图形越复杂。
|
||||
- 机器的速度是有限的,把它们分开可以获得最高的效率。
|
||||
- 使用平衡器最大化您的效率。
|
||||
- 有条不紊!尽量不要过多地穿过传送带。
|
||||
- 凡事预则立!不预则废!
|
||||
- 尽量不要删除旧的设施和生产线,您会需要他们生产的东西来升级设施并提高效率。
|
||||
- 先给自己定一个小目标:自己完成20级!!不去看别人的攻略!
|
||||
- 不要把问题复杂化,试着保持简单,您会成功的。
|
||||
- 您可能需要在游戏的后期重复使用工厂。把您的工厂规划成可重复使用的。
|
||||
- 有时,您可以在地图上直接找到您需要的图形,并不需要使用堆叠机去合成它。
|
||||
- 风车图形不会自动产生
|
||||
- 在切割前,给您的图形上色可以获得最高的效率。
|
||||
- 模块化,可以使您提高效率。
|
||||
- 记得做一个单独的蓝图工厂。
|
||||
- 仔细看看调色器,您就会调色了。
|
||||
- <b>CTRL+点击</b>能够选择一块区域。
|
||||
- 设施建得离基地太近很可能会妨碍以后的工作。
|
||||
- 使用升级列表中每个形状旁边的固定图标将其固定到屏幕上。
|
||||
- 地图无限,放飞想象,尽情创造。
|
||||
- 向您推荐Factorio!这是我最喜欢的游戏。向神作致敬!
|
||||
- 四向切割机从右上开始进行顺时针切割!
|
||||
- 在主界面您可以下载您的游戏存档文件!
|
||||
- 这个游戏有很多有用的快捷键!一定要到快捷键页面看看。
|
||||
- 这个游戏有很多设置可以提高游戏效率,请一定要了解一下!
|
||||
- 中心基地有个指向它所在方向的小指南指针!
|
||||
- 想清理传送带,可剪切那块区域然后将其在相同位置粘贴。
|
||||
- 按F4显示FPS。
|
||||
- 按两次F4显示您鼠标和镜头所在的块。
|
||||
- 您可以点击被固定在屏幕左侧的图形来解除固定。
|
||||
- 您可以点击被固定在屏幕左侧的图形来解除固定。
|
||||