mirror of
https://github.com/tobspr/shapez.io.git
synced 2025-06-13 13:04:03 +00:00
Merge remote-tracking branch 'origin/master' into better-building-variants
This commit is contained in:
commit
88d457af0e
@ -64,7 +64,7 @@ This project is based on ES5. Some ES2015 features are used but most of them are
|
||||
5. Add a constructor. **The constructor must be called with optional parameters only!** `new MyFancyComponent({})` should always work.
|
||||
6. Add any props you need in the constructor.
|
||||
7. Add the component in `src/js/game/component_registry.js`
|
||||
8. Add the componetn in `src/js/game/entity_components.js`
|
||||
8. Add the component in `src/js/game/entity_components.js`
|
||||
9. Done! You can use your component now
|
||||
|
||||
#### Adding a new building
|
||||
@ -96,6 +96,6 @@ This project is based on ES5. Some ES2015 features are used but most of them are
|
||||
|
||||
For most assets I use Adobe Photoshop, you can find them in `assets/`.
|
||||
|
||||
You will need a <a href="https://www.codeandweb.com/texturepacker" target="_blank">Texture Packer</a> license in order to regenerate the atlas. If you don't have one but want to contribute assets, let me know and I might compile it for you. I'm currently switching to an open source solution but I can't give an estimate when thats done.
|
||||
You will need a <a href="https://www.codeandweb.com/texturepacker" target="_blank">Texture Packer</a> license in order to regenerate the atlas. If you don't have one but want to contribute assets, let me know and I might compile it for you. I'm currently switching to an open source solution but I can't give an estimate when that's done.
|
||||
|
||||
<img src="https://i.imgur.com/W25Fkl0.png" alt="shapez.io Screenshot">
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.3 MiB |
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 281 KiB After Width: | Height: | Size: 276 KiB |
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 661 KiB After Width: | Height: | Size: 688 KiB |
@ -283,6 +283,7 @@
|
||||
<key type="filename">sprites/blueprints/virtual_processor-analyzer.png</key>
|
||||
<key type="filename">sprites/blueprints/virtual_processor-rotater.png</key>
|
||||
<key type="filename">sprites/blueprints/virtual_processor-shapecompare.png</key>
|
||||
<key type="filename">sprites/blueprints/virtual_processor-stacker.png</key>
|
||||
<key type="filename">sprites/blueprints/virtual_processor-unstacker.png</key>
|
||||
<key type="filename">sprites/blueprints/virtual_processor.png</key>
|
||||
<key type="filename">sprites/blueprints/wire_tunnel-coating.png</key>
|
||||
@ -310,6 +311,7 @@
|
||||
<key type="filename">sprites/buildings/virtual_processor-analyzer.png</key>
|
||||
<key type="filename">sprites/buildings/virtual_processor-rotater.png</key>
|
||||
<key type="filename">sprites/buildings/virtual_processor-shapecompare.png</key>
|
||||
<key type="filename">sprites/buildings/virtual_processor-stacker.png</key>
|
||||
<key type="filename">sprites/buildings/virtual_processor-unstacker.png</key>
|
||||
<key type="filename">sprites/buildings/virtual_processor.png</key>
|
||||
<key type="filename">sprites/buildings/wire_tunnel-coating.png</key>
|
||||
|
||||
@ -468,13 +468,24 @@ canvas {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.hint {
|
||||
position: absolute;
|
||||
@include S(left, 20px);
|
||||
@include S(right, 20px);
|
||||
@include S(bottom, 60px);
|
||||
@include Text;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.loadingStatus {
|
||||
position: absolute;
|
||||
@include S(left, 20px);
|
||||
@include S(right, 20px);
|
||||
@include S(bottom, 30px);
|
||||
@include Text;
|
||||
@include TextShadow3D(#aaa);
|
||||
@include PlainText;
|
||||
color: #aaa;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
@ -1,43 +1,79 @@
|
||||
#ingame_HUD_EntityDebugger {
|
||||
position: absolute;
|
||||
background: $ingameHudBg;
|
||||
@include S(padding, 5px);
|
||||
@include S(right, 30px);
|
||||
@include S(top, 200px);
|
||||
|
||||
font-size: 14px;
|
||||
line-height: 16px;
|
||||
color: #fff;
|
||||
background: rgba(0, 10, 20, 0.7);
|
||||
padding: 10px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
|
||||
@include SuperSmallText;
|
||||
color: #eee;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
> label {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.hint {
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
&,
|
||||
* {
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.flag {
|
||||
display: inline-block;
|
||||
background: #333438;
|
||||
@include S(padding, 2px);
|
||||
@include S(margin-right, 2px);
|
||||
|
||||
u {
|
||||
opacity: 0.5;
|
||||
}
|
||||
.propertyTable {
|
||||
@include S(margin-top, 8px);
|
||||
}
|
||||
|
||||
.components {
|
||||
@include S(margin-top, 4px);
|
||||
.propertyTable,
|
||||
.entityComponents,
|
||||
.entityComponents .object > div {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
@include S(grid-gap, 3px);
|
||||
.component {
|
||||
@include S(padding, 2px);
|
||||
background: #333;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
grid-template-columns: 1fr auto;
|
||||
@include S(column-gap, 10px);
|
||||
}
|
||||
|
||||
.data {
|
||||
@include S(width, 150px);
|
||||
@include S(height, 130px);
|
||||
.entityComponents {
|
||||
grid-column: 1 / 3;
|
||||
@include S(margin-top, 5px);
|
||||
|
||||
font-family: "Roboto Mono", "Fira Code", monospace;
|
||||
font-size: 90%;
|
||||
@include S(letter-spacing, -0.5px);
|
||||
|
||||
label,
|
||||
span {
|
||||
line-height: 1.5em;
|
||||
|
||||
&:not(span) {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
&,
|
||||
* {
|
||||
@include SuperSmallText;
|
||||
@include S(font-size, 7px, $important: true);
|
||||
@include S(line-height, 12px, $important: true);
|
||||
}
|
||||
|
||||
.object {
|
||||
grid-column: 1 / 3;
|
||||
line-height: 1.5em;
|
||||
|
||||
> summary {
|
||||
transition: opacity 0.1s ease-in-out;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
> div {
|
||||
@include S(margin-left, 4px);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -56,6 +56,17 @@
|
||||
transform: scale(1.1, 1.1);
|
||||
}
|
||||
}
|
||||
|
||||
&.saving {
|
||||
@include InlineAnimation(0.4s ease-in-out infinite) {
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
}
|
||||
pointer-events: none;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
&.settings {
|
||||
|
||||
@ -1,138 +1,137 @@
|
||||
#ingame_HUD_PinnedShapes {
|
||||
position: absolute;
|
||||
@include S(left, 9px);
|
||||
@include S(top, 150px);
|
||||
@include PlainText;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
|
||||
> .shape {
|
||||
position: relative;
|
||||
display: grid;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
grid-template-columns: auto 1fr;
|
||||
grid-template-rows: 1fr 1fr;
|
||||
@include S(margin-bottom, 4px);
|
||||
color: #333438;
|
||||
// text-shadow: #{D(1px)} #{D(1px)} 0 rgba(0, 10, 20, 0.2);
|
||||
filter: drop-shadow(#{D(1px)} #{D(1px)} 0 rgba(0, 10, 20, 0.2));
|
||||
|
||||
&.unpinable {
|
||||
> canvas {
|
||||
cursor: pointer;
|
||||
pointer-events: all;
|
||||
}
|
||||
}
|
||||
|
||||
> canvas {
|
||||
@include S(width, 25px);
|
||||
@include S(height, 25px);
|
||||
grid-column: 1 / 2;
|
||||
grid-row: 1 / 3;
|
||||
pointer-events: all;
|
||||
transition: transform 0.1s ease-in-out;
|
||||
transform-origin: D(2px) center;
|
||||
will-change: transform;
|
||||
position: relative;
|
||||
z-index: 20;
|
||||
&:hover {
|
||||
transform: scale(2);
|
||||
z-index: 21;
|
||||
}
|
||||
}
|
||||
|
||||
> .amountLabel,
|
||||
> .goalLabel {
|
||||
@include S(margin-left, 5px);
|
||||
@include SuperSmallText;
|
||||
font-weight: bold;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
grid-column: 2 / 3;
|
||||
@include S(height, 9px);
|
||||
|
||||
@include DarkThemeOverride {
|
||||
color: #eee;
|
||||
}
|
||||
}
|
||||
|
||||
> .goalLabel {
|
||||
@include S(font-size, 7px);
|
||||
opacity: 0.9;
|
||||
align-self: start;
|
||||
justify-self: start;
|
||||
font-weight: normal;
|
||||
grid-row: 2 / 3;
|
||||
}
|
||||
|
||||
> .amountLabel {
|
||||
align-self: end;
|
||||
justify-self: start;
|
||||
grid-row: 1 / 2;
|
||||
}
|
||||
|
||||
> .infoButton {
|
||||
@include S(width, 8px);
|
||||
@include S(height, 8px);
|
||||
background: uiResource("icons/info_button.png") center center / 95% no-repeat;
|
||||
position: absolute;
|
||||
opacity: 0.7;
|
||||
@include S(top, 13px);
|
||||
@include S(left, -7px);
|
||||
@include DarkThemeInvert;
|
||||
@include IncreasedClickArea(2px);
|
||||
transition: opacity 0.12s ease-in-out;
|
||||
z-index: 100;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
&.goal,
|
||||
&.blueprint {
|
||||
.amountLabel::after {
|
||||
content: " ";
|
||||
position: absolute;
|
||||
display: inline-block;
|
||||
@include S(width, 8px);
|
||||
@include S(height, 8px);
|
||||
@include S(top, 4px);
|
||||
@include S(left, -7px);
|
||||
background: center center / contain no-repeat;
|
||||
}
|
||||
|
||||
&.goal .amountLabel {
|
||||
&::after {
|
||||
background-image: uiResource("icons/current_goal_marker.png");
|
||||
background-size: 90%;
|
||||
}
|
||||
@include DarkThemeOverride {
|
||||
&::after {
|
||||
background-image: uiResource("icons/current_goal_marker_inverted.png") !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.blueprint .amountLabel {
|
||||
&::after {
|
||||
background-image: uiResource("icons/blueprint_marker.png");
|
||||
background-size: 90%;
|
||||
}
|
||||
@include DarkThemeOverride {
|
||||
&::after {
|
||||
background-image: uiResource("icons/blueprint_marker_inverted.png") !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.completed {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
#ingame_HUD_PinnedShapes {
|
||||
position: absolute;
|
||||
@include S(left, 9px);
|
||||
@include S(top, 150px);
|
||||
@include PlainText;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
|
||||
> .shape {
|
||||
position: relative;
|
||||
display: grid;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
grid-template-columns: auto 1fr;
|
||||
grid-template-rows: 1fr 1fr;
|
||||
@include S(margin-bottom, 4px);
|
||||
color: #333438;
|
||||
// text-shadow: #{D(1px)} #{D(1px)} 0 rgba(0, 10, 20, 0.2);
|
||||
|
||||
&.unpinable {
|
||||
> canvas {
|
||||
cursor: pointer;
|
||||
pointer-events: all;
|
||||
}
|
||||
}
|
||||
|
||||
> canvas {
|
||||
@include S(width, 25px);
|
||||
@include S(height, 25px);
|
||||
grid-column: 1 / 2;
|
||||
grid-row: 1 / 3;
|
||||
pointer-events: all;
|
||||
transition: transform 0.1s ease-in-out;
|
||||
transform-origin: D(2px) center;
|
||||
will-change: transform;
|
||||
position: relative;
|
||||
z-index: 20;
|
||||
&:hover {
|
||||
transform: scale(2);
|
||||
z-index: 21;
|
||||
}
|
||||
}
|
||||
|
||||
> .amountLabel,
|
||||
> .goalLabel {
|
||||
@include S(margin-left, 5px);
|
||||
@include SuperSmallText;
|
||||
font-weight: bold;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
grid-column: 2 / 3;
|
||||
@include S(height, 9px);
|
||||
|
||||
@include DarkThemeOverride {
|
||||
color: #eee;
|
||||
}
|
||||
}
|
||||
|
||||
> .goalLabel {
|
||||
@include S(font-size, 7px);
|
||||
opacity: 0.9;
|
||||
align-self: start;
|
||||
justify-self: start;
|
||||
font-weight: normal;
|
||||
grid-row: 2 / 3;
|
||||
}
|
||||
|
||||
> .amountLabel {
|
||||
align-self: end;
|
||||
justify-self: start;
|
||||
grid-row: 1 / 2;
|
||||
}
|
||||
|
||||
> .infoButton {
|
||||
@include S(width, 8px);
|
||||
@include S(height, 8px);
|
||||
background: uiResource("icons/info_button.png") center center / 95% no-repeat;
|
||||
position: absolute;
|
||||
opacity: 0.7;
|
||||
@include S(top, 13px);
|
||||
@include S(left, -7px);
|
||||
@include DarkThemeInvert;
|
||||
@include IncreasedClickArea(2px);
|
||||
transition: opacity 0.12s ease-in-out;
|
||||
z-index: 100;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
&.goal,
|
||||
&.blueprint {
|
||||
.amountLabel::after {
|
||||
content: " ";
|
||||
position: absolute;
|
||||
display: inline-block;
|
||||
@include S(width, 8px);
|
||||
@include S(height, 8px);
|
||||
@include S(top, 4px);
|
||||
@include S(left, -7px);
|
||||
background: center center / contain no-repeat;
|
||||
}
|
||||
|
||||
&.goal .amountLabel {
|
||||
&::after {
|
||||
background-image: uiResource("icons/current_goal_marker.png");
|
||||
background-size: 90%;
|
||||
}
|
||||
@include DarkThemeOverride {
|
||||
&::after {
|
||||
background-image: uiResource("icons/current_goal_marker_inverted.png") !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.blueprint .amountLabel {
|
||||
&::after {
|
||||
background-image: uiResource("icons/blueprint_marker.png");
|
||||
background-size: 90%;
|
||||
}
|
||||
@include DarkThemeOverride {
|
||||
&::after {
|
||||
background-image: uiResource("icons/blueprint_marker_inverted.png") !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.completed {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,161 +1,154 @@
|
||||
#ingame_HUD_ShapeViewer {
|
||||
$dims: 170px;
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
@include S(width, $dims);
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
overflow-x: hidden;
|
||||
|
||||
&[data-layers="3"],
|
||||
&[data-layers="4"] {
|
||||
@include S(width, 2 * $dims);
|
||||
.renderArea {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
@include S(grid-row-gap, 15px);
|
||||
}
|
||||
}
|
||||
|
||||
.renderArea {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
@include S(grid-row-gap, 10px);
|
||||
align-items: center;
|
||||
justify-items: center;
|
||||
}
|
||||
|
||||
.infoArea {
|
||||
align-self: flex-end;
|
||||
@include S(margin-top, 10px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
button {
|
||||
@include S(margin, 0);
|
||||
@include PlainText;
|
||||
}
|
||||
}
|
||||
|
||||
.seperator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.layer {
|
||||
position: relative;
|
||||
background: #eee;
|
||||
|
||||
@include DarkThemeOverride {
|
||||
background: rgba(0, 10, 20, 0.2);
|
||||
}
|
||||
@include S(width, 150px);
|
||||
@include S(height, 100px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
> canvas {
|
||||
@include S(width, 50px);
|
||||
@include S(height, 50px);
|
||||
}
|
||||
|
||||
.quad {
|
||||
position: absolute;
|
||||
width: 50%;
|
||||
height: 50%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
|
||||
$arrowDims: 23px;
|
||||
$spacing: 9px;
|
||||
@include S(padding, 6px);
|
||||
|
||||
.colorLabel {
|
||||
text-transform: uppercase;
|
||||
@include SuperSmallText;
|
||||
@include S(font-size, 9px);
|
||||
}
|
||||
|
||||
.emptyLabel {
|
||||
text-transform: uppercase;
|
||||
@include SuperSmallText;
|
||||
@include S(font-size, 9px);
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: " ";
|
||||
background: rgba(0, 10, 20, 0.5);
|
||||
@include S(width, $arrowDims);
|
||||
@include S(height, 1px);
|
||||
position: absolute;
|
||||
transform: rotate(45deg);
|
||||
transform-origin: 50% 50%;
|
||||
}
|
||||
@include DarkThemeOverride {
|
||||
&::after {
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
&.quad-0 {
|
||||
right: 0;
|
||||
top: 0;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-end;
|
||||
|
||||
&::after {
|
||||
@include S(left, $spacing);
|
||||
@include S(bottom, $arrowDims / 2 + $spacing);
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
}
|
||||
&.quad-1 {
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
|
||||
align-items: flex-end;
|
||||
justify-content: flex-end;
|
||||
|
||||
&::after {
|
||||
@include S(left, $spacing);
|
||||
@include S(top, $arrowDims / 2 + $spacing);
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
}
|
||||
&.quad-2 {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
|
||||
align-items: flex-end;
|
||||
justify-content: flex-start;
|
||||
|
||||
&::after {
|
||||
@include S(right, $spacing);
|
||||
@include S(top, $arrowDims / 2 + $spacing);
|
||||
transform: rotate(135deg);
|
||||
}
|
||||
}
|
||||
&.quad-3 {
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
|
||||
&::after {
|
||||
@include S(right, $spacing);
|
||||
@include S(bottom, $arrowDims / 2 + $spacing);
|
||||
transform: rotate(225deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#ingame_HUD_ShapeViewer {
|
||||
$dims: 170px;
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
@include S(width, $dims);
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
overflow-x: hidden;
|
||||
|
||||
&[data-layers="3"],
|
||||
&[data-layers="4"] {
|
||||
@include S(width, 2 * $dims);
|
||||
.renderArea {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
@include S(grid-row-gap, 15px);
|
||||
}
|
||||
}
|
||||
|
||||
.renderArea {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
@include S(grid-row-gap, 10px);
|
||||
align-items: center;
|
||||
justify-items: center;
|
||||
}
|
||||
|
||||
.infoArea {
|
||||
align-self: flex-end;
|
||||
@include S(margin-top, 10px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
button {
|
||||
@include S(margin, 0);
|
||||
@include PlainText;
|
||||
}
|
||||
}
|
||||
|
||||
.layer {
|
||||
position: relative;
|
||||
background: #eee;
|
||||
|
||||
@include DarkThemeOverride {
|
||||
background: rgba(0, 10, 20, 0.2);
|
||||
}
|
||||
@include S(width, 150px);
|
||||
@include S(height, 100px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
> canvas {
|
||||
@include S(width, 50px);
|
||||
@include S(height, 50px);
|
||||
}
|
||||
|
||||
.quad {
|
||||
position: absolute;
|
||||
width: 50%;
|
||||
height: 50%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
|
||||
$arrowDims: 23px;
|
||||
$spacing: 9px;
|
||||
@include S(padding, 6px);
|
||||
|
||||
.colorLabel {
|
||||
text-transform: uppercase;
|
||||
@include SuperSmallText;
|
||||
@include S(font-size, 9px);
|
||||
}
|
||||
|
||||
.emptyLabel {
|
||||
text-transform: uppercase;
|
||||
@include SuperSmallText;
|
||||
@include S(font-size, 9px);
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: " ";
|
||||
background: rgba(0, 10, 20, 0.5);
|
||||
@include S(width, $arrowDims);
|
||||
@include S(height, 1px);
|
||||
position: absolute;
|
||||
transform: rotate(45deg);
|
||||
transform-origin: 50% 50%;
|
||||
}
|
||||
@include DarkThemeOverride {
|
||||
&::after {
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
&.quad-0 {
|
||||
right: 0;
|
||||
top: 0;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-end;
|
||||
|
||||
&::after {
|
||||
@include S(left, $spacing);
|
||||
@include S(bottom, $arrowDims / 2 + $spacing);
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
}
|
||||
&.quad-1 {
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
|
||||
align-items: flex-end;
|
||||
justify-content: flex-end;
|
||||
|
||||
&::after {
|
||||
@include S(left, $spacing);
|
||||
@include S(top, $arrowDims / 2 + $spacing);
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
}
|
||||
&.quad-2 {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
|
||||
align-items: flex-end;
|
||||
justify-content: flex-start;
|
||||
|
||||
&::after {
|
||||
@include S(right, $spacing);
|
||||
@include S(top, $arrowDims / 2 + $spacing);
|
||||
transform: rotate(135deg);
|
||||
}
|
||||
}
|
||||
&.quad-3 {
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
|
||||
&::after {
|
||||
@include S(right, $spacing);
|
||||
@include S(bottom, $arrowDims / 2 + $spacing);
|
||||
transform: rotate(225deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,37 +1,46 @@
|
||||
#state_InGameState {
|
||||
.gameLoadingOverlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 9999;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: all;
|
||||
display: flex;
|
||||
background: $mainBgColor;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#ingame_Canvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
#ingame_HUD_ModalDialogs {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
@include DarkThemeOverride {
|
||||
.gameLoadingOverlay {
|
||||
background: $darkModeGameBackground;
|
||||
}
|
||||
}
|
||||
}
|
||||
#state_InGameState {
|
||||
.gameLoadingOverlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 9999;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: all;
|
||||
display: flex;
|
||||
background: $mainBgColor;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.hint {
|
||||
position: absolute;
|
||||
@include S(bottom, 40px);
|
||||
@include S(left, 20px);
|
||||
@include S(right, 20px);
|
||||
@include PlainText;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#ingame_Canvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
#ingame_HUD_ModalDialogs {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
@include DarkThemeOverride {
|
||||
.gameLoadingOverlay {
|
||||
background: $darkModeGameBackground;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,145 +1,145 @@
|
||||
#state_PreloadState {
|
||||
&.failure {
|
||||
.loadingImage,
|
||||
.loadingStatus {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.changelogDialogEntry {
|
||||
margin-top: 10px;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
text-align: left;
|
||||
padding: 10px;
|
||||
box-sizing: border-box;
|
||||
background: #eef1f4;
|
||||
|
||||
@include DarkThemeOverride {
|
||||
background: #424242;
|
||||
}
|
||||
|
||||
.version {
|
||||
@include Heading;
|
||||
}
|
||||
.date {
|
||||
@include PlainText;
|
||||
&::before {
|
||||
content: " | ";
|
||||
}
|
||||
color: #aaabaf;
|
||||
}
|
||||
|
||||
.changes {
|
||||
@include PlainText;
|
||||
@include S(padding-left, 15px);
|
||||
strong {
|
||||
background: $colorBlueBright;
|
||||
color: #fff;
|
||||
text-transform: uppercase;
|
||||
@include S(padding, 1px, 2px);
|
||||
@include S(margin-right, 3px);
|
||||
}
|
||||
a {
|
||||
color: $colorBlueBright;
|
||||
}
|
||||
li {
|
||||
@include SuperSmallText;
|
||||
@include S(margin-bottom, 10px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.failureBox {
|
||||
.logo {
|
||||
img {
|
||||
@include S(width, 240px);
|
||||
}
|
||||
|
||||
@include S(margin-bottom, 30px);
|
||||
}
|
||||
|
||||
@include InlineAnimation(0.3s ease-in-out) {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.failureInner {
|
||||
// background: darken($mainBgColor, 6);
|
||||
@include S(max-width, 350px);
|
||||
margin: 0 20px;
|
||||
text-align: left;
|
||||
|
||||
@include BoxShadow3D(#fff);
|
||||
@include S(padding, 15px);
|
||||
@include S(border-radius, $globalBorderRadius);
|
||||
@include DropShadow;
|
||||
|
||||
.errorHeader {
|
||||
color: #ef5072;
|
||||
}
|
||||
|
||||
.errorMessage {
|
||||
@include PlainText;
|
||||
display: block;
|
||||
color: #666;
|
||||
text-align: left;
|
||||
@include BreakText;
|
||||
hyphens: auto;
|
||||
// border: dotted #666;
|
||||
// @include S(border-width, 1px, 0);
|
||||
@include S(padding, 10px, 0);
|
||||
@include S(margin-top, 10px);
|
||||
}
|
||||
|
||||
.supportHelp {
|
||||
@include S(margin-top, 10px);
|
||||
@include PlainText;
|
||||
|
||||
.email {
|
||||
color: $themeColor;
|
||||
cursor: pointer;
|
||||
pointer-events: all;
|
||||
}
|
||||
}
|
||||
|
||||
.lower {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@include S(margin-top, 16px);
|
||||
|
||||
i {
|
||||
flex-grow: 1;
|
||||
text-align: right;
|
||||
color: #777;
|
||||
@include PlainText;
|
||||
}
|
||||
|
||||
button.resetApp {
|
||||
@include Button3D($colorRedBright);
|
||||
@include PlainText;
|
||||
@include S(padding, 5px, 8px, 4px);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
.status {
|
||||
transform: scale(0.7) $hardwareAcc;
|
||||
opacity: 0;
|
||||
@include StateAnim(transform, opacity);
|
||||
}
|
||||
|
||||
&.arrived {
|
||||
.status {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
#state_PreloadState {
|
||||
&.failure {
|
||||
.loadingImage,
|
||||
.loadingStatus {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.changelogDialogEntry {
|
||||
margin-top: 10px;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
text-align: left;
|
||||
padding: 10px;
|
||||
box-sizing: border-box;
|
||||
background: #eef1f4;
|
||||
|
||||
@include DarkThemeOverride {
|
||||
background: #424242;
|
||||
}
|
||||
|
||||
.version {
|
||||
@include Heading;
|
||||
}
|
||||
.date {
|
||||
@include PlainText;
|
||||
&::before {
|
||||
content: " | ";
|
||||
}
|
||||
color: #aaabaf;
|
||||
}
|
||||
|
||||
.changes {
|
||||
@include PlainText;
|
||||
@include S(padding-left, 15px);
|
||||
strong {
|
||||
background: $colorBlueBright;
|
||||
color: #fff;
|
||||
text-transform: uppercase;
|
||||
@include S(padding, 1px, 2px);
|
||||
@include S(margin-right, 3px);
|
||||
}
|
||||
a {
|
||||
color: $colorBlueBright;
|
||||
}
|
||||
li {
|
||||
@include SuperSmallText;
|
||||
@include S(margin-bottom, 10px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.failureBox {
|
||||
.logo {
|
||||
img {
|
||||
@include S(width, 240px);
|
||||
}
|
||||
|
||||
@include S(margin-bottom, 30px);
|
||||
}
|
||||
|
||||
@include InlineAnimation(0.3s ease-in-out) {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.failureInner {
|
||||
// background: darken($mainBgColor, 6);
|
||||
@include S(max-width, 350px);
|
||||
margin: 0 20px;
|
||||
text-align: left;
|
||||
|
||||
@include BoxShadow3D(#fff);
|
||||
@include S(padding, 15px);
|
||||
@include S(border-radius, $globalBorderRadius);
|
||||
@include DropShadow;
|
||||
|
||||
.errorHeader {
|
||||
color: #ef5072;
|
||||
}
|
||||
|
||||
.errorMessage {
|
||||
@include PlainText;
|
||||
display: block;
|
||||
color: #666;
|
||||
text-align: left;
|
||||
@include BreakText;
|
||||
hyphens: auto;
|
||||
// border: dotted #666;
|
||||
// @include S(border-width, 1px, 0);
|
||||
@include S(padding, 10px, 0);
|
||||
@include S(margin-top, 10px);
|
||||
}
|
||||
|
||||
.supportHelp {
|
||||
@include S(margin-top, 10px);
|
||||
@include PlainText;
|
||||
|
||||
.email {
|
||||
color: $themeColor;
|
||||
cursor: pointer;
|
||||
pointer-events: all;
|
||||
}
|
||||
}
|
||||
|
||||
.lower {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@include S(margin-top, 16px);
|
||||
|
||||
i {
|
||||
flex-grow: 1;
|
||||
text-align: right;
|
||||
color: #777;
|
||||
@include PlainText;
|
||||
}
|
||||
|
||||
button.resetApp {
|
||||
@include Button3D($colorRedBright);
|
||||
@include PlainText;
|
||||
@include S(padding, 5px, 8px, 4px);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
.status {
|
||||
transform: scale(0.7) $hardwareAcc;
|
||||
opacity: 0;
|
||||
@include StateAnim(transform, opacity);
|
||||
}
|
||||
|
||||
&.arrived {
|
||||
.status {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -109,7 +109,7 @@ export const CHANGELOG = [
|
||||
date: "17.06.2020",
|
||||
entries: [
|
||||
"You can now place straight belts (and tunnels) by holding SHIFT! (For you, @giantwaffle ❤️)",
|
||||
"Added continue button to main menu and add seperate 'New game' button (by jaysc)",
|
||||
"Added continue button to main menu and add separate 'New game' button (by jaysc)",
|
||||
"Added setting to disable smart tunnel placement introduced with the last update",
|
||||
"Added setting to disable vignette",
|
||||
"Update translations",
|
||||
|
||||
@ -13,7 +13,7 @@ import { round1Digit } from "./utils";
|
||||
|
||||
const logger = createLogger("buffers");
|
||||
|
||||
const bufferGcDurationSeconds = 5;
|
||||
const bufferGcDurationSeconds = 0.5;
|
||||
|
||||
export class BufferMaintainer {
|
||||
/**
|
||||
|
||||
@ -25,17 +25,43 @@ export function disableImageSmoothing(context) {
|
||||
context.webkitImageSmoothingEnabled = false;
|
||||
}
|
||||
|
||||
const registeredCanvas = [];
|
||||
const freeCanvasList = [];
|
||||
/**
|
||||
* @typedef {{
|
||||
* canvas: HTMLCanvasElement,
|
||||
* context: CanvasRenderingContext2D
|
||||
* }} CanvasCacheEntry
|
||||
*/
|
||||
|
||||
let vramUsage = 0;
|
||||
let bufferCount = 0;
|
||||
/**
|
||||
* @type {Array<CanvasCacheEntry>}
|
||||
*/
|
||||
const registeredCanvas = [];
|
||||
|
||||
/**
|
||||
* Buckets for each width * height combination
|
||||
* @type {Map<number, Array<CanvasCacheEntry>>}
|
||||
*/
|
||||
const freeCanvasBuckets = new Map();
|
||||
|
||||
/**
|
||||
* Track statistics
|
||||
*/
|
||||
const stats = {
|
||||
vramUsage: 0,
|
||||
backlogVramUsage: 0,
|
||||
bufferCount: 0,
|
||||
numReused: 0,
|
||||
numCreated: 0,
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {HTMLCanvasElement} canvas
|
||||
*/
|
||||
export function getBufferVramUsageBytes(canvas) {
|
||||
assert(canvas, "no canvas given");
|
||||
assert(Number.isFinite(canvas.width), "bad canvas width: " + canvas.width);
|
||||
assert(Number.isFinite(canvas.height), "bad canvas height" + canvas.height);
|
||||
return canvas.width * canvas.height * 4;
|
||||
}
|
||||
|
||||
@ -43,17 +69,31 @@ export function getBufferVramUsageBytes(canvas) {
|
||||
* Returns stats on the allocated buffers
|
||||
*/
|
||||
export function getBufferStats() {
|
||||
let numBuffersFree = 0;
|
||||
freeCanvasBuckets.forEach(bucket => {
|
||||
numBuffersFree += bucket.length;
|
||||
});
|
||||
|
||||
return {
|
||||
vramUsage,
|
||||
bufferCount,
|
||||
backlog: freeCanvasList.length,
|
||||
...stats,
|
||||
backlogKeys: freeCanvasBuckets.size,
|
||||
backlogSize: numBuffersFree,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the backlog buffers if they grew too much
|
||||
*/
|
||||
export function clearBufferBacklog() {
|
||||
while (freeCanvasList.length > 50) {
|
||||
freeCanvasList.pop();
|
||||
}
|
||||
freeCanvasBuckets.forEach(bucket => {
|
||||
while (bucket.length > 500) {
|
||||
const entry = bucket[bucket.length - 1];
|
||||
stats.backlogVramUsage -= getBufferVramUsageBytes(entry.canvas);
|
||||
delete entry.canvas;
|
||||
delete entry.context;
|
||||
bucket.pop();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -84,53 +124,29 @@ export function makeOffscreenBuffer(w, h, { smooth = true, reusable = true, labe
|
||||
let canvas = null;
|
||||
let context = null;
|
||||
|
||||
let bestMatchingOne = null;
|
||||
let bestMatchingPixelsDiff = 1e50;
|
||||
|
||||
const currentPixels = w * h;
|
||||
|
||||
// Ok, search in cache first
|
||||
for (let i = 0; i < freeCanvasList.length; ++i) {
|
||||
const { canvas: useableCanvas, context: useableContext } = freeCanvasList[i];
|
||||
const bucket = freeCanvasBuckets.get(w * h) || [];
|
||||
|
||||
for (let i = 0; i < bucket.length; ++i) {
|
||||
const { canvas: useableCanvas, context: useableContext } = bucket[i];
|
||||
if (useableCanvas.width === w && useableCanvas.height === h) {
|
||||
// Ok we found one
|
||||
canvas = useableCanvas;
|
||||
context = useableContext;
|
||||
|
||||
fastArrayDelete(freeCanvasList, i);
|
||||
// Restore past state
|
||||
context.restore();
|
||||
context.save();
|
||||
context.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
delete canvas.style.width;
|
||||
delete canvas.style.height;
|
||||
|
||||
stats.numReused++;
|
||||
stats.backlogVramUsage -= getBufferVramUsageBytes(canvas);
|
||||
fastArrayDelete(bucket, i);
|
||||
break;
|
||||
}
|
||||
|
||||
const otherPixels = useableCanvas.width * useableCanvas.height;
|
||||
const diff = Math.abs(otherPixels - currentPixels);
|
||||
if (diff < bestMatchingPixelsDiff) {
|
||||
bestMatchingPixelsDiff = diff;
|
||||
bestMatchingOne = {
|
||||
canvas: useableCanvas,
|
||||
context: useableContext,
|
||||
index: i,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Ok none matching, reuse one though
|
||||
if (!canvas && bestMatchingOne) {
|
||||
canvas = bestMatchingOne.canvas;
|
||||
context = bestMatchingOne.context;
|
||||
canvas.width = w;
|
||||
canvas.height = h;
|
||||
fastArrayDelete(freeCanvasList, bestMatchingOne.index);
|
||||
}
|
||||
|
||||
// Reset context
|
||||
if (context) {
|
||||
// Restore past state
|
||||
context.restore();
|
||||
context.save();
|
||||
context.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
delete canvas.style.width;
|
||||
delete canvas.style.height;
|
||||
}
|
||||
|
||||
// None found , create new one
|
||||
@ -138,6 +154,8 @@ export function makeOffscreenBuffer(w, h, { smooth = true, reusable = true, labe
|
||||
canvas = document.createElement("canvas");
|
||||
context = canvas.getContext("2d" /*, { alpha } */);
|
||||
|
||||
stats.numCreated++;
|
||||
|
||||
canvas.width = w;
|
||||
canvas.height = h;
|
||||
|
||||
@ -145,6 +163,7 @@ export function makeOffscreenBuffer(w, h, { smooth = true, reusable = true, labe
|
||||
context.save();
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
canvas.label = label;
|
||||
|
||||
if (smooth) {
|
||||
@ -167,8 +186,9 @@ export function makeOffscreenBuffer(w, h, { smooth = true, reusable = true, labe
|
||||
export function registerCanvas(canvas, context) {
|
||||
registeredCanvas.push({ canvas, context });
|
||||
|
||||
bufferCount += 1;
|
||||
vramUsage += getBufferVramUsageBytes(canvas);
|
||||
stats.bufferCount += 1;
|
||||
const bytesUsed = getBufferVramUsageBytes(canvas);
|
||||
stats.vramUsage += bytesUsed;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -180,6 +200,7 @@ export function freeCanvas(canvas) {
|
||||
|
||||
let index = -1;
|
||||
let data = null;
|
||||
|
||||
for (let i = 0; i < registeredCanvas.length; ++i) {
|
||||
if (registeredCanvas[i].canvas === canvas) {
|
||||
index = i;
|
||||
@ -193,8 +214,18 @@ export function freeCanvas(canvas) {
|
||||
return;
|
||||
}
|
||||
fastArrayDelete(registeredCanvas, index);
|
||||
freeCanvasList.push(data);
|
||||
|
||||
bufferCount -= 1;
|
||||
vramUsage -= getBufferVramUsageBytes(canvas);
|
||||
const key = canvas.width * canvas.height;
|
||||
const bucket = freeCanvasBuckets.get(key);
|
||||
if (bucket) {
|
||||
bucket.push(data);
|
||||
} else {
|
||||
freeCanvasBuckets.set(key, [data]);
|
||||
}
|
||||
|
||||
stats.bufferCount -= 1;
|
||||
|
||||
const bytesUsed = getBufferVramUsageBytes(canvas);
|
||||
stats.vramUsage -= bytesUsed;
|
||||
stats.backlogVramUsage += bytesUsed;
|
||||
}
|
||||
|
||||
@ -84,8 +84,8 @@ export const globalConfig = {
|
||||
// Global game speed
|
||||
gameSpeed: 1,
|
||||
|
||||
warmupTimeSecondsFast: 0.1,
|
||||
warmupTimeSecondsRegular: 1,
|
||||
warmupTimeSecondsFast: 0.5,
|
||||
warmupTimeSecondsRegular: 3,
|
||||
|
||||
smoothing: {
|
||||
smoothMainCanvas: smoothCanvas && true,
|
||||
@ -132,5 +132,5 @@ if (G_IS_DEV && globalConfig.debug.renderForTrailer) {
|
||||
}
|
||||
|
||||
if (globalConfig.debug.fastGameEnter) {
|
||||
globalConfig.debug.noArtificalDelays = true;
|
||||
globalConfig.debug.noArtificialDelays = true;
|
||||
}
|
||||
|
||||
@ -1,230 +1,230 @@
|
||||
import { makeOffscreenBuffer } from "./buffer_utils";
|
||||
import { AtlasSprite, BaseSprite, RegularSprite, SpriteAtlasLink } from "./sprites";
|
||||
import { cachebust } from "./cachebust";
|
||||
import { createLogger } from "./logging";
|
||||
|
||||
/**
|
||||
* @typedef {import("../application").Application} Application
|
||||
* @typedef {import("./atlas_definitions").AtlasDefinition} AtlasDefinition;
|
||||
*/
|
||||
|
||||
const logger = createLogger("loader");
|
||||
|
||||
const missingSpriteIds = {};
|
||||
|
||||
class LoaderImpl {
|
||||
constructor() {
|
||||
this.app = null;
|
||||
|
||||
/** @type {Map<string, BaseSprite>} */
|
||||
this.sprites = new Map();
|
||||
|
||||
this.rawImages = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Application} app
|
||||
*/
|
||||
linkAppAfterBoot(app) {
|
||||
this.app = app;
|
||||
this.makeSpriteNotFoundCanvas();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a given sprite from the cache
|
||||
* @param {string} key
|
||||
* @returns {BaseSprite}
|
||||
*/
|
||||
getSpriteInternal(key) {
|
||||
const sprite = this.sprites.get(key);
|
||||
if (!sprite) {
|
||||
if (!missingSpriteIds[key]) {
|
||||
// Only show error once
|
||||
missingSpriteIds[key] = true;
|
||||
logger.error("Sprite '" + key + "' not found!");
|
||||
}
|
||||
return this.spriteNotFoundSprite;
|
||||
}
|
||||
return sprite;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an atlas sprite from the cache
|
||||
* @param {string} key
|
||||
* @returns {AtlasSprite}
|
||||
*/
|
||||
getSprite(key) {
|
||||
const sprite = this.getSpriteInternal(key);
|
||||
assert(sprite instanceof AtlasSprite || sprite === this.spriteNotFoundSprite, "Not an atlas sprite");
|
||||
return /** @type {AtlasSprite} */ (sprite);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a regular sprite from the cache
|
||||
* @param {string} key
|
||||
* @returns {RegularSprite}
|
||||
*/
|
||||
getRegularSprite(key) {
|
||||
const sprite = this.getSpriteInternal(key);
|
||||
assert(
|
||||
sprite instanceof RegularSprite || sprite === this.spriteNotFoundSprite,
|
||||
"Not a regular sprite"
|
||||
);
|
||||
return /** @type {RegularSprite} */ (sprite);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} key
|
||||
* @returns {Promise<HTMLImageElement|null>}
|
||||
*/
|
||||
internalPreloadImage(key) {
|
||||
const url = cachebust("res/" + key);
|
||||
const image = new Image();
|
||||
|
||||
let triesSoFar = 0;
|
||||
|
||||
return Promise.race([
|
||||
new Promise((resolve, reject) => {
|
||||
setTimeout(reject, G_IS_DEV ? 500 : 10000);
|
||||
}),
|
||||
|
||||
new Promise(resolve => {
|
||||
image.onload = () => {
|
||||
image.onerror = null;
|
||||
image.onload = null;
|
||||
|
||||
if (typeof image.decode === "function") {
|
||||
// SAFARI: Image.decode() fails on safari with svgs -> we dont want to fail
|
||||
// on that
|
||||
// FIREFOX: Decode never returns if the image is in cache, so call it in background
|
||||
image.decode().then(
|
||||
() => null,
|
||||
() => null
|
||||
);
|
||||
}
|
||||
resolve(image);
|
||||
};
|
||||
|
||||
image.onerror = reason => {
|
||||
logger.warn("Failed to load '" + url + "':", reason);
|
||||
if (++triesSoFar < 4) {
|
||||
logger.log("Retrying to load image from", url);
|
||||
image.src = url + "?try=" + triesSoFar;
|
||||
} else {
|
||||
logger.warn("Failed to load", url, "after", triesSoFar, "tries with reason", reason);
|
||||
image.onerror = null;
|
||||
image.onload = null;
|
||||
resolve(null);
|
||||
}
|
||||
};
|
||||
|
||||
image.src = url;
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Preloads a sprite
|
||||
* @param {string} key
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
preloadCSSSprite(key) {
|
||||
return this.internalPreloadImage(key).then(image => {
|
||||
if (key.indexOf("game_misc") >= 0) {
|
||||
// Allow access to regular sprites
|
||||
this.sprites.set(key, new RegularSprite(image, image.width, image.height));
|
||||
}
|
||||
this.rawImages.push(image);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Preloads an atlas
|
||||
* @param {AtlasDefinition} atlas
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
preloadAtlas(atlas) {
|
||||
return this.internalPreloadImage(atlas.getFullSourcePath()).then(image => {
|
||||
// @ts-ignore
|
||||
image.label = atlas.sourceFileName;
|
||||
return this.internalParseAtlas(atlas, image);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {AtlasDefinition} atlas
|
||||
* @param {HTMLImageElement} loadedImage
|
||||
*/
|
||||
internalParseAtlas({ meta: { scale }, sourceData }, loadedImage) {
|
||||
this.rawImages.push(loadedImage);
|
||||
|
||||
for (const spriteName in sourceData) {
|
||||
const { frame, sourceSize, spriteSourceSize } = sourceData[spriteName];
|
||||
|
||||
let sprite = /** @type {AtlasSprite} */ (this.sprites.get(spriteName));
|
||||
|
||||
if (!sprite) {
|
||||
sprite = new AtlasSprite(spriteName);
|
||||
this.sprites.set(spriteName, sprite);
|
||||
}
|
||||
|
||||
const link = new SpriteAtlasLink({
|
||||
packedX: frame.x,
|
||||
packedY: frame.y,
|
||||
packedW: frame.w,
|
||||
packedH: frame.h,
|
||||
packOffsetX: spriteSourceSize.x,
|
||||
packOffsetY: spriteSourceSize.y,
|
||||
atlas: loadedImage,
|
||||
w: sourceSize.w,
|
||||
h: sourceSize.h,
|
||||
});
|
||||
sprite.linksByResolution[scale] = link;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes the canvas which shows the question mark, shown when a sprite was not found
|
||||
*/
|
||||
makeSpriteNotFoundCanvas() {
|
||||
const dims = 128;
|
||||
|
||||
const [canvas, context] = makeOffscreenBuffer(dims, dims, {
|
||||
smooth: false,
|
||||
label: "not-found-sprite",
|
||||
});
|
||||
context.fillStyle = "#f77";
|
||||
context.fillRect(0, 0, dims, dims);
|
||||
|
||||
context.textAlign = "center";
|
||||
context.textBaseline = "middle";
|
||||
context.fillStyle = "#eee";
|
||||
context.font = "25px Arial";
|
||||
context.fillText("???", dims / 2, dims / 2);
|
||||
|
||||
// TODO: Not sure why this is set here
|
||||
// @ts-ignore
|
||||
canvas.src = "not-found";
|
||||
|
||||
const sprite = new AtlasSprite("not-found");
|
||||
["0.1", "0.25", "0.5", "0.75", "1"].forEach(resolution => {
|
||||
sprite.linksByResolution[resolution] = new SpriteAtlasLink({
|
||||
packedX: 0,
|
||||
packedY: 0,
|
||||
w: dims,
|
||||
h: dims,
|
||||
packOffsetX: 0,
|
||||
packOffsetY: 0,
|
||||
packedW: dims,
|
||||
packedH: dims,
|
||||
atlas: canvas,
|
||||
});
|
||||
});
|
||||
|
||||
this.spriteNotFoundSprite = sprite;
|
||||
}
|
||||
}
|
||||
|
||||
export const Loader = new LoaderImpl();
|
||||
import { makeOffscreenBuffer } from "./buffer_utils";
|
||||
import { AtlasSprite, BaseSprite, RegularSprite, SpriteAtlasLink } from "./sprites";
|
||||
import { cachebust } from "./cachebust";
|
||||
import { createLogger } from "./logging";
|
||||
|
||||
/**
|
||||
* @typedef {import("../application").Application} Application
|
||||
* @typedef {import("./atlas_definitions").AtlasDefinition} AtlasDefinition;
|
||||
*/
|
||||
|
||||
const logger = createLogger("loader");
|
||||
|
||||
const missingSpriteIds = {};
|
||||
|
||||
class LoaderImpl {
|
||||
constructor() {
|
||||
this.app = null;
|
||||
|
||||
/** @type {Map<string, BaseSprite>} */
|
||||
this.sprites = new Map();
|
||||
|
||||
this.rawImages = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Application} app
|
||||
*/
|
||||
linkAppAfterBoot(app) {
|
||||
this.app = app;
|
||||
this.makeSpriteNotFoundCanvas();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a given sprite from the cache
|
||||
* @param {string} key
|
||||
* @returns {BaseSprite}
|
||||
*/
|
||||
getSpriteInternal(key) {
|
||||
const sprite = this.sprites.get(key);
|
||||
if (!sprite) {
|
||||
if (!missingSpriteIds[key]) {
|
||||
// Only show error once
|
||||
missingSpriteIds[key] = true;
|
||||
logger.error("Sprite '" + key + "' not found!");
|
||||
}
|
||||
return this.spriteNotFoundSprite;
|
||||
}
|
||||
return sprite;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an atlas sprite from the cache
|
||||
* @param {string} key
|
||||
* @returns {AtlasSprite}
|
||||
*/
|
||||
getSprite(key) {
|
||||
const sprite = this.getSpriteInternal(key);
|
||||
assert(sprite instanceof AtlasSprite || sprite === this.spriteNotFoundSprite, "Not an atlas sprite");
|
||||
return /** @type {AtlasSprite} */ (sprite);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a regular sprite from the cache
|
||||
* @param {string} key
|
||||
* @returns {RegularSprite}
|
||||
*/
|
||||
getRegularSprite(key) {
|
||||
const sprite = this.getSpriteInternal(key);
|
||||
assert(
|
||||
sprite instanceof RegularSprite || sprite === this.spriteNotFoundSprite,
|
||||
"Not a regular sprite"
|
||||
);
|
||||
return /** @type {RegularSprite} */ (sprite);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} key
|
||||
* @returns {Promise<HTMLImageElement|null>}
|
||||
*/
|
||||
internalPreloadImage(key) {
|
||||
const url = cachebust("res/" + key);
|
||||
const image = new Image();
|
||||
|
||||
let triesSoFar = 0;
|
||||
|
||||
return Promise.race([
|
||||
new Promise((resolve, reject) => {
|
||||
setTimeout(reject, G_IS_DEV ? 500 : 10000);
|
||||
}),
|
||||
|
||||
new Promise(resolve => {
|
||||
image.onload = () => {
|
||||
image.onerror = null;
|
||||
image.onload = null;
|
||||
|
||||
if (typeof image.decode === "function") {
|
||||
// SAFARI: Image.decode() fails on safari with svgs -> we dont want to fail
|
||||
// on that
|
||||
// FIREFOX: Decode never returns if the image is in cache, so call it in background
|
||||
image.decode().then(
|
||||
() => null,
|
||||
() => null
|
||||
);
|
||||
}
|
||||
resolve(image);
|
||||
};
|
||||
|
||||
image.onerror = reason => {
|
||||
logger.warn("Failed to load '" + url + "':", reason);
|
||||
if (++triesSoFar < 4) {
|
||||
logger.log("Retrying to load image from", url);
|
||||
image.src = url + "?try=" + triesSoFar;
|
||||
} else {
|
||||
logger.warn("Failed to load", url, "after", triesSoFar, "tries with reason", reason);
|
||||
image.onerror = null;
|
||||
image.onload = null;
|
||||
resolve(null);
|
||||
}
|
||||
};
|
||||
|
||||
image.src = url;
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Preloads a sprite
|
||||
* @param {string} key
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
preloadCSSSprite(key) {
|
||||
return this.internalPreloadImage(key).then(image => {
|
||||
if (key.indexOf("game_misc") >= 0) {
|
||||
// Allow access to regular sprites
|
||||
this.sprites.set(key, new RegularSprite(image, image.width, image.height));
|
||||
}
|
||||
this.rawImages.push(image);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Preloads an atlas
|
||||
* @param {AtlasDefinition} atlas
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
preloadAtlas(atlas) {
|
||||
return this.internalPreloadImage(atlas.getFullSourcePath()).then(image => {
|
||||
// @ts-ignore
|
||||
image.label = atlas.sourceFileName;
|
||||
return this.internalParseAtlas(atlas, image);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {AtlasDefinition} atlas
|
||||
* @param {HTMLImageElement} loadedImage
|
||||
*/
|
||||
internalParseAtlas({ meta: { scale }, sourceData }, loadedImage) {
|
||||
this.rawImages.push(loadedImage);
|
||||
|
||||
for (const spriteName in sourceData) {
|
||||
const { frame, sourceSize, spriteSourceSize } = sourceData[spriteName];
|
||||
|
||||
let sprite = /** @type {AtlasSprite} */ (this.sprites.get(spriteName));
|
||||
|
||||
if (!sprite) {
|
||||
sprite = new AtlasSprite(spriteName);
|
||||
this.sprites.set(spriteName, sprite);
|
||||
}
|
||||
|
||||
const link = new SpriteAtlasLink({
|
||||
packedX: frame.x,
|
||||
packedY: frame.y,
|
||||
packedW: frame.w,
|
||||
packedH: frame.h,
|
||||
packOffsetX: spriteSourceSize.x,
|
||||
packOffsetY: spriteSourceSize.y,
|
||||
atlas: loadedImage,
|
||||
w: sourceSize.w,
|
||||
h: sourceSize.h,
|
||||
});
|
||||
sprite.linksByResolution[scale] = link;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes the canvas which shows the question mark, shown when a sprite was not found
|
||||
*/
|
||||
makeSpriteNotFoundCanvas() {
|
||||
const dims = 128;
|
||||
|
||||
const [canvas, context] = makeOffscreenBuffer(dims, dims, {
|
||||
smooth: false,
|
||||
label: "not-found-sprite",
|
||||
});
|
||||
context.fillStyle = "#f77";
|
||||
context.fillRect(0, 0, dims, dims);
|
||||
|
||||
context.textAlign = "center";
|
||||
context.textBaseline = "middle";
|
||||
context.fillStyle = "#eee";
|
||||
context.font = "25px Arial";
|
||||
context.fillText("???", dims / 2, dims / 2);
|
||||
|
||||
// TODO: Not sure why this is set here
|
||||
// @ts-ignore
|
||||
canvas.src = "not-found";
|
||||
|
||||
const sprite = new AtlasSprite("not-found");
|
||||
["0.1", "0.25", "0.5", "0.75", "1"].forEach(resolution => {
|
||||
sprite.linksByResolution[resolution] = new SpriteAtlasLink({
|
||||
packedX: 0,
|
||||
packedY: 0,
|
||||
w: dims,
|
||||
h: dims,
|
||||
packOffsetX: 0,
|
||||
packOffsetY: 0,
|
||||
packedW: dims,
|
||||
packedH: dims,
|
||||
atlas: canvas,
|
||||
});
|
||||
});
|
||||
|
||||
this.spriteNotFoundSprite = sprite;
|
||||
}
|
||||
}
|
||||
|
||||
export const Loader = new LoaderImpl();
|
||||
|
||||
@ -30,10 +30,10 @@ export class Dialog {
|
||||
* @param {string} param0.title Title of the dialog
|
||||
* @param {string} param0.contentHTML Inner dialog html
|
||||
* @param {Array<string>} param0.buttons
|
||||
* Button list, each button contains of up to 3 parts seperated by ':'.
|
||||
* Button list, each button contains of up to 3 parts separated by ':'.
|
||||
* Part 0: The id, one of the one defined in dialog_buttons.yaml
|
||||
* Part 1: The style, either good, bad or misc
|
||||
* Part 2 (optional): Additional parameters seperated by '/', available are:
|
||||
* Part 2 (optional): Additional parameters separated by '/', available are:
|
||||
* timeout: This button is only available after some waiting time
|
||||
* kb_enter: This button is triggered by the enter key
|
||||
* kb_escape This button is triggered by the escape key
|
||||
|
||||
@ -224,7 +224,7 @@ export class ReadWriteProxy {
|
||||
return rawData;
|
||||
})
|
||||
|
||||
// Parse JSON, this could throw but thats fine
|
||||
// Parse JSON, this could throw but that's fine
|
||||
.then(res => {
|
||||
try {
|
||||
return JSON.parse(res);
|
||||
|
||||
@ -5,6 +5,8 @@ import { round3Digits } from "./utils";
|
||||
export const ORIGINAL_SPRITE_SCALE = "0.75";
|
||||
export const FULL_CLIP_RECT = new Rectangle(0, 0, 1, 1);
|
||||
|
||||
const EXTRUDE = 0.1;
|
||||
|
||||
export class BaseSprite {
|
||||
/**
|
||||
* Returns the raw handle
|
||||
@ -206,15 +208,15 @@ export class AtlasSprite extends BaseSprite {
|
||||
srcX,
|
||||
srcY,
|
||||
|
||||
// atlas src siize
|
||||
// atlas src size
|
||||
srcW,
|
||||
srcH,
|
||||
|
||||
// dest pos and size
|
||||
destX,
|
||||
destY,
|
||||
destW,
|
||||
destH
|
||||
destX - EXTRUDE,
|
||||
destY - EXTRUDE,
|
||||
destW + 2 * EXTRUDE,
|
||||
destH + 2 * EXTRUDE
|
||||
);
|
||||
}
|
||||
|
||||
@ -267,10 +269,10 @@ export class AtlasSprite extends BaseSprite {
|
||||
srcH,
|
||||
|
||||
// dest pos and size
|
||||
destX,
|
||||
destY,
|
||||
destW,
|
||||
destH
|
||||
destX - EXTRUDE,
|
||||
destY - EXTRUDE,
|
||||
destW + 2 * EXTRUDE,
|
||||
destH + 2 * EXTRUDE
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -40,9 +40,9 @@ export class StaleAreaDetector {
|
||||
* Makes this detector recompute the area of an entity whenever
|
||||
* it changes in any way
|
||||
* @param {Array<typeof Component>} components
|
||||
* @param {number} tilesAround
|
||||
* @param {number} tilesAround How many tiles arround to expand the area
|
||||
*/
|
||||
recomputeOnComponentsChanged(components, tilesAround = 1) {
|
||||
recomputeOnComponentsChanged(components, tilesAround) {
|
||||
const componentIds = components.map(component => component.getId());
|
||||
|
||||
/**
|
||||
@ -50,6 +50,10 @@ export class StaleAreaDetector {
|
||||
* @param {Entity} entity
|
||||
*/
|
||||
const checker = entity => {
|
||||
if (!this.root.gameInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for all components
|
||||
for (let i = 0; i < componentIds.length; ++i) {
|
||||
if (entity.components[componentIds[i]]) {
|
||||
|
||||
@ -2,7 +2,7 @@ import { globalConfig } from "../core/config";
|
||||
import { DrawParameters } from "../core/draw_parameters";
|
||||
import { createLogger } from "../core/logging";
|
||||
import { Rectangle } from "../core/rectangle";
|
||||
import { epsilonCompare, round4Digits, clamp } from "../core/utils";
|
||||
import { clamp, epsilonCompare, round4Digits } from "../core/utils";
|
||||
import { enumDirection, enumDirectionToVector, enumInvertedDirections, Vector } from "../core/vector";
|
||||
import { BasicSerializableObject, types } from "../savegame/serialization";
|
||||
import { BaseItem } from "./base_item";
|
||||
@ -1069,12 +1069,14 @@ export class BeltPath extends BasicSerializableObject {
|
||||
// Trigger animation on the acceptor comp
|
||||
const targetAcceptorComp = this.acceptorTarget.entity.components.ItemAcceptor;
|
||||
if (targetAcceptorComp) {
|
||||
targetAcceptorComp.onItemAccepted(
|
||||
this.acceptorTarget.slot,
|
||||
this.acceptorTarget.direction,
|
||||
item,
|
||||
remainingProgress
|
||||
);
|
||||
if (!this.root.app.settings.getAllSettings().simplifiedBelts) {
|
||||
targetAcceptorComp.onItemAccepted(
|
||||
this.acceptorTarget.slot,
|
||||
this.acceptorTarget.direction,
|
||||
item,
|
||||
remainingProgress
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
@ -1091,7 +1093,7 @@ export class BeltPath extends BasicSerializableObject {
|
||||
computePositionFromProgress(progress) {
|
||||
let currentLength = 0;
|
||||
|
||||
// floating point issuses ..
|
||||
// floating point issues ..
|
||||
assert(progress <= this.totalLength + 0.02, "Progress too big: " + progress);
|
||||
|
||||
for (let i = 0; i < this.entityPath.length; ++i) {
|
||||
@ -1179,6 +1181,35 @@ export class BeltPath extends BasicSerializableObject {
|
||||
parameters.context.fillRect(firstItemIndicator.x - 3, firstItemIndicator.y - 1, 6, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this belt path should render simplified
|
||||
*/
|
||||
checkIsPotatoMode() {
|
||||
// POTATO Mode: Only show items when belt is hovered
|
||||
if (!this.root.app.settings.getAllSettings().simplifiedBelts) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const mousePos = this.root.app.mousePosition;
|
||||
if (!mousePos) {
|
||||
// Mouse not registered
|
||||
return true;
|
||||
}
|
||||
|
||||
const tile = this.root.camera.screenToWorld(mousePos).toTileSpace();
|
||||
const contents = this.root.map.getLayerContentXY(tile.x, tile.y, "regular");
|
||||
if (!contents || !contents.components.Belt) {
|
||||
// Nothing below
|
||||
return true;
|
||||
}
|
||||
|
||||
if (contents.components.Belt.assignedPath !== this) {
|
||||
// Not this path
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the path
|
||||
* @param {DrawParameters} parameters
|
||||
@ -1193,6 +1224,30 @@ export class BeltPath extends BasicSerializableObject {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.checkIsPotatoMode()) {
|
||||
const firstItem = this.items[0];
|
||||
if (this.entityPath.length > 1 && firstItem) {
|
||||
const medianBeltIndex = clamp(
|
||||
Math.round(this.entityPath.length / 2 - 1),
|
||||
0,
|
||||
this.entityPath.length - 1
|
||||
);
|
||||
const medianBelt = this.entityPath[medianBeltIndex];
|
||||
const beltComp = medianBelt.components.Belt;
|
||||
const staticComp = medianBelt.components.StaticMapEntity;
|
||||
const centerPosLocal = beltComp.transformBeltToLocalSpace(
|
||||
this.entityPath.length % 2 === 0 ? beltComp.getEffectiveLengthTiles() : 0.5
|
||||
);
|
||||
const centerPos = staticComp.localTileToWorld(centerPosLocal).toWorldSpaceCenterOfTile();
|
||||
|
||||
parameters.context.globalAlpha = 0.5;
|
||||
firstItem[_item].drawItemCenteredClipped(centerPos.x, centerPos.y, parameters);
|
||||
parameters.context.globalAlpha = 1;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let currentItemPos = this.spacingToFirstItem;
|
||||
let currentItemIndex = 0;
|
||||
|
||||
@ -1206,7 +1261,7 @@ export class BeltPath extends BasicSerializableObject {
|
||||
|
||||
// Check if the current items are on the belt
|
||||
while (trackPos + beltLength >= currentItemPos - 1e-5) {
|
||||
// Its on the belt, render it now
|
||||
// It's on the belt, render it now
|
||||
const staticComp = entity.components.StaticMapEntity;
|
||||
assert(
|
||||
currentItemPos - trackPos >= 0,
|
||||
|
||||
@ -25,16 +25,27 @@ export const gBuildingVariants = {
|
||||
// Set later
|
||||
};
|
||||
|
||||
/**
|
||||
* Mapping from 'metaBuildingId/variant/rotationVariant' to building code
|
||||
* @type {Map<string, string>}
|
||||
*/
|
||||
const variantsCache = new Map();
|
||||
|
||||
/**
|
||||
* Registers a new variant
|
||||
* @param {number | string} id
|
||||
* @param {number | string} code
|
||||
* @param {typeof MetaBuilding} meta
|
||||
* @param {typeof MetaBuildingVariant} variant
|
||||
* @param {number} rotationVariant
|
||||
*/
|
||||
export function registerBuildingVariant(id, meta, variant, rotationVariant = 0) {
|
||||
assert(!gBuildingVariants[id], "Duplicate id: " + id);
|
||||
gBuildingVariants[id.toString()] = {
|
||||
export function registerBuildingVariant(
|
||||
code,
|
||||
meta,
|
||||
variant,
|
||||
rotationVariant = 0
|
||||
) {
|
||||
assert(!gBuildingVariants[code], "Duplicate id: " + code);
|
||||
gBuildingVariants[code.toString()] = {
|
||||
metaClass: meta,
|
||||
variant,
|
||||
rotationVariant,
|
||||
@ -53,31 +64,29 @@ export function getBuildingDataFromCode(code) {
|
||||
return gBuildingVariants[code];
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the cache for the codes
|
||||
*/
|
||||
export function buildBuildingCodeCache() {
|
||||
for (const code in gBuildingVariants) {
|
||||
const data = gBuildingVariants[code];
|
||||
const hash = data.metaInstance.getId() + "/" + data.variant + "/" + data.rotationVariant;
|
||||
variantsCache.set(hash, +code);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the code for a given variant
|
||||
* @param {MetaBuilding} metaBuilding
|
||||
* @param {typeof MetaBuildingVariant} variant
|
||||
* @param {number} rotationVariant
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getCodeFromBuildingData(metaBuilding, variant, rotationVariant) {
|
||||
for (const key in gBuildingVariants) {
|
||||
const data = gBuildingVariants[key];
|
||||
if (
|
||||
data.metaInstance.getId() === metaBuilding.getId() &&
|
||||
data.variant === variant &&
|
||||
data.rotationVariant === rotationVariant
|
||||
) {
|
||||
return key;
|
||||
}
|
||||
const hash = metaBuilding.getId() + "/" + variant.getId() + "/" + rotationVariant;
|
||||
const result = variantsCache.get(hash);
|
||||
if (G_IS_DEV) {
|
||||
assertAlways(!!result, "Building not found by data: " + hash);
|
||||
}
|
||||
assertAlways(
|
||||
false,
|
||||
"Building not found by data: " +
|
||||
metaBuilding.getId() +
|
||||
" / " +
|
||||
variant.getId() +
|
||||
" / " +
|
||||
rotationVariant
|
||||
);
|
||||
return "0";
|
||||
return result;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -2,7 +2,12 @@
|
||||
import { Application } from "../application";
|
||||
/* typehints:end */
|
||||
import { BufferMaintainer } from "../core/buffer_maintainer";
|
||||
import { disableImageSmoothing, enableImageSmoothing, registerCanvas } from "../core/buffer_utils";
|
||||
import {
|
||||
disableImageSmoothing,
|
||||
enableImageSmoothing,
|
||||
getBufferStats,
|
||||
registerCanvas,
|
||||
} from "../core/buffer_utils";
|
||||
import { globalConfig } from "../core/config";
|
||||
import { getDeviceDPI, resizeHighDPICanvas } from "../core/dpi_manager";
|
||||
import { DrawParameters } from "../core/draw_parameters";
|
||||
@ -220,9 +225,6 @@ export class GameCore {
|
||||
lastContext.clearRect(0, 0, lastCanvas.width, lastCanvas.height);
|
||||
}
|
||||
|
||||
// globalConfig.smoothing.smoothMainCanvas = getDeviceDPI() < 1.5;
|
||||
// globalConfig.smoothing.smoothMainCanvas = true;
|
||||
|
||||
canvas.classList.toggle("smoothed", globalConfig.smoothing.smoothMainCanvas);
|
||||
|
||||
// Oof, use :not() instead
|
||||
@ -375,9 +377,9 @@ export class GameCore {
|
||||
(zoomLevel / globalConfig.assetsDpi) * getDeviceDPI() * globalConfig.assetsSharpness;
|
||||
|
||||
let desiredAtlasScale = "0.25";
|
||||
if (effectiveZoomLevel > 0.8 && !lowQuality) {
|
||||
if (effectiveZoomLevel > 0.5 && !lowQuality) {
|
||||
desiredAtlasScale = ORIGINAL_SPRITE_SCALE;
|
||||
} else if (effectiveZoomLevel > 0.4 && !lowQuality) {
|
||||
} else if (effectiveZoomLevel > 0.35 && !lowQuality) {
|
||||
desiredAtlasScale = "0.5";
|
||||
}
|
||||
|
||||
@ -501,18 +503,37 @@ export class GameCore {
|
||||
);
|
||||
|
||||
const stats = this.root.buffers.getStats();
|
||||
|
||||
context.fillText(
|
||||
"Buffers: " +
|
||||
"Maintained Buffers: " +
|
||||
stats.rootKeys +
|
||||
" root keys, " +
|
||||
" root keys / " +
|
||||
stats.subKeys +
|
||||
" sub keys / buffers / VRAM: " +
|
||||
" buffers / VRAM: " +
|
||||
round2Digits(stats.vramBytes / (1024 * 1024)) +
|
||||
" MB",
|
||||
|
||||
20,
|
||||
620
|
||||
);
|
||||
const internalStats = getBufferStats();
|
||||
context.fillText(
|
||||
"Total Buffers: " +
|
||||
internalStats.bufferCount +
|
||||
" buffers / " +
|
||||
internalStats.backlogSize +
|
||||
" backlog / " +
|
||||
internalStats.backlogKeys +
|
||||
" keys in backlog / VRAM " +
|
||||
round2Digits(internalStats.vramUsage / (1024 * 1024)) +
|
||||
" MB / Backlog " +
|
||||
round2Digits(internalStats.backlogVramUsage / (1024 * 1024)) +
|
||||
" MB / Created " +
|
||||
internalStats.numCreated +
|
||||
" / Reused " +
|
||||
internalStats.numReused,
|
||||
20,
|
||||
640
|
||||
);
|
||||
}
|
||||
|
||||
if (G_IS_DEV && globalConfig.debug.testClipping) {
|
||||
|
||||
@ -1,229 +1,221 @@
|
||||
/* typehints:start */
|
||||
import { DrawParameters } from "../core/draw_parameters";
|
||||
import { Component } from "./component";
|
||||
/* typehints:end */
|
||||
|
||||
import { GameRoot } from "./root";
|
||||
import { globalConfig } from "../core/config";
|
||||
import { enumDirectionToVector, enumDirectionToAngle } from "../core/vector";
|
||||
import { BasicSerializableObject, types } from "../savegame/serialization";
|
||||
import { EntityComponentStorage } from "./entity_components";
|
||||
import { Loader } from "../core/loader";
|
||||
import { drawRotatedSprite } from "../core/draw_utils";
|
||||
import { gComponentRegistry } from "../core/global_registries";
|
||||
|
||||
export class Entity extends BasicSerializableObject {
|
||||
/**
|
||||
* @param {GameRoot} root
|
||||
*/
|
||||
constructor(root) {
|
||||
super();
|
||||
|
||||
/**
|
||||
* Handle to the global game root
|
||||
*/
|
||||
this.root = root;
|
||||
|
||||
/**
|
||||
* The components of the entity
|
||||
*/
|
||||
this.components = new EntityComponentStorage();
|
||||
|
||||
/**
|
||||
* Whether this entity was registered on the @see EntityManager so far
|
||||
*/
|
||||
this.registered = false;
|
||||
|
||||
/**
|
||||
* On which layer this entity is
|
||||
* @type {Layer}
|
||||
*/
|
||||
this.layer = "regular";
|
||||
|
||||
/**
|
||||
* Internal entity unique id, set by the @see EntityManager
|
||||
*/
|
||||
this.uid = 0;
|
||||
|
||||
/* typehints:start */
|
||||
|
||||
/**
|
||||
* Stores if this entity is destroyed, set by the @see EntityManager
|
||||
* @type {boolean} */
|
||||
this.destroyed;
|
||||
|
||||
/**
|
||||
* Stores if this entity is queued to get destroyed in the next tick
|
||||
* of the @see EntityManager
|
||||
* @type {boolean} */
|
||||
this.queuedForDestroy;
|
||||
|
||||
/**
|
||||
* Stores the reason why this entity was destroyed
|
||||
* @type {string} */
|
||||
this.destroyReason;
|
||||
|
||||
/* typehints:end */
|
||||
}
|
||||
|
||||
static getId() {
|
||||
return "Entity";
|
||||
}
|
||||
|
||||
/**
|
||||
* @see BasicSerializableObject.getSchema
|
||||
* @returns {import("../savegame/serialization").Schema}
|
||||
*/
|
||||
static getSchema() {
|
||||
return {
|
||||
uid: types.uint,
|
||||
components: types.keyValueMap(types.objData(gComponentRegistry), false),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a clone of this entity without contents
|
||||
*/
|
||||
duplicateWithoutContents() {
|
||||
const clone = new Entity(this.root);
|
||||
for (const key in this.components) {
|
||||
clone.components[key] = this.components[key].duplicateWithoutContents();
|
||||
}
|
||||
clone.layer = this.layer;
|
||||
return clone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal destroy callback
|
||||
*/
|
||||
internalDestroyCallback() {
|
||||
assert(!this.destroyed, "Can not destroy entity twice");
|
||||
this.destroyed = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new component, only possible until the entity is registered on the entity manager,
|
||||
* after that use @see EntityManager.addDynamicComponent
|
||||
* @param {Component} componentInstance
|
||||
* @param {boolean} force Used by the entity manager. Internal parameter, do not change
|
||||
*/
|
||||
addComponent(componentInstance, force = false) {
|
||||
if (!force && this.registered) {
|
||||
this.root.entityMgr.attachDynamicComponent(this, componentInstance);
|
||||
return;
|
||||
}
|
||||
assert(force || !this.registered, "Entity already registered, use EntityManager.addDynamicComponent");
|
||||
const id = /** @type {typeof Component} */ (componentInstance.constructor).getId();
|
||||
assert(!this.components[id], "Component already present");
|
||||
this.components[id] = componentInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a given component, only possible until the entity is registered on the entity manager,
|
||||
* after that use @see EntityManager.removeDynamicComponent
|
||||
* @param {typeof Component} componentClass
|
||||
* @param {boolean} force
|
||||
*/
|
||||
removeComponent(componentClass, force = false) {
|
||||
if (!force && this.registered) {
|
||||
this.root.entityMgr.removeDynamicComponent(this, componentClass);
|
||||
return;
|
||||
}
|
||||
assert(
|
||||
force || !this.registered,
|
||||
"Entity already registered, use EntityManager.removeDynamicComponent"
|
||||
);
|
||||
const id = componentClass.getId();
|
||||
assert(this.components[id], "Component does not exist on entity");
|
||||
delete this.components[id];
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the entity, to override use @see Entity.drawImpl
|
||||
* @param {DrawParameters} parameters
|
||||
*/
|
||||
drawDebugOverlays(parameters) {
|
||||
const context = parameters.context;
|
||||
const staticComp = this.components.StaticMapEntity;
|
||||
|
||||
if (G_IS_DEV && staticComp && globalConfig.debug.showEntityBounds) {
|
||||
if (staticComp) {
|
||||
const transformed = staticComp.getTileSpaceBounds();
|
||||
context.strokeStyle = "rgba(255, 0, 0, 0.5)";
|
||||
context.lineWidth = 2;
|
||||
// const boundsSize = 20;
|
||||
context.beginPath();
|
||||
context.rect(
|
||||
transformed.x * globalConfig.tileSize,
|
||||
transformed.y * globalConfig.tileSize,
|
||||
transformed.w * globalConfig.tileSize,
|
||||
transformed.h * globalConfig.tileSize
|
||||
);
|
||||
context.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
if (G_IS_DEV && staticComp && globalConfig.debug.showAcceptorEjectors) {
|
||||
const ejectorComp = this.components.ItemEjector;
|
||||
|
||||
if (ejectorComp) {
|
||||
const ejectorSprite = Loader.getSprite("sprites/debug/ejector_slot.png");
|
||||
for (let i = 0; i < ejectorComp.slots.length; ++i) {
|
||||
const slot = ejectorComp.slots[i];
|
||||
const slotTile = staticComp.localTileToWorld(slot.pos);
|
||||
const direction = staticComp.localDirectionToWorld(slot.direction);
|
||||
const directionVector = enumDirectionToVector[direction];
|
||||
const angle = Math.radians(enumDirectionToAngle[direction]);
|
||||
|
||||
context.globalAlpha = slot.item ? 1 : 0.2;
|
||||
drawRotatedSprite({
|
||||
parameters,
|
||||
sprite: ejectorSprite,
|
||||
x: (slotTile.x + 0.5 + directionVector.x * 0.37) * globalConfig.tileSize,
|
||||
y: (slotTile.y + 0.5 + directionVector.y * 0.37) * globalConfig.tileSize,
|
||||
angle,
|
||||
size: globalConfig.tileSize * 0.25,
|
||||
});
|
||||
}
|
||||
}
|
||||
const acceptorComp = this.components.ItemAcceptor;
|
||||
|
||||
if (acceptorComp) {
|
||||
const acceptorSprite = Loader.getSprite("sprites/misc/acceptor_slot.png");
|
||||
for (let i = 0; i < acceptorComp.slots.length; ++i) {
|
||||
const slot = acceptorComp.slots[i];
|
||||
const slotTile = staticComp.localTileToWorld(slot.pos);
|
||||
for (let k = 0; k < slot.directions.length; ++k) {
|
||||
const direction = staticComp.localDirectionToWorld(slot.directions[k]);
|
||||
const directionVector = enumDirectionToVector[direction];
|
||||
const angle = Math.radians(enumDirectionToAngle[direction] + 180);
|
||||
context.globalAlpha = 0.4;
|
||||
drawRotatedSprite({
|
||||
parameters,
|
||||
sprite: acceptorSprite,
|
||||
x: (slotTile.x + 0.5 + directionVector.x * 0.37) * globalConfig.tileSize,
|
||||
y: (slotTile.y + 0.5 + directionVector.y * 0.37) * globalConfig.tileSize,
|
||||
angle,
|
||||
size: globalConfig.tileSize * 0.25,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context.globalAlpha = 1;
|
||||
}
|
||||
// this.drawImpl(parameters);
|
||||
}
|
||||
|
||||
///// Helper interfaces
|
||||
|
||||
///// Interface to override by subclasses
|
||||
|
||||
/**
|
||||
* override, should draw the entity
|
||||
* @param {DrawParameters} parameters
|
||||
*/
|
||||
drawImpl(parameters) {
|
||||
abstract;
|
||||
}
|
||||
}
|
||||
/* typehints:start */
|
||||
import { DrawParameters } from "../core/draw_parameters";
|
||||
import { Component } from "./component";
|
||||
/* typehints:end */
|
||||
|
||||
import { GameRoot } from "./root";
|
||||
import { globalConfig } from "../core/config";
|
||||
import { enumDirectionToVector, enumDirectionToAngle } from "../core/vector";
|
||||
import { BasicSerializableObject, types } from "../savegame/serialization";
|
||||
import { EntityComponentStorage } from "./entity_components";
|
||||
import { Loader } from "../core/loader";
|
||||
import { drawRotatedSprite } from "../core/draw_utils";
|
||||
import { gComponentRegistry } from "../core/global_registries";
|
||||
|
||||
export class Entity extends BasicSerializableObject {
|
||||
/**
|
||||
* @param {GameRoot} root
|
||||
*/
|
||||
constructor(root) {
|
||||
super();
|
||||
|
||||
/**
|
||||
* Handle to the global game root
|
||||
*/
|
||||
this.root = root;
|
||||
|
||||
/**
|
||||
* The components of the entity
|
||||
*/
|
||||
this.components = new EntityComponentStorage();
|
||||
|
||||
/**
|
||||
* Whether this entity was registered on the @see EntityManager so far
|
||||
*/
|
||||
this.registered = false;
|
||||
|
||||
/**
|
||||
* On which layer this entity is
|
||||
* @type {Layer}
|
||||
*/
|
||||
this.layer = "regular";
|
||||
|
||||
/**
|
||||
* Internal entity unique id, set by the @see EntityManager
|
||||
*/
|
||||
this.uid = 0;
|
||||
|
||||
/* typehints:start */
|
||||
|
||||
/**
|
||||
* Stores if this entity is destroyed, set by the @see EntityManager
|
||||
* @type {boolean} */
|
||||
this.destroyed;
|
||||
|
||||
/**
|
||||
* Stores if this entity is queued to get destroyed in the next tick
|
||||
* of the @see EntityManager
|
||||
* @type {boolean} */
|
||||
this.queuedForDestroy;
|
||||
|
||||
/**
|
||||
* Stores the reason why this entity was destroyed
|
||||
* @type {string} */
|
||||
this.destroyReason;
|
||||
|
||||
/* typehints:end */
|
||||
}
|
||||
|
||||
static getId() {
|
||||
return "Entity";
|
||||
}
|
||||
|
||||
/**
|
||||
* @see BasicSerializableObject.getSchema
|
||||
* @returns {import("../savegame/serialization").Schema}
|
||||
*/
|
||||
static getSchema() {
|
||||
return {
|
||||
uid: types.uint,
|
||||
components: types.keyValueMap(types.objData(gComponentRegistry), false),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a clone of this entity without contents
|
||||
*/
|
||||
duplicateWithoutContents() {
|
||||
const clone = new Entity(this.root);
|
||||
for (const key in this.components) {
|
||||
clone.components[key] = this.components[key].duplicateWithoutContents();
|
||||
}
|
||||
clone.layer = this.layer;
|
||||
return clone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new component, only possible until the entity is registered on the entity manager,
|
||||
* after that use @see EntityManager.addDynamicComponent
|
||||
* @param {Component} componentInstance
|
||||
* @param {boolean} force Used by the entity manager. Internal parameter, do not change
|
||||
*/
|
||||
addComponent(componentInstance, force = false) {
|
||||
if (!force && this.registered) {
|
||||
this.root.entityMgr.attachDynamicComponent(this, componentInstance);
|
||||
return;
|
||||
}
|
||||
assert(force || !this.registered, "Entity already registered, use EntityManager.addDynamicComponent");
|
||||
const id = /** @type {typeof Component} */ (componentInstance.constructor).getId();
|
||||
assert(!this.components[id], "Component already present");
|
||||
this.components[id] = componentInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a given component, only possible until the entity is registered on the entity manager,
|
||||
* after that use @see EntityManager.removeDynamicComponent
|
||||
* @param {typeof Component} componentClass
|
||||
* @param {boolean} force
|
||||
*/
|
||||
removeComponent(componentClass, force = false) {
|
||||
if (!force && this.registered) {
|
||||
this.root.entityMgr.removeDynamicComponent(this, componentClass);
|
||||
return;
|
||||
}
|
||||
assert(
|
||||
force || !this.registered,
|
||||
"Entity already registered, use EntityManager.removeDynamicComponent"
|
||||
);
|
||||
const id = componentClass.getId();
|
||||
assert(this.components[id], "Component does not exist on entity");
|
||||
delete this.components[id];
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the entity, to override use @see Entity.drawImpl
|
||||
* @param {DrawParameters} parameters
|
||||
*/
|
||||
drawDebugOverlays(parameters) {
|
||||
const context = parameters.context;
|
||||
const staticComp = this.components.StaticMapEntity;
|
||||
|
||||
if (G_IS_DEV && staticComp && globalConfig.debug.showEntityBounds) {
|
||||
if (staticComp) {
|
||||
const transformed = staticComp.getTileSpaceBounds();
|
||||
context.strokeStyle = "rgba(255, 0, 0, 0.5)";
|
||||
context.lineWidth = 2;
|
||||
// const boundsSize = 20;
|
||||
context.beginPath();
|
||||
context.rect(
|
||||
transformed.x * globalConfig.tileSize,
|
||||
transformed.y * globalConfig.tileSize,
|
||||
transformed.w * globalConfig.tileSize,
|
||||
transformed.h * globalConfig.tileSize
|
||||
);
|
||||
context.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
if (G_IS_DEV && staticComp && globalConfig.debug.showAcceptorEjectors) {
|
||||
const ejectorComp = this.components.ItemEjector;
|
||||
|
||||
if (ejectorComp) {
|
||||
const ejectorSprite = Loader.getSprite("sprites/debug/ejector_slot.png");
|
||||
for (let i = 0; i < ejectorComp.slots.length; ++i) {
|
||||
const slot = ejectorComp.slots[i];
|
||||
const slotTile = staticComp.localTileToWorld(slot.pos);
|
||||
const direction = staticComp.localDirectionToWorld(slot.direction);
|
||||
const directionVector = enumDirectionToVector[direction];
|
||||
const angle = Math.radians(enumDirectionToAngle[direction]);
|
||||
|
||||
context.globalAlpha = slot.item ? 1 : 0.2;
|
||||
drawRotatedSprite({
|
||||
parameters,
|
||||
sprite: ejectorSprite,
|
||||
x: (slotTile.x + 0.5 + directionVector.x * 0.37) * globalConfig.tileSize,
|
||||
y: (slotTile.y + 0.5 + directionVector.y * 0.37) * globalConfig.tileSize,
|
||||
angle,
|
||||
size: globalConfig.tileSize * 0.25,
|
||||
});
|
||||
}
|
||||
}
|
||||
const acceptorComp = this.components.ItemAcceptor;
|
||||
|
||||
if (acceptorComp) {
|
||||
const acceptorSprite = Loader.getSprite("sprites/misc/acceptor_slot.png");
|
||||
for (let i = 0; i < acceptorComp.slots.length; ++i) {
|
||||
const slot = acceptorComp.slots[i];
|
||||
const slotTile = staticComp.localTileToWorld(slot.pos);
|
||||
for (let k = 0; k < slot.directions.length; ++k) {
|
||||
const direction = staticComp.localDirectionToWorld(slot.directions[k]);
|
||||
const directionVector = enumDirectionToVector[direction];
|
||||
const angle = Math.radians(enumDirectionToAngle[direction] + 180);
|
||||
context.globalAlpha = 0.4;
|
||||
drawRotatedSprite({
|
||||
parameters,
|
||||
sprite: acceptorSprite,
|
||||
x: (slotTile.x + 0.5 + directionVector.x * 0.37) * globalConfig.tileSize,
|
||||
y: (slotTile.y + 0.5 + directionVector.y * 0.37) * globalConfig.tileSize,
|
||||
angle,
|
||||
size: globalConfig.tileSize * 0.25,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context.globalAlpha = 1;
|
||||
}
|
||||
// this.drawImpl(parameters);
|
||||
}
|
||||
|
||||
///// Helper interfaces
|
||||
|
||||
///// Interface to override by subclasses
|
||||
|
||||
/**
|
||||
* override, should draw the entity
|
||||
* @param {DrawParameters} parameters
|
||||
*/
|
||||
drawImpl(parameters) {
|
||||
abstract;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,252 +1,258 @@
|
||||
import { arrayDeleteValue, newEmptyMap, fastArrayDeleteValue } from "../core/utils";
|
||||
import { Component } from "./component";
|
||||
import { GameRoot } from "./root";
|
||||
import { Entity } from "./entity";
|
||||
import { BasicSerializableObject, types } from "../savegame/serialization";
|
||||
import { createLogger } from "../core/logging";
|
||||
|
||||
const logger = createLogger("entity_manager");
|
||||
|
||||
// Manages all entities
|
||||
|
||||
// NOTICE: We use arrayDeleteValue instead of fastArrayDeleteValue since that does not preserve the order
|
||||
// This is slower but we need it for the street path generation
|
||||
|
||||
export class EntityManager extends BasicSerializableObject {
|
||||
constructor(root) {
|
||||
super();
|
||||
|
||||
/** @type {GameRoot} */
|
||||
this.root = root;
|
||||
|
||||
/** @type {Array<Entity>} */
|
||||
this.entities = [];
|
||||
|
||||
// We store a seperate list with entities to destroy, since we don't destroy
|
||||
// them instantly
|
||||
/** @type {Array<Entity>} */
|
||||
this.destroyList = [];
|
||||
|
||||
// Store a map from componentid to entities - This is used by the game system
|
||||
// for faster processing
|
||||
/** @type {Object.<string, Array<Entity>>} */
|
||||
this.componentToEntity = newEmptyMap();
|
||||
|
||||
// Store the next uid to use
|
||||
this.nextUid = 10000;
|
||||
}
|
||||
|
||||
static getId() {
|
||||
return "EntityManager";
|
||||
}
|
||||
|
||||
static getSchema() {
|
||||
return {
|
||||
nextUid: types.uint,
|
||||
};
|
||||
}
|
||||
|
||||
getStatsText() {
|
||||
return this.entities.length + " entities [" + this.destroyList.length + " to kill]";
|
||||
}
|
||||
|
||||
// Main update
|
||||
update() {
|
||||
this.processDestroyList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a new entity
|
||||
* @param {Entity} entity
|
||||
* @param {number=} uid Optional predefined uid
|
||||
*/
|
||||
registerEntity(entity, uid = null) {
|
||||
assert(this.entities.indexOf(entity) < 0, `RegisterEntity() called twice for entity ${entity}`);
|
||||
assert(!entity.destroyed, `Attempting to register destroyed entity ${entity}`);
|
||||
|
||||
if (G_IS_DEV && uid !== null) {
|
||||
assert(!this.findByUid(uid, false), "Entity uid already taken: " + uid);
|
||||
}
|
||||
|
||||
if (uid !== null) {
|
||||
assert(uid >= 0 && uid < Number.MAX_SAFE_INTEGER, "Invalid uid passed: " + uid);
|
||||
}
|
||||
|
||||
this.entities.push(entity);
|
||||
|
||||
// Register into the componentToEntity map
|
||||
for (const componentId in entity.components) {
|
||||
if (entity.components[componentId]) {
|
||||
if (this.componentToEntity[componentId]) {
|
||||
this.componentToEntity[componentId].push(entity);
|
||||
} else {
|
||||
this.componentToEntity[componentId] = [entity];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Give each entity a unique id
|
||||
entity.uid = uid ? uid : this.generateUid();
|
||||
entity.registered = true;
|
||||
|
||||
this.root.signals.entityAdded.dispatch(entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts all entitiy lists after a resync
|
||||
*/
|
||||
sortEntityLists() {
|
||||
this.entities.sort((a, b) => a.uid - b.uid);
|
||||
this.destroyList.sort((a, b) => a.uid - b.uid);
|
||||
|
||||
for (const key in this.componentToEntity) {
|
||||
this.componentToEntity[key].sort((a, b) => a.uid - b.uid);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a new uid
|
||||
* @returns {number}
|
||||
*/
|
||||
generateUid() {
|
||||
return this.nextUid++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call to attach a new component after the creation of the entity
|
||||
* @param {Entity} entity
|
||||
* @param {Component} component
|
||||
*/
|
||||
attachDynamicComponent(entity, component) {
|
||||
entity.addComponent(component, true);
|
||||
const componentId = /** @type {typeof Component} */ (component.constructor).getId();
|
||||
if (this.componentToEntity[componentId]) {
|
||||
this.componentToEntity[componentId].push(entity);
|
||||
} else {
|
||||
this.componentToEntity[componentId] = [entity];
|
||||
}
|
||||
this.root.signals.entityGotNewComponent.dispatch(entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Call to remove a component after the creation of the entity
|
||||
* @param {Entity} entity
|
||||
* @param {typeof Component} component
|
||||
*/
|
||||
removeDynamicComponent(entity, component) {
|
||||
entity.removeComponent(component, true);
|
||||
const componentId = /** @type {typeof Component} */ (component.constructor).getId();
|
||||
|
||||
fastArrayDeleteValue(this.componentToEntity[componentId], entity);
|
||||
this.root.signals.entityComponentRemoved.dispatch(entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds an entity buy its uid, kinda slow since it loops over all entities
|
||||
* @param {number} uid
|
||||
* @param {boolean=} errorWhenNotFound
|
||||
* @returns {Entity}
|
||||
*/
|
||||
findByUid(uid, errorWhenNotFound = true) {
|
||||
const arr = this.entities;
|
||||
for (let i = 0, len = arr.length; i < len; ++i) {
|
||||
const entity = arr[i];
|
||||
if (entity.uid === uid) {
|
||||
if (entity.queuedForDestroy || entity.destroyed) {
|
||||
if (errorWhenNotFound) {
|
||||
logger.warn("Entity with UID", uid, "not found (destroyed)");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
if (errorWhenNotFound) {
|
||||
logger.warn("Entity with UID", uid, "not found");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all entities having the given component
|
||||
* @param {typeof Component} componentHandle
|
||||
* @returns {Array<Entity>} entities
|
||||
*/
|
||||
getAllWithComponent(componentHandle) {
|
||||
return this.componentToEntity[componentHandle.getId()] || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all of a given class. This is SLOW!
|
||||
* @param {object} entityClass
|
||||
* @returns {Array<Entity>} entities
|
||||
*/
|
||||
getAllOfClass(entityClass) {
|
||||
// FIXME: Slow
|
||||
const result = [];
|
||||
for (let i = 0; i < this.entities.length; ++i) {
|
||||
const entity = this.entities[i];
|
||||
if (entity instanceof entityClass) {
|
||||
result.push(entity);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters all components of an entity from the component to entity mapping
|
||||
* @param {Entity} entity
|
||||
*/
|
||||
unregisterEntityComponents(entity) {
|
||||
for (const componentId in entity.components) {
|
||||
if (entity.components[componentId]) {
|
||||
arrayDeleteValue(this.componentToEntity[componentId], entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Processes the entities to destroy and actually destroys them
|
||||
/* eslint-disable max-statements */
|
||||
processDestroyList() {
|
||||
for (let i = 0; i < this.destroyList.length; ++i) {
|
||||
const entity = this.destroyList[i];
|
||||
|
||||
// Remove from entities list
|
||||
arrayDeleteValue(this.entities, entity);
|
||||
|
||||
// Remove from componentToEntity list
|
||||
this.unregisterEntityComponents(entity);
|
||||
|
||||
entity.registered = false;
|
||||
entity.internalDestroyCallback();
|
||||
|
||||
this.root.signals.entityDestroyed.dispatch(entity);
|
||||
}
|
||||
|
||||
this.destroyList = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Queues an entity for destruction
|
||||
* @param {Entity} entity
|
||||
*/
|
||||
destroyEntity(entity) {
|
||||
if (entity.destroyed) {
|
||||
logger.error("Tried to destroy already destroyed entity:", entity.uid);
|
||||
return;
|
||||
}
|
||||
|
||||
if (entity.queuedForDestroy) {
|
||||
logger.error("Trying to destroy entity which is already queued for destroy!", entity.uid);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.destroyList.indexOf(entity) < 0) {
|
||||
this.destroyList.push(entity);
|
||||
entity.queuedForDestroy = true;
|
||||
this.root.signals.entityQueuedForDestroy.dispatch(entity);
|
||||
} else {
|
||||
assert(false, "Trying to destroy entity twice");
|
||||
}
|
||||
}
|
||||
}
|
||||
import { arrayDeleteValue, newEmptyMap, fastArrayDeleteValue } from "../core/utils";
|
||||
import { Component } from "./component";
|
||||
import { GameRoot } from "./root";
|
||||
import { Entity } from "./entity";
|
||||
import { BasicSerializableObject, types } from "../savegame/serialization";
|
||||
import { createLogger } from "../core/logging";
|
||||
import { globalConfig } from "../core/config";
|
||||
|
||||
const logger = createLogger("entity_manager");
|
||||
|
||||
// Manages all entities
|
||||
|
||||
// NOTICE: We use arrayDeleteValue instead of fastArrayDeleteValue since that does not preserve the order
|
||||
// This is slower but we need it for the street path generation
|
||||
|
||||
export class EntityManager extends BasicSerializableObject {
|
||||
constructor(root) {
|
||||
super();
|
||||
|
||||
/** @type {GameRoot} */
|
||||
this.root = root;
|
||||
|
||||
/** @type {Array<Entity>} */
|
||||
this.entities = [];
|
||||
|
||||
// We store a separate list with entities to destroy, since we don't destroy
|
||||
// them instantly
|
||||
/** @type {Array<Entity>} */
|
||||
this.destroyList = [];
|
||||
|
||||
// Store a map from componentid to entities - This is used by the game system
|
||||
// for faster processing
|
||||
/** @type {Object.<string, Array<Entity>>} */
|
||||
this.componentToEntity = newEmptyMap();
|
||||
|
||||
// Store the next uid to use
|
||||
this.nextUid = 10000;
|
||||
}
|
||||
|
||||
static getId() {
|
||||
return "EntityManager";
|
||||
}
|
||||
|
||||
static getSchema() {
|
||||
return {
|
||||
nextUid: types.uint,
|
||||
};
|
||||
}
|
||||
|
||||
getStatsText() {
|
||||
return this.entities.length + " entities [" + this.destroyList.length + " to kill]";
|
||||
}
|
||||
|
||||
// Main update
|
||||
update() {
|
||||
this.processDestroyList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a new entity
|
||||
* @param {Entity} entity
|
||||
* @param {number=} uid Optional predefined uid
|
||||
*/
|
||||
registerEntity(entity, uid = null) {
|
||||
if (G_IS_DEV && !globalConfig.debug.disableSlowAsserts) {
|
||||
assert(this.entities.indexOf(entity) < 0, `RegisterEntity() called twice for entity ${entity}`);
|
||||
}
|
||||
assert(!entity.destroyed, `Attempting to register destroyed entity ${entity}`);
|
||||
|
||||
if (G_IS_DEV && !globalConfig.debug.disableSlowAsserts && uid !== null) {
|
||||
assert(!this.findByUid(uid, false), "Entity uid already taken: " + uid);
|
||||
assert(uid >= 0 && uid < Number.MAX_SAFE_INTEGER, "Invalid uid passed: " + uid);
|
||||
}
|
||||
|
||||
this.entities.push(entity);
|
||||
|
||||
// Register into the componentToEntity map
|
||||
for (const componentId in entity.components) {
|
||||
if (entity.components[componentId]) {
|
||||
if (this.componentToEntity[componentId]) {
|
||||
this.componentToEntity[componentId].push(entity);
|
||||
} else {
|
||||
this.componentToEntity[componentId] = [entity];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Give each entity a unique id
|
||||
entity.uid = uid ? uid : this.generateUid();
|
||||
entity.registered = true;
|
||||
|
||||
this.root.signals.entityAdded.dispatch(entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a new uid
|
||||
* @returns {number}
|
||||
*/
|
||||
generateUid() {
|
||||
return this.nextUid++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call to attach a new component after the creation of the entity
|
||||
* @param {Entity} entity
|
||||
* @param {Component} component
|
||||
*/
|
||||
attachDynamicComponent(entity, component) {
|
||||
entity.addComponent(component, true);
|
||||
const componentId = /** @type {typeof Component} */ (component.constructor).getId();
|
||||
if (this.componentToEntity[componentId]) {
|
||||
this.componentToEntity[componentId].push(entity);
|
||||
} else {
|
||||
this.componentToEntity[componentId] = [entity];
|
||||
}
|
||||
this.root.signals.entityGotNewComponent.dispatch(entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Call to remove a component after the creation of the entity
|
||||
* @param {Entity} entity
|
||||
* @param {typeof Component} component
|
||||
*/
|
||||
removeDynamicComponent(entity, component) {
|
||||
entity.removeComponent(component, true);
|
||||
const componentId = /** @type {typeof Component} */ (component.constructor).getId();
|
||||
|
||||
fastArrayDeleteValue(this.componentToEntity[componentId], entity);
|
||||
this.root.signals.entityComponentRemoved.dispatch(entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds an entity buy its uid, kinda slow since it loops over all entities
|
||||
* @param {number} uid
|
||||
* @param {boolean=} errorWhenNotFound
|
||||
* @returns {Entity}
|
||||
*/
|
||||
findByUid(uid, errorWhenNotFound = true) {
|
||||
const arr = this.entities;
|
||||
for (let i = 0, len = arr.length; i < len; ++i) {
|
||||
const entity = arr[i];
|
||||
if (entity.uid === uid) {
|
||||
if (entity.queuedForDestroy || entity.destroyed) {
|
||||
if (errorWhenNotFound) {
|
||||
logger.warn("Entity with UID", uid, "not found (destroyed)");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
if (errorWhenNotFound) {
|
||||
logger.warn("Entity with UID", uid, "not found");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a map which gives a mapping from UID to Entity.
|
||||
* This map is not updated.
|
||||
*
|
||||
* @returns {Map<number, Entity>}
|
||||
*/
|
||||
getFrozenUidSearchMap() {
|
||||
const result = new Map();
|
||||
const array = this.entities;
|
||||
for (let i = 0, len = array.length; i < len; ++i) {
|
||||
const entity = array[i];
|
||||
if (!entity.queuedForDestroy && !entity.destroyed) {
|
||||
result.set(entity.uid, entity);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all entities having the given component
|
||||
* @param {typeof Component} componentHandle
|
||||
* @returns {Array<Entity>} entities
|
||||
*/
|
||||
getAllWithComponent(componentHandle) {
|
||||
return this.componentToEntity[componentHandle.getId()] || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all of a given class. This is SLOW!
|
||||
* @param {object} entityClass
|
||||
* @returns {Array<Entity>} entities
|
||||
*/
|
||||
getAllOfClass(entityClass) {
|
||||
// FIXME: Slow
|
||||
const result = [];
|
||||
for (let i = 0; i < this.entities.length; ++i) {
|
||||
const entity = this.entities[i];
|
||||
if (entity instanceof entityClass) {
|
||||
result.push(entity);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters all components of an entity from the component to entity mapping
|
||||
* @param {Entity} entity
|
||||
*/
|
||||
unregisterEntityComponents(entity) {
|
||||
for (const componentId in entity.components) {
|
||||
if (entity.components[componentId]) {
|
||||
arrayDeleteValue(this.componentToEntity[componentId], entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Processes the entities to destroy and actually destroys them
|
||||
/* eslint-disable max-statements */
|
||||
processDestroyList() {
|
||||
for (let i = 0; i < this.destroyList.length; ++i) {
|
||||
const entity = this.destroyList[i];
|
||||
|
||||
// Remove from entities list
|
||||
arrayDeleteValue(this.entities, entity);
|
||||
|
||||
// Remove from componentToEntity list
|
||||
this.unregisterEntityComponents(entity);
|
||||
|
||||
entity.registered = false;
|
||||
entity.destroyed = true;
|
||||
|
||||
this.root.signals.entityDestroyed.dispatch(entity);
|
||||
}
|
||||
|
||||
this.destroyList = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Queues an entity for destruction
|
||||
* @param {Entity} entity
|
||||
*/
|
||||
destroyEntity(entity) {
|
||||
if (entity.destroyed) {
|
||||
logger.error("Tried to destroy already destroyed entity:", entity.uid);
|
||||
return;
|
||||
}
|
||||
|
||||
if (entity.queuedForDestroy) {
|
||||
logger.error("Trying to destroy entity which is already queued for destroy!", entity.uid);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.destroyList.indexOf(entity) < 0) {
|
||||
this.destroyList.push(entity);
|
||||
entity.queuedForDestroy = true;
|
||||
this.root.signals.entityQueuedForDestroy.dispatch(entity);
|
||||
} else {
|
||||
assert(false, "Trying to destroy entity twice");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
/* typehints:start */
|
||||
import { Application } from "../application";
|
||||
/* typehints:end */
|
||||
|
||||
import { randomChoice } from "../core/utils";
|
||||
import { T } from "../translations";
|
||||
|
||||
export class GameLoadingOverlay {
|
||||
@ -43,6 +45,7 @@ export class GameLoadingOverlay {
|
||||
this.element.classList.add("gameLoadingOverlay");
|
||||
this.parent.appendChild(this.element);
|
||||
this.internalAddSpinnerAndText(this.element);
|
||||
this.internalAddHint(this.element);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -55,4 +58,15 @@ export class GameLoadingOverlay {
|
||||
inner.innerText = T.global.loading;
|
||||
element.appendChild(inner);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a random hint
|
||||
* @param {HTMLElement} element
|
||||
*/
|
||||
internalAddHint(element) {
|
||||
const hint = document.createElement("span");
|
||||
hint.innerHTML = randomChoice(T.tips);
|
||||
hint.classList.add("hint");
|
||||
element.appendChild(hint);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,131 +1,137 @@
|
||||
/* typehints:start */
|
||||
import { Component } from "./component";
|
||||
import { Entity } from "./entity";
|
||||
/* typehints:end */
|
||||
|
||||
import { GameRoot } from "./root";
|
||||
import { GameSystem } from "./game_system";
|
||||
import { arrayDelete, arrayDeleteValue } from "../core/utils";
|
||||
|
||||
export class GameSystemWithFilter extends GameSystem {
|
||||
/**
|
||||
* Constructs a new game system with the given component filter. It will process
|
||||
* all entities which have *all* of the passed components
|
||||
* @param {GameRoot} root
|
||||
* @param {Array<typeof Component>} requiredComponents
|
||||
*/
|
||||
constructor(root, requiredComponents) {
|
||||
super(root);
|
||||
this.requiredComponents = requiredComponents;
|
||||
this.requiredComponentIds = requiredComponents.map(component => component.getId());
|
||||
|
||||
/**
|
||||
* All entities which match the current components
|
||||
* @type {Array<Entity>}
|
||||
*/
|
||||
this.allEntities = [];
|
||||
|
||||
this.root.signals.entityAdded.add(this.internalPushEntityIfMatching, this);
|
||||
this.root.signals.entityGotNewComponent.add(this.internalReconsiderEntityToAdd, this);
|
||||
this.root.signals.entityComponentRemoved.add(this.internalCheckEntityAfterComponentRemoval, this);
|
||||
this.root.signals.entityQueuedForDestroy.add(this.internalPopEntityIfMatching, this);
|
||||
|
||||
this.root.signals.postLoadHook.add(this.internalPostLoadHook, this);
|
||||
this.root.signals.bulkOperationFinished.add(this.refreshCaches, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Entity} entity
|
||||
*/
|
||||
internalPushEntityIfMatching(entity) {
|
||||
for (let i = 0; i < this.requiredComponentIds.length; ++i) {
|
||||
if (!entity.components[this.requiredComponentIds[i]]) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
assert(this.allEntities.indexOf(entity) < 0, "entity already in list: " + entity);
|
||||
this.internalRegisterEntity(entity);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Entity} entity
|
||||
*/
|
||||
internalCheckEntityAfterComponentRemoval(entity) {
|
||||
if (this.allEntities.indexOf(entity) < 0) {
|
||||
// Entity wasn't interesting anyways
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.requiredComponentIds.length; ++i) {
|
||||
if (!entity.components[this.requiredComponentIds[i]]) {
|
||||
// Entity is not interesting anymore
|
||||
arrayDeleteValue(this.allEntities, entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Entity} entity
|
||||
*/
|
||||
internalReconsiderEntityToAdd(entity) {
|
||||
for (let i = 0; i < this.requiredComponentIds.length; ++i) {
|
||||
if (!entity.components[this.requiredComponentIds[i]]) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (this.allEntities.indexOf(entity) >= 0) {
|
||||
return;
|
||||
}
|
||||
this.internalRegisterEntity(entity);
|
||||
}
|
||||
|
||||
refreshCaches() {
|
||||
this.allEntities.sort((a, b) => a.uid - b.uid);
|
||||
|
||||
// Remove all entities which are queued for destroy
|
||||
for (let i = 0; i < this.allEntities.length; ++i) {
|
||||
const entity = this.allEntities[i];
|
||||
if (entity.queuedForDestroy || entity.destroyed) {
|
||||
this.allEntities.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recomputes all target entities after the game has loaded
|
||||
*/
|
||||
internalPostLoadHook() {
|
||||
this.refreshCaches();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Entity} entity
|
||||
*/
|
||||
internalRegisterEntity(entity) {
|
||||
this.allEntities.push(entity);
|
||||
|
||||
if (this.root.gameInitialized && !this.root.bulkOperationRunning) {
|
||||
// Sort entities by uid so behaviour is predictable
|
||||
this.allEntities.sort((a, b) => a.uid - b.uid);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Entity} entity
|
||||
*/
|
||||
internalPopEntityIfMatching(entity) {
|
||||
if (this.root.bulkOperationRunning) {
|
||||
// We do this in refreshCaches afterwards
|
||||
return;
|
||||
}
|
||||
const index = this.allEntities.indexOf(entity);
|
||||
if (index >= 0) {
|
||||
arrayDelete(this.allEntities, index);
|
||||
}
|
||||
}
|
||||
}
|
||||
/* typehints:start */
|
||||
import { Component } from "./component";
|
||||
import { Entity } from "./entity";
|
||||
/* typehints:end */
|
||||
|
||||
import { GameRoot } from "./root";
|
||||
import { GameSystem } from "./game_system";
|
||||
import { arrayDelete, arrayDeleteValue } from "../core/utils";
|
||||
import { globalConfig } from "../core/config";
|
||||
|
||||
export class GameSystemWithFilter extends GameSystem {
|
||||
/**
|
||||
* Constructs a new game system with the given component filter. It will process
|
||||
* all entities which have *all* of the passed components
|
||||
* @param {GameRoot} root
|
||||
* @param {Array<typeof Component>} requiredComponents
|
||||
*/
|
||||
constructor(root, requiredComponents) {
|
||||
super(root);
|
||||
this.requiredComponents = requiredComponents;
|
||||
this.requiredComponentIds = requiredComponents.map(component => component.getId());
|
||||
|
||||
/**
|
||||
* All entities which match the current components
|
||||
* @type {Array<Entity>}
|
||||
*/
|
||||
this.allEntities = [];
|
||||
|
||||
this.root.signals.entityAdded.add(this.internalPushEntityIfMatching, this);
|
||||
this.root.signals.entityGotNewComponent.add(this.internalReconsiderEntityToAdd, this);
|
||||
this.root.signals.entityComponentRemoved.add(this.internalCheckEntityAfterComponentRemoval, this);
|
||||
this.root.signals.entityQueuedForDestroy.add(this.internalPopEntityIfMatching, this);
|
||||
|
||||
this.root.signals.postLoadHook.add(this.internalPostLoadHook, this);
|
||||
this.root.signals.bulkOperationFinished.add(this.refreshCaches, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Entity} entity
|
||||
*/
|
||||
internalPushEntityIfMatching(entity) {
|
||||
for (let i = 0; i < this.requiredComponentIds.length; ++i) {
|
||||
if (!entity.components[this.requiredComponentIds[i]]) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// This is slow!
|
||||
if (G_IS_DEV && !globalConfig.debug.disableSlowAsserts) {
|
||||
assert(this.allEntities.indexOf(entity) < 0, "entity already in list: " + entity);
|
||||
}
|
||||
|
||||
this.internalRegisterEntity(entity);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Entity} entity
|
||||
*/
|
||||
internalCheckEntityAfterComponentRemoval(entity) {
|
||||
if (this.allEntities.indexOf(entity) < 0) {
|
||||
// Entity wasn't interesting anyways
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.requiredComponentIds.length; ++i) {
|
||||
if (!entity.components[this.requiredComponentIds[i]]) {
|
||||
// Entity is not interesting anymore
|
||||
arrayDeleteValue(this.allEntities, entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Entity} entity
|
||||
*/
|
||||
internalReconsiderEntityToAdd(entity) {
|
||||
for (let i = 0; i < this.requiredComponentIds.length; ++i) {
|
||||
if (!entity.components[this.requiredComponentIds[i]]) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (this.allEntities.indexOf(entity) >= 0) {
|
||||
return;
|
||||
}
|
||||
this.internalRegisterEntity(entity);
|
||||
}
|
||||
|
||||
refreshCaches() {
|
||||
// Remove all entities which are queued for destroy
|
||||
for (let i = 0; i < this.allEntities.length; ++i) {
|
||||
const entity = this.allEntities[i];
|
||||
if (entity.queuedForDestroy || entity.destroyed) {
|
||||
this.allEntities.splice(i, 1);
|
||||
i -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
this.allEntities.sort((a, b) => a.uid - b.uid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recomputes all target entities after the game has loaded
|
||||
*/
|
||||
internalPostLoadHook() {
|
||||
this.refreshCaches();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Entity} entity
|
||||
*/
|
||||
internalRegisterEntity(entity) {
|
||||
this.allEntities.push(entity);
|
||||
|
||||
if (this.root.gameInitialized && !this.root.bulkOperationRunning) {
|
||||
// Sort entities by uid so behaviour is predictable
|
||||
this.allEntities.sort((a, b) => a.uid - b.uid);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Entity} entity
|
||||
*/
|
||||
internalPopEntityIfMatching(entity) {
|
||||
if (this.root.bulkOperationRunning) {
|
||||
// We do this in refreshCaches afterwards
|
||||
return;
|
||||
}
|
||||
const index = this.allEntities.indexOf(entity);
|
||||
if (index >= 0) {
|
||||
arrayDelete(this.allEntities, index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
22
src/js/game/hints.js
Normal file
22
src/js/game/hints.js
Normal file
@ -0,0 +1,22 @@
|
||||
import { randomChoice } from "../core/utils";
|
||||
import { T } from "../translations";
|
||||
|
||||
const hintsShown = [];
|
||||
|
||||
/**
|
||||
* Finds a new hint to show about the game which the user hasn't seen within this session
|
||||
*/
|
||||
export function getRandomHint() {
|
||||
let maxTries = 100 * T.tips.length;
|
||||
|
||||
while (maxTries-- > 0) {
|
||||
const hint = randomChoice(T.tips);
|
||||
if (!hintsShown.includes(hint)) {
|
||||
hintsShown.push(hint);
|
||||
return hint;
|
||||
}
|
||||
}
|
||||
|
||||
// All tips shown so far
|
||||
return randomChoice(T.tips);
|
||||
}
|
||||
@ -330,7 +330,7 @@ export class HUDBuildingPlacerLogic extends BaseHUDPart {
|
||||
if (tileBelow && this.root.app.settings.getAllSettings().pickMinerOnPatch) {
|
||||
this.currentMetaBuilding.set(gMetaBuildingRegistry.findByClass(MetaMinerBuilding));
|
||||
|
||||
// Select chained miner if available, since thats always desired once unlocked
|
||||
// Select chained miner if available, since that's always desired once unlocked
|
||||
if (this.root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_miner_chainable)) {
|
||||
this.currentVariant.set(ChainableMinerVariant);
|
||||
}
|
||||
|
||||
@ -1,7 +1,13 @@
|
||||
import { BaseHUDPart } from "../base_hud_part";
|
||||
/* dev:start */
|
||||
import { makeDiv, removeAllChildren } from "../../../core/utils";
|
||||
import { globalConfig } from "../../../core/config";
|
||||
import { Vector } from "../../../core/vector";
|
||||
import { Entity } from "../../entity";
|
||||
import { BaseHUDPart } from "../base_hud_part";
|
||||
import { DynamicDomAttach } from "../dynamic_dom_attach";
|
||||
|
||||
/**
|
||||
* Allows to inspect entities by pressing F8 while hovering them
|
||||
*/
|
||||
export class HUDEntityDebugger extends BaseHUDPart {
|
||||
createElements(parent) {
|
||||
this.element = makeDiv(
|
||||
@ -9,65 +15,143 @@ export class HUDEntityDebugger extends BaseHUDPart {
|
||||
"ingame_HUD_EntityDebugger",
|
||||
[],
|
||||
`
|
||||
Tile below cursor: <span class="mousePos"></span><br>
|
||||
Chunk below cursor: <span class="chunkPos"></span><br>
|
||||
<div class="entityInfo"></div>
|
||||
<label>Entity Debugger</label>
|
||||
<span class="hint">Use F8 to toggle this overlay</span>
|
||||
|
||||
<div class="propertyTable">
|
||||
<div class="entityComponents"></div>
|
||||
</div>
|
||||
`
|
||||
);
|
||||
|
||||
/** @type {HTMLElement} */
|
||||
this.mousePosElem = this.element.querySelector(".mousePos");
|
||||
/** @type {HTMLElement} */
|
||||
this.chunkPosElem = this.element.querySelector(".chunkPos");
|
||||
this.entityInfoElem = this.element.querySelector(".entityInfo");
|
||||
this.componentsElem = this.element.querySelector(".entityComponents");
|
||||
}
|
||||
|
||||
initialize() {
|
||||
this.root.camera.downPreHandler.add(this.onMouseDown, this);
|
||||
this.root.gameState.inputReciever.keydown.add(key => {
|
||||
if (key.keyCode === 119) {
|
||||
// F8
|
||||
this.pickEntity();
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* The currently selected entity
|
||||
* @type {Entity}
|
||||
*/
|
||||
this.selectedEntity = null;
|
||||
|
||||
this.lastUpdate = 0;
|
||||
|
||||
this.domAttach = new DynamicDomAttach(this.root, this.element);
|
||||
}
|
||||
|
||||
update() {
|
||||
pickEntity() {
|
||||
const mousePos = this.root.app.mousePosition;
|
||||
if (!mousePos) {
|
||||
return;
|
||||
}
|
||||
const worldPos = this.root.camera.screenToWorld(mousePos);
|
||||
const worldTile = worldPos.toTileSpace();
|
||||
|
||||
const chunk = worldTile.divideScalar(globalConfig.mapChunkSize).floor();
|
||||
this.mousePosElem.innerText = worldTile.x + " / " + worldTile.y;
|
||||
this.chunkPosElem.innerText = chunk.x + " / " + chunk.y;
|
||||
|
||||
const entity = this.root.map.getTileContent(worldTile, this.root.currentLayer);
|
||||
|
||||
this.selectedEntity = entity;
|
||||
if (entity) {
|
||||
removeAllChildren(this.entityInfoElem);
|
||||
let html = "Entity";
|
||||
|
||||
const flag = (name, val) =>
|
||||
`<span class='flag' data-value='${val ? "1" : "0"}'><u>${name}</u> ${val}</span>`;
|
||||
|
||||
html += "<div class='entityFlags'>";
|
||||
html += flag("registered", entity.registered);
|
||||
html += flag("uid", entity.uid);
|
||||
html += flag("destroyed", entity.destroyed);
|
||||
html += "</div>";
|
||||
|
||||
html += "<div class='components'>";
|
||||
|
||||
for (const componentId in entity.components) {
|
||||
const data = entity.components[componentId];
|
||||
html += "<div class='component'>";
|
||||
html += "<strong class='name'>" + componentId + "</strong>";
|
||||
html += "<textarea class='data'>" + JSON.stringify(data.serialize(), null, 2) + "</textarea>";
|
||||
|
||||
html += "</div>";
|
||||
}
|
||||
|
||||
html += "</div>";
|
||||
|
||||
this.entityInfoElem.innerHTML = html;
|
||||
this.rerenderFull(entity);
|
||||
}
|
||||
}
|
||||
|
||||
onMouseDown() {}
|
||||
/**
|
||||
*
|
||||
* @param {string} name
|
||||
* @param {any} val
|
||||
* @param {number} indent
|
||||
* @param {Array} recursion
|
||||
*/
|
||||
propertyToHTML(name, val, indent = 0, recursion = []) {
|
||||
if (val !== null && typeof val === "object") {
|
||||
// Array is displayed like object, with indexes
|
||||
recursion.push(val);
|
||||
|
||||
// Get type class name (like Array, Object, Vector...)
|
||||
let typeName = `(${val.constructor ? val.constructor.name : "unknown"})`;
|
||||
|
||||
if (Array.isArray(val)) {
|
||||
typeName = `(Array[${val.length}])`;
|
||||
}
|
||||
|
||||
if (val instanceof Vector) {
|
||||
typeName = `(Vector[${val.x}, ${val.y}])`;
|
||||
}
|
||||
|
||||
const colorStyle = `color: hsl(${30 * indent}, 100%, 80%)`;
|
||||
|
||||
let html = `<details class="object" style="${colorStyle}">
|
||||
<summary>${name} ${typeName}</summary>
|
||||
<div>`;
|
||||
|
||||
for (const property in val) {
|
||||
const isRoot = val[property] == this.root;
|
||||
const isRecursive = recursion.includes(val[property]);
|
||||
|
||||
let hiddenValue = isRoot ? "<root>" : null;
|
||||
if (isRecursive) {
|
||||
// Avoid recursion by not "expanding" object more than once
|
||||
hiddenValue = "<recursion>";
|
||||
}
|
||||
|
||||
html += this.propertyToHTML(
|
||||
property,
|
||||
hiddenValue ? hiddenValue : val[property],
|
||||
indent + 1,
|
||||
[...recursion] // still expand same value in other "branches"
|
||||
);
|
||||
}
|
||||
|
||||
html += "</div></details>";
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
const displayValue = (val + "")
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">");
|
||||
return `<label>${name}</label> <span>${displayValue}</span>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rerenders the whole container
|
||||
* @param {Entity} entity
|
||||
*/
|
||||
rerenderFull(entity) {
|
||||
removeAllChildren(this.componentsElem);
|
||||
let html = "";
|
||||
|
||||
const property = (strings, val) => `<label>${strings[0]}</label> <span>${val}</span>`;
|
||||
|
||||
html += property`registered ${!!entity.registered}`;
|
||||
html += property`uid ${entity.uid}`;
|
||||
html += property`destroyed ${!!entity.destroyed}`;
|
||||
|
||||
for (const componentId in entity.components) {
|
||||
const data = entity.components[componentId];
|
||||
html += "<details class='object'>";
|
||||
html += "<summary>" + componentId + "</summary><div>";
|
||||
|
||||
for (const property in data) {
|
||||
// Put entity into recursion list, so it won't get "expanded"
|
||||
html += this.propertyToHTML(property, data[property], 0, [entity]);
|
||||
}
|
||||
|
||||
html += "</div></details>";
|
||||
}
|
||||
|
||||
this.componentsElem.innerHTML = html;
|
||||
}
|
||||
|
||||
update() {
|
||||
this.domAttach.update(!!this.selectedEntity);
|
||||
}
|
||||
}
|
||||
|
||||
/* dev:end */
|
||||
|
||||
@ -5,6 +5,7 @@ import { enumNotificationType } from "./notifications";
|
||||
import { T } from "../../../translations";
|
||||
import { KEYMAPPINGS } from "../../key_action_mapper";
|
||||
import { DynamicDomAttach } from "../dynamic_dom_attach";
|
||||
import { TrackedState } from "../../../core/tracked_state";
|
||||
|
||||
export class HUDGameMenu extends BaseHUDPart {
|
||||
createElements(parent) {
|
||||
@ -97,12 +98,17 @@ export class HUDGameMenu extends BaseHUDPart {
|
||||
|
||||
initialize() {
|
||||
this.root.signals.gameSaved.add(this.onGameSaved, this);
|
||||
|
||||
this.trackedIsSaving = new TrackedState(this.onIsSavingChanged, this);
|
||||
}
|
||||
|
||||
update() {
|
||||
let playSound = false;
|
||||
let notifications = new Set();
|
||||
|
||||
// Check whether we are saving
|
||||
this.trackedIsSaving.set(!!this.root.gameState.currentSavePromise);
|
||||
|
||||
// Update visibility of buttons
|
||||
for (let i = 0; i < this.visibilityToUpdate.length; ++i) {
|
||||
const { condition, domAttach } = this.visibilityToUpdate[i];
|
||||
@ -154,6 +160,10 @@ export class HUDGameMenu extends BaseHUDPart {
|
||||
});
|
||||
}
|
||||
|
||||
onIsSavingChanged(isSaving) {
|
||||
this.saveButton.classList.toggle("saving", isSaving);
|
||||
}
|
||||
|
||||
onGameSaved() {
|
||||
this.saveButton.classList.toggle("animEven");
|
||||
this.saveButton.classList.toggle("animOdd");
|
||||
|
||||
@ -48,6 +48,9 @@ export class HUDMassSelector extends BaseHUDPart {
|
||||
* @param {Entity} entity
|
||||
*/
|
||||
onEntityDestroyed(entity) {
|
||||
if (this.root.bulkOperationRunning) {
|
||||
return;
|
||||
}
|
||||
this.selectedUids.delete(entity.uid);
|
||||
}
|
||||
|
||||
@ -90,14 +93,30 @@ export class HUDMassSelector extends BaseHUDPart {
|
||||
|
||||
doDelete() {
|
||||
const entityUids = Array.from(this.selectedUids);
|
||||
for (let i = 0; i < entityUids.length; ++i) {
|
||||
const uid = entityUids[i];
|
||||
const entity = this.root.entityMgr.findByUid(uid);
|
||||
if (!this.root.logic.tryDeleteBuilding(entity)) {
|
||||
logger.error("Error in mass delete, could not remove building");
|
||||
this.selectedUids.delete(uid);
|
||||
|
||||
// Build mapping from uid to entity
|
||||
/**
|
||||
* @type {Map<number, Entity>}
|
||||
*/
|
||||
const mapUidToEntity = this.root.entityMgr.getFrozenUidSearchMap();
|
||||
|
||||
this.root.logic.performBulkOperation(() => {
|
||||
for (let i = 0; i < entityUids.length; ++i) {
|
||||
const uid = entityUids[i];
|
||||
const entity = mapUidToEntity.get(uid);
|
||||
if (!entity) {
|
||||
logger.error("Entity not found by uid:", uid);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!this.root.logic.tryDeleteBuilding(entity)) {
|
||||
logger.error("Error in mass delete, could not remove building");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Clear uids later
|
||||
this.selectedUids = new Set();
|
||||
}
|
||||
|
||||
startCopy() {
|
||||
|
||||
@ -1,56 +1,55 @@
|
||||
import { BaseHUDPart } from "../base_hud_part";
|
||||
import { makeDiv } from "../../../core/utils";
|
||||
import { T } from "../../../translations";
|
||||
import { IS_DEMO } from "../../../core/config";
|
||||
|
||||
/** @enum {string} */
|
||||
export const enumNotificationType = {
|
||||
saved: "saved",
|
||||
upgrade: "upgrade",
|
||||
success: "success",
|
||||
};
|
||||
|
||||
const notificationDuration = 3;
|
||||
|
||||
export class HUDNotifications extends BaseHUDPart {
|
||||
createElements(parent) {
|
||||
this.element = makeDiv(parent, "ingame_HUD_Notifications", [], ``);
|
||||
}
|
||||
|
||||
initialize() {
|
||||
this.root.hud.signals.notification.add(this.onNotification, this);
|
||||
|
||||
/** @type {Array<{ element: HTMLElement, expireAt: number}>} */
|
||||
this.notificationElements = [];
|
||||
|
||||
// Automatic notifications
|
||||
this.root.signals.gameSaved.add(() =>
|
||||
this.onNotification(T.ingame.notifications.gameSaved, enumNotificationType.saved)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} message
|
||||
* @param {enumNotificationType} type
|
||||
*/
|
||||
onNotification(message, type) {
|
||||
const element = makeDiv(this.element, null, ["notification", "type-" + type], message);
|
||||
element.setAttribute("data-icon", "icons/notification_" + type + ".png");
|
||||
|
||||
this.notificationElements.push({
|
||||
element,
|
||||
expireAt: this.root.time.realtimeNow() + notificationDuration,
|
||||
});
|
||||
}
|
||||
|
||||
update() {
|
||||
const now = this.root.time.realtimeNow();
|
||||
for (let i = 0; i < this.notificationElements.length; ++i) {
|
||||
const handle = this.notificationElements[i];
|
||||
if (handle.expireAt <= now) {
|
||||
handle.element.remove();
|
||||
this.notificationElements.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
import { makeDiv } from "../../../core/utils";
|
||||
import { T } from "../../../translations";
|
||||
import { BaseHUDPart } from "../base_hud_part";
|
||||
|
||||
/** @enum {string} */
|
||||
export const enumNotificationType = {
|
||||
saved: "saved",
|
||||
upgrade: "upgrade",
|
||||
success: "success",
|
||||
};
|
||||
|
||||
const notificationDuration = 3;
|
||||
|
||||
export class HUDNotifications extends BaseHUDPart {
|
||||
createElements(parent) {
|
||||
this.element = makeDiv(parent, "ingame_HUD_Notifications", [], ``);
|
||||
}
|
||||
|
||||
initialize() {
|
||||
this.root.hud.signals.notification.add(this.onNotification, this);
|
||||
|
||||
/** @type {Array<{ element: HTMLElement, expireAt: number}>} */
|
||||
this.notificationElements = [];
|
||||
|
||||
// Automatic notifications
|
||||
this.root.signals.gameSaved.add(() =>
|
||||
this.onNotification(T.ingame.notifications.gameSaved, enumNotificationType.saved)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} message
|
||||
* @param {enumNotificationType} type
|
||||
*/
|
||||
onNotification(message, type) {
|
||||
const element = makeDiv(this.element, null, ["notification", "type-" + type], message);
|
||||
element.setAttribute("data-icon", "icons/notification_" + type + ".png");
|
||||
|
||||
this.notificationElements.push({
|
||||
element,
|
||||
expireAt: this.root.time.realtimeNow() + notificationDuration,
|
||||
});
|
||||
}
|
||||
|
||||
update() {
|
||||
const now = this.root.time.realtimeNow();
|
||||
for (let i = 0; i < this.notificationElements.length; ++i) {
|
||||
const handle = this.notificationElements[i];
|
||||
if (handle.expireAt <= now) {
|
||||
handle.element.remove();
|
||||
this.notificationElements.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -204,22 +204,20 @@ export function getStringForKeyCode(code) {
|
||||
case 115:
|
||||
return "F4";
|
||||
case 116:
|
||||
return "F4";
|
||||
case 117:
|
||||
return "F5";
|
||||
case 118:
|
||||
case 117:
|
||||
return "F6";
|
||||
case 119:
|
||||
case 118:
|
||||
return "F7";
|
||||
case 120:
|
||||
case 119:
|
||||
return "F8";
|
||||
case 121:
|
||||
case 120:
|
||||
return "F9";
|
||||
case 122:
|
||||
case 121:
|
||||
return "F10";
|
||||
case 123:
|
||||
case 122:
|
||||
return "F11";
|
||||
case 124:
|
||||
case 123:
|
||||
return "F12";
|
||||
|
||||
case 144:
|
||||
|
||||
@ -1,236 +1,236 @@
|
||||
import { globalConfig } from "../core/config";
|
||||
import { Vector } from "../core/vector";
|
||||
import { BasicSerializableObject, types } from "../savegame/serialization";
|
||||
import { BaseItem } from "./base_item";
|
||||
import { Entity } from "./entity";
|
||||
import { MapChunkView } from "./map_chunk_view";
|
||||
import { GameRoot } from "./root";
|
||||
|
||||
export class BaseMap extends BasicSerializableObject {
|
||||
static getId() {
|
||||
return "Map";
|
||||
}
|
||||
|
||||
static getSchema() {
|
||||
return {
|
||||
seed: types.uint,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {GameRoot} root
|
||||
*/
|
||||
constructor(root) {
|
||||
super();
|
||||
this.root = root;
|
||||
|
||||
this.seed = 0;
|
||||
|
||||
/**
|
||||
* Mapping of 'X|Y' to chunk
|
||||
* @type {Map<string, MapChunkView>} */
|
||||
this.chunksById = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the given chunk by index
|
||||
* @param {number} chunkX
|
||||
* @param {number} chunkY
|
||||
*/
|
||||
getChunk(chunkX, chunkY, createIfNotExistent = false) {
|
||||
const chunkIdentifier = chunkX + "|" + chunkY;
|
||||
let storedChunk;
|
||||
|
||||
if ((storedChunk = this.chunksById.get(chunkIdentifier))) {
|
||||
return storedChunk;
|
||||
}
|
||||
|
||||
if (createIfNotExistent) {
|
||||
const instance = new MapChunkView(this.root, chunkX, chunkY);
|
||||
this.chunksById.set(chunkIdentifier, instance);
|
||||
return instance;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets or creates a new chunk if not existent for the given tile
|
||||
* @param {number} tileX
|
||||
* @param {number} tileY
|
||||
* @returns {MapChunkView}
|
||||
*/
|
||||
getOrCreateChunkAtTile(tileX, tileY) {
|
||||
const chunkX = Math.floor(tileX / globalConfig.mapChunkSize);
|
||||
const chunkY = Math.floor(tileY / globalConfig.mapChunkSize);
|
||||
return this.getChunk(chunkX, chunkY, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a chunk if not existent for the given tile
|
||||
* @param {number} tileX
|
||||
* @param {number} tileY
|
||||
* @returns {MapChunkView?}
|
||||
*/
|
||||
getChunkAtTileOrNull(tileX, tileY) {
|
||||
const chunkX = Math.floor(tileX / globalConfig.mapChunkSize);
|
||||
const chunkY = Math.floor(tileY / globalConfig.mapChunkSize);
|
||||
return this.getChunk(chunkX, chunkY, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given tile is within the map bounds
|
||||
* @param {Vector} tile
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isValidTile(tile) {
|
||||
if (G_IS_DEV) {
|
||||
assert(tile instanceof Vector, "tile is not a vector");
|
||||
}
|
||||
return Number.isInteger(tile.x) && Number.isInteger(tile.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the tile content of a given tile
|
||||
* @param {Vector} tile
|
||||
* @param {Layer} layer
|
||||
* @returns {Entity} Entity or null
|
||||
*/
|
||||
getTileContent(tile, layer) {
|
||||
if (G_IS_DEV) {
|
||||
this.internalCheckTile(tile);
|
||||
}
|
||||
const chunk = this.getChunkAtTileOrNull(tile.x, tile.y);
|
||||
return chunk && chunk.getLayerContentFromWorldCoords(tile.x, tile.y, layer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the lower layers content of the given tile
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @returns {BaseItem=}
|
||||
*/
|
||||
getLowerLayerContentXY(x, y) {
|
||||
return this.getOrCreateChunkAtTile(x, y).getLowerLayerFromWorldCoords(x, y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the tile content of a given tile
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @param {Layer} layer
|
||||
* @returns {Entity} Entity or null
|
||||
*/
|
||||
getLayerContentXY(x, y, layer) {
|
||||
const chunk = this.getChunkAtTileOrNull(x, y);
|
||||
return chunk && chunk.getLayerContentFromWorldCoords(x, y, layer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the tile contents of a given tile
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @returns {Array<Entity>} Entity or null
|
||||
*/
|
||||
getLayersContentsMultipleXY(x, y) {
|
||||
const chunk = this.getChunkAtTileOrNull(x, y);
|
||||
if (!chunk) {
|
||||
return [];
|
||||
}
|
||||
return chunk.getLayersContentsMultipleFromWorldCoords(x, y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the tile is used
|
||||
* @param {Vector} tile
|
||||
* @param {Layer} layer
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isTileUsed(tile, layer) {
|
||||
if (G_IS_DEV) {
|
||||
this.internalCheckTile(tile);
|
||||
}
|
||||
const chunk = this.getChunkAtTileOrNull(tile.x, tile.y);
|
||||
return chunk && chunk.getLayerContentFromWorldCoords(tile.x, tile.y, layer) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the tile is used
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @param {Layer} layer
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isTileUsedXY(x, y, layer) {
|
||||
const chunk = this.getChunkAtTileOrNull(x, y);
|
||||
return chunk && chunk.getLayerContentFromWorldCoords(x, y, layer) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the tiles content
|
||||
* @param {Vector} tile
|
||||
* @param {Entity} entity
|
||||
*/
|
||||
setTileContent(tile, entity) {
|
||||
if (G_IS_DEV) {
|
||||
this.internalCheckTile(tile);
|
||||
}
|
||||
|
||||
this.getOrCreateChunkAtTile(tile.x, tile.y).setLayerContentFromWorldCords(
|
||||
tile.x,
|
||||
tile.y,
|
||||
entity,
|
||||
entity.layer
|
||||
);
|
||||
|
||||
const staticComponent = entity.components.StaticMapEntity;
|
||||
assert(staticComponent, "Can only place static map entities in tiles");
|
||||
}
|
||||
|
||||
/**
|
||||
* Places an entity with the StaticMapEntity component
|
||||
* @param {Entity} entity
|
||||
*/
|
||||
placeStaticEntity(entity) {
|
||||
assert(entity.components.StaticMapEntity, "Entity is not static");
|
||||
const staticComp = entity.components.StaticMapEntity;
|
||||
const rect = staticComp.getTileSpaceBounds();
|
||||
for (let dx = 0; dx < rect.w; ++dx) {
|
||||
for (let dy = 0; dy < rect.h; ++dy) {
|
||||
const x = rect.x + dx;
|
||||
const y = rect.y + dy;
|
||||
this.getOrCreateChunkAtTile(x, y).setLayerContentFromWorldCords(x, y, entity, entity.layer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an entity with the StaticMapEntity component
|
||||
* @param {Entity} entity
|
||||
*/
|
||||
removeStaticEntity(entity) {
|
||||
assert(entity.components.StaticMapEntity, "Entity is not static");
|
||||
const staticComp = entity.components.StaticMapEntity;
|
||||
const rect = staticComp.getTileSpaceBounds();
|
||||
for (let dx = 0; dx < rect.w; ++dx) {
|
||||
for (let dy = 0; dy < rect.h; ++dy) {
|
||||
const x = rect.x + dx;
|
||||
const y = rect.y + dy;
|
||||
this.getOrCreateChunkAtTile(x, y).setLayerContentFromWorldCords(x, y, null, entity.layer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Internal
|
||||
|
||||
/**
|
||||
* Checks a given tile for validty
|
||||
* @param {Vector} tile
|
||||
*/
|
||||
internalCheckTile(tile) {
|
||||
assert(tile instanceof Vector, "tile is not a vector: " + tile);
|
||||
assert(tile.x % 1 === 0, "Tile X is not a valid integer: " + tile.x);
|
||||
assert(tile.y % 1 === 0, "Tile Y is not a valid integer: " + tile.y);
|
||||
}
|
||||
}
|
||||
import { globalConfig } from "../core/config";
|
||||
import { Vector } from "../core/vector";
|
||||
import { BasicSerializableObject, types } from "../savegame/serialization";
|
||||
import { BaseItem } from "./base_item";
|
||||
import { Entity } from "./entity";
|
||||
import { MapChunkView } from "./map_chunk_view";
|
||||
import { GameRoot } from "./root";
|
||||
|
||||
export class BaseMap extends BasicSerializableObject {
|
||||
static getId() {
|
||||
return "Map";
|
||||
}
|
||||
|
||||
static getSchema() {
|
||||
return {
|
||||
seed: types.uint,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {GameRoot} root
|
||||
*/
|
||||
constructor(root) {
|
||||
super();
|
||||
this.root = root;
|
||||
|
||||
this.seed = 0;
|
||||
|
||||
/**
|
||||
* Mapping of 'X|Y' to chunk
|
||||
* @type {Map<string, MapChunkView>} */
|
||||
this.chunksById = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the given chunk by index
|
||||
* @param {number} chunkX
|
||||
* @param {number} chunkY
|
||||
*/
|
||||
getChunk(chunkX, chunkY, createIfNotExistent = false) {
|
||||
const chunkIdentifier = chunkX + "|" + chunkY;
|
||||
let storedChunk;
|
||||
|
||||
if ((storedChunk = this.chunksById.get(chunkIdentifier))) {
|
||||
return storedChunk;
|
||||
}
|
||||
|
||||
if (createIfNotExistent) {
|
||||
const instance = new MapChunkView(this.root, chunkX, chunkY);
|
||||
this.chunksById.set(chunkIdentifier, instance);
|
||||
return instance;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets or creates a new chunk if not existent for the given tile
|
||||
* @param {number} tileX
|
||||
* @param {number} tileY
|
||||
* @returns {MapChunkView}
|
||||
*/
|
||||
getOrCreateChunkAtTile(tileX, tileY) {
|
||||
const chunkX = Math.floor(tileX / globalConfig.mapChunkSize);
|
||||
const chunkY = Math.floor(tileY / globalConfig.mapChunkSize);
|
||||
return this.getChunk(chunkX, chunkY, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a chunk if not existent for the given tile
|
||||
* @param {number} tileX
|
||||
* @param {number} tileY
|
||||
* @returns {MapChunkView?}
|
||||
*/
|
||||
getChunkAtTileOrNull(tileX, tileY) {
|
||||
const chunkX = Math.floor(tileX / globalConfig.mapChunkSize);
|
||||
const chunkY = Math.floor(tileY / globalConfig.mapChunkSize);
|
||||
return this.getChunk(chunkX, chunkY, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given tile is within the map bounds
|
||||
* @param {Vector} tile
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isValidTile(tile) {
|
||||
if (G_IS_DEV) {
|
||||
assert(tile instanceof Vector, "tile is not a vector");
|
||||
}
|
||||
return Number.isInteger(tile.x) && Number.isInteger(tile.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the tile content of a given tile
|
||||
* @param {Vector} tile
|
||||
* @param {Layer} layer
|
||||
* @returns {Entity} Entity or null
|
||||
*/
|
||||
getTileContent(tile, layer) {
|
||||
if (G_IS_DEV) {
|
||||
this.internalCheckTile(tile);
|
||||
}
|
||||
const chunk = this.getChunkAtTileOrNull(tile.x, tile.y);
|
||||
return chunk && chunk.getLayerContentFromWorldCoords(tile.x, tile.y, layer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the lower layers content of the given tile
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @returns {BaseItem=}
|
||||
*/
|
||||
getLowerLayerContentXY(x, y) {
|
||||
return this.getOrCreateChunkAtTile(x, y).getLowerLayerFromWorldCoords(x, y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the tile content of a given tile
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @param {Layer} layer
|
||||
* @returns {Entity} Entity or null
|
||||
*/
|
||||
getLayerContentXY(x, y, layer) {
|
||||
const chunk = this.getChunkAtTileOrNull(x, y);
|
||||
return chunk && chunk.getLayerContentFromWorldCoords(x, y, layer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the tile contents of a given tile
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @returns {Array<Entity>} Entity or null
|
||||
*/
|
||||
getLayersContentsMultipleXY(x, y) {
|
||||
const chunk = this.getChunkAtTileOrNull(x, y);
|
||||
if (!chunk) {
|
||||
return [];
|
||||
}
|
||||
return chunk.getLayersContentsMultipleFromWorldCoords(x, y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the tile is used
|
||||
* @param {Vector} tile
|
||||
* @param {Layer} layer
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isTileUsed(tile, layer) {
|
||||
if (G_IS_DEV) {
|
||||
this.internalCheckTile(tile);
|
||||
}
|
||||
const chunk = this.getChunkAtTileOrNull(tile.x, tile.y);
|
||||
return chunk && chunk.getLayerContentFromWorldCoords(tile.x, tile.y, layer) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the tile is used
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @param {Layer} layer
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isTileUsedXY(x, y, layer) {
|
||||
const chunk = this.getChunkAtTileOrNull(x, y);
|
||||
return chunk && chunk.getLayerContentFromWorldCoords(x, y, layer) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the tiles content
|
||||
* @param {Vector} tile
|
||||
* @param {Entity} entity
|
||||
*/
|
||||
setTileContent(tile, entity) {
|
||||
if (G_IS_DEV) {
|
||||
this.internalCheckTile(tile);
|
||||
}
|
||||
|
||||
this.getOrCreateChunkAtTile(tile.x, tile.y).setLayerContentFromWorldCords(
|
||||
tile.x,
|
||||
tile.y,
|
||||
entity,
|
||||
entity.layer
|
||||
);
|
||||
|
||||
const staticComponent = entity.components.StaticMapEntity;
|
||||
assert(staticComponent, "Can only place static map entities in tiles");
|
||||
}
|
||||
|
||||
/**
|
||||
* Places an entity with the StaticMapEntity component
|
||||
* @param {Entity} entity
|
||||
*/
|
||||
placeStaticEntity(entity) {
|
||||
assert(entity.components.StaticMapEntity, "Entity is not static");
|
||||
const staticComp = entity.components.StaticMapEntity;
|
||||
const rect = staticComp.getTileSpaceBounds();
|
||||
for (let dx = 0; dx < rect.w; ++dx) {
|
||||
for (let dy = 0; dy < rect.h; ++dy) {
|
||||
const x = rect.x + dx;
|
||||
const y = rect.y + dy;
|
||||
this.getOrCreateChunkAtTile(x, y).setLayerContentFromWorldCords(x, y, entity, entity.layer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an entity with the StaticMapEntity component
|
||||
* @param {Entity} entity
|
||||
*/
|
||||
removeStaticEntity(entity) {
|
||||
assert(entity.components.StaticMapEntity, "Entity is not static");
|
||||
const staticComp = entity.components.StaticMapEntity;
|
||||
const rect = staticComp.getTileSpaceBounds();
|
||||
for (let dx = 0; dx < rect.w; ++dx) {
|
||||
for (let dy = 0; dy < rect.h; ++dy) {
|
||||
const x = rect.x + dx;
|
||||
const y = rect.y + dy;
|
||||
this.getOrCreateChunkAtTile(x, y).setLayerContentFromWorldCords(x, y, null, entity.layer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Internal
|
||||
|
||||
/**
|
||||
* Checks a given tile for validty
|
||||
* @param {Vector} tile
|
||||
*/
|
||||
internalCheckTile(tile) {
|
||||
assert(tile instanceof Vector, "tile is not a vector: " + tile);
|
||||
assert(tile.x % 1 === 0, "Tile X is not a valid integer: " + tile.x);
|
||||
assert(tile.y % 1 === 0, "Tile Y is not a valid integer: " + tile.y);
|
||||
}
|
||||
}
|
||||
|
||||
@ -88,16 +88,17 @@ export class MapChunkView extends MapChunk {
|
||||
});
|
||||
|
||||
const dims = globalConfig.mapChunkWorldSize;
|
||||
const extrude = 0.05;
|
||||
|
||||
// Draw chunk "pixel" art
|
||||
parameters.context.imageSmoothingEnabled = false;
|
||||
drawSpriteClipped({
|
||||
parameters,
|
||||
sprite,
|
||||
x: this.x * dims,
|
||||
y: this.y * dims,
|
||||
w: dims,
|
||||
h: dims,
|
||||
x: this.x * dims - extrude,
|
||||
y: this.y * dims - extrude,
|
||||
w: dims + 2 * extrude,
|
||||
h: dims + 2 * extrude,
|
||||
originalW: overlaySize,
|
||||
originalH: overlaySize,
|
||||
});
|
||||
|
||||
@ -66,32 +66,34 @@ export class MapView extends BaseMap {
|
||||
* @param {DrawParameters} drawParameters
|
||||
*/
|
||||
drawStaticEntityDebugOverlays(drawParameters) {
|
||||
const cullRange = drawParameters.visibleRect.toTileCullRectangle();
|
||||
const top = cullRange.top();
|
||||
const right = cullRange.right();
|
||||
const bottom = cullRange.bottom();
|
||||
const left = cullRange.left();
|
||||
if (G_IS_DEV && (globalConfig.debug.showAcceptorEjectors || globalConfig.debug.showEntityBounds)) {
|
||||
const cullRange = drawParameters.visibleRect.toTileCullRectangle();
|
||||
const top = cullRange.top();
|
||||
const right = cullRange.right();
|
||||
const bottom = cullRange.bottom();
|
||||
const left = cullRange.left();
|
||||
|
||||
const border = 1;
|
||||
const border = 1;
|
||||
|
||||
const minY = top - border;
|
||||
const maxY = bottom + border;
|
||||
const minX = left - border;
|
||||
const maxX = right + border - 1;
|
||||
const minY = top - border;
|
||||
const maxY = bottom + border;
|
||||
const minX = left - border;
|
||||
const maxX = right + border - 1;
|
||||
|
||||
// Render y from top down for proper blending
|
||||
for (let y = minY; y <= maxY; ++y) {
|
||||
for (let x = minX; x <= maxX; ++x) {
|
||||
// const content = this.tiles[x][y];
|
||||
const chunk = this.getChunkAtTileOrNull(x, y);
|
||||
if (!chunk) {
|
||||
continue;
|
||||
}
|
||||
const content = chunk.getTileContentFromWorldCoords(x, y);
|
||||
if (content) {
|
||||
let isBorder = x <= left - 1 || x >= right + 1 || y <= top - 1 || y >= bottom + 1;
|
||||
if (!isBorder) {
|
||||
content.drawDebugOverlays(drawParameters);
|
||||
// Render y from top down for proper blending
|
||||
for (let y = minY; y <= maxY; ++y) {
|
||||
for (let x = minX; x <= maxX; ++x) {
|
||||
// const content = this.tiles[x][y];
|
||||
const chunk = this.getChunkAtTileOrNull(x, y);
|
||||
if (!chunk) {
|
||||
continue;
|
||||
}
|
||||
const content = chunk.getTileContentFromWorldCoords(x, y);
|
||||
if (content) {
|
||||
let isBorder = x <= left - 1 || x >= right + 1 || y <= top - 1 || y >= bottom + 1;
|
||||
if (!isBorder) {
|
||||
content.drawDebugOverlays(drawParameters);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -236,4 +236,7 @@ export function initBuildingCodesAfterResourcesLoaded() {
|
||||
);
|
||||
variant.silhouetteColor = variant.metaInstance.getSilhouetteColor();
|
||||
}
|
||||
|
||||
// Update caches
|
||||
buildBuildingCodeCache();
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -9,14 +9,35 @@ import { MapChunkView } from "../map_chunk_view";
|
||||
export class ItemAcceptorSystem extends GameSystemWithFilter {
|
||||
constructor(root) {
|
||||
super(root, [ItemAcceptorComponent]);
|
||||
|
||||
// Well ... it's better to be verbose I guess?
|
||||
this.accumulatedTicksWhileInMapOverview = 0;
|
||||
}
|
||||
|
||||
update() {
|
||||
if (this.root.app.settings.getAllSettings().simplifiedBelts) {
|
||||
// Disabled in potato mode
|
||||
return;
|
||||
}
|
||||
|
||||
// This system doesn't render anything while in map overview,
|
||||
// so simply accumulate ticks
|
||||
if (this.root.camera.getIsMapOverlayActive()) {
|
||||
++this.accumulatedTicksWhileInMapOverview;
|
||||
return;
|
||||
}
|
||||
|
||||
// Compute how much ticks we missed
|
||||
const numTicks = 1 + this.accumulatedTicksWhileInMapOverview;
|
||||
const progress =
|
||||
this.root.dynamicTickrate.deltaSeconds *
|
||||
2 *
|
||||
this.root.hubGoals.getBeltBaseSpeed() *
|
||||
globalConfig.itemSpacingOnBelts; // * 2 because its only a half tile
|
||||
globalConfig.itemSpacingOnBelts * // * 2 because its only a half tile
|
||||
numTicks;
|
||||
|
||||
// Reset accumulated ticks
|
||||
this.accumulatedTicksWhileInMapOverview = 0;
|
||||
|
||||
for (let i = 0; i < this.allEntities.length; ++i) {
|
||||
const entity = this.allEntities[i];
|
||||
@ -40,6 +61,11 @@ export class ItemAcceptorSystem extends GameSystemWithFilter {
|
||||
* @param {MapChunkView} chunk
|
||||
*/
|
||||
drawChunk(parameters, chunk) {
|
||||
if (this.root.app.settings.getAllSettings().simplifiedBelts) {
|
||||
// Disabled in potato mode
|
||||
return;
|
||||
}
|
||||
|
||||
const contents = chunk.containedEntitiesByLayer.regular;
|
||||
for (let i = 0; i < contents.length; ++i) {
|
||||
const entity = contents[i];
|
||||
|
||||
@ -2,8 +2,11 @@ import { globalConfig } from "../../core/config";
|
||||
import { DrawParameters } from "../../core/draw_parameters";
|
||||
import { createLogger } from "../../core/logging";
|
||||
import { Rectangle } from "../../core/rectangle";
|
||||
import { StaleAreaDetector } from "../../core/stale_area_detector";
|
||||
import { enumDirection, enumDirectionToVector } from "../../core/vector";
|
||||
import { BaseItem } from "../base_item";
|
||||
import { BeltComponent } from "../components/belt";
|
||||
import { ItemAcceptorComponent } from "../components/item_acceptor";
|
||||
import { ItemEjectorComponent } from "../components/item_ejector";
|
||||
import { Entity } from "../entity";
|
||||
import { GameSystemWithFilter } from "../game_system_with_filter";
|
||||
@ -15,102 +18,52 @@ export class ItemEjectorSystem extends GameSystemWithFilter {
|
||||
constructor(root) {
|
||||
super(root, [ItemEjectorComponent]);
|
||||
|
||||
this.root.signals.entityAdded.add(this.checkForCacheInvalidation, this);
|
||||
this.root.signals.entityDestroyed.add(this.checkForCacheInvalidation, this);
|
||||
this.root.signals.postLoadHook.add(this.recomputeCache, this);
|
||||
this.staleAreaDetector = new StaleAreaDetector({
|
||||
root: this.root,
|
||||
name: "item-ejector",
|
||||
recomputeMethod: this.recomputeArea.bind(this),
|
||||
});
|
||||
|
||||
/**
|
||||
* @type {Rectangle}
|
||||
*/
|
||||
this.areaToRecompute = null;
|
||||
this.staleAreaDetector.recomputeOnComponentsChanged(
|
||||
[ItemEjectorComponent, ItemAcceptorComponent, BeltComponent],
|
||||
1
|
||||
);
|
||||
|
||||
this.root.signals.postLoadHook.add(this.recomputeCacheFull, this);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Entity} entity
|
||||
* Recomputes an area after it changed
|
||||
* @param {Rectangle} area
|
||||
*/
|
||||
checkForCacheInvalidation(entity) {
|
||||
if (!this.root.gameInitialized) {
|
||||
return;
|
||||
}
|
||||
if (!entity.components.StaticMapEntity) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Optimize for the common case: adding or removing one building at a time. Clicking
|
||||
// and dragging can cause up to 4 add/remove signals.
|
||||
const staticComp = entity.components.StaticMapEntity;
|
||||
const bounds = staticComp.getTileSpaceBounds();
|
||||
const expandedBounds = bounds.expandedInAllDirections(2);
|
||||
|
||||
if (this.areaToRecompute) {
|
||||
this.areaToRecompute = this.areaToRecompute.getUnion(expandedBounds);
|
||||
} else {
|
||||
this.areaToRecompute = expandedBounds;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Precomputes the cache, which makes up for a huge performance improvement
|
||||
*/
|
||||
recomputeCache() {
|
||||
if (this.areaToRecompute) {
|
||||
logger.log("Recomputing cache using rectangle");
|
||||
if (G_IS_DEV && globalConfig.debug.renderChanges) {
|
||||
this.root.hud.parts.changesDebugger.renderChange(
|
||||
"ejector-area",
|
||||
this.areaToRecompute,
|
||||
"#fe50a6"
|
||||
);
|
||||
}
|
||||
this.recomputeAreaCache();
|
||||
this.areaToRecompute = null;
|
||||
} else {
|
||||
logger.log("Full cache recompute");
|
||||
if (G_IS_DEV && globalConfig.debug.renderChanges) {
|
||||
this.root.hud.parts.changesDebugger.renderChange(
|
||||
"ejector-full",
|
||||
new Rectangle(-1000, -1000, 2000, 2000),
|
||||
"#fe50a6"
|
||||
);
|
||||
}
|
||||
|
||||
// Try to find acceptors for every ejector
|
||||
for (let i = 0; i < this.allEntities.length; ++i) {
|
||||
const entity = this.allEntities[i];
|
||||
this.recomputeSingleEntityCache(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recomputes the cache in the given area
|
||||
*/
|
||||
recomputeAreaCache() {
|
||||
const area = this.areaToRecompute;
|
||||
let entryCount = 0;
|
||||
|
||||
logger.log("Recomputing area:", area.x, area.y, "/", area.w, area.h);
|
||||
|
||||
// Store the entities we already recomputed, so we don't do work twice
|
||||
const recomputedEntities = new Set();
|
||||
|
||||
for (let x = area.x; x < area.right(); ++x) {
|
||||
for (let y = area.y; y < area.bottom(); ++y) {
|
||||
const entities = this.root.map.getLayersContentsMultipleXY(x, y);
|
||||
for (let i = 0; i < entities.length; ++i) {
|
||||
const entity = entities[i];
|
||||
|
||||
// Recompute the entity in case its relevant for this system and it
|
||||
// hasn't already been computed
|
||||
if (!recomputedEntities.has(entity.uid) && entity.components.ItemEjector) {
|
||||
recomputedEntities.add(entity.uid);
|
||||
this.recomputeSingleEntityCache(entity);
|
||||
recomputeArea(area) {
|
||||
/** @type {Set<number>} */
|
||||
const seenUids = new Set();
|
||||
for (let x = 0; x < area.w; ++x) {
|
||||
for (let y = 0; y < area.h; ++y) {
|
||||
const tileX = area.x + x;
|
||||
const tileY = area.y + y;
|
||||
// @NOTICE: Item ejector currently only supports regular layer
|
||||
const contents = this.root.map.getLayerContentXY(tileX, tileY, "regular");
|
||||
if (contents && contents.components.ItemEjector) {
|
||||
if (!seenUids.has(contents.uid)) {
|
||||
seenUids.add(contents.uid);
|
||||
this.recomputeSingleEntityCache(contents);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return entryCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recomputes the whole cache after the game has loaded
|
||||
*/
|
||||
recomputeCacheFull() {
|
||||
logger.log("Full cache recompute in post load hook");
|
||||
for (let i = 0; i < this.allEntities.length; ++i) {
|
||||
const entity = this.allEntities[i];
|
||||
this.recomputeSingleEntityCache(entity);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -183,9 +136,7 @@ export class ItemEjectorSystem extends GameSystemWithFilter {
|
||||
}
|
||||
|
||||
update() {
|
||||
if (this.areaToRecompute) {
|
||||
this.recomputeCache();
|
||||
}
|
||||
this.staleAreaDetector.update();
|
||||
|
||||
// Precompute effective belt speed
|
||||
let progressGrowth = 2 * this.root.dynamicTickrate.deltaSeconds;
|
||||
@ -251,7 +202,13 @@ export class ItemEjectorSystem extends GameSystemWithFilter {
|
||||
// Try to hand over the item
|
||||
if (this.tryPassOverItem(item, destEntity, destSlot.index)) {
|
||||
// Handover successful, clear slot
|
||||
targetAcceptorComp.onItemAccepted(destSlot.index, destSlot.acceptedDirection, item);
|
||||
if (!this.root.app.settings.getAllSettings().simplifiedBelts) {
|
||||
targetAcceptorComp.onItemAccepted(
|
||||
destSlot.index,
|
||||
destSlot.acceptedDirection,
|
||||
item
|
||||
);
|
||||
}
|
||||
sourceSlot.item = null;
|
||||
continue;
|
||||
}
|
||||
@ -333,6 +290,11 @@ export class ItemEjectorSystem extends GameSystemWithFilter {
|
||||
* @param {MapChunkView} chunk
|
||||
*/
|
||||
drawChunk(parameters, chunk) {
|
||||
if (this.root.app.settings.getAllSettings().simplifiedBelts) {
|
||||
// Disabled in potato mode
|
||||
return;
|
||||
}
|
||||
|
||||
const contents = chunk.containedEntitiesByLayer.regular;
|
||||
|
||||
for (let i = 0; i < contents.length; ++i) {
|
||||
|
||||
@ -553,25 +553,7 @@ export class ItemProcessorSystem extends GameSystemWithFilter {
|
||||
const bonusTimeToApply = Math.min(originalTime, processorComp.bonusTime);
|
||||
const timeToProcess = originalTime - bonusTimeToApply;
|
||||
|
||||
// Substract one tick because we already process it this frame
|
||||
// if (processorComp.bonusTime > originalTime) {
|
||||
// if (processorComp.type === enumItemProcessorTypes.reader) {
|
||||
// console.log(
|
||||
// "Bonus time",
|
||||
// round4Digits(processorComp.bonusTime),
|
||||
// "Original time",
|
||||
// round4Digits(originalTime),
|
||||
// "Overcomit by",
|
||||
// round4Digits(processorComp.bonusTime - originalTime),
|
||||
// "->",
|
||||
// round4Digits(timeToProcess),
|
||||
// "reduced by",
|
||||
// round4Digits(bonusTimeToApply)
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
processorComp.bonusTime -= bonusTimeToApply;
|
||||
|
||||
processorComp.ongoingCharges.push({
|
||||
items: outItems,
|
||||
remainingTime: timeToProcess,
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { globalConfig } from "../../core/config";
|
||||
import { Loader } from "../../core/loader";
|
||||
import { smoothPulse, round4Digits } from "../../core/utils";
|
||||
import { smoothPulse } from "../../core/utils";
|
||||
import { enumItemProcessorRequirements, enumItemProcessorTypes } from "../components/item_processor";
|
||||
import { Entity } from "../entity";
|
||||
import { GameSystem } from "../game_system";
|
||||
@ -17,7 +17,6 @@ export class ItemProcessorOverlaysSystem extends GameSystem {
|
||||
this.readerOverlaySprite = Loader.getSprite("sprites/misc/reader_overlay.png");
|
||||
|
||||
this.drawnUids = new Set();
|
||||
|
||||
this.root.signals.gameFrameStarted.add(this.clearDrawnUids, this);
|
||||
}
|
||||
|
||||
@ -40,7 +39,6 @@ export class ItemProcessorOverlaysSystem extends GameSystem {
|
||||
}
|
||||
|
||||
const requirement = processorComp.processingRequirement;
|
||||
|
||||
if (!requirement && processorComp.type !== enumItemProcessorTypes.reader) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -46,7 +46,6 @@ export class MinerSystem extends GameSystemWithFilter {
|
||||
}
|
||||
|
||||
// Check if miner is above an actual tile
|
||||
|
||||
if (!minerComp.cachedMinedItem) {
|
||||
const staticComp = entity.components.StaticMapEntity;
|
||||
const tileBelow = this.root.map.getLowerLayerContentXY(
|
||||
@ -171,7 +170,7 @@ export class MinerSystem extends GameSystemWithFilter {
|
||||
}
|
||||
|
||||
// Draw the item background - this is to hide the ejected item animation from
|
||||
// the item ejecto
|
||||
// the item ejector
|
||||
|
||||
const padding = 3;
|
||||
const destX = staticComp.origin.x * globalConfig.tileSize + padding;
|
||||
|
||||
@ -1,101 +1,101 @@
|
||||
import { GameSystemWithFilter } from "../game_system_with_filter";
|
||||
import { StorageComponent } from "../components/storage";
|
||||
import { DrawParameters } from "../../core/draw_parameters";
|
||||
import { formatBigNumber, lerp } from "../../core/utils";
|
||||
import { Loader } from "../../core/loader";
|
||||
import { BOOL_TRUE_SINGLETON, BOOL_FALSE_SINGLETON } from "../items/boolean_item";
|
||||
import { MapChunkView } from "../map_chunk_view";
|
||||
|
||||
export class StorageSystem extends GameSystemWithFilter {
|
||||
constructor(root) {
|
||||
super(root, [StorageComponent]);
|
||||
|
||||
this.storageOverlaySprite = Loader.getSprite("sprites/misc/storage_overlay.png");
|
||||
|
||||
/**
|
||||
* Stores which uids were already drawn to avoid drawing entities twice
|
||||
* @type {Set<number>}
|
||||
*/
|
||||
this.drawnUids = new Set();
|
||||
|
||||
this.root.signals.gameFrameStarted.add(this.clearDrawnUids, this);
|
||||
}
|
||||
|
||||
clearDrawnUids() {
|
||||
this.drawnUids.clear();
|
||||
}
|
||||
|
||||
update() {
|
||||
for (let i = 0; i < this.allEntities.length; ++i) {
|
||||
const entity = this.allEntities[i];
|
||||
const storageComp = entity.components.Storage;
|
||||
const pinsComp = entity.components.WiredPins;
|
||||
|
||||
// Eject from storage
|
||||
if (storageComp.storedItem && storageComp.storedCount > 0) {
|
||||
const ejectorComp = entity.components.ItemEjector;
|
||||
|
||||
const nextSlot = ejectorComp.getFirstFreeSlot();
|
||||
if (nextSlot !== null) {
|
||||
if (ejectorComp.tryEject(nextSlot, storageComp.storedItem)) {
|
||||
storageComp.storedCount--;
|
||||
|
||||
if (storageComp.storedCount === 0) {
|
||||
storageComp.storedItem = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let targetAlpha = storageComp.storedCount > 0 ? 1 : 0;
|
||||
storageComp.overlayOpacity = lerp(storageComp.overlayOpacity, targetAlpha, 0.05);
|
||||
|
||||
pinsComp.slots[0].value = storageComp.storedItem;
|
||||
pinsComp.slots[1].value = storageComp.getIsFull() ? BOOL_TRUE_SINGLETON : BOOL_FALSE_SINGLETON;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {DrawParameters} parameters
|
||||
* @param {MapChunkView} chunk
|
||||
*/
|
||||
drawChunk(parameters, chunk) {
|
||||
const contents = chunk.containedEntitiesByLayer.regular;
|
||||
for (let i = 0; i < contents.length; ++i) {
|
||||
const entity = contents[i];
|
||||
const storageComp = entity.components.Storage;
|
||||
if (!storageComp) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const storedItem = storageComp.storedItem;
|
||||
if (!storedItem) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.drawnUids.has(entity.uid)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.drawnUids.add(entity.uid);
|
||||
|
||||
const staticComp = entity.components.StaticMapEntity;
|
||||
|
||||
const context = parameters.context;
|
||||
context.globalAlpha = storageComp.overlayOpacity;
|
||||
const center = staticComp.getTileSpaceBounds().getCenter().toWorldSpace();
|
||||
storedItem.drawItemCenteredClipped(center.x, center.y, parameters, 30);
|
||||
|
||||
this.storageOverlaySprite.drawCached(parameters, center.x - 15, center.y + 15, 30, 15);
|
||||
|
||||
if (parameters.visibleRect.containsCircle(center.x, center.y + 25, 20)) {
|
||||
context.font = "bold 10px GameFont";
|
||||
context.textAlign = "center";
|
||||
context.fillStyle = "#64666e";
|
||||
context.fillText(formatBigNumber(storageComp.storedCount), center.x, center.y + 25.5);
|
||||
context.textAlign = "left";
|
||||
}
|
||||
context.globalAlpha = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
import { GameSystemWithFilter } from "../game_system_with_filter";
|
||||
import { StorageComponent } from "../components/storage";
|
||||
import { DrawParameters } from "../../core/draw_parameters";
|
||||
import { formatBigNumber, lerp } from "../../core/utils";
|
||||
import { Loader } from "../../core/loader";
|
||||
import { BOOL_TRUE_SINGLETON, BOOL_FALSE_SINGLETON } from "../items/boolean_item";
|
||||
import { MapChunkView } from "../map_chunk_view";
|
||||
|
||||
export class StorageSystem extends GameSystemWithFilter {
|
||||
constructor(root) {
|
||||
super(root, [StorageComponent]);
|
||||
|
||||
this.storageOverlaySprite = Loader.getSprite("sprites/misc/storage_overlay.png");
|
||||
|
||||
/**
|
||||
* Stores which uids were already drawn to avoid drawing entities twice
|
||||
* @type {Set<number>}
|
||||
*/
|
||||
this.drawnUids = new Set();
|
||||
|
||||
this.root.signals.gameFrameStarted.add(this.clearDrawnUids, this);
|
||||
}
|
||||
|
||||
clearDrawnUids() {
|
||||
this.drawnUids.clear();
|
||||
}
|
||||
|
||||
update() {
|
||||
for (let i = 0; i < this.allEntities.length; ++i) {
|
||||
const entity = this.allEntities[i];
|
||||
const storageComp = entity.components.Storage;
|
||||
const pinsComp = entity.components.WiredPins;
|
||||
|
||||
// Eject from storage
|
||||
if (storageComp.storedItem && storageComp.storedCount > 0) {
|
||||
const ejectorComp = entity.components.ItemEjector;
|
||||
|
||||
const nextSlot = ejectorComp.getFirstFreeSlot();
|
||||
if (nextSlot !== null) {
|
||||
if (ejectorComp.tryEject(nextSlot, storageComp.storedItem)) {
|
||||
storageComp.storedCount--;
|
||||
|
||||
if (storageComp.storedCount === 0) {
|
||||
storageComp.storedItem = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let targetAlpha = storageComp.storedCount > 0 ? 1 : 0;
|
||||
storageComp.overlayOpacity = lerp(storageComp.overlayOpacity, targetAlpha, 0.05);
|
||||
|
||||
pinsComp.slots[0].value = storageComp.storedItem;
|
||||
pinsComp.slots[1].value = storageComp.getIsFull() ? BOOL_TRUE_SINGLETON : BOOL_FALSE_SINGLETON;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {DrawParameters} parameters
|
||||
* @param {MapChunkView} chunk
|
||||
*/
|
||||
drawChunk(parameters, chunk) {
|
||||
const contents = chunk.containedEntitiesByLayer.regular;
|
||||
for (let i = 0; i < contents.length; ++i) {
|
||||
const entity = contents[i];
|
||||
const storageComp = entity.components.Storage;
|
||||
if (!storageComp) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const storedItem = storageComp.storedItem;
|
||||
if (!storedItem) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.drawnUids.has(entity.uid)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.drawnUids.add(entity.uid);
|
||||
|
||||
const staticComp = entity.components.StaticMapEntity;
|
||||
|
||||
const context = parameters.context;
|
||||
context.globalAlpha = storageComp.overlayOpacity;
|
||||
const center = staticComp.getTileSpaceBounds().getCenter().toWorldSpace();
|
||||
storedItem.drawItemCenteredClipped(center.x, center.y, parameters, 30);
|
||||
|
||||
this.storageOverlaySprite.drawCached(parameters, center.x - 15, center.y + 15, 30, 15);
|
||||
|
||||
if (parameters.visibleRect.containsCircle(center.x, center.y + 25, 20)) {
|
||||
context.font = "bold 10px GameFont";
|
||||
context.textAlign = "center";
|
||||
context.fillStyle = "#64666e";
|
||||
context.fillText(formatBigNumber(storageComp.storedCount), center.x, center.y + 25.5);
|
||||
context.textAlign = "left";
|
||||
}
|
||||
context.globalAlpha = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,406 +1,369 @@
|
||||
import { globalConfig } from "../../core/config";
|
||||
import { Loader } from "../../core/loader";
|
||||
import { createLogger } from "../../core/logging";
|
||||
import { Rectangle } from "../../core/rectangle";
|
||||
import {
|
||||
enumAngleToDirection,
|
||||
enumDirection,
|
||||
enumDirectionToAngle,
|
||||
enumDirectionToVector,
|
||||
enumInvertedDirections,
|
||||
} from "../../core/vector";
|
||||
import { enumUndergroundBeltMode, UndergroundBeltComponent } from "../components/underground_belt";
|
||||
import { Entity } from "../entity";
|
||||
import { GameSystemWithFilter } from "../game_system_with_filter";
|
||||
import { fastArrayDelete } from "../../core/utils";
|
||||
|
||||
const logger = createLogger("tunnels");
|
||||
|
||||
export class UndergroundBeltSystem extends GameSystemWithFilter {
|
||||
constructor(root) {
|
||||
super(root, [UndergroundBeltComponent]);
|
||||
|
||||
this.beltSprites = {
|
||||
[enumUndergroundBeltMode.sender]: Loader.getSprite(
|
||||
"sprites/buildings/underground_belt_entry.png"
|
||||
),
|
||||
[enumUndergroundBeltMode.receiver]: Loader.getSprite(
|
||||
"sprites/buildings/underground_belt_exit.png"
|
||||
),
|
||||
};
|
||||
|
||||
this.root.signals.entityManuallyPlaced.add(this.onEntityManuallyPlaced, this);
|
||||
|
||||
/**
|
||||
* @type {Rectangle}
|
||||
*/
|
||||
this.areaToRecompute = null;
|
||||
|
||||
this.root.signals.entityAdded.add(this.onEntityChanged, this);
|
||||
this.root.signals.entityDestroyed.add(this.onEntityChanged, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when an entity got added or removed
|
||||
* @param {Entity} entity
|
||||
*/
|
||||
onEntityChanged(entity) {
|
||||
if (!this.root.gameInitialized) {
|
||||
return;
|
||||
}
|
||||
const undergroundComp = entity.components.UndergroundBelt;
|
||||
if (!undergroundComp) {
|
||||
return;
|
||||
}
|
||||
|
||||
const affectedArea = entity.components.StaticMapEntity.getTileSpaceBounds().expandedInAllDirections(
|
||||
globalConfig.undergroundBeltMaxTilesByTier[
|
||||
globalConfig.undergroundBeltMaxTilesByTier.length - 1
|
||||
] + 1
|
||||
);
|
||||
|
||||
if (this.areaToRecompute) {
|
||||
this.areaToRecompute = this.areaToRecompute.getUnion(affectedArea);
|
||||
} else {
|
||||
this.areaToRecompute = affectedArea;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback when an entity got placed, used to remove belts between underground belts
|
||||
* @param {Entity} entity
|
||||
*/
|
||||
onEntityManuallyPlaced(entity) {
|
||||
if (!this.root.app.settings.getAllSettings().enableTunnelSmartplace) {
|
||||
// Smart-place disabled
|
||||
return;
|
||||
}
|
||||
|
||||
const undergroundComp = entity.components.UndergroundBelt;
|
||||
if (undergroundComp && undergroundComp.mode === enumUndergroundBeltMode.receiver) {
|
||||
const staticComp = entity.components.StaticMapEntity;
|
||||
const tile = staticComp.origin;
|
||||
|
||||
const direction = enumAngleToDirection[staticComp.rotation];
|
||||
const inverseDirection = enumInvertedDirections[direction];
|
||||
const offset = enumDirectionToVector[inverseDirection];
|
||||
|
||||
let currentPos = tile.copy();
|
||||
|
||||
const tier = undergroundComp.tier;
|
||||
const range = globalConfig.undergroundBeltMaxTilesByTier[tier];
|
||||
|
||||
// FIND ENTRANCE
|
||||
// Search for the entrance which is furthes apart (this is why we can't reuse logic here)
|
||||
let matchingEntrance = null;
|
||||
for (let i = 0; i < range; ++i) {
|
||||
currentPos.addInplace(offset);
|
||||
const contents = this.root.map.getTileContent(currentPos, entity.layer);
|
||||
if (!contents) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const contentsUndergroundComp = contents.components.UndergroundBelt;
|
||||
const contentsStaticComp = contents.components.StaticMapEntity;
|
||||
if (
|
||||
contentsUndergroundComp &&
|
||||
contentsUndergroundComp.tier === undergroundComp.tier &&
|
||||
contentsUndergroundComp.mode === enumUndergroundBeltMode.sender &&
|
||||
enumAngleToDirection[contentsStaticComp.rotation] === direction
|
||||
) {
|
||||
matchingEntrance = {
|
||||
entity: contents,
|
||||
range: i,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (!matchingEntrance) {
|
||||
// Nothing found
|
||||
return;
|
||||
}
|
||||
|
||||
// DETECT OBSOLETE BELTS BETWEEN
|
||||
// Remove any belts between entrance and exit which have the same direction,
|
||||
// but only if they *all* have the right direction
|
||||
currentPos = tile.copy();
|
||||
let allBeltsMatch = true;
|
||||
for (let i = 0; i < matchingEntrance.range; ++i) {
|
||||
currentPos.addInplace(offset);
|
||||
|
||||
const contents = this.root.map.getTileContent(currentPos, entity.layer);
|
||||
if (!contents) {
|
||||
allBeltsMatch = false;
|
||||
break;
|
||||
}
|
||||
|
||||
const contentsStaticComp = contents.components.StaticMapEntity;
|
||||
const contentsBeltComp = contents.components.Belt;
|
||||
if (!contentsBeltComp) {
|
||||
allBeltsMatch = false;
|
||||
break;
|
||||
}
|
||||
|
||||
// It's a belt
|
||||
if (
|
||||
contentsBeltComp.direction !== enumDirection.top ||
|
||||
enumAngleToDirection[contentsStaticComp.rotation] !== direction
|
||||
) {
|
||||
allBeltsMatch = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
currentPos = tile.copy();
|
||||
if (allBeltsMatch) {
|
||||
// All belts between this are obsolete, so drop them
|
||||
for (let i = 0; i < matchingEntrance.range; ++i) {
|
||||
currentPos.addInplace(offset);
|
||||
const contents = this.root.map.getTileContent(currentPos, entity.layer);
|
||||
assert(contents, "Invalid smart underground belt logic");
|
||||
this.root.logic.tryDeleteBuilding(contents);
|
||||
}
|
||||
}
|
||||
|
||||
// REMOVE OBSOLETE TUNNELS
|
||||
// Remove any double tunnels, by checking the tile plus the tile above
|
||||
currentPos = tile.copy().add(offset);
|
||||
for (let i = 0; i < matchingEntrance.range - 1; ++i) {
|
||||
const posBefore = currentPos.copy();
|
||||
currentPos.addInplace(offset);
|
||||
|
||||
const entityBefore = this.root.map.getTileContent(posBefore, entity.layer);
|
||||
const entityAfter = this.root.map.getTileContent(currentPos, entity.layer);
|
||||
|
||||
if (!entityBefore || !entityAfter) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const undergroundBefore = entityBefore.components.UndergroundBelt;
|
||||
const undergroundAfter = entityAfter.components.UndergroundBelt;
|
||||
|
||||
if (!undergroundBefore || !undergroundAfter) {
|
||||
// Not an underground belt
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
// Both same tier
|
||||
undergroundBefore.tier !== undergroundAfter.tier ||
|
||||
// And same tier as our original entity
|
||||
undergroundBefore.tier !== undergroundComp.tier
|
||||
) {
|
||||
// Mismatching tier
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
undergroundBefore.mode !== enumUndergroundBeltMode.sender ||
|
||||
undergroundAfter.mode !== enumUndergroundBeltMode.receiver
|
||||
) {
|
||||
// Not the right mode
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check rotations
|
||||
const staticBefore = entityBefore.components.StaticMapEntity;
|
||||
const staticAfter = entityAfter.components.StaticMapEntity;
|
||||
|
||||
if (
|
||||
enumAngleToDirection[staticBefore.rotation] !== direction ||
|
||||
enumAngleToDirection[staticAfter.rotation] !== direction
|
||||
) {
|
||||
// Wrong rotation
|
||||
continue;
|
||||
}
|
||||
|
||||
// All good, can remove
|
||||
this.root.logic.tryDeleteBuilding(entityBefore);
|
||||
this.root.logic.tryDeleteBuilding(entityAfter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recomputes the cache in the given area, invalidating all entries there
|
||||
*/
|
||||
recomputeArea() {
|
||||
const area = this.areaToRecompute;
|
||||
logger.log("Recomputing area:", area.x, area.y, "/", area.w, area.h);
|
||||
if (G_IS_DEV && globalConfig.debug.renderChanges) {
|
||||
this.root.hud.parts.changesDebugger.renderChange("tunnels", this.areaToRecompute, "#fc03be");
|
||||
}
|
||||
|
||||
for (let x = area.x; x < area.right(); ++x) {
|
||||
for (let y = area.y; y < area.bottom(); ++y) {
|
||||
const entities = this.root.map.getLayersContentsMultipleXY(x, y);
|
||||
for (let i = 0; i < entities.length; ++i) {
|
||||
const entity = entities[i];
|
||||
const undergroundComp = entity.components.UndergroundBelt;
|
||||
if (!undergroundComp) {
|
||||
continue;
|
||||
}
|
||||
|
||||
undergroundComp.cachedLinkedEntity = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
update() {
|
||||
if (this.areaToRecompute) {
|
||||
this.recomputeArea();
|
||||
this.areaToRecompute = null;
|
||||
}
|
||||
|
||||
const delta = this.root.dynamicTickrate.deltaSeconds;
|
||||
|
||||
for (let i = 0; i < this.allEntities.length; ++i) {
|
||||
const entity = this.allEntities[i];
|
||||
const undergroundComp = entity.components.UndergroundBelt;
|
||||
const pendingItems = undergroundComp.pendingItems;
|
||||
|
||||
// Decrease remaining time of all items in belt
|
||||
for (let k = 0; k < pendingItems.length; ++k) {
|
||||
const item = pendingItems[k];
|
||||
item[1] = Math.max(0, item[1] - delta);
|
||||
if (G_IS_DEV && globalConfig.debug.instantBelts) {
|
||||
item[1] = 0;
|
||||
}
|
||||
}
|
||||
if (undergroundComp.mode === enumUndergroundBeltMode.sender) {
|
||||
this.handleSender(entity);
|
||||
} else {
|
||||
this.handleReceiver(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the receiver for a given sender
|
||||
* @param {Entity} entity
|
||||
* @returns {import("../components/underground_belt").LinkedUndergroundBelt}
|
||||
*/
|
||||
findRecieverForSender(entity) {
|
||||
const staticComp = entity.components.StaticMapEntity;
|
||||
const undergroundComp = entity.components.UndergroundBelt;
|
||||
const searchDirection = staticComp.localDirectionToWorld(enumDirection.top);
|
||||
const searchVector = enumDirectionToVector[searchDirection];
|
||||
const targetRotation = enumDirectionToAngle[searchDirection];
|
||||
let currentTile = staticComp.origin;
|
||||
|
||||
// Search in the direction of the tunnel
|
||||
for (
|
||||
let searchOffset = 0;
|
||||
searchOffset < globalConfig.undergroundBeltMaxTilesByTier[undergroundComp.tier];
|
||||
++searchOffset
|
||||
) {
|
||||
currentTile = currentTile.add(searchVector);
|
||||
|
||||
const potentialReceiver = this.root.map.getTileContent(currentTile, "regular");
|
||||
if (!potentialReceiver) {
|
||||
// Empty tile
|
||||
continue;
|
||||
}
|
||||
const receiverUndergroundComp = potentialReceiver.components.UndergroundBelt;
|
||||
if (!receiverUndergroundComp || receiverUndergroundComp.tier !== undergroundComp.tier) {
|
||||
// Not a tunnel, or not on the same tier
|
||||
continue;
|
||||
}
|
||||
|
||||
const receiverStaticComp = potentialReceiver.components.StaticMapEntity;
|
||||
if (receiverStaticComp.rotation !== targetRotation) {
|
||||
// Wrong rotation
|
||||
continue;
|
||||
}
|
||||
|
||||
if (receiverUndergroundComp.mode !== enumUndergroundBeltMode.receiver) {
|
||||
// Not a receiver, but a sender -> Abort to make sure we don't deliver double
|
||||
break;
|
||||
}
|
||||
|
||||
return { entity: potentialReceiver, distance: searchOffset };
|
||||
}
|
||||
|
||||
// None found
|
||||
return { entity: null, distance: 0 };
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Entity} entity
|
||||
*/
|
||||
handleSender(entity) {
|
||||
const undergroundComp = entity.components.UndergroundBelt;
|
||||
|
||||
// Find the current receiver
|
||||
let receiver = undergroundComp.cachedLinkedEntity;
|
||||
if (!receiver) {
|
||||
// We don't have a receiver, compute it
|
||||
receiver = undergroundComp.cachedLinkedEntity = this.findRecieverForSender(entity);
|
||||
|
||||
if (G_IS_DEV && globalConfig.debug.renderChanges) {
|
||||
this.root.hud.parts.changesDebugger.renderChange(
|
||||
"sender",
|
||||
entity.components.StaticMapEntity.getTileSpaceBounds(),
|
||||
"#fc03be"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!receiver.entity) {
|
||||
// If there is no connection to a receiver, ignore this one
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we have any item
|
||||
if (undergroundComp.pendingItems.length > 0) {
|
||||
assert(undergroundComp.pendingItems.length === 1, "more than 1 pending");
|
||||
const nextItemAndDuration = undergroundComp.pendingItems[0];
|
||||
const remainingTime = nextItemAndDuration[1];
|
||||
const nextItem = nextItemAndDuration[0];
|
||||
|
||||
// Check if the item is ready to be emitted
|
||||
if (remainingTime === 0) {
|
||||
// Check if the receiver can accept it
|
||||
if (
|
||||
receiver.entity.components.UndergroundBelt.tryAcceptTunneledItem(
|
||||
nextItem,
|
||||
receiver.distance,
|
||||
this.root.hubGoals.getUndergroundBeltBaseSpeed()
|
||||
)
|
||||
) {
|
||||
// Drop this item
|
||||
fastArrayDelete(undergroundComp.pendingItems, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Entity} entity
|
||||
*/
|
||||
handleReceiver(entity) {
|
||||
const undergroundComp = entity.components.UndergroundBelt;
|
||||
|
||||
// Try to eject items, we only check the first one because it is sorted by remaining time
|
||||
const items = undergroundComp.pendingItems;
|
||||
if (items.length > 0) {
|
||||
const nextItemAndDuration = undergroundComp.pendingItems[0];
|
||||
const remainingTime = nextItemAndDuration[1];
|
||||
const nextItem = nextItemAndDuration[0];
|
||||
|
||||
if (remainingTime <= 0) {
|
||||
const ejectorComp = entity.components.ItemEjector;
|
||||
|
||||
const nextSlotIndex = ejectorComp.getFirstFreeSlot();
|
||||
if (nextSlotIndex !== null) {
|
||||
if (ejectorComp.tryEject(nextSlotIndex, nextItem)) {
|
||||
items.shift();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
import { globalConfig } from "../../core/config";
|
||||
import { Loader } from "../../core/loader";
|
||||
import { createLogger } from "../../core/logging";
|
||||
import { Rectangle } from "../../core/rectangle";
|
||||
import { StaleAreaDetector } from "../../core/stale_area_detector";
|
||||
import { fastArrayDelete } from "../../core/utils";
|
||||
import {
|
||||
enumAngleToDirection,
|
||||
enumDirection,
|
||||
enumDirectionToAngle,
|
||||
enumDirectionToVector,
|
||||
enumInvertedDirections,
|
||||
} from "../../core/vector";
|
||||
import { enumUndergroundBeltMode, UndergroundBeltComponent } from "../components/underground_belt";
|
||||
import { Entity } from "../entity";
|
||||
import { GameSystemWithFilter } from "../game_system_with_filter";
|
||||
|
||||
const logger = createLogger("tunnels");
|
||||
|
||||
export class UndergroundBeltSystem extends GameSystemWithFilter {
|
||||
constructor(root) {
|
||||
super(root, [UndergroundBeltComponent]);
|
||||
|
||||
this.beltSprites = {
|
||||
[enumUndergroundBeltMode.sender]: Loader.getSprite(
|
||||
"sprites/buildings/underground_belt_entry.png"
|
||||
),
|
||||
[enumUndergroundBeltMode.receiver]: Loader.getSprite(
|
||||
"sprites/buildings/underground_belt_exit.png"
|
||||
),
|
||||
};
|
||||
|
||||
this.staleAreaWatcher = new StaleAreaDetector({
|
||||
root: this.root,
|
||||
name: "underground-belt",
|
||||
recomputeMethod: this.recomputeArea.bind(this),
|
||||
});
|
||||
|
||||
this.root.signals.entityManuallyPlaced.add(this.onEntityManuallyPlaced, this);
|
||||
|
||||
// NOTICE: Once we remove a tunnel, we need to update the whole area to
|
||||
// clear outdated handles
|
||||
this.staleAreaWatcher.recomputeOnComponentsChanged(
|
||||
[UndergroundBeltComponent],
|
||||
globalConfig.undergroundBeltMaxTilesByTier[globalConfig.undergroundBeltMaxTilesByTier.length - 1]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback when an entity got placed, used to remove belts between underground belts
|
||||
* @param {Entity} entity
|
||||
*/
|
||||
onEntityManuallyPlaced(entity) {
|
||||
if (!this.root.app.settings.getAllSettings().enableTunnelSmartplace) {
|
||||
// Smart-place disabled
|
||||
return;
|
||||
}
|
||||
|
||||
const undergroundComp = entity.components.UndergroundBelt;
|
||||
if (undergroundComp && undergroundComp.mode === enumUndergroundBeltMode.receiver) {
|
||||
const staticComp = entity.components.StaticMapEntity;
|
||||
const tile = staticComp.origin;
|
||||
|
||||
const direction = enumAngleToDirection[staticComp.rotation];
|
||||
const inverseDirection = enumInvertedDirections[direction];
|
||||
const offset = enumDirectionToVector[inverseDirection];
|
||||
|
||||
let currentPos = tile.copy();
|
||||
|
||||
const tier = undergroundComp.tier;
|
||||
const range = globalConfig.undergroundBeltMaxTilesByTier[tier];
|
||||
|
||||
// FIND ENTRANCE
|
||||
// Search for the entrance which is farthest apart (this is why we can't reuse logic here)
|
||||
let matchingEntrance = null;
|
||||
for (let i = 0; i < range; ++i) {
|
||||
currentPos.addInplace(offset);
|
||||
const contents = this.root.map.getTileContent(currentPos, entity.layer);
|
||||
if (!contents) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const contentsUndergroundComp = contents.components.UndergroundBelt;
|
||||
const contentsStaticComp = contents.components.StaticMapEntity;
|
||||
if (
|
||||
contentsUndergroundComp &&
|
||||
contentsUndergroundComp.tier === undergroundComp.tier &&
|
||||
contentsUndergroundComp.mode === enumUndergroundBeltMode.sender &&
|
||||
enumAngleToDirection[contentsStaticComp.rotation] === direction
|
||||
) {
|
||||
matchingEntrance = {
|
||||
entity: contents,
|
||||
range: i,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (!matchingEntrance) {
|
||||
// Nothing found
|
||||
return;
|
||||
}
|
||||
|
||||
// DETECT OBSOLETE BELTS BETWEEN
|
||||
// Remove any belts between entrance and exit which have the same direction,
|
||||
// but only if they *all* have the right direction
|
||||
currentPos = tile.copy();
|
||||
let allBeltsMatch = true;
|
||||
for (let i = 0; i < matchingEntrance.range; ++i) {
|
||||
currentPos.addInplace(offset);
|
||||
|
||||
const contents = this.root.map.getTileContent(currentPos, entity.layer);
|
||||
if (!contents) {
|
||||
allBeltsMatch = false;
|
||||
break;
|
||||
}
|
||||
|
||||
const contentsStaticComp = contents.components.StaticMapEntity;
|
||||
const contentsBeltComp = contents.components.Belt;
|
||||
if (!contentsBeltComp) {
|
||||
allBeltsMatch = false;
|
||||
break;
|
||||
}
|
||||
|
||||
// It's a belt
|
||||
if (
|
||||
contentsBeltComp.direction !== enumDirection.top ||
|
||||
enumAngleToDirection[contentsStaticComp.rotation] !== direction
|
||||
) {
|
||||
allBeltsMatch = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
currentPos = tile.copy();
|
||||
if (allBeltsMatch) {
|
||||
// All belts between this are obsolete, so drop them
|
||||
for (let i = 0; i < matchingEntrance.range; ++i) {
|
||||
currentPos.addInplace(offset);
|
||||
const contents = this.root.map.getTileContent(currentPos, entity.layer);
|
||||
assert(contents, "Invalid smart underground belt logic");
|
||||
this.root.logic.tryDeleteBuilding(contents);
|
||||
}
|
||||
}
|
||||
|
||||
// REMOVE OBSOLETE TUNNELS
|
||||
// Remove any double tunnels, by checking the tile plus the tile above
|
||||
currentPos = tile.copy().add(offset);
|
||||
for (let i = 0; i < matchingEntrance.range - 1; ++i) {
|
||||
const posBefore = currentPos.copy();
|
||||
currentPos.addInplace(offset);
|
||||
|
||||
const entityBefore = this.root.map.getTileContent(posBefore, entity.layer);
|
||||
const entityAfter = this.root.map.getTileContent(currentPos, entity.layer);
|
||||
|
||||
if (!entityBefore || !entityAfter) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const undergroundBefore = entityBefore.components.UndergroundBelt;
|
||||
const undergroundAfter = entityAfter.components.UndergroundBelt;
|
||||
|
||||
if (!undergroundBefore || !undergroundAfter) {
|
||||
// Not an underground belt
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
// Both same tier
|
||||
undergroundBefore.tier !== undergroundAfter.tier ||
|
||||
// And same tier as our original entity
|
||||
undergroundBefore.tier !== undergroundComp.tier
|
||||
) {
|
||||
// Mismatching tier
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
undergroundBefore.mode !== enumUndergroundBeltMode.sender ||
|
||||
undergroundAfter.mode !== enumUndergroundBeltMode.receiver
|
||||
) {
|
||||
// Not the right mode
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check rotations
|
||||
const staticBefore = entityBefore.components.StaticMapEntity;
|
||||
const staticAfter = entityAfter.components.StaticMapEntity;
|
||||
|
||||
if (
|
||||
enumAngleToDirection[staticBefore.rotation] !== direction ||
|
||||
enumAngleToDirection[staticAfter.rotation] !== direction
|
||||
) {
|
||||
// Wrong rotation
|
||||
continue;
|
||||
}
|
||||
|
||||
// All good, can remove
|
||||
this.root.logic.tryDeleteBuilding(entityBefore);
|
||||
this.root.logic.tryDeleteBuilding(entityAfter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recomputes the cache in the given area, invalidating all entries there
|
||||
* @param {Rectangle} area
|
||||
*/
|
||||
recomputeArea(area) {
|
||||
for (let x = area.x; x < area.right(); ++x) {
|
||||
for (let y = area.y; y < area.bottom(); ++y) {
|
||||
const entities = this.root.map.getLayersContentsMultipleXY(x, y);
|
||||
for (let i = 0; i < entities.length; ++i) {
|
||||
const entity = entities[i];
|
||||
const undergroundComp = entity.components.UndergroundBelt;
|
||||
if (!undergroundComp) {
|
||||
continue;
|
||||
}
|
||||
undergroundComp.cachedLinkedEntity = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
update() {
|
||||
this.staleAreaWatcher.update();
|
||||
|
||||
const delta = this.root.dynamicTickrate.deltaSeconds;
|
||||
|
||||
for (let i = 0; i < this.allEntities.length; ++i) {
|
||||
const entity = this.allEntities[i];
|
||||
const undergroundComp = entity.components.UndergroundBelt;
|
||||
const pendingItems = undergroundComp.pendingItems;
|
||||
|
||||
// Decrease remaining time of all items in belt
|
||||
for (let k = 0; k < pendingItems.length; ++k) {
|
||||
const item = pendingItems[k];
|
||||
item[1] = Math.max(0, item[1] - delta);
|
||||
if (G_IS_DEV && globalConfig.debug.instantBelts) {
|
||||
item[1] = 0;
|
||||
}
|
||||
}
|
||||
if (undergroundComp.mode === enumUndergroundBeltMode.sender) {
|
||||
this.handleSender(entity);
|
||||
} else {
|
||||
this.handleReceiver(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the receiver for a given sender
|
||||
* @param {Entity} entity
|
||||
* @returns {import("../components/underground_belt").LinkedUndergroundBelt}
|
||||
*/
|
||||
findRecieverForSender(entity) {
|
||||
const staticComp = entity.components.StaticMapEntity;
|
||||
const undergroundComp = entity.components.UndergroundBelt;
|
||||
const searchDirection = staticComp.localDirectionToWorld(enumDirection.top);
|
||||
const searchVector = enumDirectionToVector[searchDirection];
|
||||
const targetRotation = enumDirectionToAngle[searchDirection];
|
||||
let currentTile = staticComp.origin;
|
||||
|
||||
// Search in the direction of the tunnel
|
||||
for (
|
||||
let searchOffset = 0;
|
||||
searchOffset < globalConfig.undergroundBeltMaxTilesByTier[undergroundComp.tier];
|
||||
++searchOffset
|
||||
) {
|
||||
currentTile = currentTile.add(searchVector);
|
||||
|
||||
const potentialReceiver = this.root.map.getTileContent(currentTile, "regular");
|
||||
if (!potentialReceiver) {
|
||||
// Empty tile
|
||||
continue;
|
||||
}
|
||||
const receiverUndergroundComp = potentialReceiver.components.UndergroundBelt;
|
||||
if (!receiverUndergroundComp || receiverUndergroundComp.tier !== undergroundComp.tier) {
|
||||
// Not a tunnel, or not on the same tier
|
||||
continue;
|
||||
}
|
||||
|
||||
const receiverStaticComp = potentialReceiver.components.StaticMapEntity;
|
||||
if (receiverStaticComp.rotation !== targetRotation) {
|
||||
// Wrong rotation
|
||||
continue;
|
||||
}
|
||||
|
||||
if (receiverUndergroundComp.mode !== enumUndergroundBeltMode.receiver) {
|
||||
// Not a receiver, but a sender -> Abort to make sure we don't deliver double
|
||||
break;
|
||||
}
|
||||
|
||||
return { entity: potentialReceiver, distance: searchOffset };
|
||||
}
|
||||
|
||||
// None found
|
||||
return { entity: null, distance: 0 };
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Entity} entity
|
||||
*/
|
||||
handleSender(entity) {
|
||||
const undergroundComp = entity.components.UndergroundBelt;
|
||||
|
||||
// Find the current receiver
|
||||
let cacheEntry = undergroundComp.cachedLinkedEntity;
|
||||
if (!cacheEntry) {
|
||||
// Need to recompute cache
|
||||
cacheEntry = undergroundComp.cachedLinkedEntity = this.findRecieverForSender(entity);
|
||||
}
|
||||
|
||||
if (!cacheEntry.entity) {
|
||||
// If there is no connection to a receiver, ignore this one
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we have any item
|
||||
if (undergroundComp.pendingItems.length > 0) {
|
||||
assert(undergroundComp.pendingItems.length === 1, "more than 1 pending");
|
||||
const nextItemAndDuration = undergroundComp.pendingItems[0];
|
||||
const remainingTime = nextItemAndDuration[1];
|
||||
const nextItem = nextItemAndDuration[0];
|
||||
|
||||
// Check if the item is ready to be emitted
|
||||
if (remainingTime === 0) {
|
||||
// Check if the receiver can accept it
|
||||
if (
|
||||
cacheEntry.entity.components.UndergroundBelt.tryAcceptTunneledItem(
|
||||
nextItem,
|
||||
cacheEntry.distance,
|
||||
this.root.hubGoals.getUndergroundBeltBaseSpeed()
|
||||
)
|
||||
) {
|
||||
// Drop this item
|
||||
fastArrayDelete(undergroundComp.pendingItems, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Entity} entity
|
||||
*/
|
||||
handleReceiver(entity) {
|
||||
const undergroundComp = entity.components.UndergroundBelt;
|
||||
|
||||
// Try to eject items, we only check the first one because it is sorted by remaining time
|
||||
const items = undergroundComp.pendingItems;
|
||||
if (items.length > 0) {
|
||||
const nextItemAndDuration = undergroundComp.pendingItems[0];
|
||||
const remainingTime = nextItemAndDuration[1];
|
||||
const nextItem = nextItemAndDuration[0];
|
||||
|
||||
if (remainingTime <= 0) {
|
||||
const ejectorComp = entity.components.ItemEjector;
|
||||
|
||||
const nextSlotIndex = ejectorComp.getFirstFreeSlot();
|
||||
if (nextSlotIndex !== null) {
|
||||
if (ejectorComp.tryEject(nextSlotIndex, nextItem)) {
|
||||
items.shift();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -162,7 +162,7 @@ export class WireSystem extends GameSystemWithFilter {
|
||||
const tunnelEntities = this.root.entityMgr.getAllWithComponent(WireTunnelComponent);
|
||||
const pinEntities = this.root.entityMgr.getAllWithComponent(WiredPinsComponent);
|
||||
|
||||
// Clear all network references, but not on the first update since thats the deserializing one
|
||||
// Clear all network references, but not on the first update since that's the deserializing one
|
||||
if (!this.isFirstRecompute) {
|
||||
for (let i = 0; i < wireEntities.length; ++i) {
|
||||
wireEntities[i].components.Wire.linkedNetwork = null;
|
||||
@ -432,7 +432,7 @@ export class WireSystem extends GameSystemWithFilter {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if its a tunnel, if so, go to the forwarded item
|
||||
// Check if it's a tunnel, if so, go to the forwarded item
|
||||
const tunnelComp = entity.components.WireTunnel;
|
||||
if (tunnelComp) {
|
||||
if (visitedTunnels.has(entity.uid)) {
|
||||
|
||||
@ -149,8 +149,6 @@ export class WiredPinsSystem extends GameSystemWithFilter {
|
||||
}
|
||||
}
|
||||
|
||||
update() {}
|
||||
|
||||
/**
|
||||
* Draws a given entity
|
||||
* @param {DrawParameters} parameters
|
||||
|
||||
@ -276,6 +276,7 @@ export const allApplicationSettings = [
|
||||
new BoolSetting("lowQualityMapResources", enumCategories.performance, (app, value) => {}),
|
||||
new BoolSetting("disableTileGrid", enumCategories.performance, (app, value) => {}),
|
||||
new BoolSetting("lowQualityTextures", enumCategories.performance, (app, value) => {}),
|
||||
new BoolSetting("simplifiedBelts", enumCategories.performance, (app, value) => {}),
|
||||
];
|
||||
|
||||
export function getApplicationSettingById(id) {
|
||||
@ -313,6 +314,7 @@ class SettingsStorage {
|
||||
this.lowQualityMapResources = false;
|
||||
this.disableTileGrid = false;
|
||||
this.lowQualityTextures = false;
|
||||
this.simplifiedBelts = false;
|
||||
|
||||
/**
|
||||
* @type {Object.<string, number>}
|
||||
@ -523,7 +525,7 @@ export class ApplicationSettings extends ReadWriteProxy {
|
||||
}
|
||||
|
||||
getCurrentVersion() {
|
||||
return 26;
|
||||
return 27;
|
||||
}
|
||||
|
||||
/** @param {{settings: SettingsStorage, version: number}} data */
|
||||
@ -646,6 +648,11 @@ export class ApplicationSettings extends ReadWriteProxy {
|
||||
data.version = 26;
|
||||
}
|
||||
|
||||
if (data.version < 27) {
|
||||
data.settings.simplifiedBelts = false;
|
||||
data.version = 27;
|
||||
}
|
||||
|
||||
return ExplainedResult.good();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,146 +1,146 @@
|
||||
import { ExplainedResult } from "../core/explained_result";
|
||||
import { createLogger } from "../core/logging";
|
||||
import { gComponentRegistry } from "../core/global_registries";
|
||||
import { SerializerInternal } from "./serializer_internal";
|
||||
|
||||
/**
|
||||
* @typedef {import("../game/component").Component} Component
|
||||
* @typedef {import("../game/component").StaticComponent} StaticComponent
|
||||
* @typedef {import("../game/entity").Entity} Entity
|
||||
* @typedef {import("../game/root").GameRoot} GameRoot
|
||||
* @typedef {import("../savegame/savegame_typedefs").SerializedGame} SerializedGame
|
||||
*/
|
||||
|
||||
const logger = createLogger("savegame_serializer");
|
||||
|
||||
/**
|
||||
* Serializes a savegame
|
||||
*/
|
||||
export class SavegameSerializer {
|
||||
constructor() {
|
||||
this.internal = new SerializerInternal();
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes the game root into a dump
|
||||
* @param {GameRoot} root
|
||||
* @param {boolean=} sanityChecks Whether to check for validity
|
||||
* @returns {object}
|
||||
*/
|
||||
generateDumpFromGameRoot(root, sanityChecks = true) {
|
||||
/** @type {SerializedGame} */
|
||||
const data = {
|
||||
camera: root.camera.serialize(),
|
||||
time: root.time.serialize(),
|
||||
map: root.map.serialize(),
|
||||
entityMgr: root.entityMgr.serialize(),
|
||||
hubGoals: root.hubGoals.serialize(),
|
||||
pinnedShapes: root.hud.parts.pinnedShapes.serialize(),
|
||||
waypoints: root.hud.parts.waypoints.serialize(),
|
||||
entities: this.internal.serializeEntityArray(root.entityMgr.entities),
|
||||
beltPaths: root.systemMgr.systems.belt.serializePaths(),
|
||||
};
|
||||
|
||||
if (!G_IS_RELEASE) {
|
||||
if (sanityChecks) {
|
||||
// Sanity check
|
||||
const sanity = this.verifyLogicalErrors(data);
|
||||
if (!sanity.result) {
|
||||
logger.error("Created invalid savegame:", sanity.reason, "savegame:", data);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies if there are logical errors in the savegame
|
||||
* @param {SerializedGame} savegame
|
||||
* @returns {ExplainedResult}
|
||||
*/
|
||||
verifyLogicalErrors(savegame) {
|
||||
if (!savegame.entities) {
|
||||
return ExplainedResult.bad("Savegame has no entities");
|
||||
}
|
||||
|
||||
const seenUids = [];
|
||||
|
||||
// Check for duplicate UIDS
|
||||
for (let i = 0; i < savegame.entities.length; ++i) {
|
||||
/** @type {Entity} */
|
||||
const entity = savegame.entities[i];
|
||||
|
||||
const uid = entity.uid;
|
||||
if (!Number.isInteger(uid)) {
|
||||
return ExplainedResult.bad("Entity has invalid uid: " + uid);
|
||||
}
|
||||
if (seenUids.indexOf(uid) >= 0) {
|
||||
return ExplainedResult.bad("Duplicate uid " + uid);
|
||||
}
|
||||
seenUids.push(uid);
|
||||
|
||||
// Verify components
|
||||
if (!entity.components) {
|
||||
return ExplainedResult.bad("Entity is missing key 'components': " + JSON.stringify(entity));
|
||||
}
|
||||
|
||||
const components = entity.components;
|
||||
for (const componentId in components) {
|
||||
const componentClass = gComponentRegistry.findById(componentId);
|
||||
|
||||
// Check component id is known
|
||||
if (!componentClass) {
|
||||
return ExplainedResult.bad("Unknown component id: " + componentId);
|
||||
}
|
||||
|
||||
// Verify component data
|
||||
const componentData = components[componentId];
|
||||
const componentVerifyError = /** @type {StaticComponent} */ (componentClass).verify(
|
||||
componentData
|
||||
);
|
||||
|
||||
// Check component data is ok
|
||||
if (componentVerifyError) {
|
||||
return ExplainedResult.bad(
|
||||
"Component " + componentId + " has invalid data: " + componentVerifyError
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ExplainedResult.good();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to load the savegame from a given dump
|
||||
* @param {SerializedGame} savegame
|
||||
* @param {GameRoot} root
|
||||
* @returns {ExplainedResult}
|
||||
*/
|
||||
deserialize(savegame, root) {
|
||||
// Sanity
|
||||
const verifyResult = this.verifyLogicalErrors(savegame);
|
||||
if (!verifyResult.result) {
|
||||
return ExplainedResult.bad(verifyResult.reason);
|
||||
}
|
||||
let errorReason = null;
|
||||
|
||||
errorReason = errorReason || root.entityMgr.deserialize(savegame.entityMgr);
|
||||
errorReason = errorReason || root.time.deserialize(savegame.time);
|
||||
errorReason = errorReason || root.camera.deserialize(savegame.camera);
|
||||
errorReason = errorReason || root.map.deserialize(savegame.map);
|
||||
errorReason = errorReason || root.hubGoals.deserialize(savegame.hubGoals);
|
||||
errorReason = errorReason || root.hud.parts.pinnedShapes.deserialize(savegame.pinnedShapes);
|
||||
errorReason = errorReason || root.hud.parts.waypoints.deserialize(savegame.waypoints);
|
||||
errorReason = errorReason || this.internal.deserializeEntityArray(root, savegame.entities);
|
||||
errorReason = errorReason || root.systemMgr.systems.belt.deserializePaths(savegame.beltPaths);
|
||||
|
||||
// Check for errors
|
||||
if (errorReason) {
|
||||
return ExplainedResult.bad(errorReason);
|
||||
}
|
||||
|
||||
return ExplainedResult.good();
|
||||
}
|
||||
}
|
||||
import { ExplainedResult } from "../core/explained_result";
|
||||
import { createLogger } from "../core/logging";
|
||||
import { gComponentRegistry } from "../core/global_registries";
|
||||
import { SerializerInternal } from "./serializer_internal";
|
||||
|
||||
/**
|
||||
* @typedef {import("../game/component").Component} Component
|
||||
* @typedef {import("../game/component").StaticComponent} StaticComponent
|
||||
* @typedef {import("../game/entity").Entity} Entity
|
||||
* @typedef {import("../game/root").GameRoot} GameRoot
|
||||
* @typedef {import("../savegame/savegame_typedefs").SerializedGame} SerializedGame
|
||||
*/
|
||||
|
||||
const logger = createLogger("savegame_serializer");
|
||||
|
||||
/**
|
||||
* Serializes a savegame
|
||||
*/
|
||||
export class SavegameSerializer {
|
||||
constructor() {
|
||||
this.internal = new SerializerInternal();
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes the game root into a dump
|
||||
* @param {GameRoot} root
|
||||
* @param {boolean=} sanityChecks Whether to check for validity
|
||||
* @returns {object}
|
||||
*/
|
||||
generateDumpFromGameRoot(root, sanityChecks = true) {
|
||||
/** @type {SerializedGame} */
|
||||
const data = {
|
||||
camera: root.camera.serialize(),
|
||||
time: root.time.serialize(),
|
||||
map: root.map.serialize(),
|
||||
entityMgr: root.entityMgr.serialize(),
|
||||
hubGoals: root.hubGoals.serialize(),
|
||||
pinnedShapes: root.hud.parts.pinnedShapes.serialize(),
|
||||
waypoints: root.hud.parts.waypoints.serialize(),
|
||||
entities: this.internal.serializeEntityArray(root.entityMgr.entities),
|
||||
beltPaths: root.systemMgr.systems.belt.serializePaths(),
|
||||
};
|
||||
|
||||
if (G_IS_DEV) {
|
||||
if (sanityChecks) {
|
||||
// Sanity check
|
||||
const sanity = this.verifyLogicalErrors(data);
|
||||
if (!sanity.result) {
|
||||
logger.error("Created invalid savegame:", sanity.reason, "savegame:", data);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies if there are logical errors in the savegame
|
||||
* @param {SerializedGame} savegame
|
||||
* @returns {ExplainedResult}
|
||||
*/
|
||||
verifyLogicalErrors(savegame) {
|
||||
if (!savegame.entities) {
|
||||
return ExplainedResult.bad("Savegame has no entities");
|
||||
}
|
||||
|
||||
const seenUids = new Set();
|
||||
|
||||
// Check for duplicate UIDS
|
||||
for (let i = 0; i < savegame.entities.length; ++i) {
|
||||
/** @type {Entity} */
|
||||
const entity = savegame.entities[i];
|
||||
|
||||
const uid = entity.uid;
|
||||
if (!Number.isInteger(uid)) {
|
||||
return ExplainedResult.bad("Entity has invalid uid: " + uid);
|
||||
}
|
||||
if (seenUids.has(uid)) {
|
||||
return ExplainedResult.bad("Duplicate uid " + uid);
|
||||
}
|
||||
seenUids.add(uid);
|
||||
|
||||
// Verify components
|
||||
if (!entity.components) {
|
||||
return ExplainedResult.bad("Entity is missing key 'components': " + JSON.stringify(entity));
|
||||
}
|
||||
|
||||
const components = entity.components;
|
||||
for (const componentId in components) {
|
||||
const componentClass = gComponentRegistry.findById(componentId);
|
||||
|
||||
// Check component id is known
|
||||
if (!componentClass) {
|
||||
return ExplainedResult.bad("Unknown component id: " + componentId);
|
||||
}
|
||||
|
||||
// Verify component data
|
||||
const componentData = components[componentId];
|
||||
const componentVerifyError = /** @type {StaticComponent} */ (componentClass).verify(
|
||||
componentData
|
||||
);
|
||||
|
||||
// Check component data is ok
|
||||
if (componentVerifyError) {
|
||||
return ExplainedResult.bad(
|
||||
"Component " + componentId + " has invalid data: " + componentVerifyError
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ExplainedResult.good();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to load the savegame from a given dump
|
||||
* @param {SerializedGame} savegame
|
||||
* @param {GameRoot} root
|
||||
* @returns {ExplainedResult}
|
||||
*/
|
||||
deserialize(savegame, root) {
|
||||
// Sanity
|
||||
const verifyResult = this.verifyLogicalErrors(savegame);
|
||||
if (!verifyResult.result) {
|
||||
return ExplainedResult.bad(verifyResult.reason);
|
||||
}
|
||||
let errorReason = null;
|
||||
|
||||
errorReason = errorReason || root.entityMgr.deserialize(savegame.entityMgr);
|
||||
errorReason = errorReason || root.time.deserialize(savegame.time);
|
||||
errorReason = errorReason || root.camera.deserialize(savegame.camera);
|
||||
errorReason = errorReason || root.map.deserialize(savegame.map);
|
||||
errorReason = errorReason || root.hubGoals.deserialize(savegame.hubGoals);
|
||||
errorReason = errorReason || root.hud.parts.pinnedShapes.deserialize(savegame.pinnedShapes);
|
||||
errorReason = errorReason || root.hud.parts.waypoints.deserialize(savegame.waypoints);
|
||||
errorReason = errorReason || this.internal.deserializeEntityArray(root, savegame.entities);
|
||||
errorReason = errorReason || root.systemMgr.systems.belt.deserializePaths(savegame.beltPaths);
|
||||
|
||||
// Check for errors
|
||||
if (errorReason) {
|
||||
return ExplainedResult.bad(errorReason);
|
||||
}
|
||||
|
||||
return ExplainedResult.good();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,438 +1,458 @@
|
||||
import { APPLICATION_ERROR_OCCURED } from "../core/error_handler";
|
||||
import { GameState } from "../core/game_state";
|
||||
import { logSection, createLogger } from "../core/logging";
|
||||
import { waitNextFrame } from "../core/utils";
|
||||
import { globalConfig } from "../core/config";
|
||||
import { GameLoadingOverlay } from "../game/game_loading_overlay";
|
||||
import { KeyActionMapper } from "../game/key_action_mapper";
|
||||
import { Savegame } from "../savegame/savegame";
|
||||
import { GameCore } from "../game/core";
|
||||
import { MUSIC } from "../platform/sound";
|
||||
|
||||
const logger = createLogger("state/ingame");
|
||||
|
||||
// Different sub-states
|
||||
const stages = {
|
||||
s3_createCore: "🌈 3: Create core",
|
||||
s4_A_initEmptyGame: "🌈 4/A: Init empty game",
|
||||
s4_B_resumeGame: "🌈 4/B: Resume game",
|
||||
|
||||
s5_firstUpdate: "🌈 5: First game update",
|
||||
s6_postLoadHook: "🌈 6: Post load hook",
|
||||
s7_warmup: "🌈 7: Warmup",
|
||||
|
||||
s10_gameRunning: "🌈 10: Game finally running",
|
||||
|
||||
leaving: "🌈 Saving, then leaving the game",
|
||||
destroyed: "🌈 DESTROYED: Core is empty and waits for state leave",
|
||||
initFailed: "🌈 ERROR: Initialization failed!",
|
||||
};
|
||||
|
||||
export const gameCreationAction = {
|
||||
new: "new-game",
|
||||
resume: "resume-game",
|
||||
};
|
||||
|
||||
// Typehints
|
||||
export class GameCreationPayload {
|
||||
constructor() {
|
||||
/** @type {boolean|undefined} */
|
||||
this.fastEnter;
|
||||
|
||||
/** @type {Savegame} */
|
||||
this.savegame;
|
||||
}
|
||||
}
|
||||
|
||||
export class InGameState extends GameState {
|
||||
constructor() {
|
||||
super("InGameState");
|
||||
|
||||
/** @type {GameCreationPayload} */
|
||||
this.creationPayload = null;
|
||||
|
||||
// Stores current stage
|
||||
this.stage = "";
|
||||
|
||||
/** @type {GameCore} */
|
||||
this.core = null;
|
||||
|
||||
/** @type {KeyActionMapper} */
|
||||
this.keyActionMapper = null;
|
||||
|
||||
/** @type {GameLoadingOverlay} */
|
||||
this.loadingOverlay = null;
|
||||
|
||||
/** @type {Savegame} */
|
||||
this.savegame;
|
||||
|
||||
this.boundInputFilter = this.filterInput.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Switches the game into another sub-state
|
||||
* @param {string} stage
|
||||
*/
|
||||
switchStage(stage) {
|
||||
assert(stage, "Got empty stage");
|
||||
if (stage !== this.stage) {
|
||||
this.stage = stage;
|
||||
logger.log(this.stage);
|
||||
return true;
|
||||
} else {
|
||||
// log(this, "Re entering", stage);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// GameState implementation
|
||||
getInnerHTML() {
|
||||
return "";
|
||||
}
|
||||
|
||||
getThemeMusic() {
|
||||
return MUSIC.theme;
|
||||
}
|
||||
|
||||
onBeforeExit() {
|
||||
// logger.log("Saving before quitting");
|
||||
// return this.doSave().then(() => {
|
||||
// logger.log(this, "Successfully saved");
|
||||
// // this.stageDestroyed();
|
||||
// });
|
||||
}
|
||||
|
||||
onAppPause() {
|
||||
// if (this.stage === stages.s10_gameRunning) {
|
||||
// logger.log("Saving because app got paused");
|
||||
// this.doSave();
|
||||
// }
|
||||
}
|
||||
|
||||
getHasFadeIn() {
|
||||
return false;
|
||||
}
|
||||
|
||||
getPauseOnFocusLost() {
|
||||
return false;
|
||||
}
|
||||
|
||||
getHasUnloadConfirmation() {
|
||||
return true;
|
||||
}
|
||||
|
||||
onLeave() {
|
||||
if (this.core) {
|
||||
this.stageDestroyed();
|
||||
}
|
||||
this.app.inputMgr.dismountFilter(this.boundInputFilter);
|
||||
}
|
||||
|
||||
onResized(w, h) {
|
||||
super.onResized(w, h);
|
||||
if (this.stage === stages.s10_gameRunning) {
|
||||
this.core.resize(w, h);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- End of GameState implementation
|
||||
|
||||
/**
|
||||
* Goes back to the menu state
|
||||
*/
|
||||
goBackToMenu() {
|
||||
this.saveThenGoToState("MainMenuState");
|
||||
}
|
||||
|
||||
/**
|
||||
* Goes back to the settings state
|
||||
*/
|
||||
goToSettings() {
|
||||
this.saveThenGoToState("SettingsState", {
|
||||
backToStateId: this.key,
|
||||
backToStatePayload: this.creationPayload,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Goes back to the settings state
|
||||
*/
|
||||
goToKeybindings() {
|
||||
this.saveThenGoToState("KeybindingsState", {
|
||||
backToStateId: this.key,
|
||||
backToStatePayload: this.creationPayload,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves to a state outside of the game
|
||||
* @param {string} stateId
|
||||
* @param {any=} payload
|
||||
*/
|
||||
saveThenGoToState(stateId, payload) {
|
||||
if (this.stage === stages.leaving || this.stage === stages.destroyed) {
|
||||
logger.warn(
|
||||
"Tried to leave game twice or during destroy:",
|
||||
this.stage,
|
||||
"(attempted to move to",
|
||||
stateId,
|
||||
")"
|
||||
);
|
||||
return;
|
||||
}
|
||||
this.stageLeavingGame();
|
||||
this.doSave().then(() => {
|
||||
this.stageDestroyed();
|
||||
this.moveToState(stateId, payload);
|
||||
});
|
||||
}
|
||||
|
||||
onBackButton() {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the game somehow failed to initialize. Resets everything to basic state and
|
||||
* then goes to the main menu, showing the error
|
||||
* @param {string} err
|
||||
*/
|
||||
onInitializationFailure(err) {
|
||||
if (this.switchStage(stages.initFailed)) {
|
||||
logger.error("Init failure:", err);
|
||||
this.stageDestroyed();
|
||||
this.moveToState("MainMenuState", { loadError: err });
|
||||
}
|
||||
}
|
||||
|
||||
// STAGES
|
||||
|
||||
/**
|
||||
* Creates the game core instance, and thus the root
|
||||
*/
|
||||
stage3CreateCore() {
|
||||
if (this.switchStage(stages.s3_createCore)) {
|
||||
logger.log("Creating new game core");
|
||||
this.core = new GameCore(this.app);
|
||||
|
||||
this.core.initializeRoot(this, this.savegame);
|
||||
|
||||
if (this.savegame.hasGameDump()) {
|
||||
this.stage4bResumeGame();
|
||||
} else {
|
||||
this.app.gameAnalytics.handleGameStarted();
|
||||
this.stage4aInitEmptyGame();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes a new empty game
|
||||
*/
|
||||
stage4aInitEmptyGame() {
|
||||
if (this.switchStage(stages.s4_A_initEmptyGame)) {
|
||||
this.core.initNewGame();
|
||||
this.stage5FirstUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resumes an existing game
|
||||
*/
|
||||
stage4bResumeGame() {
|
||||
if (this.switchStage(stages.s4_B_resumeGame)) {
|
||||
if (!this.core.initExistingGame()) {
|
||||
this.onInitializationFailure("Savegame is corrupt and can not be restored.");
|
||||
return;
|
||||
}
|
||||
this.app.gameAnalytics.handleGameResumed();
|
||||
this.stage5FirstUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the first game update on the game which initializes most caches
|
||||
*/
|
||||
stage5FirstUpdate() {
|
||||
if (this.switchStage(stages.s5_firstUpdate)) {
|
||||
this.core.root.logicInitialized = true;
|
||||
this.core.updateLogic();
|
||||
this.stage6PostLoadHook();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call the post load hook, this means that we have loaded the game, and all systems
|
||||
* can operate and start to work now.
|
||||
*/
|
||||
stage6PostLoadHook() {
|
||||
if (this.switchStage(stages.s6_postLoadHook)) {
|
||||
logger.log("Post load hook");
|
||||
this.core.postLoadHook();
|
||||
this.stage7Warmup();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This makes the game idle and draw for a while, because we run most code this way
|
||||
* the V8 engine can already start to optimize it. Also this makes sure the resources
|
||||
* are in the VRAM and we have a smooth experience once we start.
|
||||
*/
|
||||
stage7Warmup() {
|
||||
if (this.switchStage(stages.s7_warmup)) {
|
||||
if (G_IS_DEV && globalConfig.debug.noArtificialDelays) {
|
||||
this.warmupTimeSeconds = 0.05;
|
||||
} else {
|
||||
if (this.creationPayload.fastEnter) {
|
||||
this.warmupTimeSeconds = globalConfig.warmupTimeSecondsFast;
|
||||
} else {
|
||||
this.warmupTimeSeconds = globalConfig.warmupTimeSecondsRegular;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The final stage where this game is running and updating regulary.
|
||||
*/
|
||||
stage10GameRunning() {
|
||||
if (this.switchStage(stages.s10_gameRunning)) {
|
||||
this.core.root.signals.readyToRender.dispatch();
|
||||
|
||||
logSection("GAME STARTED", "#26a69a");
|
||||
|
||||
// Initial resize, might have changed during loading (this is possible)
|
||||
this.core.resize(this.app.screenWidth, this.app.screenHeight);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This stage destroys the whole game, used to cleanup
|
||||
*/
|
||||
stageDestroyed() {
|
||||
if (this.switchStage(stages.destroyed)) {
|
||||
// Cleanup all api calls
|
||||
this.cancelAllAsyncOperations();
|
||||
|
||||
if (this.syncer) {
|
||||
this.syncer.cancelSync();
|
||||
this.syncer = null;
|
||||
}
|
||||
|
||||
// Cleanup core
|
||||
if (this.core) {
|
||||
this.core.destruct();
|
||||
this.core = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When leaving the game
|
||||
*/
|
||||
stageLeavingGame() {
|
||||
if (this.switchStage(stages.leaving)) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
// END STAGES
|
||||
|
||||
/**
|
||||
* Filters the input (keybindings)
|
||||
*/
|
||||
filterInput() {
|
||||
return this.stage === stages.s10_gameRunning;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {GameCreationPayload} payload
|
||||
*/
|
||||
onEnter(payload) {
|
||||
this.app.inputMgr.installFilter(this.boundInputFilter);
|
||||
|
||||
this.creationPayload = payload;
|
||||
this.savegame = payload.savegame;
|
||||
|
||||
this.loadingOverlay = new GameLoadingOverlay(this.app, this.getDivElement());
|
||||
this.loadingOverlay.showBasic();
|
||||
|
||||
// Remove unneded default element
|
||||
document.body.querySelector(".modalDialogParent").remove();
|
||||
|
||||
this.asyncChannel.watch(waitNextFrame()).then(() => this.stage3CreateCore());
|
||||
}
|
||||
|
||||
/**
|
||||
* Render callback
|
||||
* @param {number} dt
|
||||
*/
|
||||
onRender(dt) {
|
||||
if (APPLICATION_ERROR_OCCURED) {
|
||||
// Application somehow crashed, do not do anything
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.stage === stages.s7_warmup) {
|
||||
this.core.draw();
|
||||
this.warmupTimeSeconds -= dt / 1000.0;
|
||||
if (this.warmupTimeSeconds < 0) {
|
||||
logger.log("Warmup completed");
|
||||
this.stage10GameRunning();
|
||||
}
|
||||
}
|
||||
|
||||
if (this.stage === stages.s10_gameRunning) {
|
||||
this.core.tick(dt);
|
||||
}
|
||||
|
||||
// If the stage is still active (This might not be the case if tick() moved us to game over)
|
||||
if (this.stage === stages.s10_gameRunning) {
|
||||
// Only draw if page visible
|
||||
if (this.app.pageVisible) {
|
||||
this.core.draw();
|
||||
}
|
||||
|
||||
this.loadingOverlay.removeIfAttached();
|
||||
} else {
|
||||
if (!this.loadingOverlay.isAttached()) {
|
||||
this.loadingOverlay.showBasic();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onBackgroundTick(dt) {
|
||||
this.onRender(dt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the game
|
||||
*/
|
||||
|
||||
doSave() {
|
||||
if (!this.savegame || !this.savegame.isSaveable()) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
if (APPLICATION_ERROR_OCCURED) {
|
||||
logger.warn("skipping save because application crashed");
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
if (
|
||||
this.stage !== stages.s10_gameRunning &&
|
||||
this.stage !== stages.s7_warmup &&
|
||||
this.stage !== stages.leaving
|
||||
) {
|
||||
logger.warn("Skipping save because game is not ready");
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// First update the game data
|
||||
logger.log("Starting to save game ...");
|
||||
this.core.root.signals.gameSaved.dispatch();
|
||||
this.savegame.updateData(this.core.root);
|
||||
return this.savegame.writeSavegameAndMetadata().catch(err => {
|
||||
logger.warn("Failed to save:", err);
|
||||
});
|
||||
}
|
||||
}
|
||||
import { APPLICATION_ERROR_OCCURED } from "../core/error_handler";
|
||||
import { GameState } from "../core/game_state";
|
||||
import { logSection, createLogger } from "../core/logging";
|
||||
import { waitNextFrame } from "../core/utils";
|
||||
import { globalConfig } from "../core/config";
|
||||
import { GameLoadingOverlay } from "../game/game_loading_overlay";
|
||||
import { KeyActionMapper } from "../game/key_action_mapper";
|
||||
import { Savegame } from "../savegame/savegame";
|
||||
import { GameCore } from "../game/core";
|
||||
import { MUSIC } from "../platform/sound";
|
||||
|
||||
const logger = createLogger("state/ingame");
|
||||
|
||||
// Different sub-states
|
||||
const stages = {
|
||||
s3_createCore: "🌈 3: Create core",
|
||||
s4_A_initEmptyGame: "🌈 4/A: Init empty game",
|
||||
s4_B_resumeGame: "🌈 4/B: Resume game",
|
||||
|
||||
s5_firstUpdate: "🌈 5: First game update",
|
||||
s6_postLoadHook: "🌈 6: Post load hook",
|
||||
s7_warmup: "🌈 7: Warmup",
|
||||
|
||||
s10_gameRunning: "🌈 10: Game finally running",
|
||||
|
||||
leaving: "🌈 Saving, then leaving the game",
|
||||
destroyed: "🌈 DESTROYED: Core is empty and waits for state leave",
|
||||
initFailed: "🌈 ERROR: Initialization failed!",
|
||||
};
|
||||
|
||||
export const gameCreationAction = {
|
||||
new: "new-game",
|
||||
resume: "resume-game",
|
||||
};
|
||||
|
||||
// Typehints
|
||||
export class GameCreationPayload {
|
||||
constructor() {
|
||||
/** @type {boolean|undefined} */
|
||||
this.fastEnter;
|
||||
|
||||
/** @type {Savegame} */
|
||||
this.savegame;
|
||||
}
|
||||
}
|
||||
|
||||
export class InGameState extends GameState {
|
||||
constructor() {
|
||||
super("InGameState");
|
||||
|
||||
/** @type {GameCreationPayload} */
|
||||
this.creationPayload = null;
|
||||
|
||||
// Stores current stage
|
||||
this.stage = "";
|
||||
|
||||
/** @type {GameCore} */
|
||||
this.core = null;
|
||||
|
||||
/** @type {KeyActionMapper} */
|
||||
this.keyActionMapper = null;
|
||||
|
||||
/** @type {GameLoadingOverlay} */
|
||||
this.loadingOverlay = null;
|
||||
|
||||
/** @type {Savegame} */
|
||||
this.savegame = null;
|
||||
|
||||
this.boundInputFilter = this.filterInput.bind(this);
|
||||
|
||||
/**
|
||||
* Whether we are currently saving the game
|
||||
* @TODO: This doesn't realy fit here
|
||||
*/
|
||||
this.currentSavePromise = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Switches the game into another sub-state
|
||||
* @param {string} stage
|
||||
*/
|
||||
switchStage(stage) {
|
||||
assert(stage, "Got empty stage");
|
||||
if (stage !== this.stage) {
|
||||
this.stage = stage;
|
||||
logger.log(this.stage);
|
||||
return true;
|
||||
} else {
|
||||
// log(this, "Re entering", stage);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// GameState implementation
|
||||
getInnerHTML() {
|
||||
return "";
|
||||
}
|
||||
|
||||
getThemeMusic() {
|
||||
return MUSIC.theme;
|
||||
}
|
||||
|
||||
onBeforeExit() {
|
||||
// logger.log("Saving before quitting");
|
||||
// return this.doSave().then(() => {
|
||||
// logger.log(this, "Successfully saved");
|
||||
// // this.stageDestroyed();
|
||||
// });
|
||||
}
|
||||
|
||||
onAppPause() {
|
||||
// if (this.stage === stages.s10_gameRunning) {
|
||||
// logger.log("Saving because app got paused");
|
||||
// this.doSave();
|
||||
// }
|
||||
}
|
||||
|
||||
getHasFadeIn() {
|
||||
return false;
|
||||
}
|
||||
|
||||
getPauseOnFocusLost() {
|
||||
return false;
|
||||
}
|
||||
|
||||
getHasUnloadConfirmation() {
|
||||
return true;
|
||||
}
|
||||
|
||||
onLeave() {
|
||||
if (this.core) {
|
||||
this.stageDestroyed();
|
||||
}
|
||||
this.app.inputMgr.dismountFilter(this.boundInputFilter);
|
||||
}
|
||||
|
||||
onResized(w, h) {
|
||||
super.onResized(w, h);
|
||||
if (this.stage === stages.s10_gameRunning) {
|
||||
this.core.resize(w, h);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- End of GameState implementation
|
||||
|
||||
/**
|
||||
* Goes back to the menu state
|
||||
*/
|
||||
goBackToMenu() {
|
||||
this.saveThenGoToState("MainMenuState");
|
||||
}
|
||||
|
||||
/**
|
||||
* Goes back to the settings state
|
||||
*/
|
||||
goToSettings() {
|
||||
this.saveThenGoToState("SettingsState", {
|
||||
backToStateId: this.key,
|
||||
backToStatePayload: this.creationPayload,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Goes back to the settings state
|
||||
*/
|
||||
goToKeybindings() {
|
||||
this.saveThenGoToState("KeybindingsState", {
|
||||
backToStateId: this.key,
|
||||
backToStatePayload: this.creationPayload,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves to a state outside of the game
|
||||
* @param {string} stateId
|
||||
* @param {any=} payload
|
||||
*/
|
||||
saveThenGoToState(stateId, payload) {
|
||||
if (this.stage === stages.leaving || this.stage === stages.destroyed) {
|
||||
logger.warn(
|
||||
"Tried to leave game twice or during destroy:",
|
||||
this.stage,
|
||||
"(attempted to move to",
|
||||
stateId,
|
||||
")"
|
||||
);
|
||||
return;
|
||||
}
|
||||
this.stageLeavingGame();
|
||||
this.doSave().then(() => {
|
||||
this.stageDestroyed();
|
||||
this.moveToState(stateId, payload);
|
||||
});
|
||||
}
|
||||
|
||||
onBackButton() {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the game somehow failed to initialize. Resets everything to basic state and
|
||||
* then goes to the main menu, showing the error
|
||||
* @param {string} err
|
||||
*/
|
||||
onInitializationFailure(err) {
|
||||
if (this.switchStage(stages.initFailed)) {
|
||||
logger.error("Init failure:", err);
|
||||
this.stageDestroyed();
|
||||
this.moveToState("MainMenuState", { loadError: err });
|
||||
}
|
||||
}
|
||||
|
||||
// STAGES
|
||||
|
||||
/**
|
||||
* Creates the game core instance, and thus the root
|
||||
*/
|
||||
stage3CreateCore() {
|
||||
if (this.switchStage(stages.s3_createCore)) {
|
||||
logger.log("Creating new game core");
|
||||
this.core = new GameCore(this.app);
|
||||
|
||||
this.core.initializeRoot(this, this.savegame);
|
||||
|
||||
if (this.savegame.hasGameDump()) {
|
||||
this.stage4bResumeGame();
|
||||
} else {
|
||||
this.app.gameAnalytics.handleGameStarted();
|
||||
this.stage4aInitEmptyGame();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes a new empty game
|
||||
*/
|
||||
stage4aInitEmptyGame() {
|
||||
if (this.switchStage(stages.s4_A_initEmptyGame)) {
|
||||
this.core.initNewGame();
|
||||
this.stage5FirstUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resumes an existing game
|
||||
*/
|
||||
stage4bResumeGame() {
|
||||
if (this.switchStage(stages.s4_B_resumeGame)) {
|
||||
if (!this.core.initExistingGame()) {
|
||||
this.onInitializationFailure("Savegame is corrupt and can not be restored.");
|
||||
return;
|
||||
}
|
||||
this.app.gameAnalytics.handleGameResumed();
|
||||
this.stage5FirstUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the first game update on the game which initializes most caches
|
||||
*/
|
||||
stage5FirstUpdate() {
|
||||
if (this.switchStage(stages.s5_firstUpdate)) {
|
||||
this.core.root.logicInitialized = true;
|
||||
this.core.updateLogic();
|
||||
this.stage6PostLoadHook();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call the post load hook, this means that we have loaded the game, and all systems
|
||||
* can operate and start to work now.
|
||||
*/
|
||||
stage6PostLoadHook() {
|
||||
if (this.switchStage(stages.s6_postLoadHook)) {
|
||||
logger.log("Post load hook");
|
||||
this.core.postLoadHook();
|
||||
this.stage7Warmup();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This makes the game idle and draw for a while, because we run most code this way
|
||||
* the V8 engine can already start to optimize it. Also this makes sure the resources
|
||||
* are in the VRAM and we have a smooth experience once we start.
|
||||
*/
|
||||
stage7Warmup() {
|
||||
if (this.switchStage(stages.s7_warmup)) {
|
||||
if (G_IS_DEV && globalConfig.debug.noArtificialDelays) {
|
||||
this.warmupTimeSeconds = 0.05;
|
||||
} else {
|
||||
if (this.creationPayload.fastEnter) {
|
||||
this.warmupTimeSeconds = globalConfig.warmupTimeSecondsFast;
|
||||
} else {
|
||||
this.warmupTimeSeconds = globalConfig.warmupTimeSecondsRegular;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The final stage where this game is running and updating regulary.
|
||||
*/
|
||||
stage10GameRunning() {
|
||||
if (this.switchStage(stages.s10_gameRunning)) {
|
||||
this.core.root.signals.readyToRender.dispatch();
|
||||
|
||||
logSection("GAME STARTED", "#26a69a");
|
||||
|
||||
// Initial resize, might have changed during loading (this is possible)
|
||||
this.core.resize(this.app.screenWidth, this.app.screenHeight);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This stage destroys the whole game, used to cleanup
|
||||
*/
|
||||
stageDestroyed() {
|
||||
if (this.switchStage(stages.destroyed)) {
|
||||
// Cleanup all api calls
|
||||
this.cancelAllAsyncOperations();
|
||||
|
||||
if (this.syncer) {
|
||||
this.syncer.cancelSync();
|
||||
this.syncer = null;
|
||||
}
|
||||
|
||||
// Cleanup core
|
||||
if (this.core) {
|
||||
this.core.destruct();
|
||||
this.core = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When leaving the game
|
||||
*/
|
||||
stageLeavingGame() {
|
||||
if (this.switchStage(stages.leaving)) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
// END STAGES
|
||||
|
||||
/**
|
||||
* Filters the input (keybindings)
|
||||
*/
|
||||
filterInput() {
|
||||
return this.stage === stages.s10_gameRunning;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {GameCreationPayload} payload
|
||||
*/
|
||||
onEnter(payload) {
|
||||
this.app.inputMgr.installFilter(this.boundInputFilter);
|
||||
|
||||
this.creationPayload = payload;
|
||||
this.savegame = payload.savegame;
|
||||
|
||||
this.loadingOverlay = new GameLoadingOverlay(this.app, this.getDivElement());
|
||||
this.loadingOverlay.showBasic();
|
||||
|
||||
// Remove unneded default element
|
||||
document.body.querySelector(".modalDialogParent").remove();
|
||||
|
||||
this.asyncChannel.watch(waitNextFrame()).then(() => this.stage3CreateCore());
|
||||
}
|
||||
|
||||
/**
|
||||
* Render callback
|
||||
* @param {number} dt
|
||||
*/
|
||||
onRender(dt) {
|
||||
if (APPLICATION_ERROR_OCCURED) {
|
||||
// Application somehow crashed, do not do anything
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.stage === stages.s7_warmup) {
|
||||
this.core.draw();
|
||||
this.warmupTimeSeconds -= dt / 1000.0;
|
||||
if (this.warmupTimeSeconds < 0) {
|
||||
logger.log("Warmup completed");
|
||||
this.stage10GameRunning();
|
||||
}
|
||||
}
|
||||
|
||||
if (this.stage === stages.s10_gameRunning) {
|
||||
this.core.tick(dt);
|
||||
}
|
||||
|
||||
// If the stage is still active (This might not be the case if tick() moved us to game over)
|
||||
if (this.stage === stages.s10_gameRunning) {
|
||||
// Only draw if page visible
|
||||
if (this.app.pageVisible) {
|
||||
this.core.draw();
|
||||
}
|
||||
|
||||
this.loadingOverlay.removeIfAttached();
|
||||
} else {
|
||||
if (!this.loadingOverlay.isAttached()) {
|
||||
this.loadingOverlay.showBasic();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onBackgroundTick(dt) {
|
||||
this.onRender(dt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the game
|
||||
*/
|
||||
|
||||
doSave() {
|
||||
if (!this.savegame || !this.savegame.isSaveable()) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
if (APPLICATION_ERROR_OCCURED) {
|
||||
logger.warn("skipping save because application crashed");
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
if (
|
||||
this.stage !== stages.s10_gameRunning &&
|
||||
this.stage !== stages.s7_warmup &&
|
||||
this.stage !== stages.leaving
|
||||
) {
|
||||
logger.warn("Skipping save because game is not ready");
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
if (this.currentSavePromise) {
|
||||
logger.warn("Skipping double save and returning same promise");
|
||||
return this.currentSavePromise;
|
||||
}
|
||||
logger.log("Starting to save game ...");
|
||||
this.savegame.updateData(this.core.root);
|
||||
|
||||
this.currentSavePromise = this.savegame
|
||||
.writeSavegameAndMetadata()
|
||||
.catch(err => {
|
||||
// Catch errors
|
||||
logger.warn("Failed to save:", err);
|
||||
})
|
||||
.then(() => {
|
||||
// Clear promise
|
||||
logger.log("Saved!");
|
||||
this.core.root.signals.gameSaved.dispatch();
|
||||
this.currentSavePromise = null;
|
||||
});
|
||||
|
||||
return this.currentSavePromise;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,293 +1,331 @@
|
||||
import { GameState } from "../core/game_state";
|
||||
import { createLogger } from "../core/logging";
|
||||
import { findNiceValue } from "../core/utils";
|
||||
import { cachebust } from "../core/cachebust";
|
||||
import { PlatformWrapperImplBrowser } from "../platform/browser/wrapper";
|
||||
import { T, autoDetectLanguageId, updateApplicationLanguage } from "../translations";
|
||||
import { HUDModalDialogs } from "../game/hud/parts/modal_dialogs";
|
||||
import { CHANGELOG } from "../changelog";
|
||||
import { globalConfig } from "../core/config";
|
||||
|
||||
const logger = createLogger("state/preload");
|
||||
|
||||
export class PreloadState extends GameState {
|
||||
constructor() {
|
||||
super("PreloadState");
|
||||
}
|
||||
|
||||
getInnerHTML() {
|
||||
return `
|
||||
<div class="loadingImage"></div>
|
||||
<div class="loadingStatus">
|
||||
<span class="desc">Booting</span>
|
||||
<span class="bar">
|
||||
<span class="inner" style="width: 0%"></span>
|
||||
<span class="status">0%</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
getThemeMusic() {
|
||||
return null;
|
||||
}
|
||||
|
||||
getHasFadeIn() {
|
||||
return false;
|
||||
}
|
||||
|
||||
onEnter() {
|
||||
this.htmlElement.classList.add("prefab_LoadingState");
|
||||
|
||||
const elementsToRemove = ["#loadingPreload", "#fontPreload"];
|
||||
for (let i = 0; i < elementsToRemove.length; ++i) {
|
||||
const elem = document.querySelector(elementsToRemove[i]);
|
||||
if (elem) {
|
||||
elem.remove();
|
||||
}
|
||||
}
|
||||
|
||||
this.dialogs = new HUDModalDialogs(null, this.app);
|
||||
const dialogsElement = document.body.querySelector(".modalDialogParent");
|
||||
this.dialogs.initializeToElement(dialogsElement);
|
||||
|
||||
/** @type {HTMLElement} */
|
||||
this.statusText = this.htmlElement.querySelector(".loadingStatus > .desc");
|
||||
/** @type {HTMLElement} */
|
||||
this.statusBar = this.htmlElement.querySelector(".loadingStatus > .bar > .inner");
|
||||
/** @type {HTMLElement} */
|
||||
this.statusBarText = this.htmlElement.querySelector(".loadingStatus > .bar > .status");
|
||||
|
||||
this.currentStatus = "booting";
|
||||
this.currentIndex = 0;
|
||||
|
||||
this.startLoading();
|
||||
}
|
||||
|
||||
onLeave() {
|
||||
// this.dialogs.cleanup();
|
||||
}
|
||||
|
||||
startLoading() {
|
||||
this.setStatus("Booting")
|
||||
|
||||
.then(() => this.setStatus("Creating platform wrapper"))
|
||||
.then(() => this.app.platformWrapper.initialize())
|
||||
|
||||
.then(() => this.setStatus("Initializing local storage"))
|
||||
.then(() => {
|
||||
const wrapper = this.app.platformWrapper;
|
||||
if (wrapper instanceof PlatformWrapperImplBrowser) {
|
||||
try {
|
||||
window.localStorage.setItem("local_storage_test", "1");
|
||||
window.localStorage.removeItem("local_storage_test");
|
||||
} catch (ex) {
|
||||
logger.error("Failed to read/write local storage:", ex);
|
||||
return new Promise(() => {
|
||||
alert(`Your brower does not support thirdparty cookies or you have disabled it in your security settings.\n\n
|
||||
In Chrome this setting is called "Block third-party cookies and site data".\n\n
|
||||
Please allow third party cookies and then reload the page.`);
|
||||
// Never return
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
.then(() => this.setStatus("Creating storage"))
|
||||
.then(() => {
|
||||
return this.app.storage.initialize();
|
||||
})
|
||||
|
||||
.then(() => this.setStatus("Initializing libraries"))
|
||||
.then(() => this.app.analytics.initialize())
|
||||
.then(() => this.app.gameAnalytics.initialize())
|
||||
|
||||
.then(() => this.setStatus("Initializing settings"))
|
||||
.then(() => {
|
||||
return this.app.settings.initialize();
|
||||
})
|
||||
|
||||
.then(() => {
|
||||
// Initialize fullscreen
|
||||
if (this.app.platformWrapper.getSupportsFullscreen()) {
|
||||
this.app.platformWrapper.setFullscreen(this.app.settings.getIsFullScreen());
|
||||
}
|
||||
})
|
||||
|
||||
.then(() => this.setStatus("Initializing language"))
|
||||
.then(() => {
|
||||
if (this.app.settings.getLanguage() === "auto-detect") {
|
||||
const language = autoDetectLanguageId();
|
||||
logger.log("Setting language to", language);
|
||||
return this.app.settings.updateLanguage(language);
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
const language = this.app.settings.getLanguage();
|
||||
updateApplicationLanguage(language);
|
||||
})
|
||||
|
||||
.then(() => this.setStatus("Initializing sounds"))
|
||||
.then(() => {
|
||||
// Notice: We don't await the sounds loading itself
|
||||
return this.app.sound.initialize();
|
||||
})
|
||||
|
||||
.then(() => {
|
||||
this.app.backgroundResourceLoader.startLoading();
|
||||
})
|
||||
|
||||
.then(() => this.setStatus("Initializing savegame"))
|
||||
.then(() => {
|
||||
return this.app.savegameMgr.initialize().catch(err => {
|
||||
logger.error("Failed to initialize savegames:", err);
|
||||
alert(
|
||||
"Your savegames failed to load, it seems your data files got corrupted. I'm so sorry!\n\n(This can happen if your pc crashed while a game was saved).\n\nYou can try re-importing your savegames."
|
||||
);
|
||||
return this.app.savegameMgr.writeAsync();
|
||||
});
|
||||
})
|
||||
|
||||
.then(() => this.setStatus("Downloading resources"))
|
||||
.then(() => {
|
||||
return this.app.backgroundResourceLoader.getPromiseForBareGame();
|
||||
})
|
||||
|
||||
.then(() => this.setStatus("Checking changelog"))
|
||||
.then(() => {
|
||||
if (G_IS_DEV && globalConfig.debug.disableUpgradeNotification) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.app.storage
|
||||
.readFileAsync("lastversion.bin")
|
||||
.catch(err => {
|
||||
logger.warn("Failed to read lastversion:", err);
|
||||
return G_BUILD_VERSION;
|
||||
})
|
||||
.then(version => {
|
||||
logger.log("Last version:", version, "App version:", G_BUILD_VERSION);
|
||||
this.app.storage.writeFileAsync("lastversion.bin", G_BUILD_VERSION);
|
||||
return version;
|
||||
})
|
||||
.then(version => {
|
||||
let changelogEntries = [];
|
||||
logger.log("Last seen version:", version);
|
||||
|
||||
for (let i = 0; i < CHANGELOG.length; ++i) {
|
||||
if (CHANGELOG[i].version === version) {
|
||||
break;
|
||||
}
|
||||
changelogEntries.push(CHANGELOG[i]);
|
||||
}
|
||||
if (changelogEntries.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let dialogHtml = T.dialogs.updateSummary.desc;
|
||||
for (let i = 0; i < changelogEntries.length; ++i) {
|
||||
const entry = changelogEntries[i];
|
||||
dialogHtml += `
|
||||
<div class="changelogDialogEntry">
|
||||
<span class="version">${entry.version}</span>
|
||||
<span class="date">${entry.date}</span>
|
||||
<ul class="changes">
|
||||
${entry.entries.map(text => `<li>${text}</li>`).join("")}
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return new Promise(resolve => {
|
||||
this.dialogs.showInfo(T.dialogs.updateSummary.title, dialogHtml).ok.add(resolve);
|
||||
});
|
||||
});
|
||||
})
|
||||
|
||||
.then(() => this.setStatus("Launching"))
|
||||
.then(
|
||||
() => {
|
||||
this.moveToState("MainMenuState");
|
||||
},
|
||||
err => {
|
||||
this.showFailMessage(err);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
setStatus(text) {
|
||||
logger.log("✅ " + text);
|
||||
this.currentIndex += 1;
|
||||
this.currentStatus = text;
|
||||
this.statusText.innerText = text;
|
||||
|
||||
const numSteps = 10; // FIXME
|
||||
|
||||
const percentage = (this.currentIndex / numSteps) * 100.0;
|
||||
this.statusBar.style.width = percentage + "%";
|
||||
this.statusBarText.innerText = findNiceValue(percentage) + "%";
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
showFailMessage(text) {
|
||||
logger.error("App init failed:", text);
|
||||
|
||||
const email = "bugs@shapez.io";
|
||||
|
||||
const subElement = document.createElement("div");
|
||||
subElement.classList.add("failureBox");
|
||||
|
||||
subElement.innerHTML = `
|
||||
<div class="logo">
|
||||
<img src="${cachebust("res/logo.png")}" alt="Shapez.io Logo">
|
||||
</div>
|
||||
<div class="failureInner">
|
||||
<div class="errorHeader">
|
||||
Failed to initialize application!
|
||||
</div>
|
||||
<div class="errorMessage">
|
||||
${this.currentStatus} failed:<br/>
|
||||
${text}
|
||||
</div>
|
||||
|
||||
<div class="supportHelp">
|
||||
Please send me an email with steps to reproduce and what you did before this happened:
|
||||
<br /><a class="email" href="mailto:${email}?subject=App%20does%20not%20launch">${email}</a>
|
||||
</div>
|
||||
|
||||
<div class="lower">
|
||||
<button class="resetApp styledButton">Reset App</button>
|
||||
<i>Build ${G_BUILD_VERSION} @ ${G_BUILD_COMMIT_HASH}</i>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.htmlElement.classList.add("failure");
|
||||
this.htmlElement.appendChild(subElement);
|
||||
|
||||
const resetBtn = subElement.querySelector("button.resetApp");
|
||||
this.trackClicks(resetBtn, this.showResetConfirm);
|
||||
}
|
||||
|
||||
showResetConfirm() {
|
||||
if (confirm("Are you sure you want to reset the app? This will delete all your savegames")) {
|
||||
this.resetApp();
|
||||
}
|
||||
}
|
||||
|
||||
resetApp() {
|
||||
this.app.settings
|
||||
.resetEverythingAsync()
|
||||
.then(() => {
|
||||
this.app.savegameMgr.resetEverythingAsync();
|
||||
})
|
||||
.then(() => {
|
||||
this.app.settings.resetEverythingAsync();
|
||||
})
|
||||
.then(() => {
|
||||
window.location.reload();
|
||||
});
|
||||
}
|
||||
}
|
||||
import { CHANGELOG } from "../changelog";
|
||||
import { cachebust } from "../core/cachebust";
|
||||
import { globalConfig } from "../core/config";
|
||||
import { GameState } from "../core/game_state";
|
||||
import { createLogger } from "../core/logging";
|
||||
import { findNiceValue } from "../core/utils";
|
||||
import { getRandomHint } from "../game/hints";
|
||||
import { HUDModalDialogs } from "../game/hud/parts/modal_dialogs";
|
||||
import { PlatformWrapperImplBrowser } from "../platform/browser/wrapper";
|
||||
import { autoDetectLanguageId, T, updateApplicationLanguage } from "../translations";
|
||||
|
||||
const logger = createLogger("state/preload");
|
||||
|
||||
export class PreloadState extends GameState {
|
||||
constructor() {
|
||||
super("PreloadState");
|
||||
}
|
||||
|
||||
getInnerHTML() {
|
||||
return `
|
||||
<div class="loadingImage"></div>
|
||||
<div class="loadingStatus">
|
||||
<span class="desc">Booting</span>
|
||||
<span class="bar">
|
||||
<span class="inner" style="width: 0%"></span>
|
||||
<span class="status">0%</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="hint"></span>
|
||||
`;
|
||||
}
|
||||
|
||||
getThemeMusic() {
|
||||
return null;
|
||||
}
|
||||
|
||||
getHasFadeIn() {
|
||||
return false;
|
||||
}
|
||||
|
||||
onEnter() {
|
||||
this.htmlElement.classList.add("prefab_LoadingState");
|
||||
|
||||
const elementsToRemove = ["#loadingPreload", "#fontPreload"];
|
||||
for (let i = 0; i < elementsToRemove.length; ++i) {
|
||||
const elem = document.querySelector(elementsToRemove[i]);
|
||||
if (elem) {
|
||||
elem.remove();
|
||||
}
|
||||
}
|
||||
|
||||
this.dialogs = new HUDModalDialogs(null, this.app);
|
||||
const dialogsElement = document.body.querySelector(".modalDialogParent");
|
||||
this.dialogs.initializeToElement(dialogsElement);
|
||||
|
||||
/** @type {HTMLElement} */
|
||||
this.statusText = this.htmlElement.querySelector(".loadingStatus > .desc");
|
||||
/** @type {HTMLElement} */
|
||||
this.statusBar = this.htmlElement.querySelector(".loadingStatus > .bar > .inner");
|
||||
/** @type {HTMLElement} */
|
||||
this.statusBarText = this.htmlElement.querySelector(".loadingStatus > .bar > .status");
|
||||
|
||||
/** @type {HTMLElement} */
|
||||
this.hintsText = this.htmlElement.querySelector(".hint");
|
||||
this.lastHintShown = -1000;
|
||||
this.nextHintDuration = 0;
|
||||
|
||||
this.currentStatus = "booting";
|
||||
this.currentIndex = 0;
|
||||
|
||||
this.startLoading();
|
||||
}
|
||||
|
||||
onLeave() {
|
||||
// this.dialogs.cleanup();
|
||||
}
|
||||
|
||||
startLoading() {
|
||||
this.setStatus("Booting")
|
||||
|
||||
.then(() => this.setStatus("Creating platform wrapper"))
|
||||
.then(() => this.app.platformWrapper.initialize())
|
||||
|
||||
.then(() => this.setStatus("Initializing local storage"))
|
||||
.then(() => {
|
||||
const wrapper = this.app.platformWrapper;
|
||||
if (wrapper instanceof PlatformWrapperImplBrowser) {
|
||||
try {
|
||||
window.localStorage.setItem("local_storage_test", "1");
|
||||
window.localStorage.removeItem("local_storage_test");
|
||||
} catch (ex) {
|
||||
logger.error("Failed to read/write local storage:", ex);
|
||||
return new Promise(() => {
|
||||
alert(`Your brower does not support thirdparty cookies or you have disabled it in your security settings.\n\n
|
||||
In Chrome this setting is called "Block third-party cookies and site data".\n\n
|
||||
Please allow third party cookies and then reload the page.`);
|
||||
// Never return
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
.then(() => this.setStatus("Creating storage"))
|
||||
.then(() => {
|
||||
return this.app.storage.initialize();
|
||||
})
|
||||
|
||||
.then(() => this.setStatus("Initializing libraries"))
|
||||
.then(() => this.app.analytics.initialize())
|
||||
.then(() => this.app.gameAnalytics.initialize())
|
||||
|
||||
.then(() => this.setStatus("Initializing settings"))
|
||||
.then(() => {
|
||||
return this.app.settings.initialize();
|
||||
})
|
||||
|
||||
.then(() => {
|
||||
// Initialize fullscreen
|
||||
if (this.app.platformWrapper.getSupportsFullscreen()) {
|
||||
this.app.platformWrapper.setFullscreen(this.app.settings.getIsFullScreen());
|
||||
}
|
||||
})
|
||||
|
||||
.then(() => this.setStatus("Initializing language"))
|
||||
.then(() => {
|
||||
if (this.app.settings.getLanguage() === "auto-detect") {
|
||||
const language = autoDetectLanguageId();
|
||||
logger.log("Setting language to", language);
|
||||
return this.app.settings.updateLanguage(language);
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
const language = this.app.settings.getLanguage();
|
||||
updateApplicationLanguage(language);
|
||||
})
|
||||
|
||||
.then(() => this.setStatus("Initializing sounds"))
|
||||
.then(() => {
|
||||
// Notice: We don't await the sounds loading itself
|
||||
return this.app.sound.initialize();
|
||||
})
|
||||
|
||||
.then(() => {
|
||||
this.app.backgroundResourceLoader.startLoading();
|
||||
})
|
||||
|
||||
.then(() => this.setStatus("Initializing savegame"))
|
||||
.then(() => {
|
||||
return this.app.savegameMgr.initialize().catch(err => {
|
||||
logger.error("Failed to initialize savegames:", err);
|
||||
alert(
|
||||
"Your savegames failed to load, it seems your data files got corrupted. I'm so sorry!\n\n(This can happen if your pc crashed while a game was saved).\n\nYou can try re-importing your savegames."
|
||||
);
|
||||
return this.app.savegameMgr.writeAsync();
|
||||
});
|
||||
})
|
||||
|
||||
.then(() => this.setStatus("Downloading resources"))
|
||||
.then(() => {
|
||||
return this.app.backgroundResourceLoader.getPromiseForBareGame();
|
||||
})
|
||||
|
||||
.then(() => this.setStatus("Checking changelog"))
|
||||
.then(() => {
|
||||
if (G_IS_DEV && globalConfig.debug.disableUpgradeNotification) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.app.storage
|
||||
.readFileAsync("lastversion.bin")
|
||||
.catch(err => {
|
||||
logger.warn("Failed to read lastversion:", err);
|
||||
return G_BUILD_VERSION;
|
||||
})
|
||||
.then(version => {
|
||||
logger.log("Last version:", version, "App version:", G_BUILD_VERSION);
|
||||
this.app.storage.writeFileAsync("lastversion.bin", G_BUILD_VERSION);
|
||||
return version;
|
||||
})
|
||||
.then(version => {
|
||||
let changelogEntries = [];
|
||||
logger.log("Last seen version:", version);
|
||||
|
||||
for (let i = 0; i < CHANGELOG.length; ++i) {
|
||||
if (CHANGELOG[i].version === version) {
|
||||
break;
|
||||
}
|
||||
changelogEntries.push(CHANGELOG[i]);
|
||||
}
|
||||
if (changelogEntries.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let dialogHtml = T.dialogs.updateSummary.desc;
|
||||
for (let i = 0; i < changelogEntries.length; ++i) {
|
||||
const entry = changelogEntries[i];
|
||||
dialogHtml += `
|
||||
<div class="changelogDialogEntry">
|
||||
<span class="version">${entry.version}</span>
|
||||
<span class="date">${entry.date}</span>
|
||||
<ul class="changes">
|
||||
${entry.entries.map(text => `<li>${text}</li>`).join("")}
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return new Promise(resolve => {
|
||||
this.dialogs.showInfo(T.dialogs.updateSummary.title, dialogHtml).ok.add(resolve);
|
||||
});
|
||||
});
|
||||
})
|
||||
|
||||
.then(() => this.setStatus("Launching"))
|
||||
.then(
|
||||
() => {
|
||||
this.moveToState("MainMenuState");
|
||||
},
|
||||
err => {
|
||||
this.showFailMessage(err);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
update() {
|
||||
const now = performance.now();
|
||||
if (now - this.lastHintShown > this.nextHintDuration) {
|
||||
this.lastHintShown = now;
|
||||
const hintText = getRandomHint();
|
||||
|
||||
this.hintsText.innerHTML = hintText;
|
||||
|
||||
/**
|
||||
* Compute how long the user will need to read the hint.
|
||||
* We calculate with 130 words per minute, with an average of 5 chars
|
||||
* that is 650 characters / minute
|
||||
*/
|
||||
this.nextHintDuration = Math.max(2500, (hintText.length / 650) * 60 * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
onRender() {
|
||||
this.update();
|
||||
}
|
||||
|
||||
onBackgroundTick() {
|
||||
this.update();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} text
|
||||
*/
|
||||
setStatus(text) {
|
||||
logger.log("✅ " + text);
|
||||
this.currentIndex += 1;
|
||||
this.currentStatus = text;
|
||||
this.statusText.innerText = text;
|
||||
|
||||
const numSteps = 10; // FIXME
|
||||
|
||||
const percentage = (this.currentIndex / numSteps) * 100.0;
|
||||
this.statusBar.style.width = percentage + "%";
|
||||
this.statusBarText.innerText = findNiceValue(percentage) + "%";
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
showFailMessage(text) {
|
||||
logger.error("App init failed:", text);
|
||||
|
||||
const email = "bugs@shapez.io";
|
||||
|
||||
const subElement = document.createElement("div");
|
||||
subElement.classList.add("failureBox");
|
||||
|
||||
subElement.innerHTML = `
|
||||
<div class="logo">
|
||||
<img src="${cachebust("res/logo.png")}" alt="Shapez.io Logo">
|
||||
</div>
|
||||
<div class="failureInner">
|
||||
<div class="errorHeader">
|
||||
Failed to initialize application!
|
||||
</div>
|
||||
<div class="errorMessage">
|
||||
${this.currentStatus} failed:<br/>
|
||||
${text}
|
||||
</div>
|
||||
|
||||
<div class="supportHelp">
|
||||
Please send me an email with steps to reproduce and what you did before this happened:
|
||||
<br /><a class="email" href="mailto:${email}?subject=App%20does%20not%20launch">${email}</a>
|
||||
</div>
|
||||
|
||||
<div class="lower">
|
||||
<button class="resetApp styledButton">Reset App</button>
|
||||
<i>Build ${G_BUILD_VERSION} @ ${G_BUILD_COMMIT_HASH}</i>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.htmlElement.classList.add("failure");
|
||||
this.htmlElement.appendChild(subElement);
|
||||
|
||||
const resetBtn = subElement.querySelector("button.resetApp");
|
||||
this.trackClicks(resetBtn, this.showResetConfirm);
|
||||
|
||||
this.hintsText.remove();
|
||||
}
|
||||
|
||||
showResetConfirm() {
|
||||
if (confirm("Are you sure you want to reset the app? This will delete all your savegames!")) {
|
||||
this.resetApp();
|
||||
}
|
||||
}
|
||||
|
||||
resetApp() {
|
||||
this.app.settings
|
||||
.resetEverythingAsync()
|
||||
.then(() => {
|
||||
this.app.savegameMgr.resetEverythingAsync();
|
||||
})
|
||||
.then(() => {
|
||||
this.app.settings.resetEverythingAsync();
|
||||
})
|
||||
.then(() => {
|
||||
window.location.reload();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -38,7 +38,7 @@ The base language is English and can be found [here](base-en.yaml).
|
||||
|
||||
## Editing existing translations
|
||||
|
||||
If you want to edit an existing translation (Fixing typos, Updating it to a newer version, etc), you can just use the github file editor to edit the file.
|
||||
If you want to edit an existing translation (Fixing typos, updating it to a newer version, etc), you can just use the github file editor to edit the file.
|
||||
|
||||
- Click the language you want to edit from the list above
|
||||
- Click the small "edit" symbol on the top right
|
||||
@ -61,6 +61,8 @@ If you want to edit an existing translation (Fixing typos, Updating it to a newe
|
||||
|
||||
Please DM me on Discord (tobspr#5407), so I can add the language template for you.
|
||||
|
||||
**Important: I am currently not accepting new languages until the wires update is out!**
|
||||
|
||||
Please use the following template:
|
||||
|
||||
```
|
||||
@ -77,4 +79,4 @@ PS: I'm super busy, but I'll give my best to do it quickly!
|
||||
|
||||
## Updating a language to the latest version
|
||||
|
||||
Run `yarn syncTranslations` in the root directory to synchronize all translations to the latest version! This will remove obsolete keys and add newly added keys. (Run `yarn` before to install packes).
|
||||
Run `yarn syncTranslations` in the root directory to synchronize all translations to the latest version! This will remove obsolete keys and add newly added keys. (Run `yarn` before to install packages).
|
||||
|
||||
@ -66,7 +66,7 @@ steamPage:
|
||||
[*] Verschiedene Karten und Herausforderungen (z.B. Karten mit Hindernissen)
|
||||
[*] Puzzle (Liefere die geforderte Form mit begrenztem Platz/limitierten Gebäuden)
|
||||
[*] Eine Kampagne mit Gebäudekosten
|
||||
[*] Konfigurierbarer Kartengenerator (Ändere die Grösse/Anzahl/Dichte der Ressourcenflecken, den Seed und viel mehr)
|
||||
[*] Konfigurierbarer Kartengenerator (Ändere die Grösse/Anzahl/Dichte der Ressourcenflecken, den Seed und vieles mehr)
|
||||
[*] Mehr Formentypen
|
||||
[*] Performanceverbesserungen (Das Spiel läuft bereits sehr gut!)
|
||||
[*] Und vieles mehr!
|
||||
@ -256,7 +256,7 @@ dialogs:
|
||||
title: Nützliche Hotkeys
|
||||
desc: >-
|
||||
Dieses Spiel hat viele Hotkeys, die den Bau von Fabriken vereinfachen und beschleunigen.
|
||||
Hier sind ein paar, aber prüfe am besten die <strong>Tastenbelegung-Einstellungen</strong>!<br><br>
|
||||
Hier sind ein paar Beispiele, aber prüfe am besten die <strong>Tastenbelegung-Einstellungen</strong>!<br><br>
|
||||
<code class='keybinding'>STRG</code> + Ziehen: Wähle Areal aus.<br>
|
||||
<code class='keybinding'>UMSCH</code>: Halten, um mehrere Gebäude zu platzieren.<br>
|
||||
<code class='keybinding'>ALT</code>: Invertiere die Platzierungsrichtung der Förderbänder.<br>
|
||||
@ -635,7 +635,7 @@ storyRewards:
|
||||
no_reward:
|
||||
title: Nächstes Level
|
||||
desc: >-
|
||||
Dieses Level hat dir keine Belohnung gegeben, aber dafür das Nächste schon! <br><br> PS: Denke daran, deine alten Fabriken nicht zu zerstören - Du wirst sie später <strong>alle</strong> noch brauchen, um <strong>Upgrades freizuschalten</strong>!
|
||||
Dieses Level hat dir keine Belohnung gegeben, aber im Nächsten gibt es eine! <br><br> PS: Denke daran, deine alten Fabriken nicht zu zerstören - Du wirst sie später <strong>alle</strong> noch brauchen, um <strong>Upgrades freizuschalten</strong>!
|
||||
|
||||
no_reward_freeplay:
|
||||
title: Nächstes Level
|
||||
@ -694,7 +694,7 @@ settings:
|
||||
movementSpeed:
|
||||
title: Bewegungsgeschwindigkeit
|
||||
description: >-
|
||||
Ändert die Geschwindigkeit, mit der der Bildschirm durch die Pfeiltasten bewegt wird.
|
||||
Ändert die Geschwindigkeit, mit welcher der Bildschirm durch die Pfeiltasten bewegt wird.
|
||||
speeds:
|
||||
super_slow: Sehr langsam
|
||||
slow: Langsam
|
||||
@ -711,7 +711,7 @@ settings:
|
||||
enableColorBlindHelper:
|
||||
title: Modus für Farbenblinde
|
||||
description: >-
|
||||
Aktiviert verschiedene Werkzeuge, die dir das Spielen trotz Farbenblindheit ermöglichen.
|
||||
Aktiviert verschiedene Werkzeuge, welche dir das Spielen trotz Farbenblindheit ermöglichen.
|
||||
|
||||
fullscreen:
|
||||
title: Vollbild
|
||||
@ -775,7 +775,7 @@ settings:
|
||||
rotationByBuilding:
|
||||
title: Rotation pro Gebäudetyp
|
||||
description: >-
|
||||
Jeder Gebäudetyp merkt sich einzeln, in welche Richtung er zeigt.
|
||||
Jeder Gebäudetyp merkt sich eigenständig, in welche Richtung er zeigt.
|
||||
Das fühlt sich möglicherweise besser an, wenn du häufig zwischen verschiedenen Gebäudetypen wechselst.
|
||||
|
||||
compactBuildingInfo:
|
||||
@ -786,7 +786,7 @@ settings:
|
||||
disableCutDeleteWarnings:
|
||||
title: Deaktiviere Warnungsdialog beim Löschen
|
||||
description: >-
|
||||
Deaktiviert die Warnung, die beim Löschen und Ausschneiden von mehr als 100 Feldern angezeigt wird.
|
||||
Deaktiviert die Warnung, welche beim Löschen und Ausschneiden von mehr als 100 Feldern angezeigt wird.
|
||||
|
||||
keybindings:
|
||||
title: Tastenbelegung
|
||||
|
||||
@ -898,6 +898,11 @@ settings:
|
||||
description: >-
|
||||
Enabled by default, selects the miner if you use the pipette when hovering a resource patch.
|
||||
|
||||
simplifiedBelts:
|
||||
title: Simplified Belts (Ugly)
|
||||
description: >-
|
||||
Does not render belt items except when hovering the belt, to save performance.
|
||||
|
||||
keybindings:
|
||||
title: Keybindings
|
||||
hint: >-
|
||||
@ -1004,3 +1009,53 @@ demo:
|
||||
exportingBase: Exporting whole Base as Image
|
||||
|
||||
settingNotAvailable: Not available in the demo.
|
||||
|
||||
tips:
|
||||
- The hub accepts input of any kind, not just the current shape!
|
||||
- Make sure your factories are stackable - it will pay out!
|
||||
- Don't build too close to the hub, or it will be a huge chaos!
|
||||
- If stacking does not work, try switching the inputs.
|
||||
- You can toggle the belt planner direction by pressing <b>R</b>.
|
||||
- Holding <b>CTRL</b> allows dragging of belts without auto-orientation.
|
||||
- Ratios stay the same, as long as all upgrades are on the same Tier.
|
||||
- Serial execution is more efficient than parallel.
|
||||
- You will unlock more variants of buildings later in the game!
|
||||
- You can use <b>T</b> to switch between different variants.
|
||||
- Symmetry is key!
|
||||
- You can weave different tiers of tunnels.
|
||||
- Try to build compact factories - it will pay out!
|
||||
- The painter has a mirrored variant which you can select with <b>T</b>
|
||||
- Having the right building ratios will maximize efficiency.
|
||||
- At maximum level, 5 extractors will fill a single belt.
|
||||
- Don't forget about tunnels!
|
||||
- You don't need to divide up items evenly for full efficiency.
|
||||
- Holding <b>SHIFT</b> will activate the belt planner, letting you place long lines of belts easily.
|
||||
- Cutters always cut vertically, regardless of their orientation.
|
||||
- To get white mix all three colors.
|
||||
- The storage buffer priorities the first output.
|
||||
- Invest time to build repeatable designs - it's worth it!
|
||||
- Holding <b>CTRL</b> allows to place multiple buildings.
|
||||
- You can hold <b>ALT</b> to invert the direction of placed belts.
|
||||
- Efficiency is key!
|
||||
- Shape patches that are further away from the hub are more complex.
|
||||
- Machines have a limited speed, divide them up for maximum efficiency.
|
||||
- Use balancers to maximize your efficiency.
|
||||
- Organization is important. Try not to cross conveyors too much.
|
||||
- Plan in advance, or it will be a huge chaos!
|
||||
- Don't remove your old factories! You'll need them to unlock upgrades.
|
||||
- Try beating level 18 on your own before seeking for help!
|
||||
- Don't complicate things, try to stay simple and you'll go far.
|
||||
- You may need to re-use factories later in the game. Plan your factories to be re-usable.
|
||||
- Sometimes, you can find a needed shape in the map without creating it with stackers.
|
||||
- Full windmills / pinwheels can never spawn naturally.
|
||||
- Color your shapes before cutting for maximum efficiency.
|
||||
- With modules, space is merely a perception; a concern for mortal men.
|
||||
- Make a separate blueprint factory. They're important for modules.
|
||||
- Have a closer look on the color mixer, and your questions will be answered.
|
||||
- Use <b>CTRL</b> + Click to select an area.
|
||||
- Building too close to the hub can get in the way of later projects.
|
||||
- The pin icon next to each shape in the upgrade list pins it to the screen.
|
||||
- Mix all primary colours together to make white!
|
||||
- You have an infinite map, don't cramp your factory, expand!
|
||||
- Also try Factorio! It's my favourite game.
|
||||
- The quad cutter cuts clockwise starting from the top right!
|
||||
|
||||
@ -168,7 +168,7 @@ dialogs:
|
||||
deleteGame: Tiedän mitä olen tekemässä
|
||||
viewUpdate: Näytä päivitys
|
||||
showUpgrades: Näytä Päivitykset
|
||||
showKeybindings: Show Keybindings
|
||||
showKeybindings: Näytä pikanäppäimet
|
||||
|
||||
importSavegameError:
|
||||
title: Tuonti Virhe
|
||||
@ -258,7 +258,7 @@ dialogs:
|
||||
createMarker:
|
||||
title: Uusi Merkki
|
||||
desc: Anna merkille kuvaava nimi, voit myös sisällyttää muodon <strong>lyhyen avaimen</strong> siihen. (Lyhyen avaimen voit luoda <a href="https://viewer.shapez.io" target="_blank">täällä</a>)
|
||||
titleEdit: Edit Marker
|
||||
titleEdit: Muokkaa merkkiä
|
||||
|
||||
markerDemoLimit:
|
||||
desc: Voit tehdä vain kaksi mukautettua merkkiä demoversiossa. Hanki itsenäinen versio saadaksesi loputtoman määrän merkkejä!
|
||||
@ -267,8 +267,8 @@ dialogs:
|
||||
title: Vie kuvakaappaus
|
||||
desc: Pyysit tukikohtasi viemistä kuvakaappauksena. Huomaa, että tämä voi olla melko hidasta isolla tukikohdalla ja voi jopa kaataa pelisi!
|
||||
massCutInsufficientConfirm:
|
||||
title: Confirm cut
|
||||
desc: You can not afford to paste this area! Are you sure you want to cut it?
|
||||
title: Vahvista leikkaus
|
||||
desc: Sinulla ei ole varaa leikata tätä aluetta! Oletko varma että haluat leikata sen?
|
||||
|
||||
ingame:
|
||||
# This is shown in the top left corner and displays useful keybindings in
|
||||
@ -303,8 +303,8 @@ ingame:
|
||||
purple: Violetti
|
||||
cyan: Syaani
|
||||
white: Valkoinen
|
||||
uncolored: Ei Väriä
|
||||
black: Black
|
||||
uncolored: Väritön
|
||||
black: Musta
|
||||
|
||||
# Everything related to placing buildings (I.e. as soon as you selected a building
|
||||
# from the toolbar)
|
||||
@ -465,7 +465,7 @@ buildings:
|
||||
|
||||
tier2:
|
||||
name: Tunneli Taso II
|
||||
description: Sallii resurssien kuljetuksen rakennuksien ja hihnojen alta.
|
||||
description: Sallii resurssien kuljetuksen rakennuksien ja hihnojen alta pidemmältä kantamalta.
|
||||
|
||||
splitter: # Internal name for the Balancer
|
||||
default:
|
||||
@ -498,11 +498,11 @@ buildings:
|
||||
name: &rotater Kääntäjä
|
||||
description: Kääntää muotoja 90 astetta myötäpäivään.
|
||||
ccw:
|
||||
name: Rotate (Vastapäivään)
|
||||
name: Kääntäjä (Vastapäivään)
|
||||
description: Kääntää muotoja 90 astetta vastapäivään.
|
||||
fl:
|
||||
name: Rotate (180)
|
||||
description: Rotates shapes by 180 degrees.
|
||||
name: Kääntäkä (180)
|
||||
description: Kääntää muotoja 180 astetta.
|
||||
|
||||
stacker:
|
||||
default:
|
||||
@ -550,11 +550,11 @@ buildings:
|
||||
description: Tuottaa sähköä kuluttamalla muotoja. Jokainen sähkögeneraattori vaatii eri muotoja.
|
||||
wire_crossings:
|
||||
default:
|
||||
name: Wire Splitter
|
||||
description: Splits a energy wire into two.
|
||||
name: Johdon jakaja
|
||||
description: Jakaa energiajohdon kahteen.
|
||||
merger:
|
||||
name: Wire Merger
|
||||
description: Merges two energy wires into one.
|
||||
name: Johtojen yhdistäjä
|
||||
description: Yhdistää kaksi energiajohtoa yhteen.
|
||||
|
||||
storyRewards:
|
||||
# Those are the rewards gained from completing the store
|
||||
@ -642,9 +642,9 @@ storyRewards:
|
||||
settings:
|
||||
title: Asetukset
|
||||
categories:
|
||||
general: General
|
||||
userInterface: User Interface
|
||||
advanced: Advanced
|
||||
general: Yleinen
|
||||
userInterface: Käyttöliittyma
|
||||
advanced: Kehittynyt
|
||||
|
||||
versionBadges:
|
||||
dev: Kehitys
|
||||
@ -736,17 +736,17 @@ settings:
|
||||
refreshRate:
|
||||
title: Simulaatiotavoite
|
||||
description: >-
|
||||
Jos sinulla on 144hz näyttö, muuta virkistystaajuus täällä jotta pelin simulaatio toimii oikein isommilla virkistystaajuuksilla. Tämä voi laskea FPS nopeutta jos tietokoneesi on liian hidas.
|
||||
Jos sinulla on 144hz näyttö, muuta virkistystaajuus täällä jotta pelin simulaatio toimii oikein isommilla virkistystaajuuksilla. Tämä voi laskea FPS nopeutta, jos tietokoneesi on liian hidas.
|
||||
|
||||
alwaysMultiplace:
|
||||
title: Monisijoitus
|
||||
description: >-
|
||||
Jos käytössä, kaikki rakennukset pysyvät valittuina sijoittamisen jälkeen kunnes peruutat sen. Tämä vastaa SHIFT:in pitämistö pohjassa ikuisesti.
|
||||
Jos käytössä, kaikki rakennukset pysyvät valittuina sijoittamisen jälkeen kunnes peruutat sen. Tämä vastaa SHIFT:in pitämistä pohjassa ikuisesti.
|
||||
|
||||
offerHints:
|
||||
title: Vihjeet & Oppaat
|
||||
description: >-
|
||||
Tarjotaanko pelaamisen aikana vihjeitä ja oppaita. Myös piilottaa tietyt käyttöliittymäelementit tietyn tason mukaan, jotta alkuunpääseminen olisi helpompaa.
|
||||
Tarjoaa pelaamisen aikana vihjeitä ja oppaita. Myös piilottaa tietyt käyttöliittymäelementit tietyn tason mukaan, jotta alkuunpääseminen olisi helpompaa.
|
||||
|
||||
enableTunnelSmartplace:
|
||||
title: Älykkäät Tunnelit
|
||||
@ -771,12 +771,12 @@ settings:
|
||||
disableCutDeleteWarnings:
|
||||
title: Poista Leikkaus/Poisto Varoitukset
|
||||
description: >-
|
||||
Poista varoitusikkunat dialogs brought up when cutting/deleting more than 100 entities.
|
||||
Poista varoitusikkunat jotka ilmestyy kun leikkaat/poistat enemmän kuin 100 entiteettiä
|
||||
|
||||
keybindings:
|
||||
title: Pikanäppäimet
|
||||
hint: >-
|
||||
Tip: Muista käyttää CTRL, VAIHTO and ALT! Ne ottavat käyttöön erilaisia sijoitteluvaihtoehtoja.
|
||||
Tip: Muista käyttää CTRL, VAIHTO ja ALT! Ne ottavat käyttöön erilaisia sijoitteluvaihtoehtoja.
|
||||
|
||||
resetKeybindings: Nollaa Pikanäppäimet
|
||||
|
||||
@ -843,13 +843,13 @@ keybindings:
|
||||
|
||||
placementDisableAutoOrientation: Poista automaattinen suunta käytöstä
|
||||
placeMultiple: Pysy sijoittamistilassa
|
||||
placeInverse: Käännä automaattinen hihnan suunta
|
||||
menuClose: Close Menu
|
||||
placeInverse: Käännä automaattinen hihnan suunta päinvastoin
|
||||
menuClose: Sulje valikko
|
||||
|
||||
about:
|
||||
title: Tietoja tästä pelistä
|
||||
body: >-
|
||||
Tämä peli on avoimen lähdekoodin ja kehitettä on <a href="https://github.com/tobspr" target="_blank">Tobias Springer</a> (tämä on minä).<br><br>
|
||||
Tämä peli on avointa lähdekoodia ja kehittäjä on <a href="https://github.com/tobspr" target="_blank">Tobias Springer</a> (tämä on minä).<br><br>
|
||||
|
||||
Jos haluat osallistua, tarkista <a href="<githublink>" target="_blank">shapez.io githubissa</a>.<br><br>
|
||||
|
||||
|
||||
@ -32,15 +32,15 @@ steamPage:
|
||||
[img]{STEAM_APP_IMAGE}/extras/store_page_gif.gif[/img]
|
||||
|
||||
shapez.io est un jeu dans lequel vous devrez construire des usines pour automatiser la création et la combinaison de formes de plus en plus complexes sur une carte infinie.
|
||||
Lors de la livraison des formes requises vous progresserez et débloquerez des améliorations pour accélerer votre usine.
|
||||
En livrant les formes requises, vous progresserez et débloquerez des améliorations pour accélérer votre usine.
|
||||
|
||||
Au vu de l'augmentation des demandes de formes, vous devrez agrandir votre usine pour répondre à la forte demande - Mais n'oubliez pas les ressources, vous drevrez vous étendre au milieu de cette [b]carte infinie[/b] !
|
||||
Vous devrez agrandir votre usine pour répondre à l’augmentation de la demande en formes — Mais n’oubliez pas les ressources, vous devrez vous étendre au milieu de cette [b]carte infinie[/b] !
|
||||
|
||||
Bientôt vous devrez mixer les couleurs et peindre vos formes avec - Combinez les ressources de couleurs rouge, verte et bleue pour produire différentes couleurs et peindre les formes avec pour satisfaire la demande.
|
||||
Bientôt, vous devrez mélanger les couleurs et peindre vos formes avec — Combinez les ressources de couleurs rouge, verte et bleue pour produire différentes couleurs et peindre les formes avec pour satisfaire la demande.
|
||||
|
||||
Ce jeu propose 18 niveaux progressifs (qui devraient déjà vous occuper quelques heures !) mais j'ajoute constamment de nouveau contenus - Il y en a beaucoup de prévus !
|
||||
Ce jeu propose 18 niveaux progressifs (qui devraient déjà vous occuper quelques heures !) mais je développe constamment plus de contenu — Il y a beaucoup de choses prévues !
|
||||
|
||||
Acheter le jeu vous donne accès à la version complète qui a des fonctionnalités additionnelles et vous recevrez aussi un accès à des fonctionnalités fraîchement développées.
|
||||
Acheter le jeu vous donne accès à la version complète qui a des fonctionnalités supplémentaires, et vous pourrez aussi accéder aux fonctionnalités fraîchement développées.
|
||||
|
||||
[b]Avantages de la version complète (standalone)[/b]
|
||||
|
||||
@ -48,30 +48,30 @@ steamPage:
|
||||
[*] Mode sombre
|
||||
[*] Balises infinies
|
||||
[*] Parties infinies
|
||||
[*] Plus d'options
|
||||
[*] Prochainement: Câbles et énergie ! Prévu pour (environ) fin Juillet 2020.
|
||||
[*] Prochainement: Plus de niveaux
|
||||
[*] Aidez moi à continuer de développer shapez.io ❤️
|
||||
[*] Plus d’options
|
||||
[*] Prochainement : Câbles et énergie ! Prévu pour (environ) fin juillet 2020.
|
||||
[*] Prochainement : Plus de niveaux
|
||||
[*] Aidez-moi à continuer de développer shapez.io ❤️
|
||||
[/list]
|
||||
|
||||
[b]Mises à jour futures[/b]
|
||||
[b]Mises à jour à venir[/b]
|
||||
|
||||
Je fais souvent des mises à jour et essaye d'en sortir une par semaine!
|
||||
Je fais souvent des mises à jour et j’essaye d’en sortir une par semaine !
|
||||
|
||||
[list]
|
||||
[*] Différentes cartes et challenges (e.g. carte avec obstacles)
|
||||
[*] Puzzles (Délivrer la forme requise avec une zone limitée/jeu de bâtiments)
|
||||
[*] Casse-tête (Livrer la forme requise avec une zone limitée / jeu de bâtiments)
|
||||
[*] Un mode histoire où les bâtiments ont un coût
|
||||
[*] Générateur de carte configurable (configuration des ressources/formes/taille/densitée, seed et plus)
|
||||
[*] Plus de formes
|
||||
[*] Amélioration des performances (Le jeu tourne déjà plutot bien !)
|
||||
[*] Et bien plus !
|
||||
[*] Générateur de carte configurable (configuration des ressources / formes / taille / densité, graine aléatoire et plus)
|
||||
[*] Plus de niveaux
|
||||
[*] Amélioration des performances (Le jeu tourne déjà plutôt bien !)
|
||||
[*] Et bien plus !
|
||||
[/list]
|
||||
|
||||
[b]Ce jeu est open source ![/b]
|
||||
[b]Ce jeu est open source ![/b]
|
||||
|
||||
Tout le monde peut contribuer, je suis très impliqué dans la communauté et j'essaye de répondre à toutes les suggestions et prendre en compte vos retours si possible.
|
||||
Jetez un coup d'œil à mon Trello pour le suivi du projet et la planification du développement !
|
||||
Tout le monde peut contribuer, je suis très impliqué dans la communauté et j’essaye de répondre à toutes les suggestions et prendre en compte vos retours si possible.
|
||||
Jetez un coup d’œil à mon Trello pour le suivi du projet et les plans de développement !
|
||||
[b]Liens[/b]
|
||||
|
||||
[list]
|
||||
@ -82,19 +82,20 @@ steamPage:
|
||||
[*] [url=https://github.com/tobspr/shapez.io/blob/master/translations/README.md]Aidez à traduire[/url]
|
||||
[/list]
|
||||
|
||||
discordLink: Discord officiel - Parlez avec moi!
|
||||
discordLink: Discord officiel — Parlez avec moi !
|
||||
|
||||
global:
|
||||
loading: Chargement
|
||||
error: Erreur
|
||||
|
||||
# How big numbers are rendered, e.g. "10,000"
|
||||
thousandsDivider: " "
|
||||
# En français, le séparateur des milliers est l’espace (fine) insécable
|
||||
thousandsDivider: " "
|
||||
|
||||
# What symbol to use to seperate the integer part from the fractional part of a number, e.g. "0.4"
|
||||
decimalSeparator: ","
|
||||
|
||||
# The suffix for large numbers, e.g. 1.3k, 400.2M, etc. cf wikipedia système international d'unité
|
||||
# The suffix for large numbers, e.g. 1.3k, 400.2M, etc. cf wikipedia système international d’unité
|
||||
# For french: https://fr.wikipedia.org/wiki/Pr%C3%A9fixes_du_Syst%C3%A8me_international_d%27unit%C3%A9s
|
||||
suffix:
|
||||
thousands: k
|
||||
@ -108,20 +109,20 @@ global:
|
||||
time:
|
||||
# Used for formatting past time dates
|
||||
oneSecondAgo: il y a une seconde
|
||||
xSecondsAgo: il y a <x> secondes
|
||||
xSecondsAgo: il y a <x> secondes
|
||||
oneMinuteAgo: il y a une minute
|
||||
xMinutesAgo: il y a <x> minutes
|
||||
xMinutesAgo: il y a <x> minutes
|
||||
oneHourAgo: il y a une heure
|
||||
xHoursAgo: il y a <x> heures
|
||||
xHoursAgo: il y a <x> heures
|
||||
oneDayAgo: il y a un jour
|
||||
xDaysAgo: il y a <x> jours
|
||||
xDaysAgo: il y a <x> jours
|
||||
|
||||
# Short formats for times, e.g. '5h 23m'
|
||||
secondsShort: <seconds>s
|
||||
minutesAndSecondsShort: <minutes>m <seconds>s
|
||||
hoursAndMinutesShort: <hours>h <minutes>m
|
||||
secondsShort: <seconds> s
|
||||
minutesAndSecondsShort: <minutes> m <seconds> s
|
||||
hoursAndMinutesShort: <hours> h <minutes> m
|
||||
|
||||
xMinutes: <x> minutes
|
||||
xMinutes: <x> minutes
|
||||
|
||||
keys:
|
||||
tab: TAB
|
||||
@ -135,21 +136,21 @@ demoBanners:
|
||||
# This is the "advertisement" shown in the main menu and other various places
|
||||
title: Version démo
|
||||
intro: >-
|
||||
Achetez la version complète pour débloquer toutes les fonctionnalités !
|
||||
Achetez la version complète pour débloquer toutes les fonctionnalités !
|
||||
|
||||
mainMenu:
|
||||
play: Jouer
|
||||
changelog: Historique
|
||||
importSavegame: Importer
|
||||
openSourceHint: Ce jeu est open source !
|
||||
openSourceHint: Ce jeu est open source !
|
||||
discordLink: Serveur Discord officiel
|
||||
helpTranslate: Contribuez à la traduction !
|
||||
helpTranslate: Contribuez à la traduction !
|
||||
|
||||
# This is shown when using firefox and other browsers which are not supported.
|
||||
browserWarning: >-
|
||||
Désolé, ce jeu est connu pour tourner lentement sur votre navigateur web ! Procurez-vous la version complète ou téléchargez Chrome pour une meilleure expérience.
|
||||
Désolé, ce jeu sera lent sur votre navigateur web ! Procurez-vous la version complète ou téléchargez Chrome pour une meilleure expérience.
|
||||
|
||||
savegameLevel: Niveau <x>
|
||||
savegameLevel: Niveau <x>
|
||||
savegameLevelUnknown: Niveau inconnu
|
||||
|
||||
continue: Continuer
|
||||
@ -165,16 +166,16 @@ dialogs:
|
||||
later: Plus tard
|
||||
restart: Relancer
|
||||
reset: Réinitialiser
|
||||
getStandalone: Se procurer la version complète
|
||||
getStandalone: Obtenir la version complète
|
||||
deleteGame: Je sais ce que je fais
|
||||
viewUpdate: Voir les mises à jour
|
||||
showUpgrades: Montrer les améliorations
|
||||
showKeybindings: Montrer les raccourcis
|
||||
|
||||
importSavegameError:
|
||||
title: Erreur d'importation
|
||||
title: Erreur d’importation
|
||||
text: >-
|
||||
Impossible d'importer votre sauvegarde:
|
||||
Impossible d’importer votre sauvegarde :
|
||||
|
||||
importSavegameSuccess:
|
||||
title: Sauvegarde importée
|
||||
@ -184,17 +185,17 @@ dialogs:
|
||||
gameLoadFailure:
|
||||
title: La sauvegarde est corrompue
|
||||
text: >-
|
||||
Impossible de charger votre sauvegarde:
|
||||
Impossible de charger votre sauvegarde :
|
||||
|
||||
confirmSavegameDelete:
|
||||
title: Confirmez la suppression
|
||||
text: >-
|
||||
Êtes-vous certains de vouloir supprimer votre partie ?
|
||||
Êtes-vous sûr de vouloir supprimer votre partie ?
|
||||
|
||||
savegameDeletionError:
|
||||
title: Impossible de supprimer
|
||||
text: >-
|
||||
Impossible de supprimer votre sauvegarde:
|
||||
Impossible de supprimer votre sauvegarde :
|
||||
|
||||
restartRequired:
|
||||
title: Redémarrage requis
|
||||
@ -211,91 +212,91 @@ dialogs:
|
||||
|
||||
keybindingsResetOk:
|
||||
title: Réinitialisation des contrôles
|
||||
desc: Les contrôles ont été réinitialisés dans leur état par défaut respectifs !
|
||||
desc: Les contrôles ont été remis à défaut !
|
||||
|
||||
featureRestriction:
|
||||
title: Version démo
|
||||
desc: Vous avez essayé d'accéder à la fonction (<feature>) qui n'est pas disponible dans la démo. Considérez l'achat de la version complète pour une expérience optimale !
|
||||
desc: Vous avez essayé d’accéder à la fonction (<feature>) qui n’est pas disponible dans la démo. Pensez à acheter la version complète pour une expérience optimale !
|
||||
|
||||
oneSavegameLimit:
|
||||
title: Sauvegardes limitées
|
||||
desc: Vous ne pouvez avoir qu'une seule sauvegarde en même temps dans la version démo. Merci d'effacer celle en cours ou alternativement de vous procurer la version complète !
|
||||
desc: Vous ne pouvez avoir qu’une seule sauvegarde en même temps dans la version démo. Merci d’effacer celle en cours ou bien de vous procurer la version complète !
|
||||
|
||||
updateSummary:
|
||||
title: Nouvelle mise à jour !
|
||||
title: Nouvelle mise à jour !
|
||||
desc: >-
|
||||
Voici les modifications depuis votre dernière session:
|
||||
Voici les changements depuis votre dernière session :
|
||||
|
||||
upgradesIntroduction:
|
||||
title: Débloquer les améliorations
|
||||
desc: >-
|
||||
Toutes les formes que vous produisez peuvent être utilisées pour débloquer des améliorations - <strong>Ne détruisez pas vos anciennes usines !</strong>
|
||||
L'onglet des améliorations se trouve dans le coin supérieur droit de l'écran.
|
||||
Toutes les formes que vous produisez peuvent être utilisées pour débloquer des améliorations — <strong>Ne détruisez pas vos anciennes usines !</strong>
|
||||
L’onglet des améliorations se trouve dans le coin supérieur droit de l’écran.
|
||||
|
||||
massDeleteConfirm:
|
||||
title: Confirmation de suppression
|
||||
desc: >-
|
||||
Vous allez supprimer pas mal de bâtiments (<count> pour être exact) ! Êtes vous certains de vouloir faire ça ?
|
||||
Vous allez supprimer beaucoup de bâtiments (<count> pour être précis) ! Êtes-vous sûr de vouloir faire ça ?
|
||||
|
||||
massCutConfirm:
|
||||
title: Confirmer la coupure
|
||||
desc: >-
|
||||
Vous vous apprêtez à couper beaucoup de bâtiments (<count> pour être précis) ! Êtes-vous certains de vouloir faire ça ?
|
||||
Vous allez couper beaucoup de bâtiments (<count> pour être précis) ! Êtes-vous sûr de vouloir faire ça ?
|
||||
|
||||
blueprintsNotUnlocked:
|
||||
title: Pas encore débloqué
|
||||
desc: >-
|
||||
Les patrons n'ont pas encore étés débloqués ! Terminez encore quelques niveaux pour y avoir accès.
|
||||
Les patrons n’ont pas encore été débloqués ! Terminez le niveau 12 pour y avoir accès.
|
||||
|
||||
keybindingsIntroduction:
|
||||
title: Raccourcis utiles
|
||||
desc: >-
|
||||
Le jeu a de nombreux raccourcis facilitant la construction de grandes usines.
|
||||
En voici quelques uns, n'hésitez pas à aller <strong>découvrir les raccourcis</strong> !<br><br>
|
||||
<code class='keybinding'>CTRL</code> + Glisser: Sélectionne une zone à copier / effacer.<br>
|
||||
<code class='keybinding'>SHIFT</code>: Laissez appuyé pour placer plusieurs fois le même bâtiment.<br>
|
||||
<code class='keybinding'>ALT</code>: Inverse l'orientation des convoyeurs placés.<br>
|
||||
En voici quelques-uns, n’hésitez pas à aller <strong>découvrir les raccourcis</strong> !<br><br>
|
||||
<code class='keybinding'>CTRL</code> + glisser : Sélectionne une zone à copier / effacer.<br>
|
||||
<code class='keybinding'>MAJ</code> : Laissez appuyé pour placer plusieurs fois le même bâtiment.<br>
|
||||
<code class='keybinding'>ALT</code> : Inverse l’orientation des convoyeurs placés.<br>
|
||||
|
||||
createMarker:
|
||||
title: Nouvelle balise
|
||||
desc: Donnez-lui un nom, vous pouvez aussi inclure <strong>le raccourci </strong> d'une forme (Que vous pouvez générer <a href="https://viewer.shapez.io" target="_blank">ici</a>)
|
||||
titleEdit: Éditer cette balise
|
||||
desc: Donnez-lui un nom, vous pouvez aussi inclure <strong>le raccourci</strong> d’une forme (que vous pouvez générer <a href="https://viewer.shapez.io" target="_blank">ici</a>).
|
||||
titleEdit: Modifier cette balise
|
||||
|
||||
markerDemoLimit:
|
||||
desc: Vous ne pouvez créer que deux balises dans la démo. Achetez la version complète pour en faire autant que vous voulez !
|
||||
desc: Vous ne pouvez créer que deux balises dans la démo. Achetez la version complète pour en placer autant que vous voulez !
|
||||
|
||||
exportScreenshotWarning:
|
||||
title: Exporter une capture d'écran
|
||||
title: Exporter une capture d’écran
|
||||
desc: >-
|
||||
Vous avez demandé à exporter votre base sous la forme d'une capture d'écran. Soyez conscient que cela peut s'avérer passablement lent pour une grande base, voire même faire planter votre jeu !
|
||||
Vous avez demandé à exporter une capture d’écran de votre base. Soyez conscient que cela peut s’avérer passablement lent pour une grande base, voire faire planter votre jeu !
|
||||
|
||||
massCutInsufficientConfirm:
|
||||
title: Confirmer la coupe
|
||||
desc: Vous n'avez pas les moyens de copier cette zone ! Etes vous certain de vouloir la couper ?
|
||||
desc: Vous n’avez pas les moyens de copier cette zone ! Êtes-vous sûr de vouloir la couper ?
|
||||
|
||||
ingame:
|
||||
# This is shown in the top left corner and displays useful keybindings in
|
||||
# every situation
|
||||
keybindingsOverlay:
|
||||
moveMap: Déplacer
|
||||
selectBuildings: Sélection d'une zone
|
||||
stopPlacement: Arrêter le placement
|
||||
selectBuildings: Sélection d’une zone
|
||||
stopPlacement: Arrêter de placer
|
||||
rotateBuilding: Tourner le bâtiment
|
||||
placeMultiple: Placement multiple
|
||||
reverseOrientation: Changer l'orientation
|
||||
disableAutoOrientation: Désactiver l'orientation automatique
|
||||
toggleHud: Basculer l'affichage tête haute (ATH)
|
||||
reverseOrientation: Changer l’orientation
|
||||
disableAutoOrientation: Désactiver l’orientation automatique
|
||||
toggleHud: Basculer l’affichage tête haute (ATH)
|
||||
placeBuilding: Placer un bâtiment
|
||||
createMarker: Créer une balise
|
||||
delete: Supprimer
|
||||
pasteLastBlueprint: Copier le dernier patron
|
||||
lockBeltDirection: Utiliser le plannificateur de convoyeurs
|
||||
plannerSwitchSide: Échanger la direction du plannificateur
|
||||
lockBeltDirection: Utiliser le planificateur de convoyeurs
|
||||
plannerSwitchSide: Inverser la direction du planificateur
|
||||
cutSelection: Couper
|
||||
copySelection: Copier
|
||||
clearSelection: Effacer la sélection
|
||||
pipette: Pipette
|
||||
switchLayers: Échanger les calques
|
||||
switchLayers: Changer de calque
|
||||
|
||||
# Everything related to placing buildings (I.e. as soon as you selected a building
|
||||
# from the toolbar)
|
||||
@ -306,29 +307,29 @@ ingame:
|
||||
|
||||
# Shows the hotkey in the ui, e.g. "Hotkey: Q"
|
||||
hotkeyLabel: >-
|
||||
Raccourci: <key>
|
||||
Raccourci : <key>
|
||||
|
||||
infoTexts:
|
||||
speed: Vitesse
|
||||
range: Portée
|
||||
storage: Espace de stockage
|
||||
oneItemPerSecond: 1 forme / s
|
||||
itemsPerSecond: <x> formes / s
|
||||
itemsPerSecondDouble: (x2)
|
||||
oneItemPerSecond: 1 forme ⁄ s
|
||||
itemsPerSecond: <x> formes ⁄ s
|
||||
itemsPerSecondDouble: (×2)
|
||||
|
||||
tiles: <x> cases
|
||||
tiles: <x> cases
|
||||
|
||||
# The notification when completing a level
|
||||
levelCompleteNotification:
|
||||
# <level> is replaced by the actual level, so this gets 'Level 03' for example.
|
||||
levelTitle: Niveau <level>
|
||||
levelTitle: Niveau <level>
|
||||
completed: Terminé
|
||||
unlockText: <reward> débloqué !
|
||||
unlockText: <reward> débloqué !
|
||||
buttonNextLevel: Niveau suivant
|
||||
|
||||
# Notifications on the lower right
|
||||
notifications:
|
||||
newUpgrade: Une nouvelle amélioration est disponible !
|
||||
newUpgrade: Une nouvelle amélioration est disponible !
|
||||
gameSaved: Votre partie a été sauvegardée.
|
||||
|
||||
# The "Upgrades" window
|
||||
@ -337,11 +338,11 @@ ingame:
|
||||
buttonUnlock: Améliorer
|
||||
|
||||
# Gets replaced to e.g. "Tier IX"
|
||||
tier: Niveau <x>
|
||||
tier: Niveau <x>
|
||||
|
||||
# The roman number for each tier
|
||||
tierLabels: [I, II, III, IV, V, VI, VII, VIII, IX, X]
|
||||
maximumLevel: NIVEAU MAXIMAL (Vitesse x<currentMult>)
|
||||
maximumLevel: NIVEAU MAXIMAL (Vitesse ×<currentMult>)
|
||||
|
||||
# The "Statistics" window
|
||||
statistics:
|
||||
@ -352,14 +353,14 @@ ingame:
|
||||
description: Affiche le nombre de formes stockées dans votre bâtiment central.
|
||||
produced:
|
||||
title: Produit
|
||||
description: Affiche tous les formes que votre usine produit, en incluant les formes intermédiaires.
|
||||
description: Affiche toutes les formes que votre usine produit, y compris les formes intermédiaires.
|
||||
delivered:
|
||||
title: Délivré
|
||||
title: Livré
|
||||
description: Affiche les formes qui ont été livrées dans votre bâtiment central.
|
||||
noShapesProduced: Aucune forme n'a été produite jusqu'à présent.
|
||||
noShapesProduced: Aucune forme produite pour le moment.
|
||||
|
||||
# Displays the shapes per minute, e.g. '523 / m'
|
||||
shapesPerMinute: <shapes> / m
|
||||
shapesPerMinute: <shapes> ⁄ m
|
||||
|
||||
# Settings menu, when you press "ESC"
|
||||
settingsMenu:
|
||||
@ -375,7 +376,7 @@ ingame:
|
||||
|
||||
# Bottom left tutorial hints
|
||||
tutorialHints:
|
||||
title: Besoin d'aide ?
|
||||
title: Besoin d’aide ?
|
||||
showHint: Indice
|
||||
hideHint: Fermer
|
||||
|
||||
@ -387,19 +388,19 @@ ingame:
|
||||
waypoints:
|
||||
waypoints: Balise
|
||||
hub: Centre
|
||||
description: Cliquez une balise pour vous y rendre, clic-droit pour l'effacer.<br><br>Appuyez sur <keybinding> pour créer une balise sur la vue actuelle, ou <strong>clic-droit</strong> pour en créer une sur l'endroit pointé.
|
||||
description: Cliquez sur une balise pour vous y rendre, clic-droit pour l’effacer.<br><br>Appuyez sur <keybinding> pour créer une balise sur la vue actuelle, ou <strong>clic-droit</strong> pour en créer une sur l’endroit pointé.
|
||||
creationSuccessNotification: La balise a été créée.
|
||||
|
||||
# Interactive tutorial
|
||||
interactiveTutorial:
|
||||
title: Tutoriel
|
||||
hints:
|
||||
1_1_extractor: Placez un <strong>extracteur</strong> sur une <strong>forme en cercle</strong> pour l'extraire !
|
||||
1_1_extractor: Placez un <strong>extracteur</strong> sur une <strong>forme en cercle</strong> pour l’extraire !
|
||||
1_2_conveyor: >-
|
||||
Connectez l'extracteur avec un <strong>convoyeur</strong> vers votre centre !<br><br>Astuce: <strong>Cliquez et faites glisser</strong> le convoyeur avec votre souris !
|
||||
Connectez l’extracteur avec un <strong>convoyeur</strong> vers votre centre !<br><br>Astuce : <strong>Cliquez et faites glisser</strong> le convoyeur avec votre souris !
|
||||
|
||||
1_3_expand: >-
|
||||
Ceci n'est <strong>PAS</strong> un jeu incrémental et inactif ! Construisez plus d'extracteurs et de convoyeurs pour atteindre plus vite votre votre but.<br><br>Astuce: Gardez <strong>MAJ</strong> enfoncé pour placer plusieurs extracteurs, et utilisez <strong>R</strong> pour les faire pivoter.
|
||||
Ceci n’est <strong>PAS</strong> un jeu incrémental et inactif ! Construisez plus d’extracteurs et de convoyeurs pour atteindre plus vite votre but.<br><br>Astuce : Gardez <strong>MAJ</strong> enfoncé pour placer plusieurs extracteurs, et utilisez <strong>R</strong> pour les faire pivoter.
|
||||
|
||||
colors:
|
||||
red: Rouge
|
||||
@ -409,92 +410,92 @@ ingame:
|
||||
purple: Violet
|
||||
cyan: Cyan
|
||||
white: Blanc
|
||||
uncolored: Non coloré
|
||||
uncolored: Sans couleur
|
||||
black: Noir
|
||||
shapeViewer:
|
||||
title: Calques
|
||||
empty: Vide
|
||||
copyKey: Copier la clé de forme
|
||||
copyKey: Copier le raccourci de la forme
|
||||
|
||||
# All shop upgrades
|
||||
shopUpgrades:
|
||||
belt:
|
||||
name: Convoyeurs, Distributeurs et Tunnels
|
||||
description: Vitesse x<currentMult> → x<newMult>
|
||||
name: Convoyeurs, distributeurs et tunnels
|
||||
description: Vitesse ×<currentMult> → ×<newMult>
|
||||
|
||||
miner:
|
||||
name: Extraction
|
||||
description: Vitesse x<currentMult> → x<newMult>
|
||||
description: Vitesse ×<currentMult> → ×<newMult>
|
||||
|
||||
processors:
|
||||
name: Découpage, Rotation et Empilage
|
||||
description: Vitesse x<currentMult> → x<newMult>
|
||||
name: Découpage, rotation et empilage
|
||||
description: Vitesse ×<currentMult> → ×<newMult>
|
||||
|
||||
painting:
|
||||
name: Mélange et Peinture
|
||||
description: Vitesse x<currentMult> → x<newMult>
|
||||
name: Mélange et peinture
|
||||
description: Vitesse ×<currentMult> → ×<newMult>
|
||||
|
||||
# Buildings and their name / description
|
||||
buildings:
|
||||
belt:
|
||||
default:
|
||||
name: &belt Convoyeur
|
||||
description: Transporte les objects, maintenez et faites glisser pour en placer plusieurs.
|
||||
description: Transporte les objets, maintenez et faites glisser pour en placer plusieurs.
|
||||
|
||||
miner: # Internal name for the Extractor
|
||||
default:
|
||||
name: &miner Extracteur
|
||||
description: Placez-le au dessus d'une forme ou couleur pour l'extraire.
|
||||
description: Placez-le au-dessus d’une forme ou couleur pour l’extraire.
|
||||
|
||||
chainable:
|
||||
name: Extracteur en série
|
||||
description: Placez-le au dessus d'une forme ou couleur pour l'extraire. Peut être mis en série.
|
||||
description: Placez-le au-dessus d’une forme ou couleur pour l’extraire. Peut être mis en série.
|
||||
|
||||
underground_belt: # Internal name for the Tunnel
|
||||
default:
|
||||
name: &underground_belt Tunnel
|
||||
description: Permet de faire passer des ressources en dessous de bâtiment et de convoyeurs.
|
||||
description: Permet de faire passer des ressources sous les bâtiments et les convoyeurs.
|
||||
|
||||
tier2:
|
||||
name: Tunnel Niveau II
|
||||
description: Permet de faire passer des ressources en dessous de bâtiment et de convoyeurs.
|
||||
name: Tunnel niveau II
|
||||
description: Permet de faire passer des ressources sous les bâtiments et les convoyeurs.
|
||||
|
||||
splitter: # Internal name for the Balancer
|
||||
default:
|
||||
name: &splitter Répartiteur
|
||||
description: Multifonctionnel - Distribue de manière équitable toutes les entrées vers toutes les sorties.
|
||||
description: Multifonctions — Distribue équitablement toutes les entrées vers toutes les sorties.
|
||||
|
||||
compact:
|
||||
name: Fusionneur (compact)
|
||||
description: Fusionne deux convoyeurs en un.
|
||||
description: Fusionne deux convoyeurs en un seul.
|
||||
|
||||
compact-inverse:
|
||||
name: Fusionneur (compact)
|
||||
description: Fusionne deux convoyeurs en un.
|
||||
description: Fusionne deux convoyeurs en un seul.
|
||||
|
||||
cutter:
|
||||
default:
|
||||
name: &cutter Découpeur
|
||||
description: Coupe une forme de haut en bas et sort les deux parties. <strong>Si vous n'utilisez qu'une seule partie, assurez-vous de détruite l'autre ou sinon, gare au blocage !</strong>
|
||||
description: Coupe une forme de haut en bas et sort les deux parties. <strong>Si vous n’utilisez qu’une seule partie, assurez-vous de détruire l’autre ou sinon, gare au blocage !</strong>
|
||||
quad:
|
||||
name: Découpeur (Quatre)
|
||||
description: Coupe une forme en quatre parties. <strong>Si vous n'utilisez pas toutes les parties, assurez-vous de détruite les autres ou sinon, gare au blocage !</strong>
|
||||
name: Découpeur (quadruple)
|
||||
description: Coupe une forme en quatre parties. <strong>Si vous n’utilisez pas toutes les parties, assurez-vous de détruire les autres ou sinon, gare au blocage !</strong>
|
||||
|
||||
rotater:
|
||||
default:
|
||||
name: &rotater Pivoteur
|
||||
description: Fait pivoter une forme de 90 degrés vers la droite.
|
||||
description: Fait pivoter une forme de 90 degrés vers la droite.
|
||||
ccw:
|
||||
name: Pivoteur inversé
|
||||
description: Fait pivoter une forme de 90 degrés vers la gauche.
|
||||
description: Fait pivoter une forme de 90 degrés vers la gauche.
|
||||
fl:
|
||||
name: Retourneur
|
||||
description: Tourne la forme de 180 degrés.
|
||||
description: Tourne une forme de 180 degrés.
|
||||
|
||||
stacker:
|
||||
default:
|
||||
name: &stacker Combineur
|
||||
description: Combine deux formes. Si elles ne peuvent pas êtres combinées, la forme de droite est placée sur la forme de gauche.
|
||||
description: Combine deux formes. Si elles ne peuvent pas être combinées, la forme de droite est placée sur la forme de gauche.
|
||||
|
||||
mixer:
|
||||
default:
|
||||
@ -506,11 +507,11 @@ buildings:
|
||||
name: &painter Peintre
|
||||
description: &painter_desc Colorie entièrement la forme de gauche avec la couleur de droite.
|
||||
double:
|
||||
name: Peintre (Double)
|
||||
name: Peintre (double)
|
||||
description: Colorie les deux formes de gauche avec la couleur de droite.
|
||||
quad:
|
||||
name: Peintre (Quadruple)
|
||||
description: Permet de colorier chaque quadrant d'une forme avec une couleur différente.
|
||||
name: Peintre (quadruple)
|
||||
description: Colorie chaque quadrant d’une forme avec une couleur différente.
|
||||
mirrored:
|
||||
name: *painter
|
||||
description: *painter_desc
|
||||
@ -518,125 +519,125 @@ buildings:
|
||||
trash:
|
||||
default:
|
||||
name: &trash Poubelle
|
||||
description: Accepte des formes de n'importe quel côté et les détruit... pour toujours.
|
||||
description: Accepte des formes de n’importe quel côté et les détruit… pour toujours.
|
||||
|
||||
storage:
|
||||
name: Stockage
|
||||
description: Stocke les formes en trop jusqu'à une certaine capacité. Peut être utilisé comme tampon.
|
||||
description: Stocke les formes en trop jusqu’à une certaine capacité. Peut être utilisé pour absorber un surplus.
|
||||
hub:
|
||||
deliver: Délivrez
|
||||
deliver: Livrez
|
||||
toUnlock: pour débloquer
|
||||
levelShortcut: NV
|
||||
wire:
|
||||
default:
|
||||
name: Ligne énergétique
|
||||
description: Permet de transporter de l'énergie.
|
||||
name: Câble
|
||||
description: Permet de transporter de l’énergie.
|
||||
advanced_processor:
|
||||
default:
|
||||
name: Inverseur de couleur
|
||||
description: Accepte une couleur ou une forme et l'inverse.
|
||||
description: Accepte une couleur ou une forme, et l’inverse.
|
||||
energy_generator:
|
||||
deliver: Délivrer
|
||||
deliver: Livrer
|
||||
toGenerateEnergy: Pour
|
||||
default:
|
||||
name: Générateur d'énergie
|
||||
description: Genère de l'énergie en consommant des formes.
|
||||
name: Générateur d’énergie
|
||||
description: Génère de l’énergie en consommant des formes.
|
||||
wire_crossings:
|
||||
default:
|
||||
name: Duplicateur de ligne
|
||||
description: Sépare une ligne énergétique en deux.
|
||||
name: Duplicateur de câble
|
||||
description: Sépare un câble en deux.
|
||||
merger:
|
||||
name: Fusionneur de ligne
|
||||
description: Fusionne deux lignes énergétiques en une seule.
|
||||
name: Fusionneur de câble
|
||||
description: Fusionne deux câbles en un seul.
|
||||
|
||||
storyRewards:
|
||||
# Those are the rewards gained from completing the store
|
||||
reward_cutter_and_trash:
|
||||
title: Découper des formes
|
||||
desc: Vous venez de débloquer le <strong>découpeur</strong> - il coupe des formes en deux <strong>de haut en bas</strong> quel que soit son orientation !<br><br>Assurez-vous de vous débarasser des déchets, sinon <strong>gare au blocage</strong> - À cet effet, je mets à votre disposition la poubelle, qui détruit tout ce que vous y mettez !
|
||||
title: Découpage de formes
|
||||
desc: Vous venez de débloquer le <strong>découpeur</strong> — il coupe des formes en deux <strong>de haut en bas</strong> quelle que soit son orientation !<br><br>Assurez-vous de vous débarrasser des déchets, sinon <strong>gare au blocage</strong> — À cet effet, je mets à votre disposition la poubelle, qui détruit tout ce que vous y mettez !
|
||||
|
||||
reward_rotater:
|
||||
title: Rotation
|
||||
desc: Le <strong>pivoteur</strong> a été débloqué ! Il pivote les formes de 90 degrés vers la droite.
|
||||
desc: Le <strong>pivoteur</strong> a été débloqué ! Il pivote les formes de 90 degrés vers la droite.
|
||||
|
||||
reward_painter:
|
||||
title: Peintre
|
||||
desc: >-
|
||||
Le <strong>peintre</strong> a été débloqué - Extrayez des pigments de couleur (comme vous le faites avec les formes) et combinez les avec une forme dans un peintre pour les colorier !<br><br>PS: Si vous êtes daltonien, il y a un <strong>mode daltonien</strong> paramétrable dans les préférences !
|
||||
Le <strong>peintre</strong> a été débloqué — Extrayez des pigments de couleur (comme vous le faites avec les formes) et combinez-les avec une forme dans un peintre pour les colorier !<br><br>PS : Si vous êtes daltonien, il y a un <strong>mode daltonien</strong> paramétrable dans les préférences !
|
||||
|
||||
reward_mixer:
|
||||
title: Mélangeur de couleurs
|
||||
desc: Le <strong>mélangeur</strong> a été débloqué - Combinez deux couleurs en utilisant <strong>la synthèse additive des couleurs</strong> avec ce bâtiment !
|
||||
desc: Le <strong>mélangeur</strong> a été débloqué — Combinez deux couleurs en utilisant <strong>la synthèse additive des couleurs</strong> avec ce bâtiment !
|
||||
|
||||
reward_stacker:
|
||||
title: Combineur
|
||||
desc: Vous pouvez maintenant combiner deux formes avec le <strong>combineur</strong> ! Les deux entrées sont combinées et si elles ne peuvent êtres mises l'une à côté de l'autre, elles sont <strong>fusionnées</strong>. Sinon, la forme de droite est <strong>placée au dessus</strong> de la forme de gauche après avoir été légèrement réduite.
|
||||
desc: Vous pouvez maintenant combiner deux formes avec le <strong>combineur</strong> ! Les deux entrées sont combinées et si elles peuvent être mises l’une à côté de l’autre, elles sont <strong>fusionnées</strong>. Sinon, la forme de droite est <strong>placée au-dessus</strong> de la forme de gauche.
|
||||
|
||||
reward_splitter:
|
||||
title: Distributeur/Rassembleur
|
||||
desc: Le <strong>répartiteur</strong> multifonctionnel a été débloqué - Il peut être utilisé pour construire de plus grandes usines en <strong>distribuant équitablement et rassemblant les formes</strong> entre plusieurs convoyeurs !<br><br>
|
||||
title: Distributeur / rassembleur
|
||||
desc: Le <strong>répartiteur</strong> multifonctionnel a été débloqué — Il peut être utilisé pour construire de plus grandes usines en <strong>distribuant équitablement et rassemblant les formes</strong> entre plusieurs convoyeurs !<br><br>
|
||||
|
||||
reward_tunnel:
|
||||
title: Tunnel
|
||||
desc: Le <strong>tunnel</strong> a été débloqué - À présent il devient possible de faire passer des formes sous les convoyeurs et les bâtiments !
|
||||
desc: Le <strong>tunnel</strong> a été débloqué — Vous pouvez maintenant faire passer des formes sous les convoyeurs et les bâtiments !
|
||||
|
||||
reward_rotater_ccw:
|
||||
title: Pivoteur inversé
|
||||
desc: Vous avez débloqué une variante du <strong>pivoteur</strong> - Elle permet de faire pivoter vers la gauche ! Pour le construire, sélectionnez le pivoteur et <strong>appuyez sur 'T' pour alterner entre les variantes</strong> !
|
||||
desc: Vous avez débloqué une variante du <strong>pivoteur</strong> — Elle permet de faire pivoter vers la gauche ! Pour le construire, sélectionnez le pivoteur et <strong>appuyez sur 'T' pour alterner entre les variantes</strong> !
|
||||
|
||||
reward_miner_chainable:
|
||||
title: Extracteur en série
|
||||
desc: Vous avez débloqué <strong>l'extracteur en série</strong> ! Il permet de <strong>transférer ses resources</strong> à d'autres extracteurs pour augmenter le débit sortant !
|
||||
desc: Vous avez débloqué <strong>l’extracteur en série</strong> ! Il permet de <strong>transférer ses ressources</strong> à d’autres extracteurs pour augmenter le débit sortant !
|
||||
|
||||
reward_underground_belt_tier_2:
|
||||
title: Tunnel niveau II
|
||||
desc: Vous avez débloqué une nouvelle variante du <strong>tunnel</strong> - Elle a une <strong>portée plus grande</strong>, et vous pouvez à présent superposer les deux variantes de tunnels !
|
||||
desc: Vous avez débloqué une nouvelle variante du <strong>tunnel</strong> — Elle a une <strong>portée plus grande</strong>, et vous pouvez superposer les deux variantes de tunnels !
|
||||
|
||||
reward_splitter_compact:
|
||||
title: Répartiteur compact
|
||||
desc: >-
|
||||
Vous avez débloqué une variante compacte du <strong>répartiteur</strong> - Elle accepte deux entrées et les rassemble en une sortie !
|
||||
Vous avez débloqué une variante compacte du <strong>répartiteur</strong> — Elle accepte deux entrées et les rassemble en une sortie !
|
||||
|
||||
reward_cutter_quad:
|
||||
title: Quadruple découpeur
|
||||
desc: Vous avez débloqué une variante du <strong>découpeur</strong> - Elle permet de découper les formes en <strong>quatre parties</strong> à la place de simplement deux !
|
||||
desc: Vous avez débloqué une variante du <strong>découpeur</strong> — Elle permet de découper les formes en <strong>quatre parties</strong> à la place de simplement deux !
|
||||
|
||||
reward_painter_double:
|
||||
title: Double peintre
|
||||
desc: Vous avez débloqué une variante du <strong>peintre</strong> - Elle fonctionne comme le peintre de base, mais elle permet de traiter <strong>deux formes à la fois</strong> en ne consommant qu'une couleur au lieu de deux !
|
||||
desc: Vous avez débloqué une variante du <strong>peintre</strong> — Elle fonctionne comme le peintre de base, mais elle permet de traiter <strong>deux formes à la fois</strong> en ne consommant qu’une couleur au lieu de deux !
|
||||
|
||||
reward_painter_quad:
|
||||
title: Quadruple peintre
|
||||
desc: Vous avez débloqué une variante du <strong>peintre</strong> - Elle permet de colorier chaque partie d'une forme individuellement !
|
||||
desc: Vous avez débloqué une variante du <strong>peintre</strong> — Elle permet de colorier chaque partie d’une forme individuellement !
|
||||
|
||||
reward_storage:
|
||||
title: Tampon de stockage
|
||||
desc: Vous avez débloqué une variante de <strong>la poubelle</strong> - Elle permet de stocker des formes jusqu'à une certaine limite !
|
||||
desc: Vous avez débloqué une variante de <strong>la poubelle</strong> — Elle permet de stocker des formes jusqu’à une certaine limite !
|
||||
|
||||
reward_freeplay:
|
||||
title: Mode libre
|
||||
desc: Vous y êtes arrivé ! Vous avez débloqué le <strong>mode libre</strong> ! Cela veut dire que dorénavant, les formes sont générées aléatoirement ! (Ne vous en faites pas, plus de contenu est prévu pour la version complète !)
|
||||
desc: Vous y êtes arrivé ! Vous avez débloqué le <strong>mode libre</strong> ! Cela veut dire que dorénavant, les formes sont générées aléatoirement ! (Ne vous en faites pas, encore plus de contenu est prévu pour la version complète !)
|
||||
|
||||
reward_blueprints:
|
||||
title: Patrons
|
||||
desc: Vous pouvez maintenant <strong>copier et coller</strong> des parties de votre usines ! Sélectionnez une zone (Appuyez sur CTRL, et sélectionnez avec votre souris), et appuyez sur 'C' pour la copier.<br><br>Coller n'est <strong>pas gratuit</strong>, vous devez produire <strong>des formes de patrons</strong> pour vous le payer (les mêmes que celles que vous venez de livrer).
|
||||
desc: Vous pouvez maintenant <strong>copier et coller</strong> des parties de votre usine ! Sélectionnez une zone (Appuyez sur CTRL, et sélectionnez avec votre souris), et appuyez sur 'C' pour la copier.<br><br>Coller n’est <strong>pas gratuit</strong>, vous devez produire <strong>des formes de patrons</strong> pour vous le payer (les mêmes que celles que vous venez de livrer).
|
||||
|
||||
# Special reward, which is shown when there is no reward actually
|
||||
no_reward:
|
||||
title: Niveau suivant
|
||||
desc: >-
|
||||
Ce niveau n'a pas de récompense mais le prochain, oui ! <br><br>PS: Vous ne devriez pas détruire votre usine actuelle - Vous aurez besoin de <strong>toutes</strong> ces formes plus tard pour <strong>débloquer des améliorations</strong>
|
||||
Ce niveau n’a pas de récompense mais le prochain, si !<br><br>PS : Ne détruisez pas votre usine actuelle — Vous aurez besoin de <strong>toutes</strong> ces formes plus tard pour <strong>débloquer des améliorations</strong>.
|
||||
|
||||
no_reward_freeplay:
|
||||
title: Niveau suivant
|
||||
desc: >-
|
||||
Bravo ! À propos, plus de contenu est prévu pour la version complète !
|
||||
Bravo ! À propos, plus de contenu est prévu pour la version complète !
|
||||
|
||||
settings:
|
||||
title: Options
|
||||
categories:
|
||||
general: Général
|
||||
userInterface: Interface Utilisateur
|
||||
userInterface: Interface utilisateur
|
||||
advanced: Avancé
|
||||
performance: Performance
|
||||
|
||||
@ -648,26 +649,26 @@ settings:
|
||||
|
||||
labels:
|
||||
uiScale:
|
||||
title: Taille de l'interface
|
||||
title: Taille de l’interface
|
||||
description: >-
|
||||
Change la taille de l'interface utilisateur. Cette interface se redimensionnera suivant la résolution de votre appareil, mais cette option contrôle le facteur de résolution.
|
||||
Change la taille de l’interface utilisateur. Cette interface se redimensionnera suivant la résolution de votre écran, mais cette option contrôle le facteur de résolution.
|
||||
scales:
|
||||
super_small: Très petite
|
||||
small: Petite
|
||||
regular: Normale
|
||||
large: Large
|
||||
huge: Très large
|
||||
large: Grande
|
||||
huge: Très grande
|
||||
|
||||
scrollWheelSensitivity:
|
||||
title: Sensibilité du zoom
|
||||
description: >-
|
||||
Change la sensibilité du zoom (aussi bien de la roulette de la souris que du pavé tactile).
|
||||
Change la sensibilité du zoom (roulette de la souris et pavé tactile).
|
||||
sensitivity:
|
||||
super_slow: Super lent
|
||||
super_slow: Très lent
|
||||
slow: Lent
|
||||
regular: Normal
|
||||
fast: Rapide
|
||||
super_fast: Super rapide
|
||||
super_fast: Très rapide
|
||||
|
||||
fullscreen:
|
||||
title: Plein écran
|
||||
@ -687,7 +688,7 @@ settings:
|
||||
theme:
|
||||
title: Thème
|
||||
description: >-
|
||||
Choisissez votre thème (clair / sombre).
|
||||
Choisissez votre thème (clair / sombre).
|
||||
|
||||
themes:
|
||||
dark: Sombre
|
||||
@ -701,7 +702,7 @@ settings:
|
||||
alwaysMultiplace:
|
||||
title: Placement multiple
|
||||
description: >-
|
||||
Si activé, tous les bâtiments resterons sélectionnés tant que vous n'aurez pas annulé. Ceci revient à garder la touche SHIFT appuyée en permanence.
|
||||
Si activé, tous les bâtiments resteront sélectionnés tant que vous n’aurez pas annulé. Ceci revient à garder la touche MAJ appuyée en permanence.
|
||||
|
||||
offerHints:
|
||||
title: Indices
|
||||
@ -711,13 +712,13 @@ settings:
|
||||
language:
|
||||
title: Langue
|
||||
description: >-
|
||||
Change la langue. Toutes les traductions sont des contributions des utilisateurs et pourraient être partiellement incomplètes !
|
||||
Change la langue. Les traductions sont une contribution des utilisateurs et peuvent être incomplètes !
|
||||
|
||||
movementSpeed:
|
||||
title: Vitesse de déplacement
|
||||
description: Change la vitesse à laquelle l'écran se déplace lors de l'utilisation du clavier.
|
||||
description: Change la vitesse de déplacement de l’écran avec les touches clavier.
|
||||
speeds:
|
||||
super_slow: Super lent
|
||||
super_slow: Très lent
|
||||
slow: Lent
|
||||
regular: Normal
|
||||
fast: Rapide
|
||||
@ -728,75 +729,75 @@ settings:
|
||||
title: Tunnels intelligents
|
||||
description: >-
|
||||
Si cette option est sélectionnée, placer des tunnels effacera automatiquement les convoyeurs inutiles.
|
||||
Cela permet aussi d'étirer les tunnels et les tunnels en surnombre seront effacés.
|
||||
Cela permet aussi d’étirer les tunnels, et les tunnels en surnombre seront effacés.
|
||||
|
||||
vignette:
|
||||
title: Effet de vignette
|
||||
description: >-
|
||||
Permet l'affichage de l'effet de vignette qui assombrit les coins de l'écran afin de rendre le texte plus facile à lire.
|
||||
Permet l’affichage de l’effet de vignette qui assombrit les coins de l’écran afin de rendre le texte plus facile à lire.
|
||||
|
||||
autosaveInterval:
|
||||
title: Fréquence des sauvegardes automatiques
|
||||
description: >-
|
||||
Contrôle avec quelle fréquence le jeu sera sauvegardé automatiquement. Vous pouvez aussi entièrement désactiver cette fonctionnalité ici.
|
||||
intervals:
|
||||
one_minute: 1 Minute
|
||||
two_minutes: 2 Minutes
|
||||
five_minutes: 5 Minutes
|
||||
ten_minutes: 10 Minutes
|
||||
twenty_minutes: 20 Minutes
|
||||
one_minute: 1 minute
|
||||
two_minutes: 2 minutes
|
||||
five_minutes: 5 minutes
|
||||
ten_minutes: 10 minutes
|
||||
twenty_minutes: 20 minutes
|
||||
disabled: Désactivé
|
||||
|
||||
compactBuildingInfo:
|
||||
title: Informations réduites sur les bâtiments
|
||||
description: >-
|
||||
Raccourcit les panneaux d'information sur les bâtiments en n'affichant que les ratios. Dans le cas contraire, une description et une imagine sont présentés.
|
||||
Raccourcit les panneaux d’information sur les bâtiments en n’affichant que les ratios. Si désactivé, montre une description et une image.
|
||||
|
||||
disableCutDeleteWarnings:
|
||||
title: Désactive les avertissement pour Couper/Effacer
|
||||
title: Désactive les avertissements pour Couper / Effacer
|
||||
description: >-
|
||||
Désactive la boîte de dialogue qui s'affiche lorsque vous vous apprêtez à couper/effacer plus de 100 entités.
|
||||
Désactive la boîte de dialogue qui s’affiche lorsque vous vous apprêtez à couper / effacer plus de 100 entités.
|
||||
|
||||
enableColorBlindHelper:
|
||||
title: Mode Daltonien
|
||||
title: Mode daltonien
|
||||
description: Active divers outils qui permettent de jouer à ce jeu si vous êtes daltonien.
|
||||
|
||||
rotationByBuilding:
|
||||
title: Rotation par catégorie de bâtiment
|
||||
description: >-
|
||||
Chaque catégorie de bâtiment enregistre le sens de rotation que vous lui avez assigné la dernière fois, de manière individuelle.
|
||||
Cela sera sans doute plus confortable si vous alternez fréquemment entre le placement de différents types de bâtiments.
|
||||
Cela sera sans doute plus agréable si vous alternez fréquemment entre le placement de différents types de bâtiments.
|
||||
|
||||
lowQualityMapResources:
|
||||
title: Ressources de la carte de plus basse qualité
|
||||
description: >-
|
||||
Simplifie le rendu des ressources sur la carte lorsqu'elle est zoomée opur améliorer les performances.
|
||||
C'est encore plus clean, n'oubliez pas d'essayer !
|
||||
Simplifie le rendu des ressources sur la carte lorsqu’elle est zoomée pour améliorer les performances.
|
||||
Ça donne un rendu encore plus propre, alors essayez-le !
|
||||
|
||||
disableTileGrid:
|
||||
title: Desactiver la grille de placement
|
||||
title: Désactiver la grille de placement
|
||||
description: >-
|
||||
Desactiver la grille de placement peut aider les performances. Ça rend aussi le jeu encore plus uni!
|
||||
Désactiver la grille de placement peut améliorer les performances. Ça rend aussi l’apparence plus unie !
|
||||
|
||||
clearCursorOnDeleteWhilePlacing:
|
||||
title: Effacer le curseur avec clic droit
|
||||
title: Déselectionner avec le clic droit
|
||||
description: >-
|
||||
Activé par défaut, efface le curseur lorsque vous faites un clic droit en ayant un bâtiment selectioné pour la constructio. Si desactivé, vous pouvez detruire les bâtiments en faisant un clic droit tout en placant un bâtiment.
|
||||
Activé par défaut. Désélectionne le bâtiment choisi pour la construction lorsque vous faites un clic droit sur un bâtiment existant. Si désactivé, vous pouvez détruire des bâtiments avec un clic droit puis continuer de placer le bâtiment sélectionné.
|
||||
|
||||
lowQualityTextures:
|
||||
title: Textures de basse résolution (Moche)
|
||||
title: Textures de basse résolution (moche)
|
||||
description: >-
|
||||
Utilise des textures de basse qualité pour augmenter les performances. Cela va rendre le jeu moche!
|
||||
Utilise des textures de basse qualité pour améliorer les performances. Rend le jeu très moche !
|
||||
|
||||
displayChunkBorders:
|
||||
title: Monter les bordures de chunks
|
||||
title: Monter les secteurs
|
||||
description: >-
|
||||
Le jeu est divisé en parties de 16x16 cases, si ce réglage est activé, les bordures de chaque partie sont affichées.
|
||||
Le jeu est divisé en secteurs de 16×16 cases. Si ce réglage est activé, les limites de chaque secteur sont affichées.
|
||||
|
||||
keybindings:
|
||||
title: Contrôles
|
||||
hint: >-
|
||||
Astuce: Soyez sûr d'utiliser CTRL, SHIFT et ALT ! Ces touches activent différentes options de placement.
|
||||
Astuce : N’oubliez pas d’utiliser CTRL, MAJ et ALT ! Ces touches activent différentes options de placement.
|
||||
|
||||
resetKeybindings: Réinitialiser les contrôles
|
||||
|
||||
@ -805,7 +806,7 @@ keybindings:
|
||||
ingame: Jeu
|
||||
navigation: Navigation
|
||||
placement: Placement
|
||||
massSelect: Suppression de zone
|
||||
massSelect: Sélection d’une zone
|
||||
buildings: Raccourcis bâtiment
|
||||
placementModifiers: Modificateurs de placement
|
||||
|
||||
@ -825,8 +826,8 @@ keybindings:
|
||||
menuOpenShop: Améliorations
|
||||
menuOpenStats: Statistiques
|
||||
|
||||
toggleHud: Basculer l'affichage tête haute (ATH)
|
||||
toggleFPSInfo: Basculer l'affichage des IPS (itérations par seconde) et des informations de débogage
|
||||
toggleHud: Basculer l’affichage tête haute (ATH)
|
||||
toggleFPSInfo: Basculer l’affichage des IPS (itérations par seconde) et des informations de débogage
|
||||
belt: *belt
|
||||
splitter: *splitter
|
||||
underground_belt: *underground_belt
|
||||
@ -840,49 +841,49 @@ keybindings:
|
||||
|
||||
rotateWhilePlacing: Pivoter
|
||||
rotateInverseModifier: >-
|
||||
Variante: Pivote à gauche
|
||||
Variante : Pivote à gauche
|
||||
cycleBuildingVariants: Alterner entre les variantes
|
||||
confirmMassDelete: Confirmer la suppression de la sélection
|
||||
cycleBuildings: Alterner entre les bâtiments
|
||||
|
||||
massSelectStart: Cliquez et maintenez pour commencer
|
||||
massSelectStart: Cliquez et glissez pour commencer
|
||||
massSelectSelectMultiple: Sélectionner plusieurs zones
|
||||
massSelectCopy: Copier la sélection
|
||||
|
||||
placementDisableAutoOrientation: Désactiver l'orientation automatique
|
||||
placementDisableAutoOrientation: Désactiver l’orientation automatique
|
||||
placeMultiple: Rester en mode placement
|
||||
placeInverse: Inverser le mode d'orientation automatique
|
||||
placeInverse: Inverser le mode d’orientation automatique
|
||||
pasteLastBlueprint: Copier le dernier patron
|
||||
massSelectCut: Couper la sélection
|
||||
exportScreenshot: Exporter toute la base en tant qu'image.
|
||||
exportScreenshot: Exporter une image de toute la base
|
||||
mapMoveFaster: Se déplacer plus vite
|
||||
lockBeltDirection: Utiliser le plannificateur de convoyeurs
|
||||
switchDirectionLockSide: "Plannificateur: changer de côté"
|
||||
lockBeltDirection: Utiliser le planificateur de convoyeurs
|
||||
switchDirectionLockSide: "Planificateur : changer de côté"
|
||||
pipette: Pipette
|
||||
menuClose: Fermer le menu
|
||||
switchLayers: Échanger les calques
|
||||
switchLayers: Basculer le calque
|
||||
advanced_processor: Inverseur de couleur
|
||||
energy_generator: Générateur d'énergie
|
||||
wire: Ligne énergétique
|
||||
energy_generator: Générateur d’énergie
|
||||
wire: Câble
|
||||
|
||||
about:
|
||||
title: À propos de ce jeu
|
||||
body: >-
|
||||
Ce jeu est open source et développé par <a href="https://github.com/tobspr"
|
||||
target="_blank">Tobias Springer</a> (c'est moi).<br><br>
|
||||
target="_blank">Tobias Springer</a> (c’est moi).<br><br>
|
||||
|
||||
Si vous souhaitez contribuer, allez voir <a href="<githublink>"
|
||||
target="_blank">shapez.io sur github</a>.<br><br>
|
||||
|
||||
Ce jeu n'aurait pu être réalisé sans la précieuse communauté Discord autour de
|
||||
mes jeux - Vous devriez vraiment envisager de joindre le <a href="<discordlink>"
|
||||
target="_blank">serveur Discord</a> !<br><br>
|
||||
Ce jeu n’aurait pas pu être réalisé sans la précieuse communauté Discord autour de
|
||||
mes jeux — Vous devriez vraiment rejoindre le <a href="<discordlink>"
|
||||
target="_blank">serveur Discord</a> !<br><br>
|
||||
|
||||
La bande son a été créée par <a href="https://soundcloud.com/pettersumelius"
|
||||
target="_blank">Peppsen</a> - Il est impressionnant !<br><br>
|
||||
target="_blank">Peppsen</a> — Il est génial !<br><br>
|
||||
|
||||
Pour terminer, un immense merci à mon meilleur amis <a
|
||||
href="https://github.com/niklas-dahl" target="_blank">Niklas</a> - Sans nos sessions sur factorio, ce jeu n'aurait jamais existé.
|
||||
Pour terminer, un immense merci à mon meilleur ami <a
|
||||
href="https://github.com/niklas-dahl" target="_blank">Niklas</a> — Sans nos sessions sur Factorio, ce jeu n’aurait jamais existé.
|
||||
|
||||
changelog:
|
||||
title: Historique
|
||||
@ -893,7 +894,7 @@ demo:
|
||||
importingGames: Importer des sauvegardes
|
||||
oneGameLimit: Limité à une sauvegarde
|
||||
customizeKeybindings: Personnalisation des contrôles
|
||||
exportingBase: Exporter toute la base en tant qu'image
|
||||
exportingBase: Exporter une image de toute la base
|
||||
|
||||
settingNotAvailable: Indisponible dans la démo.
|
||||
#
|
||||
|
||||
@ -22,10 +22,10 @@
|
||||
---
|
||||
steamPage:
|
||||
# This is the short text appearing on the steam page
|
||||
shortText: shapez.io adalah permainan membangun pabrik-pabrik dengan tujuan untuk mengautomatiskan pembentukan dan pemrosesan bentuk-bentuk yang bertambah semakin kompleks di dalam area permainan yang meluas secara tak terhingga.
|
||||
shortText: Shapez.io adalah game tentang membangun pabrik untuk mengotomatiskan pembuatan dan pemrosesan bentuk-bentuk yang semakin kompleks di peta yang meluas tanpa batas.
|
||||
|
||||
# This is the text shown above the discord link
|
||||
discordLink: Tautan Resmi Discord – Obrol dengan saya!
|
||||
discordLink: Tautan Resmi Discord – Mari mengobrol dengan saya!
|
||||
# This is the long description for the steam page - It is contained here so you can help to translate it, and I will regulary update the store page.
|
||||
# NOTICE:
|
||||
# - Do not translate the first line (This is the gif image at the start of the store)
|
||||
@ -33,13 +33,13 @@ steamPage:
|
||||
longText: >-
|
||||
[img]{STEAM_APP_IMAGE}/extras/store_page_gif.gif[/img]
|
||||
|
||||
shapez.io adalah permainan membangun pabrik-pabrik dengan tujuan untuk mengautomatiskan pembentukan dan pemrosesan bentuk-bentuk yang bertambah semakin kompleks di dalam area permainan yang meluas secara tak terhingga.
|
||||
shapez.io adalah permainan membangun pabrik dengan tujuan untuk mengautomatiskan pembentukan dan pemrosesan bentuk-bentuk yang bertambah semakin kompleks di peta yang meluas tanpa batas.
|
||||
|
||||
Setelah pengiriman bentuk-bentuk yang diminta, Anda akan maju dalam permainan dan membuka tingkatan versi-versi mesin selanjutnya untuk mempercepat pabrik Anda.
|
||||
|
||||
Seiring meningkatnya kesulitan dari bentuk-bentuk yang diminta, Anda harus meningkatkan pabrik Anda untuk mengatasi kesulitan tersebut – Jangan lupa dengan sumber-sumber daya, Anda harus memperluas ke seluruh [b]area yang tidak terbatas[/b]!
|
||||
Seiring meningkatnya kesulitan dari bentuk-bentuk yang diminta, Anda harus meningkatkan pabrik Anda untuk mengatasi kesulitan tersebut – Jangan lupa dengan sumber daya, Anda harus memperluas ke seluruh [b]area yang tidak terbatas[/b]!
|
||||
|
||||
Kemudian Anda harus mencampurkan warna-warna dan mencat bentuk-bentuk dengannya – Campurkan merah, hijau, dan biru untuk memproduksi warna-warna lain dan mencat bentuk-bentuk dengannya untuk memenuhi permintaan.
|
||||
Kemudian Anda harus mencampurkan warna-warna dan mencat bentuk-bentuk tersebut – Campurkan merah, hijau, dan biru untuk memproduksi warna-warna lain dan mencat bentuk-bentuk tersebut untuk memenuhi permintaan.
|
||||
|
||||
Permainan ini mempunyai 18 level-level progresif (yang mana akan membuat Anda sibuk berjam-jam!), akan tetapi saya akan terus menambahkan konten-konten baru – Ada banyak yang direncanakan!
|
||||
|
||||
@ -48,16 +48,16 @@ steamPage:
|
||||
[b]Keuntungan Versi Penuh[/b]
|
||||
|
||||
[list]
|
||||
[*] Versi Permainan Gelap
|
||||
[*] Mode Malam
|
||||
[*] Titik Arah Tak Terhingga
|
||||
[*] Penyimpanan Permainan Tak Terhingga
|
||||
[*] Pengaturan-pengaturan Tambahan
|
||||
[*] Akan datang: Kawat & Energi! Akan dicoba untuk dicapai untuk (kira-kira) akhir Juli 2020.
|
||||
[*] Akan datang: Kawat & Energi! Akan dicoba untuk dicapai sekitar akhir Juli 2020.
|
||||
[*] Akan datang: Level-level tambahan
|
||||
[*] Memperkenankan saya untuk terus mengembangkan shapez.io ❤️
|
||||
[*] Mendukung saya untuk terus mengembangkan shapez.io ❤️
|
||||
[/list]
|
||||
|
||||
[b]Pembaruan di Masa Depan[/b]
|
||||
[b]Pembaruan di masa yang akan datang[/b]
|
||||
|
||||
Saya seringkali membarui permainan ini dan terus mencoba untuk menciptakan pembaruan paling sedikit sekali seminggu!
|
||||
|
||||
@ -87,8 +87,8 @@ steamPage:
|
||||
[/list]
|
||||
|
||||
global:
|
||||
loading: Memuat
|
||||
error: Terdapat kesalahan
|
||||
loading: Sedang memuat
|
||||
error: Terjadi kesalahan
|
||||
|
||||
# How big numbers are rendered, e.g. "10,000"
|
||||
thousandsDivider: ","
|
||||
@ -98,9 +98,9 @@ global:
|
||||
|
||||
# The suffix for large numbers, e.g. 1.3k, 400.2M, etc.
|
||||
suffix:
|
||||
thousands: k
|
||||
millions: M
|
||||
billions: B
|
||||
thousands: K
|
||||
millions: J
|
||||
billions: M
|
||||
trillions: T
|
||||
|
||||
# Shown for infinitely big numbers
|
||||
@ -108,13 +108,13 @@ global:
|
||||
|
||||
time:
|
||||
# Used for formatting past time dates
|
||||
oneSecondAgo: satu detik yang lalu
|
||||
oneSecondAgo: sedetik yang lalu
|
||||
xSecondsAgo: <x> detik yang lalu
|
||||
oneMinuteAgo: satu menit yang lalu
|
||||
oneMinuteAgo: semenit yang lalu
|
||||
xMinutesAgo: <x> menit yang lalu
|
||||
oneHourAgo: satu jam yang lalu
|
||||
oneHourAgo: sejam yang lalu
|
||||
xHoursAgo: <x> jam yang lalu
|
||||
oneDayAgo: satu hari yang lalu
|
||||
oneDayAgo: sehari yang lalu
|
||||
xDaysAgo: <x> hari yang lalu
|
||||
|
||||
# Short formats for times, e.g. '5h 23m'
|
||||
@ -142,7 +142,7 @@ mainMenu:
|
||||
play: Mulai Permainan
|
||||
continue: Lanjutkan Permainan
|
||||
newGame: Permainan Baru
|
||||
changelog: Ganti Data Log
|
||||
changelog: Catatan Perubahan
|
||||
subreddit: Reddit
|
||||
importSavegame: Impor Data Simpanan
|
||||
openSourceHint: Permainan ini bekerja secara open source!
|
||||
@ -166,7 +166,7 @@ dialogs:
|
||||
restart: Mulai Ulang
|
||||
reset: Setel Ulang
|
||||
getStandalone: Dapatkan Versi Penuh
|
||||
deleteGame: Saya tahu apa yang saya kerjakan
|
||||
deleteGame: Saya tahu apa yang saya lakukan
|
||||
viewUpdate: Tampilkan Pembaruan
|
||||
showUpgrades: Tunjukkan Tingkatan
|
||||
showKeybindings: Tunjukan Tombol Pintas
|
||||
|
||||
@ -87,7 +87,7 @@ steamPage:
|
||||
|
||||
global:
|
||||
loading: Laden
|
||||
error: Error
|
||||
error: Fout
|
||||
|
||||
# How big numbers are rendered, e.g. "10,000"
|
||||
thousandsDivider: "."
|
||||
@ -172,7 +172,7 @@ dialogs:
|
||||
showKeybindings: Zie Sneltoetsen
|
||||
|
||||
importSavegameError:
|
||||
title: Importeer error
|
||||
title: Importeerfout
|
||||
text: >-
|
||||
Het importeren van je savegame is mislukt:
|
||||
|
||||
@ -322,7 +322,7 @@ ingame:
|
||||
# <level> is replaced by the actual level, so this gets 'Level 03' for example.
|
||||
levelTitle: Level <level>
|
||||
completed: Voltooid
|
||||
unlockText: Ontgrendeld <reward>!
|
||||
unlockText: <reward> ontgrendeld!
|
||||
buttonNextLevel: Volgende Level
|
||||
|
||||
# Notifications on the lower right
|
||||
@ -633,9 +633,9 @@ storyRewards:
|
||||
settings:
|
||||
title: Opties
|
||||
categories:
|
||||
general: General
|
||||
userInterface: User Interface
|
||||
advanced: Advanced
|
||||
general: Algemeen
|
||||
userInterface: Opmaak
|
||||
advanced: Geavanceerd
|
||||
|
||||
versionBadges:
|
||||
dev: Ontwikkeling
|
||||
@ -795,8 +795,8 @@ keybindings:
|
||||
menuOpenShop: Upgrades
|
||||
menuOpenStats: Statistieken
|
||||
|
||||
toggleHud: Toggle HUD
|
||||
toggleFPSInfo: Toggle FPS en Debug Info
|
||||
toggleHud: Schakel HUD
|
||||
toggleFPSInfo: Schakel FPS en Debug Info
|
||||
belt: *belt
|
||||
splitter: *splitter
|
||||
underground_belt: *underground_belt
|
||||
|
||||
@ -22,7 +22,7 @@
|
||||
---
|
||||
steamPage:
|
||||
# This is the short text appearing on the steam page
|
||||
shortText: shapez.io é um jogo sobre construir fábricas, automatizando a criação e combinação de formas cada vez mais complexas num mapa infinito.
|
||||
shortText: Shapez.io é um jogo sobre construir fábricas, automatizando a criação e combinação de formas cada vez mais complexas num mapa infinito.
|
||||
|
||||
# This is the text shown above the Discord link
|
||||
discordLink: Discord Oficial - Converse comigo!
|
||||
@ -34,17 +34,17 @@ steamPage:
|
||||
longText: >-
|
||||
[img]{STEAM_APP_IMAGE}/extras/store_page_gif.gif[/img]
|
||||
|
||||
shapez.io é um jogo sobre construir fábricas, automatizando a criação e combinação de formas cada vez mais complexas num mapa infinito.
|
||||
Shapez.io é um jogo sobre construir fábricas, automatizando a criação e combinação de formas cada vez mais complexas num mapa infinito.
|
||||
|
||||
Após a entrega das formas requisitadas você progredirá no jogo e desbloqueará melhorias para acelerar sua fábrica.
|
||||
Após a entrega das formas requisitadas, você avançará no jogo e desbloqueará melhorias para acelerar sua produção.
|
||||
|
||||
Conforme sua demanda por formas aumenta, você irá que aumentar sua fábrica para alcançar-la - Mas não se esqueça dos recursos, você precisará expandir pelo [b]mapa infinito[/b]!
|
||||
Conforme sua demanda por formas aumenta, você terá que aumentar sua fábrica para alcançá-la - Mas não se esqueça dos recursos, você precisará expandir pelo [b]mapa infinito[/b]!
|
||||
|
||||
Rapidamente você vai ter que misturar cores e pintar suas formas com elas - Combine recursos vermelhos, verdes e azuis para produzir cores diferentes e pintar formas com elas para satisfazer a demanda.
|
||||
Em pouco tempo você terá que misturar cores e pintar suas formas com elas - Combine recursos vermelhos, verdes e azuis para produzir cores diferentes e pintar formas com elas para satisfazer a demanda.
|
||||
|
||||
O jogo contém 18 níveis progressivos (Que já devem manter você ocupado por horas!) mas eu adiciono novo contéudo constantemente - Tem bastante coisa já planejada!
|
||||
O jogo contém 18 níveis progressivos (que já devem manter você ocupado por horas!) mas eu adiciono novo contéudo constantemente - Tem bastante coisa já planejada!
|
||||
|
||||
Comprando o jogo você terá acesso à versão completa, que contém recursos adicionais, e além disso você também terá acesso aos recursos que seram desenvolvidos.
|
||||
Comprando o jogo você terá acesso à versão completa, que contém recursos adicionais, e além disso você também terá acesso aos recursos que serão desenvolvidos.
|
||||
|
||||
[b]Vantagens da versão completa[/b]
|
||||
|
||||
@ -60,15 +60,15 @@ steamPage:
|
||||
|
||||
[b]Atualizações Futuras[/b]
|
||||
|
||||
Eu lanço atualizações frequentemente e estou tentando lançar pelo menos um por semana!
|
||||
Eu lanço atualizações frequentemente e estou tentando lançar pelo menos uma por semana!
|
||||
|
||||
[list]
|
||||
[*] Mapas diferentes e desafios (por exemplo mapas com obstáculos)
|
||||
[*] Puzzles (Entregue a forma pedida com uma área restringida ou um certo conjunto de construções)
|
||||
[*] Um modo história onde as construções têm um custo
|
||||
[*] Um geredor de mapa customizável (Configure recursos, forma, tamanho, densidade, semente e mais)
|
||||
[*] Um gerador de mapa customizável (configure recursos, formas, tamanho, densidade, semente e mais)
|
||||
[*] Mais tipos de formas
|
||||
[*] Melhorias de desempenho (O jogo já roda bem!)
|
||||
[*] Melhorias de desempenho (o jogo já roda bem!)
|
||||
[*] E muito mais!
|
||||
[/list]
|
||||
|
||||
@ -130,8 +130,8 @@ global:
|
||||
control: CTRL
|
||||
alt: ALT
|
||||
escape: ESC
|
||||
shift: SHIFT
|
||||
space: ESPAÇO
|
||||
shift: Shift
|
||||
space: Espaço
|
||||
|
||||
demoBanners:
|
||||
# This is the "advertisement" shown in the main menu and other various places
|
||||
@ -145,7 +145,7 @@ mainMenu:
|
||||
newGame: Novo jogo
|
||||
changelog: Changelog
|
||||
subreddit: Reddit
|
||||
importSavegame: Importar
|
||||
importSavegame: Importar save
|
||||
openSourceHint: Esse jogo tem código aberto!
|
||||
discordLink: Discord oficial
|
||||
helpTranslate: Ajude a traduzir!
|
||||
@ -183,7 +183,7 @@ dialogs:
|
||||
Seu jogo salvo foi importado com sucesso.
|
||||
|
||||
gameLoadFailure:
|
||||
title: Jogo salvo quebrado
|
||||
title: Jogo salvo corrompido
|
||||
text: >-
|
||||
Houve uma falha ao carregar seu jogo salvo:
|
||||
|
||||
@ -198,13 +198,13 @@ dialogs:
|
||||
Houve uma falha ao deletar seu jogo salvo:
|
||||
|
||||
restartRequired:
|
||||
title: Ação necessária
|
||||
title: Reinicialização necessária
|
||||
text: >-
|
||||
Você precisa reiniciar o jogo para aplicar as mudanças.
|
||||
|
||||
editKeybinding:
|
||||
title: Alterar tecla
|
||||
desc: Pressiona a tecla que deseja vincular, ou ESC para cancelar.
|
||||
desc: Pressione a tecla que deseja vincular, ou ESC para cancelar.
|
||||
|
||||
resetKeybindingsConfirmation:
|
||||
title: Resetar controles
|
||||
@ -234,17 +234,17 @@ dialogs:
|
||||
O guia de melhorias pode ser encontrado no canto superior direito da tela.
|
||||
|
||||
massDeleteConfirm:
|
||||
title: Deletar
|
||||
title: Deletar?
|
||||
desc: >-
|
||||
Você está deletando vários objetos (<count> para ser exato)! Você quer continuar?
|
||||
|
||||
massCutConfirm:
|
||||
title: Confirmar corte
|
||||
title: Confirmar corte?
|
||||
desc: >-
|
||||
Você está cortando vários objetos (<count> para ser exato)! Você quer continuar?
|
||||
|
||||
massCutInsufficientConfirm:
|
||||
title: Confirmar Corte
|
||||
title: Confirmar Corte?
|
||||
desc: >-
|
||||
You can not afford to paste this area! Are you sure you want to cut it?
|
||||
|
||||
@ -290,8 +290,8 @@ ingame:
|
||||
createMarker: Criar marcador
|
||||
delete: Destruir
|
||||
pasteLastBlueprint: Colar último projeto
|
||||
lockBeltDirection: Ativar Planejador de Esteiras
|
||||
plannerSwitchSide: Girar Planejador
|
||||
lockBeltDirection: Ativar Planejamento de Esteiras
|
||||
plannerSwitchSide: Girar Planejamento
|
||||
cutSelection: Cortar
|
||||
copySelection: Copiar
|
||||
clearSelection: Limpar Seleção
|
||||
@ -363,13 +363,13 @@ ingame:
|
||||
dataSources:
|
||||
stored:
|
||||
title: Estoque
|
||||
description: Exibindo a quantidade de formas armazenadas em sua construção central.
|
||||
description: Exibindo a quantidade de formas armazenadas no seu HUB.
|
||||
produced:
|
||||
title: Produção
|
||||
description: Exibindo todas as formas que toda a sua fábrica produz, incluindo produtos intermediários..
|
||||
delivered:
|
||||
title: Entregue
|
||||
description: Exibindo formas entregues na sua construção central.
|
||||
description: Exibindo formas entregues no seu HUB.
|
||||
noShapesProduced: Nenhuma forma foi produzida até o momento.
|
||||
|
||||
# Displays the shapes per minute, e.g. '523 / m'
|
||||
@ -384,14 +384,14 @@ ingame:
|
||||
|
||||
buttons:
|
||||
continue: Continuar
|
||||
settings: Definições
|
||||
settings: Configurações
|
||||
menu: Voltar ao menu
|
||||
|
||||
# Bottom left tutorial hints
|
||||
tutorialHints:
|
||||
title: Quer ajuda?
|
||||
showHint: Mostrar dica
|
||||
hideHint: Fechar
|
||||
hideHint: Esconder dica
|
||||
|
||||
# When placing a blueprint
|
||||
blueprintPlacer:
|
||||
@ -401,7 +401,7 @@ ingame:
|
||||
waypoints:
|
||||
waypoints: Marcadores
|
||||
hub: HUB
|
||||
description: Clique com o botão esquerdo do mouse em um marcador para pular, clique com o botão direito do mouse para excluí-lo. <br><br> Pressione <keybinding> para criar um marcador a partir da exibição atual ou <strong>clique com o botão direito do mouse</strong> para criar um marcador no local selecionado.
|
||||
description: Clique com o botão esquerdo do mouse em um marcador para pular, clique com o botão direito do mouse para excluí-lo. <br><br> Pressione <keybinding> para criar um marcador à partir da exibição atual ou <strong>clique com o botão direito do mouse</strong> para criar um marcador no local selecionado.
|
||||
creationSuccessNotification: Marcador criado.
|
||||
|
||||
# Shape viewer
|
||||
@ -419,7 +419,7 @@ ingame:
|
||||
Conecte o extrator com uma <strong>esteira transportadora</strong> até a sua base!<br><br>Dica, <strong>clique e arraste</strong> a esteira com o mouse!
|
||||
|
||||
1_3_expand: >-
|
||||
Este <strong>NÃO</strong> é um jogo inativo! Construa mais extratores e esteiras para concluir o objetivo mais rapidamente.<br><br>Dica, segure <strong>SHIFT</strong> para colocar vários extratores e use <strong>R</strong> para girá-los.
|
||||
Este <strong>NÃO</strong> é um jogo idle! Construa mais extratores e esteiras para concluir o objetivo mais rapidamente.<br><br>Dica, segure <strong>SHIFT</strong> para colocar vários extratores e use <strong>R</strong> para girá-los.
|
||||
|
||||
# All shop upgrades
|
||||
shopUpgrades:
|
||||
@ -433,7 +433,7 @@ shopUpgrades:
|
||||
name: Corte, Rotação e Montagem
|
||||
description: Velocidade x<currentMult> → x<newMult>
|
||||
painting:
|
||||
name: Mistura e Pintura
|
||||
name: Mistura de cores e Pintura
|
||||
description: Velocidade x<currentMult> → x<newMult>
|
||||
|
||||
# Buildings and their name / description
|
||||
@ -469,7 +469,7 @@ buildings:
|
||||
|
||||
tier2:
|
||||
name: Túnel Classe II
|
||||
description: Permite transportar recursos por baixo de construções e esteiras.
|
||||
description: Permite transportar recursos por baixo de construções e outras esteiras.
|
||||
|
||||
splitter: # Internal name for the Balancer
|
||||
default:
|
||||
@ -541,7 +541,7 @@ buildings:
|
||||
|
||||
storage:
|
||||
name: Estoque
|
||||
description: Armazena itens em excesso, até uma determinada capacidade. Pode ser usado como uma porta de transbordamento.
|
||||
description: Armazena itens em excesso, até uma determinada capacidade. Pode ser usado como uma eclusa.
|
||||
|
||||
energy_generator:
|
||||
deliver: Entregar
|
||||
@ -575,7 +575,7 @@ storyRewards:
|
||||
reward_painter:
|
||||
title: Pintura
|
||||
desc: >-
|
||||
O <strong>Pintor</strong> foi desbloqueado - Extrai alguns pigmentos coloridos (assim como você fez com as formas) e combina-os com uma forma no pintor para os colorir!<br><br>PS: Se for daltônico, existe um <strong>modo daltônico</strong> nas definições!
|
||||
O <strong>Pintor</strong> foi desbloqueado - Extraia alguns pigmentos coloridos (assim como você fez com as formas) e combine-os com uma forma no pintor para colorí-las!<br><br>PS: Se for daltônico, existe um <strong>modo daltônico</strong> nas definições!
|
||||
|
||||
reward_mixer:
|
||||
title: Misturando cores
|
||||
@ -591,7 +591,7 @@ storyRewards:
|
||||
|
||||
reward_tunnel:
|
||||
title: Túnel
|
||||
desc: O <strong>túnel</strong> foi desbloqueado - Agora você pode canalizar itens sob construções!
|
||||
desc: O <strong>túnel</strong> foi desbloqueado - Agora você pode transportar itens abaixo do solo!
|
||||
|
||||
reward_rotater_ccw:
|
||||
title: Rotação anti-horária
|
||||
@ -646,7 +646,7 @@ storyRewards:
|
||||
Parabéns! Aliás, mais conteúdo vindo na versão completa!
|
||||
|
||||
settings:
|
||||
title: opções
|
||||
title: Opções
|
||||
categories:
|
||||
general: Geral
|
||||
userInterface: Interface de Usuário
|
||||
@ -671,7 +671,7 @@ settings:
|
||||
huge: Gigante
|
||||
|
||||
autosaveInterval:
|
||||
title: Intervalo de gravação automática
|
||||
title: Intervalo de save automático
|
||||
description: >-
|
||||
Controla a frequência com que o jogo salva automaticamente. Você também pode desativá-lo totalmente aqui.
|
||||
|
||||
@ -714,7 +714,7 @@ settings:
|
||||
enableColorBlindHelper:
|
||||
title: Modo daltônico.
|
||||
description: >-
|
||||
Permite várias ferramentas que permitem jogar se você é daltônico.
|
||||
Habilita várias ferramentas que te permitem jogar se você é daltônico.
|
||||
|
||||
fullscreen:
|
||||
title: Tela Cheia
|
||||
@ -745,24 +745,24 @@ settings:
|
||||
Se você possui um monitor de 144 hz, altere a taxa de atualização aqui para que o jogo seja simulado corretamente com taxas de atualização mais altas. Isso diminuir o FPS consideravelmente se o computador for muito lento.
|
||||
|
||||
alwaysMultiplace:
|
||||
title: Multiplicidade
|
||||
title: Posicionamento Múltiplo
|
||||
description: >-
|
||||
Se ativado, todas as construções permanecerão selecionadas após o posicionamento até que você a cancele. Isso é equivalente a pressionar SHIFT permanentemente.
|
||||
|
||||
offerHints:
|
||||
title: Dicas e tutoriais
|
||||
title: Dicas e Tutoriais
|
||||
description: >-
|
||||
Se ativado, oferece dicas e tutoriais enquanto se joga. Além disso, esconde certos elementos da interface até certo ponto, para facilitar o começo do jogo.
|
||||
|
||||
enableTunnelSmartplace:
|
||||
title: Túneis inteligentes
|
||||
title: Túneis Inteligentes
|
||||
description: >-
|
||||
Quando colocados, irão remover automaticamente esteiras desnecessárias. Isso também permite arrastar túneis e túneis em excesso serão removidos.
|
||||
|
||||
vignette:
|
||||
title: Vinheta
|
||||
description: >-
|
||||
Permite o modo vinheta que escurece os cantos da tela e facilita a leitura do texto.
|
||||
Habilita o modo vinheta que escurece os cantos da tela e facilita a leitura do texto.
|
||||
|
||||
rotationByBuilding:
|
||||
title: Rotação por tipo de construção
|
||||
@ -777,7 +777,7 @@ settings:
|
||||
disableCutDeleteWarnings:
|
||||
title: Desativar avisos de recorte / exclusão
|
||||
description: >-
|
||||
Desative as caixas de diálogo de aviso exibidas ao cortar / excluir mais de 100 entidades.
|
||||
Desativa as caixas de diálogo de aviso exibidas ao cortar / excluir mais de 100 entidades.
|
||||
|
||||
keybindings:
|
||||
title: Controles
|
||||
@ -790,9 +790,9 @@ keybindings:
|
||||
general: Geral
|
||||
ingame: Jogo
|
||||
navigation: Navegação
|
||||
placement: Construção
|
||||
massSelect: Seleção
|
||||
buildings: Atalhos de objetos
|
||||
placement: Posicionamento
|
||||
massSelect: Seleção em Massa
|
||||
buildings: Construções
|
||||
placementModifiers: Modificadores
|
||||
|
||||
mappings:
|
||||
@ -807,7 +807,7 @@ keybindings:
|
||||
|
||||
mapZoomIn: Aproximar
|
||||
mapZoomOut: Distanciar
|
||||
createMarker: Criar marcação
|
||||
createMarker: Criar marcador
|
||||
|
||||
menuOpenShop: Melhorias
|
||||
menuOpenStats: Estatísticas
|
||||
@ -839,7 +839,7 @@ keybindings:
|
||||
confirmMassDelete: Confirmar exclusão em massa
|
||||
pasteLastBlueprint: Colar último projeto
|
||||
cycleBuildings: Trocar de construção
|
||||
lockBeltDirection: Ativar planejador de correia
|
||||
lockBeltDirection: Ativar planejamento de esteira
|
||||
switchDirectionLockSide: >-
|
||||
Planejador: Mudar de lado
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user