aboutsummaryrefslogtreecommitdiff
path: root/web
diff options
context:
space:
mode:
Diffstat (limited to 'web')
-rw-r--r--web/src/components/NavBar.astro16
-rw-r--r--web/src/components/NavLink.astro11
-rw-r--r--web/src/layouts/Content.astro63
-rw-r--r--web/src/pages/docs/texture-pack-format.astro15
-rw-r--r--web/src/pages/docs/texture-pack-format.md583
-rw-r--r--web/src/pages/index.astro5
-rw-r--r--web/src/pages/texture-packs.astro10
7 files changed, 695 insertions, 8 deletions
diff --git a/web/src/components/NavBar.astro b/web/src/components/NavBar.astro
index 5cecbb4..d98094f 100644
--- a/web/src/components/NavBar.astro
+++ b/web/src/components/NavBar.astro
@@ -1,4 +1,5 @@
---
+import NavLink from "./NavLink.astro";
export type Props = {
navStyle?: 'transparent' | 'full'
}
@@ -7,20 +8,19 @@ const navbar = Astro.props.navStyle ?? 'full';
---
<nav
- class={`max-w-screen-xl bg-gray-800 flex flex-wrap items-center justify-between mx-auto px-2 py-1 leading-tight ${navbar == 'transparent' ? 'bg-opacity-50 backdrop-blur-lg absolute z-10 w-full top-0' : ''}`}>
+ class={`bg-gray-800 flex flex-wrap items-center justify-between mx-auto px-2 py-2 leading-tight w-full ${navbar == 'transparent' ? 'bg-opacity-50 backdrop-blur-lg absolute z-10 top-0' : 'border-b-white border-b'}`}>
<a href="/" class="flex items-center space-x-3">
<!--<img src=""/> TODO: add logo -->
<span class="self-center font-semibold whitespace-nowrap">Firmament</span>
</a>
<div class="block w-auto">
<ul class="font-medium flex-row mt-0 flex border-0">
- <li>
- <a class="px-1"
- href="https://github.com/nea89o/Firmament/blob/master/docs/Texture%20Pack%20Format.md">Docs</a>
- </li>
- <li>
- <a class="px-1" href="/texture-packs">Texture Packs</a>
- </li>
+ <NavLink link="/docs/texture-pack-format">
+ Docs
+ </NavLink>
+ <NavLink link="/texture-packs">
+ Texture Packs
+ </NavLink>
</ul>
</div>
</nav>
diff --git a/web/src/components/NavLink.astro b/web/src/components/NavLink.astro
new file mode 100644
index 0000000..dce002b
--- /dev/null
+++ b/web/src/components/NavLink.astro
@@ -0,0 +1,11 @@
+---
+type Props = {
+ link: string
+};
+---
+
+<li class="px-0.5">
+ <a class="px-1 text-blue-500 hover:text-blue-400 transition ease-in-out duration-300" href={Astro.props.link}>
+ <slot></slot>
+ </a>
+</li>
diff --git a/web/src/layouts/Content.astro b/web/src/layouts/Content.astro
new file mode 100644
index 0000000..51ad0b8
--- /dev/null
+++ b/web/src/layouts/Content.astro
@@ -0,0 +1,63 @@
+---
+---
+
+<div class="w-3/5 mx-auto px-2 firm-content my-5">
+ <slot></slot>
+</div>
+<style is:global>
+ .firm-content {
+ min-width: 70%;
+ width: 60em;
+ max-width: 100%;
+ line-height: 1.4;
+ }
+
+ .firm-content * {
+ margin-bottom: 1em;
+ }
+
+ .firm-content pre {
+ padding: 0.3em;
+ }
+
+ .firm-content h1 {
+ font-size: 2em;
+ line-height: 1.8;
+ font-weight: bold;
+ }
+
+ .firm-content h2 {
+ font-size: 1.7em;
+ line-height: 1.8;
+ font-weight: bold;
+ }
+
+ .firm-content h3 {
+ font-size: 1.4em;
+ line-height: 1.3;
+ font-weight: bold;
+ }
+
+ .firm-content th {
+ border-bottom: 1px solid aliceblue;
+ }
+
+ .firm-content tr:nth-child(2n) {
+ background: #383b3c;
+ }
+
+ .firm-content h4 {
+ font-size: 1.2em;
+ line-height: 1.3;
+ font-weight: bold;
+ }
+
+ .firm-content a {
+ color: rgb(59 130 246);
+ transition: 300ms ease-in-out;
+ }
+
+ .firm-content a:hover {
+ color: rgb(96 165 250);
+ }
+</style>
diff --git a/web/src/pages/docs/texture-pack-format.astro b/web/src/pages/docs/texture-pack-format.astro
new file mode 100644
index 0000000..f04188f
--- /dev/null
+++ b/web/src/pages/docs/texture-pack-format.astro
@@ -0,0 +1,15 @@
+---
+import Base from "../../layouts/Base.astro";
+import NavBar from "../../components/NavBar.astro";
+import Content from "../../layouts/Content.astro";
+import MarkDown from './texture-pack-format.md';
+---
+
+<Base title="Firmament Texture Packs">
+ <NavBar></NavBar>
+ <Content>
+ <MarkDown/>
+ </Content>
+</Base>
+
+
diff --git a/web/src/pages/docs/texture-pack-format.md b/web/src/pages/docs/texture-pack-format.md
new file mode 100644
index 0000000..da66043
--- /dev/null
+++ b/web/src/pages/docs/texture-pack-format.md
@@ -0,0 +1,583 @@
+<!--
+SPDX-FileCopyrightText: 2023 Linnea Gräf <nea@nea.moe>
+
+SPDX-License-Identifier: CC0-1.0
+-->
+
+# Custom SkyBlock Items Texture Pack Format
+
+## Items by internal id (ExtraAttributes)
+
+Find the internal id of the item. This is usually stored in the ExtraAttributes tag (Check the Power User Config for
+keybinds). Once you found it, create an item model in a resource pack like you would for
+a vanilla item model, but at the coordinate `firmskyblock:<internalid>`. So for an aspect of the end, this would be
+`firmskyblock:models/item/aspect_of_the_end.json` (or `assets/firmskyblock/models/item/aspect_of_the_end.json`). Then,
+just use a normal minecraft item model. See https://github.com/nea89o/BadSkyblockTP/blob/master/assets/firmskyblock/models/item/magma_rod.json
+as an example. The id is first turned to lower case, then gets `:` replaced with `___`, `;` with `__` and all other
+characters that cannot be used in a minecraft resource location with `__XXXX` where `XXXX` is the 4 digit hex code for
+the character.
+
+## (Placed) Skulls by texture id
+
+Find the texture id of a skull. This is the hash part of an url like
+`https://textures.minecraft.net/texture/bc8ea1f51f253ff5142ca11ae45193a4ad8c3ab5e9c6eec8ba7a4fcb7bac40` (so after the
+/texture/). You can find it in game for placed skulls using the keybinding in the Power User Config. Then place the
+replacement texture at `firmskyblock:textures/placedskulls/<thathash>.png`. Keep in mind that you will probably replace
+the texture with another skin texture, meaning that skin texture has its own hash. Do not mix those up, you need to use
+the hash of the old skin.
+
+## Armor Skull Models
+
+You can replace the models of skull items (or other items) by specifying the `firmament:head_model` property on your
+model. Note that this is resolved *after* all [overrides](#predicates) and further predicates are not resolved on the
+head model.
+
+```json5
+{
+ "parent": "minecraft:item/generated",
+ "textures": {
+ "layer0": "firmskyblock:item/regular_texture"
+ },
+ "firmament:head_model": "minecraft:block/diamond_block" // when wearing on the head render a diamond block instead (can be any item model, including custom ones)
+}
+```
+
+## Tint Overrides
+
+Some items get naturally tinted by Minecraft's rendering. Examples include leather armour, spawn eggs, potions and more.
+If you want to avoid your textures getting tinted, one thing you can do is use a higher texture layer:
+
+```json
+{
+ "parent": "minecraft:item/generated",
+ "textures": {
+ // Notice the layer1 instead of layer0 here
+ "layer1": "firmskyblock:item/regular_texture"
+ }
+}
+```
+
+Some items, however, tint *all* layers. For those items you can instead specify a tint override:
+
+```json
+{
+ "parent": "minecraft:item/generated",
+ "textures": {
+ "layer0": "firmskyblock:item/regular_texture"
+ },
+ "firmament:tint_overrides": {
+ "0": -1
+ }
+}
+```
+
+This forces layer 0 to be tinted with the color `-1` (pure white, aka no tint). This property is inherited, so if you
+attach it to one of your root models that you `"parent"` other models to, all those models will have their tints
+overridden. When the property is inherited, only layers specified in the child actually overwrite the parent layers.
+You can use `"0": null` to remove the tint override in a child, which will cause a fallback to the vanilla tinting
+behaviour.
+
+## Predicates
+
+Firmament adds the ability for more complex [item model predicates](https://minecraft.wiki/w/Tutorials/Models#Item_predicates).
+Those predicates work on any model, including models for vanilla items, but they don't mix very well with vanilla model overrides.
+Vanilla predicates only ever get parsed at the top level, so including a vanilla predicate inside of a more complex
+firmament parser will result in an ignored predicate.
+
+### Example usage
+
+```json
+{
+ "parent": "minecraft:item/handheld",
+ "textures": {
+ "layer0": "firmskyblock:item/bat_wand"
+ },
+ "overrides": [
+ {
+ "predicate": {
+ "firmament:display_name": {
+ "regex": ".*§d.*",
+ "color": "preserve"
+ }
+ },
+ "model": "firmskyblock:item/recombobulated_bat_wand"
+ }
+ ]
+}
+```
+
+You specify an override like normally, with a `model` that will replace the current model and a list of `predicate`s
+that must match before that override takes place.
+
+At the top level `predicate` you can still use all the normal vanilla predicates, as well as the custom ones, which are
+all prefixed with `firmament:`.
+
+#### Display Name
+
+Matches the display name against a [string matcher](#string-matcher)
+
+```json
+"firmament:display_name": "Display Name Test"
+```
+
+#### Lore
+
+Tries to find at least one lore line that matches the given [string matcher](#string-matcher).
+
+```json
+"firmament:lore": {
+ "regex": "Mode: Red Mushrooms",
+ "color": "strip"
+}
+```
+
+#### Item type
+
+Filter by item type:
+
+```json
+"firmament:item": "minecraft:clock"
+```
+
+#### Extra attributes
+
+Filter by extra attribute NBT data:
+
+Specify a `path` to look at, separating sub elements with a `.`. You can use a `*` to check any child.
+
+Then either specify a `match` sub-object or directly inline that object in the format of an [nbt matcher](#nbt-matcher).
+
+Inlined match:
+
+```json5
+"firmament:extra_attributes": {
+ "path": "gems.JADE_0",
+ "string": "PERFECT"
+}
+```
+
+Sub object match:
+
+```json5
+"firmament:extra_attributes": {
+ "path": "gems.JADE_0",
+ "match": {
+ "string": "PERFECT"
+ }
+}
+```
+
+#### Pet Data
+
+Filter by pet information. While you can already filter by the skyblock id for pet type and tier, this allows you to
+further filter by level and some other pet info.
+
+```json5
+"firmament:pet" {
+ "id": "WOLF",
+ "exp": ">=25353230",
+ "tier": "[RARE,LEGENDARY]",
+ "level": "[50,)",
+ "candyUsed": 0
+}
+```
+
+| Name | Type | Description |
+|-------------|------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------|
+| `id` | [String](#string-matcher) | The id of the pet |
+| `exp` | [Number](#number-matcher) | The total experience of the pet |
+| `tier` | Rarity (like [Number](#number-matcher), but with rarity names instead) | The total experience of the pet |
+| `level` | [Number](#number-matcher) | The current level of the pet |
+| `candyUsed` | [Number](#number-matcher) | The number of pet candies used on the pet. This is present even if they are not shown in game (such as on a level 100 legendary pet) |
+
+Every part of this matcher is optional.
+
+
+#### Logic Operators
+
+Logic operators allow to combine other firmament predicates into one. This is done by building boolean operators:
+
+```json5
+"firmament:any": [
+ {
+ "firmament:display_name": "SkyBlock Menu (Click)"
+ },
+ {
+ "firmament:display_name": "SkyBlock",
+ "firmament:lore": "Some Lore Requirement"
+ }
+]
+```
+
+This `firmament:any` test if the display name is either "SkyBlock Menu (Click)" or "SkyBlock" (aka any of the child predicates match).
+
+Similarly, there is `firmament:all`, which requires all of its children to match.
+
+There is also `firmament:not`, which requires none of its children to match. Unlike `any` or `all`, however, `not`
+only takes in one predicate `{}` directly, not an array of predicates `[{}]`.
+
+Note also that by default all predicate dictionaries require all predicates in it to match, so you can imagine that all
+things are wrapped in an implicit `firmament:all` element.
+
+### String Matcher
+
+A string matcher allows you to match almost any string. Whenever a string matcher is expected, you can use any of these
+styles of creating one.
+
+#### Direct
+
+```json
+"firmament:display_name": "Test"
+```
+
+Directly specifying a raw string value expects the string to be *exactly* equal, after removing all formatting codes.
+
+#### Complex
+
+A complex string matcher allows you to specify whether the string will get its color codes removed or not before matching
+
+
+```json5
+"firmament:display_name": {
+ "color": "strip",
+ "color": "preserve",
+ // When omitting the color property alltogether, you will fall back to "strip"
+}
+```
+In that same object you can then also specify how the string will be matched using another property. You can only ever
+specify one of these other matchers and one color preserving property.
+
+```json5
+"firmament:display_name": {
+ "color": "strip",
+ // You can use a "regex" property to use a java.util.Pattern regex. It will try to match the entire string.
+ "regex": "So[me] Regex",
+ // You can use an "equals" property to test if the entire string is equal to some value.
+ // Equals is faster than regex, but also more limited.
+ "equals": "Some Text"
+}
+```
+
+### Number Matchers
+
+This matches a number against either a range or a specific number.
+
+#### Direct number
+
+You can directly specify a number using that value directly:
+```json5
+"firmament:pet": {
+ "level": 100
+}
+```
+
+This is best for whole numbers, since decimal numbers can be really close together but still be different.
+
+#### Intervals
+
+For ranges you can instead use an interval. This uses the standard mathematical notation for those as a string:
+
+
+```json5
+"firmament:pet": {
+ "level": "(50,100]"
+}
+```
+
+This is in the format of `(min,max)` or `[min,max]`. Either min or max can be omitted, which results in that boundary
+being ignored (so `[50,)` would be 50 until infinity). You can also vary the parenthesis on either side independently.
+
+Specifying round parenthesis `()` means the number is exclusive, so not including this number. For example `(50,100)`
+would not match just the number `50` or `100`, but would match `51`.
+
+Specifying square brackets `[]` means the number is inclusive. For example `[50,100]` would match both `50` and `100`.
+
+You can mix and match parenthesis and brackets, they only ever affect the number next to it.
+
+For more information in intervals check out [Wikipedia](https://en.wikipedia.org/wiki/Interval_(mathematics)).
+
+#### Operators
+
+If instead of specifying a range you just need to specify one boundary you can also use the standard operators to
+compare your number:
+
+```json5
+"firmament:pet": {
+ "level": "<50"
+}
+```
+
+This example would match if the level is less than fifty. The available operators are `<`, `>`, `<=` and `>=`. The
+operator needs to be specified on the left. The versions of the operator with `=` also allow the number to be equal.
+
+### Nbt Matcher
+
+This matches a single nbt element.
+
+Have the type of the nbt element as json key. Can be `string`, `int`, `float`, `double`, `long`, `short` and `byte`.
+
+The `string` type matches like a regular [string matcher](#string-matcher):
+
+```json
+"string": {
+ "color": "strip",
+ "regex": "^aaa bbb$"
+}
+```
+
+The other (numeric) types can either be matched directly against a number:
+
+```json
+"int": 10
+```
+
+Or as a range:
+
+```json
+"long": {
+ "min": 0,
+ "max": 1000
+}
+```
+
+Min and max are both optional, but you need to specify at least one. By default `min` is inclusive and `max` is exclusive.
+You can override that like so:
+
+```json
+"short": {
+ "min": 0,
+ "max": 1000,
+ "minExclusive": true,
+ "maxExclusive": false
+}
+```
+
+
+> [!WARNING]
+> This syntax for numbers is *just* for **NBT values**. This is also why specifying the type of the number is necessary.
+> For other number matchers, use [the number matchers](#number-matchers)
+
+## Armor textures
+
+You can re-*texture* armors, but not re-*model* them with firmament.
+
+To retexture a piece of armor place a json file at `assets/firmskyblock/overrides/armor_models/*.json`.
+
+```json
+{
+ "item_ids": [
+ "TARANTULA_BOOTS",
+ "TARANTULA_LEGGINGS",
+ // ETC
+ ],
+ "layers": [
+ {
+ "identifier": "firmskyblock:tarantula"
+ }
+ ]
+}
+```
+
+Only one such file can exist per item id, but multiple item ids can share one texture file this way.
+
+The `item_ids` is the items to which this override will apply when worn. Those are neu repo ids (so what will be shown
+in game as the regular SkyBlock id, not the resource pack identifier).
+
+### Layers
+
+The `layers` specify the multiple texture layers that will be used when rendering. For non leather armor, or armor
+ignoring the leather armor tint just one layer is enough.
+
+If you want to apply armor tint to the texture you will usually want two layers. The first layer has a tint applied:
+
+```json
+{
+ "identifier": "firmskyblock:angler",
+ "tint": true
+}
+```
+
+This will tint the texture before it is being rendered.
+
+The second layer will have no tint applied, but will have a suffix:
+
+```json
+{
+ "identifier": "firmskyblock:angler",
+ "suffix": "_overlay"
+}
+```
+
+This second layer is used for the countours of the armor.
+
+The layer identifier will resolve to a texture file path according to vanilla armor texture rules like so:
+
+`assets/{identifier.namespace}/textures/models/armor/{identifier.path}_layer_{isLegs ? 2 : 1}{suffix}.png`
+
+Note that there is no automatic underscore insertion for suffix, so you will need to manually specify it if you want.
+
+The leg armor piece uses a different texture, same as with vanilla.
+
+### Overrides
+
+You can also apply overrides to these layers. These work similar to item predicate overrides, but only the custom
+Firmament predicates will work. You will also just directly specify new layers instead of delegating to another file.
+
+```json
+{
+ "item_ids": [
+ "TARANTULA_BOOTS",
+ "TARANTULA_LEGGINGS",
+ // ETC
+ ],
+ "layers": [
+ {
+ "identifier": "firmskyblock:tarantula"
+ }
+ ],
+ "overrides": [
+ {
+ "layers": [
+ {
+ "identifier": "firmskyblock:tarantula_maxed"
+ }
+ ],
+ "predicate": {
+ "firmament:lore": {
+ "regex": "Piece Bonus: +285.*"
+ }
+ }
+ }
+ ]
+}
+```
+
+## UI Text Color Replacement
+
+This allows you to replace the color of text in your inventory. This includes inventory UIs like chests and anvils, but
+not screens from other mods. You can also target specific texts via a [string matcher](#string-matcher).
+
+```json
+// This file is at assets/firmskyblock/overrides/text_colors.json
+{
+ "defaultColor": -10496,
+ "overrides": [
+ {
+ "predicate": "Crafting",
+ "override": -16711936
+ }
+ ]
+}
+```
+
+| Field | Required | Description |
+|-----------------------|----------|----------------------------------------------------------------------------------------------------|
+| `defaultColor` | true | The default color to use in case no override matches |
+| `overrides` | false | Allows you to replace colors for specific strings. Is an array. |
+| `overrides.predicate` | true | This is a [string matcher](#string-matcher) that allows you to match on the text you are replacing |
+| `overrides.override` | true | This is the replacement color that will be used if the predicate matches. |
+
+## Global Item Texture Replacement
+
+Most texture replacement is done based on the SkyBlock id of the item. However, some items you might want to re-texture
+do not have an id. The next best alternative you had before was just to replace the vanilla item and add a bunch of
+predicates. This tries to fix this problem, at the cost of being more performance intensive than the other re-texturing
+methods.
+
+The entrypoint to global overrides is `firmskyblock:overrides/item`. Put your overrides into that folder, with one file
+per override.
+
+```json5
+{
+ "screen": "testrp:chocolate_factory",
+ "model": "testrp:time_tower",
+ "predicate": {
+ "firmament:display_name": {
+ "regex": "Time Tower.*"
+ }
+ }
+}
+```
+
+There are three parts to the override.
+
+The `model` is an *item id* that the item will be replaced with. This means the
+model will be loaded from `assets/<namespace>/models/item/<id>.json`. Make sure to use your own namespace to
+avoid collisions with other texture packs that might use the same id for a gui.
+
+The `predicate` is just a normal [predicate](#predicates). This one does not support the vanilla predicates. You can
+still use vanilla predicates in the resolved model, but this will not allow you to fall back to other global overrides.
+
+The `screen` specifies which screens your override will work on. This is purely for performance reasons, your filter
+should work purely based on predicates if possible. You can specify multiply screens by using a json array.
+
+### Global item texture Screens
+
+In order to improve performance not all overrides are tested all the time. Instead you can prefilter by the screen that
+is open. First the gui is resolved to `assets/<namespace>/filters/screen/<id>.json`. Make sure to use your own namespace
+to avoid collisions with other texture packs that might use the same id for a screen.
+
+```json
+{
+ "title": "Chocolate Factory"
+}
+```
+
+Currently, the only supported filter is `title`, which accepts a [string matcher](#string-matcher). You can also use
+`firmament:always` as an always on filter (this is the recommended way).
+
+## Block Model Replacements
+
+Firmament adds the ability to retexture block models. Supported renderers are vanilla, indigo (fabric), sodium (and
+anything sodium based). Firmament performs gentle world reloading so that even when the world data gets updated very
+late by the server there should be no flicker.
+
+If you want to replace block textures in the world you can do so using block overrides. Those are stored in
+`assets/firmskyblock/overrides/blocks/<id>.json`. The id does not matter, all overrides are loaded. This file specifies
+which block models are replaced under which conditions:
+
+```json
+{
+ "modes": [
+ "mining_3"
+ ],
+ "area": [
+ {
+ "min": [
+ -31,
+ 200,
+ -117
+ ],
+ "max": [
+ 12,
+ 223,
+ -95
+ ]
+ }
+ ],
+ "replacements": {
+ "minecraft:blue_wool": "firmskyblock:mithril_deep",
+ "minecraft:light_blue_wool": {
+ "block": "firmskyblock:mithril_deep",
+ "sound": "minecraft:block.wet_sponge.hit"
+ }
+ }
+}
+```
+
+| Field | Required | Description |
+|-------------------------|----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `modes` | yes | A list of `/locraw` mode names. |
+| `area` | no | A list of areas. Blocks outside of the coordinate range will be ignored. If the block is in *any* range it will be considered inside |
+| `area.min` | yes | The lowest coordinate in the area. Is included in the area. |
+| `area.max` | yes | The highest coordinate in the area. Is included in the area. |
+| `replacements` | yes | A map of block id to replacement mappings |
+| `replacements` (string) | yes | You can directly specify a string. Equivalent to just setting `replacements.block`. |
+| `replacements.block` | yes | You can specify a block model to be used instead of the regular one. The model will be loaded from `assets/<namespace>/models/block/<path>.json` like regular block models. |
+| `replacements.sound` | no | You can also specify a sound override. This is only used for the "hit" sound effect that repeats while the block is mined. The "break" sound effect played after a block was finished mining is sadly sent by hypixel directly and cannot be replaced reliably. |
+
+> A quick note about optimization: Not specifying an area (by just omitting the `area` field) is quicker than having an
+> area encompass the entire map.
+>
+> If you need to use multiple `area`s for unrelated sections of the world it might be a performance improvement to move
+> unrelated models to different files to reduce the amount of area checks being done for each block.
diff --git a/web/src/pages/index.astro b/web/src/pages/index.astro
index 139b515..4baa867 100644
--- a/web/src/pages/index.astro
+++ b/web/src/pages/index.astro
@@ -3,6 +3,7 @@ import Hero from "../components/Hero.astro"
import Base from "../layouts/Base.astro";
import Image from "../panorama.png";
import NavBar from "../components/NavBar.astro";
+import Content from "../layouts/Content.astro";
---
<Base title="Firmament">
@@ -12,4 +13,8 @@ import NavBar from "../components/NavBar.astro";
<a href="https://modrinth.com/mod/firmament" class="bg-amber-300 hover:bg-yellow-400 rounded-full text-black text-lg transition duration-300 hover:scale-110 hover:shadow-lg py-1 px-5">Download now</a>
</Hero>
<NavBar navStyle="transparent"></NavBar>
+ <Content>
+ Firmament is a 1.21 Minecraft mod for Hypixel SkyBlock. It has a variety of features such as an item list,
+ mining features.
+ </Content>
</Base>
diff --git a/web/src/pages/texture-packs.astro b/web/src/pages/texture-packs.astro
new file mode 100644
index 0000000..77d8a37
--- /dev/null
+++ b/web/src/pages/texture-packs.astro
@@ -0,0 +1,10 @@
+---
+import Base from "../layouts/Base.astro";
+import NavBar from "../components/NavBar.astro";
+import Content from "../layouts/Content.astro";
+---
+
+<Base title="Firmament Texture Packs">
+ <NavBar></NavBar>
+
+</Base>