class_name PlaymatRenderer extends Node ## PlaymatRenderer - Generates a clean programmatic play mat texture using SubViewport ## Layout matches FF-TCG official play mat zones: ## Left: Damage Zone (top) / Turn Structure (bottom) ## Center: Field (Forwards top, Backups bottom) ## Right: Deck (top) / Break Zone (bottom) const MAT_TEXTURE_WIDTH: int = 2048 const MAT_TEXTURE_HEIGHT: int = 1024 # Color scheme - dark green felt table look const BG_COLOR := Color(0.12, 0.15, 0.12) const ZONE_BORDER_COLOR := Color(0.35, 0.40, 0.30) const ZONE_FILL_COLOR := Color(0.14, 0.18, 0.14) const LABEL_COLOR := Color(0.45, 0.50, 0.40) const FIELD_LINE_COLOR := Color(0.20, 0.25, 0.20) const TURN_TEXT_COLOR := Color(0.35, 0.40, 0.30) # Zone regions as pixel rects in the texture # Left column: ~17% width const DAMAGE_RECT := Rect2(30, 30, 320, 460) const TURN_REF_RECT := Rect2(30, 520, 320, 474) # Center: ~66% width const FIELD_RECT := Rect2(380, 30, 1290, 964) # Right column: ~17% width const DECK_RECT := Rect2(1700, 30, 318, 440) const BREAK_RECT := Rect2(1700, 500, 318, 494) # Border thickness const BORDER_WIDTH: int = 3 func render() -> ImageTexture: var viewport = SubViewport.new() viewport.size = Vector2i(MAT_TEXTURE_WIDTH, MAT_TEXTURE_HEIGHT) viewport.render_target_update_mode = SubViewport.UPDATE_ONCE viewport.transparent_bg = false # Background var bg = ColorRect.new() bg.color = BG_COLOR bg.position = Vector2.ZERO bg.size = Vector2(MAT_TEXTURE_WIDTH, MAT_TEXTURE_HEIGHT) viewport.add_child(bg) # Draw zone rectangles with borders and labels _draw_zone(viewport, DAMAGE_RECT, "DAMAGE ZONE") _draw_zone(viewport, DECK_RECT, "DECK") _draw_zone(viewport, BREAK_RECT, "BREAK ZONE") _draw_zone(viewport, FIELD_RECT, "FIELD") # Divider line in Field (separating Forwards from Backups) var divider_y = FIELD_RECT.position.y + FIELD_RECT.size.y * 0.48 _draw_dashed_line(viewport, Vector2(FIELD_RECT.position.x + 20, divider_y), Vector2(FIELD_RECT.position.x + FIELD_RECT.size.x - 20, divider_y)) # Forwards / Backups sub-labels _draw_label(viewport, "FORWARDS", Vector2(FIELD_RECT.position.x + FIELD_RECT.size.x / 2, FIELD_RECT.position.y + FIELD_RECT.size.y * 0.22), 32) _draw_label(viewport, "BACKUPS", Vector2(FIELD_RECT.position.x + FIELD_RECT.size.x / 2, FIELD_RECT.position.y + FIELD_RECT.size.y * 0.74), 32) # Turn structure reference (decorative) _draw_turn_structure(viewport) # Add outer border around entire mat _draw_border_rect(viewport, Rect2(5, 5, MAT_TEXTURE_WIDTH - 10, MAT_TEXTURE_HEIGHT - 10), ZONE_BORDER_COLOR, 2) add_child(viewport) # Wait for the viewport to render await RenderingServer.frame_post_draw # Grab the rendered image var image = viewport.get_texture().get_image() var texture = ImageTexture.create_from_image(image) # Clean up viewport.queue_free() return texture func _draw_zone(viewport: SubViewport, rect: Rect2, label_text: String) -> void: # Zone fill var fill = ColorRect.new() fill.color = ZONE_FILL_COLOR fill.position = rect.position fill.size = rect.size viewport.add_child(fill) # Zone border (4 thin ColorRects forming a rectangle) _draw_border_rect(viewport, rect, ZONE_BORDER_COLOR, BORDER_WIDTH) # Zone label centered at top of zone _draw_label(viewport, label_text, Vector2(rect.position.x + rect.size.x / 2, rect.position.y + 30), 28) func _draw_border_rect(viewport: SubViewport, rect: Rect2, color: Color, width: int) -> void: # Top edge var top = ColorRect.new() top.color = color top.position = rect.position top.size = Vector2(rect.size.x, width) viewport.add_child(top) # Bottom edge var bottom = ColorRect.new() bottom.color = color bottom.position = Vector2(rect.position.x, rect.position.y + rect.size.y - width) bottom.size = Vector2(rect.size.x, width) viewport.add_child(bottom) # Left edge var left = ColorRect.new() left.color = color left.position = rect.position left.size = Vector2(width, rect.size.y) viewport.add_child(left) # Right edge var right = ColorRect.new() right.color = color right.position = Vector2(rect.position.x + rect.size.x - width, rect.position.y) right.size = Vector2(width, rect.size.y) viewport.add_child(right) func _draw_label(viewport: SubViewport, text: String, center_pos: Vector2, font_size: int) -> void: var label = Label.new() label.text = text label.add_theme_font_size_override("font_size", font_size) label.add_theme_color_override("font_color", LABEL_COLOR) label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER # Size the label wide enough and position so center_pos is the center var label_width = text.length() * font_size * 0.7 var label_height = font_size * 1.5 label.position = Vector2(center_pos.x - label_width / 2, center_pos.y - label_height / 2) label.size = Vector2(label_width, label_height) viewport.add_child(label) func _draw_dashed_line(viewport: SubViewport, from: Vector2, to: Vector2) -> void: var dash_length = 20.0 var gap_length = 15.0 var direction = (to - from).normalized() var total_length = from.distance_to(to) var current = 0.0 while current < total_length: var dash_end = min(current + dash_length, total_length) var start_pos = from + direction * current var end_pos = from + direction * dash_end var dash = ColorRect.new() dash.color = FIELD_LINE_COLOR dash.position = start_pos dash.size = Vector2(dash_end - current, 2) viewport.add_child(dash) current = dash_end + gap_length func _draw_turn_structure(viewport: SubViewport) -> void: var phases = [ "Turn Structure", "", "1. Active Phase", "2. Draw Phase", "3. Main Phase 1", "4. Attack Phase", "5. Main Phase 2", "6. End Phase", ] var start_y = TURN_REF_RECT.position.y + 20 var x = TURN_REF_RECT.position.x + 15 for i in range(phases.size()): var text = phases[i] if text == "": continue var label = Label.new() label.text = text var font_size = 20 if i == 0 else 16 label.add_theme_font_size_override("font_size", font_size) label.add_theme_color_override("font_color", TURN_TEXT_COLOR) label.position = Vector2(x, start_y + i * 28) label.size = Vector2(TURN_REF_RECT.size.x - 30, 28) viewport.add_child(label)