Compare commits

...

4 Commits

Author SHA1 Message Date
cabfc5e758
Towers goes on cooldown during raiding
Some checks are pending
CI / Foundry project (push) Waiting to run
2024-10-23 13:17:28 +02:00
56e4b5ce26
Implemented rotating counter 2024-10-23 11:14:39 +02:00
294052070a
Connected addUnit 2024-10-23 10:32:13 +02:00
20fa42cfca
Annointed is actually spelled anointed 2024-10-23 10:07:49 +02:00
18 changed files with 345 additions and 109 deletions

View File

@ -2,7 +2,7 @@
Idle game & shitcoin advanture dedicated to cohort VII of Raid Guild.
## Set up
## Set up for local DEV
### 1. Run `anvil` to setup local RPC
@ -14,7 +14,11 @@ Either use `./deploy_contract.sh` script (!! change contract values and set priv
Move to `app` dir, install deps via `npm install` and run `npm run dev` to start the dev server.
#### 3. 1. Point Metamask to Anvil network for local dev
#### 3. 1. Run `cast rpc anvil_mine`
This is so time gets set on the local chain, otherwise you will start at 0 time and first mint will give you bajillion GELD.
#### 3. 2. Point Metamask to Anvil network for local dev
### 4. Local development requires mining blocks by hand

BIN
app/public/roles/druid2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

View File

@ -0,0 +1,25 @@
import { usePlayer } from '../providers/PlayerProvider';
import styles from '../styles/Army.module.css';
const Army = () => {
const { army, addUnit } = usePlayer()
return <div className="styles.armyGathering">
<div className={`${styles.tavern_keeper} ${styles.person} ${styles.static}`} />
<div onClick={() => addUnit(0)} className={`${styles.scribe} ${styles.person} ${styles.moloch_denier} ${styles.static}`}>
<div className={styles.supply}>Moloch denier: {army?.moloch_denier.level}</div>
</div>
<div onClick={() => addUnit(1)} className={`${styles.druid} ${styles.person} ${styles.apprentice} ${styles.static}`} >
<div className={styles.supply}>Apprentice: {army?.apprentice.level}</div>
</div>
<div onClick={() => addUnit(2)} className={`${styles.ranger} ${styles.person} ${styles.anointed} ${styles.static}`} >
<div className={styles.supply}>Anointed: {army?.anointed.level}</div>
</div>
<div onClick={() => addUnit(3)} className={`${styles.warrior} ${styles.person} ${styles.champion} ${styles.static}`} >
<div className={styles.supply}>Champion: {army?.champion.level}</div>
</div>
</div>
}
export default Army

View File

@ -0,0 +1,75 @@
import { useEffect, useReducer, useRef } from "react";
import { usePlayer } from "../providers/PlayerProvider"
import styles from "../styles/Header.module.css"
const calculateBalance = (balance: bigint, perSecond: bigint, lastRaidedAt: bigint) => {
// convert to milliseconds trick so we get a more smooth counter
const millisecondsSinceLastRaid =
(new Date()).getTime() - parseInt(lastRaidedAt.toString()) * 1000;
return (
balance +
(BigInt(millisecondsSinceLastRaid) * (perSecond * BigInt(10000) /* decimals */)
/ BigInt(1000) /* deduct milliseconds*/))
}
const toReadable = (value: bigint) => {
value = value / BigInt(10000);
const suffixes = [
{ value: BigInt('1000'), suffix: 'thousand' },
{ value: BigInt('1000000'), suffix: 'million' },
{ value: BigInt('1000000000'), suffix: 'billion' },
{ value: BigInt('1000000000000'), suffix: 'trillion' },
{ value: BigInt('1000000000000000'), suffix: 'quadrillion' },
{ value: BigInt('1000000000000000000'), suffix: 'quintillion' },
{ value: BigInt('1000000000000000000000'), suffix: 'sextillion' },
{ value: BigInt('1000000000000000000000000'), suffix: 'septillion' },
{ value: BigInt('1000000000000000000000000000'), suffix: 'octillion' },
{ value: BigInt('1000000000000000000000000000000'), suffix: 'nonillion' },
{ value: BigInt('1000000000000000000000000000000000'), suffix: 'decillion' },
{ value: BigInt('1000000000000000000000000000000000000'), suffix: 'undecillion' },
{ value: BigInt('1000000000000000000000000000000000000000'), suffix: 'duodecillion' },
{ value: BigInt('1000000000000000000000000000000000000000000'), suffix: 'tredecillion' },
{ value: BigInt('1000000000000000000000000000000000000000000000'), suffix: 'quattuordecillion' },
{ value: BigInt('1000000000000000000000000000000000000000000000000'), suffix: 'quindecillion' },
{ value: BigInt('1000000000000000000000000000000000000000000000000000'), suffix: 'sexdecillion' },
{ value: BigInt('1000000000000000000000000000000000000000000000000000000'), suffix: 'septendecillion' },
];
for (let i = 0; i < suffixes.length; i++) {
if (value < suffixes[i].value) {
if (i == 0) {
return value;
} else {
const divided = value / suffixes[i - 1].value;
const remainder = value % suffixes[i - 1].value;
return `${divided.toString()}.${remainder.toString()} ${suffixes[i - 1].suffix}`;
}
}
}
return value.toString();
}
const Counter = () => {
const { balance, army, player } = usePlayer();
const [, render] = useReducer(p => !p, false);
const balanceCount = useRef(balance.toString() ?? "0")
useEffect(() => {
const tickInterval = setInterval(() => {
balanceCount.current = toReadable(calculateBalance(
balance,
army?.profit_per_second ?? BigInt(0),
player?.last_raided_at ?? BigInt(0)
)).toString();
render();
}, 100);
return () => clearInterval(tickInterval)
}, [balance, army?.profit_per_second, player?.last_raided_at])
return <p className={styles.counter}>
{balanceCount.current} GELD
</p>
}
export default Counter

View File

@ -1,21 +1,13 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"
import React, { useCallback, useMemo } from "react"
import styles from "../styles/Header.module.css"
import { usePlayer } from "../providers/PlayerProvider";
import { useAccount } from 'wagmi';
import dynamic from "next/dynamic";
import { formatUnits } from "viem";
import Counter from "./Counter";
const Header = () => {
const { isConnected } = useAccount();
const { isRegistered, register, balance } = usePlayer();
const [count, setCount] = useState("0")
const [perSecond, setPerSecond] = useState("0")
useEffect(() => {
if (balance != null) {
setCount(formatUnits(balance, 4))
}
}, [balance])
const { isRegistered, register, army } = usePlayer();
const title = useMemo(() => {
return isRegistered ? `SLAY THE MOLOCH` :
@ -25,17 +17,17 @@ const Header = () => {
const subtitle = useMemo(() => {
if (isRegistered) {
return <p className={styles.counter}>{count} GELD</p>
return <Counter />
} else {
return <p className={styles.counter}>SLAY THE MOLOCH</p>
}
}, [isRegistered, count])
}, [isRegistered])
const perSecondParagraph = useMemo(() => {
return (isRegistered) ?
<p className={styles.counter_per_seconds}>per second: {perSecond}</p>
<p className={styles.counter_per_seconds}>per second: {army?.profit_per_second.toString()}</p>
: null
}, [isRegistered, perSecond])
}, [isRegistered, army?.profit_per_second])
const onRegister = useCallback(() => {
if (isRegistered) return
@ -43,7 +35,6 @@ const Header = () => {
}, [isRegistered, register])
return <header onClick={onRegister} className={styles.header}>
{count.current} {balance}
<h1 className={styles.title}>{title}</h1>
{subtitle}
{perSecondParagraph}

View File

@ -1,8 +1,9 @@
import React from "react"
import styles from '../styles/Background.module.css';
import Tower from "./Tower";
import Army from "./Army";
const Background = () => {
const Scene = () => {
return <div className={styles.frame}>
<div className={`${styles.air} ${styles.background_asset}`} />
<div className={`${styles.clouds_small} ${styles.background_asset}`} />
@ -10,9 +11,9 @@ const Background = () => {
<Tower />
<div className={`${styles.mountains} ${styles.background_asset}`} />
<div className={`${styles.village} ${styles.background_asset}`} />
<div className={`${styles.tavern_keeper} ${styles.person} ${styles.static_keeper}`} />
<div className={`${styles.bonfire} ${styles.background_asset}`} />
<Army />
</div>
}
export default Background
export default Scene

View File

@ -1,9 +1,33 @@
import { useEffect, useReducer, useRef } from 'react';
import { usePlayer } from '../providers/PlayerProvider';
import styles from '../styles/Background.module.css';
const onCooldown = (lastRaidedAt: bigint) => (
((new Date()).getTime()
- (parseInt(lastRaidedAt.toString()) * 1000 /* convert block time to seconds */))
/ 1000 /* convert milliseconds back to seconds*/
) <= 15
const emptyFn = () => { }
const Tower = () => {
const { raid } = usePlayer();
return <div onClick={raid} className={`${styles.tower} ${styles.background_asset}`} />
const { raid, player } = usePlayer();
const isOnCooldown = useRef(false);
const [, render] = useReducer(p => !p, false);
useEffect(() => {
const checkCooldownInterval = setInterval(() => {
isOnCooldown.current = onCooldown(player?.last_raided_at ?? BigInt(0))
render()
}, 1000);
return () => clearInterval(checkCooldownInterval)
}, [player?.last_raided_at])
return <div onClick={isOnCooldown.current ? emptyFn : raid} className={`
${styles.tower}
${styles.background_asset}
${isOnCooldown.current ? styles.cooldown : ""}
`} />
}
export default Tower

View File

@ -3,7 +3,7 @@ import type { NextPage } from 'next';
import Head from 'next/head';
import styles from '../styles/Home.module.css';
import Header from '../components/Header';
import Background from '../components/Background';
import Scene from '../components/Scene';
const Home: NextPage = () => {
return (
@ -22,7 +22,7 @@ const Home: NextPage = () => {
<ConnectButton />
</div>
<Header />
<Background />
<Scene />
</main>
<footer className={styles.footer}>

View File

@ -6,26 +6,46 @@ import { parseEther } from 'viem'
const contractAddress = "0xbd06B0878888bf4c6895704fa603a5ADf7e65c66"
const abi = contractAbi.abi
export interface Player {
created_at: bigint,
last_raided_at: bigint,
total_minted: bigint
}
export interface Army {
anointed: { level: number }
apprentice: { level: number }
champion: { level: number }
moloch_denier: { level: number }
profit_per_second: bigint
}
export interface PlayerContextType {
isRegistered: boolean,
user: null | string,
player: null | Player,
army: null | Army,
balance: bigint,
register: () => void;
raid: () => void
register: () => void,
raid: () => void,
addUnit: (unit: number) => void
}
const PlayerContext = createContext<PlayerContextType>({
isRegistered: false,
user: null,
player: null,
army: null,
balance: BigInt(0),
register: () => { },
raid: () => { },
addUnit: () => { }
});
const PlayerProvider = ({ children }: { children: ReactNode }) => {
const { address, isConnected } = useAccount();
const { writeContract, error } = useWriteContract();
useEffect(() => {
console.warn(error)
}, [error])
const { data: isRegistered } = useReadContract({
address: contractAddress,
@ -49,7 +69,7 @@ const PlayerProvider = ({ children }: { children: ReactNode }) => {
}
});
const { data: playerData } = useReadContract({
const { data: player } = useReadContract({
address: contractAddress,
abi,
functionName: 'getPlayer',
@ -60,11 +80,16 @@ const PlayerProvider = ({ children }: { children: ReactNode }) => {
}
});
console.log(playerData)
useEffect(() => {
console.warn(error, playerData)
}, [error])
const { data: army } = useReadContract({
address: contractAddress,
abi,
functionName: 'getArmy',
args: [address],
query: {
enabled: isConnected,
refetchInterval: 15
}
});
const register = useCallback(() => {
writeContract({
@ -83,13 +108,24 @@ const PlayerProvider = ({ children }: { children: ReactNode }) => {
})
}, [writeContract])
const addUnit = useCallback((unit: number) => {
writeContract({
abi,
address: contractAddress,
functionName: 'addUnit',
args: [unit, 1]
})
}, [writeContract])
return (
<PlayerContext.Provider value={{
isRegistered: isRegistered as boolean,
user: null,
player: player as Player,
army: army as Army,
balance: balance as bigint,
register,
raid
raid,
addUnit
}}>
{children}
</PlayerContext.Provider>

View File

@ -0,0 +1,124 @@
.army-gathering {
position: absolute;
bottom: 22px;
left: 22px;
right: 22px;
}
.person {
position: absolute;
width: 80px;
height: 80px;
background-size: contain;
background-position: bottom center;
background-repeat: no-repeat;
transform-origin: bottom center;
}
.tavern_keeper {
background-image: url("/roles/tavern-keeper.svg");
}
.alchemist {
background-image: url("/roles/alchemist.svg");
}
.archer {
background-image: url("/roles/archer.svg");
}
.cleric {
background-image: url("/roles/cleric.svg");
}
.druid {
background-image: url("/roles/druid.svg");
}
.dwarf {
background-image: url("/roles/dwarf.svg");
}
.monk {
background-image: url("/roles/monk.svg");
}
.necromancer {
background-image: url("/roles/necromancer.svg");
}
.paladin {
background-image: url("/roles/paladin.svg");
}
.ranger {
background-image: url("/roles/ranger.svg");
}
.rogue {
background-image: url("/roles/rogue.svg");
}
.scribe {
background-image: url("/roles/scribe.svg");
}
.warrior {
background-image: url("/roles/warrior.svg");
}
.wizard {
background-image: url("/roles/wizard.svg");
}
.healer {
background-image: url("/roles/healer.svg");
}
.hunter {
background-image: url("/roles/hunter.svg");
}
.static {
width: 110px;
height: 110px;
transition: all 100ms cubic-bezier(0.265, 1.4, 0.68, 1.65);
&:not(.locked) {
cursor: pointer;
}
&:hover {
transform: scale(1.08, 1.08);
}
&:active {
transform: scale(1.2, 1.2);
}
}
.static.tavern_keeper {
right: 130px;
bottom: 160px;
width: 90px;
height: 90px;
}
.static.moloch_denier {
left: calc(20% - 60px);
bottom: 70px;
background-image: url("/roles/scribe2.png");
}
.static.apprentice {
left: calc(36% - 60px);
background-image: url("/roles/druid2.png");
bottom: 72px;
}
.static.anointed {
left: calc(50% - 60px);
bottom: 64px;
background-image: url("/roles/ranger2.png");
}
.static.champion {
left: calc(64% - 60px);
bottom: 66px;
background-image: url("/roles/warrior2.png");
}
.moloch_denier {
filter: sepia(0.1);
}
.apprentice {
}
.anointed {
filter: saturate(1.1);
}
.champion {
filter: saturate(2);
}
.supply {
position: absolute;
bottom: -40px;
background: rgba(0, 0, 0, 0.89);
padding: 0.5rem 1rem;
font-size: 0.8rem;
user-select: none;
}

View File

@ -50,7 +50,8 @@
height: 240px;
top: 200px;
animation: thunder_hue_hard 12s linear infinite;
transition: all ease-in-out 0.05s;
transition: all 0.1s cubic-bezier(0.265, 1.4, 0.68, 1.65);
transform-origin: bottom center;
&:hover {
cursor: pointer;
transform: scale(1.05, 1.1);
@ -63,6 +64,21 @@
&:active {
transform: scale(1.1, 1.22);
}
&.cooldown {
transition: all 1s cubic-bezier(0.265, 1.4, 0.68, 1.65);
transform: scale(1.1, 1.22);
&::after {
content: "RAID IN PROGRESS";
color: var(--hover-color);
top: calc(50% - 15px);
left: 0;
right: 0;
font-size: 0.9rem;
text-align: center;
animation: excited 0.5s infinite linear;
text-shadow: #000 1px 1px 1px;
}
}
}
.tower::after {
position: absolute;
@ -94,67 +110,6 @@
bonfire 12s linear infinite,
bonfire_skew 5s infinite linear;
}
.person {
position: absolute;
width: 80px;
height: 80px;
background-size: contain;
background-position: bottom center;
background-repeat: no-repeat;
}
.tavern_keeper {
background-image: url("/roles/tavern-keeper.svg");
}
.alchemist {
background-image: url("/roles/alchemist.svg");
}
.archer {
background-image: url("/roles/archer.svg");
}
.cleric {
background-image: url("/roles/cleric.svg");
}
.druid {
background-image: url("/roles/druid.svg");
}
.dwarf {
background-image: url("/roles/dwarf.svg");
}
.monk {
background-image: url("/roles/monk.svg");
}
.necromancer {
background-image: url("/roles/necromancer.svg");
}
.paladin {
background-image: url("/roles/paladin.svg");
}
.ranger {
background-image: url("/roles/ranger.svg");
}
.rogue {
background-image: url("/roles/rogue.svg");
}
.scribe {
background-image: url("/roles/scribe.svg");
}
.warrior {
background-image: url("/roles/warrior.svg");
}
.wizard {
background-image: url("/roles/wizard.svg");
}
.healer {
background-image: url("/roles/healer.svg");
}
.hunter {
background-image: url("/roles/hunter.svg");
}
.static_keeper {
right: 130px;
bottom: 160px;
}
@keyframes scrollBackground {
0% {
background-position: 0 0;

View File

@ -12,6 +12,7 @@
font-weight: 600;
margin: 0.5rem 0 0.2rem;
line-height: 2rem;
color: var(--hover-color);
}
.counter_per_seconds {
font-size: 1.2rem;

View File

@ -12,7 +12,7 @@ struct Raider {
struct Army {
Raider moloch_denier;
Raider apprentice;
Raider annointed;
Raider anointed;
Raider champion;
uint256 profit_per_second;
}
@ -53,7 +53,7 @@ contract RaidGeld is ERC20, Ownable {
armies[msg.sender] = Army({
moloch_denier: Raider({level: 0}),
apprentice: Raider({level: 0}),
annointed: Raider({level: 0}),
anointed: Raider({level: 0}),
champion: Raider({level: 0}),
profit_per_second: 0
});
@ -111,8 +111,8 @@ contract RaidGeld is ERC20, Ownable {
// apprentice
currentLevel = army.apprentice.level;
} else if (unit == 2) {
// annointed
currentLevel = army.annointed.level;
// anointed
currentLevel = army.anointed.level;
} else if (unit == 3) {
// champion
currentLevel = army.champion.level;
@ -130,8 +130,8 @@ contract RaidGeld is ERC20, Ownable {
// apprentice
army.apprentice.level += n_units;
} else if (unit == 2) {
// annointed
army.annointed.level += n_units;
// anointed
army.anointed.level += n_units;
} else if (unit == 3) {
// champion
army.champion.level += n_units;

View File

@ -24,8 +24,8 @@ library RaidGeldUtils {
// Each next unit scales progressivelly better
uint256 moloch_denier_profit = army.moloch_denier.level;
uint256 apprentice_profit = army.apprentice.level * 61 / 10;
uint256 annointed_profit = army.annointed.level * 6 * 64 / 10;
uint256 anointed_profit = army.anointed.level * 6 * 64 / 10;
uint256 champion_profit = army.champion.level * 61 / 10 * 64 / 10 * 67 / 10;
return moloch_denier_profit + apprentice_profit + annointed_profit + champion_profit;
return moloch_denier_profit + apprentice_profit + anointed_profit + champion_profit;
}
}

View File

@ -61,7 +61,7 @@ contract raid_geldTest is Test {
assertEq(army.moloch_denier.level, 0);
assertEq(army.apprentice.level, 0);
assertEq(army.annointed.level, 0);
assertEq(army.anointed.level, 0);
assertEq(army.champion.level, 0);
}
@ -117,7 +117,7 @@ contract raid_geldTest is Test {
// Add 2 units
Army memory army = raid_geld.getArmy(player1);
uint256 unit_level = army.annointed.level;
uint256 unit_level = army.moloch_denier.level;
uint256 balance = raid_geld.balanceOf(player1);
uint256 income_per_sec = army.profit_per_second;
raid_geld.addUnit(0, 2);