Skip to content

Commit 113bc0b

Browse files
Nanocom Boosters and Authentic FM-Taro Scale Logic (#315)
* Groundwork for nanocom boosters * The item use handler now has a switch for multiple item types (currently gumballs, and a stub for boosters) * All item types are now checked for expiration, not just vehicles * implement nanocom booster helpers, save and expiry * implement authentic taro and fm modfication * magic number and code refactor * make sure only close by group members are counted * add safe taro fm handling, rate command, race and mission booster logic * add config option to disable authentic group scaling * rename for consistency * make rates percentages, fix chat message, add config options * add config option to the ini file * add index guard for hasBoost functions * reorder config ini options * add bank item expiry option * fix trade oversight --------- Co-authored-by: CakeLancelot <CakeLancelot@users.noreply.github.com>
1 parent 9a62ec6 commit 113bc0b

26 files changed

Lines changed: 690 additions & 257 deletions

config.ini

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -43,17 +43,37 @@ simulatemobs=true
4343
# little message players see when they enter the game
4444
motd=Welcome to OpenFusion!
4545

46-
# The following are the default locations of the JSON files the server
47-
# requires to run. You can override them by changing their values and
48-
# uncommenting them (removing the leading # character from that line).
49-
5046
# Should drop fixes be enabled?
5147
# This will add drops to (mostly Academy-specific) mobs that don't have drops
5248
# and rearrange drop tables that are either unassigned or stranded in difficult to reach mobs
5349
# e.g. Hyper Fusionfly and Fusion Numbuh Four drops will become more accessible.
5450
# This is a polish option that is slightly inauthentic to the original game.
5551
#dropfixesenabled=true
5652

53+
# Should groups have to divide up gained Taros / FM among themselves?
54+
# Taros is divided up, FM gets diminished per group member, roughly -12.5% per group member
55+
# Original game worked like this. Uncomment below to disable this behavior.
56+
#lesstarofmingroupdisabled=true
57+
58+
# General reward percentages
59+
# You can change the rate of taro and fusion matter gains for all players.
60+
# The numbers are in percentages, i.e. 1000 is 1000%. You should only use whole numbers, no decimals.
61+
# Uncomment and change below to your desired rate. Defaults are 100%, regular gain rates.
62+
#tarorate=100
63+
#fusionmatterrate=100
64+
65+
# Should expired items in the bank disappear automatically?
66+
# Original game let you kep expired items in the bank until you take them out.
67+
# Uncomment below to enable this behavior.
68+
#removeexpireditemsfrombank=true
69+
70+
# Should there be a score cap for infected zone races?
71+
#izracescorecapped=true
72+
73+
# The following are the default locations of the JSON files the server
74+
# requires to run. You can override them by changing their values and
75+
# uncommenting them (removing the leading # character from that line).
76+
5777
# location of the tabledata folder
5878
#tdatadir=tdata/
5979
# location of the patch folder
@@ -79,9 +99,6 @@ motd=Welcome to OpenFusion!
7999
# location of the database
80100
#dbpath=database.db
81101

82-
# should there be a score cap for infected zone races?
83-
#izracescorecapped=true
84-
85102
# should tutorial flags be disabled off the bat?
86103
disablefirstuseflag=true
87104

src/Abilities.cpp

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -170,11 +170,11 @@ static SkillResult handleSkillBatteryDrain(SkillData* skill, int power, ICombata
170170
if(!blocked) {
171171
boostDrain = (int)(skill->values[0][power] * scalingFactor);
172172
if(boostDrain > plr->batteryW) boostDrain = plr->batteryW;
173-
plr->batteryW -= boostDrain;
173+
plr->subtractCapped(CappedValueType::BATTERY_W, boostDrain);
174174

175175
potionDrain = (int)(skill->values[1][power] * scalingFactor);
176176
if(potionDrain > plr->batteryN) potionDrain = plr->batteryN;
177-
plr->batteryN -= potionDrain;
177+
plr->subtractCapped(CappedValueType::BATTERY_N, potionDrain);
178178
}
179179

180180
sSkillResult_BatteryDrain result{};
@@ -364,7 +364,7 @@ void Abilities::useNPCSkill(EntityRef npc, int skillID, std::vector<ICombatant*>
364364
ICombatant* src = nullptr;
365365
if(npc.kind == EntityKind::COMBAT_NPC || npc.kind == EntityKind::MOB)
366366
src = dynamic_cast<ICombatant*>(entity);
367-
367+
368368
SkillData* skill = &SkillTable[skillID];
369369

370370
std::vector<SkillResult> results = handleSkill(skill, 0, src, affected);
@@ -443,7 +443,7 @@ std::vector<ICombatant*> Abilities::matchTargets(ICombatant* src, SkillData* ski
443443
}
444444
}
445445

446-
return targets;
446+
return targets;
447447
}
448448

449449
/* ripped from client (enums emplaced) */

src/BuiltinCommands.cpp

Lines changed: 62 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -75,30 +75,22 @@ static void setValuePlayer(CNSocket* sock, CNPacketData* data) {
7575
case CN_GM_SET_VALUE_TYPE__HP:
7676
response.iSetValue = plr->HP = setData->iSetValue;
7777
break;
78-
case CN_GM_SET_VALUE_TYPE__WEAPON_BATTERY :
79-
plr->batteryW = setData->iSetValue;
80-
81-
// caps
82-
if (plr->batteryW > 9999)
83-
plr->batteryW = 9999;
84-
78+
case CN_GM_SET_VALUE_TYPE__WEAPON_BATTERY:
79+
plr->setCapped(CappedValueType::BATTERY_W, setData->iSetValue);
8580
response.iSetValue = plr->batteryW;
8681
break;
8782
case CN_GM_SET_VALUE_TYPE__NANO_BATTERY:
88-
plr->batteryN = setData->iSetValue;
89-
90-
// caps
91-
if (plr->batteryN > 9999)
92-
plr->batteryN = 9999;
93-
83+
plr->setCapped(CappedValueType::BATTERY_N, setData->iSetValue);
9484
response.iSetValue = plr->batteryN;
9585
break;
9686
case CN_GM_SET_VALUE_TYPE__FUSION_MATTER:
97-
Missions::updateFusionMatter(sock, setData->iSetValue - plr->fusionmatter);
87+
plr->setCapped(CappedValueType::FUSIONMATTER, setData->iSetValue);
88+
Missions::updateFusionMatter(sock);
9889
response.iSetValue = plr->fusionmatter;
9990
break;
10091
case CN_GM_SET_VALUE_TYPE__CANDY:
101-
response.iSetValue = plr->money = setData->iSetValue;
92+
plr->setCapped(CappedValueType::TAROS, setData->iSetValue);
93+
response.iSetValue = plr->money;
10294
break;
10395
case CN_GM_SET_VALUE_TYPE__SPEED:
10496
case CN_GM_SET_VALUE_TYPE__JUMP:
@@ -148,6 +140,60 @@ static void setGMSpecialOnOff(CNSocket *sock, CNPacketData *data) {
148140
// this is only used for muting players, so no need to update the client since that logic is server-side
149141
}
150142

143+
static void setGMRewardRate(CNSocket *sock, CNPacketData *data) {
144+
Player *plr = PlayerManager::getPlayer(sock);
145+
146+
// access check
147+
if (plr->accountLevel > 30)
148+
return;
149+
150+
auto req = (sP_CL2FE_GM_REQ_REWARD_RATE*)data->buf;
151+
152+
if (req->iGetSet != 0) {
153+
double *rate = nullptr;
154+
155+
switch (req->iRewardType) {
156+
case REWARD_TYPE_TAROS:
157+
rate = plr->rateT;
158+
break;
159+
case REWARD_TYPE_FUSIONMATTER:
160+
rate = plr->rateF;
161+
break;
162+
}
163+
164+
if (rate == nullptr) {
165+
std::cout << "[WARN] Invalid reward type for setGMRewardRate(): " << req->iRewardType << std::endl;
166+
return;
167+
}
168+
169+
if (req->iSetRateValue < 0 || req->iSetRateValue > 1000) {
170+
std::cout << "[WARN] Invalid rate value for setGMRewardRate(): " << req->iSetRateValue << " (must be between 0 and 1000)" << std::endl;
171+
return;
172+
}
173+
174+
switch (req->iRewardRateIndex) {
175+
case RATE_SLOT_ALL:
176+
for (int i = 0; i < 5; i++)
177+
rate[i] = req->iSetRateValue / 100.0;
178+
break;
179+
case RATE_SLOT_COMBAT:
180+
case RATE_SLOT_MISSION:
181+
case RATE_SLOT_EGG:
182+
case RATE_SLOT_RACING:
183+
rate[req->iRewardRateIndex] = req->iSetRateValue / 100.0;
184+
break;
185+
}
186+
}
187+
188+
INITSTRUCT(sP_FE2CL_GM_REP_REWARD_RATE_SUCC, resp);
189+
for (int i = 0; i < 5; i++) {
190+
// double to float
191+
resp.afRewardRate_Taros[i] = plr->rateT[i];
192+
resp.afRewardRate_FusionMatter[i] = plr->rateF[i];
193+
}
194+
sock->sendPacket(resp, P_FE2CL_GM_REP_REWARD_RATE_SUCC);
195+
}
196+
151197
static void locatePlayer(CNSocket *sock, CNPacketData *data) {
152198
Player *plr = PlayerManager::getPlayer(sock);
153199

@@ -371,6 +417,7 @@ void BuiltinCommands::init() {
371417

372418
REGISTER_SHARD_PACKET(P_CL2FE_GM_REQ_PC_SPECIAL_STATE_SWITCH, setGMSpecialSwitchPlayer);
373419
REGISTER_SHARD_PACKET(P_CL2FE_GM_REQ_TARGET_PC_SPECIAL_STATE_ONOFF, setGMSpecialOnOff);
420+
REGISTER_SHARD_PACKET(P_CL2FE_GM_REQ_REWARD_RATE, setGMRewardRate);
374421

375422
REGISTER_SHARD_PACKET(P_CL2FE_GM_REQ_PC_LOCATION, locatePlayer);
376423
REGISTER_SHARD_PACKET(P_CL2FE_GM_REQ_KICK_PLAYER, kickPlayer);

src/Combat.cpp

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,7 @@ EntityRef CombatNPC::getRef() {
300300
}
301301

302302
void CombatNPC::step(time_t currTime) {
303-
303+
304304
if(stateHandlers.find(state) != stateHandlers.end())
305305
stateHandlers[state](this, currTime);
306306
else {
@@ -441,11 +441,7 @@ static void pcAttackNpcs(CNSocket *sock, CNPacketData *data) {
441441
damage = getDamage(damage.first, (int)mob->data["m_iProtection"], true, (plr->batteryW > 6 + difficulty),
442442
Nanos::nanoStyle(plr->activeNano), (int)mob->data["m_iNpcStyle"], difficulty);
443443

444-
if (plr->batteryW >= 6 + difficulty)
445-
plr->batteryW -= 6 + difficulty;
446-
else
447-
plr->batteryW = 0;
448-
444+
plr->subtractCapped(CappedValueType::BATTERY_W, 6 + difficulty);
449445
damage.first = mob->takeDamage(sock, damage.first);
450446

451447
respdata[i].iID = mob->id;
@@ -690,10 +686,7 @@ static void pcAttackChars(CNSocket *sock, CNPacketData *data) {
690686
Nanos::nanoStyle(plr->activeNano), (int)mob->data["m_iNpcStyle"], difficulty);
691687
}
692688

693-
if (plr->batteryW >= 6 + plr->level)
694-
plr->batteryW -= 6 + plr->level;
695-
else
696-
plr->batteryW = 0;
689+
plr->subtractCapped(CappedValueType::BATTERY_W, 6 + plr->level);
697690

698691
damage.first = target->takeDamage(sock, damage.first);
699692

@@ -742,7 +735,7 @@ static int8_t addBullet(Player* plr, bool isGrenade) {
742735
toAdd.weaponBoost = plr->batteryW > 0;
743736
if (toAdd.weaponBoost) {
744737
int boostCost = Rand::rand(11) + 20;
745-
plr->batteryW = boostCost > plr->batteryW ? 0 : plr->batteryW - boostCost;
738+
plr->subtractCapped(CappedValueType::BATTERY_W, boostCost);
746739
}
747740

748741
Bullets[plr->iID][findId] = toAdd;

src/Email.cpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ static void emailReceiveTaros(CNSocket* sock, CNPacketData* data) {
7575

7676
Database::EmailData email = Database::getEmail(plr->iID, pkt->iEmailIndex);
7777
// money transfer
78-
plr->money += email.Taros;
78+
plr->addCapped(CappedValueType::TAROS, email.Taros);
7979
email.Taros = 0;
8080
// update Taros in email
8181
Database::updateEmailContent(&email);
@@ -274,7 +274,7 @@ static void emailSend(CNSocket* sock, CNPacketData* data) {
274274
}
275275

276276
int cost = pkt->iCash + 50 + 20 * attachments.size(); // attached taros + postage
277-
plr->money -= cost;
277+
plr->subtractCapped(CappedValueType::TAROS, cost);
278278
Database::EmailData email = {
279279
(int)pkt->iTo_PCUID, // PlayerId
280280
Database::getNextEmailIndex(pkt->iTo_PCUID), // MsgIndex
@@ -291,7 +291,7 @@ static void emailSend(CNSocket* sock, CNPacketData* data) {
291291
};
292292

293293
if (!Database::sendEmail(&email, attachments, plr)) {
294-
plr->money += cost; // give money back
294+
plr->addCapped(CappedValueType::TAROS, cost); // give money back
295295
// give items back
296296
while (!attachments.empty()) {
297297
sItemBase attachment = attachments.back();

src/Entities.cpp

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,121 @@ sPCAppearanceData Player::getAppearanceData() {
117117
return data;
118118
}
119119

120+
bool Player::hasQuestBoost() const {
121+
if (AEQUIP_COUNT < AEQUIP_COUNT_WITH_BOOSTERS)
122+
return false;
123+
124+
const sItemBase& booster = Equip[10];
125+
return booster.iID == 153 && booster.iOpt > 0;
126+
}
127+
128+
bool Player::hasHunterBoost() const {
129+
if (AEQUIP_COUNT < AEQUIP_COUNT_WITH_BOOSTERS)
130+
return false;
131+
132+
const sItemBase& booster = Equip[11];
133+
return booster.iID == 154 && booster.iOpt > 0;
134+
}
135+
136+
bool Player::hasRacerBoost() const {
137+
if (AEQUIP_COUNT < AEQUIP_COUNT_WITH_BOOSTERS)
138+
return false;
139+
140+
const sItemBase& booster = Equip[9];
141+
return booster.iID == 155 && booster.iOpt > 0;
142+
}
143+
144+
bool Player::hasSuperBoost() const {
145+
return Player::hasQuestBoost() && Player::hasHunterBoost() && Player::hasRacerBoost();
146+
}
147+
148+
static int32_t getCap(CappedValueType type) {
149+
switch (type) {
150+
case CappedValueType::TAROS:
151+
return PC_CANDY_MAX;
152+
case CappedValueType::FUSIONMATTER:
153+
return PC_FUSIONMATTER_MAX;
154+
case CappedValueType::BATTERY_W:
155+
return PC_BATTERY_MAX;
156+
case CappedValueType::BATTERY_N:
157+
return PC_BATTERY_MAX;
158+
case CappedValueType::TAROS_IN_TRADE:
159+
return PC_CANDY_MAX;
160+
default:
161+
return INT32_MAX;
162+
}
163+
}
164+
165+
static int32_t *getCappedValue(Player *player, CappedValueType type) {
166+
switch (type) {
167+
case CappedValueType::TAROS:
168+
return &player->money;
169+
case CappedValueType::FUSIONMATTER:
170+
return &player->fusionmatter;
171+
case CappedValueType::BATTERY_W:
172+
return &player->batteryW;
173+
case CappedValueType::BATTERY_N:
174+
return &player->batteryN;
175+
case CappedValueType::TAROS_IN_TRADE:
176+
return &player->moneyInTrade;
177+
default:
178+
return nullptr;
179+
}
180+
}
181+
182+
void Player::addCapped(CappedValueType type, int32_t diff) {
183+
if (diff <= 0)
184+
return;
185+
186+
int32_t max = getCap(type);
187+
int32_t *value = getCappedValue(this, type);
188+
189+
if (value == nullptr)
190+
return;
191+
192+
if (diff > max)
193+
diff = max;
194+
195+
if (*value + diff > max)
196+
*value = max;
197+
else
198+
*value += diff;
199+
}
200+
201+
void Player::subtractCapped(CappedValueType type, int32_t diff) {
202+
if (diff <= 0)
203+
return;
204+
205+
int32_t max = getCap(type);
206+
int32_t *value = getCappedValue(this, type);
207+
208+
if (value == nullptr)
209+
return;
210+
211+
if (diff > max)
212+
diff = max;
213+
214+
if (*value - diff < 0)
215+
*value = 0;
216+
else
217+
*value -= diff;
218+
}
219+
220+
void Player::setCapped(CappedValueType type, int32_t value) {
221+
int32_t max = getCap(type);
222+
int32_t *valToSet = getCappedValue(this, type);
223+
224+
if (valToSet == nullptr)
225+
return;
226+
227+
if (value < 0)
228+
value = 0;
229+
else if (value > max)
230+
value = max;
231+
232+
*valToSet = value;
233+
}
234+
120235
// TODO: this is less effiecient than it was, because of memset()
121236
void Player::enterIntoViewOf(CNSocket *sock) {
122237
INITSTRUCT(sP_FE2CL_PC_NEW, pkt);

0 commit comments

Comments
 (0)