1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213 |
#include "gfCPurchaseActionSlotFill.h"
#include <fsAppCore/src/fsCAppCoreMain.h>
#include <fsAppCore/src/fsCEntity.h>
#include <fsAppCore/src/fsCEntityFactory.h>
#include <fsAppCore/fsIScene.h>
#include <fsCore/fsIFile.h>
#include <fsCore/fsSColour.h>
#include <fsCore/src/fsCResourceName.h>
#include <fsRenderer/src/fsCBrushManager.h>
#include <fsRenderer/src/components/fsCSpriteComponent.h>
#include <fsTileMap/fsITileMap.h>
#include <fsTileMap/fsIObject.h>
#include <fsTileMap/fsILayer.h>
#include <fsTileMap/src/fsCView.h>
#include <gfCatalogue/src/gfCCatalogueItem.h>
#include "gfCParkGuestViewComponent.h"
#include "gfCParkLevelComponent.h"
#include "gfCParkSlotStaffComponent.h"
#include "sim/gfCParkDaySim.h"
namespace
{
// Buildings draw larger than their ground footprint (presentation only - the
// sim's footprint is unaffected).
const fsF32 gBuildingDrawScale = 2.0f;
}
fsBool gfCPurchaseActionSlotFill::buildingPresentationPlace(fsNTileMap::fsCView* pView, const fsStr& pSlotName,
const fsStr& pBuildingImage)
{
if (!pView)
{
return false;
}
fsNTileMap::fsIObjectRef slotObj;
for (fsS32 layer = 0; layer < pView->mMap->layerCountGet() && !slotObj; ++layer)
{
if (fsNTileMap::fsILayer::eType::OBJECT != pView->mMap->layerTypeGet(layer))
{
continue;
}
for (fsNTileMap::fsIObjectRef const& obj : pView->mMap->triggersGet(layer))
{
if (obj->nameGet() == pSlotName)
{<--- Consider using std::find_if algorithm instead of a raw loop.
slotObj = obj;
break;
}
}
}
if (!slotObj)
{
return false;
}
// Position via the same map-pixel -> view-local transform as the backdrop
// and markers; raw posGet() would land wrong once the view is scaled.
const fsPoint slotPos = slotObj->posGet();
const fsPoint topLeft = pView->mapPixelToLocal(slotPos);
const fsPoint botRight = pView->mapPixelToLocal(
fsPoint(slotPos.x + slotObj->widthGet(), slotPos.y + slotObj->heightGet()));
fsCEntity* const placedEnt = pView->entityFactoryGet()->emptyEntityCreate(pSlotName + "Building");
placedEnt->componentAdd(fsCSpriteComponent::mTypeId);
auto* const sprt = placedEnt->spriteComponentGet();
// The catalogue's "large" image. Existence-check first: brushSet()
// dereferences the brush, so a missing file crashes. No art -> opaque block
// (which reads as "built" vs the translucent empty marker).
const fsCResourceName buildingRes(pBuildingImage, fsCResourceName::eRES_LOCATION_ASSETS);
if (!pBuildingImage.emptyGet() && fsIFile::fileExistsGet(buildingRes))
{
// No hit map: fsCSprite::hitTest samples it at native size but buildings
// are drawn scaled, so tapping would miss. Rect test is right here anyway.
sprt->brushSet(fsCBrushManager::instanceGet()->get(buildingRes, false));
}
else
{
sprt->colourSet(fsSColour4B(228, 216, 194, 255));
}
// Width = footprint x gBuildingDrawScale; height follows the art's aspect
// (no squashing); bottom-anchored to the slot's lower edge.
const fsS32 footprintW = botRight.x - topLeft.x;
const fsS32 footprintH = botRight.y - topLeft.y;
const fsS32 nativeW = sprt->widthGet();
const fsS32 nativeH = sprt->heightGet();
const fsS32 drawW = static_cast<fsS32>(footprintW * gBuildingDrawScale);
fsS32 drawH = static_cast<fsS32>(footprintH * gBuildingDrawScale);
if (nativeW > 0 && nativeH > 0)
{
drawH = static_cast<fsS32>(drawW * (static_cast<fsF32>(nativeH) / static_cast<fsF32>(nativeW)));
}
sprt->widthSet(drawW);
sprt->heightSet(drawH);
// Centre-anchored, so bottom-align = half the height above the lower edge.
placedEnt->posSet(fsPoint((topLeft.x + botRight.x) / 2, botRight.y - drawH / 2));
pView->childAdd(placedEnt);
// The building replaces the greybox "build here" marker.
if (fsCEntity* const slotMarker = pView->childFind(pSlotName + "Marker"))
{
slotMarker->destroy();
}
// Children render in insertion order, so re-front the guest layer to keep
// guests above a building bought mid-day.
if (fsCEntity* const guestLayer = pView->childFind(gfCParkGuestViewComponent::mGuestLayerName))
{
guestLayer->bringToFrontOfSiblings();
}
// Tappable to deploy/recall staff (see gfCParkSlotStaffComponent).
placedEnt->componentAdd<gfCParkSlotStaffComponent>()->slotNameSet(pSlotName);
return true;
}
fsBool gfCPurchaseActionSlotFill::purchaseHandle(const gfCCatalogueItem* pShopItem)
{
auto* const view = dynamic_cast<fsNTileMap::fsCView*>(
fsCAppCoreMain::instanceGet()->sceneGet()->childFind("tilemapView"));
if (!view)
{
return false;
}
// Catalogue keys are case-sensitive and authored all-lowercase.
const fsStr slotName = pShopItem->paramGet("slotname");
// Resolve the sim up front so affordability is checked before anything is
// placed - a rejected purchase must leave nothing behind.
gfCParkLevelComponent* parkLevel = nullptr;
gfCParkDaySim* sim = nullptr;
if (fsCEntity* const levelEnt =
fsCAppCoreMain::instanceGet()->sceneGet()->firstEntityWithComponentsOfFamilyType<gfCParkLevelComponent>())
{
parkLevel = levelEnt->firstComponentOfFamilyType<gfCParkLevelComponent>();
if (parkLevel)
{
sim = parkLevel->simGet();
}
}
// Returning false also keeps the catalogue open (it only dismisses on
// success), so a rejection is visible.
const fsS32 itemCost = pShopItem->paramGet("itemCost").toS32();
if (itemCost > 0 && (!sim || sim->economyGet().coinsGet() < itemCost))
{
return false;
}
const fsStr buildingImage = pShopItem->paramGet("imageFileName");
if (!buildingPresentationPlace(view, slotName, buildingImage))
{
// No such slot in the map - leave the purchase unmade.
return false;
}
// Unlock in the sim too, and take payment (already checked affordable).
if (sim)
{
if (itemCost > 0)
{
sim->economyGet().coinsSet(sim->economyGet().coinsGet() - itemCost);
}
sim->slotUnlock(slotName);
}
// Slots register from the TMX on a placeholder service time; apply the
// building's authored value (seconds) now it's known.
const fsS32 serviceTimeMs = pShopItem->paramGet("servicetime").toS32() * 1000;
if (sim && serviceTimeMs > 0)
{
sim->slotServiceTimeMsSet(slotName, serviceTimeMs);
}
// Same for the authored queue length.
const fsS32 maxQueueLength = pShopItem->paramGet("queuelength").toS32();
if (sim && maxQueueLength > 0)
{
sim->slotMaxQueueLengthSet(slotName, maxQueueLength);
}
// Remembered outside the sim so it's re-applied each day (the building
// persists across the rollover) and re-placed visually on Continue.
if (parkLevel)
{
parkLevel->slotPurchasedRecord(
slotName, serviceTimeMs, maxQueueLength, buildingImage,
pShopItem->paramGet("visitorquota").toS32(),
pShopItem->paramGet("visitorappeal").toS32());
}
return true;
}
gfCPurchaseActionSlotFill::gfCPurchaseActionSlotFill()
{
}
gfCPurchaseActionSlotFill::~gfCPurchaseActionSlotFill()
{
}
|