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

View file

@ -103,9 +103,9 @@ import { Direction } from "util/common";
import { galaxy, syncedSaves } from "util/galaxy";
import type { ComponentPublicInstance } from "vue";
import { computed, ref } from "vue";
import Info from "./Info.vue";
import Options from "./Options.vue";
import SavesManager from "./saves/SavesManager.vue";
import Info from "./modals/Info.vue";
import Options from "./modals/Options.vue";
import SavesManager from "./modals/SavesManager.vue";
const info = ref<ComponentPublicInstance<typeof Info> | 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>
<script setup lang="ts">
import Modal from "components/Modal.vue";
import { stringifySave } from "game/player";
import settings from "game/settings";
import LZString from "lz-string";
import { conflictingSaves, galaxy } from "util/galaxy";
import { getUniqueID, save, setupInitialStore } from "util/save";
import { ComponentPublicInstance, computed, ref, unref, watch } from "vue";
import Modal from "./Modal.vue";
import Save from "./Save.vue";
const isOpen = ref(false);

View file

@ -37,14 +37,14 @@
</template>
<script setup lang="ts">
import Modal from "components/Modal.vue";
import { hasWon } from "data/projEntry";
import projInfo from "data/projInfo.json";
import player from "game/player";
import { formatTime } from "util/bignum";
import { loadSave, newSave } from "util/save";
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;

View file

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

View file

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

View file

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

View file

@ -14,6 +14,7 @@
<Toggle :title="unthrottledTitle" v-model="unthrottled" />
<Toggle v-if="projInfo.enablePausing" :title="isPausedTitle" v-model="isPaused" />
<Toggle :title="offlineProdTitle" v-model="offlineProd" />
<Toggle :title="showHealthWarningTitle" v-model="showHealthWarning" v-if="!projInfo.disableHealthWarning" />
<Toggle :title="autosaveTitle" v-model="autosave" />
<FeedbackButton v-if="!autosave" class="button save-button" @click="save()">Manually save</FeedbackButton>
</div>
@ -28,20 +29,20 @@
</template>
<script setup lang="tsx">
import Modal from "components/Modal.vue";
import projInfo from "data/projInfo.json";
import { save } from "util/save";
import rawThemes from "data/themes";
import { jsx } from "features/feature";
import Tooltip from "features/tooltips/Tooltip.vue";
import player from "game/player";
import settings, { settingFields } from "game/settings";
import { camelToTitle, Direction } from "util/common";
import { save } from "util/save";
import { coerceComponent, render } from "util/vue";
import { computed, ref, toRefs } from "vue";
import Select from "./fields/Select.vue";
import Toggle from "./fields/Toggle.vue";
import FeedbackButton from "./fields/FeedbackButton.vue";
import FeedbackButton from "../fields/FeedbackButton.vue";
import Select from "../fields/Select.vue";
import Toggle from "../fields/Toggle.vue";
import Modal from "./Modal.vue";
const isOpen = ref(false);
const currentTab = ref("behaviour");
@ -72,7 +73,7 @@ const settingFieldsComponent = computed(() => {
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 isPaused = computed({
get() {
@ -91,10 +92,16 @@ const unthrottledTitle = jsx(() => (
));
const offlineProdTitle = jsx(() => (
<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>
</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(() => (
<span class="option-title">
Autosave<Tooltip display="Save-specific" direction={Direction.Right}>*</Tooltip>

View file

@ -60,12 +60,12 @@
</template>
<script setup lang="ts">
import Modal from "components/Modal.vue";
import projInfo from "data/projInfo.json";
import type { Player } from "game/player";
import player, { stringifySave } from "game/player";
import settings from "game/settings";
import LZString from "lz-string";
import { galaxy, syncedSaves } from "util/galaxy";
import {
clearCachedSave,
clearCachedSaves,
@ -81,8 +81,8 @@ import { computed, nextTick, ref, watch } from "vue";
import Draggable from "vuedraggable";
import Select from "../fields/Select.vue";
import Text from "../fields/Text.vue";
import Modal from "./Modal.vue";
import Save from "./Save.vue";
import { galaxy, syncedSaves } from "util/galaxy";
export type LoadablePlayerData = Omit<Partial<Player>, "id"> & { id: string; error?: unknown };
@ -311,4 +311,4 @@ function editSave(id: string, newName: string) {
.presets .vue-select[aria-expanded="true"] vue-dropdown {
visibility: hidden;
}
</style>
</style>

View file

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

View file

@ -88,6 +88,10 @@
"type": "string",
"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."
},
"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,
"offlineLimit": 1,
"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 type { App } from "vue";
import type { GenericLayer } from "./layers";
import state from "./state";
/** All types of events able to be sent or emitted from the global event bus. */
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
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);
}
}
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 {
CoercableComponent,
JSXFunction,

View file

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

View file

@ -6,6 +6,8 @@ import type { Persistent } from "./persistence";
export interface Transient {
/** A list of the duration, in ms, of the last 10 game ticks. Used for calculating TPS. */
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. */
hasNaN: boolean;
/** The location within the player save data object of the NaN value. */
@ -25,6 +27,7 @@ declare global {
/** The global transient state object. */
export default window.state = shallowReactive<Transient>({
lastTenTicks: [],
mouseActivity: [false],
hasNaN: false,
NaNPath: [],
errors: reactive([])