feat(scripting): connect give_item to wallet rewards, add [E] Interact prompt and toast feedback
checkpoint / checkpoint (push) Failing after 6m2s
checkpoint / checkpoint (push) Failing after 6m2s
This commit is contained in:
@@ -76,3 +76,38 @@ async def get_wallet_transactions(
|
|||||||
limit=limit,
|
limit=limit,
|
||||||
offset=offset,
|
offset=offset,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RewardRequest(BaseModel):
|
||||||
|
amount: int = Field(..., gt=0, le=10000, description="Amount of Buffalo Chips to reward")
|
||||||
|
reason: str | None = Field(default="in_game_reward", description="Reward source or reason")
|
||||||
|
|
||||||
|
|
||||||
|
class RewardResponse(BaseModel):
|
||||||
|
success: bool
|
||||||
|
amount: int
|
||||||
|
new_balance: int
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/reward", response_model=RewardResponse)
|
||||||
|
async def reward_chips(
|
||||||
|
payload: RewardRequest,
|
||||||
|
current_user: CurrentUser,
|
||||||
|
db: DbDep,
|
||||||
|
):
|
||||||
|
"""Credit in-game Buffalo Chips reward directly to the player's wallet."""
|
||||||
|
wallet, tx = await wallet_service.credit(
|
||||||
|
db,
|
||||||
|
user_id=current_user.id,
|
||||||
|
amount=payload.amount,
|
||||||
|
tx_type=TransactionType.admin_adjustment,
|
||||||
|
reference_type="gameplay_reward",
|
||||||
|
reference_id=payload.reason or "in_game_reward",
|
||||||
|
metadata={"reason": payload.reason or "in_game_reward"},
|
||||||
|
)
|
||||||
|
return RewardResponse(
|
||||||
|
success=True,
|
||||||
|
amount=payload.amount,
|
||||||
|
new_balance=wallet.balance,
|
||||||
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -133,3 +133,30 @@ async def test_wallet_service_credit_and_debit_flow():
|
|||||||
# Newest first: debit (-400) first, then credit (+1000)
|
# Newest first: debit (-400) first, then credit (+1000)
|
||||||
assert tx_data["transactions"][0]["amount"] == -400
|
assert tx_data["transactions"][0]["amount"] == -400
|
||||||
assert tx_data["transactions"][1]["amount"] == 1000
|
assert tx_data["transactions"][1]["amount"] == 1000
|
||||||
|
|
||||||
|
|
||||||
|
def test_wallet_reward_endpoint():
|
||||||
|
client = TestClient(app)
|
||||||
|
uid, headers = _register(client, "wallet_reward_user")
|
||||||
|
|
||||||
|
# Initial balance is 0
|
||||||
|
resp = client.get("/api/wallet", headers=headers)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.json()["balance"] == 0
|
||||||
|
|
||||||
|
# Reward 25 Buffalo Chips
|
||||||
|
reward_resp = client.post(
|
||||||
|
"/api/wallet/reward",
|
||||||
|
json={"amount": 25, "reason": "found_chest"},
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
assert reward_resp.status_code == 200
|
||||||
|
data = reward_resp.json()
|
||||||
|
assert data["success"] is True
|
||||||
|
assert data["amount"] == 25
|
||||||
|
assert data["new_balance"] == 25
|
||||||
|
|
||||||
|
# Check updated wallet
|
||||||
|
resp = client.get("/api/wallet", headers=headers)
|
||||||
|
assert resp.json()["balance"] == 25
|
||||||
|
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ var _timers: Dictionary = {} # name (String) -> {"remaining": float, "active":
|
|||||||
|
|
||||||
var _timer_nodes: Array[Timer] = []
|
var _timer_nodes: Array[Timer] = []
|
||||||
var _spawn_catalog: SpawnCatalogClass = null
|
var _spawn_catalog: SpawnCatalogClass = null
|
||||||
|
var _interact_prompt: Label3D = null
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -106,6 +107,7 @@ func _apply_loaded_behavior() -> void:
|
|||||||
rule_rejected.emit(msg)
|
rule_rejected.emit(msg)
|
||||||
_setup_area()
|
_setup_area()
|
||||||
_setup_timers()
|
_setup_timers()
|
||||||
|
_setup_prompt()
|
||||||
|
|
||||||
|
|
||||||
func _setup_area() -> void:
|
func _setup_area() -> void:
|
||||||
@@ -156,6 +158,32 @@ func _setup_timers() -> void:
|
|||||||
_timer_nodes.append(timer)
|
_timer_nodes.append(timer)
|
||||||
|
|
||||||
|
|
||||||
|
func _setup_prompt() -> void:
|
||||||
|
if _interact_prompt != null and is_instance_valid(_interact_prompt):
|
||||||
|
_interact_prompt.queue_free()
|
||||||
|
_interact_prompt = null
|
||||||
|
if behavior == null:
|
||||||
|
return
|
||||||
|
var has_interact_rule := false
|
||||||
|
for rule in behavior.rules:
|
||||||
|
if rule.trigger_type == "on_interact":
|
||||||
|
has_interact_rule = true
|
||||||
|
break
|
||||||
|
if has_interact_rule and DisplayServer.get_name() != "headless":
|
||||||
|
_interact_prompt = Label3D.new()
|
||||||
|
_interact_prompt.name = "InteractPrompt"
|
||||||
|
_interact_prompt.text = "[E] Interact"
|
||||||
|
_interact_prompt.billboard = BaseMaterial3D.BILLBOARD_ENABLED
|
||||||
|
_interact_prompt.font_size = 32
|
||||||
|
_interact_prompt.outline_size = 8
|
||||||
|
_interact_prompt.modulate = Color(1.0, 0.88, 0.2)
|
||||||
|
_interact_prompt.outline_modulate = Color(0.0, 0.0, 0.0, 0.9)
|
||||||
|
_interact_prompt.no_depth_test = true
|
||||||
|
_interact_prompt.position = Vector3(0, 1.2, 0)
|
||||||
|
_interact_prompt.visible = false
|
||||||
|
add_child(_interact_prompt)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Trigger sources
|
# Trigger sources
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -172,6 +200,8 @@ func _on_body_entered(body: Node) -> void:
|
|||||||
return
|
return
|
||||||
if not _players_in_range.has(body):
|
if not _players_in_range.has(body):
|
||||||
_players_in_range.append(body)
|
_players_in_range.append(body)
|
||||||
|
if _interact_prompt != null and is_instance_valid(_interact_prompt):
|
||||||
|
_interact_prompt.visible = true
|
||||||
var id := body.get_instance_id()
|
var id := body.get_instance_id()
|
||||||
if not _entered_once.has(id):
|
if not _entered_once.has(id):
|
||||||
_entered_once[id] = true
|
_entered_once[id] = true
|
||||||
@@ -181,6 +211,8 @@ func _on_body_entered(body: Node) -> void:
|
|||||||
func _on_body_exited(body: Node) -> void:
|
func _on_body_exited(body: Node) -> void:
|
||||||
_players_in_range.erase(body)
|
_players_in_range.erase(body)
|
||||||
_entered_once.erase(body.get_instance_id())
|
_entered_once.erase(body.get_instance_id())
|
||||||
|
if _interact_prompt != null and is_instance_valid(_interact_prompt) and _players_in_range.is_empty():
|
||||||
|
_interact_prompt.visible = false
|
||||||
|
|
||||||
|
|
||||||
func _unhandled_input(event: InputEvent) -> void:
|
func _unhandled_input(event: InputEvent) -> void:
|
||||||
@@ -356,9 +388,58 @@ func _act_give_item(action: Dictionary, player: Node) -> void:
|
|||||||
if inv == null:
|
if inv == null:
|
||||||
push_warning("ScriptInterpreterNode(%s): give_item has no player Inventory to target" % name)
|
push_warning("ScriptInterpreterNode(%s): give_item has no player Inventory to target" % name)
|
||||||
return
|
return
|
||||||
inv.give_item(action.get("item_id", ""), int(action.get("count", 0)))
|
var item_id: String = str(action.get("item_id", ""))
|
||||||
|
var count: int = int(action.get("count", 0))
|
||||||
|
inv.give_item(item_id, count)
|
||||||
verb_executed.emit("give_item", action)
|
verb_executed.emit("give_item", action)
|
||||||
|
|
||||||
|
_show_give_feedback(item_id, count)
|
||||||
|
if item_id.to_lower() in ["buffalo_chips", "chips", "credits"]:
|
||||||
|
_deposit_to_wallet(count)
|
||||||
|
|
||||||
|
|
||||||
|
func _show_give_feedback(item_id: String, count: int) -> void:
|
||||||
|
if DisplayServer.get_name() == "headless":
|
||||||
|
return
|
||||||
|
var text := ""
|
||||||
|
if item_id.to_lower() in ["buffalo_chips", "chips", "credits"]:
|
||||||
|
text = "+%d Buffalo Chips!" % count
|
||||||
|
else:
|
||||||
|
text = "+%d %s" % [count, item_id]
|
||||||
|
|
||||||
|
var toast := Label3D.new()
|
||||||
|
toast.text = text
|
||||||
|
toast.billboard = BaseMaterial3D.BILLBOARD_ENABLED
|
||||||
|
toast.font_size = 40
|
||||||
|
toast.outline_size = 10
|
||||||
|
toast.modulate = Color(1.0, 0.9, 0.2)
|
||||||
|
toast.outline_modulate = Color(0.1, 0.05, 0.0, 1.0)
|
||||||
|
toast.no_depth_test = true
|
||||||
|
toast.position = Vector3(0, 1.5, 0)
|
||||||
|
add_child(toast)
|
||||||
|
|
||||||
|
var tween := create_tween()
|
||||||
|
if tween != null:
|
||||||
|
tween.tween_property(toast, "position:y", toast.position.y + 1.2, 1.4)
|
||||||
|
tween.parallel().tween_property(toast, "modulate:a", 0.0, 1.4)
|
||||||
|
tween.tween_callback(toast.queue_free)
|
||||||
|
|
||||||
|
|
||||||
|
func _deposit_to_wallet(count: int) -> void:
|
||||||
|
if count <= 0:
|
||||||
|
return
|
||||||
|
if has_node("/root/ApiClient"):
|
||||||
|
var api = get_node("/root/ApiClient")
|
||||||
|
if api != null and api.has_token():
|
||||||
|
api.request(
|
||||||
|
HTTPClient.METHOD_POST,
|
||||||
|
"/api/wallet/reward",
|
||||||
|
JSON.stringify({"amount": count, "reason": "in_game_reward"}),
|
||||||
|
"application/json",
|
||||||
|
true
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
func _act_play_sound(action: Dictionary, _player: Node) -> void:
|
func _act_play_sound(action: Dictionary, _player: Node) -> void:
|
||||||
# No real audio asset catalog exists in this repo yet. The verb still
|
# No real audio asset catalog exists in this repo yet. The verb still
|
||||||
|
|||||||
Reference in New Issue
Block a user