1
0
forked from mico/idle_moloch

Merge pull request 'pixelated quote integration with pixel-borders library' (#5) from pixelatedQuotes into main

Reviewed-on: mico/idle_moloch#5
This commit is contained in:
mic0 2024-10-28 18:11:11 +00:00
commit b8f15bd31e
7 changed files with 328 additions and 84 deletions

60
app/package-lock.json generated
View File

@ -14,6 +14,7 @@
"next": "^14.2.10", "next": "^14.2.10",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"sass": "^1.80.4",
"viem": "^2.17.0", "viem": "^2.17.0",
"wagmi": "^2.12.17" "wagmi": "^2.12.17"
}, },
@ -22,6 +23,7 @@
"@types/react": "^18.3.5", "@types/react": "^18.3.5",
"eslint": "8.57.0", "eslint": "8.57.0",
"eslint-config-next": "14.2.15", "eslint-config-next": "14.2.15",
"pixel-borders": "^1.1.4",
"typescript": "5.5.2" "typescript": "5.5.2"
} }
}, },
@ -9103,6 +9105,11 @@
"node": ">=16.x" "node": ">=16.x"
} }
}, },
"node_modules/immutable": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz",
"integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw=="
},
"node_modules/import-fresh": { "node_modules/import-fresh": {
"version": "3.3.0", "version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@ -12110,6 +12117,16 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/pixel-borders": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/pixel-borders/-/pixel-borders-1.1.4.tgz",
"integrity": "sha512-jBX9MMRsCeYNl1sHnXnAgtBD5hLAMa59/MDvVBZUoeHtsF6t97goigzIPQHvssh8xsQ5w+vF7hqabutbpb0zxA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/pkg-dir": { "node_modules/pkg-dir": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz",
@ -13343,6 +13360,49 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/sass": {
"version": "1.80.4",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.80.4.tgz",
"integrity": "sha512-rhMQ2tSF5CsuuspvC94nPM9rToiAFw2h3JTrLlgmNw1MH79v8Cr3DH6KF6o6r+8oofY3iYVPUf66KzC8yuVN1w==",
"dependencies": {
"@parcel/watcher": "^2.4.1",
"chokidar": "^4.0.0",
"immutable": "^4.0.0",
"source-map-js": ">=0.6.2 <2.0.0"
},
"bin": {
"sass": "sass.js"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass/node_modules/chokidar": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz",
"integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==",
"dependencies": {
"readdirp": "^4.0.1"
},
"engines": {
"node": ">= 14.16.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/sass/node_modules/readdirp": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz",
"integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==",
"engines": {
"node": ">= 14.16.0"
},
"funding": {
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/scheduler": { "node_modules/scheduler": {
"version": "0.23.2", "version": "0.23.2",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",

View File

@ -15,6 +15,7 @@
"next": "^14.2.10", "next": "^14.2.10",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"sass": "^1.80.4",
"viem": "^2.17.0", "viem": "^2.17.0",
"wagmi": "^2.12.17" "wagmi": "^2.12.17"
}, },
@ -23,6 +24,7 @@
"@types/react": "^18.3.5", "@types/react": "^18.3.5",
"eslint": "8.57.0", "eslint": "8.57.0",
"eslint-config-next": "14.2.15", "eslint-config-next": "14.2.15",
"pixel-borders": "^1.1.4",
"typescript": "5.5.2" "typescript": "5.5.2"
} }
} }

View File

@ -1,7 +1,8 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { UnitType, usePlayer } from '../providers/PlayerProvider'; import { UnitType, usePlayer } from "../providers/PlayerProvider";
import styles from '../styles/Army.module.css'; import styles from "../styles/Army.module.css";
import { calculateBalance, toReadable } from './Counter'; import { calculateBalance, toReadable } from "./Counter";
import PixelatedQuote from "./PixelatedQuote";
const PRECISION = BigInt(10000); const PRECISION = BigInt(10000);
const PRICE_FACTOR = BigInt(11500); const PRICE_FACTOR = BigInt(11500);
@ -11,16 +12,20 @@ const base_cost: Record<UnitType, bigint> = {
1: BigInt(3420000), 1: BigInt(3420000),
2: BigInt(30096000), 2: BigInt(30096000),
3: BigInt(255816000), 3: BigInt(255816000),
} };
const profits: Record<UnitType, bigint> = { const profits: Record<UnitType, bigint> = {
0: BigInt(2533), 0: BigInt(2533),
1: BigInt(27863), 1: BigInt(27863),
2: BigInt(306493), 2: BigInt(306493),
3: BigInt(3371423), 3: BigInt(3371423),
} };
function calculateUnitPrice(unit: UnitType, currentLevel: number, units: number) { function calculateUnitPrice(
unit: UnitType,
currentLevel: number,
units: number
) {
let rollingPriceCalculation = base_cost[unit]; let rollingPriceCalculation = base_cost[unit];
let price = BigInt(0); let price = BigInt(0);
@ -29,127 +34,171 @@ function calculateUnitPrice(unit: UnitType, currentLevel: number, units: number)
if (i >= currentLevel) { if (i >= currentLevel) {
price += rollingPriceCalculation; price += rollingPriceCalculation;
} }
rollingPriceCalculation = rollingPriceCalculation * PRICE_FACTOR / PRECISION; rollingPriceCalculation =
(rollingPriceCalculation * PRICE_FACTOR) / PRECISION;
} }
return price; return price;
} }
function calculateProfitPerSecond(unit: UnitType, level: number) { function calculateProfitPerSecond(unit: UnitType, level: number) {
// Each next unit scales progressivelly better // Each next unit scales progressivelly better
return BigInt(level) * profits[unit] return BigInt(level) * profits[unit];
} }
const unitTypeToCss: Record<UnitType, string> = { const unitTypeToCss: Record<UnitType, string> = {
0: styles.moloch_denier, 0: styles.moloch_denier,
1: styles.apprentice, 1: styles.apprentice,
2: styles.anointed, 2: styles.anointed,
3: styles.champion 3: styles.champion,
} };
const unitTypeToName: Record<UnitType, string> = { const unitTypeToName: Record<UnitType, string> = {
0: "Moloch denier", 0: "Moloch denier",
1: "Apprentice", 1: "Apprentice",
2: "Anointed", 2: "Anointed",
3: "Champion" 3: "Champion",
} };
const defaultAvailabilityMap: Record<UnitType, boolean> = { const defaultAvailabilityMap: Record<UnitType, boolean> = {
0: false, 0: false,
1: false, 1: false,
2: false, 2: false,
3: false 3: false,
} };
const unitDiscoveredAt: Record<UnitType, bigint> = { const unitDiscoveredAt: Record<UnitType, bigint> = {
0: BigInt(0), 0: BigInt(0),
1: BigInt(300_0000), 1: BigInt(300_0000),
2: BigInt(2800_0000), 2: BigInt(2800_0000),
3: BigInt(24000_0000) 3: BigInt(24000_0000),
} };
const unitAvailableToDiscoverAt: Record<UnitType, bigint> = { const unitAvailableToDiscoverAt: Record<UnitType, bigint> = {
0: BigInt(0), 0: BigInt(0),
1: BigInt(200_0000), 1: BigInt(200_0000),
2: BigInt(2000_0000), 2: BigInt(2000_0000),
3: BigInt(25000_0000), 3: BigInt(25000_0000),
} };
interface UnitProps { interface UnitProps {
addUnit: (unitType: UnitType) => void; addUnit: (unitType: UnitType) => void;
unitType: UnitType; unitType: UnitType;
canPurchase: boolean; canPurchase: boolean;
isShrouded: boolean; isShrouded: boolean;
n_units: number n_units: number;
} }
const Unit = ({ addUnit, unitType, canPurchase, isShrouded, n_units }: UnitProps) => { const Unit = ({
addUnit,
unitType,
canPurchase,
isShrouded,
n_units,
}: UnitProps) => {
const [unitPrice, unitProfit] = useMemo(() => { const [unitPrice, unitProfit] = useMemo(() => {
return [ return [
toReadable(calculateUnitPrice(unitType, n_units, 1)), toReadable(calculateUnitPrice(unitType, n_units, 1)),
toReadable(calculateProfitPerSecond(unitType, n_units)) toReadable(calculateProfitPerSecond(unitType, n_units)),
] ];
}, [n_units, unitType]); }, [n_units, unitType]);
return <div onClick={() => addUnit(unitType)} className={`${styles.armyUnit} ${canPurchase ? "" : styles.isUnavailable}`}> return (
<div className={` <div
onClick={() => addUnit(unitType)}
className={`${styles.armyUnit} ${canPurchase ? "" : styles.isUnavailable
}`}
>
<div
className={`
${unitTypeToCss[unitType]} ${unitTypeToCss[unitType]}
${styles.person} ${styles.person}
${styles.static} ${styles.static}
${isShrouded ? styles.isShrouded : ""} ${isShrouded ? styles.isShrouded : ""}
`} /> `}
<span className={`${styles.unitName} ${styles.uiElement}`}>{isShrouded ? "???????" : unitTypeToName[unitType]}</span> />
{isShrouded ? null : <span className={`${styles.unitPrice} ${styles.uiElement}`}>{unitPrice} <small>GELD</small></span>} <span className={`${styles.unitName} ${styles.uiElement}`}>
{n_units > 0 ? <span className={`${styles.unitSupply} ${styles.uiElement}`}>{n_units}</span> : null} {isShrouded ? "???????" : unitTypeToName[unitType]}
{n_units > 0 ? <span className={`${styles.unitProfit} ${styles.uiElement}`}>{unitProfit} <small>per sec</small></span> : null} </span>
</div> {isShrouded ? null : (
} <span className={`${styles.unitPrice} ${styles.uiElement}`}>
{unitPrice} <small>GELD</small>
</span>
)}
{n_units > 0 ? (
<span className={`${styles.unitSupply} ${styles.uiElement}`}>
{n_units}
</span>
) : null}
{n_units > 0 ? (
<span className={`${styles.unitProfit} ${styles.uiElement}`}>
{unitProfit} <small>per sec</small>
</span>
) : null}
</div>
);
};
const Army = () => { const Army = () => {
const { army, addUnit, isRegistered, player, balance } = usePlayer() const { army, addUnit, isRegistered, player, balance } = usePlayer();
const [canPurchase, setCanPurchase] = useState<Record<UnitType, boolean>>(defaultAvailabilityMap); const [canPurchase, setCanPurchase] = useState<Record<UnitType, boolean>>(
const [isShrouded, setIsShrouded] = useState<Record<UnitType, boolean>>(defaultAvailabilityMap); defaultAvailabilityMap
const [canKnowAbout, setCanKnowAbout] = useState<Record<UnitType, boolean>>(defaultAvailabilityMap); );
const balanceCount = useRef(BigInt(balance ?? 0)) const [isShrouded, setIsShrouded] = useState<Record<UnitType, boolean>>(
defaultAvailabilityMap
);
const [canKnowAbout, setCanKnowAbout] = useState<Record<UnitType, boolean>>(
defaultAvailabilityMap
);
const balanceCount = useRef(BigInt(balance ?? 0));
const setAvailabilities = useCallback(() => { const setAvailabilities = useCallback(() => {
if (isRegistered) { if (isRegistered) {
const totalMinted = (player?.total_minted ?? BigInt(0)) + (balanceCount.current - (balance ?? BigInt(0))); const totalMinted =
(player?.total_minted ?? BigInt(0)) +
(balanceCount.current - (balance ?? BigInt(0)));
const n_units: Record<UnitType, number> = { const n_units: Record<UnitType, number> = {
0: army?.moloch_denier.level ?? 0, 0: army?.moloch_denier.level ?? 0,
1: army?.apprentice.level ?? 0, 1: army?.apprentice.level ?? 0,
2: army?.anointed.level ?? 0, 2: army?.anointed.level ?? 0,
3: army?.champion.level ?? 0, 3: army?.champion.level ?? 0,
} };
const inShroud = { const inShroud = {
0: totalMinted < unitDiscoveredAt[0], 0: totalMinted < unitDiscoveredAt[0],
1: totalMinted < unitDiscoveredAt[1], 1: totalMinted < unitDiscoveredAt[1],
2: totalMinted < unitDiscoveredAt[2], 2: totalMinted < unitDiscoveredAt[2],
3: totalMinted < unitDiscoveredAt[3], 3: totalMinted < unitDiscoveredAt[3],
} };
const isKnown = { const isKnown = {
0: totalMinted >= unitAvailableToDiscoverAt[0], 0: totalMinted >= unitAvailableToDiscoverAt[0],
1: totalMinted >= unitAvailableToDiscoverAt[1], 1: totalMinted >= unitAvailableToDiscoverAt[1],
2: totalMinted >= unitAvailableToDiscoverAt[2], 2: totalMinted >= unitAvailableToDiscoverAt[2],
3: totalMinted >= unitAvailableToDiscoverAt[3], 3: totalMinted >= unitAvailableToDiscoverAt[3],
}
const canActuallyBuy = {
0: balanceCount.current >= calculateUnitPrice(0, n_units[0], 1) && isKnown[0] && !inShroud[0],
1: balanceCount.current >= calculateUnitPrice(1, n_units[1], 1) && isKnown[1] && !inShroud[1],
2: balanceCount.current >= calculateUnitPrice(2, n_units[2], 1) && isKnown[2] && !inShroud[2],
3: balanceCount.current >= calculateUnitPrice(3, n_units[3], 1) && isKnown[3] && !inShroud[3],
}; };
setCanPurchase(canActuallyBuy) const canActuallyBuy = {
setIsShrouded(inShroud) 0:
setCanKnowAbout(isKnown) balanceCount.current >= calculateUnitPrice(0, n_units[0], 1) &&
isKnown[0] &&
!inShroud[0],
1:
balanceCount.current >= calculateUnitPrice(1, n_units[1], 1) &&
isKnown[1] &&
!inShroud[1],
2:
balanceCount.current >= calculateUnitPrice(2, n_units[2], 1) &&
isKnown[2] &&
!inShroud[2],
3:
balanceCount.current >= calculateUnitPrice(3, n_units[3], 1) &&
isKnown[3] &&
!inShroud[3],
};
setCanPurchase(canActuallyBuy);
setIsShrouded(inShroud);
setCanKnowAbout(isKnown);
} else { } else {
setCanPurchase(defaultAvailabilityMap) setCanPurchase(defaultAvailabilityMap);
setIsShrouded(defaultAvailabilityMap) setIsShrouded(defaultAvailabilityMap);
setCanKnowAbout(defaultAvailabilityMap) setCanKnowAbout(defaultAvailabilityMap);
} }
}, [ }, [army, balance, isRegistered, player?.total_minted]);
army,
balance,
isRegistered,
player?.total_minted
])
useEffect(() => { useEffect(() => {
const tickInterval = setInterval(() => { const tickInterval = setInterval(() => {
@ -158,20 +207,63 @@ const Army = () => {
army?.profit_per_second ?? BigInt(0), army?.profit_per_second ?? BigInt(0),
player?.last_raided_at ?? BigInt(0) player?.last_raided_at ?? BigInt(0)
); );
setAvailabilities() setAvailabilities();
}, 100); }, 100);
return () => clearInterval(tickInterval) return () => clearInterval(tickInterval);
}, [balance, army?.profit_per_second, player?.last_raided_at, setAvailabilities]) }, [
balance,
army?.profit_per_second,
player?.last_raided_at,
setAvailabilities,
]);
return <> return (
<div className={`${styles.tavern_keeper} ${styles.person} ${styles.static}`} /> <>
<div className={styles.armyUnits}> <div
{canKnowAbout[0] && <Unit n_units={army?.moloch_denier.level ?? 0} addUnit={addUnit} unitType={0} canPurchase={canPurchase[0]} isShrouded={isShrouded[0]} />} className={`${styles.tavern_keeper} ${styles.person} ${styles.static}`}
{canKnowAbout[1] && <Unit n_units={army?.apprentice.level ?? 0} addUnit={addUnit} unitType={1} canPurchase={canPurchase[1]} isShrouded={isShrouded[1]} />} >
{canKnowAbout[2] && <Unit n_units={army?.anointed.level ?? 0} addUnit={addUnit} unitType={2} canPurchase={canPurchase[2]} isShrouded={isShrouded[2]} />} <PixelatedQuote />
{canKnowAbout[3] && <Unit n_units={army?.champion.level ?? 0} addUnit={addUnit} unitType={3} canPurchase={canPurchase[3]} isShrouded={isShrouded[3]} />} </div>
</div> <div className={styles.armyUnits}>
</> {canKnowAbout[0] && (
} <Unit
n_units={army?.moloch_denier.level ?? 0}
addUnit={addUnit}
unitType={0}
canPurchase={canPurchase[0]}
isShrouded={isShrouded[0]}
/>
)}
{canKnowAbout[1] && (
<Unit
n_units={army?.apprentice.level ?? 0}
addUnit={addUnit}
unitType={1}
canPurchase={canPurchase[1]}
isShrouded={isShrouded[1]}
/>
)}
{canKnowAbout[2] && (
<Unit
n_units={army?.anointed.level ?? 0}
addUnit={addUnit}
unitType={2}
canPurchase={canPurchase[2]}
isShrouded={isShrouded[2]}
/>
)}
{canKnowAbout[3] && (
<Unit
n_units={army?.champion.level ?? 0}
addUnit={addUnit}
unitType={3}
canPurchase={canPurchase[3]}
isShrouded={isShrouded[3]}
/>
)}
</div>
</>
);
};
export default Army export default Army;

View File

@ -0,0 +1,56 @@
import { useState, useEffect, useRef } from "react";
import styles from "../styles/Army.module.css"
const tavernerQuotes = [
"There is always Moloch to be slain here...",
"We prioritize Shipping at All Costs!",
"Get out there RAIDER, Moloch won't Slay Himself!",
];
function PixelatedQuote() {
const [isShown, setIsShown] = useState(true);
const [currentQuote, setCurrentQuote] = useState(
"Welcome to the Dark Forest!"
);
const intervalIdRef = useRef<NodeJS.Timeout | null>(null); // Define the type for Node environment compatibility
useEffect(() => {
if (intervalIdRef.current !== null) {
clearInterval(intervalIdRef.current);
} // Clear interval if it exists
// Set up an interval to show the toast every 10 seconds
intervalIdRef.current = setInterval(() => {
setCurrentQuote(
tavernerQuotes[Math.floor(Math.random() * tavernerQuotes.length)]
);
setIsShown(true);
// Hide the toast after 4 seconds
setTimeout(() => {
setIsShown(false);
}, 4000);
}, 6000);
// Clean up the interval on component unmount
return () => {
if (intervalIdRef.current !== null) {
clearInterval(intervalIdRef.current); // Clear interval using correct reference
intervalIdRef.current = null;
}
};
}, []);
return (
<div>
<div
className={`pixel-borders pixel-borders--2 pixelFont ${styles.pixelQuote}`}
style={{ opacity: isShown ? 1 : 0 /* Control visibility with opacity */ }}
>
{currentQuote}
</div>
</div>
);
}
export default PixelatedQuote;

View File

@ -1,15 +1,19 @@
import '../styles/globals.css'; import "../styles/globals.css";
import '@rainbow-me/rainbowkit/styles.css'; import "../styles/pixelatedBorders.scss";
import type { AppProps } from 'next/app'; import "@rainbow-me/rainbowkit/styles.css";
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import type { AppProps } from "next/app";
import { WagmiProvider } from 'wagmi'; import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { RainbowKitProvider, midnightTheme } from '@rainbow-me/rainbowkit'; import { WagmiProvider } from "wagmi";
import { config } from '../wagmi'; import { RainbowKitProvider, midnightTheme } from "@rainbow-me/rainbowkit";
import { Texturina } from 'next/font/google' import { config } from "../wagmi";
import PlayerProvider from '../providers/PlayerProvider'; import { Press_Start_2P, Texturina } from "next/font/google";
import PlayerProvider from "../providers/PlayerProvider";
const client = new QueryClient(); const client = new QueryClient();
const font = Texturina({ weight: ['400'], subsets: ["latin"] }) const font = Texturina({ weight: ["400"], subsets: ["latin"] });
// Tavern keeper quote
const fontPixel = Press_Start_2P({ weight: ["400"], subsets: ["latin"] });
function MyApp({ Component, pageProps }: AppProps) { function MyApp({ Component, pageProps }: AppProps) {
return ( return (
@ -17,11 +21,24 @@ function MyApp({ Component, pageProps }: AppProps) {
<QueryClientProvider client={client}> <QueryClientProvider client={client}>
<RainbowKitProvider theme={midnightTheme()}> <RainbowKitProvider theme={midnightTheme()}>
<style jsx global>{` <style jsx global>{`
html, body, p, span, a { html,
body,
p,
span,
a {
font-family: ${font.style.fontFamily}; font-family: ${font.style.fontFamily};
} }
h1, h2, h3, h4, h5, h6, .title { h1,
font-family: ${font.style.fontFamily}; h2,
h3,
h4,
h5,
h6,
.title {
font-family: ${font.style.fontFamily};
}
.pixelFont {
font-family: ${fontPixel.style.fontFamily};
} }
`}</style> `}</style>
<PlayerProvider> <PlayerProvider>

View File

@ -199,6 +199,20 @@
bottom: 160px; bottom: 160px;
width: 90px; width: 90px;
height: 90px; height: 90px;
.pixelQuote {
min-width: 150px;
color: black;
font-size: 0.7rem;
position: absolute;
bottom: 5.5rem;
left: -20px;
right: 0;
padding: 0.7rem;
line-height: 0.8rem;
transition: opacity 1s ease-in-out;
box-shadow: 0px 5px 10px 5px rgba(0, 0, 0, 0.4);
}
} }
.static.moloch_denier { .static.moloch_denier {
background-image: url("/roles/scribe2.png"); background-image: url("/roles/scribe2.png");

View File

@ -0,0 +1,3 @@
@import "../../node_modules/pixel-borders/src/styles/pixel-borders.scss";
@import "../../node_modules/pixel-borders/src/styles/pixel-borders/pixel-borders-mixins";