Cómo crear recompensas diarias por conectarse

Claim Your Loot

·

Portada del artículo «Recompensas diarias con racha», de la sección Sistemas de botín

La recompensa diaria es el truco más viejo y más eficaz que hay para que la gente vuelva: entras, te dan algo, y mañana te dan un poco más si vuelves. Lo que hace que funcione no es el regalo, es la racha: nadie quiere perder siete días seguidos.

Programarla parece trivial hasta que aparecen las dos preguntas incómodas: ¿qué es «un día» exactamente, y qué pasa si el jugador se cambia la hora del ordenador?

En la demo puedes avanzar el reloj día a día, saltarte uno y ver cómo se rompe la racha.

Respuesta rápida

Guarda la hora del servidor (os.time()) de la última recompensa. Al entrar, compara con la de ahora: menos de 24 h, nada; entre 24 y 48 h, siguiente día de racha; más de 48 h, la racha vuelve a empezar. El reloj del jugador no se consulta nunca.

Lo que necesitas antes de empezar

Paso 1: qué guardamos

Solo dos datos por jugador:

local POR_DEFECTO = {
	ultimaRecompensa = 0,    -- os.time() de la ultima vez que cobro
	racha = 0,               -- cuantos dias seguidos lleva
}

os.time() devuelve los segundos transcurridos desde 1970 en UTC, igual en todos los servidores del mundo. Por eso vale: no depende de la zona horaria del jugador ni de la del servidor que le toque.

Paso 2: decidir qué le toca

local UN_DIA = 24 * 60 * 60        -- 86.400 segundos

local function estadoRecompensa(datos)
	local ahora = os.time()
	local pasado = ahora - datos.ultimaRecompensa

	if datos.ultimaRecompensa == 0 then
		return "primera", 1
	end

	if pasado < UN_DIA then
		return "esperar", datos.racha          -- ya cobro hoy
	end

	if pasado < UN_DIA * 2 then
		return "sigue", datos.racha + 1        -- vuelve a tiempo
	end

	return "rota", 1                            -- se salto un dia
end

Los tres casos, en cristiano:

  • Menos de 24 h: ya ha cobrado. Le enseñas cuánto falta.
  • Entre 24 y 48 h: ha vuelto dentro de la ventana. Racha +1.
  • Más de 48 h: se saltó un día. Vuelve al día 1.

Esa ventana de 24 a 48 horas es más justa que el «día natural». Con días naturales, quien juega a las 23:50 puede cobrar otra vez a las 00:10; y quien juega a las 10:00 tiene que esperar catorce horas. Con la ventana, todo el mundo tiene exactamente un día.

Las tres zonas de la ventana de la racha y la tabla de recompensas de siete días

Paso 3: la tabla de recompensas

local RECOMPENSAS = {
	[1] = {monedas = 50,   texto = "50 monedas"},
	[2] = {monedas = 75,   texto = "75 monedas"},
	[3] = {monedas = 100,  texto = "100 monedas"},
	[4] = {monedas = 150,  texto = "150 monedas"},
	[5] = {monedas = 200,  texto = "200 monedas"},
	[6] = {monedas = 300,  texto = "300 monedas"},
	[7] = {monedas = 750,  texto = "750 monedas + cofre", cofre = true},
}

local function recompensaDelDia(racha)
	local dia = ((racha - 1) % 7) + 1     -- del 7 vuelve al 1
	return RECOMPENSAS[dia], dia
end

Ese % hace que el ciclo se repita: día 8 = día 1 otra vez, pero con la racha intacta. Así puedes premiar rachas largas (un objeto especial a los 30 días) sin escribir treinta filas.

El día 7 tiene que ser claramente el gordo. Si el séptimo día da 350 en vez de 300, nadie aguanta la semana. El salto se tiene que ver.

Paso 4: entregarla

Un RemoteFunction va bien aquí, porque el cliente quiere una respuesta:

local Replicado = game:GetService("ReplicatedStorage")
local funcion = Replicado:WaitForChild("PedirRecompensa")

funcion.OnServerInvoke = function(jugador)
	local datos = sesiones[jugador]
	if not datos then return {ok = false, motivo = "cargando"} end

	local estado, nuevaRacha = estadoRecompensa(datos)

	if estado == "esperar" then
		local faltan = (datos.ultimaRecompensa + UN_DIA) - os.time()
		return {ok = false, motivo = "esperar", segundos = faltan}
	end

	local premio, dia = recompensaDelDia(nuevaRacha)

	-- Se apunta ANTES de entregar
	datos.racha = nuevaRacha
	datos.ultimaRecompensa = os.time()

	local stats = jugador:FindFirstChild("leaderstats")
	local monedas = stats and stats:FindFirstChild("Monedas")
	if monedas then
		monedas.Value = monedas.Value + premio.monedas
	end

	guardar(jugador)     -- esto se guarda ya, no esperamos al reloj

	return {ok = true, dia = dia, racha = nuevaRacha, texto = premio.texto}
end

Dos decisiones que parecen detalles y no lo son:

  • Se marca la fecha antes de entregar. Si entregas primero y algo se cae en medio, el jugador puede repetir la llamada y cobrar dos veces.
  • Se guarda inmediatamente. Una recompensa diaria perdida por un servidor que se cayó a los dos minutos es una queja segura en el chat del juego.

Paso 5: la cuenta atrás en el cliente

local funcion = game:GetService("ReplicatedStorage"):WaitForChild("PedirRecompensa")
local boton = script.Parent.Boton
local etiqueta = script.Parent.Aviso

local function formatear(segundos)
	local h = math.floor(segundos / 3600)
	local m = math.floor((segundos % 3600) / 60)
	return h .. " h " .. m .. " min"
end

boton.Activated:Connect(function()
	boton.Active = false
	local r = funcion:InvokeServer()

	if r.ok then
		etiqueta.Text = "Dia " .. r.dia .. ": " .. r.texto
	else
		if r.motivo == "esperar" then
			etiqueta.Text = "Vuelve en " .. formatear(r.segundos)
		else
			etiqueta.Text = "Un momento..."
			boton.Active = true
		end
	end
end)

La cuenta atrás la calcula el servidor y el cliente solo la enseña. Si dejas que el cliente decida cuándo han pasado 24 horas, adelantar el reloj del ordenador se lleva la recompensa.

Paso 6: enseñar la semana entera

El panel que se ve en los juegos que lo hacen bien es una fila de siete casillas: las cobradas marcadas, la de hoy resaltada, las de después en gris. No es adorno: enseñar el día 7 desde el día 1 es lo que sostiene la racha. Si el jugador no ve lo que se pierde, no vuelve.

Pruébalo aquí

Mueve el reloj y mira qué pasa con la racha

Vuelve al rato, vuelve al día siguiente o sáltate un día. La lógica es exactamente la de los pasos 2 y 3.

🔥 Racha: 0 días 🪙 0 monedas Hora del servidor: día 1, 00:00

Cada botón adelanta el reloj y vuelve a pedir la recompensa, igual que si el jugador entrase en ese momento.

Historial

Mejor racha: 0 días

Errores habituales

El jugador cobra varias veces seguidas

No estás guardando ultimaRecompensa, o lo guardas después de entregar y algo se cae en medio. Márcalo primero.

Puede cobrar adelantando el reloj de su PC

Estás usando la hora del cliente en alguna parte. La única hora válida es os.time() en el servidor.

La racha se rompe sola aunque entre todos los días

Estás comparando días naturales con os.date y la zona horaria te está jugando una mala pasada, o la ventana es de 24 h exactas en lugar de 48. Con pasado < UN_DIA * 2 para seguir la racha, entrar «a la misma hora más o menos» siempre funciona.

Se pierde la racha al cambiar de servidor

No se está guardando en el DataStore, o se guarda solo cada dos minutos y el jugador salió antes. Guarda al entregar.

El botón se puede pulsar veinte veces mientras responde el servidor

Falta desactivarlo (boton.Active = false) al invocar. En el servidor, además, la marca de fecha ya lo impide, pero la interfaz debe acompañar.

Preguntas frecuentes

¿Y si quiero que la racha aguante un día de fallo?

Amplía la ventana a 48 horas para seguir y rompe a las 72. Es más amable y se nota en la retención. Lo que no debes hacer es permitir cobrar dos veces en el mismo día.

¿Puedo dar un objeto en vez de monedas?

Sí: cambia premio.monedas por lo que entregues. Si es un objeto único, marca que ya lo tiene para no duplicarlo, como en la tienda.

¿Cómo hago una racha de 30 días con premio final?

La tabla se queda igual (ciclo de 7) y añades una comprobación aparte: si racha == 30, además del premio del día, el objeto especial. Y guárdalo como comprado para que no se repita al llegar a 60.

¿Debería avisar al jugador de que va a perder la racha?

Si tienes notificaciones fuera del juego, es lo que más funciona. Dentro del juego, con enseñar «racha: 6 días» bien grande junto al botón ya haces la mitad del trabajo.


Última revisión: 1 de septiembre de 2026

Claim Your Loot

Explora otras secciones

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *