diff --git a/.eslintrc.cjs b/.eslintrc.cjs index d99bec6..a7930b6 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -11,5 +11,6 @@ module.exports = { rules: { 'react-refresh/only-export-components': 'warn', '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-empty-function': 'off', }, } diff --git a/src/assets/button_invisible.png b/src/assets/button_invisible.png new file mode 100644 index 0000000..dcb8945 Binary files /dev/null and b/src/assets/button_invisible.png differ diff --git a/src/assets/button_minus.png b/src/assets/button_minus.png new file mode 100644 index 0000000..187c6a6 Binary files /dev/null and b/src/assets/button_minus.png differ diff --git a/src/assets/button_plus.png b/src/assets/button_plus.png new file mode 100644 index 0000000..d89a5d1 Binary files /dev/null and b/src/assets/button_plus.png differ diff --git a/src/assets/button_reset.png b/src/assets/button_reset.png new file mode 100644 index 0000000..302e5e9 Binary files /dev/null and b/src/assets/button_reset.png differ diff --git a/src/assets/button_visible.png b/src/assets/button_visible.png new file mode 100644 index 0000000..ae5752a Binary files /dev/null and b/src/assets/button_visible.png differ diff --git a/src/assets/entropy_cool.png b/src/assets/entropy_cool.png new file mode 100644 index 0000000..79cad52 Binary files /dev/null and b/src/assets/entropy_cool.png differ diff --git a/src/assets/entropy_heat.png b/src/assets/entropy_heat.png new file mode 100644 index 0000000..b90ddfd Binary files /dev/null and b/src/assets/entropy_heat.png differ diff --git a/src/assets/inscriber_arrows_bg_light.png b/src/assets/inscriber_arrows_bg_light.png new file mode 100644 index 0000000..c50b208 Binary files /dev/null and b/src/assets/inscriber_arrows_bg_light.png differ diff --git a/src/assets/large_slot_light.png b/src/assets/large_slot_light.png new file mode 100644 index 0000000..fe17a92 Binary files /dev/null and b/src/assets/large_slot_light.png differ diff --git a/src/assets/recipe_arrow_filled_light.png b/src/assets/recipe_arrow_filled_light.png new file mode 100644 index 0000000..e45d15f Binary files /dev/null and b/src/assets/recipe_arrow_filled_light.png differ diff --git a/src/assets/recipe_arrow_light.png b/src/assets/recipe_arrow_light.png new file mode 100644 index 0000000..b56bf6f Binary files /dev/null and b/src/assets/recipe_arrow_light.png differ diff --git a/src/assets/slot.png b/src/assets/slot.png index 218df6e..bdf05e3 100644 Binary files a/src/assets/slot.png and b/src/assets/slot.png differ diff --git a/src/assets/slot_cross.png b/src/assets/slot_cross.png new file mode 100644 index 0000000..93c3bdb Binary files /dev/null and b/src/assets/slot_cross.png differ diff --git a/src/components/model-viewer/ModelViewerInternal.module.css b/src/components/model-viewer/ModelViewerInternal.module.css index d692eed..f0b8454 100644 --- a/src/components/model-viewer/ModelViewerInternal.module.css +++ b/src/components/model-viewer/ModelViewerInternal.module.css @@ -1,31 +1,66 @@ -.root { - position: relative; +.wrapper { + display: inline-flex; + flex-direction: row; + --modelviewer-width: auto; --modelviewer-height: auto; --modelviewer-aspect-ratio: auto; - width: var(--modelviewer-width); - height: var(--modelviewer-height); - background: rgb(0 0 0 / 20%); + background-color: var(--color--scene-background); border-radius: 16px; } +.loading .controls { + display: none; +} + +.controls { + flex: 0 1 auto; + display: flex; + flex-direction: column; +} + +.controls button { + appearance: none; + border: none; + background: none; +} + +.controls button img { + width: 32px; + height: 32px; + opacity: 0.8; +} + +.controls button:hover img { + opacity: 1; +} + +.controls button:active img { + opacity: 0.6; +} + +.controls button:disabled img { + opacity: 0.2; +} + +.root { + position: relative; +} + .root > img { position: absolute; - width: var(--modelviewer-width); - height: var(--modelviewer-height); } .viewport { display: inline-block; - width: var(--modelviewer-width); - height: var(--modelviewer-height); } .root, .root > img, .root > .viewport { - width: calc(min(100%, var(--modelviewer-width))) !important; + width: var(--modelviewer-width); height: auto !important; aspect-ratio: var(--modelviewer-aspect-ratio); overflow: hidden; } + diff --git a/src/components/model-viewer/ModelViewerInternal.tsx b/src/components/model-viewer/ModelViewerInternal.tsx index 3271eb0..4e86f1d 100644 --- a/src/components/model-viewer/ModelViewerInternal.tsx +++ b/src/components/model-viewer/ModelViewerInternal.tsx @@ -29,6 +29,9 @@ import { buildInWorldAnnotation } from "./buildInWorldAnnotation.ts"; import addLevelLighting from "./addSceneLighting.ts"; import buildOverlayAnnotation from "./buildOverlayAnnotations.ts"; import TextureManager from "./TextureManager.ts"; +import plusIcon from "../../assets/button_plus.png"; +import minusIcon from "../../assets/button_minus.png"; +import resetIcon from "../../assets/button_reset.png"; const DEBUG = false; @@ -41,6 +44,23 @@ declare module "csstype" { } } +interface ControlInterface { + zoomIn(): void; + + zoomOut(): void; + + resetView(): void; + + dispose(): void; +} + +const DummyControlInterface: ControlInterface = { + dispose(): void {}, + resetView(): void {}, + zoomIn(): void {}, + zoomOut(): void {}, +}; + export type Annotation = OverlayAnnotation | InWorldAnnotation; export type OverlayAnnotation = { @@ -118,7 +138,7 @@ async function initialize( setTooltipObject: (object: ReactNode | undefined) => void, abortSignal: AbortSignal, originalWidth: number -) { +): Promise { const renderer = new THREE.WebGLRenderer({ alpha: true, premultipliedAlpha: false, @@ -213,6 +233,7 @@ async function initialize( let controls: OrbitControls | undefined; if (cameraControls) { controls = new OrbitControls(camera, viewportEl); + controls.enableZoom = false; controls.update(); } else { camera.lookAt(new Vector3()); @@ -232,6 +253,7 @@ async function initialize( if (cameraControls) { controls?.dispose(); controls = new OrbitControls(camera, viewportEl); + controls.enableZoom = false; controls.update(); } else { camera.lookAt(new Vector3()); @@ -273,18 +295,53 @@ async function initialize( console.error("Failed to render %s", source, e); } - return () => { - if (!disposed) { - console.debug("Disposing model viewer for %s", source); - disposed = true; - if (resizeObserver) { - resizeObserver.disconnect(); + return { + dispose(): void { + if (!disposed) { + console.debug("Disposing model viewer for %s", source); + disposed = true; + if (resizeObserver) { + resizeObserver.disconnect(); + } + viewportEl.removeChild(renderer.domElement); + renderer.dispose(); + controls?.dispose(); + setTooltipObject(undefined); } - viewportEl.removeChild(renderer.domElement); - renderer.dispose(); - controls?.dispose(); - setTooltipObject(undefined); - } + }, + resetView(): void { + controls?.reset(); + }, + zoomIn(): void { + if (controls) { + controls.enableZoom = true; + try { + for (let i = 0; i < 5; i++) { + const e = new WheelEvent("wheel", { + deltaY: -120, + }); + controls.domElement.dispatchEvent(e); + } + } finally { + controls.enableZoom = false; + } + } + }, + zoomOut(): void { + if (controls) { + controls.enableZoom = true; + try { + for (let i = 0; i < 5; i++) { + const e = new WheelEvent("wheel", { + deltaY: 120, + }); + controls.domElement.dispatchEvent(e); + } + } finally { + controls.enableZoom = false; + } + } + }, }; } @@ -293,7 +350,7 @@ function ModelViewerInternal({ src, placeholder, interactive = false, - background = "transparent", + background, width, height, inWorldAnnotations, @@ -304,6 +361,7 @@ function ModelViewerInternal({ const [tooltipObject, setTooltipObject] = useState(); const mousePos = useRef(null); const viewportRef = useRef(null); + const controlRef = useRef(DummyControlInterface); useLayoutEffect(() => { const viewportEl = viewportRef.current; if (!viewportEl) { @@ -327,12 +385,13 @@ function ModelViewerInternal({ abortController.signal, width ) - .then((f) => { + .then((control) => { if (disposed) { - f(); + control.dispose(); } else { setInitialized(true); - disposer = f; + controlRef.current = control; + disposer = () => control.dispose(); } }) .catch((err) => { @@ -364,6 +423,21 @@ function ModelViewerInternal({ width, ]); + function zoomIn(e: React.MouseEvent) { + e.preventDefault(); + controlRef?.current?.zoomIn(); + } + + function zoomOut(e: React.MouseEvent) { + e.preventDefault(); + controlRef?.current?.zoomOut(); + } + + function resetView(e: React.MouseEvent) { + e.preventDefault(); + controlRef?.current?.resetView(); + } + function onMouseMove(e: React.MouseEvent) { const canvas = viewportRef.current?.querySelector("canvas"); if (!canvas) { @@ -386,7 +460,15 @@ function ModelViewerInternal({ } return ( - <> +
{error && {String(error)}}
@@ -408,7 +484,26 @@ function ModelViewerInternal({
- + {interactive && ( +
+ + + + + + + + + +
+ )} +
); } diff --git a/src/components/recipes/ChargerRecipe.tsx b/src/components/recipes/ChargerRecipe.tsx new file mode 100644 index 0000000..a839b54 --- /dev/null +++ b/src/components/recipes/ChargerRecipe.tsx @@ -0,0 +1,30 @@ +import css from "./recipe.module.css"; +import RecipeIngredient from "./RecipeIngredient"; +import RecipeArrow from "./RecipeArrow"; +import { ChargerRecipeInfo, useGuide } from "../../data/Guide.ts"; +import MinecraftFrame from "../MinecraftFrame.tsx"; +import ItemIcon from "../ItemIcon.tsx"; + +export interface ChargerRecipeProps { + recipe: ChargerRecipeInfo; +} + +function ChargerRecipe({ recipe }: ChargerRecipeProps) { + const guide = useGuide(); + const resultItem = guide.getItemInfo(recipe.resultItem); + + return ( + +
+
+ Charger: {resultItem.displayName} +
+ + + +
+
+ ); +} + +export default ChargerRecipe; diff --git a/src/components/recipes/CraftingRecipe.tsx b/src/components/recipes/CraftingRecipe.tsx index ef26978..1a5b620 100644 --- a/src/components/recipes/CraftingRecipe.tsx +++ b/src/components/recipes/CraftingRecipe.tsx @@ -4,6 +4,7 @@ import RecipeIngredient from "./RecipeIngredient"; import RecipeArrow from "./RecipeArrow"; import { CraftingRecipeInfo, useGuide } from "../../data/Guide.ts"; import MinecraftFrame from "../MinecraftFrame.tsx"; +import ItemIcon from "../ItemIcon.tsx"; export interface CraftingRecipeProps { recipe: CraftingRecipeInfo; @@ -17,7 +18,9 @@ function CraftingRecipe({ recipe }: CraftingRecipeProps) {
- Crafting {recipe.shapeless ? " (Shapeless)" : null} + + Crafting + {recipe.shapeless ? " (Shapeless)" : null}
diff --git a/src/components/recipes/InscriberRecipe.tsx b/src/components/recipes/InscriberRecipe.tsx index d52e56e..a63a5b7 100644 --- a/src/components/recipes/InscriberRecipe.tsx +++ b/src/components/recipes/InscriberRecipe.tsx @@ -3,6 +3,7 @@ import RecipeIngredient from "./RecipeIngredient"; import RecipeArrow from "./RecipeArrow"; import { InscriberRecipeInfo, useGuide } from "../../data/Guide.ts"; import MinecraftFrame from "../MinecraftFrame.tsx"; +import ItemIcon from "../ItemIcon.tsx"; export interface InscriberRecipeProps { recipe: InscriberRecipeInfo; @@ -15,7 +16,9 @@ function InscriberRecipe({ recipe }: InscriberRecipeProps) { return (
-
{resultItem.displayName}
+
+ Inscriber: {resultItem.displayName} +
diff --git a/src/components/recipes/Recipe.tsx b/src/components/recipes/Recipe.tsx index a2feb85..0f5b1b3 100644 --- a/src/components/recipes/Recipe.tsx +++ b/src/components/recipes/Recipe.tsx @@ -2,39 +2,58 @@ import CraftingRecipe from "./CraftingRecipe"; import InscriberRecipe from "./InscriberRecipe"; import SmeltingRecipe from "./SmeltingRecipe"; import css from "./recipe.module.css"; -import { useGuide } from "../../data/Guide.ts"; +import { RecipeType, TaggedRecipe, useGuide } from "../../data/Guide.ts"; import ErrorText from "../ErrorText.tsx"; +import React, { JSXElementConstructor } from "react"; +import TransformRecipe from "./TransformRecipe.tsx"; +import ChargerRecipe from "./ChargerRecipe.tsx"; +import SmithingRecipe from "./SmithingRecipe.tsx"; -export interface RecipeProps { - /** - * Recipe ID - */ - id: string; -} +export type RecipeProps = + | { + /** + * Recipe ID + */ + id: string; + recipe?: never; + } + | { id?: never; recipe: TaggedRecipe }; -function Recipe({ id }: RecipeProps) { - const guide = useGuide(); - id = guide.resolveId(id); +const RecipeTypeMap: Record> = { + [RecipeType.CraftingRecipeType]: CraftingRecipe, + [RecipeType.SmeltingRecipeType]: SmeltingRecipe, + // [RecipeType.StonecuttingRecipeType]: StonecuttingRecipe, + [RecipeType.SmithingRecipeType]: SmithingRecipe, + [RecipeType.TransformRecipeType]: TransformRecipe, + [RecipeType.InscriberRecipeType]: InscriberRecipe, + [RecipeType.ChargerRecipeType]: ChargerRecipe, + // [RecipeType.EntropyRecipeType]: EntropyRecipe, + // [RecipeType.MatterCannonAmmoType]: MatterCannonAmmo, +}; - const recipe = guide.getRecipeById(id); +function Recipe(props: RecipeProps) { + const guide = useGuide(); + let recipe: TaggedRecipe | undefined; + if (typeof props.recipe !== "undefined") { + recipe = props.recipe; + } else { + recipe = guide.getRecipeById(props.id); + } if (!recipe) { - return Missing recipe {id}; + return Missing recipe {props.id}; } - return ( -
- {recipe.type === "crafting" && ( - - )} - {recipe.type === "inscriber" && ( - - )} - {recipe.type === "smelting" && ( - - )} -
- ); + const componentType = RecipeTypeMap[recipe.type]; + if (!componentType) { + return Unknown recipe type {recipe.type}; + } + + const recipeEl = React.createElement(componentType, { + recipe, + }); + + return
{recipeEl}
; } export default Recipe; diff --git a/src/components/recipes/RecipeFor.tsx b/src/components/recipes/RecipeFor.tsx index 4a9aa76..7b6211f 100644 --- a/src/components/recipes/RecipeFor.tsx +++ b/src/components/recipes/RecipeFor.tsx @@ -1,9 +1,7 @@ -import CraftingRecipe from "./CraftingRecipe"; -import InscriberRecipe from "./InscriberRecipe"; -import SmeltingRecipe from "./SmeltingRecipe"; import css from "./recipe.module.css"; import { useGuide } from "../../data/Guide.ts"; import ErrorText from "../ErrorText.tsx"; +import Recipe from "./Recipe.tsx"; export interface RecipeForProps { /** @@ -16,34 +14,16 @@ function RecipeFor({ id }: RecipeForProps) { const guide = useGuide(); id = guide.resolveId(id); - const crafting = Object.values(guide.craftingRecipes).filter( - (recipe) => recipe.resultItem === id - ); - const smelting = Object.values(guide.smeltingRecipes).filter( - (recipe) => recipe.resultItem === id - ); - const inscriber = Object.values(guide.inscriberRecipes).filter( - (recipe) => recipe.resultItem === id - ); + const recipes = guide.getRecipesForItem(id); - if ( - crafting.length === 0 && - smelting.length === 0 && - inscriber.length === 0 - ) { + if (recipes.length == 0) { return No recipes for {id}; } return (
- {crafting.map((recipe) => ( - - ))} - {inscriber.map((recipe) => ( - - ))} - {smelting.map((recipe) => ( - + {recipes.map((recipe) => ( + ))}
); diff --git a/src/components/recipes/RecipeIngredientGrid.tsx b/src/components/recipes/RecipeIngredientGrid.tsx index 15f3000..09bc28d 100644 --- a/src/components/recipes/RecipeIngredientGrid.tsx +++ b/src/components/recipes/RecipeIngredientGrid.tsx @@ -1,21 +1,25 @@ import css from "./recipe.module.css"; import RecipeIngredient from "./RecipeIngredient"; -export interface RecipeIngredientsProps { - ingredients: string[][]; - shapeless: boolean; - width: number; - height: number; -} +export type RecipeIngredientsProps = + | { + ingredients: string[][]; + shapeless: true; + } + | { + ingredients: string[][]; + shapeless: false; + width: number; + height: number; + }; function RecipeIngredientGrid({ ingredients, - shapeless, - width, + ...props }: RecipeIngredientsProps) { // Shapeless recipes do not show empty cells let className = css.ingredientsBox; - if (shapeless) { + if (props.shapeless) { ingredients = ingredients.filter((i) => i.length); if (ingredients.length <= 1) { @@ -26,6 +30,8 @@ function RecipeIngredientGrid({ className = css.ingredientsBoxShapeless3Col; } } else { + const { width } = props; + // Pad out the ingredient grid to 3x3 for shaped recipes const sparseIngredients: string[][] = [[], [], [], [], [], [], [], [], []]; for (let i = 0; i < ingredients.length; i++) { diff --git a/src/components/recipes/SmeltingRecipe.tsx b/src/components/recipes/SmeltingRecipe.tsx index 741cd4b..90e84fa 100644 --- a/src/components/recipes/SmeltingRecipe.tsx +++ b/src/components/recipes/SmeltingRecipe.tsx @@ -4,6 +4,7 @@ import smelt from "./smelt.png"; import RecipeArrow from "./RecipeArrow"; import { SmeltingRecipeInfo, useGuide } from "../../data/Guide.ts"; import MinecraftFrame from "../MinecraftFrame.tsx"; +import ItemIcon from "../ItemIcon.tsx"; export interface SmeltingRecipeProps { recipe: SmeltingRecipeInfo; @@ -16,7 +17,10 @@ function SmeltingRecipe({ recipe }: SmeltingRecipeProps) { return (
-
Smelting - {resultItem.displayName}
+
+ Smelting:{" "} + {resultItem.displayName} +
diff --git a/src/components/recipes/SmithingRecipe.tsx b/src/components/recipes/SmithingRecipe.tsx new file mode 100644 index 0000000..9d78a22 --- /dev/null +++ b/src/components/recipes/SmithingRecipe.tsx @@ -0,0 +1,31 @@ +import css from "./recipe.module.css"; +import RecipeIngredient from "./RecipeIngredient"; +import RecipeArrow from "./RecipeArrow"; +import { SmithingRecipeInfo } from "../../data/Guide.ts"; +import MinecraftFrame from "../MinecraftFrame.tsx"; +import ItemIcon from "../ItemIcon.tsx"; + +export interface SmithingRecipeProps { + recipe: SmithingRecipeInfo; +} + +function SmithingRecipe({ recipe }: SmithingRecipeProps) { + return ( + +
+
+ Smithing +
+
+ + + +
+ + +
+
+ ); +} + +export default SmithingRecipe; diff --git a/src/components/recipes/TransformRecipe.tsx b/src/components/recipes/TransformRecipe.tsx new file mode 100644 index 0000000..416cd2f --- /dev/null +++ b/src/components/recipes/TransformRecipe.tsx @@ -0,0 +1,82 @@ +import RecipeIngredientGrid from "./RecipeIngredientGrid"; +import css from "./recipe.module.css"; +import RecipeIngredient from "./RecipeIngredient"; +import RecipeArrow from "./RecipeArrow"; +import { TransformRecipeInfo, useGuide } from "../../data/Guide.ts"; +import MinecraftFrame from "../MinecraftFrame.tsx"; +import ItemIcon from "../ItemIcon.tsx"; +import ErrorText from "../ErrorText.tsx"; +import { useEffect, useState } from "react"; + +export interface TransformRecipeProps { + recipe: TransformRecipeInfo; +} + +/** + * Cycles through the fluids that this recipe can be done in. + */ +function FluidTransformCircumstance({ fluids }: { fluids: string[] }) { + const guide = useGuide(); + const [currentIndex, setCurrentIndex] = useState(0); + + useEffect(() => { + let interval: number | undefined; + if (fluids.length > 1) { + interval = window.setInterval( + () => setCurrentIndex((idx) => (idx + 1) % fluids.length), + 1000 + ); + } + + return () => { + if (interval !== undefined) { + window.clearInterval(interval); + } + setCurrentIndex(0); + }; + }, [fluids]); + + if (fluids.length == 0) { + return No fluids in transform recipe; + } + + const fluidInfo = guide.getFluidInfo(fluids[currentIndex % fluids.length]); + + return ( + <> + + {" Throw in " + fluidInfo.displayName} + + ); +} + +function TransformRecipe({ recipe }: TransformRecipeProps) { + return ( + +
+
+ {recipe.circumstance.type === "explosion" && ( + <> + + Explode + + )} + {recipe.circumstance.type === "fluid" && ( + + )} +
+ + + +
+
+ ); +} + +export default TransformRecipe; diff --git a/src/components/recipes/recipe.module.css b/src/components/recipes/recipe.module.css index a822269..3f82f3e 100644 --- a/src/components/recipes/recipe.module.css +++ b/src/components/recipes/recipe.module.css @@ -1,110 +1,124 @@ - /** * Try to be responsive on mobile */ @media screen and (max-width: 1023px) { - .recipeContainer { - max-width: 100vw; - } - - .recipeContainer > div + div { - margin-top: 1rem; - } - - .recipeContainer + .recipeContainer { - margin-top: 1rem; - } - - .recipeContainer > :first-child { - display: flex; - justify-content: center; - } - - .recipeContainer .recipeArrow { - width: 9vw; - } + .recipeContainer { + max-width: 100vw; + } + + .recipeContainer > div + div { + margin-top: 1rem; + } + + .recipeContainer + .recipeContainer { + margin-top: 1rem; + } + + .recipeContainer > :first-child { + display: flex; + justify-content: center; + } + + .recipeContainer .recipeArrow { + width: 9vw; + } } .recipeBoxLayout { - display: inline-grid; - grid-template-rows: auto auto; - grid-template-columns: max-content 1fr max-content; - row-gap: 10px; - column-gap: 10px; - align-items: center; + display: inline-grid; + grid-template-rows: auto auto; + grid-template-columns: max-content 1fr max-content; + row-gap: 10px; + column-gap: 10px; + align-items: center; + padding: 2px; } /* This is the title row */ .recipeBoxLayout > :first-child { - grid-column-start: span 3; - color: #333333; + grid-column-start: span 3; + color: #333333; + display: flex; + flex-direction: row; + align-items: center; + gap: 3px; +} + +.recipeBoxLayout > :first-child :global(.item-icon), .recipeBoxLayout > :first-child :global(.fluid-icon) { + width: calc(8px * var(--gui-scale)); + height: calc(8px * var(--gui-scale)); +} + +.fluidIcon { + width: calc(8px * var(--gui-scale)); + height: calc(8px * var(--gui-scale)); } .ingredientsBox, .ingredientsBoxShapeless1Col, .ingredientsBoxShapeless2Col, .ingredientsBoxShapeless3Col { - display: inline-grid; - align-self: center; - justify-self: start; + display: inline-grid; + align-self: center; + justify-self: start; } .ingredientsBox { - grid-template-rows: 1fr 1fr 1fr; - grid-template-columns: 1fr 1fr 1fr; + grid-template-rows: 1fr 1fr 1fr; + grid-template-columns: 1fr 1fr 1fr; } .ingredientsBoxShapeless1Col { - grid-auto-rows: 1fr; - grid-template-columns: 1fr; + grid-auto-rows: 1fr; + grid-template-columns: 1fr; } .ingredientsBoxShapeless2Col { - grid-auto-rows: 1fr; - grid-template-columns: 1fr 1fr; + grid-auto-rows: 1fr; + grid-template-columns: 1fr 1fr; } .ingredientsBoxShapeless3Col { - grid-auto-rows: 1fr; - grid-template-columns: 1fr 1fr 1fr; + grid-auto-rows: 1fr; + grid-template-columns: 1fr 1fr 1fr; } .recipeArrow { - align-self: center; - justify-self: center; - width: 85px; - height: 50px; + align-self: center; + justify-self: center; + width: 85px; + height: 50px; } .emptyIngredientBox, .ingredientBox { - display: inline-block; - width: calc(18px * var(--gui-scale)); - height: calc(18px * var(--gui-scale)); - box-sizing: border-box; - - border: calc(1px * var(--gui-scale)) solid; - image-rendering: pixelated; - border-image-source: url("../../assets/slot.png"); - border-image-slice: 1 fill; + display: inline-block; + width: calc(18px * var(--gui-scale)); + height: calc(18px * var(--gui-scale)); + box-sizing: border-box; + + border: calc(1px * var(--gui-scale)) solid; + image-rendering: pixelated; + border-image-source: url("../../assets/slot.png"); + border-image-slice: 1 fill; } .ingredientBox:hover { - background-color: #aaaaaa; + background-color: #aaaaaa; } .smeltingInputBox { - display: flex; - flex-direction: column; + display: flex; + flex-direction: column; } .smeltingInputBox > :first-child { - margin-bottom: 5px; - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; + margin-bottom: 5px; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; } .smeltingInputBox > :last-child { - border-top-left-radius: 0; - border-top-right-radius: 0; + border-top-left-radius: 0; + border-top-right-radius: 0; } \ No newline at end of file diff --git a/src/data/Guide.ts b/src/data/Guide.ts index e7b706a..dbb9515 100644 --- a/src/data/Guide.ts +++ b/src/data/Guide.ts @@ -1,36 +1,97 @@ import { createContext, useContext } from "react"; import { Root as MdAstRoot } from "mdast"; -export interface RecipeInfo { +export enum RecipeType { + CraftingRecipeType = "minecraft:crafting", + SmeltingRecipeType = "minecraft:smelting", + StonecuttingRecipeType = "minecraft:stonecutting", + SmithingRecipeType = "minecraft:smithing", + TransformRecipeType = "ae2:transform", + InscriberRecipeType = "ae2:inscriber", + ChargerRecipeType = "ae2:charger", + EntropyRecipeType = "ae2:entropy", + MatterCannonAmmoType = "ae2:matter_cannon", +} + +export interface RecipeInfo { + type: T; id: string; resultItem: string; resultCount: number; } -export interface CraftingRecipeInfo extends RecipeInfo { +export interface CraftingRecipeInfo + extends RecipeInfo { shapeless: boolean; ingredients: string[][]; width: number; height: number; } -export interface SmeltingRecipeInfo extends RecipeInfo { +export interface SmeltingRecipeInfo + extends RecipeInfo { + ingredient: string[]; +} + +export interface SmithingRecipeInfo + extends RecipeInfo { + base: string[]; + addition: string[]; + template: string[]; +} + +export interface StonecuttingRecipeInfo + extends RecipeInfo { ingredient: string[]; } -export interface InscriberRecipeInfo extends RecipeInfo { +export interface InscriberRecipeInfo + extends RecipeInfo { top: string[]; middle: string[]; bottom: string[]; consumesTopAndBottom: boolean; } +export type TransformCircumstanceInfo = + | { type: "explosion" } + | TransformInFluidCircumstanceInfo; + +export interface TransformInFluidCircumstanceInfo { + type: "fluid"; + // IDs of fluids + fluids: string[]; +} + +export interface TransformRecipeInfo + extends RecipeInfo { + ingredients: string[][]; + resultItem: string; + circumstance: TransformCircumstanceInfo; +} + +export interface EntropyRecipeInfo + extends RecipeInfo {} + +export interface ChargerRecipeInfo + extends RecipeInfo { + ingredient: string[]; +} + +export interface MatterCannonAmmoInfo + extends RecipeInfo {} + export type TaggedRecipe = - | ({ type: "crafting" } & CraftingRecipeInfo) - | ({ type: "smelting" } & SmeltingRecipeInfo) - | ({ type: "inscriber" } & InscriberRecipeInfo); + | CraftingRecipeInfo + | SmithingRecipeInfo + | SmeltingRecipeInfo + | StonecuttingRecipeInfo + | InscriberRecipeInfo + | TransformRecipeInfo + | EntropyRecipeInfo + | ChargerRecipeInfo + | MatterCannonAmmoInfo; -const validRarity = ["common", "uncommon", "rare", "epic"]; export type Rarity = "common" | "uncommon" | "rare" | "epic"; export interface ItemInfo { @@ -40,6 +101,12 @@ export interface ItemInfo { icon: string; } +export interface FluidInfo { + id: string; + displayName: string; + icon: string; +} + export interface P2PTypeInfo { tunnelItemId: string; @@ -53,6 +120,20 @@ export type ExportedPage = { frontmatter: Record; }; +export type AnimationInfo = { + frameWidth: number; + frameHeight: number; + length: number; + frameCount: number; + keyFrames: AnimationFrame[]; +}; + +export type AnimationFrame = { + index: number; + frameX: number; + frameY: number; +}; + export type DyeColor = | "yellow" | "blue" @@ -76,15 +157,16 @@ export type GuideIndex = { pages: Record; navigationRootNodes: NavigationNode[]; - items: ItemInfo[]; + items: Record; + fluids: Record; - craftingRecipes: Record; - smeltingRecipes: Record; - inscriberRecipes: Record; + recipes: Record; coloredVersions: Record>; pageIndices: Record>; + + animations: Record; }; export type NavigationNode = { @@ -102,12 +184,13 @@ export type CategoryIndex = Map; export class Guide { readonly baseUrl: string; - private readonly indexByItemId: Map; readonly coloredVersionItemIds = new Set(); readonly pageByItemIndex: ItemIndex; + private readonly recipesForItems = new Map(); + constructor( baseUrl: string, readonly gameVersion: string, @@ -117,9 +200,21 @@ export class Guide { this.index = index; this.baseUrl = baseUrl.replace(/\/+$/, ""); - /** - * Find all item ids of items that are just colored versions of something else. - */ + // Index recipes by item. + for (const [key, recipe] of Object.entries(index.recipes)) { + recipe.id = key; // ID is not part of the actual recipe object to save space + + if (recipe.resultItem) { + const list = this.recipesForItems.get(recipe.resultItem); + if (list) { + list.push(recipe); + } else { + this.recipesForItems.set(recipe.resultItem, [recipe]); + } + } + } + + // Find all item ids of items that are just colored versions of something else. for (const [, variants] of Object.entries(index.coloredVersions)) { for (const coloredItemId of Object.values(variants)) { this.coloredVersionItemIds.add(coloredItemId); @@ -135,23 +230,6 @@ export class Guide { } })() ); - - this.indexByItemId = new Map( - Object.values(index.items).map((item) => { - const icon = item.icon - .replaceAll("\\", "/") - .replaceAll(/^icons\//g, "/icons/"); - if (!validRarity.includes(item.rarity)) { - throw new Error(`Invalid rarity: ${item.rarity}`); - } - const info: ItemInfo = { - ...item, - rarity: item.rarity as Rarity, - icon, - }; - return [item.id, info]; - }) - ); } getPageUrlForItem(id: string): string | undefined { @@ -171,18 +249,6 @@ export class Guide { return this.index.defaultNamespace; } - get craftingRecipes(): Record { - return this.index.craftingRecipes; - } - - get smeltingRecipes(): Record { - return this.index.smeltingRecipes; - } - - get inscriberRecipes(): Record { - return this.index.inscriberRecipes; - } - /** * Resolves a potentially namespace-less id to an id that is guaranteed to have a namespace. * @param idText @@ -229,33 +295,31 @@ export class Guide { return entry; } + getFluidInfo(fluidId: string): FluidInfo { + const entry = this.tryGetFluidInfo(fluidId); + if (!entry) { + throw new Error(`Missing fluid-info for '${fluidId}'.`); + } + return entry; + } + tryGetItemInfo(itemId: string): ItemInfo | undefined { itemId = this.resolveId(itemId); - return this.indexByItemId.get(itemId); + return this.index.items[itemId]; } - getRecipeById(id: string): TaggedRecipe | undefined { - return ( - Guide.getRecipeTagged("crafting", this.craftingRecipes, id) ?? - Guide.getRecipeTagged("smelting", this.smeltingRecipes, id) ?? - Guide.getRecipeTagged("inscriber", this.inscriberRecipes, id) - ); + tryGetFluidInfo(fluidId: string): FluidInfo | undefined { + fluidId = this.resolveId(fluidId); + return this.index.fluids[fluidId]; } - private static getRecipeTagged( - type: T, - recipes: Record, - id: string - ): ({ type: T } & U) | undefined { - const recipe = recipes[id]; - if (recipe) { - return { - type, - ...recipe, - }; - } else { - return undefined; - } + getRecipeById(recipeId: string): TaggedRecipe | undefined { + recipeId = this.resolveId(recipeId); + return this.index.recipes[recipeId]; + } + + getRecipesForItem(item: string): TaggedRecipe[] { + return this.recipesForItems.get(item) ?? []; } pageExists(pageId: string): boolean { diff --git a/src/index.css b/src/index.css index ab87af0..1ed3f40 100644 --- a/src/index.css +++ b/src/index.css @@ -63,7 +63,7 @@ hr { --color--icon-button-disabled: rgb(64, 64, 64); --color--icon-button-hover: rgb(0, 213, 255); --color--in-world-block-highlight: #999999cc; - --color--scene-background: rgba(0, 0, 0, 0.08); + --color--scene-background: rgba(0, 0, 0, 0.2); /* Global UI scale mimicking the Minecraft scaling */ --gui-scale: 3; @@ -166,3 +166,19 @@ a:hover { width: calc(16px * var(--gui-scale)); height: calc(16px * var(--gui-scale)); } + +table { + border-collapse: collapse; +} +table td, table th { + padding: 6px; +} +th { + border-bottom: calc(1px * var(--gui-scale)) solid var(--color--table-border); +} +tr + tr td { + border-top: 1px solid var(--color--table-border); +} +td + td, th + th { + border-left: 1px solid var(--color--table-border); +}