Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

combat.gno

9.02 Kb · 419 lines
  1package fish2
  2
  3import (
  4	"chain"
  5	"strconv"
  6	"time"
  7
  8	"gno.land/r/g1n4pl5uc4yt5r96m9w6fmdznx3x0jyg8l6arhmt/party"
  9)
 10
 11type damageRow struct {
 12	owner address
 13	dmg   int
 14}
 15
 16// blows[victimID] sums HP actually removed, keyed by the attacker's owner.
 17var blows [][]damageRow
 18
 19// Bite is one user call and takes no payment. The hit test is sector-center
 20// reach plus a 42 degree mouth cone. A miss does not start cooldown. Members
 21// of the same party cannot bite each other. A killing blow pays the smaller
 22// Pool X and leaves the NFT at 30% HP. The killer share is split by HP removed.
 23// Empty cheer and viewer shares pay the season sink.
 24func Bite(cur realm, id, target int) {
 25	caller := requireUser(cur)
 26	takeExact(0)
 27	if id == target {
 28		panic("fish: bite")
 29	}
 30	atk := must(id)
 31	vic := must(target)
 32	if atk.owner != caller {
 33		panic("fish: owner")
 34	}
 35	if atk.mustHeal || vic.mustHeal {
 36		panic("fish: heal")
 37	}
 38	now := time.Now().Unix()
 39	if !liveFish(atk, now) || !liveFish(vic, now) {
 40		panic("fish: match")
 41	}
 42	if atk.owner == vic.owner {
 43		panic("fish: bite")
 44	}
 45	if party.SameParty(cross(cur), atk.owner, vic.owner) {
 46		panic("fish: party")
 47	}
 48	nowMs := time.Now().UnixMilli()
 49	// A cave or reef does not dodge. The cone still has to face the fish inside.
 50	if !mouthHit(atk, vic) && !shelterMouth(atk, vic, nowMs) {
 51		panic("fish: range")
 52	}
 53	if nowMs < atk.nextBiteMilli {
 54		panic("fish: cool")
 55	}
 56	if atk.bittenBy == vic.id && nowMs < atk.bittenUntil {
 57		panic("fish: cool")
 58	}
 59	cool := nowMs + cooldownMs(atk.rarity)
 60	atk.nextBiteMilli = cool
 61	atk.lastCombatMilli = nowMs
 62	if redirectShelter(atk, vic, nowMs) {
 63		chain.Emit("Bite", "id", strconv.Itoa(id), "target", strconv.Itoa(target), "damage", "shelter")
 64		return
 65	}
 66	vic.lastCombatMilli = nowMs
 67	vic.bittenBy = atk.id
 68	vic.bittenUntil = cool
 69	dmg := strikeFought(biteNow(atk), vic.hp, atk.size, vic.size, atk.rarity, vic.rarity)
 70	landed := dmg
 71	if landed > vic.hpNow {
 72		landed = vic.hpNow
 73	}
 74	addDamage(vic.id, atk.owner, landed)
 75	if vic.hpNow > dmg {
 76		vic.hpNow -= dmg
 77		chain.Emit("Bite", "id", strconv.Itoa(id), "target", strconv.Itoa(target), "damage", strconv.Itoa(dmg))
 78		return
 79	}
 80	payKill(atk, vic, cur)
 81}
 82
 83func payKill(atk, vic *fishBody, rlm realm) {
 84	if !rlm.IsCurrent() {
 85		panic("fish: realm")
 86	}
 87	covered := coveredStake(atk.pool, vic.pool)
 88	excess := vic.pool - covered
 89	killer, lp, season, burn, cheer, viewer := splitParts(covered)
 90	nowMs := time.Now().UnixMilli()
 91	creditBout(atk)
 92	creditBout(vic)
 93	growFromKill(atk, vic)
 94	returnCheers(vic, rlm)
 95	vic.pool = 0
 96	vic.inMatch = false
 97	vic.boutPaid = false
 98	vic.mustHeal = true
 99	vic.hpNow = downHP(vic.hp)
100	cheerHeld := bookCheerClaims(atk, vic, cheer, nowMs)
101	season += cheer - cheerHeld
102	viewHeld := bookWatchClaims(atk, vic, viewer, nowMs)
103	season += viewer - viewHeld
104	send(vic.owner, excess, rlm)
105	payKiller(atk.owner, vic.id, killer, rlm)
106	send(lpSink, lp, rlm)
107	send(seasonSink, season, rlm)
108	send(burnSink, burn, rlm)
109	clearDamage(vic.id)
110	chain.Emit(
111		"Kill",
112		"id", strconv.Itoa(atk.id),
113		"target", strconv.Itoa(vic.id),
114		"covered", strconv.FormatInt(covered, 10),
115		"killer", strconv.FormatInt(killer, 10),
116	)
117}
118
119// payKiller splits the killer share by HP removed among current members.
120// The party realm holds no coins. A solo owner, or a party with no recorded
121// damage, receives the whole share. Dust goes to the highest damage, and a
122// tie prefers the owner who landed the blow.
123func payKiller(owner address, victim int, amount int64, rlm realm) {
124	if amount <= 0 {
125		return
126	}
127	pid := party.PartyOf(cross(rlm), owner)
128	if pid == 0 {
129		send(owner, amount, rlm)
130		return
131	}
132	n := party.MemberCount(cross(rlm), pid)
133	if n <= 0 {
134		send(owner, amount, rlm)
135		return
136	}
137	owners := make([]address, n)
138	weights := make([]int, n)
139	total := 0
140	for i := 0; i < n; i++ {
141		who := party.MemberAt(cross(rlm), pid, i)
142		owners[i] = who
143		w := damageOf(victim, who)
144		if w < 0 {
145			w = 0
146		}
147		weights[i] = w
148		total += w
149	}
150	if total <= 0 {
151		send(owner, amount, rlm)
152		return
153	}
154	shares := splitWeights(amount, weights, owner, owners)
155	for i := range owners {
156		send(owners[i], shares[i], rlm)
157	}
158}
159
160func splitWeights(amount int64, weights []int, prefer address, owners []address) []int64 {
161	total := 0
162	for _, w := range weights {
163		if w > 0 {
164			total += w
165		}
166	}
167	out := make([]int64, len(weights))
168	if amount <= 0 || total <= 0 {
169		return out
170	}
171	var used int64
172	best := -1
173	bestW := 0
174	for i, w := range weights {
175		if w <= 0 {
176			continue
177		}
178		share := amount * int64(w) / int64(total)
179		out[i] = share
180		used += share
181		if best < 0 || w > bestW || (w == bestW && i < len(owners) && owners[i] == prefer) {
182			best = i
183			bestW = w
184		}
185	}
186	if used < amount && best >= 0 {
187		out[best] += amount - used
188	}
189	return out
190}
191
192func addDamage(id int, owner address, n int) {
193	if n <= 0 || id < 1 || !owner.IsValid() {
194		return
195	}
196	for len(blows) <= id {
197		blows = append(blows, nil)
198	}
199	rows := blows[id]
200	for i := 0; i < len(rows); i++ {
201		if rows[i].owner == owner {
202			rows[i].dmg += n
203			blows[id] = rows
204			return
205		}
206	}
207	blows[id] = append(rows, damageRow{owner: owner, dmg: n})
208}
209
210func damageOf(id int, owner address) int {
211	if id < 0 || id >= len(blows) {
212		return 0
213	}
214	for i := 0; i < len(blows[id]); i++ {
215		if blows[id][i].owner == owner && blows[id][i].dmg > 0 {
216			return blows[id][i].dmg
217		}
218	}
219	return 0
220}
221
222// clearDamage drops incoming blows so the next entry starts clean.
223func clearDamage(id int) {
224	if id < 0 || id >= len(blows) {
225		return
226	}
227	blows[id] = nil
228}
229
230// creditBout counts one finished entry. Leave does not call it.
231// SettleMatch does not count the entry again.
232func creditBout(body *fishBody) {
233	if body.boutPaid {
234		return
235	}
236	body.boutPaid = true
237	body.bouts++
238}
239
240func liveFish(body *fishBody, now int64) bool {
241	if now < 0 {
242		return false
243	}
244	return body.inMatch && body.pool > 0
245}
246
247func downHP(max int) int {
248	hp := max * 30 / 100
249	if hp < 1 {
250		return 1
251	}
252	return hp
253}
254
255func coveredStake(killerPool, victimPool int64) int64 {
256	if killerPool < 0 {
257		killerPool = 0
258	}
259	if victimPool < 0 {
260		victimPool = 0
261	}
262	if killerPool < victimPool {
263		return killerPool
264	}
265	return victimPool
266}
267
268// splitParts is 80/10/3/2/4/1. Dust joins burn. Season here is only the 3%.
269func splitParts(amount int64) (killer, lp, season, burn, cheer, viewer int64) {
270	if amount < 0 {
271		amount = 0
272	}
273	killer = amount * 8000 / 10000
274	lp = amount * 1000 / 10000
275	season = amount * 300 / 10000
276	burnPart := amount * 200 / 10000
277	cheer = amount * 400 / 10000
278	viewer = amount * 100 / 10000
279	dust := amount - killer - lp - season - burnPart - cheer - viewer
280	burn = burnPart + dust
281	return
282}
283
284// splitKill folds empty cheer and viewer into season. Callers that book those
285// shares use splitParts instead.
286func splitKill(amount int64) (killer, lp, season, burn int64) {
287	var cheer, viewer int64
288	killer, lp, season, burn, cheer, viewer = splitParts(amount)
289	season += cheer + viewer
290	return
291}
292
293func strikeDamage(bite, hpMax int, atkSize, defSize string) int {
294	dmg := bite * 100 / (hpMax + 100)
295	if dmg < 1 {
296		dmg = 1
297	}
298	if sizeIndex(atkSize)-sizeIndex(defSize) >= 2 {
299		dmg = dmg * 5000 / 10000
300		if dmg < 1 {
301			dmg = 1
302		}
303	}
304	return dmg
305}
306
307func sizeIndex(size string) int {
308	switch size {
309	case "small":
310		return 0
311	case "medium":
312		return 1
313	case "large":
314		return 2
315	default:
316		panic("fish: size")
317	}
318}
319
320func biteOf(rarity, size, trait string) int {
321	var base int64
322	switch rarity {
323	case "common":
324		base = 22
325	case "uncommon":
326		base = 26
327	case "rare":
328		base = 31
329	case "epic":
330		base = 37
331	case "legendary":
332		base = 44
333	case "mythic":
334		base = 52
335	default:
336		panic("fish: rarity")
337	}
338	traitBps := int64(10000)
339	if trait == "fang" {
340		traitBps = 11200
341	}
342	sizeBps := int64(10000)
343	if size == "small" {
344		sizeBps = 8600
345	} else if size == "large" {
346		sizeBps = 11800
347	}
348	v := (base*traitBps*sizeBps + 50_000_000) / 100_000_000
349	if v < 1 {
350		v = 1
351	}
352	return int(v)
353}
354
355func cooldownMs(rarity string) int64 {
356	switch rarity {
357	case "common":
358		return 2400
359	case "uncommon":
360		return 2550
361	case "rare":
362		return 2700
363	case "epic":
364		return 2850
365	case "legendary":
366		return 3000
367	case "mythic":
368		return 3200
369	default:
370		panic("fish: rarity")
371	}
372}
373
374// reachUnits is the dodged bite reach in 1/10000 of a world unit.
375// A sector is 8 world units, so the next center is 80000 units away.
376func reachUnits(rarity, size string, dodgeTenths int) int64 {
377	var spec int64
378	switch rarity {
379	case "common":
380		spec = 10000
381	case "uncommon":
382		spec = 10600
383	case "rare":
384		spec = 11200
385	case "epic":
386		spec = 11800
387	case "legendary":
388		spec = 12400
389	case "mythic":
390		spec = 13200
391	default:
392		panic("fish: rarity")
393	}
394	sizeMod := int64(10000)
395	if size == "small" {
396		sizeMod = 8600
397	} else if size == "large" {
398		sizeMod = 12200
399	}
400	reach := spec * sizeMod * 8 / 10000
401	reach -= int64(dodgeTenths) * 1000
402	if reach < 10_000 {
403		return 10_000
404	}
405	return reach
406}
407
408func inReach(atk, def *fishBody) bool {
409	return inReachDodge(atk, def, def.dodge)
410}
411
412func inReachDodge(atk, def *fishBody, dodge int) bool {
413	dx := int64(atk.sx - def.sx)
414	dz := int64(atk.sz - def.sz)
415	span := dx*dx + dz*dz
416	reach := reachUnits(atk.rarity, atk.size, dodge)
417	dist2 := span * 80_000 * 80_000
418	return dist2 <= reach*reach
419}