Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions plugins/career/passports.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@
"songs": 5,
"min_stars": 2
},
"families": [
{ "key": "metal", "match": ["metal", "djent", "grindcore", "thrash", "doom"] },
{ "key": "blues", "match": ["blues"] },
{ "key": "jazz", "match": ["jazz", "bebop", "swing", "bossa"] },
{ "key": "funk", "match": ["funk", "disco"] },
{ "key": "rock", "match": ["rock", "punk", "grunge", "shoegaze"] }
],
"genres": {
"blues": { "virtuoso_nodes": { "guitar": ["blues_shuffle"] } },
"rock": { "virtuoso_nodes": { "guitar": ["rock_power_backbeat"] } },
Expand Down
23 changes: 22 additions & 1 deletion plugins/career/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,12 +276,33 @@ def _library_genres():
key=lambda r: (-r["songs_in_library"], r["genre_key"]))


def _genre_family(gkey):
"""First family whose keyword appears in the genre key (substring — MB's
vocabulary is open: 'metalcore' must hit the 'metal' family without an
exact alias). List order decides ambiguity: families are checked top to
bottom, so 'blues rock' lands on whichever of blues/rock is listed first."""
for fam in _state["passports_content"].get("families") or []:
if not isinstance(fam, dict):
continue
for kw in fam.get("match") or []:
if isinstance(kw, str) and kw and kw in gkey:
return fam.get("key")
return None


def _badge_requirement(gkey, instrument="guitar"):
cfg = _state["passports_content"]
req = dict(cfg.get("badge_requirement") or {})
req.setdefault("songs", 5)
req.setdefault("min_stars", 2)
override = (cfg.get("genres") or {}).get(gkey)
# Exact per-genre override wins; otherwise the genre inherits its FAMILY's
# requirement — so 'death metal' / 'metalcore' passports carry the metal
# drill without curating every MB sub-genre by hand.
genres_cfg = cfg.get("genres") or {}
override = genres_cfg.get(gkey)
if not isinstance(override, dict):
family = _genre_family(gkey)
override = genres_cfg.get(family) if family else None
if isinstance(override, dict):
req.update(override)
# virtuoso_nodes: {instrument: [node_ids]} — a passport only carries its
Expand Down
37 changes: 37 additions & 0 deletions tests/plugins/career/test_passports.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,3 +208,40 @@ def test_drill_state_merge_is_gained_only(client, meta_db):
p = _passport(client)
assert p["drills"]["cleared"] == ["blues_shuffle"]
assert p["badge"] == "earned"


def test_genre_families_inherit_drills(client, meta_db):
# 'death metal' has no exact entry — it inherits the metal family's drill.
for i in range(5):
meta_db.add(f"dm{i}.feedpak", 0, 0.9, genre="Death Metal", arrangements=LEAD)
_open(client, "guitar", "Death Metal")
p = _passport(client, "guitar", "death metal")
assert p["drills"]["required"] == ["melodic_metal_gallop"]
assert p["badge"] == "in_progress"
# 'metalcore' (single word) matches by substring, no alias needed.
_open(client, "guitar", "Metalcore")
assert _passport(client, "guitar", "metalcore")["drills"]["required"] == \
["melodic_metal_gallop"]
# 'blues rock' resolves by family LIST ORDER: blues comes before rock.
_open(client, "guitar", "Blues Rock")
assert _passport(client, "guitar", "blues rock")["drills"]["required"] == \
["blues_shuffle"]
# A genre outside every family stays songs-only.
_open(client, "guitar", "Reggae")
assert _passport(client, "guitar", "reggae")["drills"]["required"] == []
# Exact per-genre entries still beat the family (the shipped 'metal' entry
# IS the exact entry for genre key 'metal').
_open(client, "guitar", "Metal")
assert _passport(client, "guitar", "metal")["drills"]["required"] == \
["melodic_metal_gallop"]


def test_family_drills_stay_per_instrument(client, meta_db):
# Family inheritance must not leak guitar drills onto other instruments.
keys_arr = [{"type": "lead", "name": "Keys"}]
for i in range(5):
meta_db.add(f"kdm{i}.feedpak", 0, 0.9, genre="Death Metal", arrangements=keys_arr)
_open(client, "keys", "Death Metal")
p = _passport(client, "keys", "death metal")
assert p["drills"]["required"] == []
assert p["badge"] == "earned"
Loading