1/*
2 * LegacyClonk
3 *
4 * Copyright (c) 1998-2000, Matthes Bender (RedWolf Design)
5 * Copyright (c) 2017-2021, The LegacyClonk Team and contributors
6 *
7 * Distributed under the terms of the ISC license; see accompanying file
8 * "COPYING" for details.
9 *
10 * "Clonk" is a registered trademark of Matthes Bender, used with permission.
11 * See accompanying file "TRADEMARK" for details.
12 *
13 * To redistribute this file separately, substitute the full license texts
14 * for the above references.
15 */
16
17/* Lots of handler functions for object action */
18
19#include <C4Include.h>
20#include <C4ObjectCom.h>
21
22#include <C4Object.h>
23#include <C4Physics.h>
24#include <C4Command.h>
25#include <C4Random.h>
26#include <C4Game.h>
27#include <C4Wrappers.h>
28#include <C4ObjectMenu.h>
29#include <C4Player.h>
30#include <C4SoundSystem.h>
31
32bool SimFlightHitsLiquid(C4Fixed fcx, C4Fixed fcy, C4Fixed xdir, C4Fixed ydir);
33
34bool ObjectActionWalk(C4Object *cObj)
35{
36 if (!cObj->SetActionByName(szActName: "Walk")) return false;
37 cObj->xdir = cObj->ydir = 0;
38 return true;
39}
40
41bool ObjectActionStand(C4Object *cObj)
42{
43 cObj->Action.ComDir = COMD_Stop;
44 if (!ObjectActionWalk(cObj)) return false;
45 return true;
46}
47
48bool ObjectActionJump(C4Object *cObj, C4Fixed xdir, C4Fixed ydir, bool fByCom)
49{
50 // scripted jump?
51 assert(cObj);
52 if (cObj->Call(PSF_OnActionJump, pPars: {C4VInt(iVal: fixtoi(x: xdir, prec: 100)), C4VInt(iVal: fixtoi(x: ydir, prec: 100)), C4VBool(fVal: fByCom)})) return true;
53 // hardcoded jump by action
54 if (!cObj->SetActionByName(szActName: "Jump")) return false;
55 cObj->xdir = xdir; cObj->ydir = ydir;
56 cObj->Mobile = 1;
57 // unstick from ground, because jump command may be issued in an Action-callback,
58 // where attach-values have already been determined for that frame
59 cObj->Action.t_attach &= ~CNAT_Bottom;
60 return true;
61}
62
63bool ObjectActionDive(C4Object *cObj, C4Fixed xdir, C4Fixed ydir)
64{
65 if (!cObj->SetActionByName(szActName: "Dive")) return false;
66 cObj->xdir = xdir; cObj->ydir = ydir;
67 cObj->Mobile = 1;
68 // unstick from ground, because jump command may be issued in an Action-callback,
69 // where attach-values have already been determined for that frame
70 cObj->Action.t_attach &= ~CNAT_Bottom;
71 return true;
72}
73
74bool ObjectActionTumble(C4Object *cObj, int32_t dir, C4Fixed xdir, C4Fixed ydir)
75{
76 if (!cObj->SetActionByName(szActName: "Tumble")) return false;
77 cObj->SetDir(dir);
78 cObj->xdir = xdir; cObj->ydir = ydir;
79 return true;
80}
81
82bool ObjectActionGetPunched(C4Object *cObj, C4Fixed xdir, C4Fixed ydir)
83{
84 if (!cObj->SetActionByName(szActName: "GetPunched")) return false;
85 cObj->xdir = xdir; cObj->ydir = ydir;
86 return true;
87}
88
89bool ObjectActionKneel(C4Object *cObj)
90{
91 if (!cObj->SetActionByName(szActName: "KneelDown")) return false;
92 cObj->xdir = cObj->ydir = 0;
93 return true;
94}
95
96bool ObjectActionFlat(C4Object *cObj, int32_t dir)
97{
98 if (!cObj->SetActionByName(szActName: "FlatUp")) return false;
99 cObj->xdir = cObj->ydir = 0;
100 cObj->SetDir(dir);
101 return true;
102}
103
104bool ObjectActionScale(C4Object *cObj, int32_t dir)
105{
106 if (!cObj->SetActionByName(szActName: "Scale")) return false;
107 cObj->xdir = cObj->ydir = 0;
108 cObj->SetDir(dir);
109 return true;
110}
111
112bool ObjectActionHangle(C4Object *cObj, int32_t dir)
113{
114 if (!cObj->SetActionByName(szActName: "Hangle")) return false;
115 cObj->xdir = cObj->ydir = 0;
116 cObj->SetDir(dir);
117 return true;
118}
119
120bool ObjectActionThrow(C4Object *cObj, C4Object *pThing)
121{
122 // No object specified, first from contents
123 if (!pThing) pThing = cObj->Contents.GetObject();
124 // Nothing to throw
125 if (!pThing) return false;
126 // Force and direction
127 C4Fixed pthrow = ValByPhysical(iPercent: 400, iPhysical: cObj->GetPhysical()->Throw);
128 int32_t iDir = 1; if (cObj->Action.Dir == DIR_Left) iDir = -1;
129 // Set action
130 if (!cObj->SetActionByName(szActName: "Throw")) return false;
131 // Exit object
132 pThing->Exit(iX: cObj->x,
133 iY: cObj->y + cObj->Shape.y - 1,
134 iR: Random(iRange: 360),
135 iXDir: pthrow * iDir, iYDir: -pthrow, iRDir: pthrow * iDir);
136 // Success
137 return true;
138}
139
140bool ObjectActionDig(C4Object *cObj)
141{
142 if (!cObj->SetActionByName(szActName: "Dig")) return false;
143 cObj->Action.Data = 0; // Material Dig2Object request
144 return true;
145}
146
147bool ObjectActionBuild(C4Object *cObj, C4Object *target)
148{
149 return cObj->SetActionByName(szActName: "Build", pTarget: target);
150}
151
152bool ObjectActionPush(C4Object *cObj, C4Object *target)
153{
154 return cObj->SetActionByName(szActName: "Push", pTarget: target);
155}
156
157bool ObjectActionFight(C4Object *cObj, C4Object *target)
158{
159 return cObj->SetActionByName(szActName: "Fight", pTarget: target);
160}
161
162bool ObjectActionChop(C4Object *cObj, C4Object *target)
163{
164 return cObj->SetActionByName(szActName: "Chop", pTarget: target);
165}
166
167bool CornerScaleOkay(C4Object *cObj, int32_t iRangeX, int32_t iRangeY)
168{
169 int32_t ctx, cty;
170 cty = cObj->y - iRangeY;
171 if (cObj->Action.Dir == DIR_Left) ctx = cObj->x - iRangeX;
172 else ctx = cObj->x + iRangeX;
173 cObj->ContactCheck(atx: ctx, aty: cty); // (resets VtxContact & t_contact)
174 if (!(cObj->t_contact & CNAT_Top))
175 if (!(cObj->t_contact & CNAT_Left))
176 if (!(cObj->t_contact & CNAT_Right))
177 if (!(cObj->t_contact & CNAT_Bottom))
178 return true;
179 return false;
180}
181
182bool CheckCornerScale(C4Object *cObj, int32_t &iRangeX, int32_t &iRangeY)
183{
184 for (iRangeX = CornerRange; iRangeX >= 1; iRangeX--)
185 for (iRangeY = CornerRange; iRangeY >= 1; iRangeY--)
186 if (CornerScaleOkay(cObj, iRangeX, iRangeY))
187 return true;
188 return false;
189}
190
191bool ObjectActionCornerScale(C4Object *cObj)
192{
193 int32_t iRangeX, iRangeY;
194 // Scaling: check range max to min
195 if (cObj->GetProcedure() == DFA_SCALE)
196 {
197 if (!CheckCornerScale(cObj, iRangeX, iRangeY)) return false;
198 }
199 // Swimming: check range min to max
200 else
201 {
202 iRangeY = 2;
203 while (!CornerScaleOkay(cObj, iRangeX: iRangeY, iRangeY))
204 {
205 iRangeY++; if (iRangeY > CornerRange) return false;
206 }
207 iRangeX = iRangeY;
208 }
209 // Do corner scale
210 if (!cObj->SetActionByName(szActName: "KneelUp"))
211 cObj->SetActionByName(szActName: "Walk");
212 cObj->xdir = cObj->ydir = 0;
213 if (cObj->Action.Dir == DIR_Left) cObj->fix_x -= itofix(x: iRangeX);
214 else cObj->fix_x += itofix(x: iRangeX);
215 cObj->fix_y -= itofix(x: iRangeY);
216 cObj->x = fixtoi(x: cObj->fix_x); cObj->y = fixtoi(x: cObj->fix_y);
217 return true;
218}
219
220bool ObjectComMovement(C4Object *cObj, int32_t comdir)
221{
222 cObj->Action.ComDir = comdir;
223
224 PlayerObjectCommand(plr: cObj->Owner, cmdf: C4CMD_Follow, pTarget: cObj);
225 // direkt turnaround if standing still
226 if (!cObj->xdir && (cObj->GetProcedure() == DFA_WALK || cObj->GetProcedure() == DFA_HANGLE))
227 switch (comdir)
228 {
229 case COMD_Left: case COMD_UpLeft: case COMD_DownLeft:
230 cObj->SetDir(DIR_Left);
231 break;
232 case COMD_Right: case COMD_UpRight: case COMD_DownRight:
233 cObj->SetDir(DIR_Right);
234 break;
235 }
236 return true;
237}
238
239bool ObjectComStop(C4Object *cObj)
240{
241 // Cease current action
242 cObj->SetActionByName(szActName: "Idle");
243 // Action walk if possible
244 return ObjectActionStand(cObj);
245}
246
247bool ObjectComGrab(C4Object *cObj, C4Object *pTarget)
248{
249 if (!pTarget) return false;
250 if (cObj->GetProcedure() != DFA_WALK) return false;
251 if (!ObjectActionPush(cObj, target: pTarget)) return false;
252 cObj->Call(PSF_Grab, pPars: {C4VObj(pObj: pTarget), C4VBool(fVal: true)});
253 if (pTarget->Status && cObj->Status)
254 {
255 pTarget->Controller = cObj->Controller;
256 pTarget->Call(PSF_Grabbed, pPars: {C4VObj(pObj: cObj), C4VBool(fVal: true)});
257 }
258 return true;
259}
260
261bool ObjectComUnGrab(C4Object *cObj)
262{
263 // Only if pushing, -> stand
264 if (cObj->GetProcedure() == DFA_PUSH)
265 {
266 C4Object *pTarget = cObj->Action.Target;
267 if (ObjectActionStand(cObj))
268 {
269 if (!cObj->CloseMenu(fForce: false)) return false;
270 cObj->Call(PSF_Grab, pPars: {C4VObj(pObj: pTarget), C4VBool(fVal: false)});
271 if (pTarget && pTarget->Status && cObj->Status)
272 pTarget->Call(PSF_Grabbed, pPars: {C4VObj(pObj: cObj), C4VBool(fVal: false)});
273 return true;
274 }
275 }
276
277 return false;
278}
279
280bool ObjectComJump(C4Object *cObj) // by ObjectComUp, ExecCMDFMoveTo, FnJump
281{
282 // Only if walking
283 if (cObj->GetProcedure() != DFA_WALK) return false;
284 // Calculate direction & forces
285 C4Fixed TXDir = Fix0;
286 C4PhysicalInfo *pPhysical = cObj->GetPhysical();
287 C4Fixed iPhysicalWalk = ValByPhysical(iPercent: 280, iPhysical: pPhysical->Walk) * itofix(x: cObj->GetCon(), prec: FullCon);
288 C4Fixed iPhysicalJump = ValByPhysical(iPercent: 1000, iPhysical: pPhysical->Jump) * itofix(x: cObj->GetCon(), prec: FullCon);
289
290 if (cObj->Action.ComDir == COMD_Left || cObj->Action.ComDir == COMD_UpLeft) TXDir = -iPhysicalWalk;
291 else if (cObj->Action.ComDir == COMD_Right || cObj->Action.ComDir == COMD_UpRight) TXDir = +iPhysicalWalk;
292 else
293 {
294 if (cObj->Action.Dir == DIR_Left) TXDir = -iPhysicalWalk;
295 if (cObj->Action.Dir == DIR_Right) TXDir = +iPhysicalWalk;
296 }
297 C4Fixed x = cObj->fix_x, y = cObj->fix_y;
298 // find bottom-most vertex, correct starting position for simulation
299 int32_t iBtmVtx = cObj->Shape.GetBottomVertex();
300 if (iBtmVtx != -1) { x += cObj->Shape.GetVertexX(iVertex: iBtmVtx); y += cObj->Shape.GetVertexY(iVertex: iBtmVtx); }
301 // Try dive
302 if (cObj->Shape.ContactDensity > C4M_Liquid)
303 if (SimFlightHitsLiquid(fcx: x, fcy: y, xdir: TXDir, ydir: -iPhysicalJump))
304 if (ObjectActionDive(cObj, xdir: TXDir, ydir: -iPhysicalJump))
305 return true;
306 // Regular jump
307 return ObjectActionJump(cObj, xdir: TXDir, ydir: -iPhysicalJump, fByCom: true);
308}
309
310bool ObjectComLetGo(C4Object *cObj, int32_t xdirf)
311{
312 // by ACTSCALE, ACTHANGLE or ExecCMDFMoveTo
313 return ObjectActionJump(cObj, xdir: itofix(x: xdirf), ydir: Fix0, fByCom: true);
314}
315
316bool ObjectComEnter(C4Object *cObj) // by pusher
317{
318 if (!cObj) return false;
319
320 // NoPushEnter
321 if (cObj->Def->NoPushEnter) return false;
322
323 // Check object entrance, try command enter
324 C4Object *pTarget;
325 uint32_t ocf = OCF_Entrance;
326 if ((pTarget = Game.Objects.AtObject(ctx: cObj->x, cty: cObj->y, ocf, exclude: cObj)))
327 if (ocf & OCF_Entrance)
328 {
329 cObj->SetCommand(iCommand: C4CMD_Enter, pTarget); return true;
330 }
331
332 return false;
333}
334
335bool ObjectComUp(C4Object *cObj) // by DFA_WALK or DFA_SWIM
336{
337 if (!cObj) return false;
338
339 // Check object entrance, try command enter
340 C4Object *pTarget;
341 uint32_t ocf = OCF_Entrance;
342 if ((pTarget = Game.Objects.AtObject(ctx: cObj->x, cty: cObj->y, ocf, exclude: cObj)))
343 if (ocf & OCF_Entrance)
344 return PlayerObjectCommand(plr: cObj->Owner, cmdf: C4CMD_Enter, pTarget);
345
346 // Try jump
347 if (cObj->GetProcedure() == DFA_WALK)
348 return PlayerObjectCommand(plr: cObj->Owner, cmdf: C4CMD_Jump);
349
350 return false;
351}
352
353bool ObjectComDig(C4Object *cObj) // by DFA_WALK
354{
355 C4PhysicalInfo *phys = cObj->GetPhysical();
356 if (!phys->CanDig || !ObjectActionDig(cObj))
357 {
358 GameMsgObject(szText: LoadResStr(id: C4ResStrTableKey::IDS_OBJ_NODIG, args: cObj->GetName()).c_str(), pTarget: cObj);
359 return false;
360 }
361 return true;
362}
363
364C4Object *CreateLine(C4ID idType, int32_t iOwner, C4Object *pFrom, C4Object *pTo)
365{
366 C4Object *pLine;
367 if (!pFrom || !pTo) return nullptr;
368 if (!(pLine = Game.CreateObject(type: idType, pCreator: pFrom, owner: iOwner, x: 0, y: 0))) return nullptr;
369 pLine->Shape.VtxNum = 2;
370 pLine->Shape.VtxX[0] = pFrom->x;
371 pLine->Shape.VtxY[0] = pFrom->y + pFrom->Shape.Hgt / 4;
372 pLine->Shape.VtxX[1] = pTo->x;
373 pLine->Shape.VtxY[1] = pTo->y + pTo->Shape.Hgt / 4;
374 pLine->Action.Target = pFrom;
375 pLine->Action.Target2 = pTo;
376 return pLine;
377}
378
379bool ObjectComLineConstruction(C4Object *cObj)
380{
381 C4Object *linekit, *tstruct, *cline;
382 uint32_t ocf;
383
384 ObjectActionStand(cObj);
385
386 // Check physical
387 if (!cObj->GetPhysical()->CanConstruct)
388 {
389 GameMsgObject(szText: LoadResStr(id: C4ResStrTableKey::IDS_OBJ_NOLINECONSTRUCT, args: cObj->GetName()).c_str(), pTarget: cObj); return false;
390 }
391
392 // Line pickup
393
394 // Check for linekit
395 if (!(linekit = cObj->Contents.Find(id: C4ID_Linekit)))
396 {
397 // Check for collection limit
398 if (cObj->Def->CollectionLimit && (cObj->Contents.ObjectCount() >= cObj->Def->CollectionLimit)) return false;
399 // Check line pickup
400 ocf = OCF_LineConstruct;
401 tstruct = Game.Objects.AtObject(ctx: cObj->x, cty: cObj->y, ocf, exclude: cObj);
402 if (!tstruct || !(ocf & OCF_LineConstruct)) return false;
403 if (!(cline = Game.FindObject(id: C4ID_None, iX: 0, iY: 0, iWdt: 0, iHgt: 0, ocf: OCF_All, szAction: "Connect", pActionTarget: tstruct))) return false;
404 // Check line connected to linekit at other end
405 if ((cline->Action.Target && (cline->Action.Target->Def->id == C4ID_Linekit))
406 || (cline->Action.Target2 && (cline->Action.Target2->Def->id == C4ID_Linekit)))
407 {
408 StartSoundEffect(name: "Error", loop: false, volume: 100, obj: cObj);
409 GameMsgObject(szText: LoadResStr(id: C4ResStrTableKey::IDS_OBJ_NODOUBLEKIT, args: cline->GetName()).c_str(), pTarget: cObj); return false;
410 }
411 // Create new linekit
412 if (!(linekit = Game.CreateObject(type: C4ID_Linekit, pCreator: cObj, owner: cline->Owner))) return false;
413 // Enter linekit into clonk
414 bool fRejectCollect;
415 if (!linekit->Enter(pTarget: cObj, fCalls: true, fCopyMotion: true, pfRejectCollect: &fRejectCollect))
416 {
417 // Enter failed: abort operation
418 linekit->AssignRemoval(); return false;
419 }
420 // Attach line to collected linekit
421 StartSoundEffect(name: "Connect", loop: false, volume: 100, obj: cObj);
422 if (cline->Action.Target == tstruct) cline->Action.Target = linekit;
423 if (cline->Action.Target2 == tstruct) cline->Action.Target2 = linekit;
424 // Message
425 GameMsgObject(szText: LoadResStr(id: C4ResStrTableKey::IDS_OBJ_DISCONNECT, args: cline->GetName(), args: tstruct->GetName()).c_str(), pTarget: tstruct);
426 return true;
427 }
428
429 // Active construction
430
431 // Active line construction
432 if (cline = Game.FindObject(id: C4ID_None, iX: 0, iY: 0, iWdt: 0, iHgt: 0, ocf: OCF_All, szAction: "Connect", pActionTarget: linekit))
433 {
434 // Check for structure connection
435 ocf = OCF_LineConstruct;
436 tstruct = Game.Objects.AtObject(ctx: cObj->x, cty: cObj->y, ocf, exclude: cObj);
437 // No structure
438 if (!tstruct || !(ocf & OCF_LineConstruct))
439 {
440 // No connect
441 StartSoundEffect(name: "Error", loop: false, volume: 100, obj: cObj);
442 GameMsgObject(szText: LoadResStr(id: C4ResStrTableKey::IDS_OBJ_NOCONNECT), pTarget: cObj); return false;
443 }
444
445 // Check short circuit -> removal
446 if ((cline->Action.Target == tstruct)
447 || (cline->Action.Target2 == tstruct))
448 {
449 StartSoundEffect(name: "Connect", loop: false, volume: 100, obj: cObj);
450 GameMsgObject(szText: LoadResStr(id: C4ResStrTableKey::IDS_OBJ_LINEREMOVAL, args: cline->GetName()).c_str(), pTarget: tstruct);
451 cline->AssignRemoval();
452 return true;
453 }
454
455 // Check for correct connection type
456 bool connect_okay = false;
457 switch (cline->Def->Line)
458 {
459 case C4D_Line_Power:
460 if (tstruct->Def->LineConnect & C4D_Power_Input) connect_okay = true;
461 if (tstruct->Def->LineConnect & C4D_Power_Output) connect_okay = true;
462 break;
463 case C4D_Line_Source:
464 if (tstruct->Def->LineConnect & C4D_Liquid_Output) connect_okay = true; break;
465 case C4D_Line_Drain:
466 if (tstruct->Def->LineConnect & C4D_Liquid_Input) connect_okay = true; break;
467 default: return false; // Undefined line type
468 }
469 if (!connect_okay)
470 {
471 StartSoundEffect(name: "Error", loop: false, volume: 100, obj: cObj);
472 GameMsgObject(szText: LoadResStr(id: C4ResStrTableKey::IDS_OBJ_NOCONNECTTYPE, args: cline->GetName(), args: tstruct->GetName()).c_str(), pTarget: tstruct); return false;
473 }
474
475 // Connect line to structure
476 StartSoundEffect(name: "Connect", loop: false, volume: 100, obj: cObj);
477 if (cline->Action.Target == linekit) cline->Action.Target = tstruct;
478 if (cline->Action.Target2 == linekit) cline->Action.Target2 = tstruct;
479 linekit->Exit();
480 linekit->AssignRemoval();
481
482 GameMsgObject(szText: LoadResStr(id: C4ResStrTableKey::IDS_OBJ_CONNECT, args: cline->GetName(), args: tstruct->GetName()).c_str(), pTarget: tstruct);
483
484 return true;
485 }
486
487 // New line
488
489 // Check for new structure connection
490 ocf = OCF_LineConstruct;
491 tstruct = Game.Objects.AtObject(ctx: cObj->x, cty: cObj->y, ocf, exclude: cObj);
492 if (!tstruct || !(ocf & OCF_LineConstruct))
493 {
494 StartSoundEffect(name: "Error", loop: false, volume: 100, obj: cObj);
495 GameMsgObject(szText: LoadResStr(id: C4ResStrTableKey::IDS_OBJ_NONEWLINE), pTarget: cObj); return false;
496 }
497
498 // Determine new line type
499 C4ID linetype = C4ID_None;
500 // Check source pipe
501 if (linetype == C4ID_None)
502 if (tstruct->Def->LineConnect & C4D_Liquid_Pump)
503 if (!Game.FindObject(id: C4ID_SourcePipe, iX: 0, iY: 0, iWdt: 0, iHgt: 0, ocf: OCF_All, szAction: "Connect", pActionTarget: tstruct))
504 linetype = C4ID_SourcePipe;
505 // Check drain pipe
506 if (linetype == C4ID_None)
507 if (tstruct->Def->LineConnect & C4D_Liquid_Output)
508 if (!Game.FindObject(id: C4ID_DrainPipe, iX: 0, iY: 0, iWdt: 0, iHgt: 0, ocf: OCF_All, szAction: "Connect", pActionTarget: tstruct))
509 linetype = C4ID_DrainPipe;
510 // Check power
511 if (linetype == C4ID_None)
512 if (tstruct->Def->LineConnect & C4D_Power_Output)
513 linetype = C4ID_PowerLine;
514 // No good
515 if (linetype == C4ID_None)
516 {
517 StartSoundEffect(name: "Error", loop: false, volume: 100, obj: cObj);
518 GameMsgObject(szText: LoadResStr(id: C4ResStrTableKey::IDS_OBJ_NONEWLINE), pTarget: cObj); return false;
519 }
520
521 // Create new line
522 C4Object *newline = CreateLine(idType: linetype, iOwner: cObj->Owner,
523 pFrom: tstruct, pTo: linekit);
524 if (!newline) return false;
525 StartSoundEffect(name: "Connect", loop: false, volume: 100, obj: cObj);
526 GameMsgObject(szText: LoadResStr(id: C4ResStrTableKey::IDS_OBJ_NEWLINE, args: newline->GetName()).c_str(), pTarget: tstruct);
527
528 return true;
529}
530
531void ObjectComDigDouble(C4Object *cObj) // "Activation" by DFA_WALK, DFA_DIG, DFA_SWIM
532{
533 C4Object *pTarget;
534 uint32_t ocf;
535 C4PhysicalInfo *phys = cObj->GetPhysical();
536
537 // Contents activation (first contents object only)
538 if (cObj->Contents.GetObject())
539 if (cObj->Contents.GetObject()->Call(PSF_Activate, pPars: {C4VObj(pObj: cObj)}))
540 return;
541
542 // Linekit: Line construction (move to linekit script...)
543 if (cObj->Contents.GetObject() && (cObj->Contents.GetObject()->id == C4ID_Linekit))
544 {
545 ObjectComLineConstruction(cObj);
546 return;
547 }
548
549 // Chop
550 ocf = OCF_Chop;
551 if (phys->CanChop)
552 if (cObj->GetProcedure() != DFA_SWIM)
553 if ((pTarget = Game.Objects.AtObject(ctx: cObj->x, cty: cObj->y, ocf, exclude: cObj)))
554 if (ocf & OCF_Chop)
555 {
556 PlayerObjectCommand(plr: cObj->Owner, cmdf: C4CMD_Chop, pTarget);
557 return;
558 }
559
560 // Line construction pick up
561 ocf = OCF_LineConstruct;
562 if (phys->CanConstruct)
563 if (!cObj->Contents.GetObject())
564 if ((pTarget = Game.Objects.AtObject(ctx: cObj->x, cty: cObj->y, ocf, exclude: cObj)))
565 if (ocf & OCF_LineConstruct)
566 if (ObjectComLineConstruction(cObj))
567 return;
568
569 // Own activation call
570 if (cObj->Call(PSF_Activate, pPars: {C4VObj(pObj: cObj)})) return;
571}
572
573bool ObjectComDownDouble(C4Object *cObj) // by DFA_WALK
574{
575 C4Object *pTarget;
576 uint32_t ocf = OCF_Construct | OCF_Grab;
577 if ((pTarget = Game.Objects.AtObject(ctx: cObj->x, cty: cObj->y, ocf, exclude: cObj)))
578 {
579 if (ocf & OCF_Construct)
580 {
581 PlayerObjectCommand(plr: cObj->Owner, cmdf: C4CMD_Build, pTarget); return true;
582 }
583 if (ocf & OCF_Grab)
584 {
585 PlayerObjectCommand(plr: cObj->Owner, cmdf: C4CMD_Grab, pTarget); return true;
586 }
587 }
588 return false;
589}
590
591bool ObjectComPut(C4Object *cObj, C4Object *pTarget, C4Object *pThing)
592{
593 // No object specified, first from contents
594 if (!pThing) pThing = cObj->Contents.GetObject();
595 // Nothing to put
596 if (!pThing) return false;
597 // No target
598 if (!pTarget) return false;
599 // Grabbing: check C4D_Grab_Put
600 if (pTarget != cObj->Contained)
601 if (!(pTarget->Def->GrabPutGet & C4D_Grab_Put))
602 {
603 // Was meant to be a drop anyway
604 if (ValidPlr(plr: cObj->Owner))
605 if (Game.Players.Get(iPlayer: cObj->Owner)->LastComDownDouble)
606 return ObjectComDrop(cObj, pThing);
607 // No grab put: fail
608 return false;
609 }
610 // Target no fullcon
611 if (!(pTarget->OCF & OCF_FullCon)) return false;
612 // Check target collection limit
613 if (pTarget->Def->CollectionLimit && (pTarget->Contents.ObjectCount() >= pTarget->Def->CollectionLimit)) return false;
614 // Transfer thing
615 bool fRejectCollect;
616 if (!pThing->Enter(pTarget, fCalls: true, fCopyMotion: true, pfRejectCollect: &fRejectCollect)) return false;
617 // Put call to object script
618 cObj->Call(PSF_Put);
619 // Target collection call
620 pTarget->Call(PSF_Collection, pPars: {C4VObj(pObj: pThing), C4VBool(fVal: true)});
621 // Success
622 return true;
623}
624
625bool ObjectComThrow(C4Object *cObj, C4Object *pThing)
626{
627 // No object specified, first from contents
628 if (!pThing) pThing = cObj->Contents.GetObject();
629 // Nothing to throw
630 if (!pThing) return false;
631 // Throw com
632 switch (cObj->GetProcedure())
633 {
634 case DFA_WALK: return ObjectActionThrow(cObj, pThing);
635 }
636 // Failure
637 return false;
638}
639
640bool ObjectComDrop(C4Object *cObj, C4Object *pThing)
641{
642 // No object specified, first from contents
643 if (!pThing) pThing = cObj->Contents.GetObject();
644 // Nothing to throw
645 if (!pThing) return false;
646 // Force and direction
647 // When dropping diagonally, drop from edge of shape
648 // When doing a diagonal forward drop during flight, exit a bit closer to the Clonk to allow planned tumbling
649 // Except when hangling, so you can mine effectively form the ceiling, and when swimming because you cannot tumble then
650 C4Fixed pthrow = ValByPhysical(iPercent: 400, iPhysical: cObj->GetPhysical()->Throw);
651 int32_t tdir = 0; int right = 0;
652 bool isHanglingOrSwimming = false;
653 int32_t iProc = DFA_NONE;
654 if (cObj->Action.Act >= 0)
655 {
656 iProc = cObj->Def->ActMap[cObj->Action.Act].Procedure;
657 if (iProc == DFA_HANGLE || iProc == DFA_SWIM) isHanglingOrSwimming = true;
658 }
659 int32_t iOutposReduction = 1; // don't exit object too far forward during jump
660 if (iProc != DFA_SCALE) // never diagonal during scaling (can have com into wall during scaling!)
661 {
662 if (ComDirLike(iComDir: cObj->Action.ComDir, COMD_Left)) { tdir = -1; right = 0; if (cObj->xdir < FIXED10(x: 15) && !isHanglingOrSwimming) --iOutposReduction; }
663 if (ComDirLike(iComDir: cObj->Action.ComDir, COMD_Right)) { tdir = +1; right = 1; if (cObj->xdir > FIXED10(x: -15) && !isHanglingOrSwimming) --iOutposReduction; }
664 }
665 // Exit object
666 pThing->Exit(iX: cObj->x + (cObj->Shape.x + cObj->Shape.Wdt * right) * !!tdir * iOutposReduction,
667 iY: cObj->y + cObj->Shape.y + cObj->Shape.Hgt - (pThing->Shape.y + pThing->Shape.Hgt), iR: 0, iXDir: pthrow * tdir, iYDir: Fix0, iRDir: Fix0);
668 // NoCollectDelay
669 cObj->NoCollectDelay = 2;
670 // Update OCF
671 cObj->SetOCF();
672 // Ungrab
673 ObjectComUnGrab(cObj);
674 // Done
675 return true;
676}
677
678bool ObjectComChop(C4Object *cObj, C4Object *pTarget)
679{
680 if (!pTarget) return false;
681 if (!cObj->GetPhysical()->CanChop)
682 {
683 GameMsgObject(szText: LoadResStr(id: C4ResStrTableKey::IDS_OBJ_NOCHOP, args: cObj->GetName()).c_str(), pTarget: cObj);
684 return false;
685 }
686 if (cObj->GetProcedure() != DFA_WALK) return false;
687 return ObjectActionChop(cObj, target: pTarget);
688}
689
690bool ObjectComBuild(C4Object *cObj, C4Object *pTarget)
691{
692 if (!pTarget) return false;
693 // Needs to be idle or walking
694 if (cObj->Action.Act != ActIdle)
695 if (cObj->GetProcedure() != DFA_WALK)
696 return false;
697 return ObjectActionBuild(cObj, target: pTarget);
698}
699
700bool ObjectComPutTake(C4Object *cObj, C4Object *pTarget, C4Object *pThing) // by C4CMD_Throw
701{
702 // by C4CMD_Drop
703 // Valid checks
704 if (!pTarget) return false;
705 // No object specified, first from contents
706 if (!pThing) pThing = cObj->Contents.GetObject();
707 // Has thing, put to target
708 if (pThing)
709 return ObjectComPut(cObj, pTarget, pThing);
710 // If target is own container, activate activation menu
711 if (pTarget == cObj->Contained)
712 return ObjectComTake(cObj); // carlo
713 // Assuming target is grabbed, check for grab get
714 if (pTarget->Def->GrabPutGet & C4D_Grab_Get)
715 {
716 // Activate get menu
717 return cObj->ActivateMenu(iMenu: C4MN_Get, iMenuSelect: 0, iMenuData: 0, iMenuPosition: 0, pTarget);
718 }
719 // Failure
720 return false;
721}
722
723// carlo
724bool ObjectComTake(C4Object *cObj) // by C4CMD_Take
725{
726 return cObj->ActivateMenu(iMenu: C4MN_Activate);
727}
728
729// carlo
730bool ObjectComTake2(C4Object *cObj) // by C4CMD_Take2
731{
732 return cObj->ActivateMenu(iMenu: C4MN_Get, iMenuSelect: 0, iMenuData: 0, iMenuPosition: 0, pTarget: cObj->Contained);
733}
734
735bool ObjectComPunch(C4Object *cObj, C4Object *pTarget, int32_t punch)
736{
737 if (!cObj || !pTarget) return false;
738 if (!punch)
739 if (pTarget->GetPhysical()->Fight)
740 punch = BoundBy<int32_t>(bval: 5 * cObj->GetPhysical()->Fight / pTarget->GetPhysical()->Fight, lbound: 0, rbound: 10);
741 if (!punch) return true;
742 bool fBlowStopped = static_cast<bool>(pTarget->Call(PSF_QueryCatchBlow, pPars: {C4VObj(pObj: cObj)}));
743 if (fBlowStopped && punch > 1) punch = punch / 2; // half damage for caught blow, so shield+armor help in fistfight and vs monsters
744 pTarget->DoEnergy(iChange: -punch, fExact: false, C4FxCall_EngGetPunched, iCausedByPlr: cObj->Controller);
745 int32_t tdir = +1; if (cObj->Action.Dir == DIR_Left) tdir = -1;
746 pTarget->Action.ComDir = COMD_Stop;
747 // No tumbles when blow was caught
748 if (fBlowStopped) return false;
749 // Hard punch
750 if (punch >= 10)
751 if (ObjectActionTumble(cObj: pTarget, dir: pTarget->Action.Dir, xdir: FIXED100(x: 150) * tdir, ydir: itofix(x: -2)))
752 {
753 pTarget->LastEnergyLossCausePlayer = cObj->Controller; // for kill tracing when pushing enemies off a cliff
754 pTarget->Call(PSF_CatchBlow, pPars: {C4VInt(iVal: punch), C4VObj(pObj: cObj)});
755 return true;
756 }
757
758 // Regular punch
759 if (ObjectActionGetPunched(cObj: pTarget, xdir: FIXED100(x: 250) * tdir, ydir: Fix0))
760 {
761 pTarget->LastEnergyLossCausePlayer = cObj->Controller; // for kill tracing when pushing enemies off a cliff
762 pTarget->Call(PSF_CatchBlow, pPars: {C4VInt(iVal: punch), C4VObj(pObj: cObj)});
763 return true;
764 }
765
766 return false;
767}
768
769bool ObjectComCancelAttach(C4Object *cObj)
770{
771 if (cObj->GetProcedure() == DFA_ATTACH)
772 return cObj->SetAction(iAct: ActIdle);
773 return false;
774}
775
776void ObjectComStopDig(C4Object *cObj)
777{
778 // Stand
779 ObjectActionStand(cObj);
780 // Clear digging command
781 if (cObj->Command)
782 if (cObj->Command->Command == C4CMD_Dig)
783 cObj->ClearCommand(pUntil: cObj->Command);
784}
785
786int32_t ComOrder(int32_t iIndex)
787{
788 static uint8_t bComOrder[ComOrderNum] =
789 {
790 COM_Left, COM_Right, COM_Up, COM_Down, COM_Throw, COM_Dig, COM_Special, COM_Special2,
791 COM_Left_S, COM_Right_S, COM_Up_S, COM_Down_S, COM_Throw_S, COM_Dig_S, COM_Special_S, COM_Special2_S,
792 COM_Left_D, COM_Right_D, COM_Up_D, COM_Down_D, COM_Throw_D, COM_Dig_D, COM_Special_D, COM_Special2_D
793 };
794
795 if (Inside<int32_t>(ival: iIndex, lbound: 0, rbound: ComOrderNum - 1)) return bComOrder[iIndex];
796
797 return COM_None;
798}
799
800const char *ComName(int32_t iCom)
801{
802 switch (iCom)
803 {
804 case COM_Up: return "Up";
805 case COM_Up_S: return "UpSingle";
806 case COM_Up_D: return "UpDouble";
807 case COM_Up_R: return "UpReleased";
808 case COM_Down: return "Down";
809 case COM_Down_S: return "DownSingle";
810 case COM_Down_D: return "DownDouble";
811 case COM_Down_R: return "DownReleased";
812 case COM_Left: return "Left";
813 case COM_Left_S: return "LeftSingle";
814 case COM_Left_D: return "LeftDouble";
815 case COM_Left_R: return "LeftReleased";
816 case COM_Right: return "Right";
817 case COM_Right_S: return "RightSingle";
818 case COM_Right_D: return "RightDouble";
819 case COM_Right_R: return "RightReleased";
820 case COM_Dig: return "Dig";
821 case COM_Dig_S: return "DigSingle";
822 case COM_Dig_D: return "DigDouble";
823 case COM_Dig_R: return "DigReleased";
824 case COM_Throw: return "Throw";
825 case COM_Throw_S: return "ThrowSingle";
826 case COM_Throw_D: return "ThrowDouble";
827 case COM_Throw_R: return "ThrowReleased";
828 case COM_Special: return "Special";
829 case COM_Special_S: return "SpecialSingle";
830 case COM_Special_D: return "SpecialDouble";
831 case COM_Special_R: return "SpecialReleased";
832 case COM_Special2: return "Special2";
833 case COM_Special2_S: return "Special2Single";
834 case COM_Special2_D: return "Special2Double";
835 case COM_Special2_R: return "Special2Released";
836 case COM_WheelUp: return "WheelUp";
837 case COM_WheelDown: return "WheelDown";
838 case COM_CursorLeft: return "CursorLeft";
839 case COM_CursorLeft_S: return "CursorLeftSingle";
840 case COM_CursorLeft_D: return "CursorLeftDouble";
841 case COM_CursorLeft_R: return "CursorLeftReleased";
842 case COM_CursorToggle: return "CursorToggle";
843 case COM_CursorToggle_S: return "CursorToggleSingle";
844 case COM_CursorToggle_D: return "CursorToggleDouble";
845 case COM_CursorToggle_R: return "CursorToggleReleased";
846 case COM_CursorRight: return "CursorRight";
847 case COM_CursorRight_S: return "CursorRightSingle";
848 case COM_CursorRight_D: return "CursorRightDouble";
849 case COM_CursorRight_R: return "CursorRightReleased";
850 }
851 return "Undefined";
852}
853
854int32_t Com2Control(int32_t iCom)
855{
856 iCom = iCom & ~(COM_Double | COM_Single);
857 switch (iCom)
858 {
859 case COM_CursorLeft: return CON_CursorLeft;
860 case COM_CursorToggle: return CON_CursorToggle;
861 case COM_CursorRight: return CON_CursorRight;
862 case COM_Throw: return CON_Throw;
863 case COM_Up: return CON_Up;
864 case COM_Dig: return CON_Dig;
865 case COM_Left: return CON_Left;
866 case COM_Down: return CON_Down;
867 case COM_Right: return CON_Right;
868 case COM_Special: return CON_Special;
869 case COM_Special2: return CON_Special2;
870 }
871 return CON_Menu;
872}
873
874int32_t Control2Com(int32_t iControl, bool fUp)
875{
876 static const char con2com[C4MaxKey] =
877 {
878 COM_CursorLeft, COM_CursorToggle, COM_CursorRight,
879 COM_Throw, COM_Up, COM_Dig,
880 COM_Left, COM_Down, COM_Right,
881 COM_PlayerMenu, COM_Special, COM_Special2
882 };
883 static const char con2com_r[C4MaxKey] =
884 {
885 COM_CursorLeft_R, COM_CursorToggle_R, COM_CursorRight_R,
886 COM_Throw_R, COM_Up_R, COM_Dig_R,
887 COM_Left_R, COM_Down_R, COM_Right_R,
888 COM_None, COM_Special_R, COM_Special2_R
889 };
890 if (fUp)
891 {
892 if (Inside<int32_t>(ival: iControl, lbound: 0, rbound: C4MaxKey - 1))
893 return con2com_r[iControl];
894 }
895 else
896 {
897 if (Inside<int32_t>(ival: iControl, lbound: 0, rbound: C4MaxKey - 1))
898 return con2com[iControl];
899 }
900 return COM_None;
901}
902
903int32_t Coms2ComDir(int32_t iComs)
904{
905 // This is possible because COM_Left - COM_Down are < 32
906 static int32_t DirComs = (1 << COM_Left) | (1 << COM_Right) | (1 << COM_Up) | (1 << COM_Down);
907 switch (iComs & DirComs)
908 {
909 case (1 << COM_Up): return COMD_Up;
910 case (1 << COM_Up) | (1 << COM_Right): return COMD_UpRight;
911 case (1 << COM_Right): return COMD_Right;
912 case (1 << COM_Down) | (1 << COM_Right): return COMD_DownRight;
913 case (1 << COM_Down): return COMD_Down;
914 case (1 << COM_Down) | (1 << COM_Left): return COMD_DownLeft;
915 case (1 << COM_Left): return COMD_Left;
916 case (1 << COM_Up) | (1 << COM_Left): return COMD_UpLeft;
917 // up, right and left could be interpreted as COMD_Up etc., but that's too complicated for now
918 default: return COMD_Stop;
919 }
920}
921
922bool ComDirLike(int32_t iComDir, int32_t iSample)
923{
924 if (iComDir == iSample) return true;
925 if (iComDir % 8 + 1 == iSample) return true;
926 if (iComDir == iSample % 8 + 1) return true;
927 return false;
928}
929
930void DrawCommandKey(C4Facet &cgo, int32_t iCom, bool fPressed, const char *szText)
931{
932 // Draw key
933 Game.GraphicsResource.fctKey.Draw(cgo, fAspect: false, iPhaseX: fPressed);
934 // Draw control symbol
935 if (iCom == COM_PlayerMenu)
936 Game.GraphicsResource.fctOKCancel.Draw(cgo, fAspect: true, iPhaseX: 1, iPhaseY: 1);
937 else
938 Game.GraphicsResource.fctCommand.Draw(cgo, fAspect: true, iPhaseX: Com2Control(iCom), iPhaseY: ((iCom & COM_Double) != 0));
939 // Use smaller font on smaller buttons
940 CStdFont &rFont = (cgo.Hgt <= C4MN_SymbolSize) ? Game.GraphicsResource.FontTiny : Game.GraphicsResource.FontRegular;
941 // Draw text
942 if (szText && Config.Graphics.ShowCommandKeys)
943 Application.DDraw->TextOut(szText, rFont, fZoom: 1.0, sfcDest: cgo.Surface, iTx: cgo.X + cgo.Wdt / 2, iTy: cgo.Y + cgo.Hgt - rFont.GetLineHeight() - 2, dwFCol: CStdDDraw::DEFAULT_MESSAGE_COLOR, byForm: ACenter);
944}
945
946void DrawControlKey(C4Facet &cgo, int32_t iControl, bool fPressed, const char *szText)
947{
948 // Draw key
949 Game.GraphicsResource.fctKey.Draw(cgo, fAspect: false, iPhaseX: fPressed);
950 // Draw control symbol
951 Game.GraphicsResource.fctCommand.Draw(cgo, fAspect: true, iPhaseX: iControl);
952 // Use smaller font on smaller buttons
953 CStdFont &rFont = (cgo.Hgt <= C4MN_SymbolSize) ? Game.GraphicsResource.FontRegular : Game.GraphicsResource.FontTiny;
954 // Draw text
955 if (szText)
956 Application.DDraw->TextOut(szText, rFont, fZoom: 1.0, sfcDest: cgo.Surface, iTx: cgo.X + cgo.Wdt / 2, iTy: cgo.Y + cgo.Hgt - rFont.GetLineHeight() - 2, dwFCol: CStdDDraw::DEFAULT_MESSAGE_COLOR, byForm: ACenter);
957}
958
959bool SellFromBase(int32_t iPlr, C4Object *pBaseObj, C4ID id, C4Object *pSellObj)
960{
961 C4Object *pThing;
962 // Valid checks
963 if (!ValidPlr(plr: iPlr)) return false;
964 if (!pBaseObj || !ValidPlr(plr: pBaseObj->Base)) return false;
965 if (~Game.C4S.Game.Realism.BaseFunctionality & BASEFUNC_Sell) return false;
966 // Base owner eliminated
967 if (Game.Players.Get(iPlayer: pBaseObj->Base)->Eliminated)
968 {
969 StartSoundEffect(name: "Error", loop: false, volume: 100, obj: pBaseObj);
970 GameMsgPlayer(szText: LoadResStr(id: C4ResStrTableKey::IDS_PLR_ELIMINATED, args: Game.Players.Get(iPlayer: pBaseObj->Base)->GetName()).c_str(), iPlayer: iPlr); return false;
971 }
972 // Base owner hostile
973 if (Hostile(plr1: iPlr, plr2: pBaseObj->Base))
974 {
975 StartSoundEffect(name: "Error", loop: false, volume: 100, obj: pBaseObj);
976 GameMsgPlayer(szText: LoadResStr(id: C4ResStrTableKey::IDS_PLR_HOSTILE, args: Game.Players.Get(iPlayer: pBaseObj->Base)->GetName()).c_str(), iPlayer: iPlr); return false;
977 }
978 // check validity of sell object, if specified
979 if (pThing = pSellObj)
980 if (!pThing->Status || pThing->Contained != pBaseObj)
981 pThing = nullptr;
982 // Get object from home pBaseObj via selected id, if no or an anvalid thing has been specified
983 if (!pThing)
984 if (!(pThing = pBaseObj->Contents.Find(id))) return false;
985 // check definition NoSell
986 if (pThing->Def->NoSell) return false;
987 // Sell object (pBaseObj owner gets the money)
988 return Game.Players.Get(iPlayer: pBaseObj->Base)->Sell2Home(tobj: pThing);
989}
990
991bool Buy2Base(int32_t iPlr, C4Object *pBase, C4ID id, bool fShowErrors)
992{
993 C4Object *pThing;
994 // Validity
995 if (!ValidPlr(plr: iPlr)) return false;
996 if (!pBase || !ValidPlr(plr: pBase->Base)) return false;
997 if (~Game.C4S.Game.Realism.BaseFunctionality & BASEFUNC_Buy) return false;
998 // Base owner hostile
999 if (Hostile(plr1: iPlr, plr2: pBase->Base))
1000 {
1001 if (!fShowErrors) return false;
1002 StartSoundEffect(name: "Error", loop: false, volume: 100, obj: pBase);
1003 GameMsgPlayer(szText: LoadResStr(id: C4ResStrTableKey::IDS_PLR_HOSTILE, args: Game.Players.Get(iPlayer: pBase->Base)->GetName()).c_str(), iPlayer: iPlr); return false;
1004 }
1005 // buy
1006 if (!(pThing = Game.Players.Get(iPlayer: pBase->Base)->Buy(id, fShowErrors, iForPlr: iPlr, pBuyObj: pBase))) return false;
1007 // Object enter target object
1008 pThing->Enter(pTarget: pBase);
1009 // Success
1010 return true;
1011}
1012
1013bool PlayerObjectCommand(int32_t plr, int32_t cmdf, C4Object *pTarget, int32_t tx, int32_t ty)
1014{
1015 C4Player *pPlr = Game.Players.Get(iPlayer: plr);
1016 if (!pPlr) return false;
1017 int32_t iAddMode = C4P_Command_Set;
1018 // Adjust for old-style keyboard throw/drop control: add & in range
1019 if (cmdf == C4CMD_Throw || cmdf == C4CMD_Drop) iAddMode = C4P_Command_Add | C4P_Command_Range;
1020 if (cmdf == C4CMD_Throw)
1021 {
1022 bool fConvertToDrop = false;
1023 // Drop on down-down-throw (classic)
1024 if (pPlr->LastComDownDouble)
1025 {
1026 fConvertToDrop = true;
1027 // Dropping one object automatically reenables LastComDownDouble to
1028 // allow easy dropping of multiple objects. Also set LastCom for
1029 // script compatibility (custom scripted dropping) and to prevent
1030 // unwanted ThrowDouble for mass dropping
1031 pPlr->LastCom = COM_Down | COM_Double;
1032 pPlr->LastComDownDouble = C4DoubleClick;
1033 }
1034 // Jump'n'Run: Drop on combined Down/Left/Right+Throw
1035 if (pPlr->ControlStyle && (pPlr->PressedComs & (1 << COM_Down))) fConvertToDrop = true;
1036 if (fConvertToDrop) return pPlr->ObjectCommand(iCommand: C4CMD_Drop, pTarget, iTx: tx, iTy: ty, pTarget2: nullptr, iData: 0, iAddMode);
1037 }
1038 // Route to player
1039 return pPlr->ObjectCommand(iCommand: cmdf, pTarget, iTx: tx, iTy: ty, pTarget2: nullptr, iData: 0, iAddMode);
1040}
1041