Color blind mode

This commit is contained in:
tobspr 2020-06-22 14:32:24 +02:00
parent f204189fdb
commit 9a67115ba7
22 changed files with 518 additions and 11 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 577 B

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1dc8775fdf5155097d6e1d60a436f48916af56eec14fb9034e71b32ad3b6f1b0
size 358896

View File

@ -0,0 +1,18 @@
#ingame_HUD_ColorBlindBelowTileHelper {
position: absolute;
@include SuperSmallText;
color: #fff;
background: $ingameHudBg;
@include S(padding, 5px);
@include S(top, 20px);
left: 50%;
transform: translateX(-50%);
text-transform: uppercase;
&:not(.visible) {
display: none;
}
@include DarkThemeInvert;
}

View File

@ -72,6 +72,24 @@
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 {

View File

@ -0,0 +1,131 @@
#ingame_HUD_ShapeViewer {
.dialogInner {
@include S(width, 160px);
}
.content {
display: flex;
flex-direction: column;
width: 100%;
align-items: center;
justify-items: center;
.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);
}
}
}
}
}
}

View File

@ -195,6 +195,25 @@
}
}
button.showInfo {
@include S(width, 11px);
@include S(height, 11px);
background: uiResource("icons/info_button.png") center center / 95% no-repeat;
position: absolute;
@include S(top, 17px);
@include S(right, 2.5px);
opacity: 0.5;
cursor: pointer;
pointer-events: all;
@include IncreasedClickArea(5px);
transition: opacity 0.12s ease-in-out;
@include DarkThemeInvert;
&:hover {
opacity: 0.6;
}
}
canvas {
@include S(width, 40px);
@include S(height, 40px);
@ -241,7 +260,7 @@
&.complete {
background-color: $colorGreenBright;
@include DarkThemeOverride {
background-color: $colorGreenBright;
}

View File

@ -49,6 +49,8 @@
@import "ingame_hud/blueprint_placer";
@import "ingame_hud/waypoints";
@import "ingame_hud/interactive_tutorial";
@import "ingame_hud/color_blind_helper";
@import "ingame_hud/shape_viewer";
// prettier-ignore
$elements:
@ -74,6 +76,7 @@ ingame_HUD_buildings_toolbar,
ingame_HUD_BlueprintPlacer,
ingame_HUD_Waypoints_Hint,
ingame_HUD_Watermark,
ingame_HUD_ColorBlindBelowTileHelper,
// Overlays
ingame_HUD_BetaOverlay,
@ -81,6 +84,7 @@ ingame_HUD_BetaOverlay,
// Dialogs
ingame_HUD_Shop,
ingame_HUD_Statistics,
ingame_HUD_ShapeViewer,
ingame_HUD_UnlockNotification,
ingame_HUD_SettingsMenu,
ingame_HUD_ModalDialogs;

View File

@ -3,6 +3,8 @@ export const CHANGELOG = [
version: "1.1.17",
date: "unreleased",
entries: [
"Color blind mode! You can now activate it in the settings and it will show you which color is below your cursor (Either resource or on the belt)",
"Add info buttons to all shapes so you can figure out how they are built! (And also, which colors they have)",
"Allow configuring autosave interval and disabling it in the settings",
"The smart-tunnel placement has been reworked to properly replace belts. Thus the setting has been turned on again by default",
"The soundtrack now has a higher quality on the standalone version than the web version",
@ -10,6 +12,8 @@ export const CHANGELOG = [
"Fix bug where belts in blueprints don't orient correctly (by hexy)",
"Fix camera moving weird after dragging and holding (by hexy)",
"Fix keybinding for pipette showing while pasting blueprints",
"Improve visibility of shape background in dark mode",
"Added sound when destroying a building",
"Update tutorial image for tier 2 tunnels to explain mix/match (by jimmyshadow1)",
"Prevent default actions on all keybindings in the web version so you don't accidentally use builtin browser shortcuts",
],

View File

@ -34,7 +34,8 @@ import { HUDPartTutorialHints } from "./parts/tutorial_hints";
import { HUDWaypoints } from "./parts/waypoints";
import { HUDInteractiveTutorial } from "./parts/interactive_tutorial";
import { HUDScreenshotExporter } from "./parts/screenshot_exporter";
import { Entity } from "../entity";
import { HUDColorBlindHelper } from "./parts/color_blind_helper";
import { HUDShapeViewer } from "./parts/shape_viewer";
export class GameHUD {
/**
@ -68,6 +69,7 @@ export class GameHUD {
debugInfo: new HUDDebugInfo(this.root),
dialogs: new HUDModalDialogs(this.root),
screenshotExporter: new HUDScreenshotExporter(this.root),
shapeViewer: new HUDShapeViewer(this.root),
};
this.signals = {
@ -76,7 +78,8 @@ export class GameHUD {
shapeUnpinRequested: /** @type {TypedSignal<[string]>} */ (new Signal()),
notification: /** @type {TypedSignal<[string, enumNotificationType]>} */ (new Signal()),
buildingsSelectedForCopy: /** @type {TypedSignal<[Array<number>]>} */ (new Signal()),
pasteBlueprintRequested: new Signal(),
pasteBlueprintRequested: /** @type {TypedSignal<[]>} */ (new Signal()),
viewShapeDetailsRequested: /** @type {TypedSignal<[ShapeDefinition]>} */ (new Signal()),
};
if (!IS_MOBILE) {
@ -100,6 +103,10 @@ export class GameHUD {
this.parts.vignetteOverlay = new HUDVignetteOverlay(this.root);
}
if (this.root.app.settings.getAllSettings().enableColorBlindHelper) {
this.parts.colorBlindHelper = new HUDColorBlindHelper(this.root);
}
const frag = document.createDocumentFragment();
for (const key in this.parts) {
this.parts[key].createElements(frag);
@ -208,7 +215,13 @@ export class GameHUD {
* @param {DrawParameters} parameters
*/
draw(parameters) {
const partsOrder = ["waypoints", "massSelector", "buildingPlacer", "blueprintPlacer"];
const partsOrder = [
"waypoints",
"massSelector",
"buildingPlacer",
"blueprintPlacer",
"colorBlindHelper",
];
for (let i = 0; i < partsOrder.length; ++i) {
if (this.parts[partsOrder[i]]) {

View File

@ -10,6 +10,7 @@ import { blueprintShape } from "../../upgrades";
import { BaseHUDPart } from "../base_hud_part";
import { DynamicDomAttach } from "../dynamic_dom_attach";
import { Blueprint } from "./blueprint";
import { SOUNDS } from "../../../platform/sound";
export class HUDBlueprintPlacer extends BaseHUDPart {
createElements(parent) {
@ -103,7 +104,7 @@ export class HUDBlueprintPlacer extends BaseHUDPart {
if (blueprint.tryPlace(this.root, tile)) {
const cost = blueprint.getCost();
this.root.hubGoals.takeShapeByKey(blueprintShape, cost);
this.root.soundProxy.playUi(SOUNDS.placeBuilding);
// This actually feels weird
// if (!this.root.keyMapper.getBinding(KEYMAPPINGS.placementModifiers.placeMultiple).pressed) {
// this.currentBlueprint.set(null);

View File

@ -10,6 +10,7 @@ import { Entity } from "../../entity";
import { KEYMAPPINGS } from "../../key_action_mapper";
import { defaultBuildingVariant, MetaBuilding } from "../../meta_building";
import { BaseHUDPart } from "../base_hud_part";
import { SOUNDS } from "../../../platform/sound";
/**
* Contains all logic for the building placer - this doesn't include the rendering
@ -215,6 +216,7 @@ export class HUDBuildingPlacerLogic extends BaseHUDPart {
const contents = this.root.map.getTileContent(tile);
if (contents) {
this.root.logic.tryDeleteBuilding(contents);
this.root.soundProxy.playUi(SOUNDS.destroyBuilding);
}
}
@ -650,6 +652,7 @@ export class HUDBuildingPlacerLogic extends BaseHUDPart {
const contents = this.root.map.getTileContentXY(x0, y0);
if (contents && !contents.queuedForDestroy && !contents.destroyed) {
this.root.logic.tryDeleteBuilding(contents);
this.root.soundProxy.playUi(SOUNDS.destroyBuilding);
}
} else {
this.tryPlaceCurrentBuildingAt(new Vector(x0, y0));

View File

@ -0,0 +1,106 @@
import { BaseHUDPart } from "../base_hud_part";
import { makeDiv } from "../../../core/utils";
import { TrackedState } from "../../../core/tracked_state";
import { enumColors } from "../../colors";
import { ColorItem } from "../../items/color_item";
import { DrawParameters } from "../../../core/draw_parameters";
import { THEME } from "../../theme";
import { globalConfig } from "../../../core/config";
import { T } from "../../../translations";
export class HUDColorBlindHelper extends BaseHUDPart {
createElements(parent) {
this.belowTileIndicator = makeDiv(parent, "ingame_HUD_ColorBlindBelowTileHelper", []);
}
initialize() {
this.trackedColorBelowTile = new TrackedState(this.onColorBelowTileChanged, this);
}
/**
* Called when the color below the current tile changed
* @param {enumColors|null} color
*/
onColorBelowTileChanged(color) {
this.belowTileIndicator.classList.toggle("visible", !!color);
if (color) {
this.belowTileIndicator.innerText = T.ingame.colors[color];
}
}
/**
* Computes the color below the current tile
* @returns {enumColors}
*/
computeColorBelowTile() {
const mousePosition = this.root.app.mousePosition;
if (!mousePosition) {
// Not on screen
return null;
}
const worldPos = this.root.camera.screenToWorld(mousePosition);
const tile = worldPos.toTileSpace();
const contents = this.root.map.getTileContent(tile);
if (contents && !contents.components.Miner) {
const beltComp = contents.components.Belt;
// Check if the belt has a color item
if (beltComp) {
const firstItem = beltComp.sortedItems[0];
if (firstItem && firstItem[1] instanceof ColorItem) {
return firstItem[1].color;
}
}
// Check if we are ejecting an item, if so use that color
const ejectorComp = contents.components.ItemEjector;
if (ejectorComp) {
for (let i = 0; i < ejectorComp.slots.length; ++i) {
const slot = ejectorComp.slots[i];
if (slot.item && slot.item instanceof ColorItem) {
return slot.item.color;
}
}
}
} else {
// We hovered a lower layer, show the color there
const lowerLayer = this.root.map.getLowerLayerContentXY(tile.x, tile.y);
if (lowerLayer && lowerLayer instanceof ColorItem) {
return lowerLayer.color;
}
}
return null;
}
update() {
this.trackedColorBelowTile.set(this.computeColorBelowTile());
}
/**
* Draws the currently selected tile
* @param {DrawParameters} parameters
*/
draw(parameters) {
const mousePosition = this.root.app.mousePosition;
if (!mousePosition) {
// Not on screen
return null;
}
const below = this.computeColorBelowTile();
if (below) {
// We have something below our tile
const worldPos = this.root.camera.screenToWorld(mousePosition);
const tile = worldPos.toTileSpace().toWorldSpace();
parameters.context.strokeStyle = THEME.map.colorBlindPickerTile;
parameters.context.lineWidth = 1;
parameters.context.beginPath();
parameters.context.rect(tile.x, tile.y, globalConfig.tileSize, globalConfig.tileSize);
parameters.context.stroke();
}
}
}

View File

@ -26,7 +26,8 @@ export class HUDPinnedShapes extends BaseHUDPart {
* amountLabel: HTMLElement,
* lastRenderedValue: string,
* element: HTMLElement,
* detector?: ClickDetector
* detector?: ClickDetector,
* infoDetector?: ClickDetector
* }>}
*/
this.handles = [];
@ -155,6 +156,10 @@ export class HUDPinnedShapes extends BaseHUDPart {
if (detector) {
detector.cleanup();
}
const infoDetector = this.handles[i].infoDetector;
if (infoDetector) {
infoDetector.cleanup();
}
}
this.handles = [];
@ -198,12 +203,24 @@ export class HUDPinnedShapes extends BaseHUDPart {
detector = new ClickDetector(element, {
consumeEvents: true,
preventDefault: true,
targetOnly: true,
});
detector.click.add(() => this.unpinShape(key));
} else {
element.classList.add("marked");
}
// Show small info icon
const infoButton = document.createElement("button");
infoButton.classList.add("infoButton");
element.appendChild(infoButton);
const infoDetector = new ClickDetector(infoButton, {
consumeEvents: true,
preventDefault: true,
targetOnly: true,
});
infoDetector.click.add(() => this.root.hud.signals.viewShapeDetailsRequested.dispatch(definition));
const amountLabel = makeDiv(element, null, ["amountLabel"], "");
const goal = this.findGoalValueForShape(key);
@ -216,6 +233,8 @@ export class HUDPinnedShapes extends BaseHUDPart {
element,
amountLabel,
lastRenderedValue: "",
detector,
infoDetector,
});
}

View File

@ -0,0 +1,109 @@
import { BaseHUDPart } from "../base_hud_part";
import { makeDiv, removeAllChildren } from "../../../core/utils";
import { T } from "../../../translations";
import { defaultBuildingVariant } from "../../meta_building";
import { ShapeDefinition } from "../../shape_definition";
import { KEYMAPPINGS, KeyActionMapper } from "../../key_action_mapper";
import { InputReceiver } from "../../../core/input_receiver";
import { DynamicDomAttach } from "../dynamic_dom_attach";
export class HUDShapeViewer extends BaseHUDPart {
createElements(parent) {
this.background = makeDiv(parent, "ingame_HUD_ShapeViewer", ["ingameDialog"]);
// DIALOG Inner / Wrapper
this.dialogInner = makeDiv(this.background, null, ["dialogInner"]);
this.title = makeDiv(this.dialogInner, null, ["title"], T.ingame.shapeViewer.title);
this.closeButton = makeDiv(this.title, null, ["closeButton"]);
this.trackClicks(this.closeButton, this.close);
this.contentDiv = makeDiv(this.dialogInner, null, ["content"]);
}
initialize() {
this.root.hud.signals.viewShapeDetailsRequested.add(this.renderForShape, this);
this.domAttach = new DynamicDomAttach(this.root, this.background, {
attachClass: "visible",
});
this.inputReciever = new InputReceiver("shape_viewer");
this.keyActionMapper = new KeyActionMapper(this.root, this.inputReciever);
this.keyActionMapper.getBinding(KEYMAPPINGS.general.back).add(this.close, this);
this.close();
}
/**
* Closes the dialog
*/
close() {
this.visible = false;
document.body.classList.remove("ingameDialogOpen");
this.root.app.inputMgr.makeSureDetached(this.inputReciever);
this.update();
}
/**
* Shows the viewer for a given definition
* @param {ShapeDefinition} definition
*/
renderForShape(definition) {
this.visible = true;
document.body.classList.add("ingameDialogOpen");
this.root.app.inputMgr.makeSureAttachedAndOnTop(this.inputReciever);
removeAllChildren(this.contentDiv);
const layers = definition.layers;
for (let i = 0; i < layers.length; ++i) {
const layerElem = makeDiv(this.contentDiv, null, ["layer", "layer-" + i]);
let fakeLayers = [];
for (let k = 0; k < i; ++k) {
fakeLayers.push([null, null, null, null]);
}
fakeLayers.push(layers[i]);
const thisLayerOnly = new ShapeDefinition({ layers: fakeLayers });
const thisLayerCanvas = thisLayerOnly.generateAsCanvas(160);
layerElem.appendChild(thisLayerCanvas);
for (let quad = 0; quad < 4; ++quad) {
const quadElem = makeDiv(layerElem, null, ["quad", "quad-" + quad]);
const contents = layers[i][quad];
if (contents) {
const colorLabelElem = makeDiv(
quadElem,
null,
["colorLabel"],
T.ingame.colors[contents.color]
);
} else {
const emptyLabelElem = makeDiv(
quadElem,
null,
["emptyLabel"],
T.ingame.shapeViewer.empty
);
}
}
if (i < layers.length - 1) {
makeDiv(this.contentDiv, null, ["seperator"], "+");
}
}
}
/**
* Cleans up everything
*/
cleanup() {
document.body.classList.remove("ingameDialogOpen");
}
update() {
this.domAttach.update(this.visible);
}
}

View File

@ -79,6 +79,7 @@ export class HUDShop extends BaseHUDPart {
const requiredHandle = handle.requireIndexToElement[i];
requiredHandle.container.remove();
requiredHandle.pinDetector.cleanup();
requiredHandle.infoDetector.cleanup();
}
// Cleanup
@ -122,6 +123,10 @@ export class HUDShop extends BaseHUDPart {
pinButton.classList.add("pin");
container.appendChild(pinButton);
const viewInfoButton = document.createElement("button");
viewInfoButton.classList.add("showInfo");
container.appendChild(viewInfoButton);
const currentGoalShape = this.root.hubGoals.currentGoal.definition.getHash();
if (shape === currentGoalShape) {
pinButton.classList.add("isGoal");
@ -145,6 +150,14 @@ export class HUDShop extends BaseHUDPart {
}
});
const infoDetector = new ClickDetector(viewInfoButton, {
consumeEvents: true,
preventDefault: true,
});
infoDetector.click.add(() =>
this.root.hud.signals.viewShapeDetailsRequested.dispatch(shapeDef)
);
handle.requireIndexToElement.push({
container,
progressLabel,
@ -152,6 +165,7 @@ export class HUDShop extends BaseHUDPart {
definition: shapeDef,
required: amount,
pinDetector,
infoDetector,
});
});
}
@ -202,6 +216,7 @@ export class HUDShop extends BaseHUDPart {
const requiredHandle = handle.requireIndexToElement[i];
requiredHandle.container.remove();
requiredHandle.pinDetector.cleanup();
requiredHandle.infoDetector.cleanup();
}
handle.requireIndexToElement = [];
}

View File

@ -333,7 +333,7 @@ export class ShapeDefinition extends BasicSerializableObject {
const quadrantSize = 10;
const quadrantHalfSize = quadrantSize / 2;
context.fillStyle = "rgba(40, 50, 65, 0.1)";
context.fillStyle = THEME.items.circleBackground;
context.beginCircle(0, 0, quadrantSize * 1.15);
context.fill();

View File

@ -8,7 +8,7 @@ import { SOUNDS } from "../platform/sound";
const avgSoundDurationSeconds = 0.25;
const maxOngoingSounds = 2;
const maxOngoingUiSounds = 25;
const maxOngoingUiSounds = 10;
// Proxy to the application sound instance
export class SoundProxy {

View File

@ -12,6 +12,8 @@
"directionLock": "rgb(74, 237, 134)",
"directionLockTrack": "rgba(74, 237, 134, 0.2)",
"colorBlindPickerTile": "rgba(255, 255, 255, 0.5)",
"resources": {
"shape": "#3d3f4a",
"red": "#4a3d3f",
@ -26,6 +28,7 @@
"items": {
"outline": "#111418",
"outlineWidth": 0.75
"outlineWidth": 0.75,
"circleBackground": "rgba(20, 30, 40, 0.3)"
}
}

View File

@ -12,6 +12,8 @@
"directionLock": "rgb(74, 237, 134)",
"directionLockTrack": "rgba(74, 237, 134, 0.2)",
"colorBlindPickerTile": "rgba(50, 50, 50, 0.4)",
"resources": {
"shape": "#eaebec",
"red": "#ffbfc1",
@ -27,6 +29,7 @@
"items": {
"outline": "#55575a",
"outlineWidth": 0.75
"outlineWidth": 0.75,
"circleBackground": "rgba(40, 50, 65, 0.1)"
}
}

View File

@ -22,6 +22,7 @@ export const SOUNDS = {
levelComplete: "level_complete",
destroyBuilding: "destroy_building",
placeBuilding: "place_building",
placeBelt: "place_belt",
};

View File

@ -172,6 +172,15 @@ export const allApplicationSettings = [
(app, value) => app.sound.setMusicMuted(value)
),
new BoolSetting(
"enableColorBlindHelper",
categoryApp,
/**
* @param {Application} app
*/
(app, value) => null
),
// GAME
new BoolSetting("offerHints", categoryGame, (app, value) => {}),
@ -269,6 +278,8 @@ class SettingsStorage {
this.compactBuildingInfo = false;
this.disableCutDeleteWarnings = false;
this.enableColorBlindHelper = false;
/**
* @type {Object.<string, number>}
*/
@ -468,7 +479,7 @@ export class ApplicationSettings extends ReadWriteProxy {
}
getCurrentVersion() {
return 16;
return 17;
}
/** @param {{settings: SettingsStorage, version: number}} data */
@ -536,6 +547,11 @@ export class ApplicationSettings extends ReadWriteProxy {
data.version = 16;
}
if (data.version < 17) {
data.settings.enableColorBlindHelper = false;
data.version = 17;
}
return ExplainedResult.good();
}
}

View File

@ -290,6 +290,17 @@ ingame:
clearSelection: Clear Selection
pipette: Pipette
# Names of the colors, used for the color blind mode
colors:
red: Red
green: Green
blue: Blue
yellow: Yellow
purple: Purple
cyan: Cyan
white: White
uncolored: No color
# Everything related to placing buildings (I.e. as soon as you selected a building
# from the toolbar)
buildingPlacement:
@ -384,6 +395,11 @@ ingame:
description: Left-click a marker to jump to it, right-click to delete it.<br><br>Press <keybinding> to create a marker from the current view, or <strong>right-click</strong> to create a marker at the selected location.
creationSuccessNotification: Marker has been created.
# Shape viewer
shapeViewer:
title: Layers
empty: Empty
# Interactive tutorial
interactiveTutorial:
title: Tutorial
@ -652,6 +668,11 @@ settings:
description: >-
Change the language. All translations are user contributed and might be incomplete!
enableColorBlindHelper:
title: Color Blind Mode
description: >-
Enables various tools which allow to play the game if you are color blind.
fullscreen:
title: Fullscreen
description: >-