载入中...
搜索中...
未找到
HouseGenerator.cpp
浏览该文件的文档.
3
4#include <algorithm>
5#include <random>
6#include <tuple>
7#include <unordered_map>
8#include <unordered_set>
9
10namespace eve::housegen {
11namespace {
12
13const HouseComponent *pick(const std::vector<const HouseComponent *> &choices, std::mt19937 &rng) {
14 if (choices.empty()) return nullptr;
15 int total = 0;
16 for (auto *c : choices) total += c->weight;
17 std::uniform_int_distribution<int> roll(1, total);
18 int target = roll(rng);
19 for (auto *c : choices) { target -= c->weight; if (target <= 0) return c; }
20 return choices.back();
21}
22
23bool has(const std::vector<const HouseComponent *> &values) { return !values.empty(); }
24
25bool isWindowComponent(const HouseComponent *component) {
26 if (!component) return false;
27 if (component->id.find("window") != std::string::npos) return true;
28 return std::find(component->tags.begin(), component->tags.end(), "window") != component->tags.end();
29}
30
31std::vector<const HouseComponent *> facadeVariant(const std::vector<const HouseComponent *> &choices,
32 bool wantWindow) {
33 bool hasWindow = false, hasSolid = false;
34 for (const auto *choice : choices) {
35 if (isWindowComponent(choice)) hasWindow = true;
36 else hasSolid = true;
37 }
38 if (!hasWindow || !hasSolid) return choices;
39 std::vector<const HouseComponent *> filtered;
40 for (const auto *choice : choices)
41 if (isWindowComponent(choice) == wantWindow) filtered.push_back(choice);
42 return filtered;
43}
44
45bool oneOf(const std::string &value, std::initializer_list<const char *> options) {
46 for (const char *option : options) if (value == option) return true;
47 return false;
48}
49
50SocketDirection directionFromName(const std::string &name) {
51 if (name == "east") return SocketDirection::East;
52 if (name == "south") return SocketDirection::South;
53 if (name == "west") return SocketDirection::West;
55}
56
57std::string directionName(SocketDirection direction) {
58 if (direction == SocketDirection::East) return "east";
59 if (direction == SocketDirection::South) return "south";
60 if (direction == SocketDirection::West) return "west";
61 return "north";
62}
63
64int directionRotation(SocketDirection direction) {
65 if (direction == SocketDirection::East) return 90;
66 if (direction == SocketDirection::South) return 180;
67 if (direction == SocketDirection::West) return 270;
68 return 0;
69}
70
71std::vector<uint8_t> footprintMask(const std::string &shape, int width, int depth, int inset) {
72 std::vector<uint8_t> mask(size_t(width * depth), 0);
73 const int minX = inset, minY = inset, maxX = width - 1 - inset, maxY = depth - 1 - inset;
74 if (minX > maxX || minY > maxY) return mask;
75 const int spanX = maxX - minX + 1, spanY = maxY - minY + 1;
76 const int cutX = minX + std::max(1, spanX / 2) - 1;
77 const int cutY = minY + std::max(1, spanY / 2) - 1;
78 const int stemInset = spanX >= 5 ? 1 : 0;
79 for (int y = minY; y <= maxY; ++y) for (int x = minX; x <= maxX; ++x) {
80 bool active = true;
81 if (shape == "l_shape") active = !(x > cutX && y > cutY);
82 else if (shape == "t_shape") active = y <= cutY || (x >= minX + stemInset && x <= maxX - stemInset);
83 if (active) mask[size_t(y * width + x)] = 1;
84 }
85 return mask;
86}
87
88bool active(const std::vector<uint8_t> &mask, int width, int depth, int x, int y) {
89 return x >= 0 && y >= 0 && x < width && y < depth && mask[size_t(y * width + x)] != 0;
90}
91
92SocketDirection rotate(SocketDirection direction, int degrees) {
93 if (direction == SocketDirection::Up || direction == SocketDirection::Down) return direction;
94 int side = direction == SocketDirection::North ? 0 : direction == SocketDirection::East ? 1 :
95 direction == SocketDirection::South ? 2 : 3;
96 side = (side + degrees / 90) % 4;
97 return side == 0 ? SocketDirection::North : side == 1 ? SocketDirection::East :
99}
100
101std::vector<const HouseComponent *> compatibleOnFace(const std::vector<const HouseComponent *> &choices,
102 SocketDirection face, int rotation) {
103 std::vector<const HouseComponent *> out;
104 for (const auto *c : choices) {
105 // Components without sockets are intentionally wildcard-compatible. This preserves
106 // compatibility with small legacy kits while new player kits opt into strict sockets.
107 if (c->sockets.empty()) { out.push_back(c); continue; }
108 for (const auto &socket : c->sockets) {
109 if (rotate(socket.direction, rotation) == face && !socket.type.empty()) {
110 out.push_back(c);
111 break;
112 }
113 }
114 }
115 return out;
116}
117
118} // namespace
119
120bool HouseGenerator::generate(const HouseRequest &r, HouseLayout &out, std::string *error) const {
121 out.clear();
122 if (!library_) { if (error) *error = "no component library"; return false; }
123 if (r.width < 3 || r.depth < 3 || r.floors < 1 || r.maxAttempts < 1) {
124 if (error) *error = "house needs a 3x3 plot, at least one floor and one attempt";
125 return false;
126 }
127 if (!oneOf(r.footprint, {"auto", "rectangle", "l_shape", "t_shape"}) ||
128 !oneOf(r.roof, {"auto", "gable", "flat", "shed"}) ||
129 !oneOf(r.entrance, {"auto", "north", "east", "south", "west"})) {
130 if (error) *error = "unsupported footprint, roof or entrance mode";
131 return false;
132 }
133 const auto foundation = library_->byCategory("foundation", r.style);
134 const auto floor = library_->byCategory("floor", r.style);
135 const auto wall = library_->byCategory("wall", r.style);
136 const auto door = library_->byCategory("door", r.style);
137 const auto roof = library_->byCategory("roof", r.style);
138 if (!has(foundation) || !has(floor) || !has(wall) || !has(door) || !has(roof)) {
139 if (error) *error = "library needs foundation, floor, wall, door and roof categories";
140 return false;
141 }
142
143 std::mt19937 rng(r.seed);
144 out.seed = r.seed;
145 out.moduleSize = r.moduleSize;
146 out.floorHeight = r.floorHeight;
147 static constexpr const char *shapes[] = {"rectangle", "l_shape", "t_shape"};
148 static constexpr const char *roofs[] = {"gable", "flat", "shed"};
151 out.footprintStyle = r.footprint == "auto" ? shapes[rng() % 3] : r.footprint;
152 out.roofStyle = r.roof == "auto" ? roofs[rng() % 3] : r.roof;
153 const SocketDirection entranceDirection =
154 r.entrance == "auto" ? sides[rng() % 4] : directionFromName(r.entrance);
155 out.entranceSide = directionName(entranceDirection);
156
157 // Grammar pass: construct a connected footprint mask, then emit one module per exposed face.
158 // Upper masks are monotonically inset, preserving a direct vertical support chain.
159 int inset = 0;
160 std::vector<uint8_t> previousMask;
161 for (int z = 0; z < r.floors; ++z) {
162 // Upper floors may step inward, but can never expand again above an inset floor. This
163 // gives every floor cell a direct support chain to the foundation.
164 if (z > 0 && inset == 0 && r.width > 4 && r.depth > 4 && (rng() & 3u) == 0u)
165 inset = 1;
166 const auto mask = footprintMask(out.footprintStyle, r.width, r.depth, inset);
167 // Any lower-floor cell not covered by this floor becomes a roof terrace/canopy at the
168 // current level. Thus every floor cell is covered by either another floor or a roof.
169 if (!previousMask.empty()) {
170 for (int y = 0; y < r.depth; ++y) for (int x = 0; x < r.width; ++x) {
171 if (active(previousMask, r.width, r.depth, x, y) &&
172 !active(mask, r.width, r.depth, x, y))
173 out.instances.push_back({pick(roof, rng)->id, x, y, z, 0});
174 }
175 }
176 int minX = r.width, minY = r.depth, maxX = -1, maxY = -1;
177 for (int y = 0; y < r.depth; ++y) for (int x = 0; x < r.width; ++x) if (active(mask, r.width, r.depth, x, y)) {
178 minX = std::min(minX, x); minY = std::min(minY, y);
179 maxX = std::max(maxX, x); maxY = std::max(maxY, y);
180 if (z == 0) out.instances.push_back({pick(foundation, rng)->id, x, y, z, 0});
181 out.instances.push_back({pick(floor, rng)->id, x, y, z, 0});
182 }
183 if (maxX < minX || maxY < minY) { if (error) *error = "footprint collapsed after inset"; return false; }
184 out.rooms.push_back({z == 0 ? "living" : "upper", minX, minY,
185 maxX - minX + 1, maxY - minY + 1});
186
187 using Face = std::tuple<int, int, SocketDirection>;
188 std::vector<Face> faces;
189 std::vector<Face> entranceCandidates;
190 std::unordered_map<int, int> facesPerCell;
191 for (int y = 0; y < r.depth; ++y) for (int x = 0; x < r.width; ++x) {
192 if (!active(mask, r.width, r.depth, x, y)) continue;
195 const int dx[] = {0, 1, 0, -1};
196 const int dy[] = {-1, 0, 1, 0};
197 for (int side = 0; side < 4; ++side) if (!active(mask, r.width, r.depth, x + dx[side], y + dy[side])) {
198 Face face{x, y, directions[side]};
199 faces.push_back(face);
200 ++facesPerCell[y * r.width + x];
201 if (z == 0 && directions[side] == entranceDirection) entranceCandidates.push_back(face);
202 }
203 }
204 std::sort(entranceCandidates.begin(), entranceCandidates.end(), [](const Face &a, const Face &b) {
205 return std::tie(std::get<1>(a), std::get<0>(a)) < std::tie(std::get<1>(b), std::get<0>(b));
206 });
207 Face entranceFace{-1, -1, entranceDirection};
208 if (z == 0 && !entranceCandidates.empty()) entranceFace = entranceCandidates[entranceCandidates.size() / 2];
209 for (const auto &faceData : faces) {
210 const auto [x, y, face] = faceData;
211 const bool isEntrance = z == 0 && faceData == entranceFace;
212 const int rotation = directionRotation(face);
213 auto choices = compatibleOnFace(isEntrance ? door : wall, face, rotation);
214 if (!isEntrance) {
215 const bool corner = facesPerCell[y * r.width + x] > 1;
216 const bool besideEntrance = z == 0 &&
217 std::abs(x - std::get<0>(entranceFace)) + std::abs(y - std::get<1>(entranceFace)) <= 1;
218 const int facadeAxis = (face == SocketDirection::North || face == SocketDirection::South) ? x : y;
219 const bool rhythmicWindow = ((facadeAxis + int(r.seed & 1u)) & 1) == 0;
220 choices = facadeVariant(choices, !corner && !besideEntrance && rhythmicWindow);
221 }
222 const auto *selected = pick(choices, rng);
223 if (!selected) {
224 if (error) *error = "no socket-compatible " + std::string(isEntrance ? "door" : "wall") + " component";
225 return false;
226 }
227 out.instances.push_back({selected->id, x, y, z, rotation});
228 }
229 if (z + 1 == r.floors) {
230 for (int y = 0; y < r.depth; ++y) for (int x = 0; x < r.width; ++x)
231 if (active(mask, r.width, r.depth, x, y)) out.instances.push_back({pick(roof, rng)->id, x, y, z + 1, 0});
232 }
233 previousMask = mask;
234 }
235 if (!r.requiredRooms.empty() && r.requiredRooms.front() != "living")
236 out.diagnostics.push_back("requested room types beyond living/upper are approximated by the base grammar");
237 return out.validate(*library_, error);
238}
239
240} // namespace eve::housegen
bool active
Definition CardTypes.cpp:34
float degrees
Definition CardTypes.cpp:33
std::string value
std::map< std::string, Var > values
int y
Definition Grass.cpp:135
int z
Definition Grass.cpp:135
float u
Definition Grass.cpp:234
int x
Definition Grass.cpp:135
std::string error
float depth
uint32_t a
uint32_t b
uint32_t c
int width
const char * name
Definition RockMesh.cpp:21
int sides
Definition TreeMesh.cpp:204
std::vector< const HouseComponent * > byCategory(const std::string &category, const std::string &style={}) const
按分类(可选风格)查询组件。
bool generate(const HouseRequest &request, HouseLayout &out, std::string *error=nullptr) const
生成布局;失败返回 false 并写入 error。
一次生成的房屋布局:实例 + 房间 + 元信息,可 JSON 序列化 / 实例化。
Definition HouseLayout.h:16
std::string footprintStyle
布局风格结果。
Definition HouseLayout.h:23
std::vector< HouseRoom > rooms
Definition HouseLayout.h:28
float moduleSize
生成参数回显。
Definition HouseLayout.h:20
void clear()
清空布局。
std::vector< HouseInstance > instances
组件实例 / 房间 / 诊断信息。
Definition HouseLayout.h:27
SocketDirection
组件连接点方向(上下 + 四向)。
一次房屋生成请求(参数集)。
int maxAttempts
生成重试上限。
std::string footprint
auto | rectangle | l_shape | t_shape。
std::string roof
auto | gable | flat | shed。
std::string entrance
auto | north | east | south | west。
int width
占地尺寸(格)与层数。
float moduleSize
模块尺寸 / 层高(世界单位)。