Made Dream Hero

This commit is contained in:
thepaperpilot 2021-03-23 23:11:12 -05:00
parent 353d7e3e2d
commit 1763c851a1
33 changed files with 1646 additions and 342 deletions

442
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -9,7 +9,10 @@
}, },
"dependencies": { "dependencies": {
"core-js": "^3.6.5", "core-js": "^3.6.5",
"vue": "^2.6.11" "pad-end": "^1.0.2",
"vue": "^2.6.11",
"vue-panzoom": "^1.1.3",
"vue2-perfect-scrollbar": "^1.5.0"
}, },
"devDependencies": { "devDependencies": {
"@vue/cli-plugin-babel": "~4.5.0", "@vue/cli-plugin-babel": "~4.5.0",

BIN
public/assets/bat.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
public/assets/city.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

BIN
public/assets/default.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
public/assets/discord.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
public/assets/dollar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

BIN
public/assets/gold.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
public/assets/graveyard.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

BIN
public/assets/hero.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
public/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

BIN
public/assets/potion.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
public/assets/savanna.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

BIN
public/assets/shield.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
public/assets/skeleton.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
public/assets/slime.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

BIN
public/assets/witch.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

@ -5,11 +5,12 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0"> <meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico"> <link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title> <link href="https://fonts.googleapis.com/css2?family=Roboto+Mono&display=swap" rel="stylesheet">
<title>Dream Hero</title>
</head> </head>
<body> <body>
<noscript> <noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong> <strong>We're sorry but Dream Hero doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript> </noscript>
<div id="app"></div> <div id="app"></div>
<!-- built files will be auto injected --> <!-- built files will be auto injected -->

View file

@ -1,28 +1,173 @@
<template> <template>
<div id="app"> <transition name="victory" v-if="$store.cycle >= 5 && !$store.keepPlaying">
<img alt="Vue logo" src="./assets/logo.png"> <div class="victory">
<HelloWorld msg="Welcome to Your Vue.js App"/> <h1>You Win!</h1>
<h2>Congratulations, you beat the game in:<br/>{{ formatTime($store.timePlayed) }}</h2>
<h3>You can keep going if you'd like, but things might get weird</h3>
<button v-on:click="keepGoing">Keep Going</button>
</div> </div>
</transition>
<div id="app" v-else-if="$store.started">
<Header />
<Town />
<Dream ref="dream" />
</div>
<transition name="app" v-else>
<div class="welcome" v-on:click="start">
<img src="assets/logo.png" alt="Dream Hero" />
</div>
</transition>
</template> </template>
<script> <script>
import HelloWorld from './components/HelloWorld.vue' import Header from './components/Header.vue'
import Town from './components/Town.vue'
import Dream from './components/Dream.vue'
export default { export default {
name: 'App', name: 'App',
components: { components: {
HelloWorld Header,
Town,
Dream
},
methods: {
start() {
this.$store.started = true;
},
keepGoing() {
this.$store.keepPlaying = true;
}
} }
} }
</script> </script>
<style> <style>
#app { :root {
font-family: Avenir, Helvetica, Arial, sans-serif; --fg-color: #292831;
--bg-color: #ee8695;
--hi-color: #333f58;
--raised-color: #fbbbad;
--other-color: #4a7a96;
}
* {
transition-duration: 0.5s;
font-family: "Roboto Mono", monospace;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: none;
text-size-adjust: none;
}
html {
width: 100%;
height: 100%;
}
body {
color: var(--fg-color);
background-color: var(--bg-color);
width: 100%;
height: 100%;
margin: 0;
}
#app {
width: 100%;
height: 100%;
display: flex;
flex-flow: column;
}
button {
outline: none;
border: solid 2px var(--fg-color);
background: var(--bg-color);
}
#app .ps__thumb-y {
background-color: var(--fg-color);
}
#app .ps .ps__rail-x:hover,
#app .ps .ps__rail-y:hover,
#app .ps .ps__rail-x:focus,
#app .ps .ps__rail-y:focus,
#app .ps .ps__rail-x.ps--clicking,
#app .ps .ps__rail-y.ps--clicking {
background-color: var(--other-color);
}
img, [background-image] {
image-rendering: crisp-edges;
}
.victory-enter {
opacity: 0;
filter: blur(100px);
}
.victory-leave-active {
opacity: 0;
filter: blur(100px);
}
.victory {
position: fixed;
width: 100%;
height: 100%;
box-sizing: border-box;
background: var(--fg-color);
color: var(--bg-color);
text-align: center; text-align: center;
color: #2c3e50; padding: 20px;
margin-top: 60px; transition-duration: 2s;
z-index: 100;
}
.victory button {
font-size: large;
font-weight: 900;
}
.welcome-leave-active {
opacity: 0;
filter: blur(100px);
}
.welcome {
transition-duration: 2s;
background: var(--fg-color);
position: fixed;
width: 100%;
height: 100%;
z-index: 100;
}
.welcome img {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
animation: blur 5s infinite;
}
.dream img {
filter: drop-shadow(4px 4px 4px var(--fg-color));
}
@keyframes blur {
from {
filter: blur(0px);
}
33% {
filter: blur(4px);
}
66%, to {
filter: blur(0px);
}
} }
</style> </style>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

2
src/break_eternity.js Normal file

File diff suppressed because one or more lines are too long

63
src/common.js Normal file
View file

@ -0,0 +1,63 @@
import Decimal from './break_eternity.js'
global.Decimal = Decimal
const bgColor = "#ee8695";
const fgColor = "#292831";
const hiColor = "#333f58";
const raisedColor = "#fbbbad";
const otherColor = "#4a7a96";
const decimalZero = new Decimal(0);
const decimalOne = new Decimal(1);
const decimalNaN = new Decimal(NaN);
const buildingInfo = {
Cot: {
background: "default",
enemies: [ "bat" ],
upgrades: [
{ description: "I'd sleep better on something comfier", cost: new Decimal(2) },
{ description: "An even comfier bed could give me better control on when I wake up", cost: new Decimal(2500) },
// TODO upgrade to select order of dream path
]
},
Bank: {
background: "city",
enemies: [ "slime" ],
upgrades: [
{ description: "Building a bank allows me to adventure to cities in my dreams, with increased riches", cost: new Decimal(100) }
],
infinite: {
description: "Improve the bank to double all gold gain",
r: 5,
base: 100
}
},
Apothecary: {
background: "savanna",
enemies: [ "witch" ],
upgrades: [
{ description: "Building an apothecary will allow me to find potions in my dreams", cost: new Decimal(10000) }
],
infinite: {
description: "Improve the apothecary to increase how much potions heal",
r: 3,
base: 10000
}
},
Armory: {
background: "graveyard",
enemies: [ "skeleton" ],
upgrades: [
{ description: "Building an armory will help my gear up in my dreams", cost: new Decimal(10) }
],
infinite: {
description: "Improve the armory to increase starting gear level",
r: 8,
base: 10
}
}
}
export default { bgColor, fgColor, hiColor, raisedColor, otherColor, decimalZero, decimalOne, decimalNaN, buildingInfo };

147
src/components/Action.vue Normal file
View file

@ -0,0 +1,147 @@
<template>
<div class="action" v-bind:style="{ backgroundImage: 'url(assets/' + tile.type + '.png)' }">
<img class="shake left" src="assets/hero.png" alt="hero" />
<div class="health left">
<span v-bind:style="{ color: $store.hp.gt(getMaxHealth()) ? 'var(--raised-color)' : ''}">{{ formatWhole($store.hp) }}</span>
<div class="health-fill"
v-bind:style="{ width: 100 * $store.hp / getMaxHealth() + '%' }"></div>
</div>
<div class="shake right">
<img v-if="tile.actions[$store.currentAction].type === 'gold'"
v-bind:src="'assets/' + (tile.actions[$store.currentAction].image || 'gold') + '.png'"
v-bind:alt="tile.actions[$store.currentAction].image || 'gold'" />
<img v-else-if="tile.actions[$store.currentAction].type === 'enemy'"
v-bind:src="'assets/' + tile.actions[$store.currentAction].enemy + '.png'"
v-bind:alt="tile.actions[$store.currentAction].enemy"/>
<img v-else-if="tile.actions[$store.currentAction].type === 'potion'"
src="assets/potion.png" alt="potion"/>
<img v-else-if="tile.actions[$store.currentAction].type === 'gear'"
src="assets/shield.png" alt="shield"/>
</div>
<span v-if="tile.actions[$store.currentAction].type === 'gold'" class="amount right">
{{ formatWhole(tile.actions[$store.currentAction].amount) }}
</span>
<div class="health right" v-if="tile.actions[$store.currentAction].type === 'enemy'">
<span>{{ formatWhole(tile.actions[$store.currentAction].hp) }}</span>
<div class="health-fill"
v-bind:style="{ width: 100 * tile.actions[$store.currentAction].hp / tile.actions[$store.currentAction].maxHp + '%' }"></div>
</div>
</div>
</template>
<script>
export default {
name: 'Action',
props: {
tile: Object
}
}
</script>
<style scoped>
.action {
border-top: solid var(--bg-color) 0;
height: 0;
box-sizing: border-box;
background-size: cover;
background-position: center;
position: relative;
overflow: hidden;
}
.tile.active .action {
height: 200px;
border-top-width: 10px;
}
.action img {
width: 128px;
height: 128px;
}
.left {
position: absolute;
left: 30%;
top: 50%;
transform: translate(-50%, -50%);
transition-duration: 0s;
display: inline-block;
}
.right {
position: absolute;
left: 70%;
top: 50%;
transform: translate(-50%, -50%);
transition-duration: 0s;
display: inline-block;
}
.tile:not(.active) .left,
.tile:not(.active) .right {
display: none;
}
.shake {
animation: shake 1.5s infinite;
}
.health {
width: 150px;
height: 16px;
background: var(--bg-color);
border: solid 2px var(--fg-color);
position: absolute;
top: 90%;
text-align: center;
overflow: hidden;
}
.health span {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-weight: 900;
font-size: small;
z-index: 1;
}
.health-fill {
position: absolute;
top: 0;
bottom: 0;
left: 0;
background: var(--other-color);
transition-duration: 0s;
}
.right.amount {
position: absolute;
left: unset;
transform: unset;
right: calc(30% - 60px);
top: calc(50% - 60px);
font-size: x-large;
font-weight: 900;
color: var(--other-color);
background: var(--fg-color);
padding: 4px;
border-radius: 4px;
opacity: 0.9;
}
@keyframes shake {
from, 45% {
transform: translate(-50%, -50%) rotateZ(-15deg);
}
55%, 90% {
transform: translate(-50%, -50%) rotateZ(15deg);
}
to {
transform: translate(-50%, -50%) rotateZ(-15deg);
}
}
</style>

View file

@ -0,0 +1,55 @@
<template>
<img v-if="action.type === 'enemy'" v-bind:class="{ actionPreview: true, active: index === $store.currentAction }"
v-bind:src="'assets/' + action.enemy + '.png'"
v-bind:alt="action.enemy"/>
<div v-else-if="action.type === 'gold'" class="amount-container">
<img class="actionPreview"
v-bind:src="'assets/' + (action.image || 'gold') + '.png'"
v-bind:alt="action.image || 'gold'"
v-bind:class="{ actionPreview: true, active: index === $store.currentAction }" />
<span class="amount">{{ formatWhole(action.amount) }}</span>
</div>
<img v-else-if="action.type === 'potion'" v-bind:class="{ actionPreview: true, active: index === $store.currentAction }"
src="assets/potion.png" alt="potion"/>
<img v-else-if="action.type === 'gear'" v-bind:class="{ actionPreview: true, active: index === $store.currentAction }"
src="assets/shield.png" alt="shield"/>
</template>
<script>
export default {
name: 'ActionPreview',
props: {
action: Object,
index: Number
}
}
</script>
<style scoped>
.actionPreview {
margin: 9px;
width: 64px;
height: 64px;
}
.tile.active .actionPreview.active {
transform: scale(1.5);
}
.amount-container {
position: relative;
}
.amount {
position: absolute;
top: 4px;
right: 4px;
font-weight: 900;
color: var(--other-color);
background: var(--fg-color);
padding: 4px;
border-radius: 4px;
font-size: small;
opacity: 0.9;
}
</style>

78
src/components/Dream.vue Normal file
View file

@ -0,0 +1,78 @@
<template>
<scroll class="dream" ref="scroll">
<Floor v-for="(tile, index) in $store.path" :key="index" :index="index" />
<div v-if="$store.upgrades.Cot >= 1" class="endAtLoop" v-on:click="toggleEndAtLoop">
<h2 v-if="$store.endAtLoop">Waking up at end of this sleep cycle</h2>
<h2 v-else>Entering deeper sleep at end of this sleep cycle</h2>
<span>Click to toggle</span>
</div>
<Modal :show="$store.endingDream" @close="$actions.endDream">
<h3 slot="header">Time to wake up</h3>
<div slot="body">
<span v-if="$store.endingDreamStatus === 'death'">
Unfortunately, your dream has met an untimely end. You will only receive a portion of your coins:<br/>+{{ formatWhole($store.tempPoints.pow(0.8)) }}
</span>
<span v-else-if="$store.endingDreamStatus === 'floor'">
You wake up early, avoiding potential death at the cost of some of your potential coins:<br/>+{{ formatWhole($store.tempPoints.pow(0.9)) }}
</span>
<span v-else>
You wake up feeling refreshed, with a heavier wallet:<br/>+{{ formatWhole($store.tempPoints) }}
</span>
</div>
<div slot="footer">
<button v-on:click="$actions.endDream">Wake Up</button>
</div>
</Modal>
</scroll>
</template>
<script>
import Floor from './Floor.vue'
import Modal from './Modal.vue'
export default {
name: 'Dream',
components: {
Floor,
Modal
},
methods: {
toggleEndAtLoop() {
this.$store.endAtLoop = !this.$store.endAtLoop
}
}
}
</script>
<style scoped>
.dream {
position: absolute;
top: 50px;
bottom: 0;
left: 0;
right: 0;
background: var(--bg-color);
padding: 20px;
}
.endAtLoop {
width: 600px;
max-width: 90vw;
margin: 10px auto;
background: var(--raised-color);
height: 100px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
padding: 10px;
text-align: center;
box-sizing: border-box;
}
.endAtLoop > * {
margin: 0;
user-select: none;
}
</style>

125
src/components/Floor.vue Normal file
View file

@ -0,0 +1,125 @@
<template>
<div v-bind:class="{ tile: true, blur: $store.position < index, active: $store.position === index }">
<span class="indicator">
<img v-if="$store.position === index" class="indicator-hero" src="assets/hero.png" alt="hero" />
<div v-else class="indicator-index">{{ index + 1 }}</div>
</span>
<span class="actions-container"
v-bind:style="{
backgroundImage: 'url(assets/' + $store.path[index].type + '.png)',
width: $store.upgrades.Cot >= 2 && $store.position === index ? '70%' : '85%'
}">
<ActionPreview v-for="(action, index) in $store.path[index].actions"
:key="index" :action="action" :index="index" />
</span>
<span v-bind:style="{ width: $store.upgrades.Cot >= 2 && $store.position === index ? '15%' : '0%' }"
class="endAtFloor" v-on:click="toggleEndAtFloor">
Wake up early:<br/><b>{{ $store.endAtFloor ? "On" : "Off" }}</b>
</span>
<Action :tile="$store.path[index]" />
<div class="actionProgress">
<div class="actionProgress-fill"
v-bind:style="{ width: 100 * $store.actionProgress / getActionDuration() + '%' }"></div>
</div>
</div>
</template>
<script>
import Action from './Action.vue'
import ActionPreview from './ActionPreview.vue'
export default {
name: 'Floor',
props: {
index: Number
},
components: {
Action,
ActionPreview
},
methods: {
toggleEndAtFloor() {
this.$store.endAtFloor = !this.$store.endAtFloor
}
}
}
</script>
<style scoped>
.tile {
width: 600px;
max-width: 90vw;
margin: 10px auto;
background: var(--raised-color);
}
.tile.blur {
filter: blur(2px);
}
.tile.active {
margin-bottom: 30px;
}
.indicator {
width: 15%;
height: 100px;
display: inline-flex;
align-items: center;
justify-content: center;
background: var(--other-color);
}
.indicator-hero {
width: 64px;
height: 64px;
}
.indicator-index {
font-size: xx-large;
font-weight: 900;
}
.actions-container {
width: 85%;
display: inline-flex;
height: 100px;
vertical-align: bottom;
padding: 9px 16px;
box-sizing: border-box;
background-size: cover;
background-position: bottom;
}
.actionProgress {
height: 0;
position: relative;
overflow: hidden;
}
.tile.active .actionProgress {
height: 20px;
}
.actionProgress-fill {
position: absolute;
top: 0;
bottom: 0;
left: 0;
background: var(--other-color);
transition-duration: 0s;
}
.endAtFloor {
display: inline-flex;
flex-direction: column;
justify-content: center;
width: 15%;
height: 100px;
text-align: center;
vertical-align: bottom;
cursor: pointer;
user-select: none;
overflow: hidden;
}
</style>

43
src/components/Header.vue Normal file
View file

@ -0,0 +1,43 @@
<template>
<div class="header">
<h2>Dream Hero</h2>
<h2>{{ formatWhole($store.points) }}</h2>
<h2 v-if="$store.dreaming" style="color: var(--hi-color);">+{{ formatWhole($store.tempPoints) }}</h2>
<h2 v-if="$store.dreaming" style="color: var(--hi-color);">Cycle {{ $store.cycle + 1 }}</h2>
<a href="https://discord.gg/WzejVAx" target="_blank"><img src="assets/discord.png" /></a>
</div>
</template>
<script>
export default {
name: 'Header'
}
</script>
<style scoped>
.header {
background: var(--raised-color);
padding: 8px;
border-bottom: solid 2px var(--fg-color);
position: fixed;
top: 0;
left: 0;
right: 0;
}
h2 {
margin: 0;
padding-right: 8px;
margin-right: 8px;
display: inline;
}
h2:not(:last-of-type) {
border-right: solid 2px var(--fg-color);
}
img {
height: 32px;
float: right;
}
</style>

View file

@ -1,58 +0,0 @@
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<p>
For a guide and recipes on how to configure / customize this project,<br>
check out the
<a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
</p>
<h3>Installed CLI Plugins</h3>
<ul>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" target="_blank" rel="noopener">eslint</a></li>
</ul>
<h3>Essential Links</h3>
<ul>
<li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
<li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
<li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
<li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
<li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
</ul>
<h3>Ecosystem</h3>
<ul>
<li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
<li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
<li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
<li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
<li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
</ul>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
props: {
msg: String
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h3 {
margin: 40px 0 0;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
</style>

100
src/components/Modal.vue Normal file
View file

@ -0,0 +1,100 @@
<template>
<transition name="modal">
<div class="modal-mask" v-if="show">
<div class="modal-wrapper" v-on:click.self="$emit('close')">
<div class="modal-container">
<div class="modal-header">
<slot name="header">
default header
</slot>
</div>
<div class="modal-body">
<slot name="body">
default body
</slot>
</div>
<div class="modal-footer">
<slot name="footer">
<button class="modal-default-button" @click="$emit('close')">
Close
</button>
</slot>
</div>
</div>
</div>
</div>
</transition>
</template>
<script>
export default {
name: 'Modal',
props: {
show: Boolean
}
}
</script>
<style scoped>
.modal-mask {
position: fixed;
z-index: 9998;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: table;
transition: opacity 0.3s ease;
}
.modal-wrapper {
display: table-cell;
vertical-align: middle;
}
.modal-container {
width: 300px;
margin: 0px auto;
padding: 20px 30px;
background-color: var(--raised-color);
border-radius: 2px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.33);
transition: all 0.3s ease;
font-family: Helvetica, Arial, sans-serif;
}
.modal-header h3 {
margin-top: 0;
color: var(--hi-color);
}
.modal-body {
margin: 20px 0;
}
.modal-footer {
min-height: 24px;
}
.modal-default-button {
float: right;
}
.modal-enter {
opacity: 0;
}
.modal-leave-active {
opacity: 0;
}
.modal-enter .modal-container,
.modal-leave-active .modal-container {
-webkit-transform: scale(1.1);
transform: scale(1.1);
}
</style>

202
src/components/Town.vue Normal file
View file

@ -0,0 +1,202 @@
<template>
<transition name="town">
<div class="town-container" v-if="!this.$store.dreaming">
<panZoom @init="onInit">
<div class="town">
<h1 class="background">World Map</h1>
<div v-bind:class="{ building: true, highlight: $store.tutorialOne }" style="top: 500px; left: 700px;"
v-on:click="$actions.openBuilding('Cot')">
Cot
</div>
<div class="building" v-if="!$store.tutorialOne" style="top: 200px; left: 600px;"
v-on:click="$actions.openBuilding('Bank')">
Bank
</div>
<div class="building" v-if="!$store.tutorialOne" style="top: 800px; left: 200px;"
v-on:click="$actions.openBuilding('Apothecary')">
Apothecary
</div>
<div class="building" v-if="!$store.tutorialOne" style="top: 750px; left: 800px;"
v-on:click="$actions.openBuilding('Armory')">
Armory
</div>
</div>
</panZoom>
<Modal :show="$store.openBuilding !== ''" @close="$actions.closeBuilding">
<div slot="header" style="position: relative;">
<img v-bind:src="'assets/' + buildingInfo.background + '.png'" alt="$store.openBuilding" class="header"/>
<div class="header-enemies">
<img v-for="enemy in buildingInfo.enemies" v-bind:src="'assets/' + enemy + '.png'" v-bind:alt="enemy" v-bind:key="enemy" />
</div>
<h3>{{ $store.openBuilding }}</h3>
</div>
<div slot="body">
<div v-if="$store.openBuilding === 'Cot'" style="display: flex; margin-bottom: 8px; border-bottom: solid 2px var(--fg-color); padding-bottom: 8px;">
<span style="flex-grow: 1;">I'm feeling tired...</span>
<button @click="$actions.startDream()" style="float: right">Dream</button>
</div>
<div v-if="!$store.tutorialOne && upgradeInfo" style="display: flex;">
<span style="flex-grow: 1;">{{ upgradeInfo.description }}</span>
<button @click="upgradeBuilding()" style="float: right; margin-left: 4px;"
v-bind:disabled="$store.points.lt(upgradeInfo.cost)">
Cost: {{ formatWhole(upgradeInfo.cost) }}
</button>
</div>
<div v-else>
You've fully upgraded this!
</div>
</div>
<div slot="footer" style="margin-bottom: -24px"></div>
</Modal>
</div>
</transition>
</template>
<script>
import Modal from './Modal.vue'
import Common from '../common.js'
import Decimal from '../break_eternity.js'
export default {
name: 'Town',
components: {
Modal
},
computed: {
buildingInfo() {
return this.$store.openBuilding && Common.buildingInfo[this.$store.openBuilding];
},
upgradeInfo() {
if (!this.$store.openBuilding) {
return null;
}
const buildingInfo = Common.buildingInfo[this.$store.openBuilding];
let upgrade = buildingInfo.upgrades[this.$store.upgrades[this.$store.openBuilding]];
if (!upgrade && buildingInfo.infinite) {
upgrade = {
description: buildingInfo.infinite.description,
cost: Decimal.times(buildingInfo.infinite.base, Decimal.pow(buildingInfo.infinite.r, this.$store.upgrades[this.$store.openBuilding]))
};
}
return upgrade;
}
},
methods: {
onInit: function(panzoomInstance) {
panzoomInstance.setTransformOrigin(null);
},
upgradeBuilding: function() {
const buildingInfo = Common.buildingInfo[this.$store.openBuilding];
let cost;
if (this.$store.upgrades[this.$store.openBuilding] in buildingInfo.upgrades) {
cost = buildingInfo.upgrades[this.$store.upgrades[this.$store.openBuilding]].cost;
} else if (buildingInfo.infinite) {
cost = Decimal.times(buildingInfo.infinite.base, Decimal.pow(buildingInfo.infinite.r, this.$store.upgrades[this.$store.openBuilding]));
}
if (cost.lte(this.$store.points)) {
this.$store.points = this.$store.points.sub(cost);
this.$store.upgrades[this.$store.openBuilding]++;
}
}
}
}
</script>
<style scoped>
.town-container {
flex-grow: 1;
transition-duration: 2s;
position: absolute;
top: 50px;
bottom: 0;
left: 0;
right: 0;
background: var(--bg-color);
z-index: 1;
}
.town-enter {
opacity: 0;
filter: blur(100px);
}
.town-leave-active {
opacity: 0;
filter: blur(100px);
}
.vue-pan-zoom-item {
overflow: hidden;
height: 100%
}
.town {
width: 1000px;
height: 1000px;
position: relative;
transition-duration: 0s;
}
.town:before {
content: "";
position: absolute;
top: 0%;
bottom: 0%;
left: 0%;
right: 0%;
background: var(--hi-color);
filter: blur(10px);
}
.background {
position: absolute;
top: 500px;
left: 500px;
transform: translate(-50%, -50%);
font-size: 160px;
font-weight: 900;
margin: 0;
text-align: center;
color: var(--other-color);
cursor: default;
}
.building {
position: absolute;
height: 50px;
color: var(--other-color);
font-size: xx-large;
transform: translate(-50%, -50%);
font-weight: 900;
cursor: pointer;
border-radius: 50%;
padding: 8px;
background: var(--fg-color);
}
.building.highlight {
box-shadow: var(--bg-color) 0 0 8px 4px;
}
.header {
margin: -30px;
margin-bottom: 0;
width: calc(100% + 60px);
}
.header-enemies {
display: flex;
position: absolute;
top: -30px;
height: 120px;
justify-content: center;
align-items: center;
width: 100%;
}
.header-enemies img {
width: 96px;
height: 96px;
filter: drop-shadow(4px 4px 4px var(--fg-color));
}
</style>

View file

@ -1,8 +1,368 @@
import Vue from 'vue' import Vue from 'vue';
import App from './App.vue' import App from './App.vue';
import panZoom from 'vue-panzoom';
import PerfectScrollbar from 'vue2-perfect-scrollbar';
import 'vue2-perfect-scrollbar/dist/vue2-perfect-scrollbar.css';
import Decimal from './break_eternity.js'
import { } from './common.js'
import { format, formatWhole, formatTime } from './numberFormatting.js'
Vue.config.productionTip = false const storageKey = "thepaperpilot-dream";
new Vue({ // Load data from localStorage
const startData = {
timePlayed: 0,
keepPlaying: false,
points: new Decimal(0),
tempPoints: new Decimal(0),
dreaming: false,
autoSave: true,
openBuilding: '',
tutorialOne: true,
path: new Array(10).fill(0).map(() => ({
actions: new Array(100).fill(0).map(() => ({
type: "",
enemy: "",
maxHp: new Decimal(0),
hp: new Decimal(0),
attackDuration: 0,
damage: new Decimal(0),
progress: 0
})),
type: ""
})),
currentAction: 0,
actionProgress: -1,
attackProgress: 0,
cycle: 0,
currentTime: performance.now(),
hp: new Decimal(0),
paused: false,
upgrades: {
Cot: 0,
Bank: 0,
Apothecary: 0,
Armory: 0
},
gearLevel: 0,
started: false,
endAtLoop: false,
endAtFloor: false,
endingDream: false,
endingDreamStatus: "death" // "loop", "floor"
};
function fixData(data, startData) {
for (let dataKey in startData) {
if (startData[dataKey] == null) {
if (data[dataKey] === undefined) {
data[dataKey] = null;
}
} else if (Array.isArray(startData[dataKey])) {
if (data[dataKey] === undefined) {
data[dataKey] = startData[dataKey];
} else {
fixData(startData[dataKey], data[dataKey]);
}
} else if (startData[dataKey] instanceof Decimal) { // Convert to Decimal
if (data[dataKey] == undefined) {
data[dataKey] = startData[dataKey];
} else {
data[dataKey] = new Decimal(data[dataKey]);
}
} else if ((!!startData[dataKey]) && (typeof startData[dataKey] === "object")) {
if (data[dataKey] == undefined || (typeof data[dataKey] !== "object")) {
data[dataKey] = startData[dataKey];
} else {
fixData(startData[dataKey], data[dataKey]);
}
} else {
if (data[dataKey] == undefined) {
data[dataKey] = startData[dataKey];
}
}
}
}
let loadedData = localStorage.getItem(storageKey);
if (loadedData == null) {
loadedData = startData;
} else {
loadedData = Object.assign({}, startData, JSON.parse(atob(loadedData)));
fixData(loadedData, startData);
}
const store = window.player = Vue.observable(loadedData);
Vue.prototype.$store = store;
// Set up auto-saving every 5s
window.save = function() {
if (store.autoSave) {
localStorage.setItem(storageKey, btoa(JSON.stringify(window.player)));
}
}
setInterval(window.save, 5000);
// Add getters to Vue
function getAttackDuration() {
return Decimal.times(1, Decimal.pow(.95, store.gearLevel)).clamp(Number.MIN_VALUE, Number.MAX_VALUE).toNumber();
}
Vue.prototype.getAttackDuration = window.getAttackDuration = getAttackDuration;
function getAttackDamage() {
let damage = Decimal.add(2, store.gearLevel).pow(2);
if (store.hp.gt(getMaxHealth())) {
damage = damage.times(2);
}
return damage;
}
Vue.prototype.getAttackDamage = window.getAttackDamage = getAttackDamage;
function getActionDuration() {
return Decimal.times(2, Decimal.pow(.98, store.gearLevel)).clamp(Number.MIN_VALUE, Number.MAX_VALUE).toNumber();
}
Vue.prototype.getActionDuration = window.getActionDuration = getActionDuration;
function getMaxHealth(gearLevel) {
return new Decimal(25).times(Decimal.add(1, gearLevel || store.gearLevel).pow(2));
}
Vue.prototype.getMaxHealth = window.getMaxHealth = getMaxHealth;
function isCombatActive() {
if (!store.dreaming) {
return false;
}
if (store.path[store.position].actions[store.currentAction].type !== "enemy") {
return false;
}
if (store.actionProgress < getActionDuration()) {
return false;
}
return true;
}
Vue.prototype.isCombatActive = window.isCombatActive = isCombatActive;
// Set up actions
function getRandomModifier(cycle) {
return (Math.random() * 0.2 + 0.8) * (cycle * 1.5);
}
const tiles = {
default: [
cycle => { // Bat
const hp = new Decimal(getRandomModifier(cycle) + 3).factorial().floor();
return Vue.observable({
type: "enemy",
enemy: "bat",
maxHp: hp,
hp,
attackDuration: Decimal.times(2, Decimal.pow(.9, cycle)).toNumber(),
damage: new Decimal(getRandomModifier(cycle) + 1.5).factorial().floor(),
progress: 0
});
},
cycle => { // Gold
return Vue.observable({ type: "gold", amount: new Decimal(getRandomModifier(cycle) + 1).factorial().times(Decimal.pow(2, store.upgrades.Bank)).floor() });
}
],
city: [
cycle => { // Slime
const hp = new Decimal(getRandomModifier(cycle) + 2.75).factorial().floor();
return Vue.observable({
type: "enemy",
enemy: "slime",
maxHp: hp,
hp,
attackDuration: Decimal.times(1, Decimal.pow(.5, cycle + 1)).toNumber(),
damage: new Decimal(cycle + 1).sqrt(),
progress: 0
});
},
cycle => { // Gold
return Vue.observable({ type: "gold", image: "dollar", amount: new Decimal(getRandomModifier(cycle) + 2).factorial().times(Decimal.pow(2, store.upgrades.Bank)).floor() });
}
],
savanna: [
cycle => { // Witch
const hp = new Decimal(getRandomModifier(cycle) + 3).factorial().floor();
return Vue.observable({
type: "enemy",
enemy: "witch",
maxHp: hp,
hp,
attackDuration: Decimal.times(2, Decimal.pow(.95, cycle)).toNumber(),
damage: new Decimal(getRandomModifier(cycle) + 2).factorial().floor(),
progress: 0
});
},
() => { // Potion
return Vue.observable({ type: "potion" });
}
],
graveyard: [
cycle => { // Skeleton
const hp = new Decimal(getRandomModifier(cycle) + 2.5).factorial().floor();
return Vue.observable({
type: "enemy",
enemy: "skeleton",
maxHp: hp,
hp,
attackDuration: Decimal.times(3, Decimal.pow(.98, cycle)).toNumber(),
damage: new Decimal(getRandomModifier(cycle) + 2.5).factorial().floor(),
progress: 0
});
},
cycle => { // Gear
return Vue.observable({ type: "gear", amount: (cycle + 1) / 10 });
}
]
}
const actions = window.actions = {
startDream() {
store.endAtLoop = false;
store.endAtFloor = false;
store.tutorialOne = false;
store.openBuilding = '';
store.cycle = -1;
let tiles = [ "default" ];
if (store.upgrades["Bank"] >= 1) {
tiles.push("city");
}
if (store.upgrades["Apothecary"] >= 1) {
tiles.push("savanna");
}
if (store.upgrades["Armory"] >= 1) {
tiles.push("graveyard");
}
store.path = new Array(10).fill(0).map(() => ({ type: tiles[Math.floor(Math.random() * tiles.length)] }));
store.position = 0;
store.tempPoints = new Decimal(0);
store.gearLevel = store.upgrades.Armory;
store.hp = getMaxHealth();
this.startLoop();
store.dreaming = true;
},
endDream() {
let modifier = 1;
if (store.endingDreamStatus === "death") {
modifier = 0.8;
} else if (store.endingDreamStatus === "floor") {
modifier = 0.9;
}
store.points = store.points.add(store.tempPoints.pow(modifier));
store.dreaming = false;
store.endingDream = false;
},
startLoop() {
store.cycle++;
store.position = -1;
store.path.forEach(tile => {
tile.actions = new Array(store.cycle + 1).fill(0).map(() => tiles[tile.type][Math.floor(Math.random() * tiles[tile.type].length)](store.cycle));
});
window.vue.$root.$children[0].$refs.dream.$refs.scroll.$el.scrollTo({top: 0, behavior: 'smooth'});
this.nextFloor();
},
nextFloor() {
store.position = store.position + 1;
if (store.position >= 10) {
if (store.upgrades.Cot >= 1 && !store.endAtLoop) {
this.startLoop();
} else {
store.endingDreamStatus = "loop";
store.endingDream = true;
store.position = store.points - 1;
store.currentAction = store.currentAction - 1;
}
return;
}
store.currentAction = -1;
const scrollTarget = window.vue.$root.$children[0].$refs.dream.$refs.scroll.$el.children[store.position].offsetTop - 250;
window.vue.$root.$children[0].$refs.dream.$refs.scroll.$el.scrollTo({ top: scrollTarget, behavior: 'smooth' });
this.nextAction();
},
nextAction() {
store.currentAction++;
if (store.currentAction >= store.path[store.position].actions.length) {
if (store.upgrades.Cot < 2 || !store.endAtFloor) {
this.nextFloor();
} else {
store.endingDreamStatus = "floor";
store.endingDream = true;
store.currentAction = store.currentAction - 1;
}
return;
}
store.actionProgress = 0;
store.attackProgress = 0;
},
openBuilding(building) {
store.openBuilding = building;
},
closeBuilding() {
store.openBuilding = '';
}
};
Vue.prototype.$actions = actions;
// Add utility functions to Vue
Vue.prototype.format = format;
Vue.prototype.formatWhole = formatWhole;
Vue.prototype.formatTime = formatTime;
// Setup Vue
Vue.config.productionTip = false;
Vue.use(panZoom);
Vue.use(PerfectScrollbar, { name: 'scroll' });
// Start Vue
window.vue = new Vue({
render: h => h(App), render: h => h(App),
}).$mount('#app') }).$mount('#app');
// Setup update loop
function update(currTime) {
// TODO offline time doesn't work if using performance.now()
const delta = (currTime - store.currentTime) / 1000;
if (delta > 0 && !store.paused && store.started && (store.cycle < 5 || store.keepPlaying)) {
store.timePlayed += delta;
if (store.dreaming && !store.endingDream) {
store.actionProgress += delta;
if (isCombatActive()) {
store.attackProgress += delta;
store.path[store.position].actions[store.currentAction].progress += delta;
let alive = true;
if (store.attackProgress >= getAttackDuration()) {
store.attackProgress = 0;
store.path[store.position].actions[store.currentAction].hp =
store.path[store.position].actions[store.currentAction].hp.sub(getAttackDamage());
if (store.path[store.position].actions[store.currentAction].hp.lte(0)) {
actions.nextAction();
alive = false;
}
}
if (alive && store.path[store.position].actions[store.currentAction].progress >= store.path[store.position].actions[store.currentAction].attackDuration) {
store.path[store.position].actions[store.currentAction].progress = 0;
store.hp = store.hp.sub(store.path[store.position].actions[store.currentAction].damage);
if (store.hp.lte(0)) {
store.endingDream = true;
store.endingDreamStatus = "death";
}
}
} else {
if (store.actionProgress >= getActionDuration()) {
switch (store.path[store.position].actions[store.currentAction].type) {
case "gold":
store.tempPoints = store.tempPoints.add(store.path[store.position].actions[store.currentAction].amount);
break;
case "gear": {
const oldGearLevel = store.gearLevel;
store.gearLevel += store.path[store.position].actions[store.currentAction].amount * store.upgrades.Armory;
store.hp = store.hp.add(getMaxHealth().sub(getMaxHealth(oldGearLevel)));
break;
}
case "potion":
store.hp = store.hp.add(getMaxHealth().times(0.25).times(store.upgrades.Apothecary + 1));
break;
}
actions.nextAction();
}
}
}
}
store.currentTime = currTime;
requestAnimationFrame(update);
}
update(performance.now());

115
src/numberFormatting.js Normal file
View file

@ -0,0 +1,115 @@
import Decimal from './break_eternity.js'
function exponentialFormat(num, precision, mantissa = true) {
let e = num.log10().floor();
let m = num.div(Decimal.pow(10, e));
if(m.toStringWithDecimalPlaces(precision) === 10) {
m = new Decimal(1);
e = e.add(1);
}
e = commaFormat(e);
if (mantissa) {
return m.toStringWithDecimalPlaces(precision)+"e"+e;
} else {
return "e"+e;
}
}
function commaFormat(num, precision) {
if (num === null || num === undefined) {
return "NaN";
}
if (num.mag < 0.001) {
return (0).toFixed(precision);
}
if (precision === null || precision === undefined) {
if (num.layer > 1) {
let firstPart = new Decimal(num);
firstPart.mag = Math.floor(num.mag);
let secondPart = new Decimal(num);
secondPart.layer = 0;
secondPart.mag = num.mag - firstPart.mag;
return firstPart.floor().toString().replace(/(\d)(?=(\d\d\d)+(?!\d))/g, "$1,") + secondPart.toStringWithDecimalPlaces(2).substr(1);
}
return num.floor().toString().replace(/(\d)(?=(\d\d\d)+(?!\d))/g, "$1,");
}
return num.toStringWithDecimalPlaces(precision).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, "$1,");
}
function regularFormat(num, precision) {
if (num === null || num === undefined) {
return "NaN";
}
if (num.eq(0)) {
return (0).toFixed(precision);
}
if (num.mag < 0.001) {
return num.toExponential(precision);
}
return num.toStringWithDecimalPlaces(precision);
}
function format(decimal, precision=2,) {
decimal = new Decimal(decimal);
if (isNaN(decimal.sign)||isNaN(decimal.layer)||isNaN(decimal.mag)) {
return "NaN";
}
if (decimal.sign<0) {
return "-"+format(decimal.neg(), precision);
}
if (decimal.mag === Number.POSITIVE_INFINITY) {
return "Infinity";
}
if (decimal.gte("eeee1000")) {
const slog = decimal.slog();
if (slog.gte(1e6)) {
return "F" + format(slog.floor());
} else {
return Decimal.pow(10, slog.sub(slog.floor())).toStringWithDecimalPlaces(3) + "F" + commaFormat(slog.floor(), 0);
}
} else if (decimal.gte("1e100000")) {
return exponentialFormat(decimal, 0, false);
} else if (decimal.gte("1e1000")) {
return exponentialFormat(decimal, 0);
} else if (decimal.gte(1e6)) {
return exponentialFormat(decimal, precision);
} else if (decimal.gte(1e3)) {
return commaFormat(decimal, 0);
} else {
return regularFormat(decimal, precision);
}
}
function formatWhole(decimal) {
decimal = new Decimal(decimal).floor();
if (decimal.gte(1e6)) {
return format(decimal, 2);
}
if (decimal.lte(0.98) && !decimal.eq(0)) {
return format(decimal, 2);
}
return format(decimal, 0);
}
function formatTime(s) {
if (s<60) {
return format(s)+"s";
} else if (s<3600) {
return formatWhole(Math.floor(s/60))+"m "+format(s%60)+"s";
} else if (s<86400) {
return formatWhole(Math.floor(s/3600))+"h "+formatWhole(Math.floor(s/60)%60)+"m "+format(s%60)+"s";
} else if (s<31536000) {
return formatWhole(Math.floor(s/84600)%365)+"d " + formatWhole(Math.floor(s/3600)%24)+"h "+formatWhole(Math.floor(s/60)%60)+"m "+format(s%60)+"s";
} else {
return formatWhole(Math.floor(s/31536000))+"y "+formatWhole(Math.floor(s/84600)%365)+"d " + formatWhole(Math.floor(s/3600)%24)+"h "+formatWhole(Math.floor(s/60)%60)+"m "+format(s%60)+"s";
}
}
window.format = format;
window.formatWhole = formatWhole;
window.formatTime = formatTime;
window.regularFormat = regularFormat;
window.commaFormat = commaFormat;
window.exponentialFormat = exponentialFormat;
export { format, formatWhole, formatTime, regularFormat, commaFormat, exponentialFormat };

3
vue.config.js Normal file
View file

@ -0,0 +1,3 @@
module.exports = {
publicPath: ''
};