diff --git a/src/components/board/Board.vue b/src/components/board/Board.vue index 78d6ca0..1b2eb99 100644 --- a/src/components/board/Board.vue +++ b/src/components/board/Board.vue @@ -2,16 +2,24 @@ + + + + + number)(); @@ -186,6 +192,9 @@ export default defineComponent({ } this.dragging = null; + } else if (!this.hasDragged) { + player.layers[this.layer].boards[this.id].selectedNode = null; + player.layers[this.layer].boards[this.id].selectedAction = null; } } } @@ -202,4 +211,9 @@ export default defineComponent({ #g1 { transition-duration: 0s; } + +.link-enter-from, +.link-leave-to { + opacity: 0; +} diff --git a/src/components/board/BoardLink.vue b/src/components/board/BoardLink.vue new file mode 100644 index 0000000..28af0ba --- /dev/null +++ b/src/components/board/BoardLink.vue @@ -0,0 +1,55 @@ + + + + + diff --git a/src/components/board/BoardNode.vue b/src/components/board/BoardNode.vue index c4d5746..fb5405d 100644 --- a/src/components/board/BoardNode.vue +++ b/src/components/board/BoardNode.vue @@ -6,10 +6,12 @@ > + - - {{ action.icon }} + + {{ + typeof action.icon === "function" ? action.icon(node) : action.icon + }} @@ -31,7 +46,9 @@ @mouseenter="mouseEnter" @mouseleave="mouseLeave" @mousedown="mouseDown" + @touchstart="mouseDown" @mouseup="mouseUp" + @touchend="mouseUp" > {{ title }} + + + {{ label.text }} + + + + Tap again to confirm + @@ -124,14 +164,12 @@ import themes from "@/data/themes"; import { ProgressDisplay, Shape } from "@/game/enums"; import { layers } from "@/game/layers"; import player from "@/game/player"; -import { BoardNode, BoardNodeAction, NodeType } from "@/typings/features/board"; +import { BoardNode, BoardNodeAction, NodeLabel, NodeType } from "@/typings/features/board"; import { getNodeTypeProperty } from "@/util/features"; -import { InjectLayerMixin } from "@/util/vue"; import { defineComponent, PropType } from "vue"; export default defineComponent({ name: "BoardNode", - mixins: [InjectLayerMixin], data() { return { ProgressDisplay, @@ -174,6 +212,9 @@ export default defineComponent({ selected() { return this.board.selectedNode?.id === this.node.id; }, + selectedAction() { + return this.board.selectedAction; + }, actions(): BoardNodeAction[] | null | undefined { return getNodeTypeProperty(this.nodeType, this.node, "actions"); }, @@ -203,6 +244,9 @@ export default defineComponent({ title(): string { return getNodeTypeProperty(this.nodeType, this.node, "title"); }, + label(): NodeLabel | null | undefined { + return getNodeTypeProperty(this.nodeType, this.node, "label"); + }, progress(): number { return getNodeTypeProperty(this.nodeType, this.node, "progress") || 0; }, @@ -263,8 +307,14 @@ export default defineComponent({ mouseLeave() { this.hovering = false; }, - performAction(action: BoardNodeAction) { + performAction(e: MouseEvent, action: BoardNodeAction) { action.onClick(this.node); + // If the onClick function made this action selected, + // don't propagate the event (which will deselect everything) + if (this.board.selectedAction === action) { + e.preventDefault(); + e.stopPropagation(); + } } }, watch: { @@ -295,11 +345,13 @@ export default defineComponent({ transform: rotate(-90deg); } -.action:hover circle { +.action:hover circle, +.action.selected circle { r: 25; } -.action:hover text { +.action:hover text, +.action.selected text { font-size: 187.5%; /* 150% * 1.25 */ } @@ -307,6 +359,29 @@ export default defineComponent({ text-anchor: middle; dominant-baseline: central; } + +.fade-enter-from, +.fade-leave-to { + opacity: 0; +} + +.pulsing { + animation: pulsing 2s ease-in infinite; +} + +@keyframes pulsing { + 0% { + opacity: 0.25; + } + + 50% { + opacity: 1; + } + + 100% { + opacity: 0.25; + } +}
" + - (node.data as ActionNodeData).log.join("
") + + "
" + + (node.data as ActionNodeData).log + .map(log => { + let display = log.description; + if (log.effectDescription) { + display += `
${log.effectDescription}
`; + } + return display; + }) + .join("
") + "
" ); break; diff --git a/src/data/layers/main.ts b/src/data/layers/main.ts index d15d95c..03effcc 100644 --- a/src/data/layers/main.ts +++ b/src/data/layers/main.ts @@ -1,8 +1,10 @@ import { ProgressDisplay, Shape } from "@/game/enums"; +import { layers } from "@/game/layers"; import player from "@/game/player"; import Decimal, { DecimalSource } from "@/lib/break_eternity"; import { RawLayer } from "@/typings/layer"; import { camelToTitle } from "@/util/common"; +import { getUniqueNodeID } from "@/util/features"; import themes from "../themes"; import Main from "./Main.vue"; @@ -19,9 +21,64 @@ export type ItemNodeData = { export type ActionNodeData = { actionType: string; - log: string[]; + log: LogEntry[]; }; +export type LogEntry = { + description: string; + effectDescription?: string; +}; + +export type WeightedEvent = { + event: () => LogEntry; + weight: number; +}; + +const redditEvents = [ + { + event: () => ({ description: "You blink and half an hour has passed before you know it." }), + weight: 1 + }, + { + event: () => { + const id = getUniqueNodeID(layers.main.boards!.data.main); + player.layers.main.boards.main.nodes.push({ + id, + position: { x: 0, y: 150 }, // TODO function to get nearest unoccupied space + type: "item", + data: { + itemType: "speed", + amount: new Decimal(15 * 60) + } + }); + return { + description: "You found some funny memes and actually feel a bit refreshed.", + effectDescription: `Added Speed node` + }; + }, + weight: 0.5 + } +]; + +function getRandomEvent(events: WeightedEvent[]): LogEntry | null { + if (events.length === 0) { + return null; + } + const totalWeight = events.reduce((acc, curr) => acc + curr.weight, 0); + const random = Math.random() * totalWeight; + + let weight = 0; + for (const outcome of events) { + weight += outcome.weight; + if (random <= weight) { + return outcome.event(); + } + } + + // Should never reach here + return null; +} + export default { id: "main", display: Main, @@ -80,6 +137,21 @@ export default { title(node) { return (node.data as ResourceNodeData).resourceType; }, + label(node) { + if (player.layers[this.layer].boards[this.id].selectedAction == null) { + return null; + } + const action = player.layers[this.layer].boards[this.id].selectedAction; + switch (action) { + default: + return null; + case "reddit": + if ((node.data as ResourceNodeData).resourceType === "time") { + return { text: "30m", color: "red", pulsing: true }; + } + return null; + } + }, draggable: true, progress(node) { const data = node.data as ResourceNodeData; @@ -109,6 +181,10 @@ export default { otherNode ); player.layers[this.layer].boards[this.id].nodes.splice(index, 1); + (node.data as ResourceNodeData).amount = Decimal.add( + (node.data as ResourceNodeData).amount, + (otherNode.data as ItemNodeData).amount + ); } }, item: { @@ -134,6 +210,9 @@ export default { { id: "info", icon: "history_edu", + fillColor() { + return themes[player.theme].variables["--separator"]; + }, tooltip: "Log", onClick(node) { player.layers.main.openNode = node.id; @@ -145,7 +224,44 @@ export default { icon: "reddit", tooltip: "Browse Reddit", onClick(node) { - // TODO + if (player.layers.main.boards.main.selectedAction === this.id) { + const timeNode = player.layers.main.boards.main.nodes.find( + node => + node.type === "resource" && + (node.data as ResourceNodeData).resourceType === + "time" + ); + if (timeNode) { + (timeNode.data as ResourceNodeData).amount = Decimal.sub( + (timeNode.data as ResourceNodeData).amount, + 30 * 60 + ); + player.layers.main.boards.main.selectedAction = null; + (node.data as ActionNodeData).log.push( + getRandomEvent(redditEvents)! + ); + } + } else { + player.layers.main.boards.main.selectedAction = this.id; + } + }, + links(node) { + return [ + { + // TODO this is ridiculous and needs some utility + // function to shrink it down + from: player.layers.main.boards.main.nodes.find( + node => + node.type === "resource" && + (node.data as ResourceNodeData).resourceType === + "time" + ), + to: node, + stroke: "red", + "stroke-width": 4, + pulsing: true + } + ]; } } ] diff --git a/src/data/mod.ts b/src/data/mod.ts index 1c3ef96..3035aee 100644 --- a/src/data/mod.ts +++ b/src/data/mod.ts @@ -1,6 +1,7 @@ import { RawLayer } from "@/typings/layer"; import { PlayerData } from "@/typings/player"; import Decimal from "@/util/bignum"; +import { hardReset } from "@/util/save"; import { computed } from "vue"; import main from "./layers/main"; diff --git a/src/game/layers.ts b/src/game/layers.ts index a1b62c1..495aa50 100644 --- a/src/game/layers.ts +++ b/src/game/layers.ts @@ -450,10 +450,26 @@ export function addLayer(layer: RawLayer, player?: Partial): void { if (nodeType.actions === null) { return null; } - if (typeof nodeType.actions === "function") { - return nodeType.actions(this.selectedNode); + const actions = + typeof nodeType.actions === "function" + ? nodeType.actions(this.selectedNode) + : nodeType.actions; + return actions?.find( + action => + action.id === playerProxy.layers[this.layer].boards[this.id].selectedAction + ); + }); + setDefault(layer.boards.data[id], "links", function() { + if (this.selectedAction == null) { + return null; } - return nodeType.actions; + if (this.selectedAction.links) { + if (typeof this.selectedAction.links === "function") { + return this.selectedAction.links(this.selectedNode); + } + return this.selectedAction.links; + } + return null; }); for (const nodeType in layer.boards.data[id].types) { layer.boards.data[id].types[nodeType].layer = layer.id; diff --git a/src/typings/branches.d.ts b/src/typings/branches.d.ts index 93b263b..cfd0ee6 100644 --- a/src/typings/branches.d.ts +++ b/src/typings/branches.d.ts @@ -17,9 +17,10 @@ export interface BranchOptions { target?: string; featureType?: string; stroke?: string; - "stroke-width"?: string; + strokeWidth?: number | string; startOffset?: Position; endOffset?: Position; + [key: string]: any; } export interface Position { diff --git a/src/typings/features/board.d.ts b/src/typings/features/board.d.ts index a10d348..f5d716c 100644 --- a/src/typings/features/board.d.ts +++ b/src/typings/features/board.d.ts @@ -4,7 +4,7 @@ import { State } from "../state"; import { Feature, RawFeature } from "./feature"; export interface BoardNode { - id: string; + id: number; position: { x: number; y: number; @@ -28,6 +28,7 @@ export interface Board extends Feature { nodes: BoardNode[]; selectedNode: BoardNode | null; selectedAction: BoardNodeAction | null; + links: BoardNodeLink[] | null; } export type RawBoard = Omit, "types" | "startNodes"> & { @@ -37,7 +38,8 @@ export type RawBoard = Omit, "types" | "startNodes"> & { export interface NodeType extends Feature { title: string | ((node: BoardNode) => string); - size: number | ((node: BoardNode) => number); + label?: NodeLabel | null | ((node: BoardNode) => NodeLabel | null); + size: number | string | ((node: BoardNode) => number | string); draggable: boolean | ((node: BoardNode) => boolean); shape: Shape | ((node: BoardNode) => Shape); canAccept: boolean | ((node: BoardNode, otherNode: BoardNode) => boolean); @@ -57,7 +59,24 @@ export interface NodeType extends Feature { export interface BoardNodeAction { id: string; - icon: string; - tooltip: string; + icon: string | ((node: BoardNode) => string); + fillColor?: string | ((node: BoardNode) => string); + tooltip: string | ((node: BoardNode) => string); onClick: (node: BoardNode) => void; + links?: BoardNodeLink[] | ((node: BoardNode) => BoardNodeLink[]); +} + +export interface BoardNodeLink { + from: BoardNode; + to: BoardNode; + stroke: string; + strokeWidth: number | string; + pulsing?: boolean; + [key: string]: any; +} + +export interface NodeLabel { + text: string; + color?: string; + pulsing?: boolean; } diff --git a/src/util/features.ts b/src/util/features.ts index 40a6809..18794d9 100644 --- a/src/util/features.ts +++ b/src/util/features.ts @@ -1,5 +1,5 @@ import { layers } from "@/game/layers"; -import { NodeType, BoardNode } from "@/typings/features/board"; +import { NodeType, BoardNode, Board } from "@/typings/features/board"; import { GridCell } from "@/typings/features/grid"; import { State } from "@/typings/state"; import Decimal, { DecimalSource } from "@/util/bignum"; @@ -107,3 +107,13 @@ export function getNodeTypeProperty( ? (nodeType[property] as (node: BoardNode) => T)(node) : (nodeType[property] as T); } + +export function getUniqueNodeID(board: Board): number { + let id = 0; + board.nodes.forEach(node => { + if (node.id >= id) { + id = node.id + 1; + } + }); + return id; +} diff --git a/src/util/save.ts b/src/util/save.ts index 45ef182..0b14d81 100644 --- a/src/util/save.ts +++ b/src/util/save.ts @@ -187,7 +187,7 @@ window.onbeforeunload = () => { } }; window.save = save; -window.hardReset = async () => { +export const hardReset = window.hardReset = async () => { await loadSave(newSave()); const modData = JSON.parse(decodeURIComponent(escape(atob(localStorage.getItem(modInfo.id)!)))); modData.active = player.id;