diff --git a/internal/world/ruined_portal_piece.go b/internal/world/ruined_portal_piece.go index 0bf6a12..71a9e2c 100644 --- a/internal/world/ruined_portal_piece.go +++ b/internal/world/ruined_portal_piece.go @@ -1,6 +1,7 @@ package world import ( + "strings" "sync" "regionio/internal/worldgen" @@ -86,29 +87,85 @@ func PlaceRuinedPortalPiece(region *decorationRegion, stub *RuinedPortalStub) er return region.setBlock(x, y, z, state) } + orientState := func(state uint16) uint16 { + if mirror == "none" && stub.Rotation == 0 { + return state + } + s, ok := stateByID(state) + if !ok { + return state + } + facing, hasFacing := s.Properties["facing"] + if !hasFacing { + return state + } + switch facing { + case "north", "east", "south", "west": + default: + return state + } + steps := stub.Rotation + if mirror == "front_back" { + switch facing { + case "east": + facing = "west" + case "west": + facing = "east" + } + } else if mirror == "left_right" { + switch facing { + case "north": + facing = "south" + case "south": + facing = "north" + } + } + for i := 0; i < steps; i++ { + switch facing { + case "north": + facing = "east" + case "east": + facing = "south" + case "south": + facing = "west" + case "west": + facing = "north" + } + } + props := make(map[string]string, len(s.Properties)) + for k, v := range s.Properties { + props[k] = v + } + props["facing"] = facing + if id, resolved := nameToStateID(s.Name, props); resolved { + return id + } + return state + } + processState := func(x, y, z int, localPos [3]int, state uint16) uint16 { seed := worldgen.MthGetSeed(x, y, z) roll := func(p float32) bool { r := worldgen.NewLegacy(seed) return r.NextFloat() < p } - switch state { - case ruinedGoldID: + switch { + case state == ruinedGoldID: if roll(0.3) { return ruinedAirID } - case ruinedLavaID: + case state == ruinedLavaID: switch { case stub.Cold: return ruinedNetherrack case roll(0.2): return ruinedMagmaID } - case ruinedNetherrack: + case state == ruinedNetherrack: if roll(0.07) { return ruinedMagmaID } - case stoneBricksID, stoneID, chiseledStoneBricks: + case isStoneFamilyForAge(state): r := worldgen.NewLegacy(seed) if r.NextFloat() >= 0.5 { break @@ -117,7 +174,28 @@ func PlaceRuinedPortalPiece(region *decorationRegion, stub *RuinedPortalStub) er return mossyStoneBricksID } return crackedStoneBricksID - case ruinedObsidianID: + case isStairState(state): + r := worldgen.NewLegacy(seed) + if r.NextFloat() >= 0.5 { + break + } + mossyStairs := withProps("minecraft:mossy_stone_brick_stairs", state) + mossySlab := mustState("minecraft:mossy_stone_brick_slab", nil) + nonMossy := []uint16{mustState("minecraft:stone_slab", nil), mustState("minecraft:stone_brick_slab", nil)} + if r.NextFloat() < stub.Mossiness { + pick := int(r.NextIntN(2)) + if pick == 0 { + return mossyStairs + } + return mossySlab + } + return nonMossy[pick2(r)] + case isSlabState(state): + r := worldgen.NewLegacy(seed) + if r.NextFloat() < stub.Mossiness { + return withProps("minecraft:mossy_stone_brick_slab", state) + } + case state == ruinedObsidianID: if roll(0.15) { return cryingObsidianID } @@ -126,6 +204,7 @@ func PlaceRuinedPortalPiece(region *decorationRegion, stub *RuinedPortalStub) er } // Two passes like buildInfoList: solids land before any template air. + var airLocals [][3]int for _, passAir := range []bool{false, true} { for _, b := range blocks { isAirLocal := b.State == ruinedAirID || b.State == ruinedCaveAirID @@ -134,7 +213,15 @@ func PlaceRuinedPortalPiece(region *decorationRegion, stub *RuinedPortalStub) er } p := worldgen.TransformBlockPos(b.Pos, mirror, stub.Rotation, pivot) x, y, z := stub.X+p[0], stub.Y+p[1], stub.Z+p[2] + if passAir { + airLocals = append(airLocals, b.Pos) + if !isLavaState(region.getBlock(x, y, z)) { + placeCell(b.Pos, ruinedAirID) + } + continue + } final := processState(x, y, z, b.Pos, b.State) + final = orientState(final) // LavaSubmerged: a template block landing in existing lava keeps // the lava unless the template itself brings lava or magma. if final != ruinedLavaID && final != ruinedMagmaID && isLavaState(region.getBlock(x, y, z)) { @@ -144,10 +231,64 @@ func PlaceRuinedPortalPiece(region *decorationRegion, stub *RuinedPortalStub) er } } + floodWaterIntoAir(region, airLocals, mirror, stub.Rotation, pivot, stub) + addNetherrackDripColumnsBelowPortal(region, stub, blocks, size, mirror, pivot) return nil } +// floodWaterIntoAir replicates vanilla's scheduled fluid ticks over a freshly +// placed air pocket: every pocket cell connected to surrounding water floods, +// while cells sealed off by solids stay air. +func floodWaterIntoAir(region *decorationRegion, airLocals [][3]int, mirror string, rotation int, pivot [3]int, stub *RuinedPortalStub) { + type cell struct{ x, y, z int } + templateAir := map[[3]int]bool{} + var airCells []cell + for _, local := range airLocals { + p := worldgen.TransformBlockPos(local, mirror, rotation, pivot) + w := cell{stub.X + p[0], stub.Y + p[1], stub.Z + p[2]} + if !monsterIsAir(region.getBlock(w.x, w.y, w.z)) { + continue + } + templateAir[[3]int{w.x, w.y, w.z}] = true + airCells = append(airCells, w) + } + + flooded := map[[3]int]bool{} + queue := []cell{} + for _, c := range airCells { + key := [3]int{c.x, c.y, c.z} + if flooded[key] { + continue + } + touchesWater := false + for _, o := range [][3]int{{1, 0, 0}, {-1, 0, 0}, {0, 1, 0}, {0, -1, 0}, {0, 0, 1}, {0, 0, -1}} { + if isWaterState(region.getBlock(c.x+o[0], c.y+o[1], c.z+o[2])) { + touchesWater = true + break + } + } + if touchesWater { + queue = append(queue, c) + flooded[key] = true + } + } + for len(queue) > 0 { + c := queue[0] + queue = queue[1:] + region.setBlock(c.x, c.y, c.z, StateWater) + for _, o := range [][3]int{{1, 0, 0}, {-1, 0, 0}, {0, 1, 0}, {0, -1, 0}, {0, 0, 1}, {0, 0, -1}} { + n := cell{c.x + o[0], c.y + o[1], c.z + o[2]} + key := [3]int{n.x, n.y, n.z} + if flooded[key] || !templateAir[key] || !monsterIsAir(region.getBlock(n.x, n.y, n.z)) { + continue + } + flooded[key] = true + queue = append(queue, n) + } + } +} + func addNetherrackDripColumnsBelowPortal(region *decorationRegion, stub *RuinedPortalStub, blocks []worldgen.TemplateBlockInfo, size [3]int, mirror string, pivot [3]int) { minX, minY, minZ, maxX, _, maxZ := boundingBoxOf(size, mirror, stub.Rotation, pivot, stub.X, stub.Z) for x := minX + 1; x < maxX; x++ { @@ -190,3 +331,47 @@ func stubColdAt(region *decorationRegion, x, z int) bool { return false } +func pick2(r *worldgen.Legacy) int { return int(r.NextIntN(2)) } + +func mustState(name string, props map[string]string) uint16 { + id, ok := nameToStateID(name, props) + if !ok { + panic("world: missing state: " + name) + } + return id +} + +func withProps(name string, from uint16) uint16 { + if s, ok := stateByID(from); ok { + if id, resolved := nameToStateID(name, s.Properties); resolved { + return id + } + } + return 0 +} + +func isStairState(state uint16) bool { + if s, ok := stateByID(state); ok { + return strings.HasSuffix(s.Name, "_stairs") + } + return false +} + +func isSlabState(state uint16) bool { + if s, ok := stateByID(state); ok { + return strings.HasSuffix(s.Name, "_slab") + } + return false +} + +// isStoneFamilyForAge covers BlockAgeProcessor's full-block branch: +// stone_bricks, stone, and chiseled_stone_bricks. +func isStoneFamilyForAge(state uint16) bool { + switch state { + case stoneBricksID, stoneID, chiseledStoneBricks: + return true + } + return false +} + +