Add modal to take a mental health break

This commit is contained in:
thepaperpilot 2024-03-17 00:50:22 -05:00
parent b98f6db1c4
commit 329b859fd6
19 changed files with 145 additions and 30 deletions

View file

@ -6,6 +6,7 @@
<Nav v-if="useHeader" /> <Nav v-if="useHeader" />
<Game /> <Game />
<TPS v-if="unref(showTPS)" /> <TPS v-if="unref(showTPS)" />
<AddictionWarning />
<GameOverScreen /> <GameOverScreen />
<NaNScreen /> <NaNScreen />
<CloudSaveResolver /> <CloudSaveResolver />
@ -17,15 +18,16 @@
<script setup lang="tsx"> <script setup lang="tsx">
import "@fontsource/roboto-mono"; import "@fontsource/roboto-mono";
import Error from "components/Error.vue"; import Error from "components/Error.vue";
import CloudSaveResolver from "components/saves/CloudSaveResolver.vue"; import AddictionWarning from "components/modals/AddictionWarning.vue";
import CloudSaveResolver from "components/modals/CloudSaveResolver.vue";
import GameOverScreen from "components/modals/GameOverScreen.vue";
import NaNScreen from "components/modals/NaNScreen.vue";
import { jsx } from "features/feature"; import { jsx } from "features/feature";
import state from "game/state"; import state from "game/state";
import { coerceComponent, render } from "util/vue"; import { coerceComponent, render } from "util/vue";
import type { CSSProperties } from "vue"; import type { CSSProperties } from "vue";
import { computed, toRef, unref } from "vue"; import { computed, toRef, unref } from "vue";
import Game from "./components/Game.vue"; import Game from "./components/Game.vue";
import GameOverScreen from "./components/GameOverScreen.vue";
import NaNScreen from "./components/NaNScreen.vue";
import Nav from "./components/Nav.vue"; import Nav from "./components/Nav.vue";
import TPS from "./components/TPS.vue"; import TPS from "./components/TPS.vue";
import projInfo from "./data/projInfo.json"; import projInfo from "./data/projInfo.json";

View file

@ -103,9 +103,9 @@ import { Direction } from "util/common";
import { galaxy, syncedSaves } from "util/galaxy"; import { galaxy, syncedSaves } from "util/galaxy";
import type { ComponentPublicInstance } from "vue"; import type { ComponentPublicInstance } from "vue";
import { computed, ref } from "vue"; import { computed, ref } from "vue";
import Info from "./Info.vue"; import Info from "./modals/Info.vue";
import Options from "./Options.vue"; import Options from "./modals/Options.vue";
import SavesManager from "./saves/SavesManager.vue"; import SavesManager from "./modals/SavesManager.vue";
const info = ref<ComponentPublicInstance<typeof Info> | null>(null); const info = ref<ComponentPublicInstance<typeof Info> | null>(null);
const savesManager = ref<ComponentPublicInstance<typeof SavesManager> | null>(null); const savesManager = ref<ComponentPublicInstance<typeof SavesManager> | null>(null);

View file

@ -0,0 +1,83 @@
<template>
<Modal v-model="isOpen" v-bind="$attrs">
<template v-slot:header>
<div class="vga-modal-header">
<h2>Kindly consider taking a break.</h2>
</div>
</template>
<template v-slot:body>
<p>
You've been actively enjoying this game for awhile recently - and it's great that
you've been having a good time! That said, there are dangers to games like these that you should be aware of:
</p>
<p>
While incremental games can be fun and even healthy in certain contexts, they can
exacerbate video game addiction even more than other genres. If you feel like
playing incremental games is taking priority over other things in your life, or
manipulating your sleep schedule, it may be prudent to seek help.
</p>
<p>
<h4>Resources:</h4>
<span>
<a style="display: inline" href="https://www.samhsa.gov/" target="_blank">
SAMHSA
</a>
(<a style="display: inline" href="tel:1-800-662-4357">1-800-662-HELP</a>)
</span>
<br />
<a href="https://www.reddit.com/r/StopGaming/">r/StopGaming</a>
</p>
</template>
<template v-slot:footer>
<div class="vga-footer">
<button @click="neverShow" class="button">Never show this again</button>
<button @click="isOpen = false" class="button">Close</button>
</div>
</template>
</Modal>
<SavesManager ref="savesManager" />
</template>
<script setup lang="ts">
import projInfo from "data/projInfo.json";
import settings from "game/settings";
import state from "game/state";
import { ref, watchEffect } from "vue";
import Modal from "./Modal.vue";
const isOpen = ref(false);
watchEffect(() => {
if (
projInfo.disableHealthWarning === false &&
settings.showHealthWarning &&
state.mouseActivity.filter(i => i).length > 6
) {
isOpen.value = true;
}
});
function neverShow() {
settings.showHealthWarning = false;
isOpen.value = false;
}
</script>
<style scoped>
.vga-modal-header {
padding-top: 10px;
margin-left: 10px;
}
.vga-footer {
display: flex;
justify-content: flex-end;
}
.vga-footer button {
margin: 0 10px;
}
p {
margin-bottom: 10px;
}
</style>

View file

@ -74,13 +74,13 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import Modal from "components/Modal.vue";
import { stringifySave } from "game/player"; import { stringifySave } from "game/player";
import settings from "game/settings"; import settings from "game/settings";
import LZString from "lz-string"; import LZString from "lz-string";
import { conflictingSaves, galaxy } from "util/galaxy"; import { conflictingSaves, galaxy } from "util/galaxy";
import { getUniqueID, save, setupInitialStore } from "util/save"; import { getUniqueID, save, setupInitialStore } from "util/save";
import { ComponentPublicInstance, computed, ref, unref, watch } from "vue"; import { ComponentPublicInstance, computed, ref, unref, watch } from "vue";
import Modal from "./Modal.vue";
import Save from "./Save.vue"; import Save from "./Save.vue";
const isOpen = ref(false); const isOpen = ref(false);

View file

@ -37,14 +37,14 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import Modal from "components/Modal.vue";
import { hasWon } from "data/projEntry"; import { hasWon } from "data/projEntry";
import projInfo from "data/projInfo.json"; import projInfo from "data/projInfo.json";
import player from "game/player"; import player from "game/player";
import { formatTime } from "util/bignum"; import { formatTime } from "util/bignum";
import { loadSave, newSave } from "util/save"; import { loadSave, newSave } from "util/save";
import { computed, toRef } from "vue"; import { computed, toRef } from "vue";
import Toggle from "./fields/Toggle.vue"; import Toggle from "../fields/Toggle.vue";
import Modal from "./Modal.vue";
const { title, logo, discordName, discordLink, versionNumber, versionTitle } = projInfo; const { title, logo, discordName, discordLink, versionNumber, versionTitle } = projInfo;

View file

@ -60,7 +60,6 @@
</template> </template>
<script setup lang="tsx"> <script setup lang="tsx">
import Modal from "components/Modal.vue";
import type Changelog from "data/Changelog.vue"; import type Changelog from "data/Changelog.vue";
import projInfo from "data/projInfo.json"; import projInfo from "data/projInfo.json";
import { jsx } from "features/feature"; import { jsx } from "features/feature";
@ -69,6 +68,7 @@ import { infoComponents } from "game/settings";
import { formatTime } from "util/bignum"; import { formatTime } from "util/bignum";
import { coerceComponent, render } from "util/vue"; import { coerceComponent, render } from "util/vue";
import { computed, ref, toRefs, unref } from "vue"; import { computed, ref, toRefs, unref } from "vue";
import Modal from "./Modal.vue";
const { title, logo, author, discordName, discordLink, versionNumber, versionTitle } = projInfo; const { title, logo, author, discordName, discordLink, versionNumber, versionTitle } = projInfo;

View file

@ -42,7 +42,7 @@
<script setup lang="ts"> <script setup lang="ts">
import type { FeatureNode } from "game/layers"; import type { FeatureNode } from "game/layers";
import { computed, ref, toRefs, unref } from "vue"; import { computed, ref, toRefs, unref } from "vue";
import Context from "./Context.vue"; import Context from "../Context.vue";
const _props = defineProps<{ const _props = defineProps<{
modelValue: boolean; modelValue: boolean;

View file

@ -46,7 +46,6 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import Modal from "components/Modal.vue";
import projInfo from "data/projInfo.json"; import projInfo from "data/projInfo.json";
import player from "game/player"; import player from "game/player";
import state from "game/state"; import state from "game/state";
@ -54,8 +53,9 @@ import type { DecimalSource } from "util/bignum";
import Decimal, { format } from "util/bignum"; import Decimal, { format } from "util/bignum";
import type { ComponentPublicInstance } from "vue"; import type { ComponentPublicInstance } from "vue";
import { computed, ref, toRef, watch } from "vue"; import { computed, ref, toRef, watch } from "vue";
import Toggle from "./fields/Toggle.vue"; import Toggle from "../fields/Toggle.vue";
import SavesManager from "./saves/SavesManager.vue"; import Modal from "./Modal.vue";
import SavesManager from "./SavesManager.vue";
const { discordName, discordLink } = projInfo; const { discordName, discordLink } = projInfo;
const autosave = ref(true); const autosave = ref(true);

View file

@ -14,6 +14,7 @@
<Toggle :title="unthrottledTitle" v-model="unthrottled" /> <Toggle :title="unthrottledTitle" v-model="unthrottled" />
<Toggle v-if="projInfo.enablePausing" :title="isPausedTitle" v-model="isPaused" /> <Toggle v-if="projInfo.enablePausing" :title="isPausedTitle" v-model="isPaused" />
<Toggle :title="offlineProdTitle" v-model="offlineProd" /> <Toggle :title="offlineProdTitle" v-model="offlineProd" />
<Toggle :title="showHealthWarningTitle" v-model="showHealthWarning" v-if="!projInfo.disableHealthWarning" />
<Toggle :title="autosaveTitle" v-model="autosave" /> <Toggle :title="autosaveTitle" v-model="autosave" />
<FeedbackButton v-if="!autosave" class="button save-button" @click="save()">Manually save</FeedbackButton> <FeedbackButton v-if="!autosave" class="button save-button" @click="save()">Manually save</FeedbackButton>
</div> </div>
@ -28,20 +29,20 @@
</template> </template>
<script setup lang="tsx"> <script setup lang="tsx">
import Modal from "components/Modal.vue";
import projInfo from "data/projInfo.json"; import projInfo from "data/projInfo.json";
import { save } from "util/save";
import rawThemes from "data/themes"; import rawThemes from "data/themes";
import { jsx } from "features/feature"; import { jsx } from "features/feature";
import Tooltip from "features/tooltips/Tooltip.vue"; import Tooltip from "features/tooltips/Tooltip.vue";
import player from "game/player"; import player from "game/player";
import settings, { settingFields } from "game/settings"; import settings, { settingFields } from "game/settings";
import { camelToTitle, Direction } from "util/common"; import { camelToTitle, Direction } from "util/common";
import { save } from "util/save";
import { coerceComponent, render } from "util/vue"; import { coerceComponent, render } from "util/vue";
import { computed, ref, toRefs } from "vue"; import { computed, ref, toRefs } from "vue";
import Select from "./fields/Select.vue"; import FeedbackButton from "../fields/FeedbackButton.vue";
import Toggle from "./fields/Toggle.vue"; import Select from "../fields/Select.vue";
import FeedbackButton from "./fields/FeedbackButton.vue"; import Toggle from "../fields/Toggle.vue";
import Modal from "./Modal.vue";
const isOpen = ref(false); const isOpen = ref(false);
const currentTab = ref("behaviour"); const currentTab = ref("behaviour");
@ -72,7 +73,7 @@ const settingFieldsComponent = computed(() => {
return coerceComponent(jsx(() => (<>{settingFields.map(render)}</>))); return coerceComponent(jsx(() => (<>{settingFields.map(render)}</>)));
}); });
const { showTPS, theme, unthrottled, alignUnits } = toRefs(settings); const { showTPS, theme, unthrottled, alignUnits, showHealthWarning } = toRefs(settings);
const { autosave, offlineProd } = toRefs(player); const { autosave, offlineProd } = toRefs(player);
const isPaused = computed({ const isPaused = computed({
get() { get() {
@ -91,10 +92,16 @@ const unthrottledTitle = jsx(() => (
)); ));
const offlineProdTitle = jsx(() => ( const offlineProdTitle = jsx(() => (
<span class="option-title"> <span class="option-title">
Offline Production<Tooltip display="Save-specific" direction={Direction.Right}>*</Tooltip> Offline production<Tooltip display="Save-specific" direction={Direction.Right}>*</Tooltip>
<desc>Simulate production that occurs while the game is closed.</desc> <desc>Simulate production that occurs while the game is closed.</desc>
</span> </span>
)); ));
const showHealthWarningTitle = jsx(() => (
<span class="option-title">
Show videogame addiction warning
<desc>Show a helpful warning after playing for a long time about video game addiction and encouraging you to take a break.</desc>
</span>
));
const autosaveTitle = jsx(() => ( const autosaveTitle = jsx(() => (
<span class="option-title"> <span class="option-title">
Autosave<Tooltip display="Save-specific" direction={Direction.Right}>*</Tooltip> Autosave<Tooltip display="Save-specific" direction={Direction.Right}>*</Tooltip>

View file

@ -60,12 +60,12 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import Modal from "components/Modal.vue";
import projInfo from "data/projInfo.json"; import projInfo from "data/projInfo.json";
import type { Player } from "game/player"; import type { Player } from "game/player";
import player, { stringifySave } from "game/player"; import player, { stringifySave } from "game/player";
import settings from "game/settings"; import settings from "game/settings";
import LZString from "lz-string"; import LZString from "lz-string";
import { galaxy, syncedSaves } from "util/galaxy";
import { import {
clearCachedSave, clearCachedSave,
clearCachedSaves, clearCachedSaves,
@ -81,8 +81,8 @@ import { computed, nextTick, ref, watch } from "vue";
import Draggable from "vuedraggable"; import Draggable from "vuedraggable";
import Select from "../fields/Select.vue"; import Select from "../fields/Select.vue";
import Text from "../fields/Text.vue"; import Text from "../fields/Text.vue";
import Modal from "./Modal.vue";
import Save from "./Save.vue"; import Save from "./Save.vue";
import { galaxy, syncedSaves } from "util/galaxy";
export type LoadablePlayerData = Omit<Partial<Player>, "id"> & { id: string; error?: unknown }; export type LoadablePlayerData = Omit<Partial<Player>, "id"> & { id: string; error?: unknown };

View file

@ -19,7 +19,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import Modal from "components/Modal.vue"; import Modal from "components/modals/Modal.vue";
import { ref } from "vue"; import { ref } from "vue";
const isOpen = ref(false); const isOpen = ref(false);

View file

@ -88,6 +88,10 @@
"type": "string", "type": "string",
"enum": ["base64", "lz", "plain"], "enum": ["base64", "lz", "plain"],
"description": "The encoding to use when exporting to the clipboard. Plain-text is fast to generate but is easiest for the player to manipulate and cheat with. Base 64 is slightly slower and the string will be longer but will offer a small barrier to people trying to cheat. LZ-String is the slowest method, but produces the smallest strings and still offers a small barrier to those trying to cheat. Some sharing platforms like pastebin may automatically delete base64 encoded text, and some sites might not support all the characters used in lz-string exports." "description": "The encoding to use when exporting to the clipboard. Plain-text is fast to generate but is easiest for the player to manipulate and cheat with. Base 64 is slightly slower and the string will be longer but will offer a small barrier to people trying to cheat. LZ-String is the slowest method, but produces the smallest strings and still offers a small barrier to those trying to cheat. Some sharing platforms like pastebin may automatically delete base64 encoded text, and some sites might not support all the characters used in lz-string exports."
},
"disableHealthWarning": {
"type": "boolean",
"description": "Whether or not to disable the health warning that appears to the player after excessive playtime (activity during 6 of the last 8 hours). If left enabled, the player will still be able to individually turn off the health warning in settings or by clicking \"Do not show again\" in the warning itself."
} }
} }
} }

View file

@ -22,5 +22,6 @@
"maxTickLength": 3600, "maxTickLength": 3600,
"offlineLimit": 1, "offlineLimit": 1,
"enablePausing": true, "enablePausing": true,
"exportEncoding": "base64" "exportEncoding": "base64",
"disableHealthWarning": false
} }

View file

@ -2,6 +2,7 @@ import type { Settings } from "game/settings";
import { createNanoEvents } from "nanoevents"; import { createNanoEvents } from "nanoevents";
import type { App } from "vue"; import type { App } from "vue";
import type { GenericLayer } from "./layers"; import type { GenericLayer } from "./layers";
import state from "./state";
/** All types of events able to be sent or emitted from the global event bus. */ /** All types of events able to be sent or emitted from the global event bus. */
export interface GlobalEvents { export interface GlobalEvents {
@ -59,3 +60,7 @@ if ("fonts" in document) {
// JSDom doesn't add document.fonts, and Object.defineProperty doesn't seem to work on document // JSDom doesn't add document.fonts, and Object.defineProperty doesn't seem to work on document
document.fonts.onloadingdone = () => globalBus.emit("fontsLoaded"); document.fonts.onloadingdone = () => globalBus.emit("fontsLoaded");
} }
document.onmousemove = function () {
state.mouseActivity[state.mouseActivity.length - 1] = true;
};

View file

@ -107,3 +107,7 @@ export async function startGameLoop() {
intervalID = setInterval(update, 50); intervalID = setInterval(update, 50);
} }
} }
setInterval(() => {
state.mouseActivity = [...state.mouseActivity.slice(-7), false];
}, 1000 * 60 * 60);

View file

@ -1,4 +1,4 @@
import Modal from "components/Modal.vue"; import Modal from "components/modals/Modal.vue";
import type { import type {
CoercableComponent, CoercableComponent,
JSXFunction, JSXFunction,

View file

@ -20,6 +20,8 @@ export interface Settings {
unthrottled: boolean; unthrottled: boolean;
/** Whether to align modifiers to the unit. */ /** Whether to align modifiers to the unit. */
alignUnits: boolean; alignUnits: boolean;
/** Whether or not to show a video game health warning after playing excessively. */
showHealthWarning: boolean;
} }
const state = reactive<Partial<Settings>>({ const state = reactive<Partial<Settings>>({
@ -28,7 +30,8 @@ const state = reactive<Partial<Settings>>({
showTPS: true, showTPS: true,
theme: Themes.Nordic, theme: Themes.Nordic,
unthrottled: false, unthrottled: false,
alignUnits: false alignUnits: false,
showHealthWarning: true
}); });
watch( watch(
@ -56,12 +59,15 @@ declare global {
export default window.settings = state as Settings; export default window.settings = state as Settings;
/** A function that erases all player settings, including all saves. */ /** A function that erases all player settings, including all saves. */
export const hardResetSettings = (window.hardResetSettings = () => { export const hardResetSettings = (window.hardResetSettings = () => {
const settings = { // Only partial because of any properties that are only added during the loadSettings event.
const settings: Partial<Settings> = {
active: "", active: "",
saves: [], saves: [],
showTPS: true, showTPS: true,
theme: Themes.Nordic, theme: Themes.Nordic,
alignUnits: false unthrottled: false,
alignUnits: false,
showHealthWarning: true
}; };
globalBus.emit("loadSettings", settings); globalBus.emit("loadSettings", settings);
Object.assign(state, settings); Object.assign(state, settings);

View file

@ -6,6 +6,8 @@ import type { Persistent } from "./persistence";
export interface Transient { export interface Transient {
/** A list of the duration, in ms, of the last 10 game ticks. Used for calculating TPS. */ /** A list of the duration, in ms, of the last 10 game ticks. Used for calculating TPS. */
lastTenTicks: number[]; lastTenTicks: number[];
/** A list of bools represnting which of the last few hours had mouse activity. */
mouseActivity: boolean[];
/** Whether or not a NaN value has been detected and undealt with. */ /** Whether or not a NaN value has been detected and undealt with. */
hasNaN: boolean; hasNaN: boolean;
/** The location within the player save data object of the NaN value. */ /** The location within the player save data object of the NaN value. */
@ -25,6 +27,7 @@ declare global {
/** The global transient state object. */ /** The global transient state object. */
export default window.state = shallowReactive<Transient>({ export default window.state = shallowReactive<Transient>({
lastTenTicks: [], lastTenTicks: [],
mouseActivity: [false],
hasNaN: false, hasNaN: false,
NaNPath: [], NaNPath: [],
errors: reactive([]) errors: reactive([])