final beta

This commit is contained in:
Simon Marsh 2023-03-31 17:00:53 +01:00
parent 5e2755faec
commit 0949e23199
Signed by: burble
GPG Key ID: E9B4156C1659C079
32 changed files with 1117 additions and 175 deletions

View File

@ -1,7 +1,4 @@
# Vue 3 + Vite # Clicker42
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more. This is the source code for https://clicker.burble.dn42
## Recommended IDE Setup
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).

View File

@ -2,12 +2,16 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/x-icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Vue</title> <title>Clicker42</title>
</head> </head>
<body> <body>
<div id="app"></div> <nav class="navbar bg-dark mb-1 p-0">
<script type="module" src="/src/main.js"></script> <span class="navbar-brand ms-3 p-2 text-light fs-2">Clicker42</span>
<span class="navbar-text p-0"><img src="/dn42.svg" height="60" class="p-0" style="width: 300px; object-fit: cover" alt="dn42"/></span>
</nav>
<div id="clicker42"></div>
<script type="module" src="/src/clicker42.js"></script>
</body> </body>
</html> </html>

View File

@ -9,6 +9,9 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"bootstrap": "^5.2.3",
"bootstrap-icons": "^1.10.3",
"pinia": "^2.0.33",
"vue": "^3.2.47" "vue": "^3.2.47"
}, },
"devDependencies": { "devDependencies": {

30
public/dn42.svg Normal file
View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 595.3 841.9" style="enable-background:new 0 0 595.3 841.9;" xml:space="preserve">
<style type="text/css">
.st0{fill:#3C3C3B;}
.st1{fill:#575756;}
.st2{fill:#706F6F;}
.st3{fill:#9D9D9C;}
.st4{fill:#878787;}
</style>
<polygon points="358.4,408.2 326.2,408.2 310.1,436.1 326.2,464 358.4,464 374.5,436.1 "/>
<polygon class="st0" points="408.7,436.7 376.5,436.7 360.4,464.6 376.5,492.5 408.7,492.5 424.8,464.6 "/>
<polygon class="st1" points="408.7,379 376.5,379 360.4,406.9 376.5,434.8 408.7,434.8 424.8,406.9 "/>
<polygon class="st2" points="458.8,408.2 426.6,408.2 410.5,436.1 426.6,464 458.8,464 474.9,436.1 "/>
<polygon class="st3" points="509.7,436.7 477.5,436.7 461.4,464.6 477.5,492.5 509.7,492.5 525.8,464.6 "/>
<polygon class="st4" points="458.8,350.2 426.6,350.2 410.5,378.1 426.6,405.9 458.8,405.9 474.9,378.1 "/>
<g>
<path d="M106.7,382.1h10.8v82.8H94.5c-8.1,0-14.5-2.5-19.3-7.6S68,445.3,68,436.8c0-8,2.5-14.6,7.6-19.8c5-5.2,11.5-7.8,19.3-7.8
c3.6,0,7.6,0.8,11.9,2.3V382.1z M106.7,455.7v-34.6c-3.4-1.7-6.8-2.5-10.2-2.5c-5.4,0-9.7,1.8-12.8,5.3c-3.2,3.5-4.8,8.3-4.8,14.2
c0,5.6,1.4,9.9,4.1,13c1.7,1.8,3.4,3,5.3,3.7c1.9,0.6,5.2,0.9,10,0.9H106.7z"/>
<path d="M143,410.4v6.9c4.8-5.3,10.3-8,16.4-8c3.4,0,6.6,0.9,9.5,2.6c2.9,1.8,5.1,4.2,6.7,7.2c1.5,3.1,2.3,7.9,2.3,14.5v31.2H167
v-31.1c0-5.6-0.9-9.6-2.5-11.9c-1.7-2.4-4.5-3.6-8.5-3.6c-5.1,0-9.4,2.5-13,7.6v38.9h-11v-54.5H143z"/>
<path d="M228.3,381h4.9v46.5h9.2v10.1h-9.2v27.3h-11.6v-27.3h-35v-5.1L228.3,381z M221.6,427.5v-24l-19.2,24H221.6z"/>
<path d="M270.9,453.5h31.5v11.4h-52.4v-0.8l5-5.9c7.8-9.6,14-17.8,18.6-24.5c4.6-6.7,7.6-11.8,9-15.2c1.4-3.4,2.1-6.8,2.1-10.2
c0-4.7-1.3-8.4-4-11.2c-2.6-2.8-6.2-4.2-10.5-4.2c-3.3,0-6.6,1-9.8,2.9c-3.2,2-6.1,4.7-8.7,8.3v-15.1c6.4-5.3,13.1-7.9,19.9-7.9
c7.2,0,13.2,2.4,17.9,7.2c4.7,4.8,7,10.9,7,18.4c0,3.3-0.6,6.9-1.7,10.6c-1.2,3.8-3.2,8.1-6.2,13c-3,4.9-8,11.6-15.1,20.1
L270.9,453.5z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 B

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,30 +0,0 @@
<script setup>
import HelloWorld from './components/HelloWorld.vue'
</script>
<template>
<div>
<a href="https://vitejs.dev" target="_blank">
<img src="/vite.svg" class="logo" alt="Vite logo" />
</a>
<a href="https://vuejs.org/" target="_blank">
<img src="./assets/vue.svg" class="logo vue" alt="Vue logo" />
</a>
</div>
<HelloWorld msg="Vite + Vue" />
</template>
<style scoped>
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.vue:hover {
filter: drop-shadow(0 0 2em #42b883aa);
}
</style>

63
src/Clicker42.vue Normal file
View File

@ -0,0 +1,63 @@
<script setup>
/////////////////////////////////////////////////////////////////////////
import { onMounted, onUnmounted, ref } from 'vue'
import LevelPanel from './components/LevelPanel.vue'
import BWPanel from './components/BWPanel.vue'
import ServicesPanel from './components/ServicesPanel.vue'
import SettingsPanel from './components/SettingsPanel.vue'
import UpgradeIcon from './components/UpgradeIcon.vue'
import Elapsed from './components/Elapsed.vue'
import { useState } from './model/state.js'
import { nstr } from './model/numbers';
const state = useState()
// navigation tab
const tab = ref("levels")
// install timer tick
let timerID
onMounted(() => {
state.restore()
timerID = setInterval(() => state.tick(state, 1), 1000)
})
onUnmounted(() => {
clearInterval(timerID)
})
/////////////////////////////////////////////////////////////////////////
</script>
<template>
<div class="px-3 py-0">
<div class="row p-0 m-1">
<div class="col" v-html="state.ui.notice"></div>
<div class="col-md-auto text-nowrap"><Elapsed></Elapsed></div>
</div>
<div class="mb-3">
<ul class="nav nav-tabs">
<li class="nav-item text-nowrap">
<a class="nav-link" :class="{ active: tab == 'levels' }" @click="tab = 'levels'">
<UpgradeIcon :active="false"></UpgradeIcon> Network {{ nstr(state.levels[0].count) }}
</a>
</li>
<li class="nav-item text-nowrap">
<a class="nav-link" :class="{ active: tab == 'bw' }" @click="tab = 'bw'">
<UpgradeIcon :active="state.bandwidth.canUpgrade(state)"></UpgradeIcon> Bandwidth {{ nstr(state.bandwidth.count) }}
</a>
</li>
<li class="nav-item text-nowrap">
<a class="nav-link" :class="{ active: tab == 'services' }" @click="tab = 'services'">
<UpgradeIcon :active="false"></UpgradeIcon> Services
</a>
</li>
<li class="nav-item text-nowrap">
<a class="nav-link" :class="{ active: tab == 'settings' }" @click="tab = 'settings'"><i>Settings</i></a>
</li>
</ul>
</div>
<LevelPanel v-if="tab == 'levels'"></LevelPanel>
<BWPanel v-if="tab == 'bw'"></BWPanel>
<ServicesPanel v-if="tab == 'services'"></ServicesPanel>
<SettingsPanel v-if="tab == 'settings'"></SettingsPanel>
</div></template>

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

Before

Width:  |  Height:  |  Size: 496 B

20
src/clicker42.js Normal file
View File

@ -0,0 +1,20 @@
/////////////////////////////////////////////////////////////////////////
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-icons/font/bootstrap-icons.css'
import Clicker42 from './Clicker42.vue'
/////////////////////////////////////////////////////////////////////////
const pinia = createPinia()
const clicker = createApp(Clicker42)
/////////////////////////////////////////////////////////////////////////
clicker.use(pinia)
clicker.mount('#clicker42')
/////////////////////////////////////////////////////////////////////////
// end of file

27
src/components/BWInfo.vue Normal file
View File

@ -0,0 +1,27 @@
<script setup>
/////////////////////////////////////////////////////////////////////////
import { computed } from 'vue';
import { useState } from '../model/state.js'
import { nstr } from '../model/numbers.js'
import UpgradeBuyer from './UpgradeBuyer.vue'
const state = useState()
const bandwidth = computed(() => state.bandwidth)
/////////////////////////////////////////////////////////////////////////
</script>
<template>
<div class="px-5">
<h1>Bandwidth Stats</h1>
<p>You have {{ nstr(bandwidth.count) }}b available.</p>
<p>Your current bandwidth rate is {{ nstr(bandwidth.rate) }}b/second
(+{{ nstr(bandwidth.serviceBonus(state)) }}b/second bonus).</p>
<hr />
<h2>Upgrades</h2>
<h3 class="pt-3">Upstream Upgrade</h3>
<p>Improving your upstream links increases your bandwidth rate.</p>
<UpgradeBuyer v-bind:cost="bandwidth.rateCost" v-bind:available="state.levels[0].count"
v-bind:units="state.levels[0].units" @buy="bandwidth.upgrade(state)"></UpgradeBuyer>
</div>
</template>

View File

@ -0,0 +1,14 @@
<script setup>
/////////////////////////////////////////////////////////////////////////
import BWInfo from './BWInfo.vue'
import { useState } from '../model/state.js'
const state = useState()
/////////////////////////////////////////////////////////////////////////
</script>
<template>
<div>
<BWInfo></BWInfo>
</div>
</template>

32
src/components/Buyer.vue Normal file
View File

@ -0,0 +1,32 @@
<script setup>
/////////////////////////////////////////////////////////////////////////
import { nstr } from '../model/numbers.js'
const props = defineProps(['max'])
const emits = defineEmits(['buy'])
function third() { return (props.max / 3n) }
/////////////////////////////////////////////////////////////////////////
</script>
<template>
<div class="container text-center">
<div class="row border border-secondary p-0" v-if="max == 0">
<div class="col text-secondary p-1">
Can't add
</div>
</div>
<div class="row border border-dark p-0" v-else>
<div class="col border p-1" @click="$emit('buy', BigInt(1))">
Add 1
</div>
<div class="col border p-1" @click="$emit('buy', third())" v-if="third() > 1">
Add {{ nstr(third()) }}
</div>
<div class="col border p-1" @click="$emit('buy', max)" v-if="max > 1">
Add {{ nstr(max) }}
</div>
</div>
</div>
</template>

View File

@ -0,0 +1,36 @@
<script setup>
/////////////////////////////////////////////////////////////////////////
import { computed } from 'vue';
import { useState } from '../model/state.js'
const state = useState()
const seconds = computed(() => {
return state.ui.elapsed % 60
})
const minutes = computed(() => {
return Math.floor(state.ui.elapsed / 60) % 60
})
const hours = computed(() => {
return Math.floor(state.ui.elapsed / 3600)
})
/////////////////////////////////////////////////////////////////////////
</script>
<template>
<span>
<span v-if="state.ui.elapsed >= 3600">
{{ hours }}h
</span>
<span v-if="state.ui.elapsed >= 60">
{{ minutes }}m
</span>
{{ seconds }}s wasted
<span class="p-2">
<i class="bi bi-box-arrow-down text-success" v-if="state.ui.save"></i>
<span style="padding-left: 12px" v-else>&nbsp;</span>
</span>
</span>
</template>

View File

@ -1,40 +0,0 @@
<script setup>
import { ref } from 'vue'
defineProps({
msg: String,
})
const count = ref(0)
</script>
<template>
<h1>{{ msg }}</h1>
<div class="card">
<button type="button" @click="count++">count is {{ count }}</button>
<p>
Edit
<code>components/HelloWorld.vue</code> to test HMR
</p>
</div>
<p>
Check out
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
>create-vue</a
>, the official Vue + Vite starter
</p>
<p>
Install
<a href="https://github.com/vuejs/language-tools" target="_blank">Volar</a>
in your IDE for a better DX
</p>
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
</template>
<style scoped>
.read-the-docs {
color: #888;
}
</style>

View File

@ -0,0 +1,53 @@
<script setup>
/////////////////////////////////////////////////////////////////////////
import { computed } from 'vue';
import { useState } from '../model/state.js'
import { nstr } from '../model/numbers.js'
import Buyer from './Buyer.vue'
import UpgradeBuyer from './UpgradeBuyer.vue'
const state = useState()
const data = computed(() => state.levels[state.ui.selectedLevel])
const previous = computed(() => state.levels[state.ui.selectedLevel - 1])
const next = computed(() => state.levels[state.ui.selectedLevel + 1])
/////////////////////////////////////////////////////////////////////////
</script>
<template>
<div class="container">
<h1>{{ data.name }}</h1>
<p class="pb-2">{{ data.blurb }}</p>
<p class="m-0">You have {{ nstr(data.count) }} {{ data.units }}</p>
<div v-if="state.ui.selectedLevel != 0">
<p class="m-0">Each one costs {{ nstr(data.cost) }} {{ previous.units }}
and produces {{ nstr(data.multiplier) }} {{ previous.units }} per second.</p>
<p class="m-0">In total your {{ data.units }} produce
{{ nstr(data.multiplier * data.count) }} {{ previous.units }} per second.</p>
</div>
<p v-if="state.ui.selectedLevel != (state.count - 1)">You earn
{{ nstr(next.multiplier * next.count) }} {{ data.units }} per second.</p>
<hr />
<p>You can buy {{ nstr(data.efficiency*(state.bandwidth.rate + state.bandwidth.serviceBonus(state))) }} {{ data.units }} per second.</p>
<p class="m-0">Adding the maximum of {{ nstr(data.maxCanBuy(state)) }} {{ data.units }} will cost:</p>
<ul class="pb-2 ps-4">
<li v-if="state.ui.selectedLevel != 0">{{ data.calcUnitCostPercent(state) }}% of your {{ previous.units }}</li>
<li>{{ data.calcBWCostPercent(state) }}% of your available bandwidth.</li>
</ul>
<Buyer :max="data.maxCanBuy(state)" @buy="(amount) => data.buy(state, amount)" />
<hr />
<h2>Upgrades</h2>
<template v-if="(state.ui.selectedLevel != 0)">
<h3 class="pt-3">Power Up</h3>
<p>Each power up doubles the number of {{ previous.units }} created every second</p>
<UpgradeBuyer :cost="data.multiplierCost" :available="data.count"
:units="data.units" @buy="data.upgradeMultiplier()"></UpgradeBuyer>
</template>
<template v-if="(state.ui.selectedLevel != (state.count - 1)) && next.count >= 1">
<h3 class="pt-3">Bandwidth Efficiency</h3>
<p>Upgrading bandwidth efficiency doubles the number of {{ data.units }} per unit of bandwidth</p>
<UpgradeBuyer :cost="data.efficiencyCost" :available="next.count"
:units="next.units" @buy="data.upgradeEfficiency(state)"></UpgradeBuyer>
</template>
</div></template>

View File

@ -0,0 +1,23 @@
<script setup>
/////////////////////////////////////////////////////////////////////////
import { ref } from 'vue'
import LevelSelector from './LevelSelector.vue'
import LevelInfo from './LevelInfo.vue'
import { useState } from '../model/state.js'
const state = useState()
/////////////////////////////////////////////////////////////////////////
</script>
<template>
<div class="container p-2">
<div class="row justify-content-start">
<div class="col-md-auto p-0">
<LevelSelector></LevelSelector>
</div>
<div class="col p-0 ps-2">
<LevelInfo></LevelInfo>
</div>
</div>
</div>
</template>

View File

@ -0,0 +1,44 @@
<script setup>
/////////////////////////////////////////////////////////////////////////
import { useState } from '../model/state.js'
import { nstr } from '../model/numbers.js'
import UpgradeIcon from './UpgradeIcon.vue';
const emits = defineEmits(['select-level'])
const state = useState()
/////////////////////////////////////////////////////////////////////////
// is 'l' the selected level ?
function isSelected(l) { return state.ui.selectedLevel == l }
// return the level indices in reverse order
function reverseLevels() {
var count = state.count
return Array.from({ length: count }, (x, i) => count - i - 1)
}
/////////////////////////////////////////////////////////////////////////
</script>
<template>
<table>
<template v-for="l in reverseLevels()">
<tr class="py-1 px-2 m-0 text-primary" :class="{ 'bg-light': isSelected(l),
'border-bottom': isSelected(l), 'text-dark': isSelected(l) }"
v-if="state.levels[l].unlocked"
@click="state.ui.selectedLevel = l">
<td class="text-center p-1">
<UpgradeIcon :active="(l != 0) && state.levels[l].canUpgrade()"></UpgradeIcon>
</td>
<td class="text-nowrap text-start p-1">
{{ state.levels[l].name }}
</td>
<td class="text-nowrap text-end p-1">
{{ nstr(state.levels[l].count) }}
</td>
</tr>
</template>
</table>
</template>

View File

@ -0,0 +1,57 @@
<script setup>
/////////////////////////////////////////////////////////////////////////
import { computed } from 'vue';
import { useState } from '../model/state.js'
import { nstr } from '../model/numbers.js'
import UpgradeBuyer from './UpgradeBuyer.vue'
const state = useState()
const time = computed(() => state.services.time)
const auto = computed(() => state.services.auto)
const anyUnlocked = computed(() => {
return (state.services.time.unlocked || state.services.auto.unlocked)
})
/////////////////////////////////////////////////////////////////////////
</script>
<template>
<div class="px-5">
<h1>Services</h1>
<p>Gain upgrades by introducing new services.</p>
<template v-if="anyUnlocked">
<div v-if="time.unlocked">
<h2 class="mt-3">Time Control</h2>
<p v-if="time.level >= 2">You have purchased all the time based upgrades.</p>
<template v-else>
<p>Install an NTP service to get time based upgrades.</p>
<p class="m-0" v-if="time.level == 0">Accurate clocks add bonus bandwidth the longer you've been in a level.</p>
<p class="m-0" v-if="time.level == 1">Precision timekeeping adds bandwidth based on time wasted.</p>
<UpgradeBuyer :cost="state.services.tCost()" :available="state.services.tValue(state)"
:units="state.services.tUnits(state)" @buy="state.services.tUpgrade(state)"></UpgradeBuyer>
</template>
</div>
<div v-if="auto.unlocked">
<h2 class="mt-3">Automation</h2>
<p v-if="auto.level >= 2">You have purchased all the automation upgrades.</p>
<template v-else>
<p>Install ansible to automate your nodes.</p>
<p class="m-0" v-if="auto.level == 0">Add runbooks to manage bandwidth.</p>
<p class="m-0" v-if="auto.level == 1">Node configuration scripts to manage your network.</p>
<UpgradeBuyer :cost="state.services.aCost()" :available="state.services.aValue(state)"
:units="state.services.aUnits(state)" @buy="state.services.aUpgrade(state)"></UpgradeBuyer>
</template>
<p class="mt-3" v-if="auto.level > 0">Configure autobuyers:</p>
<p class="m-0 ps-4" v-if="auto.level > 0"><input class="form-check-input me-2"
type="checkbox" id="enablebwauto" v-model="state.autobuyer.bandwidth"><label class="form-check-label"
for="enablebwauto">Enable bandwidth autobuyer</label></p>
<p class="m-0 ps-4" v-if="auto.level > 1"><input class="form-check-input me-2"
type="checkbox" id="enablelevelauto" v-model="state.autobuyer.levels"><label class="form-check-label"
for="enablelevelauto">Enable network autobuyer</label></p>
</div>
</template>
<p v-else>
No services are currently available, come back later for more.
</p>
</div>
</template>

View File

@ -0,0 +1,21 @@
<script setup>
/////////////////////////////////////////////////////////////////////////
import { useState } from '../model/state.js'
const state = useState()
/////////////////////////////////////////////////////////////////////////
</script>
<template>
<div class="p-2">
<h1>Settings</h1>
<hr />
<div>
<h5>Clear all settings and reset</h5>
<p class="text-danger"><b>*DANGER*</b> This will clear all progress and data! <b>*DANGER*</b></p>
<button type="button" class="btn btn-danger p-3" @click="state.reset()"><i
class="bi bi-exclamation-triangle-fill"></i>&nbsp;Reset</button>
</div>
<hr />
</div>
</template>

View File

@ -0,0 +1,25 @@
<script setup>
/////////////////////////////////////////////////////////////////////////
import { nstr } from '../model/numbers.js'
const props = defineProps(['cost', 'available', 'units'])
const emits = defineEmits(['buy'])
function completed() {
if (props.available > props.cost) { return 100 }
return Number((props.available * 100n) / props.cost)
}
/////////////////////////////////////////////////////////////////////////
</script>
<template>
<p>Next upgrade costs {{ nstr(cost) }} {{ units }}</p>
<div class="progress" style="height: 32px" v-if="available < cost">
<div class="progress-bar" role="progressbar" :style="{ 'width': completed() + '%' }" :aria-valuenow="completed()"
aria-valuemin="0" aria-valuemax="100">{{ completed() }}%</div>
</div>
<div class="text-center text-light rounded bg-success" style="height: 32px" @click="$emit('buy')" v-else>
<span class="align-middle">Buy</span>
</div>
</template>

View File

@ -0,0 +1,12 @@
<script setup>
/////////////////////////////////////////////////////////////////////////
const props = defineProps(['active'])
/////////////////////////////////////////////////////////////////////////
</script>
<template>
<i class="bi bi-wifi" v-if="active == true"></i>
<span style="padding-left: 12px" v-else>&nbsp;</span>
</template>

View File

@ -1,5 +0,0 @@
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
createApp(App).mount('#app')

42
src/model/autobuyer.js Normal file
View File

@ -0,0 +1,42 @@
/////////////////////////////////////////////////////////////////////////
// level functions
export class Autobuyer {
constructor() {
this.levels = false
this.bandwidth = false
}
// save data
save() {
return {
levels: this.levels,
bandwidth: this.bandwidth
}
}
restore(save_data) {
Object.assign(this, save_data)
}
// timer tick
tick(state, interval) {
if (this.levels) {
state.levels.forEach(l => {
if (l.shouldAutobuy()) { l.upgradeMultiplier() }
});
}
if (this.bandwidth) {
if (state.bandwidth.canUpgrade(state)) {
state.bandwidth.upgrade(state)
}
}
}
}
/////////////////////////////////////////////////////////////////////////
// end of file

81
src/model/bandwidth.js Normal file
View File

@ -0,0 +1,81 @@
/////////////////////////////////////////////////////////////////////////
// Bandwidth functions
export class Bandwidth {
constructor() {
this.count = BigInt(0)
this.rate = BigInt(1)
this.rateCost = BigInt(321)
}
// save data
save() {
let save_data = {}
let props = [
'count',
'rate', 'rateCost'
]
props.forEach((prop) => {
save_data[prop] = this[prop]
})
return save_data
}
restore(save_data) {
Object.assign(this, save_data)
}
// handle upgrades
canUpgrade(state) {
return this.rateCost <= state.levels[0].count
}
upgrade(state) {
// use up packets
state.levels[0].count -= this.rateCost
// increase bandwidth rate and cost
if (this.rate < 10) {
this.rate += 1n
this.rateCost *= 2n
}
else {
this.rate *= 2n
this.rateCost += this.rateCost * this.rate
}
}
serviceBonus(state) {
let bonus = 0n
if (state.services.time.level > 0) {
let i
for(i = state.levels.length - 1; i >= 0; i--) {
if (state.levels[i].unlocked) {
let multiplier = state.ui.elapsed - state.levels[i].unlockTime
// multiplier = multiplier > 7200 ? 7200 : multiplier
bonus += (this.rate * BigInt(multiplier)) / BigInt(7200 / i)
break
}
}
if (state.services.time.level > 1) {
//let multiplier = state.ui.elapsed > 172800 ? 86400 : state.ui.elapsed
let multiplier = state.ui.elapsed
bonus += (this.rate * BigInt(multiplier)) / BigInt(86400/i)
}
}
return bonus
}
// regular updates
tick(state, interval) {
this.count += this.rate * interval + this.serviceBonus(state)
}
}
/////////////////////////////////////////////////////////////////////////
// end of file

89
src/model/leveldefs.js Normal file
View File

@ -0,0 +1,89 @@
/////////////////////////////////////////////////////////////////////////
import { LevelTemplate } from './levels.js'
/////////////////////////////////////////////////////////////////////////
export const LevelDefs = [
new LevelTemplate({
name: 'Newcomer',
blurb: "Your PR has been merged and it's time to start pushing packets.",
units: 'packets',
cost: BigInt(1),
unlocked: true
}),
new LevelTemplate({
name: 'Peering Pro',
blurb: 'Increase the number of peers to get more packets.',
units: 'peers',
cost: BigInt(10)
}),
new LevelTemplate({
name: 'IPv6 Rollout',
blurb: 'Rollout IPv6 across your network.',
units: 'link local addresses',
cost: BigInt(100)
}),
new LevelTemplate({
name: 'Routing Updates',
blurb: 'Optimise your bird config to get ROA filters (look, no-one said this should make sense).',
units: 'ROA filters',
cost: BigInt(1000)
}),
new LevelTemplate({
name: 'Node Operator',
blurb: 'Expand your global footprint with new nodes.',
units: 'low end vps',
cost: BigInt(10000)
}),
new LevelTemplate({
name: 'Security Guru',
blurb: 'Improve your security by implementing firewalls.',
units: 'firewalls',
cost: BigInt(100000)
}),
new LevelTemplate({
name: 'File Sharing',
blurb: 'Spread your packets around.',
units: 'files transferred',
cost: BigInt(1000000)
}),
new LevelTemplate({
name: 'Anycast Deployment',
blurb: 'Create resilient global services.',
units: 'paths',
cost: BigInt(10000000)
}),
new LevelTemplate({
name: 'Community Member',
blurb: 'Help others by contributing to the wiki.',
units: 'wiki edits',
cost: BigInt(100000000)
}),
new LevelTemplate({
name: 'Multicast Streamer',
blurb: 'Use up all your bandwidth streaming music and video.',
units: 'streams',
cost: BigInt(1000000000)
}),
new LevelTemplate({
name: 'DN42 Master',
blurb: 'The network must grow !',
units: 'rp_filters disabled',
cost: BigInt(10000000000)
})
]
/////////////////////////////////////////////////////////////////////////
// end of file

141
src/model/levels.js Normal file
View File

@ -0,0 +1,141 @@
/////////////////////////////////////////////////////////////////////////
// level functions
export class LevelTemplate {
constructor(data) {
// common variables
this.count = BigInt(0)
this.multiplier = BigInt(1)
this.multiplierCost = BigInt(10)
this.efficiency = BigInt(1)
this.efficiencyCost = BigInt(1)
this.unlocked = false
this.unlockTime = 0
// copy level specific data in to object
Object.assign(this, data)
}
// save data
save() {
let save_data = { }
let props = [
'index', 'count',
'multiplier', 'multiplierCost',
'efficiency', 'efficiencyCost',
'unlocked', 'unlockTime'
]
props.forEach((prop) => {
save_data[prop] = this[prop]
})
return save_data
}
restore(save_data) {
Object.assign(this, save_data)
}
// unlock this level
unlock(elapsed) {
this.unlocked = true
this.unlockTime = elapsed
}
// upgrade functions
canUpgrade() {
return this.count >= this.multiplierCost
}
shouldAutobuy() {
return this.count >= (this.multiplierCost * 2n)
}
upgradeMultiplier() {
this.count -= this.multiplierCost
this.multiplier *= 2n
this.multiplierCost *= 12n
}
upgradeEfficiency(state) {
// am I the last level ?
if (this.index == (state.count - 1)) { return }
let nextLevel = state.levels[this.index + 1]
nextLevel.count -= this.efficiencyCost
this.efficiency *= 2n
this.efficiencyCost *= 11n
}
// calculate max unit cost as percentage of previous level units
calcUnitCostPercent(state) {
if (this.index == 0) { return 100 }
let max = this.maxCanBuyInternal(state)
let units = state.levels[this.index - 1].count
if (units == 0) { return 100 }
return Number((max * this.cost * 100n) / units)
}
// calculate max unit cost as percentage of available bandwidth
calcBWCostPercent(state) {
let max = this.maxCanBuyInternal(state)
let bw = state.bandwidth.count
if (bw == 0) { return 100 }
return Number((max * 100n) / bw)
}
// unit purchasing
buy(state, amount) {
// take cost based on amount less the efficiency
let a = amount / this.efficiency
state.bandwidth.count -= a
if (this.index != 0) {
state.levels[this.index - 1].count -= a * this.cost
}
// increase units
this.count += amount
}
// calculate the maximum number of units that can be bought
maxCanBuyInternal(state) {
let max
if (this.index == 0) {
// level zero is limited by bandwidth only
max = state.bandwidth.count
} else {
// otherwise its the maximum of bw and previous level
max = state.levels[this.index - 1].count / this.cost
if (max > state.bandwidth.count) {
max = state.bandwidth.count
}
}
return max
}
maxCanBuy(state) {
return this.maxCanBuyInternal(state) * this.efficiency
}
// timer tick
tick(state, interval) {
// am I the last level ?
if (this.index == (state.count - 1)) { return }
// add units to this level
let nextLevel = state.levels[this.index + 1]
this.count += nextLevel.count * nextLevel.multiplier
// unlock next level if possible
if ((this.count > 0) && (!nextLevel.unlocked)) { nextLevel.unlock(state.ui.elapsed) }
}
}
/////////////////////////////////////////////////////////////////////////
// end of file

43
src/model/numbers.js Normal file
View File

@ -0,0 +1,43 @@
/////////////////////////////////////////////////////////////////////////
// number functions
const SISym = [
'', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y', 'R', 'Q'
]
export function nstr(big) {
let m = 0
let r = 0
const onek = BigInt(1000)
while (big >= onek) {
r = Number(big % onek)
big /= onek
m++
}
// convert to fraction
let n = Number(big)
let f = n + (r / 1000)
if (m == 0) {
// simple number
return n.toString()
} else if (m < SISym.length) {
// with SI suffix
return f.toFixed(2) + SISym[m]
} else {
// scientific notation
m *= 3
while (f > 10) {
f /= 10
m++
}
return f.toFixed(2) + 'e' + m
}
}
/////////////////////////////////////////////////////////////////////////
// end of file

90
src/model/services.js Normal file
View File

@ -0,0 +1,90 @@
/////////////////////////////////////////////////////////////////////////
// level functions
export class Services {
constructor() {
this.time = {
unlocked: false,
level: 0,
}
this.auto = {
unlocked: false,
level: 0
}
}
// save data
save() {
let save_data = {
time: this.time,
auto: this.auto
}
return save_data
}
restore(save_data) {
Object.assign(this, save_data)
}
tick(state, interval) {
if (state.levels[2].count > 0) { this.auto.unlocked = true }
if (state.levels[3].count > 0) { this.time.unlocked = true }
}
// time costs
tCost() {
switch (this.time.level) {
case 0:
return 1000000n
case 1:
return 1000000000n
default:
return 0n
}
}
tUnits(state) {
return state.levels[(this.time.level*2)+3].units
}
tValue(state) {
return state.levels[(this.time.level*2)+3].count
}
tUpgrade(state) {
state.levels[(this.time.level*2)+3].count -= this.tCost()
this.time.level++
}
// auto costs
aCost() {
switch (this.auto.level) {
case 0:
return 1000000n
case 1:
return 1000000000n
default:
return 0n
}
}
aUnits(state) {
return state.levels[(this.auto.level*2)+2].units
}
aValue(state) {
return state.levels[(this.auto.level*2)+2].count
}
aUpgrade(state) {
state.levels[(this.auto.level*2)+2].count -= this.aCost()
this.auto.level++
}
}
/////////////////////////////////////////////////////////////////////////
// end of file

133
src/model/state.js Normal file
View File

@ -0,0 +1,133 @@
/////////////////////////////////////////////////////////////////////////
import { defineStore } from "pinia"
import { computed, reactive } from "vue"
import { LevelDefs } from './leveldefs.js'
import { Bandwidth } from './bandwidth.js'
import { Autobuyer } from "./autobuyer.js"
import { Services } from "./services.js"
/////////////////////////////////////////////////////////////////////////
export const useState = defineStore('state', () => {
// timer tick
function tick(state, i) {
let interval = BigInt(i)
ui.elapsed += i
// update bandwidth
bandwidth.tick(state, interval)
// update levels
levels.forEach((l) => {
l.tick(state, interval)
})
ui.save = ((ui.elapsed % 30) == 0)
if (ui.save) { save() }
autobuyer.tick(state, i)
services.tick(state, i)
}
// save and restore
function reset() {
localStorage.clear()
location.reload()
}
function save() {
let save_data = {
version: version,
elapsed: ui.elapsed,
levels: Array.from(levels, (l) => l.save()),
bandwidth: bandwidth.save(),
services: services.save(),
autobuyer: autobuyer.save()
}
// replacer func to handle big ints
const replacer = (key, value) =>
typeof value === 'bigint' ? '__big__' + value.toString() : value
let encoded = JSON.stringify(save_data, replacer)
localStorage.setItem('clicker42', encoded)
}
function restore() {
let encoded = localStorage.getItem('clicker42')
if (!encoded) { return }
// reviver func to handle big ints
const reviver = (key, value) =>
((typeof value === 'string') && (value.startsWith('__big__'))) ? BigInt(value.substring(7)) : value;
let save_data = JSON.parse(encoded, reviver)
// don't load older versions
if (version > save_data.version) { return }
ui.elapsed = save_data.elapsed
// restore levels
save_data.levels.forEach((l) => {
let index = l.index
levels[index].restore(l)
})
// and bandwidth
bandwidth.restore(save_data.bandwidth)
services.restore(save_data.services)
autobuyer.restore(save_data.autobuyer)
}
function noticeClear() {
ui.notice = '<span class="p-2">&nbsp;</span>'
}
function noticeTemplate(style, text) {
ui.notice = '<span class="badge p-2 rounded-pill ' + style + '">' + text + '</span>'
}
function noticeSuccess(text) { noticeTemplate('bg-success', text) }
function noticeDanger(text) { noticeTemplate('bg-danger', text) }
// game data
const version = 0.1
const ui = reactive({
selectedLevel: 0,
elapsed: 0,
notice: '<span class="p-2">&nbsp;</span>',
save: false
})
const autobuyer = reactive(new Autobuyer())
// level data
const levels = reactive(LevelDefs)
levels.forEach((l, ix) => {
l.index = ix
l.multiplier = BigInt(ix)
})
const bandwidth = reactive(new Bandwidth())
const services = reactive(new Services())
// computed functions
const count = computed(() => levels.length)
// return the things that will form part of the store
return {
// data
version, ui, levels, bandwidth, services, autobuyer,
// computed
count,
// functions
tick, reset, save, restore,
noticeClear, noticeSuccess, noticeDanger
}
})
/////////////////////////////////////////////////////////////////////////
// end of file

View File

@ -1,89 +0,0 @@
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
.card {
padding: 2em;
}
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

View File

@ -164,6 +164,11 @@
"@vue/compiler-dom" "3.2.47" "@vue/compiler-dom" "3.2.47"
"@vue/shared" "3.2.47" "@vue/shared" "3.2.47"
"@vue/devtools-api@^6.5.0":
version "6.5.0"
resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.5.0.tgz#98b99425edee70b4c992692628fa1ea2c1e57d07"
integrity sha512-o9KfBeaBmCKl10usN4crU53fYtC1r7jJwdGKjPT24t348rHxgfpZ0xL3Xm/gLUYnc0oTp8LAmrxOeLyu6tbk2Q==
"@vue/reactivity-transform@3.2.47": "@vue/reactivity-transform@3.2.47":
version "3.2.47" version "3.2.47"
resolved "https://registry.yarnpkg.com/@vue/reactivity-transform/-/reactivity-transform-3.2.47.tgz#e45df4d06370f8abf29081a16afd25cffba6d84e" resolved "https://registry.yarnpkg.com/@vue/reactivity-transform/-/reactivity-transform-3.2.47.tgz#e45df4d06370f8abf29081a16afd25cffba6d84e"
@ -212,6 +217,16 @@
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.2.47.tgz#e597ef75086c6e896ff5478a6bfc0a7aa4bbd14c" resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.2.47.tgz#e597ef75086c6e896ff5478a6bfc0a7aa4bbd14c"
integrity sha512-BHGyyGN3Q97EZx0taMQ+OLNuZcW3d37ZEVmEAyeoA9ERdGvm9Irc/0Fua8SNyOtV1w6BS4q25wbMzJujO9HIfQ== integrity sha512-BHGyyGN3Q97EZx0taMQ+OLNuZcW3d37ZEVmEAyeoA9ERdGvm9Irc/0Fua8SNyOtV1w6BS4q25wbMzJujO9HIfQ==
bootstrap-icons@^1.10.3:
version "1.10.3"
resolved "https://registry.yarnpkg.com/bootstrap-icons/-/bootstrap-icons-1.10.3.tgz#c587b078ca6743bef4653fe90434b4aebfba53b2"
integrity sha512-7Qvj0j0idEm/DdX9Q0CpxAnJYqBCFCiUI6qzSPYfERMcokVuV9Mdm/AJiVZI8+Gawe4h/l6zFcOzvV7oXCZArw==
bootstrap@^5.2.3:
version "5.2.3"
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-5.2.3.tgz#54739f4414de121b9785c5da3c87b37ff008322b"
integrity sha512-cEKPM+fwb3cT8NzQZYEu4HilJ3anCrWqh3CHAok1p9jXqMPsPTBhU25fBckEJHJ/p+tTxTFTsFQGM+gaHpi3QQ==
csstype@^2.6.8: csstype@^2.6.8:
version "2.6.21" version "2.6.21"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.21.tgz#2efb85b7cc55c80017c66a5ad7cbd931fda3a90e" resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.21.tgz#2efb85b7cc55c80017c66a5ad7cbd931fda3a90e"
@ -296,6 +311,14 @@ picocolors@^1.0.0:
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
pinia@^2.0.33:
version "2.0.33"
resolved "https://registry.yarnpkg.com/pinia/-/pinia-2.0.33.tgz#b70065be697874d5824e9792f59bd5d87ddb5e7d"
integrity sha512-HOj1yVV2itw6rNIrR2f7+MirGNxhORjrULL8GWgRwXsGSvEqIQ+SE0MYt6cwtpegzCda3i+rVTZM+AM7CG+kRg==
dependencies:
"@vue/devtools-api" "^6.5.0"
vue-demi "*"
postcss@^8.1.10, postcss@^8.4.21: postcss@^8.1.10, postcss@^8.4.21:
version "8.4.21" version "8.4.21"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.21.tgz#c639b719a57efc3187b13a1d765675485f4134f4" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.21.tgz#c639b719a57efc3187b13a1d765675485f4134f4"
@ -353,6 +376,11 @@ vite@^4.2.0:
optionalDependencies: optionalDependencies:
fsevents "~2.3.2" fsevents "~2.3.2"
vue-demi@*:
version "0.13.11"
resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.13.11.tgz#7d90369bdae8974d87b1973564ad390182410d99"
integrity sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==
vue@^3.2.47: vue@^3.2.47:
version "3.2.47" version "3.2.47"
resolved "https://registry.yarnpkg.com/vue/-/vue-3.2.47.tgz#3eb736cbc606fc87038dbba6a154707c8a34cff0" resolved "https://registry.yarnpkg.com/vue/-/vue-3.2.47.tgz#3eb736cbc606fc87038dbba6a154707c8a34cff0"