diff --git a/src/App.vue b/src/App.vue index 6a365ef..ebdf971 100644 --- a/src/App.vue +++ b/src/App.vue @@ -6,6 +6,7 @@ <Nav v-if="useHeader" /> <Game /> <TPS v-if="unref(showTPS)" /> + <AddictionWarning /> <GameOverScreen /> <NaNScreen /> <component :is="gameComponent" /> @@ -16,14 +17,14 @@ <script setup lang="tsx"> import "@fontsource/roboto-mono"; import Error from "components/Error.vue"; +import AddictionWarning from "components/modals/AddictionWarning.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 { CSSProperties, watch } from "vue"; -import { computed, toRef, unref } from "vue"; +import { CSSProperties, 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"; @@ -69,3 +70,4 @@ const gameComponent = computed(() => { position: static; } </style> +./components/modals/GameOverScreen.vue./components/modals/NaNScreen.vue \ No newline at end of file diff --git a/src/components/Nav.vue b/src/components/Nav.vue index c5d0026..4c8ef93 100644 --- a/src/components/Nav.vue +++ b/src/components/Nav.vue @@ -101,9 +101,9 @@ import Tooltip from "features/tooltips/Tooltip.vue"; import { Direction } from "util/common"; import type { ComponentPublicInstance } from "vue"; import { ref } from "vue"; -import Info from "./Info.vue"; -import Options from "./Options.vue"; -import SavesManager from "./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); @@ -265,3 +265,4 @@ function openDiscord() { text-shadow: none; } </style> +./modals/Info.vue./modals/Options.vue./modals/SavesManager.vue diff --git a/src/components/modals/AddictionWarning.vue b/src/components/modals/AddictionWarning.vue new file mode 100644 index 0000000..14eaf1e --- /dev/null +++ b/src/components/modals/AddictionWarning.vue @@ -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> diff --git a/src/components/GameOverScreen.vue b/src/components/modals/GameOverScreen.vue similarity index 97% rename from src/components/GameOverScreen.vue rename to src/components/modals/GameOverScreen.vue index 999554d..e7e375a 100644 --- a/src/components/GameOverScreen.vue +++ b/src/components/modals/GameOverScreen.vue @@ -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; diff --git a/src/components/Info.vue b/src/components/modals/Info.vue similarity index 98% rename from src/components/Info.vue rename to src/components/modals/Info.vue index 15395bf..12be65d 100644 --- a/src/components/Info.vue +++ b/src/components/modals/Info.vue @@ -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; diff --git a/src/components/Modal.vue b/src/components/modals/Modal.vue similarity index 98% rename from src/components/Modal.vue rename to src/components/modals/Modal.vue index 9fd5f06..728807e 100644 --- a/src/components/Modal.vue +++ b/src/components/modals/Modal.vue @@ -41,7 +41,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; diff --git a/src/components/NaNScreen.vue b/src/components/modals/NaNScreen.vue similarity index 97% rename from src/components/NaNScreen.vue rename to src/components/modals/NaNScreen.vue index f9c7b6f..f8735f4 100644 --- a/src/components/NaNScreen.vue +++ b/src/components/modals/NaNScreen.vue @@ -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,7 +53,8 @@ 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 Toggle from "../fields/Toggle.vue"; +import Modal from "./Modal.vue"; import SavesManager from "./SavesManager.vue"; const { discordName, discordLink } = projInfo; diff --git a/src/components/Options.vue b/src/components/modals/Options.vue similarity index 86% rename from src/components/Options.vue rename to src/components/modals/Options.vue index 93b57fa..11df99e 100644 --- a/src/components/Options.vue +++ b/src/components/modals/Options.vue @@ -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> diff --git a/src/components/Save.vue b/src/components/modals/Save.vue similarity index 96% rename from src/components/Save.vue rename to src/components/modals/Save.vue index 77d2988..1876860 100644 --- a/src/components/Save.vue +++ b/src/components/modals/Save.vue @@ -74,9 +74,9 @@ import Tooltip from "features/tooltips/Tooltip.vue"; import player from "game/player"; import { Direction } from "util/common"; import { computed, ref, toRefs, watch } from "vue"; -import DangerButton from "./fields/DangerButton.vue"; -import FeedbackButton from "./fields/FeedbackButton.vue"; -import Text from "./fields/Text.vue"; +import DangerButton from "../fields/DangerButton.vue"; +import FeedbackButton from "../fields/FeedbackButton.vue"; +import Text from "../fields/Text.vue"; import type { LoadablePlayerData } from "./SavesManager.vue"; const _props = defineProps<{ @@ -202,3 +202,4 @@ function changeName() { margin: 0; } </style> +./modals/SavesManager.vue diff --git a/src/components/SavesManager.vue b/src/components/modals/SavesManager.vue similarity index 98% rename from src/components/SavesManager.vue rename to src/components/modals/SavesManager.vue index b1bf7e0..f40cec8 100644 --- a/src/components/SavesManager.vue +++ b/src/components/modals/SavesManager.vue @@ -57,7 +57,6 @@ </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"; @@ -67,8 +66,9 @@ import { getUniqueID, loadSave, newSave, save } from "util/save"; import type { ComponentPublicInstance } from "vue"; import { computed, nextTick, ref, shallowReactive, watch } from "vue"; import Draggable from "vuedraggable"; -import Select from "./fields/Select.vue"; -import Text from "./fields/Text.vue"; +import Select from "../fields/Select.vue"; +import Text from "../fields/Text.vue"; +import Modal from "./Modal.vue"; import Save from "./Save.vue"; export type LoadablePlayerData = Omit<Partial<Player>, "id"> & { id: string; error?: unknown }; @@ -332,3 +332,4 @@ function editSave(id: string, newName: string) { visibility: hidden; } </style> +./Save.vue diff --git a/src/data/Changelog.vue b/src/data/Changelog.vue index 54e53a5..675fe38 100644 --- a/src/data/Changelog.vue +++ b/src/data/Changelog.vue @@ -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); diff --git a/src/data/projInfo-schema.json b/src/data/projInfo-schema.json index 0cea13c..ceb448e 100644 --- a/src/data/projInfo-schema.json +++ b/src/data/projInfo-schema.json @@ -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." } } } \ No newline at end of file diff --git a/src/data/projInfo.json b/src/data/projInfo.json index b32ef22..6618d79 100644 --- a/src/data/projInfo.json +++ b/src/data/projInfo.json @@ -22,5 +22,6 @@ "maxTickLength": 3600, "offlineLimit": 1, "enablePausing": true, - "exportEncoding": "base64" + "exportEncoding": "base64", + "disableHealthWarning": false } diff --git a/src/game/events.ts b/src/game/events.ts index a167042..9e74714 100644 --- a/src/game/events.ts +++ b/src/game/events.ts @@ -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; +}; diff --git a/src/game/gameLoop.ts b/src/game/gameLoop.ts index b001dd6..a50476d 100644 --- a/src/game/gameLoop.ts +++ b/src/game/gameLoop.ts @@ -107,3 +107,7 @@ export async function startGameLoop() { intervalID = setInterval(update, 50); } } + +setInterval(() => { + state.mouseActivity = [...state.mouseActivity.slice(-7), false]; +}, 1000 * 60 * 60); diff --git a/src/game/layers.tsx b/src/game/layers.tsx index bbb6079..7e82ead 100644 --- a/src/game/layers.tsx +++ b/src/game/layers.tsx @@ -1,4 +1,4 @@ -import Modal from "components/Modal.vue"; +import Modal from "components/modals/Modal.vue"; import type { CoercableComponent, JSXFunction, diff --git a/src/game/settings.ts b/src/game/settings.ts index 0748d68..52ff08d 100644 --- a/src/game/settings.ts +++ b/src/game/settings.ts @@ -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); diff --git a/src/game/state.ts b/src/game/state.ts index 9e672c3..95c17bb 100644 --- a/src/game/state.ts +++ b/src/game/state.ts @@ -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([])