mirror of
https://github.com/tobspr/shapez.io.git
synced 2025-12-17 04:01:51 +00:00
Merge 9f47684d66 into a7a2aad2b6
This commit is contained in:
commit
cc8b61eab3
BIN
res/ui/icons/upload.png
Normal file
BIN
res/ui/icons/upload.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.6 KiB |
@ -49,6 +49,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.building {
|
.building {
|
||||||
|
&:not(.hidden) {
|
||||||
display: flex;
|
display: flex;
|
||||||
@include S(width, 40px);
|
@include S(width, 40px);
|
||||||
position: relative;
|
position: relative;
|
||||||
@ -116,6 +117,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.puzzle-lock {
|
.puzzle-lock {
|
||||||
|
&.active {
|
||||||
& {
|
& {
|
||||||
/* @load-async */
|
/* @load-async */
|
||||||
background: uiResource("locked_building.png") center center / 90% no-repeat;
|
background: uiResource("locked_building.png") center center / 90% no-repeat;
|
||||||
@ -145,3 +147,5 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
background: $ingameHudBg;
|
background: $ingameHudBg;
|
||||||
@include S(padding, 10px);
|
@include S(padding, 10px);
|
||||||
@include S(bottom, 60px);
|
@include S(bottom, 70px);
|
||||||
@include S(left, 10px);
|
@include S(left, 10px);
|
||||||
|
|
||||||
@include SuperSmallText;
|
@include SuperSmallText;
|
||||||
@ -12,6 +12,17 @@
|
|||||||
@include S(border-radius, $globalBorderRadius);
|
@include S(border-radius, $globalBorderRadius);
|
||||||
|
|
||||||
> .section {
|
> .section {
|
||||||
|
.disabled {
|
||||||
|
transition: opacity 0.12s ease-in-out;
|
||||||
|
opacity: 0.6;
|
||||||
|
button {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
:not(.disabled) {
|
||||||
|
transition: opacity 0.12s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
> label {
|
> label {
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
@ -44,7 +55,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> .buttons {
|
> .mainButtons {
|
||||||
> .buttonBar {
|
> .buttonBar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -57,15 +68,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
> .buildingsButton {
|
> .testToggle {
|
||||||
display: grid;
|
display: grid;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@include S(margin-top, 4px);
|
@include S(margin-top, 4px);
|
||||||
> button {
|
> button {
|
||||||
|
&.disabled {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
@include SuperSmallText;
|
@include SuperSmallText;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
44
src/css/ingame_hud/puzzle_import_export.scss
Normal file
44
src/css/ingame_hud/puzzle_import_export.scss
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
#ingame_HUD_PuzzleImportExport {
|
||||||
|
position: absolute;
|
||||||
|
@include S(top, 35px);
|
||||||
|
left: 50%;
|
||||||
|
|
||||||
|
transform: translateX(-50%);
|
||||||
|
backdrop-filter: blur(D(1px));
|
||||||
|
padding: D(3px);
|
||||||
|
|
||||||
|
> .button {
|
||||||
|
@include PlainText;
|
||||||
|
pointer-events: all;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
color: #333438;
|
||||||
|
transition: all 0.12s ease-in-out;
|
||||||
|
transition-property: opacity, transform;
|
||||||
|
text-transform: uppercase;
|
||||||
|
@include PlainText;
|
||||||
|
@include S(width, 20px);
|
||||||
|
@include S(height, 20px);
|
||||||
|
margin: 8px 5px;
|
||||||
|
|
||||||
|
@include DarkThemeInvert;
|
||||||
|
|
||||||
|
opacity: 1;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.9 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.pressed {
|
||||||
|
transform: scale(0.95) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.import {
|
||||||
|
background: uiResource("icons/upload.png") center center / D(15px) no-repeat !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.export {
|
||||||
|
background: uiResource("icons/download.png") center center / D(15px) no-repeat !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
> .section {
|
> .section {
|
||||||
display: grid;
|
display: grid;
|
||||||
@include S(grid-gap, 5px);
|
@include S(grid-gap, 7px);
|
||||||
grid-auto-flow: row;
|
grid-auto-flow: row;
|
||||||
|
|
||||||
> button {
|
> button {
|
||||||
|
|||||||
@ -66,6 +66,7 @@
|
|||||||
@import "ingame_hud/puzzle_play_metadata";
|
@import "ingame_hud/puzzle_play_metadata";
|
||||||
@import "ingame_hud/puzzle_complete_notification";
|
@import "ingame_hud/puzzle_complete_notification";
|
||||||
@import "ingame_hud/puzzle_next";
|
@import "ingame_hud/puzzle_next";
|
||||||
|
@import "ingame_hud/puzzle_import_export";
|
||||||
|
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
$elements:
|
$elements:
|
||||||
@ -86,6 +87,8 @@ ingame_HUD_KeybindingOverlay,
|
|||||||
ingame_HUD_PuzzleBackToMenu,
|
ingame_HUD_PuzzleBackToMenu,
|
||||||
ingame_HUD_PuzzleNextPuzzle,
|
ingame_HUD_PuzzleNextPuzzle,
|
||||||
ingame_HUD_PuzzleEditorReview,
|
ingame_HUD_PuzzleEditorReview,
|
||||||
|
ingame_HUD_PuzzleImportExport,
|
||||||
|
ingame_HUD_PuzzleNextPuzzle,
|
||||||
ingame_HUD_PuzzleEditorControls,
|
ingame_HUD_PuzzleEditorControls,
|
||||||
ingame_HUD_PuzzleEditorTitle,
|
ingame_HUD_PuzzleEditorTitle,
|
||||||
ingame_HUD_PuzzleEditorSettings,
|
ingame_HUD_PuzzleEditorSettings,
|
||||||
@ -138,6 +141,11 @@ body.uiHidden {
|
|||||||
#ingame_HUD_PuzzleBackToMenu,
|
#ingame_HUD_PuzzleBackToMenu,
|
||||||
#ingame_HUD_PuzzleNextPuzzle,
|
#ingame_HUD_PuzzleNextPuzzle,
|
||||||
#ingame_HUD_PuzzleEditorReview,
|
#ingame_HUD_PuzzleEditorReview,
|
||||||
|
#ingame_HUD_PuzzleEditorSettings,
|
||||||
|
#ingame_HUD_PuzzlePlaySettings,
|
||||||
|
#ingame_HUD_PuzzleEditorControls,
|
||||||
|
#ingame_HUD_PuzzleImportExport,
|
||||||
|
#ingame_HUD_PuzzlePlayMetadata,
|
||||||
#ingame_HUD_Notifications,
|
#ingame_HUD_Notifications,
|
||||||
#ingame_HUD_TutorialHints,
|
#ingame_HUD_TutorialHints,
|
||||||
#ingame_HUD_Waypoints,
|
#ingame_HUD_Waypoints,
|
||||||
|
|||||||
@ -8,9 +8,13 @@
|
|||||||
justify-self: start;
|
justify-self: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
@include S(margin-right, 5px);
|
||||||
|
}
|
||||||
|
|
||||||
.createPuzzle {
|
.createPuzzle {
|
||||||
background-color: $colorGreenBright;
|
background-color: $colorGreenBright;
|
||||||
@include S(margin-left, 5px);
|
@include S(margin-right, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -19,7 +19,8 @@ export class MetaBlockBuilding extends MetaBuilding {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
getIsRemovable(root) {
|
getIsRemovable(root) {
|
||||||
return root.gameMode.getIsEditor();
|
const settings = root.hud.parts.puzzleEditorSettings;
|
||||||
|
return settings ? !settings.getIsTestMode() : false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -23,7 +23,8 @@ export class MetaConstantProducerBuilding extends MetaBuilding {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
getIsRemovable(root) {
|
getIsRemovable(root) {
|
||||||
return root.gameMode.getIsEditor();
|
const settings = root.hud.parts.puzzleEditorSettings;
|
||||||
|
return settings ? !settings.getIsTestMode() : false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -23,7 +23,8 @@ export class MetaGoalAcceptorBuilding extends MetaBuilding {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
getIsRemovable(root) {
|
getIsRemovable(root) {
|
||||||
return root.gameMode.getIsEditor();
|
const settings = root.hud.parts.puzzleEditorSettings;
|
||||||
|
return settings ? !settings.getIsTestMode() : false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -33,6 +33,12 @@ export class HUDBaseToolbar extends BaseHUDPart {
|
|||||||
this.htmlElementId = htmlElementId;
|
this.htmlElementId = htmlElementId;
|
||||||
this.layer = layer;
|
this.layer = layer;
|
||||||
|
|
||||||
|
this.requiredBuildings = [
|
||||||
|
gMetaBuildingRegistry.findByClass(MetaConstantProducerBuilding),
|
||||||
|
gMetaBuildingRegistry.findByClass(MetaGoalAcceptorBuilding),
|
||||||
|
gMetaBuildingRegistry.findByClass(MetaBlockBuilding),
|
||||||
|
];
|
||||||
|
|
||||||
/** @type {Object.<string, {
|
/** @type {Object.<string, {
|
||||||
* metaBuilding: MetaBuilding,
|
* metaBuilding: MetaBuilding,
|
||||||
* unlocked: boolean,
|
* unlocked: boolean,
|
||||||
@ -60,12 +66,10 @@ export class HUDBaseToolbar extends BaseHUDPart {
|
|||||||
const filtered = [];
|
const filtered = [];
|
||||||
|
|
||||||
for (let i = 0; i < buildings.length; i++) {
|
for (let i = 0; i < buildings.length; i++) {
|
||||||
if (this.root.gameMode.isBuildingExcluded(buildings[i])) {
|
if (!this.root.gameMode.isBuildingExcluded(buildings[i])) {
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
filtered.push(buildings[i]);
|
filtered.push(buildings[i]);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return filtered;
|
return filtered;
|
||||||
}
|
}
|
||||||
@ -119,13 +123,15 @@ export class HUDBaseToolbar extends BaseHUDPart {
|
|||||||
});
|
});
|
||||||
|
|
||||||
//lock icon for puzzle editor
|
//lock icon for puzzle editor
|
||||||
if (this.root.gameMode.getIsEditor() && !this.inRequiredBuildings(metaBuilding)) {
|
const settings = this.root.hud.parts.puzzleEditorSettings;
|
||||||
const puzzleLock = makeDiv(itemContainer, null, ["puzzle-lock"]);
|
if (settings && !settings.getIsTestMode()) {
|
||||||
|
|
||||||
itemContainer.classList.toggle("editor", true);
|
itemContainer.classList.toggle("editor", true);
|
||||||
this.trackClicks(puzzleLock, () => this.toggleBuildingLock(metaBuilding), {
|
if (!this.inRequiredBuildings(metaBuilding)) {
|
||||||
clickSound: null,
|
const puzzleLock = makeDiv(itemContainer, null, ["puzzle-lock"]);
|
||||||
});
|
puzzleLock.classList.add("active");
|
||||||
|
|
||||||
|
this.trackClicks(puzzleLock, () => this.toggleBuildingLock(metaBuilding));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.buildingHandles[metaBuilding.id] = {
|
this.buildingHandles[metaBuilding.id] = {
|
||||||
@ -149,13 +155,15 @@ export class HUDBaseToolbar extends BaseHUDPart {
|
|||||||
});
|
});
|
||||||
this.lastSelectedIndex = 0;
|
this.lastSelectedIndex = 0;
|
||||||
actionMapper.getBinding(KEYMAPPINGS.placement.cycleBuildings).add(this.cycleBuildings, this);
|
actionMapper.getBinding(KEYMAPPINGS.placement.cycleBuildings).add(this.cycleBuildings, this);
|
||||||
|
|
||||||
|
this.switchingTestMode = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the toolbar
|
* Updates the toolbar
|
||||||
*/
|
*/
|
||||||
update() {
|
update() {
|
||||||
const visible = this.visibilityCondition();
|
const visible = this.visibilityCondition() && !this.switchingTestMode;
|
||||||
this.domAttach.update(visible);
|
this.domAttach.update(visible);
|
||||||
|
|
||||||
if (visible) {
|
if (visible) {
|
||||||
@ -253,9 +261,12 @@ export class HUDBaseToolbar extends BaseHUDPart {
|
|||||||
|
|
||||||
const handle = this.buildingHandles[metaBuilding.getId()];
|
const handle = this.buildingHandles[metaBuilding.getId()];
|
||||||
if (handle.puzzleLocked) {
|
if (handle.puzzleLocked) {
|
||||||
|
const settings = this.root.hud.parts.puzzleEditorSettings;
|
||||||
|
if (settings && !settings.getIsTestMode()) {
|
||||||
handle.puzzleLocked = false;
|
handle.puzzleLocked = false;
|
||||||
handle.element.classList.toggle("unlocked", false);
|
handle.element.classList.toggle("unlocked", false);
|
||||||
this.root.soundProxy.playUiClick();
|
this.root.soundProxy.playUiClick();
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -274,9 +285,28 @@ export class HUDBaseToolbar extends BaseHUDPart {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {MetaBuilding} metaBuilding
|
* @param {boolean} testMode
|
||||||
*/
|
*/
|
||||||
toggleBuildingLock(metaBuilding) {
|
toggleTestMode(testMode) {
|
||||||
|
// toggle the puzzle lock buttons and the editor-only buildings
|
||||||
|
|
||||||
|
this.element.querySelectorAll(".building > .puzzle-lock").forEach(element => {
|
||||||
|
element.classList.toggle("active", !testMode);
|
||||||
|
});
|
||||||
|
|
||||||
|
for (let i = 0; i < this.requiredBuildings.length; ++i) {
|
||||||
|
const metaBuilding = this.requiredBuildings[i];
|
||||||
|
const handle = this.buildingHandles[metaBuilding.getId()];
|
||||||
|
handle.puzzleLocked = testMode;
|
||||||
|
handle.element.classList.toggle("hidden", testMode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {MetaBuilding} metaBuilding
|
||||||
|
* @param {boolean | null} force
|
||||||
|
*/
|
||||||
|
toggleBuildingLock(metaBuilding, force = null) {
|
||||||
if (!this.visibilityCondition()) {
|
if (!this.visibilityCondition()) {
|
||||||
// Not active
|
// Not active
|
||||||
return;
|
return;
|
||||||
@ -288,9 +318,12 @@ export class HUDBaseToolbar extends BaseHUDPart {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handle = this.buildingHandles[metaBuilding.getId()];
|
const handle = this.buildingHandles[metaBuilding.getId()];
|
||||||
|
if (force != null) {
|
||||||
|
handle.puzzleLocked = force;
|
||||||
|
} else {
|
||||||
handle.puzzleLocked = !handle.puzzleLocked;
|
handle.puzzleLocked = !handle.puzzleLocked;
|
||||||
|
}
|
||||||
handle.element.classList.toggle("unlocked", !handle.puzzleLocked);
|
handle.element.classList.toggle("unlocked", !handle.puzzleLocked);
|
||||||
this.root.soundProxy.playUiClick();
|
|
||||||
|
|
||||||
const entityManager = this.root.entityMgr;
|
const entityManager = this.root.entityMgr;
|
||||||
for (const entity of entityManager.getAllWithComponent(StaticMapEntityComponent)) {
|
for (const entity of entityManager.getAllWithComponent(StaticMapEntityComponent)) {
|
||||||
@ -312,11 +345,6 @@ export class HUDBaseToolbar extends BaseHUDPart {
|
|||||||
* @param {MetaBuilding} metaBuilding
|
* @param {MetaBuilding} metaBuilding
|
||||||
*/
|
*/
|
||||||
inRequiredBuildings(metaBuilding) {
|
inRequiredBuildings(metaBuilding) {
|
||||||
const requiredBuildings = [
|
return this.requiredBuildings.includes(metaBuilding);
|
||||||
gMetaBuildingRegistry.findByClass(MetaConstantProducerBuilding),
|
|
||||||
gMetaBuildingRegistry.findByClass(MetaGoalAcceptorBuilding),
|
|
||||||
gMetaBuildingRegistry.findByClass(MetaBlockBuilding),
|
|
||||||
];
|
|
||||||
return requiredBuildings.includes(metaBuilding);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -44,6 +44,7 @@ export class HUDBlueprintPlacer extends BaseHUDPart {
|
|||||||
this.root.camera.movePreHandler.add(this.onMouseMove, this);
|
this.root.camera.movePreHandler.add(this.onMouseMove, this);
|
||||||
|
|
||||||
this.root.hud.signals.selectedPlacementBuildingChanged.add(this.abortPlacement, this);
|
this.root.hud.signals.selectedPlacementBuildingChanged.add(this.abortPlacement, this);
|
||||||
|
this.root.signals.testModeChanged.add(this.abortPlacement, this);
|
||||||
this.root.signals.editModeChanged.add(this.onEditModeChanged, this);
|
this.root.signals.editModeChanged.add(this.onEditModeChanged, this);
|
||||||
|
|
||||||
this.domAttach = new DynamicDomAttach(this.root, this.costDisplayParent);
|
this.domAttach = new DynamicDomAttach(this.root, this.costDisplayParent);
|
||||||
|
|||||||
@ -130,6 +130,7 @@ export class HUDBuildingPlacerLogic extends BaseHUDPart {
|
|||||||
this.root.signals.storyGoalCompleted.add(() => this.signals.variantChanged.dispatch());
|
this.root.signals.storyGoalCompleted.add(() => this.signals.variantChanged.dispatch());
|
||||||
this.root.signals.upgradePurchased.add(() => this.signals.variantChanged.dispatch());
|
this.root.signals.upgradePurchased.add(() => this.signals.variantChanged.dispatch());
|
||||||
this.root.signals.editModeChanged.add(this.onEditModeChanged, this);
|
this.root.signals.editModeChanged.add(this.onEditModeChanged, this);
|
||||||
|
this.root.signals.testModeChanged.add(this.abortPlacement, this);
|
||||||
|
|
||||||
// MOUSE BINDINGS
|
// MOUSE BINDINGS
|
||||||
this.root.camera.downPreHandler.add(this.onMouseDown, this);
|
this.root.camera.downPreHandler.add(this.onMouseDown, this);
|
||||||
@ -384,8 +385,8 @@ export class HUDBuildingPlacerLogic extends BaseHUDPart {
|
|||||||
const buildingCode = contents.components.StaticMapEntity.code;
|
const buildingCode = contents.components.StaticMapEntity.code;
|
||||||
const extracted = getBuildingDataFromCode(buildingCode);
|
const extracted = getBuildingDataFromCode(buildingCode);
|
||||||
|
|
||||||
// Disable pipetting the hub
|
// Disable pipetting a non removeable building
|
||||||
if (extracted.metaInstance.getId() === gMetaBuildingRegistry.findByClass(MetaHubBuilding).getId()) {
|
if (!extracted.metaInstance.getIsRemovable(this.root)) {
|
||||||
this.currentMetaBuilding.set(null);
|
this.currentMetaBuilding.set(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -45,6 +45,7 @@ export class HUDMassSelector extends BaseHUDPart {
|
|||||||
|
|
||||||
this.root.hud.signals.selectedPlacementBuildingChanged.add(this.clearSelection, this);
|
this.root.hud.signals.selectedPlacementBuildingChanged.add(this.clearSelection, this);
|
||||||
this.root.signals.editModeChanged.add(this.clearSelection, this);
|
this.root.signals.editModeChanged.add(this.clearSelection, this);
|
||||||
|
this.root.signals.testModeChanged.add(this.clearSelection, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import { globalConfig } from "../../../core/config";
|
import { globalConfig } from "../../../core/config";
|
||||||
import { gMetaBuildingRegistry } from "../../../core/global_registries";
|
import { gMetaBuildingRegistry } from "../../../core/global_registries";
|
||||||
import { createLogger } from "../../../core/logging";
|
|
||||||
import { Rectangle } from "../../../core/rectangle";
|
import { Rectangle } from "../../../core/rectangle";
|
||||||
import { makeDiv } from "../../../core/utils";
|
import { makeDiv } from "../../../core/utils";
|
||||||
import { T } from "../../../translations";
|
import { T } from "../../../translations";
|
||||||
@ -8,11 +7,10 @@ import { MetaBlockBuilding } from "../../buildings/block";
|
|||||||
import { MetaConstantProducerBuilding } from "../../buildings/constant_producer";
|
import { MetaConstantProducerBuilding } from "../../buildings/constant_producer";
|
||||||
import { MetaGoalAcceptorBuilding } from "../../buildings/goal_acceptor";
|
import { MetaGoalAcceptorBuilding } from "../../buildings/goal_acceptor";
|
||||||
import { StaticMapEntityComponent } from "../../components/static_map_entity";
|
import { StaticMapEntityComponent } from "../../components/static_map_entity";
|
||||||
|
import { Entity } from "../../entity";
|
||||||
import { PuzzleGameMode } from "../../modes/puzzle";
|
import { PuzzleGameMode } from "../../modes/puzzle";
|
||||||
import { BaseHUDPart } from "../base_hud_part";
|
import { BaseHUDPart } from "../base_hud_part";
|
||||||
|
|
||||||
const logger = createLogger("puzzle-editor");
|
|
||||||
|
|
||||||
export class HUDPuzzleEditorSettings extends BaseHUDPart {
|
export class HUDPuzzleEditorSettings extends BaseHUDPart {
|
||||||
createElements(parent) {
|
createElements(parent) {
|
||||||
this.element = makeDiv(parent, "ingame_HUD_PuzzleEditorSettings");
|
this.element = makeDiv(parent, "ingame_HUD_PuzzleEditorSettings");
|
||||||
@ -27,7 +25,7 @@ export class HUDPuzzleEditorSettings extends BaseHUDPart {
|
|||||||
`
|
`
|
||||||
<label>${T.ingame.puzzleEditorSettings.zoneTitle}</label>
|
<label>${T.ingame.puzzleEditorSettings.zoneTitle}</label>
|
||||||
|
|
||||||
<div class="buttons">
|
<div class="mainButtons">
|
||||||
<div class="zoneWidth plusMinus">
|
<div class="zoneWidth plusMinus">
|
||||||
<label>${T.ingame.puzzleEditorSettings.zoneWidth}</label>
|
<label>${T.ingame.puzzleEditorSettings.zoneWidth}</label>
|
||||||
<button class="styledButton minus">-</button>
|
<button class="styledButton minus">-</button>
|
||||||
@ -47,10 +45,10 @@ export class HUDPuzzleEditorSettings extends BaseHUDPart {
|
|||||||
<button class="styledButton clearItems">${T.ingame.puzzleEditorSettings.clearItems}</button>
|
<button class="styledButton clearItems">${T.ingame.puzzleEditorSettings.clearItems}</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="buildingsButton">
|
|
||||||
<button class="styledButton resetPuzzle">${T.ingame.puzzleEditorSettings.resetPuzzle}</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="testToggle">
|
||||||
|
<button class="styledButton testPuzzle">${T.ingame.puzzleEditorSettings.enableTestMode}</button>
|
||||||
</div>`
|
</div>`
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -60,7 +58,12 @@ export class HUDPuzzleEditorSettings extends BaseHUDPart {
|
|||||||
bind(".zoneHeight .plus", () => this.modifyZone(0, 1));
|
bind(".zoneHeight .plus", () => this.modifyZone(0, 1));
|
||||||
bind("button.trim", this.trim);
|
bind("button.trim", this.trim);
|
||||||
bind("button.clearItems", this.clearItems);
|
bind("button.clearItems", this.clearItems);
|
||||||
bind("button.resetPuzzle", this.resetPuzzle);
|
bind("button.testPuzzle", this.toggleTestMode);
|
||||||
|
|
||||||
|
this.testMode = false;
|
||||||
|
|
||||||
|
/** @type {Entity[]} */
|
||||||
|
this.storedSolution = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,27 +71,67 @@ export class HUDPuzzleEditorSettings extends BaseHUDPart {
|
|||||||
this.root.logic.clearAllBeltsAndItems();
|
this.root.logic.clearAllBeltsAndItems();
|
||||||
}
|
}
|
||||||
|
|
||||||
resetPuzzle() {
|
toggleTestMode() {
|
||||||
|
this.testMode = !this.testMode;
|
||||||
|
|
||||||
|
this.element.querySelector(".section > label").classList.toggle("disabled", this.testMode);
|
||||||
|
this.element.querySelector(".mainButtons").classList.toggle("disabled", this.testMode);
|
||||||
|
const testButton = this.element.querySelector(".testToggle > .testPuzzle");
|
||||||
|
testButton.textContent = this.testMode
|
||||||
|
? T.ingame.puzzleEditorSettings.disableTestMode
|
||||||
|
: T.ingame.puzzleEditorSettings.enableTestMode;
|
||||||
|
|
||||||
|
testButton.classList.toggle("disabled", true);
|
||||||
|
|
||||||
|
const buildingsToolbar = this.root.hud.parts.buildingsToolbar;
|
||||||
|
buildingsToolbar.switchingTestMode = true;
|
||||||
|
this.root.signals.testModeChanged.dispatch(this.testMode);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
buildingsToolbar.switchingTestMode = false;
|
||||||
|
buildingsToolbar.toggleTestMode(this.testMode);
|
||||||
|
|
||||||
|
testButton.classList.toggle("disabled", false);
|
||||||
|
}, 140);
|
||||||
|
|
||||||
|
if (this.testMode) {
|
||||||
|
const newSolution = [];
|
||||||
for (const entity of this.root.entityMgr.getAllWithComponent(StaticMapEntityComponent)) {
|
for (const entity of this.root.entityMgr.getAllWithComponent(StaticMapEntityComponent)) {
|
||||||
const staticComp = entity.components.StaticMapEntity;
|
if (this.isExcludedEntity(entity)) {
|
||||||
const goalComp = entity.components.GoalAcceptor;
|
|
||||||
|
|
||||||
if (goalComp) {
|
|
||||||
goalComp.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
[MetaGoalAcceptorBuilding, MetaConstantProducerBuilding, MetaBlockBuilding]
|
|
||||||
.map(metaClass => gMetaBuildingRegistry.findByClass(metaClass).id)
|
|
||||||
.includes(staticComp.getMetaBuilding().id)
|
|
||||||
) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
newSolution.push(entity.clone());
|
||||||
|
|
||||||
|
this.root.map.removeStaticEntity(entity);
|
||||||
|
this.root.entityMgr.destroyEntity(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.root.entityMgr.processDestroyList();
|
||||||
|
this.storedSolution = newSolution;
|
||||||
|
} else if (this.storedSolution.length) {
|
||||||
|
this.root.logic.performBulkOperation(() => {
|
||||||
|
for (const entity of this.root.entityMgr.getAllWithComponent(StaticMapEntityComponent)) {
|
||||||
|
if (this.isExcludedEntity(entity)) continue;
|
||||||
|
|
||||||
this.root.map.removeStaticEntity(entity);
|
this.root.map.removeStaticEntity(entity);
|
||||||
this.root.entityMgr.destroyEntity(entity);
|
this.root.entityMgr.destroyEntity(entity);
|
||||||
}
|
}
|
||||||
this.root.entityMgr.processDestroyList();
|
this.root.entityMgr.processDestroyList();
|
||||||
|
|
||||||
|
for (let i = 0; i < this.storedSolution.length; ++i) {
|
||||||
|
const entity = this.storedSolution[i];
|
||||||
|
const placedEntity = this.root.logic.tryPlaceEntity(entity);
|
||||||
|
|
||||||
|
for (const key in entity.components) {
|
||||||
|
/** @type {import("../../../core/global_registries").Component} */ (entity.components[
|
||||||
|
key
|
||||||
|
]).copyAdditionalStateTo(placedEntity.components[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.storedSolution = [];
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
trim() {
|
trim() {
|
||||||
@ -228,4 +271,21 @@ export class HUDPuzzleEditorSettings extends BaseHUDPart {
|
|||||||
this.element.querySelector(".zoneWidth > .value").textContent = String(mode.zoneWidth);
|
this.element.querySelector(".zoneWidth > .value").textContent = String(mode.zoneWidth);
|
||||||
this.element.querySelector(".zoneHeight > .value").textContent = String(mode.zoneHeight);
|
this.element.querySelector(".zoneHeight > .value").textContent = String(mode.zoneHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getIsTestMode() {
|
||||||
|
return this.testMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
isExcludedEntity(entity) {
|
||||||
|
const metaBuilding = entity.components.StaticMapEntity.getMetaBuilding();
|
||||||
|
|
||||||
|
if (
|
||||||
|
[MetaConstantProducerBuilding, MetaBlockBuilding, MetaGoalAcceptorBuilding]
|
||||||
|
.map(metaClass => gMetaBuildingRegistry.findByClass(metaClass).id)
|
||||||
|
.includes(metaBuilding.id)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
132
src/js/game/hud/parts/puzzle_import_export.js
Normal file
132
src/js/game/hud/parts/puzzle_import_export.js
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
import { gMetaBuildingRegistry } from "../../../core/global_registries";
|
||||||
|
import { ReadWriteProxy } from "../../../core/read_write_proxy";
|
||||||
|
import { generateFileDownload, makeDiv, startFileChoose, waitNextFrame } from "../../../core/utils";
|
||||||
|
import { PuzzleSerializer } from "../../../savegame/puzzle_serializer";
|
||||||
|
import { T } from "../../../translations";
|
||||||
|
import { GoalAcceptorComponent } from "../../components/goal_acceptor";
|
||||||
|
import { StaticMapEntityComponent } from "../../components/static_map_entity";
|
||||||
|
import { PuzzleGameMode } from "../../modes/puzzle";
|
||||||
|
import { BaseHUDPart } from "../base_hud_part";
|
||||||
|
|
||||||
|
export class HUDPuzzleImportExport extends BaseHUDPart {
|
||||||
|
constructor(root) {
|
||||||
|
super(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
createElements(parent) {
|
||||||
|
this.element = makeDiv(parent, "ingame_HUD_PuzzleImportExport");
|
||||||
|
this.importButton = document.createElement("button");
|
||||||
|
this.importButton.classList.add("button", "import");
|
||||||
|
this.element.appendChild(this.importButton);
|
||||||
|
|
||||||
|
this.exportButton = document.createElement("button");
|
||||||
|
this.exportButton.classList.add("button", "export");
|
||||||
|
this.element.appendChild(this.exportButton);
|
||||||
|
|
||||||
|
this.trackClicks(this.importButton, this.importPuzzle);
|
||||||
|
|
||||||
|
this.trackClicks(this.exportButton, () => {
|
||||||
|
const { yes } = this.root.hud.parts.dialogs.showWarning(
|
||||||
|
T.dialogs.puzzleExport.title,
|
||||||
|
T.dialogs.puzzleExport.desc,
|
||||||
|
["no", "yes:good:enter"]
|
||||||
|
);
|
||||||
|
yes.add(() => this.exportPuzzle());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
initialize() {}
|
||||||
|
|
||||||
|
importPuzzle() {
|
||||||
|
startFileChoose(".bin").then(file => {
|
||||||
|
if (file) {
|
||||||
|
const closeLoader = this.root.hud.parts.dialogs.showLoadingDialog("Importing Puzzle");
|
||||||
|
waitNextFrame().then(() => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.addEventListener("load", event => {
|
||||||
|
const fileContents = String(event.target.result);
|
||||||
|
|
||||||
|
/** @type {import("../../../savegame/savegame_typedefs").PuzzleGameData} */
|
||||||
|
let gameData;
|
||||||
|
|
||||||
|
try {
|
||||||
|
gameData = ReadWriteProxy.deserializeObject(fileContents);
|
||||||
|
} catch (err) {
|
||||||
|
closeLoader();
|
||||||
|
this.root.hud.parts.dialogs.showWarning(T.global.error, String(err));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mode = /** @type {PuzzleGameMode} */ (this.root.gameMode);
|
||||||
|
let errorText;
|
||||||
|
try {
|
||||||
|
// set excluded buildings first so if we get an error we haven't removed buildings yet
|
||||||
|
const toolbar = this.root.hud.parts.buildingsToolbar;
|
||||||
|
const handles = toolbar.buildingHandles;
|
||||||
|
const ids = gMetaBuildingRegistry.getAllIds();
|
||||||
|
|
||||||
|
for (let i = 0; i < ids.length; ++i) {
|
||||||
|
const handle = handles[ids[i]];
|
||||||
|
if (handle && !toolbar.inRequiredBuildings(handle.metaBuilding)) {
|
||||||
|
const locked = gameData.excludedBuildings.includes(ids[i]);
|
||||||
|
|
||||||
|
toolbar.toggleBuildingLock(handle.metaBuilding, locked);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const entity of this.root.entityMgr.getAllWithComponent(
|
||||||
|
StaticMapEntityComponent
|
||||||
|
)) {
|
||||||
|
this.root.map.removeStaticEntity(entity);
|
||||||
|
this.root.entityMgr.destroyEntity(entity);
|
||||||
|
}
|
||||||
|
this.root.entityMgr.processDestroyList();
|
||||||
|
|
||||||
|
mode.zoneWidth = gameData.bounds.w;
|
||||||
|
mode.zoneHeight = gameData.bounds.h;
|
||||||
|
this.root.hud.parts.puzzleEditorSettings.updateZoneValues();
|
||||||
|
|
||||||
|
errorText = new PuzzleSerializer().deserializePuzzle(this.root, gameData);
|
||||||
|
} catch (ex) {
|
||||||
|
errorText = ex.message || ex;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errorText) {
|
||||||
|
this.root.hud.parts.dialogs.showWarning(
|
||||||
|
T.dialogs.puzzleLoadError.title,
|
||||||
|
T.dialogs.puzzleLoadError.desc + " " + errorText
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.root.hud.parts.dialogs.showInfo(
|
||||||
|
T.dialogs.puzzleImport.title,
|
||||||
|
T.dialogs.puzzleImport.desc
|
||||||
|
);
|
||||||
|
}
|
||||||
|
closeLoader();
|
||||||
|
});
|
||||||
|
reader.readAsText(file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
exportPuzzle() {
|
||||||
|
// Make sure all acceptors have an item
|
||||||
|
for (const entity of this.root.entityMgr.getAllWithComponent(GoalAcceptorComponent)) {
|
||||||
|
const goalComp = entity.components.GoalAcceptor;
|
||||||
|
if (!goalComp.item) {
|
||||||
|
this.root.hud.parts.dialogs.showWarning(
|
||||||
|
T.puzzleMenu.validation.title,
|
||||||
|
T.puzzleMenu.validation.goalAcceptorNoItem
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const serialized = new PuzzleSerializer().generateDumpFromGameRoot(this.root);
|
||||||
|
|
||||||
|
const data = ReadWriteProxy.serializeObject(serialized);
|
||||||
|
const filename = "puzzle.bin";
|
||||||
|
generateFileDownload(filename, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -116,6 +116,15 @@ export class GameLogic {
|
|||||||
rotationVariant,
|
rotationVariant,
|
||||||
variant,
|
variant,
|
||||||
});
|
});
|
||||||
|
return this.tryPlaceEntity(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to place the given entity
|
||||||
|
* @param {Entity} entity
|
||||||
|
* @returns {Entity}
|
||||||
|
*/
|
||||||
|
tryPlaceEntity(entity) {
|
||||||
if (this.checkCanPlaceEntity(entity)) {
|
if (this.checkCanPlaceEntity(entity)) {
|
||||||
this.freeEntityAreaBeforeBuild(entity);
|
this.freeEntityAreaBeforeBuild(entity);
|
||||||
this.root.map.placeStaticEntity(entity);
|
this.root.map.placeStaticEntity(entity);
|
||||||
|
|||||||
@ -22,6 +22,10 @@ import { MetaTransistorBuilding } from "../buildings/transistor";
|
|||||||
import { HUDPuzzleEditorControls } from "../hud/parts/puzzle_editor_controls";
|
import { HUDPuzzleEditorControls } from "../hud/parts/puzzle_editor_controls";
|
||||||
import { HUDPuzzleEditorReview } from "../hud/parts/puzzle_editor_review";
|
import { HUDPuzzleEditorReview } from "../hud/parts/puzzle_editor_review";
|
||||||
import { HUDPuzzleEditorSettings } from "../hud/parts/puzzle_editor_settings";
|
import { HUDPuzzleEditorSettings } from "../hud/parts/puzzle_editor_settings";
|
||||||
|
import { createLogger } from "../../core/logging";
|
||||||
|
import { HUDPuzzleImportExport } from "../hud/parts/puzzle_import_export";
|
||||||
|
|
||||||
|
const logger = createLogger("puzzle-edit");
|
||||||
|
|
||||||
export class PuzzleEditGameMode extends PuzzleGameMode {
|
export class PuzzleEditGameMode extends PuzzleGameMode {
|
||||||
static getId() {
|
static getId() {
|
||||||
@ -32,7 +36,9 @@ export class PuzzleEditGameMode extends PuzzleGameMode {
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param {GameRoot} root */
|
/**
|
||||||
|
* @param {GameRoot} root
|
||||||
|
*/
|
||||||
constructor(root) {
|
constructor(root) {
|
||||||
super(root);
|
super(root);
|
||||||
|
|
||||||
@ -58,6 +64,7 @@ export class PuzzleEditGameMode extends PuzzleGameMode {
|
|||||||
this.additionalHudParts.puzzleEditorControls = HUDPuzzleEditorControls;
|
this.additionalHudParts.puzzleEditorControls = HUDPuzzleEditorControls;
|
||||||
this.additionalHudParts.puzzleEditorReview = HUDPuzzleEditorReview;
|
this.additionalHudParts.puzzleEditorReview = HUDPuzzleEditorReview;
|
||||||
this.additionalHudParts.puzzleEditorSettings = HUDPuzzleEditorSettings;
|
this.additionalHudParts.puzzleEditorSettings = HUDPuzzleEditorSettings;
|
||||||
|
this.additionalHudParts.puzzleEditorDownload = HUDPuzzleImportExport;
|
||||||
}
|
}
|
||||||
|
|
||||||
getIsEditor() {
|
getIsEditor() {
|
||||||
|
|||||||
@ -192,6 +192,7 @@ export class GameRoot {
|
|||||||
|
|
||||||
// Puzzle mode
|
// Puzzle mode
|
||||||
puzzleComplete: /** @type {TypedSignal<[]>} */ (new Signal()),
|
puzzleComplete: /** @type {TypedSignal<[]>} */ (new Signal()),
|
||||||
|
testModeChanged: /** @type {TypedSignal<[Boolean]>} */ (new Signal()),
|
||||||
};
|
};
|
||||||
|
|
||||||
// RNG's
|
// RNG's
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { DrawParameters } from "../../core/draw_parameters";
|
|||||||
import { clamp, lerp } from "../../core/utils";
|
import { clamp, lerp } from "../../core/utils";
|
||||||
import { Vector } from "../../core/vector";
|
import { Vector } from "../../core/vector";
|
||||||
import { GoalAcceptorComponent } from "../components/goal_acceptor";
|
import { GoalAcceptorComponent } from "../components/goal_acceptor";
|
||||||
|
import { enumGameModeIds } from "../game_mode";
|
||||||
import { GameSystemWithFilter } from "../game_system_with_filter";
|
import { GameSystemWithFilter } from "../game_system_with_filter";
|
||||||
import { MapChunk } from "../map_chunk";
|
import { MapChunk } from "../map_chunk";
|
||||||
import { GameRoot } from "../root";
|
import { GameRoot } from "../root";
|
||||||
@ -42,7 +43,7 @@ export class GoalAcceptorSystem extends GameSystemWithFilter {
|
|||||||
!this.puzzleCompleted &&
|
!this.puzzleCompleted &&
|
||||||
this.root.gameInitialized &&
|
this.root.gameInitialized &&
|
||||||
allAccepted &&
|
allAccepted &&
|
||||||
!this.root.gameMode.getIsEditor()
|
!(this.root.gameMode.getId() == enumGameModeIds.puzzleEdit)
|
||||||
) {
|
) {
|
||||||
this.root.signals.puzzleComplete.dispatch();
|
this.root.signals.puzzleComplete.dispatch();
|
||||||
this.puzzleCompleted = true;
|
this.puzzleCompleted = true;
|
||||||
|
|||||||
@ -589,7 +589,8 @@ export class ItemProcessorSystem extends GameSystemWithFilter {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.root.gameMode.getIsEditor()) {
|
const settings = this.root.hud.parts.puzzleEditorSettings;
|
||||||
|
if (settings && !settings.getIsTestMode()) {
|
||||||
// while playing in editor, assign the item
|
// while playing in editor, assign the item
|
||||||
goalComp.item = item;
|
goalComp.item = item;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -609,6 +609,7 @@ export class PuzzleMenuState extends TextualGameState {
|
|||||||
const savegame = Savegame.createPuzzleSavegame(this.app);
|
const savegame = Savegame.createPuzzleSavegame(this.app);
|
||||||
this.moveToState("InGameState", {
|
this.moveToState("InGameState", {
|
||||||
gameModeId: enumGameModeIds.puzzleEdit,
|
gameModeId: enumGameModeIds.puzzleEdit,
|
||||||
|
gameModeParameters: {},
|
||||||
savegame,
|
savegame,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -207,6 +207,8 @@ dialogs:
|
|||||||
retry: Retry
|
retry: Retry
|
||||||
continue: Continue
|
continue: Continue
|
||||||
playOffline: Play Offline
|
playOffline: Play Offline
|
||||||
|
yes: Yes
|
||||||
|
no: No
|
||||||
|
|
||||||
importSavegameError:
|
importSavegameError:
|
||||||
title: Import Error
|
title: Import Error
|
||||||
@ -418,6 +420,16 @@ dialogs:
|
|||||||
desc: >-
|
desc: >-
|
||||||
Are you sure you want to delete '<title>'? This can not be undone!
|
Are you sure you want to delete '<title>'? This can not be undone!
|
||||||
|
|
||||||
|
puzzleImport:
|
||||||
|
title: Puzzle Imported
|
||||||
|
desc: >-
|
||||||
|
Your puzzle has been successfully imported.
|
||||||
|
|
||||||
|
puzzleExport:
|
||||||
|
title: Export Puzzle
|
||||||
|
desc: >-
|
||||||
|
Do you want to download this puzzle?
|
||||||
|
|
||||||
ingame:
|
ingame:
|
||||||
# This is shown in the top left corner and displays useful keybindings in
|
# This is shown in the top left corner and displays useful keybindings in
|
||||||
# every situation
|
# every situation
|
||||||
@ -652,6 +664,8 @@ ingame:
|
|||||||
clearItems: Clear Items
|
clearItems: Clear Items
|
||||||
clearBuildings: Clear Buildings
|
clearBuildings: Clear Buildings
|
||||||
resetPuzzle: Reset Puzzle
|
resetPuzzle: Reset Puzzle
|
||||||
|
enableTestMode: Enable Test Mode
|
||||||
|
disableTestMode: Disable Test Mode
|
||||||
share: Share
|
share: Share
|
||||||
report: Report
|
report: Report
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user