diff --git a/CHANGELOG.md b/CHANGELOG.md index 320360f..43ce567 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,43 @@ All notable changes to this project are documented here. The format follows [Semantic Versioning](https://semver.org/). Pre-releases (`1.0.0-alpha.N`) mark the phases on the way to 1.0.0; see [docs/ROADMAP.md](docs/ROADMAP.md). +## [1.1.0-beta.5] - 2026-10-31 + +Phase 9 of the road to 1.1.0: the vehicle screen and what goes with it, supply vehicles and +thermal view. Betas are not uploaded to CurseForge, and this one ships the Forge 26.3 jar only. +Worlds from 1.1.0-beta.4 load as before; their vehicles come back with empty slots. Players and +servers need the same version, since the network protocol changed. + +### Added + +- The vehicle screen, on I in any seat (the original's GUI key, R, switches seats here). It has + three fuel slots that empty fuel cans into the tank while the vehicle stands on something, a + parachute slot for each ejection seat, and a weapon panel that shows every weapon's rounds and + reload cost and reloads it from your own items. Closing it after a reload loads every magazine. +- The cargo chest of vehicles with an `InventorySize`, from the vehicle screen's Cargo button or + with Shift + right-click on the vehicle from outside. Sizes between rows round up to a whole row. + What a vehicle carries drops when it is destroyed or packed up. +- Ammunition trucks (`AmmoSupplyRange`) rearm every vehicle around them that stands on something, + a tenth of each weapon's maximum every two seconds. +- Repair vehicles: `RepairOtherVehicles = range[, value]` mends every vehicle in range by `value` + hit points a second (10 by default). None of the original vehicles uses it; packs can. +- Thermal view: K now steps from night vision to a white hot thermal view with outlines and back + to normal. The HUD's `cam_mode` reads 2 in it, as in the original. The original's vanilla + "pencil" shader no longer exists, so the filter is our own. + +### Changed + +- Ejection seats need a parachute in the vehicle screen's parachute slots, one per ejection, as in + the original. Without one the seat stays put and says so. Creative players need none. +- Reloading a weapon from your inventory moved from the N key to the vehicle screen and only works + on the ground, as in the original. The empty magazine hint on the HUD names the new key. +- The network protocol is now version 8. + +### Fixed + +- A helicopter on the ground no longer tilts with the pilot's mouse; it stays level until it + lifts off. + ## [1.1.0-beta.4] - 2026-10-30 Phase 8 of the road to 1.1.0: fuel, the first equipment carried over from phase 4, hit boxes that @@ -515,6 +552,7 @@ Phase 1: foundation and flyable helicopters. - Free look moved from Left Ctrl to Left Alt, because Left Ctrl is now the vanilla sprint key. - Roll damping runs per tick instead of per frame, so handling no longer depends on frame rate. +[1.1.0-beta.5]: https://github.com/JnH-Projects/MCHeli-Rework-Forge/releases/tag/v1.1.0-beta.5 [1.1.0-beta.4]: https://github.com/JnH-Projects/MCHeli-Rework-Forge/releases/tag/v1.1.0-beta.4 [1.1.0-beta.3]: https://github.com/JnH-Projects/MCHeli-Rework-Forge/releases/tag/v1.1.0-beta.3 [1.1.0-beta.2]: https://github.com/JnH-Projects/MCHeli-Rework-Forge/releases/tag/v1.1.0-beta.2 diff --git a/README.md b/README.md index e71ec52..c0c773f 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,18 @@ Phase 4 clips (click a picture to open the video): ## Screenshots +Phase 9 in a development build (`1.1.0-beta.5`): thermal view, the vehicle screen, ejection seats +with parachutes from the vehicle, and cargo. + +| | | +|---|---| +| ![The AH-64 cockpit in thermal view: the desert in white hot greys with outlines, the green HUD reading THERMAL VISION](docs/images/phase-9/ah-64-thermal-view.png) | ![The cargo chest of a B1 Centauro, two rows of slots above the player inventory](docs/images/phase-9/centauro-cargo-chest.png) | +| Thermal view from the AH-64's cockpit | The Centauro's cargo chest, opened from outside | +| ![The AH-64's vehicle screen: fuel slots on the left, the M230 chain gun at 15 of 1200 rounds with its reload cost of iron and gunpowder](docs/images/phase-9/vehicle-screen-empty-reserve.png) | ![The same screen after reloading, the chain gun at 1200 of 1200 in green and the Reload button greyed out](docs/images/phase-9/vehicle-screen-reloaded.png) | +| The vehicle screen with the chain gun's reserve empty | Reloaded from the player's own iron and gunpowder | +| ![A player in an ejection seat climbing away above a desert lake after leaving an F-35B](docs/images/phase-9/ejection-seat.png) | ![Looking down the parachute's rigging lines at the player in the seat, the F-35B and an AH-64 below](docs/images/phase-9/ejection-parachute-open.png) | +| Ejecting from an F-35B, with a parachute from its slot | The canopy opens once the seat falls | + Phase 8 in a development build (`1.1.0-beta.4`): hit boxes that cover the whole vehicle, and shot down planes that spiral in. @@ -228,7 +240,8 @@ to climb. | J | Load nearby vehicles onto your racks, or drive onto a carrier's rack | | U | Unload the last vehicle from your racks, or leave the carrier | | Z | Through a camera, gunner sight or gunner mode: zoom (2x, 4x and so on up to the vehicle's limit, then back) | -| K | Night vision on and off, in vehicles that have it | +| K | Night vision, then thermal view, then back to normal, in vehicles that have night vision | +| I | The vehicle screen: fuel and parachute slots, reloading, and the cargo chest | | Shift | Leave the vehicle | **Weapons** @@ -239,7 +252,6 @@ to climb. | Middle mouse or G | Next weapon for your seat; with Shift, the previous one | | X | Fire mode, for weapons that have one (HE rounds, top attack, TV guidance) | | C | Release flares (pilot and first crew seat) | -| N | Resupply the selected weapon, paying its item cost from your inventory | Each seat fires the weapons its vehicle assigns to it; the pilot also fires a gunner's weapons while that seat has no player. Creative players have unlimited ammunition. A small readout at the @@ -249,12 +261,18 @@ bottom right shows hit points, the weapon, ammunition, reload, heat, lock and fl Vehicles burn fuel while the throttle is open; the HUD shows the tank and warns below 10%. With an empty tank the engine winds down. Refuel with the **fuel can** from the Equipment tab: -right-click a vehicle to pour in up to 100 units, and right-click in the air with coal or -charcoal in your inventory to fill the can again. A fuel truck tops up every vehicle around it. -The **wrench** repairs 10 hit points per click, once a second; with Shift it switches the -vehicle's paint. The **parachute** opens when used in mid-air; hold W to glide forward. -Ejection seats come down on a parachute of their own. All three are crafted with the original's -recipes. +right-click a vehicle to pour in up to 100 units, or put cans in the fuel slots of the vehicle +screen (I), which empty into the tank while the vehicle is on the ground. Right-click in the air +with coal or charcoal in your inventory to fill a can again. A fuel truck tops up every vehicle +around it, and an ammunition truck rearms them. The **wrench** repairs 10 hit points per click, +once a second; with Shift it switches the vehicle's paint. The **parachute** opens when used in +mid-air; hold W to glide forward. Ejection seats need a parachute in the vehicle screen's +parachute slots, one per ejection. All three are crafted with the original's recipes. + +The vehicle screen also reloads weapons from your own items, one supply per click, while the +vehicle is on the ground; closing it loads every magazine. Vehicles with cargo space have a +**Cargo** button there, and Shift + right-click opens their cargo chest from outside. What a +vehicle carries drops when it is destroyed. ## Commands and server settings diff --git a/common/src/main/java/com/jnhprojects/mcheli/content/aircraft/AircraftDefinition.java b/common/src/main/java/com/jnhprojects/mcheli/content/aircraft/AircraftDefinition.java index 1404d13..4f5a1f5 100644 --- a/common/src/main/java/com/jnhprojects/mcheli/content/aircraft/AircraftDefinition.java +++ b/common/src/main/java/com/jnhprojects/mcheli/content/aircraft/AircraftDefinition.java @@ -68,6 +68,9 @@ public abstract class AircraftDefinition extends Definition { public float fuelConsumption = 1.0F; public float fuelSupplyRange; public float ammoSupplyRange; + /** {@code RepairOtherVehicles}: how far this vehicle repairs others, and by how much a second. */ + public float repairOtherVehiclesRange; + public int repairOtherVehiclesValue = 10; public int inventorySize; // Body and physics @@ -268,6 +271,26 @@ public int seatCount() { return seats.size(); } + /** + * Rows of the cargo chest: {@code InventorySize} rounded up to whole rows of nine, the only + * sizes a chest screen comes in. Zero means the vehicle has no chest. + */ + public int cargoRows() { + return (inventorySize + 8) / 9; + } + + /** + * Seats that can bail out, one parachute slot each in the vehicle screen: seats zero and one + * with {@code EnableEjectionSeat}, except the pilot seat of a drone, whose pilot is not aboard. + */ + public int ejectionSeats() { + if (!ejectionSeat) { + return 0; + } + int seats = Math.min(seatCount(), 2); + return uav && seats > 0 ? seats - 1 : seats; + } + /** Crew seats followed by racks, the index space used by {@code ExclusionSeat} and the seat map. */ public int seatAndRackCount() { return seats.size() + racks.size(); @@ -312,6 +335,13 @@ protected void registerKeys(KeyTable keys) { keys.put("FuelConsumption", v -> fuelConsumption = toFloat(v, 0.0F, 10000.0F)); keys.put("FuelSupplyRange", v -> fuelSupplyRange = toFloat(v, 0.0F, 1000.0F)); keys.put("AmmoSupplyRange", v -> ammoSupplyRange = toFloat(v, 0.0F, 1000.0F)); + keys.put("RepairOtherVehicles", v -> { + String[] s = split(v); + repairOtherVehiclesRange = toFloat(s[0], 0.0F, 1000.0F); + if (s.length >= 2) { + repairOtherVehiclesValue = toInt(s[1], 0, 10000000); + } + }); keys.put("InventorySize", v -> inventorySize = toInt(v, 0, 54)); keys.put("Width", v -> bodyWidth = toFloat(v, 0.1F, 1000.0F)); diff --git a/common/src/main/java/com/jnhprojects/mcheli/supply/SupplyRules.java b/common/src/main/java/com/jnhprojects/mcheli/supply/SupplyRules.java new file mode 100644 index 0000000..0c73ae5 --- /dev/null +++ b/common/src/main/java/com/jnhprojects/mcheli/supply/SupplyRules.java @@ -0,0 +1,48 @@ +package com.jnhprojects.mcheli.supply; + +/** + * How much fuel, ammunition and repair one supply step moves, after the original's + * {@code updateFuel}, {@code supplyAmmoToOtherAircraft} and {@code repairOtherAircraft}. Kept apart + * from the entities so the numbers can be checked without a game. + */ +public final class SupplyRules { + /** The most a fuel slot pours into the tank per step, every half second. */ + public static final int FUEL_SLOT_POUR = 100; + /** Ticks between two steps of the fuel slots: half a second. */ + public static final int FUEL_SLOT_INTERVAL = 10; + /** Ticks between two rounds of resupply from an ammunition truck: two seconds. */ + public static final int AMMO_TRUCK_INTERVAL = 40; + /** Ticks between two repairs from a repair vehicle: one second. */ + public static final int REPAIR_INTERVAL = 20; + + private SupplyRules() { + } + + /** + * Fuel one slot pours this step. + * + * @param missing room left in the tank + * @param contents fuel left in the can + */ + public static int fuelFromCan(int missing, int contents) { + return Math.max(Math.min(Math.min(missing, FUEL_SLOT_POUR), contents), 0); + } + + /** + * Rounds an ammunition truck adds to one weapon per step: a tenth of the weapon's + * {@code MaxAmmo}, at least one, and never past the maximum. + * + * @param carried magazine and reserve together + */ + public static int roundsFromTruck(int carried, int maxAmmo) { + if (carried >= maxAmmo) { + return 0; + } + return Math.min(Math.max(maxAmmo / 10, 1), maxAmmo - carried); + } + + /** Hit points a repair vehicle gives back per step, never past the maximum. */ + public static int repair(int hp, int maxHp, int value) { + return Math.max(Math.min(value, maxHp - hp), 0); + } +} diff --git a/common/src/main/resources/assets/mcheli/lang/en_us.json b/common/src/main/resources/assets/mcheli/lang/en_us.json index 79dac13..06b6014 100644 --- a/common/src/main/resources/assets/mcheli/lang/en_us.json +++ b/common/src/main/resources/assets/mcheli/lang/en_us.json @@ -28,6 +28,12 @@ "entity.mcheli.torpedo": "Torpedo", "entity.mcheli.tv_missile": "TV-Guided Missile", "entity.mcheli.vehicle": "Ground Vehicle", + "gui.mcheli.vehicle.cargo": "Cargo", + "gui.mcheli.vehicle.fuel": "Fuel %s%%", + "gui.mcheli.vehicle.no_weapons": "No weapons", + "gui.mcheli.vehicle.parachute": "Parachutes", + "gui.mcheli.vehicle.reload": "Reload", + "gui.mcheli.vehicle.reload_hint": "Land to reload", "hud.mcheli.ammo": "%s / %s", "hud.mcheli.ammo_infinite": "%s / Infinite", "hud.mcheli.ammo_none": "No ammunition needed", @@ -41,7 +47,7 @@ "hud.mcheli.overheat": "OVERHEAT", "hud.mcheli.preparing": "Preparing", "hud.mcheli.reloading": "Reloading", - "hud.mcheli.resupply": "Empty, resupply with [%s]", + "hud.mcheli.resupply": "Empty, reload on the ground [%s]", "item.mcheli.helicopter": "Helicopter", "item.mcheli.plane": "Plane", "item.mcheli.tank": "Tank", @@ -52,21 +58,22 @@ "itemGroup.mcheli.tanks": "MCHeli Tanks and Cars", "itemGroup.mcheli.vehicles": "MCHeli Ground Vehicles", "key.category.mcheli.main": "MCHeli", - "key.mcheli.camera_mode": "Night Vision", + "key.mcheli.camera_mode": "Night Vision / Thermal", "key.mcheli.flare": "Flares", "key.mcheli.fold": "Zoom / Fold Wings / Hatch", "key.mcheli.free_look": "Free Look", "key.mcheli.landing_gear": "Landing Gear", "key.mcheli.rack_load": "Load Rack / Board Carrier", "key.mcheli.rack_unload": "Unload Rack / Leave Carrier", - "key.mcheli.resupply": "Resupply Weapon", "key.mcheli.switch_camera": "Camera / Gunner Sight", "key.mcheli.switch_seat": "Switch Seat", "key.mcheli.switch_weapon": "Switch Weapon", + "key.mcheli.vehicle_screen": "Vehicle Screen", "key.mcheli.vtol": "VTOL Mode", "key.mcheli.weapon_mode": "Weapon Mode", "message.mcheli.content.import_failed": "MCHeli could not import the original vehicles: %s. See logs/latest.log.", "message.mcheli.content.missing": "MCHeli has no vehicles yet. Download %s from the MCHeli CurseForge page, put it in %s and restart the game.", + "message.mcheli.no_parachute": "No parachute in the vehicle: put one in its parachute slot first", "message.mcheli.pack.downloading": "MCHeli: downloading %s content packs (%s MB) from the server", "message.mcheli.pack.failed": "MCHeli: content pack %s could not be downloaded: %s", "message.mcheli.pack.more_problems": "MCHeli: and %s more problems with content packs, see logs/latest.log", diff --git a/common/src/main/resources/assets/mcheli/lang/ja_jp.json b/common/src/main/resources/assets/mcheli/lang/ja_jp.json index d3b946e..181f726 100644 --- a/common/src/main/resources/assets/mcheli/lang/ja_jp.json +++ b/common/src/main/resources/assets/mcheli/lang/ja_jp.json @@ -4,6 +4,12 @@ "entity.mcheli.plane": "飛行機", "entity.mcheli.tank": "戦車", "entity.mcheli.vehicle": "地上兵器", + "gui.mcheli.vehicle.cargo": "積荷", + "gui.mcheli.vehicle.fuel": "燃料 %s%%", + "gui.mcheli.vehicle.no_weapons": "武装なし", + "gui.mcheli.vehicle.parachute": "パラシュート", + "gui.mcheli.vehicle.reload": "補給", + "gui.mcheli.vehicle.reload_hint": "着陸すると補給できます", "item.mcheli.helicopter": "ヘリコプター", "item.mcheli.plane": "飛行機", "item.mcheli.tank": "戦車", @@ -14,7 +20,7 @@ "itemGroup.mcheli.tanks": "MCHeli 戦車・車両", "itemGroup.mcheli.vehicles": "MCHeli 地上兵器", "key.category.mcheli.main": "MCHeli", - "key.mcheli.camera_mode": "暗視", + "key.mcheli.camera_mode": "暗視 / 熱線映像", "key.mcheli.fold": "ズーム / 翼・ハッチの折りたたみ", "key.mcheli.free_look": "フリールック", "key.mcheli.landing_gear": "着陸脚", @@ -22,9 +28,11 @@ "key.mcheli.rack_unload": "ラックから降ろす / 離脱", "key.mcheli.switch_camera": "カメラ / 照準切り替え", "key.mcheli.switch_seat": "座席切り替え", + "key.mcheli.vehicle_screen": "機体画面", "key.mcheli.vtol": "VTOLモード", "message.mcheli.content.import_failed": "MCHeli: 元の機体データを読み込めませんでした: %s。logs/latest.log を確認してください。", "message.mcheli.content.missing": "MCHeli: 機体データがありません。MCHeli の CurseForge ページから %s をダウンロードし、%s に置いてからゲームを再起動してください。", + "message.mcheli.no_parachute": "機体にパラシュートがありません。先にパラシュート欄に入れてください", "message.mcheli.pack.downloading": "MCHeli: サーバーからコンテンツパックを %s 個 (%s MB) ダウンロードしています", "message.mcheli.pack.failed": "MCHeli: コンテンツパック %s をダウンロードできませんでした: %s", "message.mcheli.pack.ready": "MCHeli: コンテンツパック %s 個のダウンロードが完了しました", diff --git a/common/src/test/java/com/jnhprojects/mcheli/content/HelicopterDefinitionTest.java b/common/src/test/java/com/jnhprojects/mcheli/content/HelicopterDefinitionTest.java index 30cdaa1..c162a93 100644 --- a/common/src/test/java/com/jnhprojects/mcheli/content/HelicopterDefinitionTest.java +++ b/common/src/test/java/com/jnhprojects/mcheli/content/HelicopterDefinitionTest.java @@ -65,6 +65,29 @@ void appliesHelicopterDefaults() throws IOException { assertEquals(List.of("heli", "heli_gnr"), def.huds); } + @Test + void readsInventoryAndSupplyKeys() throws IOException { + HelicopterDefinition def = parse("cargo", """ + AddSeat = 0, 1, 0 + AddSeat = 0, 1, 1 + AddSeat = 0, 1, 2 + InventorySize = 10 + EnableEjectionSeat = true + RepairOtherVehicles = 20, 5 + """); + + // Ten slots need two rows of nine; only seats zero and one have ejection seats. + assertEquals(2, def.cargoRows()); + assertEquals(2, def.ejectionSeats()); + assertEquals(20.0F, def.repairOtherVehiclesRange); + assertEquals(5, def.repairOtherVehiclesValue); + + HelicopterDefinition bare = parse("bare", "AddSeat = 0, 1, 0"); + assertEquals(0, bare.cargoRows()); + assertEquals(0, bare.ejectionSeats()); + assertEquals(10, bare.repairOtherVehiclesValue); + } + @Test void rejectsDefinitionWithoutSeats() throws IOException { DefinitionReader.Result result = parseResult("empty", "DisplayName = Nothing"); diff --git a/common/src/test/java/com/jnhprojects/mcheli/supply/SupplyRulesTest.java b/common/src/test/java/com/jnhprojects/mcheli/supply/SupplyRulesTest.java new file mode 100644 index 0000000..589e98a --- /dev/null +++ b/common/src/test/java/com/jnhprojects/mcheli/supply/SupplyRulesTest.java @@ -0,0 +1,33 @@ +package com.jnhprojects.mcheli.supply; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class SupplyRulesTest { + + @Test + void fuelSlotPoursAtMostOneHundred() { + assertEquals(100, SupplyRules.fuelFromCan(450, 600)); + assertEquals(30, SupplyRules.fuelFromCan(30, 600)); + assertEquals(12, SupplyRules.fuelFromCan(450, 12)); + assertEquals(0, SupplyRules.fuelFromCan(0, 600)); + assertEquals(0, SupplyRules.fuelFromCan(450, 0)); + } + + @Test + void ammunitionTruckAddsATenthOfTheMaximum() { + assertEquals(40, SupplyRules.roundsFromTruck(0, 400)); + // Never past the maximum, and at least one round for small magazines. + assertEquals(10, SupplyRules.roundsFromTruck(390, 400)); + assertEquals(1, SupplyRules.roundsFromTruck(2, 4)); + assertEquals(0, SupplyRules.roundsFromTruck(400, 400)); + } + + @Test + void repairStopsAtFullHealth() { + assertEquals(10, SupplyRules.repair(50, 100, 10)); + assertEquals(3, SupplyRules.repair(97, 100, 10)); + assertEquals(0, SupplyRules.repair(100, 100, 10)); + } +} diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 7532a48..a70b107 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -488,6 +488,30 @@ player, steered by the rider's forward input from `ServerPlayer.getLastClientInp `Kind` picks the model: the round canopy for the item, the seat for ejection (whose canopy opens once it falls), the bare canopy for drops. +### Vehicle inventory and supply + +`entity.VehicleInventory` is the original's `MCH_AircraftInventory` plus the container half of +`W_EntityContainer`: five equipment slots (three fuel cans, two parachutes) and a 54 slot cargo +chest, saved as `Equipment` and `Cargo`. The chest always has room for the largest size and the +screen shows `AircraftDefinition.cargoRows()` rows of it, so a content reload that shrinks +`InventorySize` hides items instead of deleting them. Everything drops when the vehicle is removed +for good (`RemovalReason.shouldDestroy`), not when its chunk unloads. + +`menu.VehicleMenu` is the vehicle screen, registered as `mcheli:vehicle` in `registry.ModMenus`; +the cargo chest reuses vanilla's `ChestMenu` and `GENERIC_9xN` types. A crew member opens the +screen with `CrewActionPacket.Action.VEHICLE_SCREEN`; the client side of the menu finds the +vehicle as the one its player rides, so the open packet needs no extra data and the menu works on +every loader. Both sides always build the same 41 slots, parachute slots the vehicle lacks are +simply inactive. The server sends each weapon's rounds and a bitmask of the weapons that can be +reloaded now through data slots, because a client only hears about its own seat's ammunition. +The screen's buttons go back through vanilla's container button packet (`clickMenuButton`): ids +from zero reload that weapon through `weapon.Resupply`, `BUTTON_CARGO` opens the chest. + +The supply amounts are in `supply.SupplyRules` in `common`: a fuel slot pours up to 100 units +every half second while `canSupply()` (the vehicle stands on something, and floats or is out of +the water), an ammunition truck (`AmmoSupplyRange`) adds a tenth of each weapon's maximum every +two seconds, and a repair vehicle (`RepairOtherVehicles`) mends its neighbours once a second. + ## Client **CrewController** samples keys each client tick and the mouse every frame, for whichever seat the @@ -544,7 +568,11 @@ original's `MCH_Camera`. Zoom narrows the field of view through `ViewportEvent.C offered on the fold key whenever the seat looks through a sight, as the original did. Night vision asks the server through `CrewActionPacket`, and `AircraftEntity` keeps a short ambient night vision effect topped up on that seat's occupant, removing it again on leaving (unless a longer -effect, such as a potion, was there). +effect, such as a potion, was there). Thermal view is night vision plus the `mcheli:thermal` post +effect (`assets/mcheli/post_effect/thermal.json`, shader `post/thermal.fsh`), a white hot +monochrome with outlines in place of the original's vanilla "pencil" shader, which no longer +exists. `VehicleCamera.tick` keeps it in `LocalPlayer.getActivePostEffects()`, the list the game +reads every frame, so no loader hook is needed. **CrewController** also hides the first person hand through `RenderHandEvent` wherever the original did: for the pilot, and for anyone looking through a camera or with a weapon selected. diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index f8daa36..d5fa05e 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -38,8 +38,8 @@ wait for the other process to finish and build again. Shared properties live in the root `gradle.properties`: `mod_version`, the mod metadata, the list of loader modules (`platforms`) and the library versions `common` compiles against. Each loader module has its own `gradle.properties` with its loader, Minecraft and Java versions. During betas -4 to 7, `platforms` lists only `forge-26.3`, so CI and the release build one jar; the other modules -stay in the repository and come back to the list for 1.1.0 (roadmap phase 12). +4 to 8, `platforms` lists only `forge-26.3`, so CI and the release build one jar; the other modules +stay in the repository and come back to the list for 1.1.0 (roadmap phase 13). `mods.toml`, `neoforge.mods.toml`, `fabric.mod.json` and `pack.mcmeta` are filled in from them at build time. What every loader module shares (source folders, jar contents, manifest, file names) is in `gradle/loader.gradle`. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 7ac4dfc..a7cb408 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -12,7 +12,7 @@ tagged pre-release that installs and runs on its own. | 3 | `1.0.0-alpha.3` | Weapons and combat | Implemented, in-game test in progress | | 4 | `1.0.0-beta.1` | HUD and polish | Done | | | `1.0.0` | Release: code-only jar | Released | -| 5-12 | `1.1.0-beta.1` to `beta.8` | Content packs, NeoForge and Fabric, 1.21.1, equipment, workshop, 1.20.1 ([plan](#road-to-110)) | In progress | +| 5-13 | `1.1.0-beta.1` to `beta.9` | Content packs, NeoForge and Fabric, 1.21.1, equipment, workshop, 1.20.1 ([plan](#road-to-110)) | In progress | | | `1.1.0` | Release on CurseForge | Planned | ## Phase 1: foundation and flyable helicopters @@ -97,7 +97,7 @@ The rest of the original phase 4 list moved to 1.1.0 (phases 8 and 9 below), so 1.1.0 adds three big things on top of 1.0.0: content packs, more Minecraft versions and loaders, and the vehicle workshop. It also picks up the phase 4 items that were left out of 1.0.0. The work is -split into eight phases, each ending in a beta. +split into nine phases, each ending in a beta. ### Release channels @@ -135,18 +135,19 @@ Rules that hold for every beta: | 6 | `1.1.0-beta.2` | Content packs and multiplayer sync | Released | | 7 | `1.1.0-beta.3` | Loaders (Forge, NeoForge, Fabric) and 1.21.1 | Released | | 8 | `1.1.0-beta.4` | Fuel, first equipment, hull hit boxes (Forge 26.3 only) | Released | -| 9 | `1.1.0-beta.5` | The rest of the equipment (Forge 26.3 only) | Planned | -| 10 | `1.1.0-beta.6` | Workshop: model, parts and tuning (Forge 26.3 only) | Planned | -| 11 | `1.1.0-beta.7` | Workshop: textures, sounds and sharing (Forge 26.3 only) | Planned | -| 12 | `1.1.0-beta.8` | Every loader, 1.21.1 and 1.20.1 | Planned | +| 9 | `1.1.0-beta.5` | Vehicle screen, cargo, supply vehicles, thermal view (Forge 26.3 only) | Released | +| 10 | `1.1.0-beta.6` | The rest of the equipment (Forge 26.3 only) | Planned | +| 11 | `1.1.0-beta.7` | Workshop: model, parts and tuning (Forge 26.3 only) | Planned | +| 12 | `1.1.0-beta.8` | Workshop: textures, sounds and sharing (Forge 26.3 only) | Planned | +| 13 | `1.1.0-beta.9` | Every loader, 1.21.1 and 1.20.1 | Planned | | | `1.1.0` | Release on CurseForge | Planned | The order is deliberate. The registry and the source split come first because everything else sits on them. Content packs come before the workshop, since the workshop's output is a pack. The loader split and 1.21.1 come right after packs, while the code is still close to 1.0.0; from then on each feature is built once in shared code and only its Minecraft glue is written per -version and loader. Betas 4 to 7 build only the Forge 26.3 jar, since six modules on three -toolchains made every build slow; phase 12 writes the glue for the rest, adds 1.20.1 and ships +version and loader. Betas 4 to 8 build only the Forge 26.3 jar, since six modules on three +toolchains made every build slow; phase 13 writes the glue for the rest, adds 1.20.1 and ships every jar again. ### Supported versions @@ -231,7 +232,7 @@ almost everything, adds NeoForge and Fabric on 26.3, then brings all three to 1. ## Phase 8: fuel, first equipment and hull hit boxes (`1.1.0-beta.4`) Gameplay carried over from phase 4, built once in shared code with per-loader glue. Built and -tested on Forge 26.3 only; the other loaders and versions get it in phase 12. What was not done +tested on Forge 26.3 only; the other loaders and versions get it in phase 13. What was not done in this phase moved to phase 9. - [x] Glows under Iris shaders: the custom glow pipelines are handed to Iris, so tracers, motors, @@ -246,20 +247,26 @@ in this phase moved to phase 9. projectiles and by collisions between vehicles - [x] Heavy aircraft cook off in three blasts -## Phase 9: the rest of the equipment (`1.1.0-beta.5`) +## Phase 9: vehicle screen, cargo and supply vehicles (`1.1.0-beta.5`) + +Built and tested on Forge 26.3 only. What was not done in this phase moved to phase 10. + +- [x] Thermal view +- [x] The vehicle inventory, with its fuel and parachute slots, and the cargo chest +- [x] Resupply from the vehicle screen and from ammunition trucks; repair vehicles +- [x] Helicopters stay level on the ground + +## Phase 10: the rest of the equipment (`1.1.0-beta.6`) -- [ ] Thermal view -- [ ] The vehicle inventory, with its fuel slots - [ ] Equipment: GLTD, rangefinder, UAV station and portable controller, chain towing, container, Stinger and Javelin, smoke grenades - [ ] Parachutes for rack drops, containers on racks, repelling and troop drops - [ ] `HideEntity`, search lights, splash and exhaust particles, helicopter blade folding -- [ ] Resupply from the inventory screen and from supply vehicles, marking blocks with targeting - pods, mortar distance +- [ ] Marking blocks with targeting pods, mortar distance - [ ] The drafting table and its recipe browser (pack vehicles included) - [ ] Multiplayer team manager and spotting -## Phase 10: workshop, model, parts and tuning (`1.1.0-beta.6`) +## Phase 11: workshop, model, parts and tuning (`1.1.0-beta.7`) The first half of the [editor](#the-editor). Its output is a pack from phase 6, so anything built here syncs to other players with no extra work. @@ -272,7 +279,7 @@ built here syncs to other players with no extra work. - [ ] Save, export and load packages; undo and redo - [ ] Workshop guide in `docs/content/workshop.md` -## Phase 11: workshop, textures, sounds and sharing (`1.1.0-beta.7`) +## Phase 12: workshop, textures, sounds and sharing (`1.1.0-beta.8`) - [ ] Texture painting on the model and PNG import per part (cockpit textures included), with live preview @@ -280,13 +287,13 @@ built here syncs to other players with no extra work. - [ ] Package browser, author credits shown in game - [ ] Operator approval before a player's new vehicle goes live on a server -## Phase 12: every loader, 1.21.1 and 1.20.1 (`1.1.0-beta.8`) +## Phase 13: every loader, 1.21.1 and 1.20.1 (`1.1.0-beta.9`) -Betas 4 to 7 ship only the Forge 26.3 jar, to keep builds fast. This phase brings the features +Betas 4 to 8 ship only the Forge 26.3 jar, to keep builds fast. This phase brings the features added since beta.3 to NeoForge, Fabric and 1.21.1, and adds 1.20.1, the largest modpack version and the furthest from 26.3. All jars ship again from here on. -- [ ] Every module back in `platforms`; phases 8 to 11 ported to NeoForge and Fabric on 26.3 and +- [ ] Every module back in `platforms`; phases 8 to 12 ported to NeoForge and Fabric on 26.3 and to all three loaders on 1.21.1 - [ ] `common` compiles for Java 17 (no pattern matching `switch`, no `SequencedCollection` methods) @@ -299,7 +306,7 @@ and the furthest from 26.3. All jars ship again from here on. ## 1.1.0 release -No new features after beta.8; a `1.1.0-rc.N` follows only if beta.8 testing finds problems. +No new features after beta.9; a `1.1.0-rc.N` follows only if beta.9 testing finds problems. - [ ] Every phase checklist done on every supported Minecraft version - [ ] A 1.0.0 world and the beta worlds load with their vehicles and items intact diff --git a/docs/TESTING.md b/docs/TESTING.md index 0f382db..10b7490 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -161,8 +161,9 @@ Run this for each jar in `build/release`: Forge, NeoForge and Fabric on every Mi - [ ] `ah-6`: right mouse fires the M134 in a stream with muzzle flashes, ejected cases and its sound; middle mouse (or G) switches to the Hydra rockets, which leave a smoke trail and explode on impact. Shift with G goes back. -- [ ] Survival: the readout counts ammunition down and reloads; when the reserve is empty, N with - iron, gunpowder (and redstone where listed) in the inventory buys one resupply. +- [ ] Survival: the readout counts ammunition down and reloads; when the reserve is empty, landed, + Reload in the vehicle screen (I) with iron, gunpowder (and redstone where listed) in the + inventory buys one resupply. - [ ] Creative: ammunition shows as infinite. - [ ] `f-35b`: selecting the AIM-9X opens its weapon bay. Holding right mouse at a flying mob or a second aircraft shows "Locking" and fires once locked; the missile chases the target. @@ -478,6 +479,51 @@ Install Sodium and Iris for 26.3 with a shader pack (Complementary or BSL). Leaving the vehicle turns it off. A night vision potion is not cut short. - [ ] K in a vehicle without night vision plays the "no" sound and does nothing. +## Phase 9 checklist (`1.1.0-beta.5`) + +Forge 26.3 only. Start from a world made by beta.4 with a few vehicles in it, in survival. + +### Thermal view + +- [ ] K in an `ah-64` cycles normal, night vision, thermal view (white hot, with outlines) and back + to normal. The HUD's `cam_mode` shows 1 and 2. Night vision stays on through the thermal view. +- [ ] Leaving the vehicle or switching seat in thermal view goes back to the normal view. +- [ ] With an Iris shader pack on, thermal view still shows (or note what it looks like). + +### Vehicle screen + +- [ ] I in any seat opens the vehicle screen, named after the vehicle, with three fuel slots and, + a parachute slot per ejection seat (one in the single seat `f-35b`). A helicopter without + ejection seats has none. +- [ ] Only fuel cans go in the fuel slots and only parachutes in the parachute slots. Shift-click + from the inventory sends them there. +- [ ] Landed with a fuel can in a slot, the fuel percentage and gauge climb by up to 100 units + every half second and the can's bar drops; in the air nothing pours. +- [ ] Empty a weapon's reserve, land: Reload is active with the items in the inventory, takes them + and adds the rounds. In the air it is greyed out with "Land to reload" as its tooltip. + Closing the screen loads the magazine. In creative it costs nothing. +- [ ] `<` and `>` step through every weapon of the vehicle, including other seats' weapons, with + their rounds as the server has them. +- [ ] Space twice in an `f-35b` without a parachute in the slots: no ejection, the "no" sound and a + message. With a parachute in a slot, the seat ejects and the parachute is used up. +- [ ] Items in the vehicle screen stay after leaving, saving and reloading the world. + +### Helicopter on the ground + +- [ ] Sit in a parked helicopter and move the mouse around: it stays level. Throttle up until it + lifts off: from then on the mouse tilts it as before. + +### Cargo and supply vehicles + +- [ ] A vehicle with an `InventorySize` (`ch47`, `c5`, `centauro`) shows a Cargo button in the + vehicle screen that opens a chest of the right size. Shift + right-click on the vehicle from + outside opens the same chest; walking away closes it. +- [ ] The `m1129` (`InventorySize = 10`) gets two rows. +- [ ] Walk far enough for the vehicle's chunk to unload and come back: the cargo is still there. +- [ ] Destroy a loaded vehicle (or pack it up by punching it): the fuel cans, parachutes and cargo + drop. +- [ ] Park next to an `ammo_box`: weapons with a low reserve fill up a tenth at a time. + ## Reporting problems Attach `logs/latest.log` (and the crash report, if any) with the helicopter name, what you did diff --git a/docs/content/aircraft.md b/docs/content/aircraft.md index 945ac05..26c4e97 100644 --- a/docs/content/aircraft.md +++ b/docs/content/aircraft.md @@ -50,7 +50,7 @@ CameraPosition = 0.0, 1.1, 3.7, true, 30, 45 | `CameraZoom = 3` | Maximum camera zoom. | | `HUD = heli, heli_gnr, none, gunner` | HUD file per seat (`hud/.txt`). `none` hides the HUD. Missing seats use the defaults below. | | `EnableGunnerMode = true` | Allow switching to gunner mode. | -| `EnableNightVision = true` | Allow switching night vision. | +| `EnableNightVision = true` | Allow switching night vision and thermal view. | | `EnableEntityRadar = false` | Fit a radar. | | `DefaultFreelook = true` | Start in free look right after boarding. Mostly used for tanks. | | `CameraRotationSpeed = 25` | Camera rotation speed. Tanks use it to limit turret traverse speed. | @@ -244,7 +244,7 @@ the old damage values to the flattened item names. | `ArmorMinDamage = 5` | Hits below this are ignored. | | `ArmorMaxDamage = 500` | Hits above this are capped. With 100, a 300 point hit only deals 100. | | `DamageFactor = 0.2` | Damage multiplier for players inside. 0.2 means 20%. The vehicle still takes the full hit. | -| `InventorySize = 18` | Inventory size, a multiple of 9. | +| `InventorySize = 18` | Slots in the cargo chest, up to 54. Sizes between rows round up to a whole row of 9. | | `Regeneration = true` | Mobs from seat 2 onwards heal over time. | | `Stealth = 0.5` | 0.0 to 1.0, default 0.0. Higher values make missile locks take longer and shorten lock range. | @@ -265,7 +265,8 @@ seconds is `MaxFuel / FuelConsumption`, here 1200 seconds. | Key | Meaning | |-----|---------| | `FuelSupplyRange = 25` | Refuels every vehicle within 25 m. The supplier's own fuel does not drop, but it cannot refuel itself. | -| `AmmoSupplyRange = 35` | Rearms every vehicle within 35 m, with the same rules. | +| `AmmoSupplyRange = 35` | Rearms every vehicle within 35 m that stands on something, a tenth of each weapon's maximum every two seconds. | +| `RepairOtherVehicles = 20, 10` | Repairs every vehicle within 20 m by 10 hit points a second. The value is optional (default 10). | ### Rendering and sound @@ -411,7 +412,7 @@ Files: `planes/.txt`, `models/planes/.mqo`, `textures/planes/. | `EnableVtol = true` | VTOL capable. | | `DefaultVtol = true` | Switch to VTOL automatically on the ground. | | `VtolYaw = 0.3`, `VtolPitch = 0.3` | Turn and pitch rate in VTOL. | -| `EnableEjectionSeat = true` | Adds ejection seats to the GUI, one per seat for up to two seats. | +| `EnableEjectionSeat = true` | Seats zero and one can eject; each ejection takes a parachute from the vehicle screen, which gets a parachute slot per ejection seat. | | `AddParticleSplash = X, Y, Z, count, size, speed, lifetime, rise, gravity` | Spray while moving on water. Independent of `EnableSeaSurfaceParticle`. | | `EnableSeaSurfaceParticle = true` | Spray when flying low over the sea. Scales with `ParticlesScale`, around 0.7 is recommended. | diff --git a/docs/images/phase-9/ah-64-thermal-view.png b/docs/images/phase-9/ah-64-thermal-view.png new file mode 100644 index 0000000..3eb20d1 Binary files /dev/null and b/docs/images/phase-9/ah-64-thermal-view.png differ diff --git a/docs/images/phase-9/centauro-cargo-chest.png b/docs/images/phase-9/centauro-cargo-chest.png new file mode 100644 index 0000000..81a9828 Binary files /dev/null and b/docs/images/phase-9/centauro-cargo-chest.png differ diff --git a/docs/images/phase-9/ejection-parachute-open.png b/docs/images/phase-9/ejection-parachute-open.png new file mode 100644 index 0000000..f559924 Binary files /dev/null and b/docs/images/phase-9/ejection-parachute-open.png differ diff --git a/docs/images/phase-9/ejection-seat.png b/docs/images/phase-9/ejection-seat.png new file mode 100644 index 0000000..e551115 Binary files /dev/null and b/docs/images/phase-9/ejection-seat.png differ diff --git a/docs/images/phase-9/vehicle-screen-empty-reserve.png b/docs/images/phase-9/vehicle-screen-empty-reserve.png new file mode 100644 index 0000000..c49058c Binary files /dev/null and b/docs/images/phase-9/vehicle-screen-empty-reserve.png differ diff --git a/docs/images/phase-9/vehicle-screen-reloaded.png b/docs/images/phase-9/vehicle-screen-reloaded.png new file mode 100644 index 0000000..210b833 Binary files /dev/null and b/docs/images/phase-9/vehicle-screen-reloaded.png differ diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/forge/ForgeClient.java b/forge-26.3/src/main/java/com/jnhprojects/mcheli/forge/ForgeClient.java index 269072d..aa46450 100644 --- a/forge-26.3/src/main/java/com/jnhprojects/mcheli/forge/ForgeClient.java +++ b/forge-26.3/src/main/java/com/jnhprojects/mcheli/forge/ForgeClient.java @@ -17,14 +17,17 @@ import net.minecraftforge.client.event.ViewportEvent; import net.minecraftforge.client.gui.overlay.ForgeLayeredDraw; import net.minecraftforge.event.TickEvent; +import net.minecraftforge.eventbus.api.bus.BusGroup; +import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent; /** Client events on Forge. Only loaded on a physical client. */ final class ForgeClient { private ForgeClient() { } - static void init() { + static void init(BusGroup modBus) { ClientHooks.init(); + FMLClientSetupEvent.getBus(modBus).addListener(event -> event.enqueueWork(ClientHooks::registerMenuScreens)); EntityRenderersEvent.RegisterRenderers.BUS.addListener(event -> ClientHooks.registerEntityRenderers(event::registerEntityRenderer)); RegisterParticleProvidersEvent.BUS.addListener(event -> event.registerSpriteSet(ModParticles.GLOW.get(), GlowParticle.Provider::new)); RegisterKeyMappingsEvent.BUS.addListener(event -> ClientHooks.keyMappings().forEach(event::register)); diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/forge/McheliForge.java b/forge-26.3/src/main/java/com/jnhprojects/mcheli/forge/McheliForge.java index d9b6998..42082fa 100644 --- a/forge-26.3/src/main/java/com/jnhprojects/mcheli/forge/McheliForge.java +++ b/forge-26.3/src/main/java/com/jnhprojects/mcheli/forge/McheliForge.java @@ -55,7 +55,7 @@ public McheliForge(FMLJavaModLoadingContext context) { }); if (Platform.get().isClient()) { - ForgeClient.init(); + ForgeClient.init(modBus); } } diff --git a/gradle.properties b/gradle.properties index dd60f85..f73d9ed 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ net.minecraftforge.gradle.merge-source-sets=true mod_id=mcheli mod_name=MCHeli Rework mod_group=com.jnhprojects.mcheli -mod_version=1.1.0-beta.4 +mod_version=1.1.0-beta.5 mod_authors=JayMar921 (rework), EMB4 (original author), Fureniku (CurseForge maintainer) mod_license=MCHeli Rework Licence 1.0 (see LICENSE) diff --git a/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/MCHeli.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/MCHeli.java index 21d4185..002caf0 100644 --- a/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/MCHeli.java +++ b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/MCHeli.java @@ -6,6 +6,7 @@ import com.jnhprojects.mcheli.registry.ModDataComponents; import com.jnhprojects.mcheli.registry.ModEntities; import com.jnhprojects.mcheli.registry.ModItems; +import com.jnhprojects.mcheli.registry.ModMenus; import com.jnhprojects.mcheli.registry.ModParticles; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -40,6 +41,7 @@ public static void init() { ModDataComponents.init(); ModEntities.init(); ModParticles.init(); + ModMenus.init(); ModItems.init(content); ModCreativeTabs.init(); } diff --git a/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/ClientHooks.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/ClientHooks.java index 2305d50..ae92df7 100644 --- a/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/ClientHooks.java +++ b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/ClientHooks.java @@ -12,16 +12,19 @@ import com.jnhprojects.mcheli.client.render.ParachuteRenderer; import com.jnhprojects.mcheli.client.render.ProjectileRenderer; import com.jnhprojects.mcheli.client.render.RiderPose; +import com.jnhprojects.mcheli.client.screen.VehicleScreen; import com.jnhprojects.mcheli.client.sound.EngineSoundPlayer; import com.jnhprojects.mcheli.entity.ClientAccess; import com.jnhprojects.mcheli.platform.Platform; import com.jnhprojects.mcheli.registry.ModEntities; +import com.jnhprojects.mcheli.registry.ModMenus; import com.mojang.blaze3d.vertex.PoseStack; import net.minecraft.client.Camera; import net.minecraft.client.DeltaTracker; import net.minecraft.client.KeyMapping; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.screens.MenuScreens; import net.minecraft.client.player.LocalPlayer; import net.minecraft.client.renderer.entity.EntityRendererProvider; import net.minecraft.client.renderer.entity.state.LivingEntityRenderState; @@ -79,6 +82,11 @@ public static void registerEntityRenderers(EntityRenderers renderers) { renderers.register(ModEntities.PARACHUTE.get(), ParachuteRenderer::new); } + /** On the main thread during client setup, once our menu types are registered. */ + public static void registerMenuScreens() { + MenuScreens.register(ModMenus.VEHICLE.get(), VehicleScreen::new); + } + public static List keyMappings() { return KeyBindings.ALL; } @@ -96,6 +104,7 @@ public static void clientTickEnd() { Glow.assignShaderPrograms(); } CrewController.onClientTick(); + VehicleCamera.tick(); WeaponController.onClientTick(); HudRenderer.tick(); EngineSoundPlayer.tick(); diff --git a/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/CrewController.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/CrewController.java index f0d52dd..1772884 100644 --- a/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/CrewController.java +++ b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/CrewController.java @@ -175,6 +175,11 @@ static void onClientTick() { while (KeyBindings.CAMERA_MODE.consumeClick()) { VehicleCamera.onCameraModeKey(player, vehicle); } + while (KeyBindings.VEHICLE_SCREEN.consumeClick()) { + if (!vehicle.isDestroyed()) { + send(vehicle, CrewActionPacket.Action.VEHICLE_SCREEN); + } + } while (KeyBindings.VTOL.consumeClick()) { if (pilot) { send(vehicle, CrewActionPacket.Action.VTOL); diff --git a/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/KeyBindings.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/KeyBindings.java index 11f9426..df4ab7e 100644 --- a/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/KeyBindings.java +++ b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/KeyBindings.java @@ -33,13 +33,13 @@ public final class KeyBindings { public static final KeyMapping WEAPON_MODE = new KeyMapping("key.mcheli.weapon_mode", InputConstants.KEY_X, CATEGORY); /** The original used V, which VTOL took over. */ public static final KeyMapping FLARE = new KeyMapping("key.mcheli.flare", InputConstants.KEY_C, CATEGORY); - /** The original resupplied from the vehicle's inventory screen, which does not exist yet. */ - public static final KeyMapping RESUPPLY = new KeyMapping("key.mcheli.resupply", InputConstants.KEY_N, CATEGORY); - /** Night vision on and off in vehicles with {@code EnableNightVision}. The original used C, now the flare key. */ + /** Fuel and parachute slots, reloading and the cargo chest. The original used R, which switches seats here. */ + public static final KeyMapping VEHICLE_SCREEN = new KeyMapping("key.mcheli.vehicle_screen", InputConstants.KEY_I, CATEGORY); + /** Night vision, thermal view and back in vehicles with {@code EnableNightVision}. The original used C, now the flare key. */ public static final KeyMapping CAMERA_MODE = new KeyMapping("key.mcheli.camera_mode", InputConstants.KEY_K, CATEGORY); public static final List ALL = List.of(FREE_LOOK, SWITCH_CAMERA, LANDING_GEAR, FOLD, VTOL, SWITCH_SEAT, RACK_LOAD, RACK_UNLOAD, - SWITCH_WEAPON, WEAPON_MODE, FLARE, RESUPPLY, CAMERA_MODE); + SWITCH_WEAPON, WEAPON_MODE, FLARE, VEHICLE_SCREEN, CAMERA_MODE); private KeyBindings() { } diff --git a/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/VehicleCamera.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/VehicleCamera.java index 56027d7..b94a0f9 100644 --- a/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/VehicleCamera.java +++ b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/VehicleCamera.java @@ -1,26 +1,36 @@ package com.jnhprojects.mcheli.client; +import com.jnhprojects.mcheli.MCHeli; import com.jnhprojects.mcheli.ModSounds; import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; import com.jnhprojects.mcheli.entity.AircraftEntity; import com.jnhprojects.mcheli.network.CrewActionPacket; import com.jnhprojects.mcheli.network.ModNetwork; +import net.minecraft.client.Minecraft; import net.minecraft.client.player.LocalPlayer; +import net.minecraft.resources.Identifier; import net.minecraft.sounds.SoundEvents; import net.minecraft.sounds.SoundSource; +import java.util.List; + /** * The local crew member's sight, {@code MCH_Camera}: zoom for vehicles with a {@code CameraZoom} - * above one, and night vision for those with {@code EnableNightVision}. Both reset whenever the - * player changes seat or leaves the vehicle. + * above one, and night vision and thermal view for those with {@code EnableNightVision}. Both + * reset whenever the player changes seat or leaves the vehicle. */ public final class VehicleCamera { /** The original's camera modes; the HUD reads the ordinal as {@code cam_mode}. */ public enum Mode { NORMAL, - NIGHT_VISION + NIGHT_VISION, + /** Night vision seen through a white hot filter, the original's "pencil" shader mode. */ + THERMAL } + /** The post effect of the thermal view, {@code assets/mcheli/post_effect/thermal.json}. */ + private static final Identifier THERMAL_EFFECT = Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "thermal"); + private static float zoom = 1.0F; private static Mode mode = Mode.NORMAL; @@ -41,6 +51,24 @@ static void reset() { mode = Mode.NORMAL; } + /** + * Keeps the thermal filter in the player's post effects while the thermal view is on. The list + * is the one a server's post effect packet fills, so ours is put back if that replaces it. + */ + static void tick() { + LocalPlayer player = Minecraft.getInstance().player; + if (player == null) { + return; + } + List effects = player.getActivePostEffects(); + boolean active = effects.contains(THERMAL_EFFECT); + if (mode == Mode.THERMAL && !active) { + effects.add(THERMAL_EFFECT); + } else if (mode != Mode.THERMAL && active) { + effects.remove(THERMAL_EFFECT); + } + } + /** * {@code zoomCamera}: each press doubles the zoom up to the vehicle's {@code CameraZoom}, and * the press after that goes back to one. The caller checks that the seat looks through a sight. @@ -56,16 +84,23 @@ static void onZoomKey(LocalPlayer player, AircraftEntity vehicle) { player.level().playLocalSound(player, ModSounds.get("zoom"), SoundSource.PLAYERS, 0.5F, 1.0F); } - /** {@code switchCameraMode}: night vision on or off, which the server applies as an effect. */ + /** + * {@code switchCameraMode}: steps from the plain view to night vision, to thermal view and + * back. The server applies night vision as an effect in both of the last two; the thermal + * filter is drawn here. + */ static void onCameraModeKey(LocalPlayer player, AircraftEntity vehicle) { AircraftDefinition def = vehicle.definition(); if (def == null || !def.nightVision || vehicle.isDestroyed()) { refuse(player); return; } - mode = mode == Mode.NORMAL ? Mode.NIGHT_VISION : Mode.NORMAL; - ModNetwork.sendToServer(new CrewActionPacket(vehicle.getId(), - mode == Mode.NIGHT_VISION ? CrewActionPacket.Action.NIGHT_VISION_ON : CrewActionPacket.Action.NIGHT_VISION_OFF)); + Mode next = Mode.values()[(mode.ordinal() + 1) % Mode.values().length]; + if (mode == Mode.NORMAL || next == Mode.NORMAL) { + ModNetwork.sendToServer(new CrewActionPacket(vehicle.getId(), + next == Mode.NORMAL ? CrewActionPacket.Action.NIGHT_VISION_OFF : CrewActionPacket.Action.NIGHT_VISION_ON)); + } + mode = next; player.level().playLocalSound(player, SoundEvents.UI_BUTTON_CLICK.value(), SoundSource.PLAYERS, 0.3F, 1.0F); } diff --git a/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/WeaponController.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/WeaponController.java index 1bc2ec0..2cfff27 100644 --- a/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/WeaponController.java +++ b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/WeaponController.java @@ -92,9 +92,6 @@ static void onClientTick() { click(); } } - while (KeyBindings.RESUPPLY.consumeClick()) { - send(s.vehicle, CrewActionPacket.Action.RESUPPLY); - } if (mc.gui.screen() == null && mc.options.keyUse.isDown() && !s.vehicle.isDestroyed()) { fire(s, player); diff --git a/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/WeaponHud.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/WeaponHud.java index 3b98002..c742003 100644 --- a/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/WeaponHud.java +++ b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/WeaponHud.java @@ -88,7 +88,7 @@ static void draw(GuiGraphicsExtractor g, DeltaTracker delta) { } else if (set.isInPreparation()) { status = Component.translatable("hud.mcheli.preparing"); } else if (set.magazineSize() > 0 && set.ammo() == 0 && set.restAmmo() == 0) { - status = Component.translatable("hud.mcheli.resupply", KeyBindings.RESUPPLY.getTranslatedKeyMessage()); + status = Component.translatable("hud.mcheli.resupply", KeyBindings.VEHICLE_SCREEN.getTranslatedKeyMessage()); color = RED; } Guidance lock = weapons.guidance(weapons.selected(seat)); diff --git a/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/screen/VehicleScreen.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/screen/VehicleScreen.java new file mode 100644 index 0000000..a320b92 --- /dev/null +++ b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/screen/VehicleScreen.java @@ -0,0 +1,180 @@ +package com.jnhprojects.mcheli.client.screen; + +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.weapon.RoundItem; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.VehicleInventory; +import com.jnhprojects.mcheli.menu.VehicleMenu; +import com.jnhprojects.mcheli.weapon.LegacyItems; +import com.jnhprojects.mcheli.weapon.VehicleWeapons; +import com.jnhprojects.mcheli.weapon.WeaponSet; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.Tooltip; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import org.jspecify.annotations.Nullable; + +/** + * The vehicle screen, {@code MCH_AircraftGui}: fuel and parachute slots on the left, the weapon + * panel on the right with its reload cost and button, and a button into the cargo chest for + * vehicles that have one. The panel is drawn from the chest texture so it looks like vanilla. + */ +public class VehicleScreen extends AbstractContainerScreen { + private static final Identifier CHEST = Identifier.withDefaultNamespace("textures/gui/container/generic_54.png"); + private static final Identifier SLOT = Identifier.withDefaultNamespace("container/slot"); + /** Height of the part above the player inventory. */ + private static final int TOP = 88; + private static final int PANEL_X = 72; + private static final int PANEL_WIDTH = 96; + private static final int LABEL = 0xFF404040; + private static final int WHITE = 0xFFFFFFFF; + private static final int RED = 0xFFFF5555; + private static final int GREEN = 0xFF55FF55; + + private int weapon; + private boolean cargoButton; + private @Nullable Button reload; + private @Nullable Button previous; + private @Nullable Button next; + + public VehicleScreen(VehicleMenu menu, Inventory inventory, Component title) { + super(menu, inventory, title, 176, TOP + 96); + inventoryLabelY = imageHeight - 94; + } + + @Override + protected void init() { + super.init(); + previous = addRenderableWidget(Button.builder(Component.literal("<"), b -> step(-1)) + .bounds(leftPos + PANEL_X, topPos + 68, 16, 16).build()); + reload = addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.vehicle.reload"), b -> press(weapon)) + .bounds(leftPos + PANEL_X + 18, topPos + 68, PANEL_WIDTH - 36, 16).build()); + next = addRenderableWidget(Button.builder(Component.literal(">"), b -> step(1)) + .bounds(leftPos + PANEL_X + PANEL_WIDTH - 16, topPos + 68, 16, 16).build()); + AircraftEntity vehicle = menu.vehicle(); + AircraftDefinition def = vehicle != null ? vehicle.definition() : null; + cargoButton = def != null && def.cargoRows() > 0; + if (cargoButton) { + addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.vehicle.cargo"), b -> press(VehicleMenu.BUTTON_CARGO)) + .bounds(leftPos + imageWidth - 52, topPos + 3, 46, 13).build()); + } + updateButtons(); + } + + @Override + protected void containerTick() { + super.containerTick(); + updateButtons(); + } + + private int weaponCount() { + AircraftEntity vehicle = menu.vehicle(); + VehicleWeapons weapons = vehicle != null ? vehicle.weapons() : null; + return weapons != null ? Math.min(weapons.sets().size(), VehicleMenu.MAX_WEAPONS) : 0; + } + + private void step(int direction) { + int count = weaponCount(); + if (count > 0) { + weapon = Math.floorMod(weapon + direction, count); + updateButtons(); + } + } + + private void press(int button) { + if (minecraft.gameMode != null) { + minecraft.gameMode.handleInventoryButtonClick(menu.containerId, button); + } + } + + private void updateButtons() { + if (reload == null || previous == null || next == null) { + return; + } + int count = weaponCount(); + reload.visible = count > 0; + previous.visible = count > 1; + next.visible = count > 1; + reload.active = menu.canReload(weapon); + AircraftEntity vehicle = menu.vehicle(); + boolean grounded = vehicle != null && vehicle.canSupply(); + reload.setTooltip(grounded ? null : Tooltip.create(Component.translatable("gui.mcheli.vehicle.reload_hint"))); + } + + @Override + public void extractBackground(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float a) { + super.extractBackground(graphics, mouseX, mouseY, a); + int x = leftPos; + int y = topPos; + // The chest's title bar, its plain middle stretched down to the inventory, then the inventory. + graphics.blit(RenderPipelines.GUI_TEXTURED, CHEST, x, y, 0.0F, 0.0F, imageWidth, 17, 256, 256); + graphics.blit(RenderPipelines.GUI_TEXTURED, CHEST, x, y + 17, 0.0F, 12.0F, imageWidth, TOP - 17, imageWidth, 1, 256, 256); + graphics.blit(RenderPipelines.GUI_TEXTURED, CHEST, x, y + TOP, 0.0F, 126.0F, imageWidth, 96, 256, 256); + for (int i = 0; i < VehicleInventory.FUEL_SLOTS; i++) { + graphics.blitSprite(RenderPipelines.GUI_TEXTURED, SLOT, x + VehicleMenu.FUEL_X + i * 18 - 1, y + VehicleMenu.FUEL_Y - 1, 18, 18); + } + for (int i = 0; i < menu.parachuteSlots(); i++) { + graphics.blitSprite(RenderPipelines.GUI_TEXTURED, SLOT, x + VehicleMenu.PARACHUTE_X + i * 18 - 1, y + VehicleMenu.PARACHUTE_Y - 1, 18, 18); + } + graphics.fill(x + PANEL_X - 2, y + 17, x + PANEL_X + PANEL_WIDTH + 2, y + 66, 0xFF373737); + } + + @Override + protected void extractLabels(GuiGraphicsExtractor graphics, int mouseX, int mouseY) { + AircraftEntity vehicle = menu.vehicle(); + // Long names only give way to the Cargo button, where there is one. + int titleWidth = cargoButton ? imageWidth - 64 : imageWidth - 16; + graphics.text(font, font.plainSubstrByWidth(title.getString(), titleWidth), titleLabelX, titleLabelY, LABEL, false); + graphics.text(font, playerInventoryTitle, inventoryLabelX, inventoryLabelY, LABEL, false); + if (vehicle == null) { + return; + } + if (vehicle.maxFuel() > 0) { + // A percentage and a gauge: the exact units would run into the weapon panel. + int percent = Math.round(vehicle.fuelRatio() * 100.0F); + graphics.text(font, Component.translatable("gui.mcheli.vehicle.fuel", percent), VehicleMenu.FUEL_X, VehicleMenu.FUEL_Y - 11, LABEL, false); + int gaugeY = VehicleMenu.FUEL_Y + 18; + int width = VehicleInventory.FUEL_SLOTS * 18 - 2; + graphics.fill(VehicleMenu.FUEL_X, gaugeY, VehicleMenu.FUEL_X + width, gaugeY + 3, 0xFF373737); + graphics.fill(VehicleMenu.FUEL_X, gaugeY, VehicleMenu.FUEL_X + width * percent / 100, gaugeY + 3, percent <= 10 ? RED : GREEN); + } + if (menu.parachuteSlots() > 0) { + graphics.text(font, Component.translatable("gui.mcheli.vehicle.parachute"), VehicleMenu.PARACHUTE_X, VehicleMenu.PARACHUTE_Y - 11, LABEL, false); + } + weaponPanel(graphics, vehicle); + } + + /** Name, rounds carried against the maximum, and what one reload costs. */ + private void weaponPanel(GuiGraphicsExtractor graphics, AircraftEntity vehicle) { + VehicleWeapons weapons = vehicle.weapons(); + WeaponSet set = weapons != null && weaponCount() > 0 ? weapons.set(Math.min(weapon, weaponCount() - 1)) : null; + if (set == null) { + graphics.text(font, Component.translatable("gui.mcheli.vehicle.no_weapons"), PANEL_X, 20, WHITE, false); + return; + } + graphics.text(font, font.plainSubstrByWidth(set.displayName(), PANEL_WIDTH), PANEL_X, 20, WHITE, false); + int rounds = menu.rounds(weapon); + int max = set.maxAmmo(); + int color = rounds == 0 && max > 0 ? RED : rounds >= max ? GREEN : WHITE; + graphics.text(font, rounds + " / " + max, PANEL_X, 31, color, false); + int itemX = PANEL_X; + for (RoundItem cost : set.definition().roundItems) { + Item item = LegacyItems.item(cost.item(), cost.damage()); + if (item == null) { + continue; + } + graphics.item(new ItemStack(item), itemX, 42); + graphics.text(font, String.valueOf(cost.count()), itemX + 17, 47, WHITE, false); + itemX += 32; + if (itemX > PANEL_X + PANEL_WIDTH - 16) { + break; + } + } + } +} diff --git a/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/entity/AircraftEntity.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/entity/AircraftEntity.java index 31a66a6..4893846 100644 --- a/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/entity/AircraftEntity.java +++ b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/entity/AircraftEntity.java @@ -14,17 +14,20 @@ import com.jnhprojects.mcheli.item.AircraftItem; import com.jnhprojects.mcheli.item.FuelItem; import com.jnhprojects.mcheli.item.WrenchItem; +import com.jnhprojects.mcheli.menu.VehicleMenu; import com.jnhprojects.mcheli.particle.GlowParticleOptions; import com.jnhprojects.mcheli.physics.AircraftRotation; import com.jnhprojects.mcheli.physics.CrashSpin; import com.jnhprojects.mcheli.physics.GroundImpact; import com.jnhprojects.mcheli.registry.ModItems; +import com.jnhprojects.mcheli.supply.SupplyRules; import com.jnhprojects.mcheli.weapon.Explosions; import com.jnhprojects.mcheli.weapon.VehicleWeapons; import com.jnhprojects.mcheli.weapon.WeaponSet; import com.mojang.serialization.Codec; import net.minecraft.core.BlockPos; import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.network.chat.Component; import net.minecraft.network.syncher.EntityDataAccessor; import net.minecraft.network.syncher.EntityDataSerializers; import net.minecraft.network.syncher.SynchedEntityData; @@ -37,6 +40,7 @@ import net.minecraft.util.Mth; import net.minecraft.world.InteractionHand; import net.minecraft.world.InteractionResult; +import net.minecraft.world.SimpleMenuProvider; import net.minecraft.world.damagesource.DamageSource; import net.minecraft.world.damagesource.DamageTypes; import net.minecraft.world.effect.MobEffectInstance; @@ -51,6 +55,8 @@ import net.minecraft.world.entity.Pose; import net.minecraft.world.entity.player.Player; import net.minecraft.world.entity.vehicle.VehicleEntity; +import net.minecraft.world.inventory.ChestMenu; +import net.minecraft.world.inventory.MenuType; import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; @@ -181,6 +187,8 @@ public abstract class AircraftEntity extends VehicleEntity { private int fuelSuppliedCount; /** One bit per seat whose occupant has night vision on. Server only, not saved. */ private int nightVisionSeats; + /** Fuel and parachute slots and the cargo chest. Only the server's copy holds anything. */ + private final VehicleInventory inventory = new VehicleInventory(this); // Crashes, measured from position so they work whichever side simulates the vehicle. /** Blocks per tick into terrain, sideways, that count as a crash: about 25 m/s. */ @@ -473,6 +481,101 @@ private void supplyFuel(AircraftDefinition def) { } } + // Inventory and supply + + public VehicleInventory inventory() { + return inventory; + } + + /** + * {@code canSupply}: fuel cans and resupply only work while the vehicle stands on something, + * within three blocks; a vehicle that cannot float must not be in the water either. + */ + public boolean canSupply() { + AircraftDefinition def = definition(); + return def != null && hasBlockBelow(1, 3, false) && (def.floats || !isInWater()); + } + + /** {@code openGui}: shows a crew member the vehicle screen. Server only. */ + public void openScreen(ServerPlayer player) { + AircraftDefinition def = definition(); + if (def == null || isDestroyed() || player.getVehicle() != this) { + return; + } + player.openMenu(new SimpleMenuProvider((id, playerInventory, p) -> new VehicleMenu(id, playerInventory, this), displayName(def))); + } + + /** + * {@code openInventory}: shows the cargo chest of a vehicle with an {@code InventorySize}. + * Server only. + * + * @return false when the vehicle has none + */ + public boolean openCargo(ServerPlayer player) { + AircraftDefinition def = definition(); + if (def == null || def.cargoRows() <= 0 || isDestroyed() || !inventory.stillValid(player)) { + return false; + } + int rows = Math.min(def.cargoRows(), 6); + MenuType type = switch (rows) { + case 1 -> MenuType.GENERIC_9x1; + case 2 -> MenuType.GENERIC_9x2; + case 3 -> MenuType.GENERIC_9x3; + case 4 -> MenuType.GENERIC_9x4; + case 5 -> MenuType.GENERIC_9x5; + default -> MenuType.GENERIC_9x6; + }; + player.openMenu(new SimpleMenuProvider((id, playerInventory, p) -> new ChestMenu(type, id, playerInventory, inventory.cargo(), rows), + displayName(def))); + return true; + } + + /** The vehicle's name as its item shows it. */ + private static Component displayName(AircraftDefinition def) { + return Component.translatableWithFallback("item.mcheli." + def.name(), def.displayName); + } + + /** + * {@code supplyAmmoToOtherAircraft}: an ammunition truck ({@code AmmoSupplyRange}) tops up the + * weapons of every vehicle within range that stands on something, a tenth of each weapon's + * maximum every two seconds. + */ + private void supplyAmmo(AircraftDefinition def) { + float range = def.ammoSupplyRange; + if (range <= 0.0F || ticksAlive % SupplyRules.AMMO_TRUCK_INTERVAL != 0 || isDestroyed()) { + return; + } + for (AircraftEntity other : level().getEntitiesOfClass(AircraftEntity.class, getBoundingBox().inflate(range))) { + VehicleWeapons theirs = other.weapons(); + if (other == this || theirs == null || other.isDestroyed() || !other.canSupply()) { + continue; + } + for (WeaponSet set : theirs.sets()) { + int rounds = SupplyRules.roundsFromTruck(set.ammo() + set.restAmmo(), set.maxAmmo()); + if (rounds > 0) { + set.setRestAmmo(set.restAmmo() + rounds); + if (set.ammo() <= 0) { + set.reloadMagazine(); + } + } + } + } + } + + /** {@code repairOtherAircraft}: a repair vehicle ({@code RepairOtherVehicles}) mends those in range. */ + private void repairOthers(AircraftDefinition def) { + float range = def.repairOtherVehiclesRange; + if (range <= 0.0F || ticksAlive % SupplyRules.REPAIR_INTERVAL != 0 || isDestroyed()) { + return; + } + for (AircraftEntity other : level().getEntitiesOfClass(AircraftEntity.class, getBoundingBox().inflate(range))) { + AircraftDefinition theirs = other.definition(); + if (other != this && theirs != null) { + other.repair(SupplyRules.repair(other.hp(), theirs.maxHp, def.repairOtherVehiclesValue)); + } + } + } + /** Whether the flight model holds a hover: in hover mode, or while the pilot mans the guns. */ public boolean isHovering() { return isHoveringMode() || isGunnerMode(); @@ -623,7 +726,12 @@ public void tick() { if (!level().isClientSide()) { supplyFuel(def); + supplyAmmo(def); + repairOthers(def); burnFuel(def); + if (def.maxFuel > 0 && fuel() < def.maxFuel && !isDestroyed()) { + inventory.pourFuelCans(ticksAlive); + } tickNightVision(); } @@ -1232,10 +1340,18 @@ public InteractionResult interact(Player player, InteractionHand hand, Vec3 loca return held.getItem() instanceof FuelItem ? FuelItem.pour(held, this, player) : WrenchItem.useOn(held, this, player, hand); } InteractionResult result = super.interact(player, hand, location); + AircraftDefinition def = definition(); + if (result == InteractionResult.PASS && player.isSecondaryUseActive() && def != null && def.cargoRows() > 0 + && !isDestroyed() && player.getVehicle() != this) { + // Sneaking opens the cargo chest from outside, as in the original. + if (player instanceof ServerPlayer server) { + openCargo(server); + } + return InteractionResult.SUCCESS; + } if (result != InteractionResult.PASS || player.isSecondaryUseActive()) { return result; } - AircraftDefinition def = definition(); if (def == null || !def.canRide || player.getVehicle() == this) { return InteractionResult.PASS; } @@ -1413,7 +1529,9 @@ public boolean canEject(Entity entity) { /** * {@code ejectSeat}: throws the occupant clear of the aircraft in an ejection seat, whose - * parachute opens once it starts to fall. Server only. + * parachute opens once it starts to fall. Each ejection takes a parachute from the parachute + * slots of the vehicle screen; without one the seat stays put. Creative players need none. + * Server only. */ public void eject(Entity entity) { AircraftDefinition def = definition(); @@ -1421,6 +1539,14 @@ public void eject(Entity entity) { if (def == null || !canEject(entity)) { return; } + boolean creative = entity instanceof Player player && player.getAbilities().instabuild; + if (!creative && !inventory.takeParachute()) { + if (entity instanceof ServerPlayer player) { + ModSounds.playTo(player, "ng", 1.0F, 1.0F); + player.sendOverlayMessage(Component.translatable("message.mcheli.no_parachute")); + } + return; + } Vector3 seatPos = def.seats.get(seat).position(); Vec3 out = toWorld(seatPos.add(0.0, 2.0, 0.0)); Vector3f push = toWorldOffset(new Vector3(0.0, 2.0, 0.0)); @@ -2162,6 +2288,15 @@ private void wreckFire(AircraftDefinition def) { } } + /** A vehicle that is destroyed, packed up or killed spills what it carried; one that unloads keeps it. */ + @Override + public void remove(RemovalReason reason) { + if (reason.shouldDestroy() && !level().isClientSide() && !isRemoved()) { + inventory.dropAll(); + } + super.remove(reason); + } + @Override protected Item getDropItem() { AircraftDefinition def = definition(); @@ -2216,6 +2351,7 @@ protected void addAdditionalSaveData(ValueOutput output) { if (weapons != null) { output.putIntArray("WeaponsAmmo", weapons.saveAmmo()); } + inventory.save(output); ValueOutput.TypedOutputList seats = output.list("Seats", Codec.STRING); for (int i = 0; i < seatIds.length; i++) { Entity e = occupant(i); @@ -2250,6 +2386,7 @@ protected void readAdditionalSaveData(ValueInput input) { savedAmmo = ammo; } }); + inventory.load(input); savedSeats.clear(); List seats = input.listOrEmpty("Seats", Codec.STRING).stream().toList(); for (int i = 0; i < seats.size(); i++) { diff --git a/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/entity/HelicopterEntity.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/entity/HelicopterEntity.java index b140713..4d3143b 100644 --- a/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/entity/HelicopterEntity.java +++ b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/entity/HelicopterEntity.java @@ -126,7 +126,8 @@ public void applyPilotLook(float dYaw, float dPitch) { if (!isNearGround(2)) { yaw = clamp(x, 40.0F * def.mobilityYaw) * LOOK_SCALE; } - if (!isHovering()) { + // Standing on its skids or wheels a helicopter stays level; the mouse only tilts it in the air. + if (!isHovering() && !onGround()) { pitch = clamp(y, 40.0F * def.mobilityPitch) * LOOK_SCALE; rollDelta = clamp(x, 40.0F * def.mobilityRoll) * LOOK_SCALE; } diff --git a/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/entity/VehicleInventory.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/entity/VehicleInventory.java new file mode 100644 index 0000000..1fdc978 --- /dev/null +++ b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/entity/VehicleInventory.java @@ -0,0 +1,145 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.item.FuelItem; +import com.jnhprojects.mcheli.item.ParachuteItem; +import com.jnhprojects.mcheli.supply.SupplyRules; +import net.minecraft.world.ContainerHelper; +import net.minecraft.world.Containers; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; + +/** + * What a vehicle carries besides its crew, {@code MCH_AircraftInventory} and the container half of + * {@code W_EntityContainer}: the equipment slots of the vehicle screen (three fuel cans and a + * parachute per ejection seat) and the cargo chest of vehicles with an {@code InventorySize}. + * + *

The cargo chest always has room for the largest size, 54 slots, and the screen shows as many + * rows as the definition asks for. Items in rows a content reload took away are kept rather than + * lost, and still drop when the vehicle is destroyed. + */ +public final class VehicleInventory { + public static final int FUEL_SLOTS = 3; + public static final int PARACHUTE_SLOTS = 2; + public static final int EQUIPMENT_SLOTS = FUEL_SLOTS + PARACHUTE_SLOTS; + public static final int CARGO_SLOTS = 54; + /** How far from the vehicle's centre a player outside may keep a screen open, past its width. */ + private static final double REACH = 12.0; + + private final AircraftEntity vehicle; + private final SimpleContainer equipment = new GuardedContainer(EQUIPMENT_SLOTS); + private final SimpleContainer cargo = new GuardedContainer(CARGO_SLOTS); + + VehicleInventory(AircraftEntity vehicle) { + this.vehicle = vehicle; + } + + public SimpleContainer equipment() { + return equipment; + } + + public SimpleContainer cargo() { + return cargo; + } + + /** {@code isUseableByPlayer}: the crew, or someone standing next to the vehicle. */ + public boolean stillValid(Player player) { + if (vehicle.isRemoved() || vehicle.definition() == null) { + return false; + } + if (player.getVehicle() == vehicle) { + return true; + } + double reach = REACH + vehicle.definition().bodyWidth / 2.0; + return player.distanceToSqr(vehicle) <= reach * reach; + } + + // Fuel + + /** + * {@code updateFuel}: every half second, while the vehicle stands on something, each fuel can + * in a fuel slot pours up to 100 units into the tank. Server only. + * + * @return whether any fuel went in + */ + boolean pourFuelCans(int ticks) { + if (ticks % SupplyRules.FUEL_SLOT_INTERVAL != 0 || !vehicle.canSupply()) { + return false; + } + boolean poured = false; + for (int slot = 0; slot < FUEL_SLOTS; slot++) { + ItemStack can = equipment.getItem(slot); + if (!(can.getItem() instanceof FuelItem)) { + continue; + } + int amount = SupplyRules.fuelFromCan(vehicle.maxFuel() - vehicle.fuel(), FuelItem.contents(can)); + if (amount > 0) { + vehicle.refuel(amount); + can.setDamageValue(can.getDamageValue() + amount); + poured = true; + } + } + if (poured) { + equipment.setChanged(); + } + return poured; + } + + // Parachutes + + /** Parachute slots the vehicle screen offers: one per ejection seat. */ + public static int parachuteSlots(AircraftDefinition def) { + return Math.min(def.ejectionSeats(), PARACHUTE_SLOTS); + } + + /** + * {@code consumeParachute}: takes one parachute out of the parachute slots for a crew member + * who bails out. + * + * @return false when there is none left + */ + boolean takeParachute() { + for (int slot = FUEL_SLOTS; slot < EQUIPMENT_SLOTS; slot++) { + if (equipment.getItem(slot).getItem() instanceof ParachuteItem) { + equipment.removeItem(slot, 1); + return true; + } + } + return false; + } + + // Saving and dropping + + void save(ValueOutput output) { + ContainerHelper.saveAllItems(output.child("Equipment"), equipment.getItems(), false); + ContainerHelper.saveAllItems(output.child("Cargo"), cargo.getItems(), false); + } + + void load(ValueInput input) { + equipment.clearContent(); + cargo.clearContent(); + input.child("Equipment").ifPresent(in -> ContainerHelper.loadAllItems(in, equipment.getItems())); + input.child("Cargo").ifPresent(in -> ContainerHelper.loadAllItems(in, cargo.getItems())); + } + + /** {@code setDead}: everything aboard falls out where the vehicle was. Server only. */ + void dropAll() { + Containers.dropContents(vehicle.level(), vehicle, equipment); + Containers.dropContents(vehicle.level(), vehicle, cargo); + } + + /** A container that only stays open for players {@link #stillValid(Player)} allows. */ + private final class GuardedContainer extends SimpleContainer { + GuardedContainer(int size) { + super(size); + } + + @Override + public boolean stillValid(Player player) { + return VehicleInventory.this.stillValid(player); + } + } +} diff --git a/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/item/FuelItem.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/item/FuelItem.java index aa2113b..efa8f29 100644 --- a/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/item/FuelItem.java +++ b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/item/FuelItem.java @@ -16,9 +16,9 @@ * The fuel can, {@code MCH_ItemFuel}. It holds 600 units; the durability bar shows how much is * left. Using it refills it from the coal and charcoal in the inventory, as in the original. * - *

The original poured the can into a vehicle through the fuel slots of the vehicle's inventory - * screen. That screen does not exist yet, so right-clicking a vehicle with the can pours up to 100 - * units at a time instead. + *

A can in a fuel slot of the vehicle screen empties into the tank while the vehicle stands on + * something, as in the original. Not in the original: right-clicking a vehicle with the can pours + * up to 100 units at once, which saves opening the screen. */ public class FuelItem extends Item { /** Units a full can holds, the original's maximum damage. */ diff --git a/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/menu/VehicleMenu.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/menu/VehicleMenu.java new file mode 100644 index 0000000..b2dba1e --- /dev/null +++ b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/menu/VehicleMenu.java @@ -0,0 +1,217 @@ +package com.jnhprojects.mcheli.menu; + +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.VehicleInventory; +import com.jnhprojects.mcheli.item.FuelItem; +import com.jnhprojects.mcheli.item.ParachuteItem; +import com.jnhprojects.mcheli.registry.ModMenus; +import com.jnhprojects.mcheli.weapon.Resupply; +import com.jnhprojects.mcheli.weapon.VehicleWeapons; +import com.jnhprojects.mcheli.weapon.WeaponSet; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.Container; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerData; +import net.minecraft.world.inventory.SimpleContainerData; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import org.jspecify.annotations.Nullable; + +/** + * The vehicle screen, {@code MCH_AircraftGuiContainer}: fuel slots, a parachute slot per ejection + * seat, reloading the weapons from the crew member's own items and the way into the cargo chest. + * + *

Only crew open it, so the client finds the vehicle as the one its player rides. The weapon + * rounds and which weapons can be reloaded right now come from the server through data slots, + * since a client only hears about the ammunition of the seat it sits in. + */ +public class VehicleMenu extends AbstractContainerMenu { + /** Weapons the screen can show; more than any vehicle in the original has. */ + public static final int MAX_WEAPONS = 32; + /** Button ids from zero up reload that weapon; this one opens the cargo chest. */ + public static final int BUTTON_CARGO = 1000; + + public static final int FUEL_X = 8; + public static final int FUEL_Y = 30; + public static final int PARACHUTE_X = 8; + public static final int PARACHUTE_Y = 66; + public static final int INVENTORY_Y = 102; + + private static final int DATA_RELOADABLE = MAX_WEAPONS; + private static final int DATA_SIZE = MAX_WEAPONS + 2; + + private final @Nullable AircraftEntity vehicle; + private final Container equipment; + private final ContainerData data; + private final int parachuteSlots; + private boolean supplied; + + /** The client side, from the open screen packet. */ + public VehicleMenu(int containerId, Inventory inventory) { + this(containerId, inventory, inventory.player.getVehicle() instanceof AircraftEntity v ? v : null, + new SimpleContainer(VehicleInventory.EQUIPMENT_SLOTS), new SimpleContainerData(DATA_SIZE)); + } + + /** The server side. */ + public VehicleMenu(int containerId, Inventory inventory, AircraftEntity vehicle) { + this(containerId, inventory, vehicle, vehicle.inventory().equipment(), new ServerData(vehicle, inventory.player)); + } + + private VehicleMenu(int containerId, Inventory inventory, @Nullable AircraftEntity vehicle, Container equipment, ContainerData data) { + super(ModMenus.VEHICLE.get(), containerId); + this.vehicle = vehicle; + this.equipment = equipment; + this.data = data; + AircraftDefinition def = vehicle != null ? vehicle.definition() : null; + parachuteSlots = def != null ? VehicleInventory.parachuteSlots(def) : 0; + checkContainerSize(equipment, VehicleInventory.EQUIPMENT_SLOTS); + + for (int i = 0; i < VehicleInventory.FUEL_SLOTS; i++) { + addSlot(new Slot(equipment, i, FUEL_X + i * 18, FUEL_Y) { + @Override + public boolean mayPlace(ItemStack stack) { + return stack.getItem() instanceof FuelItem; + } + }); + } + for (int i = 0; i < VehicleInventory.PARACHUTE_SLOTS; i++) { + boolean active = i < parachuteSlots; + addSlot(new Slot(equipment, VehicleInventory.FUEL_SLOTS + i, PARACHUTE_X + i * 18, PARACHUTE_Y) { + @Override + public boolean mayPlace(ItemStack stack) { + return active && stack.getItem() instanceof ParachuteItem; + } + + @Override + public boolean isActive() { + return active; + } + }); + } + addStandardInventorySlots(inventory, 8, INVENTORY_Y); + addDataSlots(data); + } + + public @Nullable AircraftEntity vehicle() { + return vehicle; + } + + /** Parachute slots in use: one per ejection seat. */ + public int parachuteSlots() { + return parachuteSlots; + } + + /** Magazine and reserve of a weapon together, as the server last reported them. */ + public int rounds(int weapon) { + return weapon >= 0 && weapon < MAX_WEAPONS ? data.get(weapon) : 0; + } + + /** Whether the server would reload this weapon now: the vehicle is down and the items are there. */ + public boolean canReload(int weapon) { + if (weapon < 0 || weapon >= MAX_WEAPONS) { + return false; + } + int bits = (data.get(DATA_RELOADABLE) & 0xFFFF) | (data.get(DATA_RELOADABLE + 1) & 0xFFFF) << 16; + return (bits & 1 << weapon) != 0; + } + + @Override + public boolean clickMenuButton(Player player, int id) { + if (vehicle == null || !(player instanceof ServerPlayer server)) { + return false; + } + if (id == BUTTON_CARGO) { + return vehicle.openCargo(server); + } + if (id >= 0 && id < MAX_WEAPONS && Resupply.supply(server, vehicle, id)) { + supplied = true; + return true; + } + return false; + } + + @Override + public boolean stillValid(Player player) { + return vehicle != null && equipment.stillValid(player); + } + + @Override + public ItemStack quickMoveStack(Player player, int index) { + Slot slot = slots.get(index); + if (!slot.hasItem()) { + return ItemStack.EMPTY; + } + ItemStack stack = slot.getItem(); + ItemStack moved = stack.copy(); + int equipmentEnd = VehicleInventory.EQUIPMENT_SLOTS; + if (index < equipmentEnd) { + if (!moveItemStackTo(stack, equipmentEnd, slots.size(), true)) { + return ItemStack.EMPTY; + } + } else if (stack.getItem() instanceof FuelItem) { + if (!moveItemStackTo(stack, 0, VehicleInventory.FUEL_SLOTS, false)) { + return ItemStack.EMPTY; + } + } else if (stack.getItem() instanceof ParachuteItem && parachuteSlots > 0) { + int first = VehicleInventory.FUEL_SLOTS; + if (!moveItemStackTo(stack, first, first + parachuteSlots, false)) { + return ItemStack.EMPTY; + } + } else { + return ItemStack.EMPTY; + } + if (stack.isEmpty()) { + slot.setByPlayer(ItemStack.EMPTY); + } else { + slot.setChanged(); + } + return moved; + } + + /** As in the original, closing the screen after a resupply loads every magazine from its reserve. */ + @Override + public void removed(Player player) { + super.removed(player); + if (supplied && vehicle != null && vehicle.weapons() != null && !player.level().isClientSide()) { + vehicle.weapons().reloadAll(); + } + } + + /** Rounds per weapon and the reload bits, worked out on the server whenever the menu syncs. */ + private record ServerData(AircraftEntity vehicle, Player player) implements ContainerData { + @Override + public int get(int index) { + VehicleWeapons weapons = vehicle.weapons(); + if (weapons == null) { + return 0; + } + if (index < MAX_WEAPONS) { + WeaponSet set = weapons.set(index); + // Data slots travel as shorts. + return set != null ? Math.min(set.ammo() + set.restAmmo(), Short.MAX_VALUE) : 0; + } + int bits = 0; + if (player instanceof ServerPlayer server) { + for (int i = 0; i < Math.min(weapons.sets().size(), MAX_WEAPONS); i++) { + if (Resupply.canSupply(server, vehicle, i)) { + bits |= 1 << i; + } + } + } + return index == DATA_RELOADABLE ? bits & 0xFFFF : bits >>> 16; + } + + @Override + public void set(int index, int value) { + } + + @Override + public int getCount() { + return DATA_SIZE; + } + } +} diff --git a/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/network/CrewActionPacket.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/network/CrewActionPacket.java index 0ec1652..84f1f3e 100644 --- a/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/network/CrewActionPacket.java +++ b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/network/CrewActionPacket.java @@ -2,7 +2,6 @@ import com.jnhprojects.mcheli.entity.AircraftEntity; import com.jnhprojects.mcheli.entity.PlaneEntity; -import com.jnhprojects.mcheli.weapon.Resupply; import com.jnhprojects.mcheli.weapon.VehicleWeapons; import net.minecraft.network.FriendlyByteBuf; import net.minecraft.network.RegistryFriendlyByteBuf; @@ -44,8 +43,8 @@ public enum Action { WEAPON_MODE, /** Release flares. Pilot and first crew seat. */ FLARE, - /** Pay the selected weapon's {@code Item} cost from the inventory for one resupply. */ - RESUPPLY, + /** Open the vehicle screen: fuel and parachute slots, reloading, the way to the cargo chest. */ + VEHICLE_SCREEN, /** Hand the controls to the autopilot and man the weapons, or take them back. Pilot only. */ GUNNER_MODE, /** Turn night vision on for the sender, in vehicles with {@code EnableNightVision}. */ @@ -135,7 +134,7 @@ public static void handle(CrewActionPacket msg, ServerPlayer sender) { weapons.flares.use(); } } - case RESUPPLY -> Resupply.fromInventory(sender, vehicle); + case VEHICLE_SCREEN -> vehicle.openScreen(sender); case NIGHT_VISION_ON, NIGHT_VISION_OFF -> vehicle.setNightVision(sender, msg.action == Action.NIGHT_VISION_ON); } } diff --git a/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/network/ModNetwork.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/network/ModNetwork.java index 8630baa..d8eb5e9 100644 --- a/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/network/ModNetwork.java +++ b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/network/ModNetwork.java @@ -21,7 +21,7 @@ */ public final class ModNetwork { /** Bump whenever a packet layout changes. Clients and servers must match exactly. */ - public static final int PROTOCOL_VERSION = 7; + public static final int PROTOCOL_VERSION = 8; /** Client to server. The handler gets the sending player. */ public static final List> SERVERBOUND = List.of( diff --git a/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/registry/ModMenus.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/registry/ModMenus.java new file mode 100644 index 0000000..58ecce9 --- /dev/null +++ b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/registry/ModMenus.java @@ -0,0 +1,21 @@ +package com.jnhprojects.mcheli.registry; + +import com.jnhprojects.mcheli.menu.VehicleMenu; +import com.jnhprojects.mcheli.platform.Registered; +import com.jnhprojects.mcheli.platform.Registration; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.flag.FeatureFlags; +import net.minecraft.world.inventory.MenuType; + +/** Our screens with slots. The cargo chest uses vanilla's chest menus and needs no entry here. */ +public final class ModMenus { + public static final Registered> VEHICLE = Registration.register(Registries.MENU, "vehicle", + key -> new MenuType<>(VehicleMenu::new, FeatureFlags.VANILLA_SET)); + + private ModMenus() { + } + + /** Declares the entries; the loader module registers them. */ + public static void init() { + } +} diff --git a/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/weapon/Resupply.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/weapon/Resupply.java index 44df1ac..533f5fb 100644 --- a/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/weapon/Resupply.java +++ b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/weapon/Resupply.java @@ -10,30 +10,35 @@ import net.minecraft.world.item.Items; /** - * Resupplying a weapon from the crew member's inventory, the {@code supplyAmmo} path of the - * original. There the vehicle's inventory screen did it; until that screen exists a key does. - * One supply costs the weapon's {@code Item} lines and adds {@code SuppliedNum} rounds. + * Resupplying a weapon from a crew member's own items, the {@code supplyAmmo} path of the + * original's vehicle screen. One supply costs the weapon's {@code Item} lines and adds + * {@code SuppliedNum} rounds; it only works while the vehicle stands on something. */ public final class Resupply { private Resupply() { } - public static boolean fromInventory(ServerPlayer player, AircraftEntity vehicle) { + /** {@code canPlayerSupplyAmmo}: whether {@link #supply} would do anything right now. */ + public static boolean canSupply(ServerPlayer player, AircraftEntity vehicle, int weapon) { VehicleWeapons weapons = vehicle.weapons(); - int seat = vehicle.seatOf(player); - WeaponSet set = weapons != null ? weapons.current(seat) : null; - if (set == null || vehicle.isDestroyed()) { + WeaponSet set = weapons != null ? weapons.set(weapon) : null; + if (set == null || vehicle.isDestroyed() || player.getVehicle() != vehicle || !vehicle.canSupply()) { return false; } - WeaponDefinition w = set.definition(); if (set.ammo() + set.restAmmo() >= set.maxAmmo()) { return false; } + return player.getAbilities().instabuild || canPay(player.getInventory(), set.definition()); + } + + /** Pays for and adds one supply of {@code weapon}. Server only. */ + public static boolean supply(ServerPlayer player, AircraftEntity vehicle, int weapon) { + if (!canSupply(player, vehicle, weapon)) { + return false; + } + WeaponSet set = vehicle.weapons().set(weapon); if (!player.getAbilities().instabuild) { - if (!canPay(player.getInventory(), w)) { - return false; - } - pay(player.getInventory(), w); + pay(player.getInventory(), set.definition()); } set.supply(); if (set.ammo() <= 0) { diff --git a/minecraft-26.3/src/main/resources/assets/mcheli/post_effect/thermal.json b/minecraft-26.3/src/main/resources/assets/mcheli/post_effect/thermal.json new file mode 100644 index 0000000..593e905 --- /dev/null +++ b/minecraft-26.3/src/main/resources/assets/mcheli/post_effect/thermal.json @@ -0,0 +1,38 @@ +{ + "targets": { + "swap": {} + }, + "passes": [ + { + "vertex_shader": "minecraft:core/screenquad", + "fragment_shader": "mcheli:post/thermal", + "inputs": [ + { + "sampler_name": "In", + "target": "minecraft:main" + } + ], + "output": "swap" + }, + { + "vertex_shader": "minecraft:core/screenquad", + "fragment_shader": "minecraft:post/blit", + "inputs": [ + { + "sampler_name": "In", + "target": "swap" + } + ], + "uniforms": { + "BlitConfig": [ + { + "name": "ColorModulate", + "type": "vec4", + "value": [ 1.0, 1.0, 1.0, 1.0 ] + } + ] + }, + "output": "minecraft:main" + } + ] +} diff --git a/minecraft-26.3/src/main/resources/assets/mcheli/shaders/post/thermal.fsh b/minecraft-26.3/src/main/resources/assets/mcheli/shaders/post/thermal.fsh new file mode 100644 index 0000000..83e3065 --- /dev/null +++ b/minecraft-26.3/src/main/resources/assets/mcheli/shaders/post/thermal.fsh @@ -0,0 +1,38 @@ +#version 330 +#extension GL_ARB_separate_shader_objects : require + +// Thermal view, white hot. A post shader only sees colours, so heat is faked: bright and warm +// colours (fire, lava, skin, lamps) read hot, the sky and water cold, and outlines are drawn +// where the brightness changes sharply, like the original's "pencil" shader. + +uniform sampler2D InSampler; + +layout(location = 0) in vec2 texCoord; + +layout(std140) uniform SamplerInfo { + vec2 OutSize; + vec2 InSize; +}; + +layout(location = 0) out vec4 fragColor; + +const vec3 Gray = vec3(0.3, 0.59, 0.11); + +float heat(vec3 c) { + float luma = dot(c, Gray); + float warmth = clamp(c.r - c.b, 0.0, 1.0); + return clamp(luma * 0.7 + warmth * 0.6, 0.0, 1.0); +} + +void main() { + vec2 oneTexel = 1.0 / InSize; + float c = heat(texture(InSampler, texCoord).rgb); + float l = heat(texture(InSampler, texCoord - vec2(oneTexel.x, 0.0)).rgb); + float r = heat(texture(InSampler, texCoord + vec2(oneTexel.x, 0.0)).rgb); + float u = heat(texture(InSampler, texCoord - vec2(0.0, oneTexel.y)).rgb); + float d = heat(texture(InSampler, texCoord + vec2(0.0, oneTexel.y)).rgb); + float edge = clamp(abs(r - l) + abs(d - u), 0.0, 1.0); + + float value = smoothstep(0.08, 0.85, c) + edge * 0.6; + fragColor = vec4(vec3(clamp(value, 0.0, 1.0)), 1.0); +}