@tool
extends AGMPluginAction
class_name CA_ShadowCastAction

# =====================================================================
# CA_ShadowCastAction - 床に描いた影を、キャラの上にも落とす
#
# 【何を解決するか】
#   見下ろし画面で、床に半透明の影を乗せている。そこへキャラが入っても
#   キャラは明るいままなので、影の上に浮いて見える。
#
#   影を「上のレイヤー」に置いてキャラごと覆う手もあるが、それをやると
#   影に入っていないキャラまで覆われる。レイヤーに置いた時点で
#   「キャラより上か下か」の二択しかなく、**どちらも正解ではない**。
#
# 【考え方】
#   影は前後関係を持つ「物」ではなく、**その場所にある物を暗くする操作**。
#   だから並べ替えの外へ出す。
#
#   影レイヤーの絵をそのまま焼いて**ワールドに貼りついたマスク**にし、
#   キャラのシェーダーが自分の各ピクセルの**ワールド座標**でそれを引く。
#
#   ★これでマスクは自動的に世界に固定される。キャラが動いても境目は
#     動かないので、影に入っていくと境目が体の上を斜めに走り、
#     完全に入れば全身が暗くなる。
#
#   ★★角度の設定は要らない。マスクは**描いた絵そのもの**なので、
#     床の影とキャラの影は**作りからして一致する**。手で合わせ込む
#     必要がなく、絵を描き直しても勝手に追従する。
#
# 【焼くのは1回だけ】
#   影レイヤーは動かないので、ステートに入った時に1回焼くだけ。
#   歩いている間の費用はゼロ（シェーダーが引くだけ）。
#
# 【制限】
#   ・影レイヤーに回転をかけている場合は合いません（拡大縮小は可）
#   ・実行中に影レイヤーの中身を書き換えても焼き直しません
#     （焼き直したい場合はステートに入り直してください）
# =====================================================================

const TARGET_OWNER: int = 0
const TARGET_NODE_NAME: int = 1
const TARGET_TSCN_PATH: int = 2

# ★貼れなかった時に挑み直す間隔（コマ数）。毎コマ木を舐めると重い。
#   ポータルで戻った直後の「まだ出来ていない」を拾えれば十分なので、
#   1秒に4回くらいで足りる
const RETRY_FRAMES: int = 15

# ★★落ち影（長丸）を、床の影と**溶かして**描くためのシェーダー。
#
#   影は「暗くする操作」であって、重ねて足すものではない。
#   すでに影の中にある床へ、もう一枚ふつうに重ねると**二重に暗くなり、
#   そこだけ浮いて見える**。
#
#   床は base から mix(base, shade, a床) になっている。
#   落ち影まで含めた正解は mix(base, shade, max(a床, a落))。
#   いま画面にあるのは mix(base, shade, a床) なので、
#   その上に足すべき濃さ x は
#
#       mix(cur, shade, x) = mix(base, shade, A)      A = max(a床, a落)
#       →  x = 1 - (1 - A) / (1 - a床)
#
#   a落 <= a床 なら x = 0、つまり**影の中では落ち影が消える**。
#   縁では滑らかに繋がる。しきい値で切るのと違って段差が出ない。
const DROP_SHADER_CODE = """
shader_type canvas_item;

uniform sampler2D shadow_mask : filter_nearest, repeat_disable;
uniform vec2 mask_origin = vec2(0.0);
uniform vec2 mask_size_px = vec2(1.0);
uniform vec4 mask_tint : source_color = vec4(1.0);
// 0 = 床の影と溶かす（既定）／1 = そのまま重ねる／2 = 薄める
uniform int blend_mode = 0;

varying vec2 world_pos;

void vertex() {
	world_pos = (MODEL_MATRIX * vec4(VERTEX, 0.0, 1.0)).xy;
}

void fragment() {
	vec4 tex = texture(TEXTURE, UV);
	float a_drop = tex.a * COLOR.a;
	float a_floor = 0.0;
	if (mask_size_px.x > 0.0 && mask_size_px.y > 0.0) {
		vec2 uv = (world_pos - mask_origin) / mask_size_px;
		if (uv.x >= 0.0 && uv.x <= 1.0 && uv.y >= 0.0 && uv.y <= 1.0) {
			a_floor = texture(shadow_mask, uv).a * mask_tint.a;
		}
	}
	float a_out = a_drop;
	if (blend_mode == 0) {
		float big = max(a_floor, a_drop);
		a_out = 1.0 - (1.0 - big) / max(1.0 - a_floor, 0.001);
	} else if (blend_mode == 2) {
		a_out = a_drop * (1.0 - a_floor);
	}
	COLOR = vec4(COLOR.rgb, clamp(a_out, 0.0, 1.0));
}
"""

# ★ワールド座標でマスクを引くだけの、ごく短いシェーダー。
#   毎フレームの計算は「座標をUVに直して1回サンプルする」だけ。
const SHADOW_SHADER_CODE = """
shader_type canvas_item;

// 影レイヤーを焼いた絵。ドット絵なので必ず filter_nearest。
// repeat_disable にしないと、マスクの外側が反対側の端を拾う。
uniform sampler2D shadow_mask : filter_nearest, repeat_disable;
uniform vec2 mask_origin = vec2(0.0);      // マスク左上のワールド座標
uniform vec2 mask_size_px = vec2(1.0);     // マスクが覆うワールドの大きさ
uniform vec4 mask_tint : source_color = vec4(1.0);
uniform float shadow_gain = 1.0;           // 濃さの倍率
uniform bool debug_show = false;
// ★★キャラは「立った板」。光が斜めに来るので、体の上の境目は斜めになる。
//   床の縁の形をそのまま写しただけでは、まっすぐな縁がまっすぐのまま
//   出てしまい、絵として正しくない。
uniform vec2 light_dir = vec2(0.7071, 0.7071);  // 影が伸びる向き（画面）
uniform float slant = 1.0;                 // 高さ1pxあたり、床を何px進むか
uniform float local_bottom = 16.0;         // 絵のローカル座標での足元のy
// ★★専用の斜めマスク。床の影の形がどうであれ、キャラは**一定角度の
//   斜め線**で切る。線の位置は「足元が影の奥へどれだけ入ったか」で決まる。
uniform bool use_slant_mask = true;
uniform float edge_u = 0.0;                // 体のどこまで影が上がるか（px）
uniform vec4 foot_shade : source_color = vec4(0.0);  // 足元の影の色と濃さ
// ★形だけを確かめる用。色と濃さを無視して真っ黒で塗る
uniform bool solid_debug = false;
// ★★問題の切り分け用。影の判定を見ず、必ず斜めで切る
uniform bool force_diagonal = false;
// ★斜め線の傾き。cot(角度)。0で水平、1で45度
uniform float lean = 1.0;

varying vec2 world_pos;
varying vec2 feet_world;

void vertex() {
	// ★ここが全部の肝。MODEL_MATRIX でローカル→ワールドへ直す。
	//   これを渡すから、マスクがキャラに追従せず世界に固定される。
	world_pos = (MODEL_MATRIX * vec4(VERTEX, 0.0, 1.0)).xy;
	// ★足元のワールド座標。MODEL_MATRIX を通すので、キャラが動けば
	//   勝手に付いてくる。毎コマ送り直す必要がない
	feet_world = (MODEL_MATRIX * vec4(0.0, local_bottom, 0.0, 1.0)).xy;
}

// ★★fragment の中で return は使えない（Godotのシェーダー言語の決まり）。
//   早期 return で書くと "Using 'return' in the 'fragment' processor
//   function is incorrect." で弾かれる。入れ子の if で書くこと。
//   GDScript側の --check-only はシェーダー文字列を見ないので、
//   これは別立てのシェーダー検査でしか捕まらない。
void fragment() {
	float a = 0.0;
	vec3 shade = vec3(0.0);
	bool on_shadow = false;      // 足元（またはその画素）が影の上か
	bool cut_lit = false;        // 斜め線の光側で、まだ日向か

	// ★★【問題の切り分け用】影の判定を一切見ず、必ず斜めで切る。
	//   「キャラに斜めのマスクをかけられるのか」だけを確かめる。
	//   ここで斜めが出れば、残るのは影の判定の話だけになる。
	//   出なければ、貼り方そのものが効いていない。
	// ★★斜めの「高さ」。足元から上へ数え、lean で横に傾ける。
	//   ここを dot(…, -light_dir) で作っていたら、角度を180度回した
	//   ときに符号ごと反転し、掃き方が成立しなくなる（225 で確認できる）。
	//   **傾き（lean）と、暗くなる向き（常に足元から上へ）を分ける。**
	float dy = feet_world.y - world_pos.y;   // 足元からの高さ（上が+）
	float dx = world_pos.x - feet_world.x;
	float u = dy - lean * dx;

	if (force_diagonal) {
		if (u < local_bottom) {          // 絵のまんなか辺りで切る
			COLOR.rgb = vec3(0.0);
		}
	} else if (use_slant_mask) {
		// ★★【本命】専用の斜めマスク。
		//   影の色と濃さは foot_shade を使う。これは足元を**面で平均**した
		//   値で、GDScript側が毎コマ入れている。
		//   ★ここでマスクを1点だけ読んではいけない。ドット絵の半透明は
		//     ディザ（市松）で描かれることが多く、隙間を引いて影が消える。
		//   切り方は光に直角な一本の線。u は「足元から光の側への距離」。
		//   頭ほど大きく、edge_u（影が上がる高さ）を超えた所はまだ日向。
		on_shadow = (foot_shade.a > 0.0);
		cut_lit = (u >= edge_u);
		if (on_shadow && !cut_lit) {
			a = clamp(foot_shade.a * shadow_gain, 0.0, 1.0);
			shade = foot_shade.rgb;
			if (solid_debug) {
				a = 1.0;
				shade = vec3(0.0);
			}
		}
	} else {
		// 【比べる用】床の影の形をそのままキャラへ写す。
		//   足元からの高さぶん、光の向きへずらして床を読む。
		float h = max(0.0, feet_world.y - world_pos.y);
		vec2 floor_pos = vec2(world_pos.x, feet_world.y)
			+ light_dir * (h * slant);
		vec2 uv = (floor_pos - mask_origin) / mask_size_px;
		if (uv.x >= 0.0 && uv.x <= 1.0 && uv.y >= 0.0 && uv.y <= 1.0) {
			vec4 s = texture(shadow_mask, uv);
			on_shadow = (s.a > 0.0);
			a = clamp(s.a * mask_tint.a * shadow_gain, 0.0, 1.0);
			shade = s.rgb * mask_tint.rgb;
		}
	}

	if (a > 0.0) {
		// ★床と同じ計算で重ねる。床は「半透明の影を上に乗せた」結果なので、
		//   キャラにも同じ混ぜ方をすれば境目で色がつながる。
		COLOR.rgb = mix(COLOR.rgb, shade, a);
	}

	// ★★「何も起きない」を3通りに割る。色が付かない＝そもそも
	//   このシェーダーが絵に乗っていない、まで読み取れるようにする。
	//     赤 … 影が掛かっている（効いている）
	//     青 … 足元は影だが、この画素は斜め線の日向側
	//     緑 … 足元が影の上ではない
	//     無色 … シェーダーが絵に乗っていない
	if (debug_show) {
		vec3 mark = vec3(0.0, 1.0, 0.0);
		if (on_shadow) {
			mark = (a > 0.0) ? vec3(1.0, 0.0, 0.0) : vec3(0.0, 0.0, 1.0);
		}
		COLOR.rgb = mix(COLOR.rgb, mark, 0.45);
	}
}
"""


# ---------------------------------------------------------------------
# 設定
# ---------------------------------------------------------------------

@export_group("Shadow Layer / 影のタイルレイヤー")
## 影を塗ってあるTileMapLayerの名前。ここに塗った絵がそのままマスクになります。
## 影が何枚かのレイヤーに分かれているときは、カンマで並べてください
## （例： Shadow, Shadow2, 柱の影 ）。
## ★床の絵そのものに影を描き込んだレイヤーを入れると、床ぜんぶが
##   影になります。入れるのは「半透明の影だけを乗せたレイヤー」です。
@export var shadow_layer_name_影レイヤー名: String = "Shadow"

@export_group("Target / 対象")
@export_enum("Owner / 実行オブジェクト", "Name Search / 名前検索", "Tscn / tscn指定") var target_mode_対象: int = TARGET_OWNER
@export var target_node_name_対象ノード名: String = ""
@export_file("*.tscn") var target_tscn_path_対象tscn: String = ""
## 空欄なら対象の中から自動で探します（AnimatedSprite2D → Sprite2D → ボーン → Polygon2D の順）
@export var apply_node_name_適用ノード名: String = ""

@export_group("Look / 見え方")
## 1.0で床とまったく同じ濃さ。キャラを読みやすくしたい時は少し下げます。
@export_range(0.0, 2.0, 0.05) var shadow_gain_影の濃さ倍率: float = 1.0

@export_subgroup("Light / 光の向き")
## ★★キャラを切る斜め線の傾き。**これを直接いじってください。**
## 0で水平、+で右上がり、-で右下がり。45で45度の右上がり。
## ★線の傾きを「光の向き度」から計算することはできません。
##   傾きは180度で一周するため 45 と 225 が同じ値になり、
##   設定をいじっても何も変わらなくなります。だから専用の設定にしてあります。
@export_range(-80.0, 80.0, 1.0) var cut_angle_境目の傾き度: float = -50.0
## 影が画面のどちらへ伸びているか。**こちらは「深さの測り方」にだけ使います。**
## 0=右、90=下、180=左、270=上。境目の傾きには影響しません。
@export_range(0.0, 360.0, 1.0) var light_angle_光の向き度: float = 0.0
## 高さ1pxにつき、床を何px進むか。大きいほど境目が寝ます（＝光が低い）。
## 0にすると傾きなし＝床の縁の形がそのまま出ます。
@export_range(0.0, 6.0, 0.05) var slant_影の伸び: float = 1.0
## ★★オンで「専用の斜めマスク」。床の影の形がどうであれ、キャラは
## 一定角度の斜め線で切られます（ご提案の形）。
## オフにすると、床の影の形をそのままキャラへ写します（比べる用）。
@export var slant_mask_斜めマスクを使う: bool = true
## 足元の影を読むときの半径(px)。ドット絵のディザ（市松）を平均するため
## 面で読みます。0にすると1点だけ読むので、ディザだと影が消えます。
@export_range(0, 12, 1) var foot_sample_足元を読む半径px: int = 4

@export_group("Drop Shadow / 落ち影（おまけ）")
## 足元から伸びる長丸の影を1枚出します。色は**床の影と同じ色**を
## 焼いたマスクから拾うので、色の設定はありません。
@export var drop_enabled_落ち影を出す: bool = true
## オンにすると、上の「光の向き度」と同じ向きへ伸ばします。
## 床の影とオブジェの影が**同じ方角を向く**ので、まずはこれを推奨。
## ★向きが逆に見えたら、次の「落ち影の向き度」に 180 足した値を
##   手で入れるか、このスイッチを切ってください。
@export var drop_follow_光の向きに合わせる: bool = false
## 影が伸びる向き（度）。0=右、90=下、45=右下。
## 上のスイッチがオンのときは使いません。
@export_range(0.0, 360.0, 1.0) var drop_angle_落ち影の向き度: float = 350.0
## 起点の左右のズラし（px）。+で右へ。
## ★起点＝影が生えてくる足元の点です。ここをずらすと、長丸ぜんぶが
##   同じだけ動きます（長さや向きは変わりません）。
@export_range(-64.0, 64.0, 1.0) var drop_ox_落ち影の起点ズラシXpx: float = -3.0
## 起点の上下のズラし（px）。+で下へ。
## ★絵の足元より少し下から生やしたい、という時に使います。
@export_range(-64.0, 64.0, 1.0) var drop_oy_落ち影の起点ズラシYpx: float = -2.0
## 長丸の長さ（px）。足元から、この長さだけ伸びます。
@export_range(0.0, 200.0, 1.0) var drop_len_落ち影の長さpx: float = 26.0
## 長丸の太さ（px）。
@export_range(1.0, 100.0, 1.0) var drop_wide_落ち影の太さpx: float = 11.0
## 濃さ(%)。床の影より薄めにすると馴染みます。
@export_range(0.0, 100.0, 1.0) var drop_alpha_落ち影の濃さ: float = 45.0
## 縁のぼかし(px)。0で硬い縁。ドット絵なら1〜2くらい。
@export_range(0.0, 16.0, 0.5) var drop_blur_落ち影のぼかしpx: float = 2.0
## ★★床の影と重なった時の混ぜ方。
## 「溶かす」＝影の中では落ち影が消えます（影は重ねて足す物ではないため）。
## 「そのまま重ねる」＝影の上でも足すので、そこだけ二重に暗くなります。
## 「薄める」＝その中間。床が暗いほど落ち影を弱めます。
@export_enum("床の影と溶かす:0", "そのまま重ねる:1", "薄める:2")
var drop_blend_落ち影の混ぜ方: int = 0

@export_group("Runtime / 実行時")
@export var remove_on_state_exit_ステート終了で戻す: bool = true
## 焼くマスクの画素数の上限。広いマップで固まらないための安全弁です。
@export_range(65536, 16777216, 65536) var max_mask_pixels_マスクの上限画素数: int = 4194304

@export_group("Debug / デバッグ")
@export var show_warnings_警告表示: bool = true
@export var debug_log_ログ表示: bool = false
## 影が掛かっているピクセルを赤く染めます。判定と絵がズレていないかの確認用。
@export var debug_paint_判定を赤く塗る: bool = false
## 焼いたマスクを、マップの上に紫で重ねて見せます。
## 描いた影とピタリ重なっていれば、焼きは正しく出来ています。
@export var debug_mask_焼いたマスクを重ねる: bool = false
## ★★マスクの形だけを確かめるための表示。影の色と濃さを無視して、
## 掛かっている所を**真っ黒**で塗ります。薄さや色で迷わずに、
## 「斜めに切れているか」だけを見られます。まずこれで形を確定させます。
@export var debug_solid_真っ黒で形を見る: bool = false
## ★★【切り分け用】影の判定を一切見ず、**必ず**斜めの真っ黒を出します。
## 「キャラに斜めをかけられるのか」だけを確かめるためのものです。
## ここで斜めが出れば、残るのは影の判定の話だけになります。
@export var debug_force_斜めを必ず出す: bool = false


var _owner_node: Node = null
var _apply_node: CanvasItem = null
var _prev_apply_node: CanvasItem = null
var _parent_mat_forced: bool = false
var _drop_sprite: Sprite2D = null
var _drop_mat: ShaderMaterial = null
# ★床の影の代表色。落ち影を同じ色にするために、焼いた時に拾っておく
var _mask_rep_color: Color = Color(0.05, 0.04, 0.12, 1.0)
var _original_material: Material = null
var _applied: bool = false
var _scene_signal_connected: bool = false
var _mask_tex: ImageTexture = null
var _mask_origin: Vector2 = Vector2.ZERO
var _mask_size: Vector2 = Vector2.ONE
var _mask_tint: Color = Color.WHITE
# ★★どのステージの、どのレイヤーを焼いたのかを覚えておく。
#   これが無いと、ポータルでステージが入れ替わっても古いマスクを
#   使い回し、**新しいマップでは影がどこにも無いこと**になる。
#   遷移の合図（game_scene_about_to_change）だけに頼らない。
#   合図が来ない経路（同じステージ内の移動、合図の接続に失敗、
#   F5リセット）で黙って壊れるため。
var _baked_scene_id: int = 0
var _baked_layer_ids: Array[int] = []
# ★毎コマの仕事を止める先。_owner_node が死ぬと get_tree() が取れず、
#   「切ったつもりで繋がったまま」になるので、繋いだ木を控えておく
var _tick_tree: SceneTree = null
# ★貼れなかった時に挑み直す間隔（コマ数）。毎コマ find_children を
#   舐めると重いので間引く
var _retry_wait: int = 0
var _warned_no_layer: bool = false
var _warned_no_cell: bool = false
var _warned_no_target: bool = false
var _warned_no_sprite: bool = false
var _dbg_layer: CanvasLayer = null
var _dbg_label: Label = null
var _dbg_state: String = "（まだ動いていません）"
var _dbg_mask_sprite: Sprite2D = null
var _shader_mat: ShaderMaterial = null
var _last_bottom: float = NAN
var _tick_connected: bool = false
var _dbg_edge: float = 0.0
var _dbg_foot_on: bool = false
var _dbg_steal: int = 0
var _dbg_took: String = ""
var _dbg_cands: String = ""
var _dbg_foot_col: Color = Color(0, 0, 0, 0)
var _dbg_foot_at: Vector2 = Vector2.ZERO
var _dbg_panel: ColorRect = null
var _dbg_dot: ColorRect = null
# ★焼いた元の情報。エディタで見える「升の番号」で出すために持っておく。
#   ワールド座標だけ出しても、ユーザーはエディタ側と突き合わせられない。
var _dbg_mask_img: Image = null
var _dbg_connected: bool = false
var _dbg_frame: int = -1
var _dbg_cell_rect: Rect2i = Rect2i()
var _dbg_cell_count: int = 0
var _dbg_tile_px: Vector2i = Vector2i.ZERO
var _dbg_layer_path: String = ""


# ---------------------------------------------------------------------
# AGMakerエディタ上での表示情報
# ---------------------------------------------------------------------

func get_plugin_tab_name() -> String:
	return "Lighting / 光と影"


func get_group_name() -> String:
	return "Shadow Cast / 影落とし"


func get_description() -> String:
	return "床に塗った影のタイルレイヤーを読み取り、その影をキャラの上にも落とします。影に入ると境目が体の上を走り、完全に入れば全身が暗くなります。角度の設定は要りません（描いた絵がそのまま使われます）。"


# ---------------------------------------------------------------------
# ライフサイクル
# ---------------------------------------------------------------------

func on_state_enter(p_owner: Object, _p_arg1 = null, _p_arg2 = null, _p_arg3 = null) -> void:
	if Engine.is_editor_hint():
		return
	_owner_node = p_owner as Node
	if _owner_node == null:
		_warn("p_owner が Node ではありません。")
		return
	_connect_scene_change_signal()
	# ★Resourceなので前回の実行状態が残る。入るたびに旗を下ろす
	_warned_no_layer = false
	_warned_no_cell = false
	_warned_no_target = false
	_warned_no_sprite = false
	_retry_wait = 0
	# ★★まずここで表示を出す。この1行が画面に出るかどうかで
	#   「アクションが走っているのか、そもそも呼ばれていないのか」が割れる
	_dbg_state = "動き始めました（owner=" + str(_owner_node.name) + "）"
	_dbg_show()
	_apply_shadow()


func on_state_update(p_owner: Object, _p_delta_time = 0.0, _p_arg1 = null, _p_arg2 = null, _p_arg3 = null) -> void:
	if Engine.is_editor_hint():
		return
	if _owner_node == null or not is_instance_valid(_owner_node):
		_owner_node = p_owner as Node
	if _owner_node == null:
		return
	# ★貼った相手が作り直された時に貼り直す。ここが無いと、
	#   キャラのスプライトが差し替わった瞬間に影が消えたまま戻らない
	if not _applied or _apply_node == null or not is_instance_valid(_apply_node):
		_apply_shadow()
	elif debug_log_ログ表示:
		# ★キャラの位置は動くので、出しているなら毎コマ書き直す
		_dbg_report()


func on_state_exit(_p_owner: Object, _p_arg1 = null, _p_arg2 = null, _p_arg3 = null) -> void:
	if Engine.is_editor_hint():
		return
	if remove_on_state_exit_ステート終了で戻す:
		_remove_shadow()


# ★解体中は関数を呼ばず、その場で戻す
func _notification(what: int) -> void:
	if what == NOTIFICATION_PREDELETE:
		# ★毎コマの仕事を先に外す。繋いだまま消えると、木のほうから
		#   居ない相手を呼びに行くことになる
		if _tick_connected and _tick_tree != null \
				and is_instance_valid(_tick_tree):
			var cb := Callable(self, "_on_tick")
			if _tick_tree.process_frame.is_connected(cb):
				_tick_tree.process_frame.disconnect(cb)
		_tick_connected = false
		_tick_tree = null
		if _applied and _apply_node != null and is_instance_valid(_apply_node):
			_apply_node.material = _original_material
		_applied = false
		_apply_node = null
		_original_material = null
		_mask_tex = null
		if _dbg_layer != null and is_instance_valid(_dbg_layer):
			_dbg_layer.queue_free()
		_dbg_layer = null
		_dbg_label = null
		_dbg_panel = null
		_dbg_dot = null
		if _drop_sprite != null and is_instance_valid(_drop_sprite):
			_drop_sprite.queue_free()
		_drop_sprite = null
		if _dbg_mask_sprite != null and is_instance_valid(_dbg_mask_sprite):
			_dbg_mask_sprite.queue_free()
		_dbg_mask_sprite = null


func _connect_scene_change_signal() -> void:
	if _scene_signal_connected:
		return
	if Engine.has_singleton("AGMakerManager"):
		var mgr = Engine.get_singleton("AGMakerManager")
		if mgr != null and mgr.has_signal("game_scene_about_to_change"):
			var cb = Callable(self, "_on_game_scene_about_to_change")
			if not mgr.game_scene_about_to_change.is_connected(cb):
				mgr.game_scene_about_to_change.connect(cb)
			_scene_signal_connected = true
		# ★★「遷移が終わった」ほうも拾う。直前の合図だけだと、
		#   新しいステージが出来た時に貼り直す機会が無い。
		#   ポータルで同じステージへ戻る経路がこれで拾える
		if mgr != null and mgr.has_signal("game_scene_changed"):
			var cb2 = Callable(self, "_on_game_scene_changed")
			if not mgr.game_scene_changed.is_connected(cb2):
				mgr.game_scene_changed.connect(cb2)


# ★引数を1つ受け取ること（共通ハンドオーバー 第4章-②）
func _on_game_scene_about_to_change(_current_scene: Object = null) -> void:
	_remove_shadow()
	_dbg_free()
	# ★マスクもここで捨てる。次のステージは影の位置が違うので、
	#   持ち越すと**前のマップの影がキャラに乗ったまま**になる
	_mask_tex = null
	_baked_scene_id = 0
	_baked_layer_ids.clear()
	# ★前のステージのノードを指したままにしない。
	#   解放済みのオブジェクトと比べに行くと事故る
	_prev_apply_node = null


# ★遷移が終わった直後。相手が生きていれば貼り直す。
#   新しいステージの物は、まだ自分の on_state_enter を迎えていないので
#   ここで触らない（_owner_node が前のステージの死骸のことがある）
func _on_game_scene_changed(_current_scene: Object = null) -> void:
	if _owner_node == null or not is_instance_valid(_owner_node):
		return
	if not _owner_node.is_inside_tree():
		return
	_warned_no_layer = false
	_warned_no_cell = false
	_retry_wait = 0
	_apply_shadow()


# ---------------------------------------------------------------------
# 貼る／剥がす
# ---------------------------------------------------------------------

func _apply_shadow() -> void:
	var target := _resolve_target_node()
	if target == null:
		# ★★「見つかりません」だけだと、どのモードで何を探して外したのかが
		#   分からない。探した条件をそのまま出す。
		var how := "実行オブジェクト"
		if target_mode_対象 == TARGET_NODE_NAME:
			how = "名前「" + target_node_name_対象ノード名 + "」"
		elif target_mode_対象 == TARGET_TSCN_PATH:
			how = "tscn「" + target_tscn_path_対象tscn + "」"
		# ★1回だけ出す。挑み直すたびに出すとログが埋まる
		if not _warned_no_target:
			_warned_no_target = true
			_warn("対象が見つかりません（探し方＝" + how + "）。\n" +
				"キャラのステートに入れているなら、対象は" +
				"「Owner / 実行オブジェクト」にしてください。")
		_tick_connect()
		return
	_apply_node = _find_apply_node(target)
	if _apply_node == null:
		if not _warned_no_sprite:
			_warned_no_sprite = true
			_warn("影を貼るノード（Sprite2D等）が対象の中に見つかりません。")
		_tick_connect()
		return

	if _mask_tex == null or _mask_is_stale():
		if not _bake_mask():
			# ★★ここで黙って帰らない。初期アクションに入れる運用だと
			#   on_state_update は二度と来ないので、**毎コマの仕事の中で
			#   挑み直す**。ポータルで戻った直後はまだタイルマップが
			#   木に入っていないことがあり、1回きりだと永久に出ない。
			_tick_connect()
			return

	# ★同じノードへ貼り直す時に、控えを取り直さない。取り直すと
	#   **自分が貼ったシェーダーを「元の材質」として覚えて**しまい、
	#   剥がしても元に戻らなくなる
	if not (_applied and _apply_node == _prev_apply_node):
		_original_material = _apply_node.material
	_prev_apply_node = _apply_node

	var shader := Shader.new()
	shader.code = SHADOW_SHADER_CODE
	var material := ShaderMaterial.new()
	material.shader = shader
	material.set_shader_parameter("shadow_mask", _mask_tex)
	material.set_shader_parameter("mask_origin", _mask_origin)
	material.set_shader_parameter("mask_size_px", _mask_size)
	material.set_shader_parameter("mask_tint", _mask_tint)
	material.set_shader_parameter("shadow_gain", shadow_gain_影の濃さ倍率)
	material.set_shader_parameter("debug_show", debug_paint_判定を赤く塗る)
	var rad := deg_to_rad(light_angle_光の向き度)
	material.set_shader_parameter("light_dir", Vector2(cos(rad), sin(rad)))
	# ★★傾きは「光の向き」から計算しない。cot(角度) は180度で一周するので、
	#   45 と 225 が同じ値になり、**設定をいじっても何も変わらない**
	#   。傾きの角度をそのまま使う。
	material.set_shader_parameter("lean",
		tan(deg_to_rad(clampf(cut_angle_境目の傾き度, -80.0, 80.0))))
	material.set_shader_parameter("slant", slant_影の伸び)
	material.set_shader_parameter("use_slant_mask", slant_mask_斜めマスクを使う)
	material.set_shader_parameter("edge_u", 0.0)
	material.set_shader_parameter("foot_shade", Color(0, 0, 0, 0))
	material.set_shader_parameter("solid_debug", debug_solid_真っ黒で形を見る)
	material.set_shader_parameter("force_diagonal", debug_force_斜めを必ず出す)
	_shader_mat = material
	_last_bottom = NAN            # 次の更新で必ず送り直させる
	_refresh_bottom()
	_tick_connect()
	_apply_node.material = material
	_applied = true
	# ★★見えている絵と、貼った絵が同じとは限らない。
	#   対象の中の描くノードを全部並べて、どれに貼ったかを出す。
	#   ここを出さないと「貼ったのに何も起きない」の原因が絞れない。
	_dbg_cands = _list_canvas_items(target)
	_drop_setup()

	if debug_log_ログ表示:
		print("[ShadowCast] applied to: ", _apply_node.name,
			" mask=", _mask_tex.get_size(), " origin=", _mask_origin,
			" size=", _mask_size)
	_dbg_report()


func _remove_shadow() -> void:
	_tick_disconnect()
	_drop_free()
	# ★勝手に外した use_parent_material は、必ず戻す
	if _parent_mat_forced and _apply_node != null:
		if is_instance_valid(_apply_node):
			_apply_node.use_parent_material = true
	_parent_mat_forced = false
	_shader_mat = null
	_last_bottom = NAN
	if _applied and _apply_node != null and is_instance_valid(_apply_node):
		_apply_node.material = _original_material
	_applied = false
	_apply_node = null
	_original_material = null
	_dbg_free()


# ---------------------------------------------------------------------
# 影レイヤーを焼いてマスクにする
# ---------------------------------------------------------------------
# ★ここで作るのは「影レイヤーの見た目そのまま」の絵。色も透明度も
#   いじらない。床に乗っているものと同じものをキャラにも乗せたいので、
#   途中で作り替えると**床とキャラで濃さが変わる**。

func _bake_mask() -> bool:
	# ★★影が何枚かのレイヤーに分かれていることがある。
	#   1枚しか読まないと、残りの影は「存在しないこと」になり、
	#   そこに立ってもキャラは明るいまま。**しかも警告は出ない**。
	var layers := _find_shadow_layers()
	if layers.is_empty():
		# ★1回だけ出す。毎フレーム出すとログが埋まる
		if not _warned_no_layer:
			_warned_no_layer = true
			_warn("影のタイルレイヤー「" + shadow_layer_name_影レイヤー名 +
				"」が見つかりません。レイヤー名を確認してください。" +
				"複数あるときはカンマで区切ります。")
		return false

	# ★升の大きさは1枚目に合わせる。混在していたら知らせる
	var first: TileMapLayer = layers[0]
	var tile: Vector2i = first.tile_set.tile_size
	# ★★全部のレイヤーを含む四角を先に出す。1枚目の範囲で切ると、
	#   はみ出したレイヤーの影が黙って欠ける
	var rect: Rect2i = Rect2i()
	var got_rect := false
	for node in layers:
		var lay: TileMapLayer = node
		if lay.tile_set.tile_size != tile:
			_warn("レイヤー「" + str(lay.name) + "」だけ升の大きさが違います（" +
				str(lay.tile_set.tile_size) + " と " + str(tile) + "）。" +
				"混ぜられないので、このレイヤーは飛ばします。")
			continue
		var r: Rect2i = lay.get_used_rect()
		if r.size.x <= 0 or r.size.y <= 0:
			continue
		if not got_rect:
			rect = r
			got_rect = true
		else:
			rect = rect.merge(r)
	if not got_rect:
		if not _warned_no_cell:
			_warned_no_cell = true
			_warn("影のタイルレイヤーにタイルが1枚も塗られていません。")
		return false

	var w: int = rect.size.x * tile.x
	var h: int = rect.size.y * tile.y
	# ★広すぎるマップで固まらないための安全弁。黙って諦めず、理由を出す
	if w * h > max_mask_pixels_マスクの上限画素数:
		_warn("影の範囲が広すぎます（" + str(w) + "x" + str(h) +
			"）。「マスクの上限画素数」を上げるか、影レイヤーの範囲を狭めてください。")
		return false

	var img := Image.create(w, h, false, Image.FORMAT_RGBA8)
	img.fill(Color(0.0, 0.0, 0.0, 0.0))

	var painted: int = 0
	var names: Array[String] = []
	for node in layers:
		var layer: TileMapLayer = node
		var ts: TileSet = layer.tile_set
		if ts.tile_size != tile:
			continue
		names.append(str(layer.name))
		# ★レイヤーごとの色・透明度は、ここで畳み込む。
		#   1枚だけの時は uniform で渡せたが、複数だと畳むしかない
		var lay_mod: Color = layer.modulate
		for cell in layer.get_used_cells():
			var sid: int = layer.get_cell_source_id(cell)
			if sid < 0:
				continue
			var src := ts.get_source(sid) as TileSetAtlasSource
			if src == null:
				continue
			var tex: Texture2D = src.texture
			if tex == null:
				continue
			var simg: Image = tex.get_image()
			if simg == null:
				continue
			# ★圧縮テクスチャはそのままでは画素を読めない。読める形へ戻す
			if simg.is_compressed():
				if simg.decompress() != OK:
					continue
			var coords: Vector2i = layer.get_cell_atlas_coords(cell)
			var region: Rect2i = src.get_tile_texture_region(coords)
			var dst := Vector2i(
				(cell.x - rect.position.x) * tile.x,
				(cell.y - rect.position.y) * tile.y)
			var td: TileData = layer.get_cell_tile_data(cell)
			var mod: Color = lay_mod
			if td != null:
				mod = mod * td.modulate
			if mod.is_equal_approx(Color.WHITE):
				# ★白ならそのまま貼る。blit_rect は C++ 側なので速い
				img.blit_rect(simg, region, dst)
			else:
				# ★色が掛かっている時だけ、1画素ずつ掛け算する。
				#   全部この道を通すと、広い影レイヤーで秒単位かかる
				_blit_modulated(img, simg, region, dst, mod)
			painted += 1

	if painted == 0:
		if not _warned_no_cell:
			_warned_no_cell = true
			_warn("影のタイルレイヤーから絵を1枚も読めませんでした。")
		return false

	_mask_tex = ImageTexture.create_from_image(img)
	_dbg_mask_img = null
	# ★★床の影の代表色を拾っておく。落ち影を同じ色にするため。
	#   不透明だった画素だけの平均。透明な所を混ぜると薄く出る。
	var sum := Vector3.ZERO
	var lit := 0
	for sy in range(0, h, maxi(1, h / 64)):
		for sx in range(0, w, maxi(1, w / 64)):
			var pc := img.get_pixel(sx, sy)
			if pc.a > 0.1:
				sum += Vector3(pc.r, pc.g, pc.b)
				lit += 1
	if lit > 0:
		var v := sum / float(lit)
		_mask_rep_color = Color(v.x, v.y, v.z, 1.0)
	_dbg_cell_rect = rect
	_dbg_cell_count = painted
	_dbg_tile_px = tile
	_dbg_layer_path = ", ".join(names)
	# ★左上の升の中心から半升ぶん戻すと、その升の左上角になる。
	#   to_global を通すので、レイヤーに拡大縮小が掛かっていても合う
	var half := Vector2(tile) * 0.5
	var tl: Vector2 = first.to_global(first.map_to_local(rect.position) - half)
	var br: Vector2 = first.to_global(
		first.map_to_local(rect.position + rect.size) - half)
	_mask_origin = tl
	_mask_size = br - tl
	# ★レイヤーごとの色はもう焼き込んである。ここは素通し。
	_mask_tint = Color.WHITE
	# ★★「いつ焼いたか」ではなく「何を焼いたか」を控える。
	#   次に貼る時、同じステージの同じレイヤーかを見て古さを判断する
	var stage := _get_game_scene()
	_baked_scene_id = stage.get_instance_id() if stage != null else 0
	_baked_layer_ids.clear()
	for node in layers:
		_baked_layer_ids.append((node as Node).get_instance_id())
	if debug_log_ログ表示:
		print("[ShadowCast] baked ", painted, " cells → ", img.get_size(),
			" origin=", _mask_origin, " size=", _mask_size,
			" tint=", _mask_tint)
	return true


# ★★焼いたマスクが「今のステージのもの」かどうか。
#   ポータルでステージが入れ替わると、マスクは前のマップの座標のまま。
#   キャラはその四角の外に居るので、**影はどこにも無いことになる**。
#   これが「戻ってくると影が出ない」の正体。
func _mask_is_stale() -> bool:
	var stage := _get_game_scene()
	var now_id: int = stage.get_instance_id() if stage != null else 0
	if now_id != _baked_scene_id:
		return true
	# ★ステージが同じでも、レイヤーだけ作り直されることがある
	for id in _baked_layer_ids:
		if not is_instance_valid(instance_from_id(id)):
			return true
	return false


func _blit_modulated(dst_img: Image, src_img: Image, region: Rect2i,
		dst: Vector2i, mod: Color) -> void:
	for y in range(region.size.y):
		for x in range(region.size.x):
			var c: Color = src_img.get_pixel(region.position.x + x,
				region.position.y + y)
			if c.a <= 0.0:
				continue
			dst_img.set_pixel(dst.x + x, dst.y + y, c * mod)


func _find_shadow_layers() -> Array[TileMapLayer]:
	var out: Array[TileMapLayer] = []
	var host := _get_game_scene()
	if host == null or not is_instance_valid(host):
		return out
	# ★カンマ区切りで複数。前後の空白は落とす（「Shadow, Shadow2」と書ける）
	var wanted: Array[String] = []
	for part in shadow_layer_name_影レイヤー名.split(","):
		var name_s := part.strip_edges()
		if name_s != "":
			wanted.append(name_s)
	if wanted.is_empty():
		return out
	var found: Array = host.find_children("*", "TileMapLayer", true, false)
	# ★書いた順に並べる。同じ場所に重なっている時、あとの物が上に乗る
	for name_s in wanted:
		for node in found:
			var layer := node as TileMapLayer
			if layer == null or layer.tile_set == null:
				continue
			if str(layer.name) == name_s:
				out.append(layer)
	return out


# ---------------------------------------------------------------------
# 置き場所・対象の解決（共通ハンドオーバーの作法どおり）
# ---------------------------------------------------------------------

func _get_owner_tree() -> SceneTree:
	if _owner_node == null or not is_instance_valid(_owner_node):
		return null
	if not _owner_node.is_inside_tree():
		return null
	return _owner_node.get_tree()


# ★生成物は置かないが、タイルマップを探すのに「ステージ」が要る。
#   current_scene は常駐の CoreScene なので、そこから探すと
#   別ステージのタイルマップを拾う（共通ハンドオーバー 第4章-⑰）
func _get_game_scene() -> Node:
	if Engine.has_singleton("AGMakerManager"):
		var mgr = Engine.get_singleton("AGMakerManager")
		if mgr != null and mgr.has_method("get_current_game_scene"):
			var scene: Variant = mgr.get_current_game_scene()
			if is_instance_valid(scene) and scene is Node:
				var scene_node: Node = scene
				if scene_node.is_inside_tree():
					return scene_node
	var node: Node = _owner_node
	while node != null and is_instance_valid(node):
		if node.get_class() == "GameScene":
			return node
		node = node.get_parent()
	var tree := _get_owner_tree()
	if tree != null:
		return tree.current_scene
	return null


func _resolve_target_node() -> Node:
	if _owner_node == null or not is_instance_valid(_owner_node):
		return null
	var host := _get_game_scene()
	match target_mode_対象:
		TARGET_OWNER:
			return _owner_node
		TARGET_NODE_NAME:
			if target_node_name_対象ノード名 != "" and host != null:
				return _find_node_by_name_recursive(host,
					target_node_name_対象ノード名)
		TARGET_TSCN_PATH:
			if target_tscn_path_対象tscn != "" and host != null:
				return _find_node_by_scene_path_recursive(host,
					target_tscn_path_対象tscn)
	return null


func _find_apply_node(root: Node) -> CanvasItem:
	if apply_node_name_適用ノード名 != "":
		var named = _find_node_by_name_recursive(root,
			apply_node_name_適用ノード名)
		if named is CanvasItem:
			return named as CanvasItem
		_warn("適用ノード名「" + apply_node_name_適用ノード名 +
			"」が見つかりません。自動で探します。")
	var found: CanvasItem = _find_first_of_class(root, "AnimatedSprite2D")
	if found == null:
		found = _find_first_of_class(root, "Sprite2D")
	if found == null:
		# ★ボーンアニメのルート。配下の Polygon2D は use_parent_material が
		#   立っているので、親に貼れば全パーツに効く
		found = _find_first_bone_root(root)
	if found == null:
		found = _find_first_of_class(root, "Polygon2D")
	if found != null:
		return found
	# ★★ここから下は「絵が見つからなかった」道。
	#   実行オブジェクトのルートに貼っても、そのノード自身が何も描いて
	#   いなければ**画面は1ピクセルも変わらず、警告も出ない**。
	#   黙って失敗するので、必ず知らせる。
	if root is CanvasItem:
		_warn("キャラの絵（Sprite2D / AnimatedSprite2D など）が「" +
			str(root.name) + "」の中に見つかりませんでした。" +
			"実行オブジェクトそのものに貼りますが、そのノードが絵を" +
			"描いていない場合は**何も起きません**。" +
			"その時は「適用ノード名」で絵のノードを直接指定してください。")
		return root as CanvasItem
	_warn("キャラの絵が見つからず、貼れるノードもありませんでした。")
	return null


func _list_canvas_items(root: Node) -> String:
	var out: Array[String] = []
	_walk_canvas(root, out)
	return " / ".join(out) if out.size() > 0 else "（描くノードなし）"


func _walk_canvas(node: Node, out: Array) -> void:
	if node == null or not is_instance_valid(node):
		return
	if node is CanvasItem and out.size() < 8:
		var ci := node as CanvasItem
		var mark := "2605" if ci == _apply_node else ""
		out.append(mark + str(ci.name) + "(" + ci.get_class() + ")"
			+ ("見" if ci.visible else "隠")
			+ ("親材質" if ci.use_parent_material else ""))
	for child in node.get_children():
		_walk_canvas(child, out)


func _find_first_bone_root(node: Node) -> CanvasItem:
	if node == null or not is_instance_valid(node):
		return null
	if node is CanvasItem and node.get_class().begins_with("BoneAnimationRoot"):
		return node as CanvasItem
	for child in node.get_children():
		var hit := _find_first_bone_root(child)
		if hit != null:
			return hit
	return null


func _find_first_of_class(node: Node, cls: String) -> CanvasItem:
	if node == null or not is_instance_valid(node):
		return null
	if node.is_class(cls) and node is CanvasItem:
		return node as CanvasItem
	for child in node.get_children():
		var hit := _find_first_of_class(child, cls)
		if hit != null:
			return hit
	return null


func _find_node_by_name_recursive(node: Node, wanted_name: String):
	if node == null or not is_instance_valid(node):
		return null
	if str(node.name) == wanted_name:
		return node
	for child in node.get_children():
		var hit = _find_node_by_name_recursive(child, wanted_name)
		if hit != null:
			return hit
	return null


func _find_node_by_scene_path_recursive(node: Node, scene_path: String):
	if node == null or not is_instance_valid(node):
		return null
	if _node_matches_scene_path(node, scene_path):
		return node
	for child in node.get_children():
		var hit = _find_node_by_scene_path_recursive(child, scene_path)
		if hit != null:
			return hit
	return null


# ★★完全一致だけで探すと**まず見つからない**。実行時のノードが持つ
#   scene_file_path は、指定した文字列と綴りが揃わないことがある
#   （区切り文字、インスタンスの入れ子）。ファイル名でも拾う。
func _node_matches_scene_path(node: Node, scene_path: String) -> bool:
	if node == null or scene_path == "":
		return false
	var wanted := scene_path.replace("\\", "/")
	var mine := String(node.scene_file_path).replace("\\", "/")
	if mine == "":
		return false
	if mine == wanted:
		return true
	return mine.get_file() == wanted.get_file()


func _warn(message: String) -> void:
	if show_warnings_警告表示:
		push_warning("[CA_ShadowCastAction] " + message)
	# ★★push_warning はAGMakerの画面では見えない所へ出る。
	#   「何も出ない」が**いちばん困る状態**なので、画面にも出す。
	_dbg_state = message
	_dbg_show()


# ---------------------------------------------------------------------
# 画面に出す切り分け表示
# ---------------------------------------------------------------------
# ★★これが出ていなければ「アクションがそもそも走っていない」と分かる。
#   ログが見えない環境では、**出ないこと自体が答え**になる表示が要る。

func _dbg_show() -> void:
	if not debug_log_ログ表示:
		return
	if _dbg_layer == null or not is_instance_valid(_dbg_layer):
		var host := _get_game_scene()
		if host == null or not is_instance_valid(host):
			return
		_dbg_layer = CanvasLayer.new()
		# ★掃除用の名前にインスタンスIDを入れる（共通ハンドオーバー 第4章-⑩）
		_dbg_layer.name = "AGM_ShadowCastDebug_%d" % get_instance_id()
		_dbg_layer.layer = 128
		_dbg_label = Label.new()
		_dbg_label.position = Vector2(8, 8)
		_dbg_label.add_theme_color_override("font_color", Color(1, 1, 0.4))
		_dbg_label.add_theme_color_override("font_outline_color", Color.BLACK)
		_dbg_label.add_theme_constant_override("outline_size", 4)
		_dbg_layer.add_child(_dbg_label)
		host.add_child(_dbg_layer)
	# ★★焼いたマスクを、画面の隅に小さく出す。
	#   マップに重ねる表示は、暗い影の上に紫を乗せるので形が読めない。
	#   別枠に出せば「塗ってある影がどんな形か」が一目で分かる。
	#   ここが四角い帯なら、どう計算しても境目は斜めにならない。
	if _mask_tex != null and _dbg_panel == null:
		var pw := 220.0
		var ph: float = pw * float(_mask_tex.get_height()) \
			/ maxf(1.0, float(_mask_tex.get_width()))
		var bg := ColorRect.new()
		bg.color = Color(0, 0, 0, 0.6)
		bg.position = Vector2(8, 200)
		bg.size = Vector2(pw, ph)
		var tr := TextureRect.new()
		tr.texture = _mask_tex
		tr.position = Vector2.ZERO
		tr.size = Vector2(pw, ph)
		tr.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
		tr.stretch_mode = TextureRect.STRETCH_SCALE
		tr.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
		tr.modulate = Color(1.0, 1.0, 0.2, 1.0)
		bg.add_child(tr)
		var dot := ColorRect.new()
		dot.color = Color(1.0, 0.2, 0.2)
		dot.size = Vector2(5, 5)
		bg.add_child(dot)
		_dbg_layer.add_child(bg)
		_dbg_panel = bg
		_dbg_dot = dot
	if _dbg_label != null and is_instance_valid(_dbg_label):
		_dbg_label.text = "[影落とし]\n" + _dbg_state
	# ★表示を出したら、毎コマ書き直す口も繋いでおく。
	#   ここで繋がないと、ステートを抜けた瞬間に数字が凍る
	_dbg_connect()


func _dbg_report() -> void:
	if not debug_log_ログ表示:
		return
	var lines: Array[String] = []
	lines.append("owner=" + (str(_owner_node.name) if _owner_node != null else "なし"))
	if _apply_node != null and is_instance_valid(_apply_node):
		lines.append("貼った絵=" + str(_apply_node.name) +
			"(" + _apply_node.get_class() + ")")
	else:
		lines.append("貼った絵=なし")
	if _mask_tex != null:
		# ★★エディタで見える「升の番号」で出す。ワールド座標だけだと
		#   ユーザーが自分の塗った範囲と突き合わせられない
		lines.append("塗ってある升=(" + str(_dbg_cell_rect.position.x) + "," +
			str(_dbg_cell_rect.position.y) + ")〜(" +
			str(_dbg_cell_rect.end.x - 1) + "," +
			str(_dbg_cell_rect.end.y - 1) + ")  " +
			str(_dbg_cell_count) + "枚  1升=" + str(_dbg_tile_px.x) + "px")
		lines.append("レイヤー=" + _dbg_layer_path)
		lines.append("原点=" + str(_mask_origin.round()) +
			" 大きさ=" + str(_mask_size.round()))
		# ★★キャラの**絵の四角**とマスクの四角が重なっているか。
		#   基準点1点で見ると嘘になる（足元が外でも頭は中、が普通に起きる）。
		#   シェーダーは1画素ずつ判定するので、重なっていれば効く。
		var mask_rect := Rect2(_mask_origin, _mask_size)
		var sprite_rect := _dbg_sprite_rect()
		if sprite_rect.size == Vector2.ZERO:
			# ★キャストの前に解放済みかを見る（下の _refresh_edge と同じ理由）
			var node2d: Node2D = null
			if _apply_node != null and is_instance_valid(_apply_node):
				node2d = _apply_node as Node2D
			if node2d != null:
				lines.append("キャラ=" + str(node2d.global_position.round()) +
					"（絵の大きさが取れません）")
		else:
			var hit: bool = mask_rect.intersects(sprite_rect)
			lines.append("キャラの絵=" + str(sprite_rect.position.round()) +
				"〜" + str((sprite_rect.position + sprite_rect.size).round()))
			if hit:
				# ★★四角どうしが重なっただけでは意味がない。
				#   マスクの四角は「塗ってある升ぜんぶを囲む箱」なので、
				#   中身はほとんど透明。**実際に塗ってある画素に
				#   当たっているか**を数えないと、透明な所に立っていても
				#   「重なっています」と出てしまう。
				var pct := _dbg_coverage(sprite_rect)
				if pct > 0:
					lines.append("→ ★影が当たっています（絵の " +
						str(pct) + "%）")
				else:
					lines.append("→ 箱は重なっていますが、そこは透明です" +
						"（影は当たっていません）")
			else:
				# ★★「重なっていない」だけでは手が打てない。
				#   **どちらへ何升ぶん足りないか**を出す。そのまま
				#   「その升に影を塗る」「そこまで歩く」の指示になる。
				lines.append("→ ★重なっていません：" +
					_dbg_gap_text(mask_rect, sprite_rect))
	else:
		lines.append("マスク=まだ焼けていません")
	# ★★傾きの3つの数字を出す。足元が 0 だと傾きが潰れるので、
	#   「設定は入れたのに斜めにならない」時はここを見る
	# ★★斜めマスクは足元の1点だけで決める。ここが「なし」なら
	#   何も描かれないので、まずこの行を見る
	lines.append("足元=" + str(_dbg_foot_at.round()) +
		("  影の上です" if _dbg_foot_on else "  ★影の上ではありません"))
	lines.append("  足元の影の濃さ=" + str(snappedf(_dbg_foot_col.a, 0.01)) +
		"  色=" + str(_dbg_foot_col.to_html(false)) +
		"  → キャラは " + str(int(round(clampf(_dbg_foot_col.a
		* shadow_gain_影の濃さ倍率, 0.0, 1.0) * 100.0))) + "% 暗くなります")
	lines.append("材質=" + ("自分のもの" if (_apply_node != null
		and is_instance_valid(_apply_node)
		and _apply_node.material == _shader_mat) else "★別のもの") +
		"  奪われ直した回数=" + str(_dbg_steal) + "  " + _dbg_took)
	lines.append("絵の候補: " + _dbg_cands)
	lines.append("傾き：向き=" + str(int(light_angle_光の向き度)) +
		"度 伸び=" + str(slant_影の伸び) +
		" 足元=" + ("未取得" if is_nan(_last_bottom) else str(_last_bottom)) +
		" 影の高さ=" + str(snappedf(_dbg_edge, 0.1)) + "px" +
		("  ★足元が0＝斜めは出ません" if (not is_nan(_last_bottom)
			and is_zero_approx(_last_bottom)) else ""))
	_dbg_state = "\n".join(lines)
	_dbg_show()
	_dbg_mask_overlay()
	_dbg_move_dot()


# ★★表示をステートから切り離す。
#   初期アクションに入れて遷移すると on_state_update が二度と来ないので、
#   ステート任せにすると**表示が入った瞬間の値で凍る**。
#   シェーダーは貼ったまま生きているのに、数字が動かないせいで
#   「キャラが違うのでは」と疑わせてしまう。
func _dbg_connect() -> void:
	if _dbg_connected or not debug_log_ログ表示:
		return
	var tree := _get_owner_tree()
	if tree == null:
		return
	var cb := Callable(self, "_dbg_tick")
	if not tree.process_frame.is_connected(cb):
		tree.process_frame.connect(cb)
	_dbg_connected = true


func _dbg_disconnect() -> void:
	if not _dbg_connected:
		return
	var tree := _get_owner_tree()
	if tree != null:
		var cb := Callable(self, "_dbg_tick")
		if tree.process_frame.is_connected(cb):
			tree.process_frame.disconnect(cb)
	_dbg_connected = false


func _dbg_tick() -> void:
	# ★同じコマに2回呼ばれることがある（共通ハンドオーバー 第4章-③）
	var f: int = Engine.get_process_frames()
	if f == _dbg_frame:
		return
	_dbg_frame = f
	if not debug_log_ログ表示:
		return
	if _applied and _apply_node != null and is_instance_valid(_apply_node):
		_dbg_report()


# ★キャラの絵のうち、何%が塗ってある影に当たっているか。
#   シェーダーが1画素ずつやっていることを、粗い網で真似して数える。
func _dbg_coverage(sprite_rect: Rect2) -> int:
	if _mask_tex == null:
		return 0
	if _dbg_mask_img == null:
		_dbg_mask_img = _mask_tex.get_image()
	if _dbg_mask_img == null:
		return 0
	var iw: int = _dbg_mask_img.get_width()
	var ih: int = _dbg_mask_img.get_height()
	if iw <= 0 or ih <= 0 or _mask_size.x <= 0.0 or _mask_size.y <= 0.0:
		return 0
	var steps := 8
	var lit := 0
	var total := 0
	for gy in range(steps):
		for gx in range(steps):
			var w := sprite_rect.position + Vector2(
				sprite_rect.size.x * (float(gx) + 0.5) / float(steps),
				sprite_rect.size.y * (float(gy) + 0.5) / float(steps))
			total += 1
			var u := (w.x - _mask_origin.x) / _mask_size.x * float(iw)
			var v := (w.y - _mask_origin.y) / _mask_size.y * float(ih)
			var px := int(floorf(u))
			var py := int(floorf(v))
			if px < 0 or py < 0 or px >= iw or py >= ih:
				continue
			if _dbg_mask_img.get_pixel(px, py).a > 0.1:
				lit += 1
	if total == 0:
		return 0
	return int(round(float(lit) * 100.0 / float(total)))


# 影の四角から、キャラがどちらへどれだけ外れているかを日本語にする
func _dbg_gap_text(mask_rect: Rect2, sprite_rect: Rect2) -> String:
	var px: float = maxf(1.0, float(_dbg_tile_px.y))
	var pxx: float = maxf(1.0, float(_dbg_tile_px.x))
	var parts: Array[String] = []
	if sprite_rect.position.y > mask_rect.end.y:
		var d: float = sprite_rect.position.y - mask_rect.end.y
		parts.append("影より下へ " + str(snappedf(d / px, 0.1)) + "升")
	elif sprite_rect.end.y < mask_rect.position.y:
		var d2: float = mask_rect.position.y - sprite_rect.end.y
		parts.append("影より上へ " + str(snappedf(d2 / px, 0.1)) + "升")
	if sprite_rect.position.x > mask_rect.end.x:
		var d3: float = sprite_rect.position.x - mask_rect.end.x
		parts.append("影より右へ " + str(snappedf(d3 / pxx, 0.1)) + "升")
	elif sprite_rect.end.x < mask_rect.position.x:
		var d4: float = mask_rect.position.x - sprite_rect.end.x
		parts.append("影より左へ " + str(snappedf(d4 / pxx, 0.1)) + "升")
	if parts.is_empty():
		return "（原因不明。数字を見てください）"
	return "、".join(parts) + " ずれています"


# 小窓の中で、キャラが影のどこにいるかを赤い点で示す
func _dbg_move_dot() -> void:
	if _dbg_panel == null or not is_instance_valid(_dbg_panel):
		return
	if _dbg_dot == null or not is_instance_valid(_dbg_dot):
		return
	if _apply_node == null or not is_instance_valid(_apply_node):
		return
	var n2 := _apply_node as Node2D
	if n2 == null:
		return
	if _mask_size.x <= 0.0 or _mask_size.y <= 0.0:
		return
	var p: Vector2 = n2.global_position
	var u: float = (p.x - _mask_origin.x) / _mask_size.x
	var v: float = (p.y - _mask_origin.y) / _mask_size.y
	_dbg_dot.position = Vector2(
		clampf(u, 0.0, 1.0) * _dbg_panel.size.x - 2.0,
		clampf(v, 0.0, 1.0) * _dbg_panel.size.y - 2.0)
	# ★枠の外にいる時は色を変える。点が縁に張り付いたまま
	#   「中にいる」と誤解させないため
	_dbg_dot.color = Color(1.0, 0.2, 0.2) if (u >= 0.0 and u <= 1.0
		and v >= 0.0 and v <= 1.0) else Color(0.3, 0.6, 1.0)


func _dbg_free() -> void:
	_dbg_disconnect()
	_dbg_panel = null
	_dbg_dot = null
	if _dbg_layer != null and is_instance_valid(_dbg_layer):
		_dbg_layer.queue_free()
	_dbg_layer = null
	_dbg_label = null
	if _dbg_mask_sprite != null and is_instance_valid(_dbg_mask_sprite):
		_dbg_mask_sprite.queue_free()
	_dbg_mask_sprite = null


# ★★焼いたマスクそのものをマップに重ねる。
#   数字で「原点はここ」と言われても、それが**描いた影と合っているか**は
#   分からない。重ねて見せれば、合っているかズレているかが一目で済む。
#   ズレていれば、どちらへ何px ずれているかもその場で読める。
func _dbg_mask_overlay() -> void:
	if not debug_mask_焼いたマスクを重ねる or _mask_tex == null:
		return
	if _dbg_mask_sprite != null and is_instance_valid(_dbg_mask_sprite):
		return
	var host := _get_game_scene()
	if host == null or not is_instance_valid(host):
		return
	var s := Sprite2D.new()
	s.name = "AGM_ShadowCastMask_%d" % get_instance_id()
	s.texture = _mask_tex
	s.centered = false
	# ★top_level にしないと親の座標が掛かる。マスクの原点はワールド座標なので、
	#   掛かると**ズレを見るための表示自体がズレる**
	s.top_level = true
	s.position = _mask_origin
	s.z_index = 4096
	s.z_as_relative = false
	s.modulate = Color(1.0, 0.2, 1.0, 0.75)
	s.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
	host.add_child(s)
	_dbg_mask_sprite = s


# ★★足元の位置を毎コマ取り直して、変わった時だけ送る。
#   貼るのは起動直後なので、AnimatedSprite2D のアニメがまだ用意できて
#   おらず、**絵の大きさが 0 で返ることがある**。0 のまま固定すると
#   傾きの起点が潰れて、斜めがほとんど出ない。
#   送るのは変わった時だけなので、毎コマの費用はほぼゼロ。
func _tick_connect() -> void:
	if _tick_connected:
		return
	var tree := _get_owner_tree()
	if tree == null:
		return
	var cb := Callable(self, "_on_tick")
	if not tree.process_frame.is_connected(cb):
		tree.process_frame.connect(cb)
	# ★★繋いだ木を控える。切る時に _owner_node から取り直すと、
	#   相手が死んでいる場面で null が返って**切れないまま**になる
	#   （旗だけ下りて、呼び出しは永久に続く）
	_tick_tree = tree
	_tick_connected = true


func _tick_disconnect() -> void:
	if not _tick_connected:
		return
	var tree := _tick_tree
	if tree == null or not is_instance_valid(tree):
		tree = _get_owner_tree()
	if tree != null and is_instance_valid(tree):
		var cb := Callable(self, "_on_tick")
		if tree.process_frame.is_connected(cb):
			tree.process_frame.disconnect(cb)
	_tick_tree = null
	_tick_connected = false


func _on_tick() -> void:
	# ★★相手が居なくなったら、まず自分を止める。ここを入れないと
	#   ステージが捨てられたあとも死んだノードを触り続け、
	#   毎コマ「Trying to cast a freed object」を吐く
	if _owner_node == null or not is_instance_valid(_owner_node):
		_tick_disconnect()
		return
	# ★キャラの絵だけ作り直されることがある（アニメの差し替えなど）。
	#   貼った旗を下ろして、下の挑み直しに拾わせる
	if _applied and (_apply_node == null or not is_instance_valid(_apply_node)):
		_applied = false
	if not _applied:
		# ★貼れていない間は、間を置いて挑み直す。
		#   ポータルで戻った直後は、まだタイルマップもキャラの絵も
		#   木に入っていないことがある
		_retry_wait -= 1
		if _retry_wait <= 0:
			_retry_wait = RETRY_FRAMES
			_apply_shadow()
		return
	_reassert_material()
	_drop_refresh()
	_refresh_bottom()
	_refresh_edge()


# ★★貼った材質を毎コマ貼り直す。
#   本体や他のプラグインが material を上書きすることがある
#   （点滅演出など）。奪われたまま黙っていると「シェーダーが
#   1度も効かない」という形で出て、原因にたどり着けない。
#   ★同じ物なら何もしないので、費用はほぼゼロ。
func _reassert_material() -> void:
	if _shader_mat == null or not is_instance_valid(_shader_mat):
		return
	if _apply_node == null or not is_instance_valid(_apply_node):
		return
	# ★use_parent_material が立っていると、自分の material は**無視される**。
	#   ここを見落とすと、貼れているのに何も起きない
	if _apply_node.use_parent_material:
		# ★★AGMakerのキャラは use_parent_material が立っていることがある。
		#   立っていると**自分の material は完全に無視される**。
		#   貼れているのに1ピクセルも変わらず、警告も出ないので、
		#   原因として非常に見つけにくい。
		# ★外したことを覚えておいて、剥がす時に必ず戻す。
		#   戻さないと、他の演出（親から材質をもらう類）を壊す。
		if not _parent_mat_forced:
			_parent_mat_forced = true
		_apply_node.use_parent_material = false
		_dbg_took = "★use_parent_material を外しました"
	if _apply_node.material != _shader_mat:
		_apply_node.material = _shader_mat
		_dbg_steal += 1


# ★★足元が影の奥へ何px 入っているか。
#   足元から「光の側」へ少しずつ戻って、影が切れた所までの距離。
#   これが斜め線の位置になる。奥へ入るほど線が上がり、全身が沈む。
# ★マスクの画素を読むだけ。1コマ最大32回なので、費用は無視できる。
func _refresh_edge() -> void:
	if _shader_mat == null or not is_instance_valid(_shader_mat):
		return
	if not slant_mask_斜めマスクを使う:
		return
	# ★★解放済みかどうかは**キャストの前**に見ること。
	#   `_apply_node as Node2D` を先に書くと、相手が捨てられた瞬間から
	#   毎コマ「Trying to cast a freed object」を吐き続ける
	if _apply_node == null or not is_instance_valid(_apply_node):
		return
	var n2 := _apply_node as Node2D
	if n2 == null:
		return
	var img := _mask_image()
	if img == null:
		return
	var rad := deg_to_rad(light_angle_光の向き度)
	var dir := Vector2(cos(rad), sin(rad))
	var feet: Vector2 = n2.global_position + Vector2(0.0, _last_bottom) \
		if not is_nan(_last_bottom) else n2.global_position
	var reach: float = maxf(8.0, slant_影の伸び * 64.0)
	var step := 2.0
	var depth := 0.0
	var k := 0.0
	while k <= reach:
		# ★★影が伸びていく先（+dir）へ進み、「まだ先に残っている影の長さ」を測る。
		#
		#   壁の高さ h・伸び s なら影の長さは L = h*s。
		#   壁から d の地点での影の高さは (L-d)/s ＝ 残りの長さに比例する。
		#   だから体が暗くなる高さを決めるのは「残り」であって、
		#   「ここまで何px入ったか」ではない。
		#
		#   逆向き（-dir、光の側）に測ると裏返る。
		#   壁のすぐ横＝残りは長いのに、光の側はすぐ壁で途切れるので
		#   「入り始め」になり、いちばん暗いはずの場所がいちばん明るくなる。
		#
		# ★★ここも**面で読む**こと。1点読みにすると、ディザ（市松）の
		#   隙間を1歩目で引いて即座に打ち切り、残りが常に0になる。
		if _mask_avg_at(img, feet + dir * k,
				foot_sample_足元を読む半径px).a <= 0.1:
			break
		depth = k
		k += step
	if k > reach:
		depth = reach
	# ★床の距離を、体の高さへ直す。
	#   depth は床の上の長さ、edge_u は体の上の高さ。
	#   伸び s は「高さ1pxあたり床を何px進むか」なので、割って戻す。
	#   伸びが1.0の時だけ偶然一致するため、入れ忘れても表に出ない
	var rise: float = depth / maxf(slant_影の伸び, 0.0001)
	_shader_mat.set_shader_parameter("edge_u", rise)
	_dbg_edge = rise
	# ★★足元が影の画素に乗っているか。斜めマスクはここだけで決めるので、
	#   これが「なし」なら何も描かれない。高さ0の理由がこれで割れる。
	# ★★足元で読んだ影の色と濃さを、そのまま控える。
	#   「影の上です」だけでは、濃さが 0.05 でも「上です」になる。
	#   薄ければ掛かっていても見えないので、実数を出さないと切り分かない。
	_dbg_foot_col = _mask_avg_at(img, feet, foot_sample_足元を読む半径px)
	_dbg_foot_on = _dbg_foot_col.a > 0.1
	_dbg_foot_at = feet
	# ★★影の色と濃さは、ここで測ったものをそのまま渡す。
	#   シェーダー側で1点だけ読むと、ディザの隙間を引いて消える。
	_shader_mat.set_shader_parameter("foot_shade", _dbg_foot_col)


func _mask_image() -> Image:
	if _mask_tex == null:
		return null
	if _dbg_mask_img == null:
		_dbg_mask_img = _mask_tex.get_image()
	return _dbg_mask_img


# ★★足元は「1点」ではなく「面」で読む。
#   ドット絵の半透明は、**不透明な点と透明な点を交互に置くディザ**で
#   描かれていることが多い。1点だけ読むと半分の確率で透明を引き、
#   影の真ん中に立っていても「影の外」と判定してしまう。
#   面で平均を取れば、ディザでもベタでも同じ答えになる。
# ---------------------------------------------------------------------
# 落ち影（おまけ）―― 足元から伸びる長丸
# ---------------------------------------------------------------------
# ★キャラの子にする。そうすれば動きに勝手に付いてくる。
#   ワールドに置くと毎コマ位置を送り直すことになる。
# ★絵より後ろに出す。前に出すと足を隠す。

func _drop_setup() -> void:
	if not drop_enabled_落ち影を出す:
		return
	# ★★保存済みのアクションには、あとから足した @export の既定値が
	#   入らず 0 が入る（この案件で既に1度はまっている）。
	#   長さ0・濃さ0 は「出るが見えない」ので、黙って失敗する道になる。
	if is_zero_approx(drop_alpha_落ち影の濃さ) \
			or is_zero_approx(drop_len_落ち影の長さpx):
		_warn("落ち影の長さか濃さが0です。保存済みのアクションだと"
			+ "既定値が入りません。インスペクタの「↩」を押すか、"
			+ "長さ26／太さ11／濃さ45 を手で入れてください。")
		return
	if _drop_sprite != null and is_instance_valid(_drop_sprite):
		return
	if _apply_node == null or not is_instance_valid(_apply_node):
		return
	var s := Sprite2D.new()
	s.name = "AGM_ShadowCastDrop_%d" % get_instance_id()
	s.texture = _drop_texture()
	s.centered = true
	# ★★キャラの絵の**子**にして show_behind_parent で真後ろへ置く。
	#   z_index を -1 にしたら、床（z 0）より後ろへ回って**隠れた**。
	#   子＋show_behind_parent なら、床より前・キャラより後ろに入る。
	# ★flip_h は変形ではなく描画時の旗なので、子の向きは影響を受けない。
	s.show_behind_parent = true
	s.z_as_relative = true
	# ★床の影と溶かすために、落ち影も焼いたマスクを引く
	var sh := Shader.new()
	sh.code = DROP_SHADER_CODE
	var mat := ShaderMaterial.new()
	mat.shader = sh
	s.material = mat
	_drop_mat = mat
	_apply_node.add_child(s)
	_drop_sprite = s
	_drop_refresh()


# 長丸の絵をその場で作る。縁のぼかしは距離で落とす
func _drop_texture() -> ImageTexture:
	var w: int = 64
	var h: int = 64
	var img := Image.create(w, h, false, Image.FORMAT_RGBA8)
	var cx := (float(w) - 1.0) * 0.5
	var cy := (float(h) - 1.0) * 0.5
	# ★ぼかしは「縁から内側へ何割か」で表す。絵を64で作って
	#   あとで拡縮するので、px ではなく割合で持つ
	var soft: float = clampf(drop_blur_落ち影のぼかしpx
		/ maxf(1.0, drop_wide_落ち影の太さpx), 0.0, 0.9)
	for y in range(h):
		for x in range(w):
			var dx := (float(x) - cx) / cx
			var dy := (float(y) - cy) / cy
			var r: float = sqrt(dx * dx + dy * dy)
			var a: float = 1.0
			if soft <= 0.0:
				a = 1.0 if r <= 1.0 else 0.0
			else:
				a = clampf((1.0 - r) / soft, 0.0, 1.0)
			img.set_pixel(x, y, Color(1, 1, 1, a))
	return ImageTexture.create_from_image(img)


# 伸ばす向き（度）。既定は床の影と同じ向きを借りる。
# ★「光の向き度」の +dir は、行進で深さが**減る**側＝影の外の縁。
#   つまり影は壁から +dir へ伸びている。落ち影も同じ向きで合う。
func _drop_angle() -> float:
	if drop_follow_光の向きに合わせる:
		return light_angle_光の向き度
	return drop_angle_落ち影の向き度


func _drop_refresh() -> void:
	if _drop_sprite == null or not is_instance_valid(_drop_sprite):
		return
	if _apply_node == null or not is_instance_valid(_apply_node):
		return
	var rad := deg_to_rad(_drop_angle())
	var dir := Vector2(cos(rad), sin(rad))
	var bottom: float = 0.0 if is_nan(_last_bottom) else _last_bottom
	# ★起点＝絵の足元 ＋ 手で入れたズラし。
	#   ★向きを回す前に足すこと。回したあとに足すと、向きを変えるたびに
	#     ズラしの意味が変わって合わせ直しになる
	var root := Vector2(drop_ox_落ち影の起点ズラシXpx,
		bottom + drop_oy_落ち影の起点ズラシYpx)
	# ★足元を起点に、伸びる向きへ張り出させる。中心は半分だけ先
	var half_len: float = maxf(1.0, drop_len_落ち影の長さpx) * 0.5
	var here: Vector2 = root + dir * (half_len
		- drop_wide_落ち影の太さpx * 0.5)
	_drop_sprite.position = here
	_drop_sprite.rotation = rad
	# 64x64 の絵を、長さ×太さへ伸ばす
	_drop_sprite.scale = Vector2(
		maxf(1.0, drop_len_落ち影の長さpx) / 64.0,
		maxf(1.0, drop_wide_落ち影の太さpx) / 64.0)
	# ★色は床の影と同じものを使う。別の色を持たせると、
	#   床とキャラと落ち影で色が3つに割れる
	var c: Color = _mask_rep_color
	c.a = clampf(drop_alpha_落ち影の濃さ / 100.0, 0.0, 1.0)
	_drop_sprite.modulate = c
	# ★マスクは焼き直されることがある（ポータルでステージが変わる）ので
	#   毎コマ入れ直す。同じ物なら費用はほぼゼロ
	if _drop_mat != null and is_instance_valid(_drop_mat):
		_drop_mat.set_shader_parameter("shadow_mask", _mask_tex)
		_drop_mat.set_shader_parameter("mask_origin", _mask_origin)
		_drop_mat.set_shader_parameter("mask_size_px", _mask_size)
		_drop_mat.set_shader_parameter("mask_tint", _mask_tint)
		_drop_mat.set_shader_parameter("blend_mode", drop_blend_落ち影の混ぜ方)


func _drop_free() -> void:
	if _drop_sprite != null and is_instance_valid(_drop_sprite):
		_drop_sprite.queue_free()
	_drop_sprite = null
	_drop_mat = null


func _mask_avg_at(img: Image, world: Vector2, half: int) -> Color:
	if _mask_size.x <= 0.0 or _mask_size.y <= 0.0:
		return Color(0, 0, 0, 0)
	var cx := int(floorf((world.x - _mask_origin.x) / _mask_size.x
		* float(img.get_width())))
	var cy := int(floorf((world.y - _mask_origin.y) / _mask_size.y
		* float(img.get_height())))
	var sum_a := 0.0
	var sum_rgb := Vector3.ZERO
	var lit := 0
	var n := 0
	for dy in range(-half, half + 1):
		for dx in range(-half, half + 1):
			var px := cx + dx
			var py := cy + dy
			if px < 0 or py < 0 or px >= img.get_width() \
					or py >= img.get_height():
				continue
			n += 1
			var c := img.get_pixel(px, py)
			sum_a += c.a
			if c.a > 0.1:
				lit += 1
				sum_rgb += Vector3(c.r, c.g, c.b)
	if n == 0:
		return Color(0, 0, 0, 0)
	# ★色は「不透明だった点」だけの平均。透明な点の色を混ぜると
	#   ディザの隙間の色に引っぱられて、影の色が薄く出る
	var rgb := (sum_rgb / float(lit)) if lit > 0 else Vector3.ZERO
	return Color(rgb.x, rgb.y, rgb.z, sum_a / float(n))


func _mask_color_at(img: Image, world: Vector2) -> Color:
	if _mask_size.x <= 0.0 or _mask_size.y <= 0.0:
		return Color(0, 0, 0, 0)
	var px := int(floorf((world.x - _mask_origin.x) / _mask_size.x
		* float(img.get_width())))
	var py := int(floorf((world.y - _mask_origin.y) / _mask_size.y
		* float(img.get_height())))
	if px < 0 or py < 0 or px >= img.get_width() or py >= img.get_height():
		return Color(0, 0, 0, 0)
	return img.get_pixel(px, py)


func _mask_alpha_at(img: Image, world: Vector2) -> float:
	if _mask_size.x <= 0.0 or _mask_size.y <= 0.0:
		return 0.0
	var px := int(floorf((world.x - _mask_origin.x) / _mask_size.x
		* float(img.get_width())))
	var py := int(floorf((world.y - _mask_origin.y) / _mask_size.y
		* float(img.get_height())))
	if px < 0 or py < 0 or px >= img.get_width() or py >= img.get_height():
		return 0.0
	return img.get_pixel(px, py).a


func _refresh_bottom() -> void:
	if _shader_mat == null or not is_instance_valid(_shader_mat):
		return
	if _apply_node == null or not is_instance_valid(_apply_node):
		return
	var b := _local_bottom(_apply_node)
	if is_equal_approx(b, _last_bottom):
		return
	_last_bottom = b
	_shader_mat.set_shader_parameter("local_bottom", b)


# ★絵のローカル座標での「足元」のy。シェーダーが高さを測る基準になる。
#   ここを間違えると、傾きの起点がずれて境目の位置が全部ずれる。
func _local_bottom(ci: CanvasItem) -> float:
	if ci == null or not is_instance_valid(ci):
		return 0.0
	var size := Vector2.ZERO
	var centered := true
	var offset := Vector2.ZERO
	if ci is Sprite2D:
		var sp := ci as Sprite2D
		if sp.texture != null:
			size = sp.texture.get_size()
		centered = sp.centered
		offset = sp.offset
	elif ci is AnimatedSprite2D:
		var an := ci as AnimatedSprite2D
		var frames := an.sprite_frames
		if frames != null and frames.has_animation(an.animation):
			var tex := frames.get_frame_texture(an.animation, an.frame)
			if tex != null:
				size = tex.get_size()
		centered = an.centered
		offset = an.offset
	if size == Vector2.ZERO:
		return 0.0
	return offset.y + (size.y * 0.5 if centered else size.y)


# キャラの絵が、ワールドのどこを占めているか（基準点ではなく**四角**）
# ★基準点1点で「中か外か」を言うと嘘になる。足元が外でも頭は中、が普通に起きる。
func _dbg_sprite_rect() -> Rect2:
	var ci := _apply_node
	if ci == null or not is_instance_valid(ci):
		return Rect2()
	var size := Vector2.ZERO
	var centered := true
	var offset := Vector2.ZERO
	if ci is Sprite2D:
		var sp := ci as Sprite2D
		if sp.texture != null:
			size = sp.texture.get_size()
		centered = sp.centered
		offset = sp.offset
	elif ci is AnimatedSprite2D:
		var an := ci as AnimatedSprite2D
		var frames := an.sprite_frames
		if frames != null and frames.has_animation(an.animation):
			var tex := frames.get_frame_texture(an.animation, an.frame)
			if tex != null:
				size = tex.get_size()
		centered = an.centered
		offset = an.offset
	if size == Vector2.ZERO:
		return Rect2()
	var local_pos := offset - (size * 0.5 if centered else Vector2.ZERO)
	var xf := ci.get_global_transform()
	var tl: Vector2 = xf * local_pos
	var br: Vector2 = xf * (local_pos + size)
	return Rect2(Vector2(minf(tl.x, br.x), minf(tl.y, br.y)),
		(br - tl).abs())
