diff --git a/data/recipes.json b/data/recipes.json index e8904c5..d7cfdec 100644 --- a/data/recipes.json +++ b/data/recipes.json @@ -5,6 +5,7 @@ "output": "stone_axe", "count": 1, "station": "", + "category": "tools", "inputs": { "stick": 2, "flint": 2 @@ -15,6 +16,7 @@ "output": "stone_pickaxe", "count": 1, "station": "", + "category": "tools", "inputs": { "stick": 2, "stone": 3, @@ -26,6 +28,7 @@ "output": "campfire", "count": 1, "station": "", + "category": "stations", "inputs": { "stone": 5, "stick": 4 @@ -36,6 +39,7 @@ "output": "workbench", "count": 1, "station": "", + "category": "stations", "inputs": { "wood": 10, "flint": 2 @@ -46,6 +50,7 @@ "output": "club", "count": 1, "station": "", + "category": "weapons", "inputs": { "wood": 4 } @@ -55,6 +60,7 @@ "output": "chest", "count": 1, "station": "workbench", + "category": "furniture", "inputs": { "wood": 10, "stick": 4 @@ -65,6 +71,7 @@ "output": "torch", "count": 1, "station": "", + "category": "tools", "inputs": { "stick": 1, "fiber": 2 @@ -75,6 +82,7 @@ "output": "roasted_mushroom", "count": 1, "station": "campfire", + "category": "cooking", "inputs": { "mushroom": 1 } @@ -84,6 +92,7 @@ "output": "wood_floor", "count": 1, "station": "workbench", + "category": "building", "inputs": { "wood": 2 } @@ -93,6 +102,7 @@ "output": "wood_wall", "count": 1, "station": "workbench", + "category": "building", "inputs": { "wood": 2 } @@ -102,6 +112,7 @@ "output": "wood_door", "count": 1, "station": "workbench", + "category": "building", "inputs": { "wood": 4 } @@ -111,6 +122,7 @@ "output": "wood_roof", "count": 1, "station": "workbench", + "category": "building", "inputs": { "wood": 2, "fiber": 1 @@ -121,6 +133,7 @@ "output": "wood_gable", "count": 1, "station": "workbench", + "category": "building", "inputs": { "wood": 1 } @@ -130,6 +143,7 @@ "output": "bed", "count": 1, "station": "workbench", + "category": "furniture", "inputs": { "wood": 8, "fiber": 6 @@ -140,6 +154,7 @@ "output": "cooked_meat", "count": 1, "station": "campfire", + "category": "cooking", "inputs": { "raw_meat": 1 } @@ -149,6 +164,7 @@ "output": "spear", "count": 1, "station": "workbench", + "category": "weapons", "inputs": { "wood": 3, "flint": 2, @@ -160,6 +176,7 @@ "output": "bow", "count": 1, "station": "workbench", + "category": "weapons", "inputs": { "wood": 4, "fiber": 6 @@ -170,6 +187,7 @@ "output": "arrow", "count": 5, "station": "workbench", + "category": "weapons", "inputs": { "stick": 2, "flint": 1, @@ -181,6 +199,7 @@ "output": "hide_cloak", "count": 1, "station": "workbench", + "category": "clothing", "inputs": { "hide": 4, "fiber": 4 @@ -191,6 +210,7 @@ "output": "wolf_cloak", "count": 1, "station": "workbench", + "category": "clothing", "inputs": { "wolf_pelt": 3, "hide": 2, @@ -202,6 +222,7 @@ "output": "fishing_rod", "count": 1, "station": "workbench", + "category": "tools", "inputs": { "stick": 2, "fiber": 3, @@ -213,6 +234,7 @@ "output": "grilled_perch", "count": 1, "station": "campfire", + "category": "cooking", "inputs": { "perch": 1 } @@ -222,6 +244,7 @@ "output": "grilled_trout", "count": 1, "station": "campfire", + "category": "cooking", "inputs": { "trout": 1 } diff --git a/scripts/core/item_db.gd b/scripts/core/item_db.gd index 962c88d..49c9deb 100644 --- a/scripts/core/item_db.gd +++ b/scripts/core/item_db.gd @@ -52,8 +52,29 @@ func define_recipe(r: Dictionary) -> void: return r.count = int(r.get("count", 1)) r.station = String(r.get("station", "")) + r.category = String(r.get("category", guess_category(items[r.output]))) + if not CATEGORIES.has(r.category): + r.category = "other" recipes[r.id] = r +## The crafting screen's tabs, in order: id -> label. +const CATEGORIES := {"tools": "Tools", "weapons": "Weapons", "cooking": "Cooking", "building": "Building", + "stations": "Stations", "furniture": "Furniture", "clothing": "Clothing", "other": "Other"} + +## For recipes (an addon's, say) that don't say: worked out from what they make. +static func guess_category(item: Dictionary) -> String: + if item.has("food"): + return "cooking" + if item.has("weapon") or item.has("bow"): + return "weapons" + if item.has("tool") or item.has("fishing") or item.has("light"): + return "tools" + if item.has("wear"): + return "clothing" + if item.has("place"): + return "building" if item.place.has("kind") else "furniture" + return "other" + func _load_fishing(path: String) -> void: var data = JSON.parse_string(FileAccess.get_file_as_string(path)) if typeof(data) != TYPE_DICTIONARY or not data.has("waters"): diff --git a/scripts/ui/inventory_screen.gd b/scripts/ui/inventory_screen.gd index fc3d114..fdf19c4 100644 --- a/scripts/ui/inventory_screen.gd +++ b/scripts/ui/inventory_screen.gd @@ -100,8 +100,14 @@ func bind(p: CharacterBody3D) -> void: crv.add_child(UITheme.label("Crafting", 20)) _station_label = UITheme.label("", 13, UITheme.TEXT_DIM) crv.add_child(_station_label) + # Categories: Tools, Weapons, Cooking... (ItemDB.CATEGORIES). Two rows + # of tabs, so they all fit. + _tabs = HFlowContainer.new() + _tabs.add_theme_constant_override("h_separation", 4) + _tabs.add_theme_constant_override("v_separation", 4) + crv.add_child(_tabs) var scroll := ScrollContainer.new() - scroll.custom_minimum_size = Vector2(320, 380) + scroll.custom_minimum_size = Vector2(320, 330) scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED _recipe_list = VBoxContainer.new() _recipe_list.size_flags_horizontal = Control.SIZE_EXPAND_FILL @@ -139,6 +145,7 @@ func open(chest: Node) -> void: visible = true player.ui_open = true Input.mouse_mode = Input.MOUSE_MODE_VISIBLE + _pick_tab() _rebuild_chest() _refresh() @@ -250,8 +257,59 @@ func _rebuild_recipes() -> void: if Actions.station_near(player, st): stations.append(st) _station_label.text = "Nearby: " + (", ".join(stations) if not stations.is_empty() else "nothing (hand crafting only)") + _rebuild_tabs() for rid in ItemDB.recipes: - _recipe_list.add_child(_recipe_row(ItemDB.recipes[rid])) + var r: Dictionary = ItemDB.recipes[rid] + if r.category == category: + _recipe_list.add_child(_recipe_row(r)) + +var _tabs: HFlowContainer +var category := "tools" ## the tab showing +var _picked := false ## chose a tab since opening (don't jump about) + +## Tabs for the categories that have recipes, each with how many of its +## recipes you could make right now. +func _rebuild_tabs() -> void: + for c in _tabs.get_children(): + c.queue_free() + var count := {} + var ready := {} + for rid in ItemDB.recipes: + var r: Dictionary = ItemDB.recipes[rid] + count[r.category] = int(count.get(r.category, 0)) + 1 + if Actions.can_craft(player, rid): + ready[r.category] = int(ready.get(r.category, 0)) + 1 + if not count.has(category): + category = count.keys()[0] if not count.is_empty() else "tools" + for cat in ItemDB.CATEGORIES: + if not count.has(cat): + continue + var b := Button.new() + var n := int(ready.get(cat, 0)) + b.text = ItemDB.CATEGORIES[cat] + (" (%d)" % n if n > 0 else "") + b.toggle_mode = true + b.button_pressed = cat == category + b.focus_mode = Control.FOCUS_NONE + b.add_theme_font_size_override("font_size", 13) + if n > 0 and cat != category: + b.add_theme_color_override("font_color", UITheme.OK) + b.pressed.connect(func(): + category = cat + _picked = true + _rebuild_recipes()) + _tabs.add_child(b) + +## On opening: Cooking if you're at a lit fire with something to cook, +## otherwise the tab you were on last. +func _pick_tab() -> void: + _picked = false + if not Actions.station_near(player, "campfire"): + return + for rid in ItemDB.recipes: + var r: Dictionary = ItemDB.recipes[rid] + if r.category == "cooking" and Actions.can_craft(player, rid): + category = "cooking" + return func _recipe_row(r: Dictionary) -> Control: var ok := Actions.can_craft(player, r.id) @@ -275,7 +333,7 @@ func _recipe_row(r: Dictionary) -> Control: req.bbcode_enabled = true req.fit_content = true req.scroll_active = false - req.autowrap_mode = TextServer.AUTOWRAP_OFF + req.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART # (long lists wrap rather than widen the panel) req.add_theme_font_size_override("normal_font_size", 13) var parts: Array[String] = [] for id in r.inputs: @@ -287,7 +345,7 @@ func _recipe_row(r: Dictionary) -> Control: var col := UITheme.OK if Actions.station_near(player, r.station) else UITheme.BAD parts.append("[color=#%s]@ %s[/color]" % [col.to_html(false), r.station]) req.text = " ".join(parts) - req.custom_minimum_size = Vector2(200, 0) + req.custom_minimum_size = Vector2(170, 0) v.add_child(req) h.add_child(v) var b := Button.new() diff --git a/tests/test_net_world.gd b/tests/test_net_world.gd index e7e029c..ce7151a 100644 --- a/tests/test_net_world.gd +++ b/tests/test_net_world.gd @@ -107,9 +107,18 @@ func _body() -> void: var end_anim: int = av.anim if is_instance_valid(av) else -1 var br := await _result(bob) var moving := samples.filter(func(s): return s[1] > 0.5) + # Top speed over 0.2 s windows (4 samples): one late frame on this machine + # reads as a burst in a single 50 ms sample, while a real jump (half a + # metre extra) still shows as over 6.5 m/s. var top := 0.0 - for s in samples: - top = maxf(top, s[1]) + for i in range(3, samples.size()): + var dt_sum := 0.0 + var dist := 0.0 + for j in range(i - 3, i + 1): + var dtj: float = samples[j][0] - (samples[j - 1][0] if j > 0 else 0.0) + dt_sum += dtj + dist += float(samples[j][1]) * dtj + top = maxf(top, dist / maxf(dt_sum, 0.001)) var stalls := 0 if moving.size() > 2: # Count gaps inside the walking stretch where he froze.