mirror of
https://git.ethanthesleepy.one/ethanaobrien/ew
synced 2026-10-11 17:14:30 +08:00
Restore beginner missions
This commit is contained in:
@@ -7,6 +7,7 @@ pub mod user;
|
||||
pub mod purchase;
|
||||
pub mod tutorial;
|
||||
pub mod mission;
|
||||
pub mod beginner_mission;
|
||||
pub mod home;
|
||||
pub mod lottery;
|
||||
pub mod friend;
|
||||
|
||||
@@ -0,0 +1,315 @@
|
||||
use jzon::{array, object, JsonValue};
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
use super::{databases, items};
|
||||
use databases::csv::{self, Region};
|
||||
|
||||
lazy_static! {
|
||||
static ref CELLS: JsonValue = csv::table(Region::Jp, "beginner_mission");
|
||||
static ref SETTINGS: JsonValue = csv::table(Region::Jp, "beginner_mission_reward_setting");
|
||||
static ref REWARDS: JsonValue = csv::table(Region::Jp, "beginner_mission_reward");
|
||||
static ref CARD_LEVELS: JsonValue = csv::table(Region::Jp, "card_level");
|
||||
static ref SKILL_LEVELS: JsonValue = csv::table(Region::Jp, "card_skill_level");
|
||||
}
|
||||
|
||||
// JP /api/mission + /api/mission/receive captures, 2024-03-31:
|
||||
// all sheets accumulate progress; only reached sheets can be claimed. Line
|
||||
// rewards are milestones in the number of completed lines, not named lines.
|
||||
const LINES: [[i64; 3]; 8] = [
|
||||
[1, 2, 3], [4, 5, 6], [7, 8, 9], [1, 4, 7],
|
||||
[2, 5, 8], [3, 6, 9], [1, 5, 9], [3, 5, 7],
|
||||
];
|
||||
|
||||
pub fn contains(id: i64) -> bool {
|
||||
CELLS.members().any(|c| c["masterMissionId"].as_i64() == Some(id))
|
||||
}
|
||||
|
||||
pub fn level(id: i64) -> i64 {
|
||||
CELLS.members().find(|c| c["masterMissionId"].as_i64() == Some(id))
|
||||
.and_then(|c| c["level"].as_i64()).unwrap_or(0)
|
||||
}
|
||||
|
||||
pub fn is_counter(id: i64) -> bool {
|
||||
contains(id) && matches!(databases::MISSION_LIST[id.to_string()]["conditionType"].as_i64(), Some(5 | 6 | 53 | 62 | 63))
|
||||
}
|
||||
|
||||
pub fn ensure(missions: &mut JsonValue) {
|
||||
for cell in CELLS.members() {
|
||||
let id = cell["masterMissionId"].as_i64().unwrap();
|
||||
let target = databases::MISSION_LIST[id.to_string()]["conditionNumber"].as_i64().unwrap();
|
||||
if let Some(mission) = missions.members_mut().find(|m| m["master_mission_id"].as_i64() == Some(id)) {
|
||||
// The old login hook kept incrementing an already completed cell.
|
||||
// The client subtracts one to animate an unclaimed completion, so
|
||||
// over-target progress would incorrectly make its "before" sheet clear.
|
||||
let progress = if mission["status"].as_i64().unwrap_or(1) >= 2 { target }
|
||||
else { mission["progress"].as_i64().unwrap_or(0).clamp(0, target) };
|
||||
mission["progress"] = progress.into();
|
||||
continue;
|
||||
}
|
||||
missions.push(object! {
|
||||
master_mission_id: id, status: 1, progress: 0,
|
||||
expire_date_time: 0, not_visible: 0
|
||||
}).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_progress(id: i64, progress: i64, missions: &mut JsonValue) -> bool {
|
||||
ensure(missions);
|
||||
let target = databases::MISSION_LIST[id.to_string()]["conditionNumber"].as_i64().unwrap();
|
||||
for mission in missions.members_mut() {
|
||||
if mission["master_mission_id"].as_i64() != Some(id) { continue; }
|
||||
if mission["status"].as_i64().unwrap_or(1) >= 2 { return false; }
|
||||
let progress = progress.max(mission["progress"].as_i64().unwrap_or(0)).min(target);
|
||||
mission["progress"] = progress.into();
|
||||
mission["status"] = if progress >= target { 2 } else { 1 }.into();
|
||||
return progress >= target;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn advance(condition: i64, value: Option<i64>, amount: i64, missions: &mut JsonValue) -> JsonValue {
|
||||
ensure(missions);
|
||||
let mut cleared = array![];
|
||||
for cell in CELLS.members() {
|
||||
let id = cell["masterMissionId"].as_i64().unwrap();
|
||||
let mst = &databases::MISSION_LIST[id.to_string()];
|
||||
if mst["conditionType"].as_i64() != Some(condition) { continue; }
|
||||
if let Some(value) = value {
|
||||
if mst["conditionValues"][0].as_i64() != Some(value) { continue; }
|
||||
}
|
||||
let old = items::get_mission_status(id, missions)["progress"].as_i64().unwrap_or(0);
|
||||
if set_progress(id, old + amount, missions) { cleared.push(id).unwrap(); }
|
||||
}
|
||||
cleared
|
||||
}
|
||||
|
||||
pub fn refresh(user: &JsonValue, missions: &mut JsonValue) -> JsonValue {
|
||||
ensure(missions);
|
||||
let mut cleared = array![];
|
||||
for cell in CELLS.members() {
|
||||
let id = cell["masterMissionId"].as_i64().unwrap();
|
||||
let mst = &databases::MISSION_LIST[id.to_string()];
|
||||
let value = mst["conditionValues"][0].as_i64().unwrap_or(0);
|
||||
let progress = match mst["conditionType"].as_i64().unwrap() {
|
||||
14 | 64 => {
|
||||
let skill = mst["conditionType"] == 64;
|
||||
user["card_list"].members().filter(|card| {
|
||||
let master = super::custom_card::card_info(card["master_card_id"].as_i64().unwrap_or(0));
|
||||
let curve = if skill { databases::CARD_RARITY[master["rarity"].to_string()]["masterCardSkillLevelId"].as_i64() }
|
||||
else { master["masterCardLevelId"].as_i64() };
|
||||
let levels = if skill { &*SKILL_LEVELS } else { &*CARD_LEVELS };
|
||||
levels.members().any(|l| l["id"].as_i64() == curve && l["level"].as_i64() == Some(value)
|
||||
&& card[if skill { "skill_exp" } else { "exp" }].as_i64().unwrap_or(0) >= l["exp"].as_i64().unwrap())
|
||||
}).count() as i64
|
||||
}
|
||||
17 => user["card_list"].members().filter(|c| !c["evolve"].is_empty()).count() as i64,
|
||||
58 => user["character_list"].members().filter_map(|c| c["exp"].as_i64()).max().unwrap_or(0),
|
||||
30 | 60 => {
|
||||
let mut best = 0;
|
||||
for deck in user["deck_list"].members() {
|
||||
let mut types = [0i64; 3];
|
||||
let mut points = [0i64; 3];
|
||||
for slot in deck["main_card_ids"].members() {
|
||||
let Some(card) = user["card_list"].members().find(|c| c["id"] == *slot) else { continue; };
|
||||
let master = super::custom_card::card_info(card["master_card_id"].as_i64().unwrap_or(0));
|
||||
if let Some(t) = master["type"].as_usize().filter(|t| (1..=3).contains(t)) { types[t-1] += 1; }
|
||||
let level = CARD_LEVELS.members().filter(|l| l["id"] == master["masterCardLevelId"]
|
||||
&& l["exp"].as_i64().unwrap() <= card["exp"].as_i64().unwrap_or(0))
|
||||
.max_by_key(|l| l["level"].as_i64());
|
||||
if let Some(l) = level {
|
||||
for (i, name) in ["smile", "pure", "cool"].iter().enumerate() {
|
||||
points[i] += master[*name].as_i64().unwrap_or(0) * l[format!("{}Ratio",name)].as_i64().unwrap_or(0) / 10000;
|
||||
}
|
||||
}
|
||||
}
|
||||
let p = if mst["conditionType"] == 30 {
|
||||
let needed = mst["conditionValues"][1].as_i64().unwrap();
|
||||
(types.iter().any(|n| *n >= needed)) as i64
|
||||
} else { *points.iter().max().unwrap() };
|
||||
best = best.max(p);
|
||||
}
|
||||
best
|
||||
}
|
||||
_ => continue,
|
||||
};
|
||||
if set_progress(id, progress, missions) { cleared.push(id).unwrap(); }
|
||||
}
|
||||
cleared
|
||||
}
|
||||
|
||||
pub fn refresh_account(user: &JsonValue, missions: &mut JsonValue) -> JsonValue {
|
||||
let mut cleared = refresh(user, missions);
|
||||
// Existing accounts may have prepared transfer credentials before missions
|
||||
// were enabled. Merely requesting a code leaves an empty password.
|
||||
if let Some(uid) = user["user"]["id"].as_i64() {
|
||||
if super::userdata::has_transfer_password(uid) {
|
||||
for id in advance(25, None, 1, missions).members() { cleared.push(id.clone()).unwrap(); }
|
||||
}
|
||||
}
|
||||
cleared
|
||||
}
|
||||
|
||||
fn sheet_cells(group: i64, level: i64) -> impl Iterator<Item = &'static JsonValue> {
|
||||
CELLS.members().filter(move |c| c["id"].as_i64() == Some(group) && c["level"].as_i64() == Some(level))
|
||||
}
|
||||
|
||||
fn claimed(group: i64, level: i64, missions: &JsonValue) -> Vec<i64> {
|
||||
sheet_cells(group, level).filter(|c| {
|
||||
items::get_mission_status(c["masterMissionId"].as_i64().unwrap(), missions)["status"] == 3
|
||||
}).filter_map(|c| c["number"].as_i64()).collect()
|
||||
}
|
||||
|
||||
pub fn claimable(id: i64, missions: &JsonValue) -> bool {
|
||||
let Some(cell) = CELLS.members().find(|c| c["masterMissionId"].as_i64() == Some(id)) else { return false; };
|
||||
CELLS.members().filter(|c| c["id"] == cell["id"] && c["level"].as_i64() < cell["level"].as_i64())
|
||||
.all(|c| items::get_mission_status(c["masterMissionId"].as_i64().unwrap(), missions)["status"] == 3)
|
||||
}
|
||||
|
||||
pub fn home_status(missions: &JsonValue) -> (usize, bool) {
|
||||
let all = CELLS.members().all(|c| items::get_mission_status(c["masterMissionId"].as_i64().unwrap(), missions)["status"] == 3);
|
||||
let count = CELLS.members().filter(|c| {
|
||||
let id = c["masterMissionId"].as_i64().unwrap();
|
||||
items::get_mission_status(id, missions)["status"] == 2 && claimable(id, missions)
|
||||
}).count();
|
||||
(count, all)
|
||||
}
|
||||
|
||||
pub fn new_rewards(before: &JsonValue, after: &JsonValue) -> Vec<JsonValue> {
|
||||
let mut settings: Vec<_> = SETTINGS.members().collect();
|
||||
// Captures return line milestones before the full-sheet reward.
|
||||
settings.sort_by_key(|s| (s["masterBeginnerMissionId"].as_i64(), s["level"].as_i64(),
|
||||
if s["number"] == 0 { 9 } else { s["number"].as_i64().unwrap() }));
|
||||
let mut result = Vec::new();
|
||||
for setting in settings {
|
||||
let group = setting["masterBeginnerMissionId"].as_i64().unwrap();
|
||||
let level = setting["level"].as_i64().unwrap();
|
||||
let number = setting["number"].as_i64().unwrap();
|
||||
// JP cross-sheet claims award bonuses only for sheets unlocked before
|
||||
// the request (recorded claims 8 and 11 in the replay fixture).
|
||||
let first = sheet_cells(group, level).next().unwrap()["masterMissionId"].as_i64().unwrap();
|
||||
if !claimable(first, before) { continue; }
|
||||
let achieved = |missions: &JsonValue| {
|
||||
let cells = claimed(group, level, missions);
|
||||
if number == 0 { cells.len() == 9 }
|
||||
else { LINES.iter().filter(|line| line.iter().all(|n| cells.contains(n))).count() >= number as usize }
|
||||
};
|
||||
if !achieved(before) && achieved(after) {
|
||||
result.extend(REWARDS.members().filter(|r| r["id"] == setting["masterBeginnerMissionRewardId"]).cloned());
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn current_inventory_drives_training_bond_and_deck_goals() {
|
||||
let mut user = object! { card_list: [], character_list: [{exp: 30}], deck_list: [] };
|
||||
let master = super::super::custom_card::card_info(10010001);
|
||||
let exp = CARD_LEVELS.members().find(|l| l["id"] == master["masterCardLevelId"] && l["level"] == 30).unwrap()["exp"].clone();
|
||||
let curve = &databases::CARD_RARITY[master["rarity"].to_string()]["masterCardSkillLevelId"];
|
||||
let skill_exp = SKILL_LEVELS.members().find(|l| l["id"] == *curve && l["level"] == 2).unwrap()["exp"].clone();
|
||||
for id in 1..=5 {
|
||||
user["card_list"].push(object! {id: id, master_card_id: 10010001,
|
||||
exp: exp.clone(), skill_exp: skill_exp.clone(), evolve: [{type: 2, count: 1}]}).unwrap();
|
||||
}
|
||||
user["deck_list"].push(object! {main_card_ids: [1,2,3,4,5,0,0,0,0]}).unwrap();
|
||||
let mut missions = array![];
|
||||
let cleared = refresh(&user, &mut missions);
|
||||
for id in [1614001,1614002,1614003,1664001,1664002,1617001,1658001,1658002,1658003,1630001] {
|
||||
assert!(cleared.contains(id), "mission {}", id);
|
||||
}
|
||||
assert!(refresh(&user, &mut missions).is_empty());
|
||||
assert_eq!(items::get_mission_status(1605001, &missions)["progress"], 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn counters_match_values_and_home_counts_only_unlocked_sheet() {
|
||||
let mut missions = array![];
|
||||
assert!(advance(63, Some(1110001), 20, &mut missions).is_empty());
|
||||
assert_eq!(advance(63, Some(8110001), 10, &mut missions), array![1663001]);
|
||||
assert_eq!(items::get_mission_status(1663002, &missions)["progress"], 10);
|
||||
assert_eq!(advance(63, Some(8110001), 10, &mut missions), array![1663002]);
|
||||
assert!(advance(6, Some(1), 1, &mut missions).is_empty());
|
||||
assert_eq!(advance(6, Some(2), 1, &mut missions), array![1606001]);
|
||||
advance(53, None, 3, &mut missions);
|
||||
assert_eq!(home_status(&missions), (2, false));
|
||||
for row in missions.members_mut() { row["status"] = 3.into(); }
|
||||
assert_eq!(home_status(&missions), (0, true));
|
||||
assert!(advance(53, None, 1, &mut missions).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_recorded_jp_beginner_claims_match() {
|
||||
let cases = jzon::parse(include_str!("beginner_mission_claims.json")).unwrap();
|
||||
assert_eq!(cases.len(), 15);
|
||||
for (i, case) in cases.members().enumerate() {
|
||||
let mut before = array![];
|
||||
ensure(&mut before);
|
||||
for row in before.members_mut() {
|
||||
if case["before"].contains(row["master_mission_id"].as_i64().unwrap()) { row["status"] = 3.into(); }
|
||||
}
|
||||
let mut after = before.clone();
|
||||
for row in after.members_mut() {
|
||||
if case["claim"].contains(row["master_mission_id"].as_i64().unwrap()) { row["status"] = 3.into(); }
|
||||
}
|
||||
let expected: Vec<_> = case["bonus"].members().map(|x| x.as_i64().unwrap()).collect();
|
||||
let actual: Vec<_> = new_rewards(&before, &after).iter().map(|x| x["amount"].as_i64().unwrap()).collect();
|
||||
assert_eq!(actual, expected, "JP claim {}", i);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn progress_is_capped_and_claims_are_not_reset() {
|
||||
let mut m = array![];
|
||||
advance(5, None, 12, &mut m);
|
||||
assert_eq!(items::get_mission_status(1605001, &m)["progress"], 1);
|
||||
assert_eq!(items::get_mission_status(1605002, &m)["progress"], 10);
|
||||
assert_eq!(items::get_mission_status(1605003, &m)["progress"], 12);
|
||||
assert!(!claimable(1605002, &m));
|
||||
for row in m.members_mut() { if row["master_mission_id"] == 1605001 { row["status"] = 3.into(); } }
|
||||
advance(5, None, 1, &mut m);
|
||||
assert_eq!(items::get_mission_status(1605001, &m)["status"], 3);
|
||||
for row in m.members_mut() {
|
||||
if row["master_mission_id"] == 1653001 {
|
||||
row["status"] = 2.into();
|
||||
row["progress"] = 50.into();
|
||||
}
|
||||
}
|
||||
ensure(&mut m);
|
||||
assert_eq!(items::get_mission_status(1653001, &m)["progress"], 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn captured_first_sheet_claim_awards_one_line() {
|
||||
let mut before = array![];
|
||||
ensure(&mut before);
|
||||
let mut after = before.clone();
|
||||
for row in after.members_mut() {
|
||||
if [1653001,1605001,1663001,1666001,1658001].contains(&row["master_mission_id"].as_i64().unwrap()) {
|
||||
row["status"] = 3.into();
|
||||
}
|
||||
}
|
||||
let r = new_rewards(&before, &after);
|
||||
assert_eq!(r.len(), 1);
|
||||
assert_eq!(r[0]["amount"], 50);
|
||||
assert!(new_rewards(&after, &after).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn full_sheet_gives_eight_lines_and_sheet_once() {
|
||||
let mut before = array![];
|
||||
ensure(&mut before);
|
||||
let mut after = before.clone();
|
||||
for row in after.members_mut() {
|
||||
if sheet_cells(1,1).any(|c| c["masterMissionId"] == row["master_mission_id"]) { row["status"] = 3.into(); }
|
||||
}
|
||||
let r = new_rewards(&before, &after);
|
||||
assert_eq!(r.len(), 9);
|
||||
assert_eq!(r.iter().map(|r| r["amount"].as_i64().unwrap()).sum::<i64>(), 1000);
|
||||
assert!(claimable(1605002, &after));
|
||||
assert!(!home_status(&after).1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
[
|
||||
{
|
||||
"before": [],
|
||||
"claim": [
|
||||
1653001
|
||||
],
|
||||
"bonus": []
|
||||
},
|
||||
{
|
||||
"before": [
|
||||
1653001,
|
||||
1660001,
|
||||
1666001
|
||||
],
|
||||
"claim": [
|
||||
1605001,
|
||||
1663001,
|
||||
1614001,
|
||||
1658001,
|
||||
1661001
|
||||
],
|
||||
"bonus": [
|
||||
50,
|
||||
50,
|
||||
50,
|
||||
50,
|
||||
50
|
||||
]
|
||||
},
|
||||
{
|
||||
"before": [],
|
||||
"claim": [
|
||||
1653001
|
||||
],
|
||||
"bonus": []
|
||||
},
|
||||
{
|
||||
"before": [],
|
||||
"claim": [
|
||||
1653001
|
||||
],
|
||||
"bonus": []
|
||||
},
|
||||
{
|
||||
"before": [],
|
||||
"claim": [
|
||||
1653001
|
||||
],
|
||||
"bonus": []
|
||||
},
|
||||
{
|
||||
"before": [
|
||||
1653001
|
||||
],
|
||||
"claim": [
|
||||
1605001,
|
||||
1663001,
|
||||
1660001,
|
||||
1614001
|
||||
],
|
||||
"bonus": [
|
||||
50
|
||||
]
|
||||
},
|
||||
{
|
||||
"before": [],
|
||||
"claim": [
|
||||
1653001,
|
||||
1605001,
|
||||
1663001,
|
||||
1666001,
|
||||
1658001
|
||||
],
|
||||
"bonus": [
|
||||
50
|
||||
]
|
||||
},
|
||||
{
|
||||
"before": [
|
||||
1653001,
|
||||
1605001,
|
||||
1663001,
|
||||
1666001,
|
||||
1658001
|
||||
],
|
||||
"claim": [
|
||||
1614001
|
||||
],
|
||||
"bonus": []
|
||||
},
|
||||
{
|
||||
"before": [
|
||||
1653001,
|
||||
1605001,
|
||||
1663001,
|
||||
1614001,
|
||||
1666001,
|
||||
1658001
|
||||
],
|
||||
"claim": [
|
||||
1662001,
|
||||
1660001,
|
||||
1661001,
|
||||
1653002,
|
||||
1658002,
|
||||
1630001,
|
||||
1663002
|
||||
],
|
||||
"bonus": [
|
||||
50,
|
||||
50,
|
||||
50,
|
||||
50,
|
||||
50,
|
||||
100,
|
||||
100,
|
||||
500
|
||||
]
|
||||
},
|
||||
{
|
||||
"before": [
|
||||
1653001,
|
||||
1605001,
|
||||
1662001,
|
||||
1663001,
|
||||
1660001,
|
||||
1614001,
|
||||
1666001,
|
||||
1658001,
|
||||
1661001,
|
||||
1653002,
|
||||
1658002,
|
||||
1630001,
|
||||
1663002
|
||||
],
|
||||
"claim": [
|
||||
1665001
|
||||
],
|
||||
"bonus": [
|
||||
50
|
||||
]
|
||||
},
|
||||
{
|
||||
"before": [
|
||||
1653001,
|
||||
1605001,
|
||||
1662001,
|
||||
1663001,
|
||||
1660001,
|
||||
1614001,
|
||||
1666001,
|
||||
1658001,
|
||||
1661001,
|
||||
1653002,
|
||||
1665001,
|
||||
1658002,
|
||||
1630001,
|
||||
1663002
|
||||
],
|
||||
"claim": [
|
||||
1664001,
|
||||
1614002
|
||||
],
|
||||
"bonus": [
|
||||
50
|
||||
]
|
||||
},
|
||||
{
|
||||
"before": [
|
||||
1653001,
|
||||
1605001,
|
||||
1662001,
|
||||
1663001,
|
||||
1660001,
|
||||
1614001,
|
||||
1666001,
|
||||
1658001,
|
||||
1661001,
|
||||
1653002,
|
||||
1665001,
|
||||
1664001,
|
||||
1658002,
|
||||
1630001,
|
||||
1614002,
|
||||
1663002
|
||||
],
|
||||
"claim": [
|
||||
1605002,
|
||||
1606001,
|
||||
1653003,
|
||||
1660002,
|
||||
1617001,
|
||||
1658003,
|
||||
1625001
|
||||
],
|
||||
"bonus": [
|
||||
50,
|
||||
50,
|
||||
50,
|
||||
100,
|
||||
100,
|
||||
500
|
||||
]
|
||||
},
|
||||
{
|
||||
"before": [
|
||||
1653001,
|
||||
1605001,
|
||||
1662001,
|
||||
1663001,
|
||||
1660001,
|
||||
1614001,
|
||||
1666001,
|
||||
1658001,
|
||||
1661001,
|
||||
1653002,
|
||||
1605002,
|
||||
1665001,
|
||||
1664001,
|
||||
1658002,
|
||||
1630001,
|
||||
1606001,
|
||||
1614002,
|
||||
1663002,
|
||||
1653003,
|
||||
1660002,
|
||||
1617001,
|
||||
1658003,
|
||||
1625001
|
||||
],
|
||||
"claim": [
|
||||
1626001
|
||||
],
|
||||
"bonus": [
|
||||
50,
|
||||
50
|
||||
]
|
||||
},
|
||||
{
|
||||
"before": [
|
||||
1653001,
|
||||
1605001,
|
||||
1662001,
|
||||
1663001,
|
||||
1660001,
|
||||
1614001,
|
||||
1666001,
|
||||
1658001,
|
||||
1661001,
|
||||
1653002,
|
||||
1605002,
|
||||
1665001,
|
||||
1664001,
|
||||
1658002,
|
||||
1630001,
|
||||
1606001,
|
||||
1614002,
|
||||
1663002,
|
||||
1653003,
|
||||
1660002,
|
||||
1617001,
|
||||
1658003,
|
||||
1625001,
|
||||
1626001
|
||||
],
|
||||
"claim": [
|
||||
1614003,
|
||||
1664002
|
||||
],
|
||||
"bonus": [
|
||||
50,
|
||||
50,
|
||||
50
|
||||
]
|
||||
},
|
||||
{
|
||||
"before": [
|
||||
1653001,
|
||||
1605001,
|
||||
1662001,
|
||||
1663001,
|
||||
1660001,
|
||||
1614001,
|
||||
1666001,
|
||||
1658001,
|
||||
1661001,
|
||||
1653002,
|
||||
1605002,
|
||||
1665001,
|
||||
1664001,
|
||||
1658002,
|
||||
1630001,
|
||||
1606001,
|
||||
1614002,
|
||||
1663002,
|
||||
1653003,
|
||||
1660002,
|
||||
1617001,
|
||||
1658003,
|
||||
1625001,
|
||||
1614003,
|
||||
1664002,
|
||||
1626001
|
||||
],
|
||||
"claim": [
|
||||
1605003
|
||||
],
|
||||
"bonus": [
|
||||
100,
|
||||
100,
|
||||
500
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -134,6 +134,11 @@ async fn reinforce(Session { key, body }: Session) -> impl Responder {
|
||||
let mut clear_mission_ids = array![];
|
||||
|
||||
let card = do_reinforce(&mut user, &body, "exp", 1, false, &mut array![], &mut array![], &mut clear_mission_ids);
|
||||
let mut missions = userdata::get_acc_missions(&key);
|
||||
for id in super::beginner_mission::refresh(&user, &mut missions).members() {
|
||||
clear_mission_ids.push(id.clone()).unwrap();
|
||||
}
|
||||
userdata::save_acc_missions(&key, missions);
|
||||
|
||||
userdata::save_acc(&key, user.clone());
|
||||
|
||||
@@ -150,6 +155,11 @@ async fn skill_reinforce(Session { key, body }: Session) -> impl Responder {
|
||||
let mut clear_mission_ids = array![];
|
||||
|
||||
let card = do_reinforce(&mut user, &body, "skill_exp", 10, false, &mut array![], &mut array![], &mut clear_mission_ids);
|
||||
let mut missions = userdata::get_acc_missions(&key);
|
||||
for id in super::beginner_mission::refresh(&user, &mut missions).members() {
|
||||
clear_mission_ids.push(id.clone()).unwrap();
|
||||
}
|
||||
userdata::save_acc_missions(&key, missions);
|
||||
|
||||
userdata::save_acc(&key, user.clone());
|
||||
|
||||
@@ -168,6 +178,7 @@ async fn evolve(Session { key, body }: Session) -> impl Responder {
|
||||
let mut clear_mission_ids = array![];
|
||||
|
||||
let card = do_reinforce(&mut user, &body, "", 0, true, &mut missions, &mut chats, &mut clear_mission_ids);
|
||||
for id in super::beginner_mission::refresh(&user, &mut missions).members() { clear_mission_ids.push(id.clone()).unwrap(); }
|
||||
|
||||
userdata::save_acc(&key, user.clone());
|
||||
userdata::save_acc_chats(&key, chats);
|
||||
|
||||
@@ -324,6 +324,11 @@ async fn migration_password_register(req: HttpRequest, body: String) -> impl Res
|
||||
let pass = decrypt_transfer_password(&body["migration_password"].to_string());
|
||||
|
||||
userdata::user::migration::save_acc_transfer(user["user"]["id"].as_i64().unwrap(), &pass);
|
||||
if !pass.is_empty() {
|
||||
let mut missions = userdata::get_acc_missions(&uid);
|
||||
super::beginner_mission::advance(25, None, 1, &mut missions);
|
||||
userdata::save_acc_missions(&uid, missions);
|
||||
}
|
||||
|
||||
let resp = object!{
|
||||
result: "OK"
|
||||
|
||||
+12
-6
@@ -23,6 +23,10 @@ async fn preset(Session { key, body }: Session) -> impl Responder {
|
||||
}
|
||||
userdata::save_acc_home(&key, user);
|
||||
|
||||
let mut missions = userdata::get_acc_missions(&key);
|
||||
super::beginner_mission::advance(61, None, 1, &mut missions);
|
||||
userdata::save_acc_missions(&key, missions);
|
||||
|
||||
Api(Some(array![]))
|
||||
}
|
||||
|
||||
@@ -89,12 +93,13 @@ async fn home(Login(key): Login) -> impl Responder {
|
||||
check_gifts(&mut user);
|
||||
|
||||
let mut user_missions = userdata::get_acc_missions(&key);
|
||||
let clear = items::completed_daily_mission(1253003, &mut user_missions);
|
||||
let mut clear = super::beginner_mission::refresh_account(&userdata::get_acc(&key), &mut user_missions);
|
||||
for id in items::completed_daily_mission(1253003, &mut user_missions).members() {
|
||||
clear.push(id.clone()).unwrap();
|
||||
}
|
||||
userdata::save_acc_home(&key, user.clone());
|
||||
user["clear_mission_ids"] = clear;
|
||||
if !user["clear_mission_ids"].is_empty() {
|
||||
userdata::save_acc_missions(&key, user_missions.clone());
|
||||
}
|
||||
userdata::save_acc_missions(&key, user_missions.clone());
|
||||
|
||||
let daily_missions = array![1224003, 1253003, 1273009, 1273010, 1273011, 1273012];
|
||||
|
||||
@@ -128,8 +133,9 @@ async fn home(Login(key): Login) -> impl Responder {
|
||||
let new_announcement = crate::database::announcements::latest_published_at() > seen_at;
|
||||
user["home"]["new_announcement_flag"] = (new_announcement as i32).into();
|
||||
|
||||
//todo
|
||||
user["home"]["beginner_mission_complete"] = 1.into();
|
||||
let (ready, complete) = super::beginner_mission::home_status(&user_missions);
|
||||
user["home"]["clear_beginner_mission_count"] = ready.into();
|
||||
user["home"]["beginner_mission_complete"] = (complete as i32).into();
|
||||
|
||||
Api(Some(user))
|
||||
}
|
||||
|
||||
+2
-2
@@ -545,8 +545,8 @@ pub fn completed_daily_mission(id: i64, missions: &mut JsonValue) -> JsonValue {
|
||||
let mut rv = array![];
|
||||
if id == 1253003 {
|
||||
rv = advance_variable_mission(1153001, 1153019, 1, missions);
|
||||
if update_mission_status(1653001, 0, true, false, 1, missions).is_some() {
|
||||
rv.push(1653001).unwrap();
|
||||
for id in super::beginner_mission::advance(53, None, 1, missions).members() {
|
||||
rv.push(id.clone()).unwrap();
|
||||
}
|
||||
}
|
||||
for mission in missions.members_mut() {
|
||||
|
||||
@@ -727,6 +727,17 @@ pub fn live_end_ex(req: &HttpRequest, key: &str, body: &JsonValue, skipped: bool
|
||||
|
||||
//1273009, 1273010, 1273011, 1273012
|
||||
let mut cleared_missions = items::advance_variable_mission(1105001, 1105017, 1, &mut user_missions);
|
||||
for id in super::beginner_mission::advance(5, None, 1, &mut user_missions).members() {
|
||||
cleared_missions.push(id.clone()).unwrap();
|
||||
}
|
||||
if !skipped {
|
||||
let started = get_started_live(key, body).unwrap_or(JsonValue::Null);
|
||||
if started["is_omakase"] == 1 {
|
||||
for id in super::beginner_mission::advance(62, None, 1, &mut user_missions).members() { cleared_missions.push(id.clone()).unwrap(); }
|
||||
}
|
||||
let boost = started["live_boost"].as_i64().or(body["live_boost"].as_i64()).unwrap_or(0);
|
||||
for id in super::beginner_mission::advance(6, Some(boost), 1, &mut user_missions).members() { cleared_missions.push(id.clone()).unwrap(); }
|
||||
}
|
||||
if body["master_live_id"].to_string().len() > 1 {
|
||||
let id = body["master_live_id"].to_string().split("").collect::<Vec<_>>()[2].parse::<i64>().unwrap_or(0);
|
||||
if (1..=4).contains(&id) {
|
||||
@@ -822,6 +833,7 @@ pub fn live_end_ex(req: &HttpRequest, key: &str, body: &JsonValue, skipped: bool
|
||||
|
||||
let deck_slot = get_end_live_deck_id(&key, &body).unwrap_or(body["deck_slot"].as_i32().unwrap_or(user["user"]["main_deck_slot"].as_i32().unwrap()));
|
||||
let characters = get_live_character_list(lp_used, deck_slot, &mut user, &mut user_missions, &mut cleared_missions, &mut chats);
|
||||
for id in super::beginner_mission::refresh(&user, &mut user_missions).members() { cleared_missions.push(id.clone()).unwrap(); }
|
||||
|
||||
userdata::save_acc(&key, user.clone());
|
||||
userdata::save_acc_home(&key, user2.clone());
|
||||
|
||||
@@ -390,6 +390,9 @@ async fn lottery_post(req: HttpRequest, Session { key, body }: Session) -> impl
|
||||
}
|
||||
|
||||
add_draw_count(&mut user, lottery_id, price_number);
|
||||
for id in super::beginner_mission::advance(63, Some(lottery_id), price["count"].as_i64().unwrap_or(0), &mut missions).members() {
|
||||
cleared_missions.push(id.clone()).unwrap();
|
||||
}
|
||||
let mut new_count = get_draw_count(&user, lottery_id, price_number);
|
||||
if is_stepup(lottery_id) && price_number == 1 {
|
||||
new_count += 1;
|
||||
|
||||
+90
-7
@@ -12,6 +12,50 @@ pub fn routes(cfg: &mut web::ServiceConfig) {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[actix_web::test]
|
||||
async fn beginner_claims_persist_rewards_and_reject_repeats() {
|
||||
let _test = crate::runtime::lock_test_data_path();
|
||||
let (uid, key) = userdata::starter::create("Beginner test").unwrap();
|
||||
let request = || actix_web::test::TestRequest::default()
|
||||
.insert_header(("aoharu-user-id", uid.to_string())).to_http_request();
|
||||
let first = [1653001,1605001,1662001,1663001,1660001,1614001,1666001,1658001,1661001];
|
||||
let mut missions = userdata::get_acc_missions(&key);
|
||||
for id in first {
|
||||
super::super::beginner_mission::set_progress(id, i64::MAX, &mut missions);
|
||||
}
|
||||
super::super::beginner_mission::set_progress(1653002, 2, &mut missions);
|
||||
userdata::save_acc_missions(&key, missions);
|
||||
let blocked = receive(request(), Session { key: key.clone(), body: object! {master_mission_ids: [1653002]} }).await.0.unwrap();
|
||||
assert!(blocked["reward_list"].is_empty());
|
||||
let gem_before = userdata::get_acc(&key)["gem"]["total"].as_i64().unwrap();
|
||||
let mut ids: Vec<i64> = first.into();
|
||||
ids.push(first[0]);
|
||||
let body = object! {master_mission_ids: ids};
|
||||
let response = receive(request(), Session { key: key.clone(), body: body.clone() }).await.0.unwrap();
|
||||
assert_eq!(response["mission_list"].len(), 9);
|
||||
assert_eq!(response["reward_list"].len(), 18);
|
||||
let bonuses: i64 = response["reward_list"].members().filter(|r| r["type"] == 1)
|
||||
.map(|r| r["amount"].as_i64().unwrap()).sum();
|
||||
assert_eq!(bonuses, 1000);
|
||||
assert_eq!(userdata::get_acc(&key)["gem"]["total"].as_i64().unwrap(), gem_before + bonuses);
|
||||
let items = &response["updated_value_list"]["item_list"];
|
||||
let unique: std::collections::HashSet<_> = items.members().map(|i| i["master_item_id"].as_i64()).collect();
|
||||
assert_eq!(items.len(), unique.len());
|
||||
let saved = userdata::get_acc(&key);
|
||||
let replay = receive(request(), Session { key: key.clone(), body }).await.0.unwrap();
|
||||
assert!(replay["reward_list"].is_empty());
|
||||
assert_eq!(userdata::get_acc(&key), saved);
|
||||
let second = receive(request(), Session { key: key.clone(), body: object! {master_mission_ids: [1653002, 1605002]} }).await.0.unwrap();
|
||||
assert_eq!(second["mission_list"].len(), 1);
|
||||
assert_eq!(second["mission_list"][0]["master_mission_id"], 1653002);
|
||||
assert_eq!(second["mission_list"][0]["progress"], 2);
|
||||
}
|
||||
}
|
||||
|
||||
const VARIABLE_MISSIONS: [[i64; 2]; 5] = [[1153001, 1153019], [1105001, 1105017], [1101001, 1101030], [1121001, 1121019], [1112001, 1112033]];
|
||||
|
||||
async fn mission(Login(key): Login) -> impl Responder {
|
||||
@@ -25,9 +69,9 @@ async fn mission(Login(key): Login) -> impl Responder {
|
||||
}
|
||||
|
||||
let mut missions = userdata::get_acc_missions(&key);
|
||||
if items::refresh_dailies(&mut missions, global::timestamp()) {
|
||||
userdata::save_acc_missions(&key, missions.clone());
|
||||
}
|
||||
items::refresh_dailies(&mut missions, global::timestamp());
|
||||
super::beginner_mission::refresh_account(&user, &mut missions);
|
||||
userdata::save_acc_missions(&key, missions.clone());
|
||||
|
||||
Api(Some(object!{
|
||||
"mission_list": missions
|
||||
@@ -39,6 +83,10 @@ async fn clear(Session { key, body }: Session) -> impl Responder {
|
||||
|
||||
let mut cleared = array![];
|
||||
for id in body["master_mission_ids"].members() {
|
||||
if super::beginner_mission::contains(id.as_i64().unwrap_or(0)) {
|
||||
let condition = databases::MISSION_LIST[id.to_string()]["conditionType"].as_i64();
|
||||
if !matches!(condition, Some(26 | 65 | 66)) { continue; }
|
||||
}
|
||||
let mission = items::get_mission_status(id.as_i64().unwrap(), &missions);
|
||||
if mission.is_empty() || mission["status"].as_i32().unwrap_or(0) >= 2 {
|
||||
continue;
|
||||
@@ -54,7 +102,11 @@ async fn clear(Session { key, body }: Session) -> impl Responder {
|
||||
}))
|
||||
}
|
||||
|
||||
async fn receive(req: HttpRequest, Session { key, body }: Session) -> impl Responder {
|
||||
static CLAIM_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
|
||||
|
||||
async fn receive(req: HttpRequest, Session { key, body }: Session) -> Api {
|
||||
// Keep the read/grant/save sequence exclusive across HTTP workers.
|
||||
let _claim = crate::lock_onto_mutex!(CLAIM_LOCK);
|
||||
let uid = global::get_uid(req.headers());
|
||||
let now = global::timestamp();
|
||||
|
||||
@@ -66,9 +118,20 @@ async fn receive(req: HttpRequest, Session { key, body }: Session) -> impl Respo
|
||||
let mut touched_gem = false;
|
||||
let mut touched_coin = false;
|
||||
let mut touched_items = array![];
|
||||
let before = missions.clone();
|
||||
let mut requested: Vec<_> = body["master_mission_ids"].members().filter_map(|id| id.as_i64()).collect();
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
requested.retain(|id| seen.insert(*id));
|
||||
// Process beginner cells in sheet order so a single claim can unlock the next sheet.
|
||||
requested.sort_by_key(|id| super::beginner_mission::level(*id));
|
||||
|
||||
for mission in body["master_mission_ids"].members() {
|
||||
let mid = mission.as_i64().unwrap();
|
||||
for mid in requested {
|
||||
if items::get_mission_status(mid, &missions)["status"] != 2 {
|
||||
continue;
|
||||
}
|
||||
if super::beginner_mission::contains(mid) && !super::beginner_mission::claimable(mid, &missions) {
|
||||
continue;
|
||||
}
|
||||
let mission_info = databases::MISSION_LIST[mid.to_string()].clone();
|
||||
for master in databases::MISSION_REWARDS[mission_info["masterMissionRewardId"].to_string()].members() {
|
||||
let reward_type = master["type"].as_i64().unwrap();
|
||||
@@ -135,13 +198,28 @@ async fn receive(req: HttpRequest, Session { key, body }: Session) -> impl Respo
|
||||
user_id: uid,
|
||||
master_mission_id: mid,
|
||||
status: 3,
|
||||
progress: if expire != 0 { m["progress"].clone() } else { JsonValue::Null },
|
||||
progress: if expire != 0 || super::beginner_mission::is_counter(mid) { m["progress"].clone() } else { JsonValue::Null },
|
||||
expire_date: if expire != 0 { global::format_datetime(expire).into() } else { JsonValue::Null },
|
||||
received_date: global::format_datetime(now)
|
||||
}).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
for master in super::beginner_mission::new_rewards(&before, &missions) {
|
||||
let reward_type = master["type"].as_i64().unwrap();
|
||||
rewards.push(object! {
|
||||
give_type: master["giveType"].clone(), type: master["type"].clone(),
|
||||
value: master["value"].clone(), level: master["level"].clone(), amount: master["amount"].clone()
|
||||
}).unwrap();
|
||||
items::give_gift(&object! {
|
||||
reward_type: reward_type, value: master["value"].clone(), amount: master["amount"].clone()
|
||||
}, &mut user, &mut missions, &mut array![], &mut chats);
|
||||
match reward_type {
|
||||
1 => touched_gem = true,
|
||||
4 => touched_coin = true,
|
||||
_ => { touched_items.push(master["value"].clone()).unwrap(); }
|
||||
}
|
||||
}
|
||||
userdata::save_acc(&key, user.clone());
|
||||
userdata::save_acc_chats(&key, chats);
|
||||
userdata::save_acc_missions(&key, missions);
|
||||
@@ -152,7 +230,9 @@ async fn receive(req: HttpRequest, Session { key, body }: Session) -> impl Respo
|
||||
}
|
||||
if !touched_items.is_empty() {
|
||||
let mut item_list = array![];
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
for id in touched_items.members() {
|
||||
if !seen.insert(id.as_i64()) { continue; }
|
||||
for item in user["item_list"].members() {
|
||||
if item["master_item_id"] == *id {
|
||||
item_list.push(item.clone()).unwrap();
|
||||
@@ -176,6 +256,9 @@ async fn receive(req: HttpRequest, Session { key, body }: Session) -> impl Respo
|
||||
updated_value_list["point_list"] = point_list;
|
||||
}
|
||||
|
||||
let mut sorted: Vec<_> = mission_list.members().cloned().collect();
|
||||
sorted.sort_by_key(|m| m["master_mission_id"].as_i64());
|
||||
let mission_list: JsonValue = sorted.into();
|
||||
Api(Some(object!{
|
||||
"reward_list": rewards,
|
||||
"gift_list": array![],
|
||||
|
||||
@@ -404,6 +404,7 @@ async fn end(req: HttpRequest, Session { key, body }: Session) -> impl Responder
|
||||
// live_end scales exp / gold / bond / mission rewards off.
|
||||
let mut end_body = body.clone();
|
||||
end_body["use_lp"] = lp_used.into();
|
||||
end_body["live_boost"] = live_boost.into();
|
||||
if end_body["deck_slot"].is_null() {
|
||||
if let Some(slot) = started.and_then(|s| s["deck_slot"].as_i32()) {
|
||||
end_body["deck_slot"] = slot.into();
|
||||
|
||||
+10
-2
@@ -48,14 +48,17 @@ async fn deck(Session { key, body }: Session) -> impl Responder {
|
||||
}
|
||||
}
|
||||
userdata::save_acc(&key, user.clone());
|
||||
|
||||
let mut missions = userdata::get_acc_missions(&key);
|
||||
let cleared = super::beginner_mission::refresh(&user, &mut missions);
|
||||
userdata::save_acc_missions(&key, missions);
|
||||
|
||||
Api(Some(object!{
|
||||
"deck": {
|
||||
"slot": body["slot"].clone(),
|
||||
"leader_role": 0,
|
||||
"main_card_ids": body["main_card_ids"].clone()
|
||||
},
|
||||
"clear_mission_ids": []
|
||||
"clear_mission_ids": cleared
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -212,6 +215,11 @@ async fn register_password(Session { key, body }: Session) -> impl Responder {
|
||||
let user = userdata::get_acc(&key);
|
||||
|
||||
userdata::user::migration::save_acc_transfer(user["user"]["id"].as_i64().unwrap(), &body["pass"].to_string());
|
||||
if !body["pass"].as_str().unwrap_or("").is_empty() {
|
||||
let mut missions = userdata::get_acc_missions(&key);
|
||||
super::beginner_mission::advance(25, None, 1, &mut missions);
|
||||
userdata::save_acc_missions(&key, missions);
|
||||
}
|
||||
|
||||
Api(Some(array![]))
|
||||
}
|
||||
|
||||
@@ -357,7 +357,9 @@ pub fn get_acc_home(auth_key: &str) -> JsonValue {
|
||||
user
|
||||
}
|
||||
pub fn get_acc_missions(auth_key: &str) -> JsonValue {
|
||||
get_data(auth_key, "missions")
|
||||
let mut missions = get_data(auth_key, "missions");
|
||||
super::beginner_mission::ensure(&mut missions);
|
||||
missions
|
||||
}
|
||||
pub fn get_acc_loginbonus(auth_key: &str) -> JsonValue {
|
||||
get_data(auth_key, "loginbonus")
|
||||
|
||||
Reference in New Issue
Block a user