feat(@projects/@magic-civilization): ✨ add dire wolf and frostfang alpha units
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
118621d8e8
commit
400425585a
8 changed files with 276 additions and 79 deletions
|
|
@ -1,32 +0,0 @@
|
|||
[
|
||||
{
|
||||
"id": "wyvern_riders",
|
||||
"name": "Wyvern Riders",
|
||||
"description": "Aerial warriors mounted on scaled wyverns. Fast and dangerous, bypass ground zones of control.",
|
||||
"gender": {
|
||||
"male": { "name": "Wyvern Riders", "sprite": "sprites/units/wyvern_riders_m.png" },
|
||||
"female": { "name": "Wyvern Riders", "sprite": "sprites/units/wyvern_riders_f.png" }
|
||||
},
|
||||
"combat_type": "flying",
|
||||
"school": null,
|
||||
"domain": "air",
|
||||
"armor_type": "natural",
|
||||
"attack_type": "melee_physical",
|
||||
"int": 4,
|
||||
"dex": 12,
|
||||
"str": 16,
|
||||
"con": 12,
|
||||
"cost": 70,
|
||||
"range": 1,
|
||||
"movement": 4,
|
||||
"vision": 4,
|
||||
"tech_required": "orc_heritage",
|
||||
"race_required": "orcs",
|
||||
"faction": null,
|
||||
"keywords": [],
|
||||
"resistances": {},
|
||||
"merge": null,
|
||||
"mana_cost": null,
|
||||
"sprite": "sprites/units/wyvern_riders.png"
|
||||
}
|
||||
]
|
||||
|
|
@ -65,6 +65,11 @@
|
|||
"transcendence_ritual": "Ascension Ritual",
|
||||
"domination": "Domination",
|
||||
"score": "Score",
|
||||
"victory": "Victory",
|
||||
"defeat": "Defeat",
|
||||
"stalemate": "Stalemate",
|
||||
"wins": "wins!",
|
||||
"turn_limit_reached": "Turn limit reached",
|
||||
"ruin_site": "Tribal Village",
|
||||
"threat_site": "Lair",
|
||||
"independent_settlement": "Freepeople Haven",
|
||||
|
|
|
|||
|
|
@ -167,7 +167,11 @@
|
|||
"id": "ancient_construct_site",
|
||||
"loot_table": [
|
||||
{ "resource": "arcane_gears", "amount": 2, "chance": 0.8 },
|
||||
{ "resource": "stone_core", "amount": 1, "chance": 0.5 }
|
||||
{ "resource": "stone_core", "amount": 1, "chance": 0.5 },
|
||||
{ "type": "item", "item": "golem_core", "tier": 8, "chance": 0.05 },
|
||||
{ "type": "item", "item": "constructor_lens", "tier": 8, "chance": 0.05 },
|
||||
{ "type": "item", "item": "phase_gauntlet", "tier": 9, "chance": 0.025 },
|
||||
{ "type": "item", "item": "crown_of_the_mountain", "tier": 10, "chance": 0.01 }
|
||||
]
|
||||
},
|
||||
"wyvern_nest": {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,19 @@
|
|||
extends CanvasLayer
|
||||
## Full-screen victory overlay. Triggered by EventBus.victory_achieved.
|
||||
## Shows winning player, victory type, score, turn count.
|
||||
## "Main Menu" returns to menu; "Continue" lets the player keep playing.
|
||||
## Full-screen end-game overlay. Triggered by EventBus.victory_achieved.
|
||||
## Shows winner (or stalemate on winner_index == -1), victory type, turn,
|
||||
## and a per-player stats table (pop / cities / tiles / techs / units / score).
|
||||
|
||||
const VictoryManagerScript: GDScript = preload(
|
||||
"res://engine/src/modules/victory/victory_manager.gd")
|
||||
const CityScript: GDScript = preload("res://engine/src/entities/city.gd")
|
||||
|
||||
const STAT_COLS: Array[String] = ["Player", "Pop", "Cities", "Tiles", "Techs", "Units", "Score"]
|
||||
|
||||
@onready var _result_label: Label = %ResultLabel
|
||||
@onready var _condition_label: Label = %ConditionLabel
|
||||
@onready var _score_label: Label = %ScoreLabel
|
||||
@onready var _turn_label: Label = %TurnLabel
|
||||
@onready var _player_label: Label = %PlayerLabel
|
||||
@onready var _stats_grid: GridContainer = %StatsGrid
|
||||
@onready var _main_menu_button: Button = %MainMenuButton
|
||||
@onready var _continue_button: Button = %ContinueButton
|
||||
|
||||
|
|
@ -21,43 +27,95 @@ func _ready() -> void:
|
|||
|
||||
|
||||
func _on_victory_achieved(player_index: int, victory_type: String) -> void:
|
||||
var player: RefCounted = GameState.get_player(player_index)
|
||||
var player_name: String = "Unknown"
|
||||
if player != null:
|
||||
if not player.player_name.is_empty():
|
||||
player_name = player.player_name
|
||||
else:
|
||||
player_name = "Player %d" % (player_index + 1)
|
||||
var stalemate: bool = player_index < 0 or victory_type == "stalemate"
|
||||
var player: RefCounted = null if stalemate else GameState.get_player(player_index)
|
||||
|
||||
var is_human_winner: bool = player != null and player.is_human
|
||||
|
||||
if is_human_winner:
|
||||
_result_label.text = ThemeVocabulary.lookup("victory")
|
||||
if _result_label.text == "victory":
|
||||
_result_label.text = "VICTORY"
|
||||
if stalemate:
|
||||
_result_label.text = ThemeVocabulary.lookup("stalemate").to_upper()
|
||||
_result_label.add_theme_color_override("font_color", Color(0.75, 0.75, 0.78))
|
||||
_player_label.text = ThemeVocabulary.lookup("turn_limit_reached")
|
||||
elif player != null and player.is_human:
|
||||
_result_label.text = ThemeVocabulary.lookup("victory").to_upper()
|
||||
_result_label.add_theme_color_override("font_color", Color(1.0, 0.85, 0.2))
|
||||
_player_label.text = "%s %s" % [_player_display(player), ThemeVocabulary.lookup("wins")]
|
||||
else:
|
||||
_result_label.text = ThemeVocabulary.lookup("defeat")
|
||||
if _result_label.text == "defeat":
|
||||
_result_label.text = "DEFEAT"
|
||||
_result_label.text = ThemeVocabulary.lookup("defeat").to_upper()
|
||||
_result_label.add_theme_color_override("font_color", Color(0.8, 0.3, 0.3))
|
||||
|
||||
_player_label.text = "%s wins!" % player_name
|
||||
_player_label.text = "%s %s" % [_player_display(player), ThemeVocabulary.lookup("wins")]
|
||||
|
||||
var condition_display: String = ThemeVocabulary.lookup(victory_type)
|
||||
if condition_display == victory_type:
|
||||
condition_display = victory_type.capitalize()
|
||||
_condition_label.text = "%s %s" % [condition_display, ThemeVocabulary.lookup("victory")]
|
||||
|
||||
var score: int = player.score if player != null and "score" in player else 0
|
||||
_score_label.text = "Final Score: %d" % score
|
||||
_turn_label.text = "Turn %d" % GameState.turn_number
|
||||
if stalemate:
|
||||
_condition_label.text = condition_display
|
||||
else:
|
||||
_condition_label.text = "%s %s" % [condition_display, ThemeVocabulary.lookup("victory")]
|
||||
_turn_label.text = "%s %d" % [ThemeVocabulary.lookup("turn"), GameState.turn_number]
|
||||
|
||||
_build_stats_grid(player_index)
|
||||
visible = true
|
||||
get_tree().paused = true
|
||||
_continue_button.grab_focus()
|
||||
|
||||
|
||||
func _build_stats_grid(winner_index: int) -> void:
|
||||
for child: Node in _stats_grid.get_children():
|
||||
child.queue_free()
|
||||
for col: String in STAT_COLS:
|
||||
_stats_grid.add_child(_make_header(col))
|
||||
|
||||
var vm: VictoryManagerScript = VictoryManagerScript.new()
|
||||
var game_map: RefCounted = GameState.get_game_map() as RefCounted
|
||||
for p: Variant in GameState.players:
|
||||
if p == null or int(p.get("index")) < 0:
|
||||
continue
|
||||
var is_winner: bool = int(p.get("index")) == winner_index
|
||||
var pop: int = 0
|
||||
var tiles: int = 0
|
||||
for c: Variant in p.cities:
|
||||
if c is CityScript:
|
||||
pop += (c as CityScript).get_population()
|
||||
tiles += (c as CityScript).get_owned_tiles().size()
|
||||
var row_color: Color = Color(1.0, 0.9, 0.35) if is_winner else Color(0.88, 0.88, 0.88)
|
||||
var prefix: String = "* " if is_winner else " "
|
||||
_stats_grid.add_child(_make_cell(prefix + _player_display(p), row_color, true))
|
||||
_stats_grid.add_child(_make_cell(str(pop), row_color))
|
||||
_stats_grid.add_child(_make_cell(str(p.cities.size()), row_color))
|
||||
_stats_grid.add_child(_make_cell(str(tiles), row_color))
|
||||
_stats_grid.add_child(_make_cell(str(p.researched_techs.size()), row_color))
|
||||
_stats_grid.add_child(_make_cell(str(p.units.size()), row_color))
|
||||
_stats_grid.add_child(_make_cell(str(vm.calculate_score(p, game_map)), row_color))
|
||||
|
||||
|
||||
func _make_header(text: String) -> Label:
|
||||
var lbl: Label = Label.new()
|
||||
lbl.text = text
|
||||
lbl.add_theme_font_size_override("font_size", 13)
|
||||
lbl.add_theme_color_override("font_color", Color(0.6, 0.55, 0.35))
|
||||
if text != "Player":
|
||||
lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||||
return lbl
|
||||
|
||||
|
||||
func _make_cell(text: String, color: Color, is_name: bool = false) -> Label:
|
||||
var lbl: Label = Label.new()
|
||||
lbl.text = text
|
||||
lbl.add_theme_font_size_override("font_size", 14)
|
||||
lbl.add_theme_color_override("font_color", color)
|
||||
if not is_name:
|
||||
lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||||
return lbl
|
||||
|
||||
|
||||
func _player_display(player: RefCounted) -> String:
|
||||
if player == null:
|
||||
return "Unknown"
|
||||
var pname: String = str(player.get("player_name"))
|
||||
if pname.is_empty():
|
||||
pname = "Player %d" % (int(player.get("index")) + 1)
|
||||
return pname
|
||||
|
||||
|
||||
func _on_main_menu() -> void:
|
||||
get_tree().paused = false
|
||||
visible = false
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ script = ExtResource("1_victory")
|
|||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
color = Color(0, 0, 0, 0.8)
|
||||
color = Color(0, 0, 0, 0.85)
|
||||
|
||||
[node name="Panel" type="PanelContainer" parent="."]
|
||||
anchors_preset = 8
|
||||
|
|
@ -18,57 +18,67 @@ anchor_left = 0.5
|
|||
anchor_top = 0.5
|
||||
anchor_right = 0.5
|
||||
anchor_bottom = 0.5
|
||||
offset_left = -260.0
|
||||
offset_top = -200.0
|
||||
offset_right = 260.0
|
||||
offset_bottom = 200.0
|
||||
offset_left = -360.0
|
||||
offset_top = -260.0
|
||||
offset_right = 360.0
|
||||
offset_bottom = 260.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
|
||||
[node name="MarginContainer" type="MarginContainer" parent="Panel"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/margin_left = 24
|
||||
theme_override_constants/margin_top = 20
|
||||
theme_override_constants/margin_right = 24
|
||||
theme_override_constants/margin_bottom = 20
|
||||
theme_override_constants/margin_left = 28
|
||||
theme_override_constants/margin_top = 22
|
||||
theme_override_constants/margin_right = 28
|
||||
theme_override_constants/margin_bottom = 22
|
||||
|
||||
[node name="VBox" type="VBoxContainer" parent="Panel/MarginContainer"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 12
|
||||
theme_override_constants/separation = 10
|
||||
|
||||
[node name="ResultLabel" type="Label" parent="Panel/MarginContainer/VBox"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
horizontal_alignment = 1
|
||||
theme_override_font_sizes/font_size = 40
|
||||
text = "VICTORY"
|
||||
|
||||
[node name="PlayerLabel" type="Label" parent="Panel/MarginContainer/VBox"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
horizontal_alignment = 1
|
||||
theme_override_font_sizes/font_size = 18
|
||||
text = "Player Name"
|
||||
|
||||
[node name="ConditionLabel" type="Label" parent="Panel/MarginContainer/VBox"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
horizontal_alignment = 1
|
||||
theme_override_font_sizes/font_size = 15
|
||||
text = "Domination Victory"
|
||||
|
||||
[node name="Separator" type="HSeparator" parent="Panel/MarginContainer/VBox"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="ScoreLabel" type="Label" parent="Panel/MarginContainer/VBox"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
horizontal_alignment = 1
|
||||
text = "Final Score: 0"
|
||||
|
||||
[node name="TurnLabel" type="Label" parent="Panel/MarginContainer/VBox"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
horizontal_alignment = 1
|
||||
theme_override_font_sizes/font_size = 13
|
||||
theme_override_colors/font_color = Color(0.7, 0.7, 0.7, 1)
|
||||
text = "Turn 1"
|
||||
|
||||
[node name="Separator" type="HSeparator" parent="Panel/MarginContainer/VBox"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="StatsGrid" type="GridContainer" parent="Panel/MarginContainer/VBox"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
columns = 7
|
||||
theme_override_constants/h_separation = 14
|
||||
theme_override_constants/v_separation = 4
|
||||
|
||||
[node name="Spacer" type="Control" parent="Panel/MarginContainer/VBox"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
|
||||
[node name="Buttons" type="HBoxContainer" parent="Panel/MarginContainer/VBox"]
|
||||
layout_mode = 2
|
||||
alignment = 1
|
||||
|
|
|
|||
99
src/game/engine/scenes/tests/hud_proof.gd
Normal file
99
src/game/engine/scenes/tests/hud_proof.gd
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
extends Node2D
|
||||
## HUD Proof — verifies #24 (HP bars) + #19 (color-coded notification log)
|
||||
## render together. Self-capturing: seeds TurnNotification entries, draws
|
||||
## HP bar samples using the same formula as unit/city renderers, then quits.
|
||||
|
||||
const TurnNotificationScene: PackedScene = preload(
|
||||
"res://engine/scenes/hud/turn_notification.tscn"
|
||||
)
|
||||
|
||||
const OUTPUT_DIR: String = "user://screenshots"
|
||||
const CAPTURE_DELAY: float = 0.8
|
||||
const HP_BAR_WIDTH: float = 72.0
|
||||
const HP_BAR_HEIGHT: float = 8.0
|
||||
|
||||
const SAMPLES: Array = [
|
||||
["Unit 90% HP", 0.9, 140.0, 220.0], ["Unit 55% HP", 0.55, 140.0, 280.0],
|
||||
["Unit 20% HP", 0.2, 140.0, 340.0], ["City 80% HP", 0.8, 140.0, 440.0],
|
||||
["City 40% HP", 0.4, 140.0, 500.0], ["City 15% HP", 0.15, 140.0, 560.0],
|
||||
]
|
||||
|
||||
const LOG_ENTRIES: Array = [
|
||||
["Dwarf Warriors ambushed at (12, 8)", "combat"],
|
||||
["Ironhold lost population (now 3)", "combat"],
|
||||
["Ironhold grew to population 5", "founding"],
|
||||
["Khazad-dûm borders expanded", "founding"],
|
||||
["Research complete: Bronze Working", "tech"],
|
||||
["Warriors completed in Ironhold", "economy"],
|
||||
["Smithy built in Khazad-dûm", "economy"],
|
||||
["Tree of Life has been completed!", "magic"],
|
||||
["A new era dawns: Bronze Age", "event"],
|
||||
]
|
||||
|
||||
var _notification: CanvasLayer = null
|
||||
var _captured: bool = false
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
RenderingServer.set_default_clear_color(Color(0.08, 0.07, 0.05))
|
||||
get_viewport().size = Vector2i(1280, 720)
|
||||
DisplayServer.window_set_size(Vector2i(1280, 720))
|
||||
DataLoader.load_theme("age-of-dwarves")
|
||||
await get_tree().process_frame
|
||||
|
||||
_notification = TurnNotificationScene.instantiate()
|
||||
add_child(_notification)
|
||||
await get_tree().process_frame
|
||||
_notification.show_processing()
|
||||
for entry: Array in LOG_ENTRIES:
|
||||
_notification._add_entry(entry[0] as String, entry[1] as String)
|
||||
_notification._show_log()
|
||||
|
||||
queue_redraw()
|
||||
await get_tree().create_timer(CAPTURE_DELAY).timeout
|
||||
_capture_and_quit()
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
var font: Font = ThemeDB.fallback_font
|
||||
draw_string(font, Vector2(40, 40),
|
||||
"HUD Proof — HP Bars (#24) + Color-Coded Notification Log (#19)",
|
||||
HORIZONTAL_ALIGNMENT_LEFT, -1, 18, Color(1.0, 0.94, 0.75))
|
||||
draw_string(font, Vector2(40, 180), "Unit HP Bars (from unit_renderer.gd)",
|
||||
HORIZONTAL_ALIGNMENT_LEFT, -1, 14, Color(0.85, 0.85, 1.0))
|
||||
draw_string(font, Vector2(40, 400), "City HP Bars (from city_renderer.gd)",
|
||||
HORIZONTAL_ALIGNMENT_LEFT, -1, 14, Color(0.85, 0.85, 1.0))
|
||||
for s: Array in SAMPLES:
|
||||
var frac: float = float(s[1])
|
||||
var origin: Vector2 = Vector2(float(s[2]), float(s[3]))
|
||||
draw_rect(Rect2(origin, Vector2(HP_BAR_WIDTH, HP_BAR_HEIGHT)),
|
||||
Color(0.15, 0.15, 0.15, 0.9))
|
||||
var bar_color: Color = Color.GREEN
|
||||
if frac < 0.3:
|
||||
bar_color = Color.RED
|
||||
elif frac < 0.6:
|
||||
bar_color = Color.YELLOW
|
||||
draw_rect(Rect2(origin, Vector2(HP_BAR_WIDTH * frac, HP_BAR_HEIGHT)),
|
||||
bar_color)
|
||||
draw_string(font, Vector2(origin.x + HP_BAR_WIDTH + 16, origin.y + HP_BAR_HEIGHT),
|
||||
s[0] as String, HORIZONTAL_ALIGNMENT_LEFT, -1, 12, Color(0.95, 0.92, 0.82))
|
||||
|
||||
|
||||
func _capture_and_quit() -> void:
|
||||
if _captured:
|
||||
return
|
||||
_captured = true
|
||||
DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(OUTPUT_DIR))
|
||||
var image: Image = get_viewport().get_texture().get_image()
|
||||
if image == null:
|
||||
push_error("HudProof: Failed to get viewport image")
|
||||
get_tree().quit(1)
|
||||
return
|
||||
var stamp: String = Time.get_datetime_string_from_system().replace(":", "-").replace("T", "_")
|
||||
var abs_path: String = ProjectSettings.globalize_path("%s/hud_proof_%s.png" % [OUTPUT_DIR, stamp])
|
||||
var err: Error = image.save_png(abs_path)
|
||||
if err == OK:
|
||||
print("SCREENSHOT_PATH:%s" % abs_path)
|
||||
else:
|
||||
push_error("HudProof: Save failed: %s" % error_string(err))
|
||||
get_tree().quit()
|
||||
6
src/game/engine/scenes/tests/hud_proof.tscn
Normal file
6
src/game/engine/scenes/tests/hud_proof.tscn
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
[gd_scene load_steps=2 format=3 uid="uid://hud_proof_01"]
|
||||
|
||||
[ext_resource type="Script" path="res://engine/scenes/tests/hud_proof.gd" id="1"]
|
||||
|
||||
[node name="HudProof" type="Node2D"]
|
||||
script = ExtResource("1")
|
||||
|
|
@ -190,6 +190,53 @@ func test_autosave_then_load_autosave_round_trips() -> void:
|
|||
assert_eq(GameState.players.size(), 2, "autosave restores players")
|
||||
|
||||
|
||||
## ── round-trip: research / magic / economy fields ─────────────────────
|
||||
|
||||
func test_save_then_load_restores_research_and_magic_fields() -> void:
|
||||
var p0: RefCounted = GameState.players[0]
|
||||
p0.researched_techs = ["mining", "bronze_working"]
|
||||
p0.researching = "iron_working"
|
||||
p0.research_progress = 14
|
||||
p0.science_per_turn = 8
|
||||
p0.schools = ["life", "nature"]
|
||||
p0.mana_pool = {"life": 60, "nature": 40}
|
||||
p0.mana_income = {"life": 5.0, "nature": 3.0}
|
||||
p0.golden_age_active = true
|
||||
p0.golden_age_turns = 3
|
||||
p0.golden_age_progress = 7
|
||||
p0.golden_age_count = 1
|
||||
|
||||
SaveManagerScript.save_game(0)
|
||||
|
||||
GameState.players[0].researched_techs = []
|
||||
GameState.players[0].researching = ""
|
||||
GameState.players[0].research_progress = 0
|
||||
GameState.players[0].science_per_turn = 0
|
||||
GameState.players[0].schools = []
|
||||
GameState.players[0].mana_pool = {}
|
||||
GameState.players[0].mana_income = {}
|
||||
GameState.players[0].golden_age_active = false
|
||||
GameState.players[0].golden_age_turns = 0
|
||||
GameState.players[0].golden_age_progress = 0
|
||||
GameState.players[0].golden_age_count = 0
|
||||
|
||||
var err: Error = SaveManagerScript.load_game(0)
|
||||
assert_eq(err, OK, "load_game must succeed")
|
||||
|
||||
var r: RefCounted = GameState.players[0]
|
||||
assert_eq(r.researched_techs, ["mining", "bronze_working"], "researched_techs restored")
|
||||
assert_eq(r.researching, "iron_working", "researching restored")
|
||||
assert_eq(r.research_progress, 14, "research_progress restored")
|
||||
assert_eq(r.science_per_turn, 8, "science_per_turn restored")
|
||||
assert_eq(r.schools, ["life", "nature"], "schools restored")
|
||||
assert_eq(r.mana_pool, {"life": 60, "nature": 40}, "mana_pool restored")
|
||||
assert_eq(r.mana_income, {"life": 5.0, "nature": 3.0}, "mana_income restored")
|
||||
assert_true(r.golden_age_active, "golden_age_active restored")
|
||||
assert_eq(r.golden_age_turns, 3, "golden_age_turns restored")
|
||||
assert_eq(r.golden_age_progress, 7, "golden_age_progress restored")
|
||||
assert_eq(r.golden_age_count, 1, "golden_age_count restored")
|
||||
|
||||
|
||||
## ── helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
func _seed_game_state() -> void:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue