extends RefCounted
## Zone 2 of docs/ZONES_ROADMAP.md, Frostveil Tundra (Lv 110-124 field) + Frostveil Hollow (Lv 124-130 ice cave),
## selftest --only=zone2 (also at the end of the full run): data (9 scaled monsters with own names + cards, basic
## loot + plain Lv 110 gear, 3 quests, NPC, 2 maps, portals Blossom Meadows <-> Tundra <-> Hollow), the snow field
## built on the client (snow ground, frozen lakes, ice crystals, frosted trees) and on the server (monsters, crystal
## colliders on the map's own layer), the ice cave (floor, crystals, mini-boss), music keys.

const SelfTest = preload("res://scripts/selftest.gd")
const WorldBuilder = preload("res://scripts/world_builder.gd")
const EnvFrost = preload("res://scripts/env_frost.gd")
const EnvLayout = preload("res://scripts/env_layout.gd")

const FIELD_MONS := ["frost_pup", "snowcap_seal", "rime_wisp", "drift_yeti", "frostfang_stalker", "glacier_golem"]
const CAVE_MONS := ["shardling", "rimeback_drake", "hoarfrost_behemoth"]
const GEAR := ["frostbrand_sword", "icicle_pike", "rimewood_bow", "glacierglass_staff", "frostward_mail", "snowdrift_cloak"]


static func check(ok: bool, what: String) -> void:
	SelfTest.check(ok, "zone2: " + what)


static func _bodies_at(main: Node, p: Vector3, mask: int) -> Array:
	var q := PhysicsPointQueryParameters3D.new()
	q.position = Vector3(p.x, 1.0, p.z)
	q.collision_mask = mask
	q.collide_with_areas = false
	return main.get_world_3d().direct_space_state.intersect_point(q, 8)


static func _portal_to(to: String) -> int:
	for i in WorldBuilder.portals().size():
		if str(WorldBuilder.portals()[i]["to_map"]) == to:
			return i
	return -1


static func run(main: Node) -> void:
	print("SELFTEST zone2")
	data_tests()
	await world_tests(main)


static func data_tests() -> void:
	var bad: Array = []
	for mid in FIELD_MONS + CAVE_MONS:
		var d: Dictionary = GameData.monsters.get(mid, {})
		if d.is_empty():
			bad.append(mid + " missing")
			continue
		if int(d["lv"]) < 110 or int(d["lv"]) > 130 or str(d.get("name", "")) == "":
			bad.append(mid + " lv/name")
		if int(d["hp"]) < 10000 or int(d["exp"]) <= 0 or int(d["def"]) > 70 or int(d["mdef"]) > 70:
			bad.append(mid + " stats")
		var card := false
		for dr in d.get("drops", []):
			if GameData.item(str(dr["id"])).is_empty():
				bad.append(mid + " drop " + str(dr["id"]))
			if str(dr["id"]) == mid + "_card" and is_equal_approx(float(dr["chance"]), 0.01):
				card = true
		if not card:
			bad.append(mid + " card")
	check(bad.is_empty(), "9 monsters Lv 110-130, own names, DEF/MDEF <= 70, loot + a 0.01%% card each %s" % str(bad))
	var boss: Dictionary = GameData.monsters.get("hoarfrost_behemoth", {})
	check(bool(boss.get("miniboss", false)) and not bool(boss.get("boss", true)) and int(boss.get("lv", 0)) == 130, "Hoarfrost Behemoth: Lv 130 mini-boss (normal class)")
	var gear_ok := true
	for g in GEAR:
		var it: Dictionary = GameData.item(g)
		gear_ok = gear_ok and str(it.get("type", "")) == "equip" and int(it.get("req_lv", 0)) == 110
	check(gear_ok, "6 plain tier pieces, Base Lv 110")
	for mp in [["frostveil_tundra", 67], ["frostveil_hollow", 25]]:
		var m: Dictionary = GameData.map(str(mp[0]))
		var total := 0
		for z in m.get("zones", []):
			for e in z["mobs"]:
				total += int(e["count"])
				if not GameData.monsters.has(str(e["id"])):
					total = -999
		check(not m.is_empty() and total == int(mp[1]) and total == int(m.get("monster_total", -1)), "%s: %d monsters" % [str(mp[0]), total])
	var from_blossom := false
	for pt in GameData.map("blossom_meadows").get("portals", []):
		if str(pt["to_map"]) == "frostveil_tundra":
			from_blossom = true
	check(from_blossom, "Blossom Meadows has a warp to the tundra")
	check(int(GameData.map("frostveil_tundra")["look"].get("ground_kind", 0)) == 3 and str(GameData.map("frostveil_tundra")["env"]["kind"]) == "snow",
		"tundra: snow ground + snow scenery kind")
	var qok := true
	for q in ["frost_yetis", "frost_cores", "frost_behemoth"]:
		var qd: Dictionary = GameData.quest(q)
		qok = qok and not qd.is_empty() and str(qd.get("giver", "")) == "frost_trapper"
	check(qok and not SelfTest.find_npc("frost_trapper").is_empty(), "3 quests from Trapper Olin")


static func world_tests(main: Node) -> void:
	var tree = main.get_tree()
	var pl = main.player
	SelfTest.make_job(main, "swordman")
	PlayerState.ch.base_lv = 110
	main.me().dead = false
	PlayerState.hp = PlayerState.max_hp()
	main.load_map("blossom_meadows")
	await SelfTest.wait_map(main, "blossom_meadows")
	await SelfTest.frames(main, 3)
	var idx := _portal_to("frostveil_tundra")
	check(idx >= 0, "warp Blossom Meadows -> tundra present")
	if idx < 0:
		return
	var to: String = await SelfTest.take_portal(main, idx)
	check(to == "frostveil_tundra" and main.map_id == to and pl.global_position.distance_to(Vector3(-76, 0, 0)) < 1.0, "portal: Blossom Meadows -> Frostveil Tundra")
	await SelfTest.frames(main, 3)
	PlayerState.ch.add_buff("hiding", 600.0, 10)
	var mids: Dictionary = SelfTest.mob_ids(main)
	var all := true
	for mid in FIELD_MONS:
		all = all and mids.has(mid)
	check(tree.get_nodes_in_group("monsters").size() == 67 and all, "67 monsters, the whole field set (%d)" % tree.get_nodes_in_group("monsters").size())
	check(EnvLayout.kind() == "snow" and int(main.last_build.get("trees", 0)) > 80, "snow field built (%s trees)" % str(main.last_build.get("trees", 0)))
	check(tree.get_nodes_in_group("frozen_lakes").size() == 2 and tree.get_nodes_in_group("ice_crystals").size() == 1, "2 frozen lakes + ice crystal fields drawn")
	check(tree.get_nodes_in_group("ice_crystal_colliders").size() >= 18, "ice crystal clusters with colliders (%d)" % tree.get_nodes_in_group("ice_crystal_colliders").size())
	check(WorldBuilder.music_area(Vector3(-70, 0, 0)) == "field_7" and main.music.fallbacks("field_7") == ["field_7", "field"], "tundra music field_7 -> field")
	var mi = main.server.maps.get("frostveil_tundra", null)
	var cc: Vector3 = EnvFrost.clusters(Vector3(2, 0, -18), 5, 4.0)[0]
	check(mi != null and not _bodies_at(main, cc, mi.col_layer).is_empty(), "server collider on an ice crystal cluster (%s)" % str(cc))
	check(mi != null and mi.walkable(Vector3(-8, 0, 20)), "the frozen lake can be walked on")
	# the ice cave
	var ci := _portal_to("frostveil_hollow")
	var cave: String = await SelfTest.take_portal(main, ci)
	check(cave == "frostveil_hollow" and main.map_id == cave and WorldBuilder.walkable(pl.global_position), "portal: tundra -> Frostveil Hollow (on the cave floor)")
	await SelfTest.frames(main, 3)
	PlayerState.ch.add_buff("hiding", 600.0, 10)
	var cm: Dictionary = SelfTest.mob_ids(main)
	check(tree.get_nodes_in_group("monsters").size() == 25 and cm.has("shardling") and cm.has("rimeback_drake") and cm.has("hoarfrost_behemoth"),
		"hollow: 25 monsters incl. the Hoarfrost Behemoth (%d)" % tree.get_nodes_in_group("monsters").size())
	check(tree.get_nodes_in_group("cave_crystals").size() >= 20, "hollow: glowing ice crystals (%d)" % tree.get_nodes_in_group("cave_crystals").size())
	var bh = SelfTest.nearest(main, "hoarfrost_behemoth")
	var bv = main.view(bh.id) if bh else null
	check(bv != null and bv.is_mini and absf(bv.scl - 1.9) < 0.01, "Hoarfrost Behemoth: mini-boss x1.9 in its den")
	check(WorldBuilder.music_area(Vector3(-50, 0, 0)) == "dungeon_4", "hollow music dungeon_4")
	PlayerState.ch.remove_buff("hiding")
	var back: String = await SelfTest.take_portal(main, 0)
	check(back == "frostveil_tundra" and pl.global_position.distance_to(Vector3(50, 0, -66)) < 1.0, "portal: Hollow -> tundra by the cave mouth")
	var home: String = await SelfTest.take_portal(main, _portal_to("blossom_meadows"))
	check(home == "blossom_meadows" and pl.global_position.distance_to(Vector3(76, 0, -50)) < 1.0, "portal: tundra -> Blossom Meadows")
	main.load_map(GameData.start_map())
	await SelfTest.wait_map(main, GameData.start_map())
